[HDX-4406] Number tile conditional color rules (ordered thresholds)#2386
Conversation
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
E2E Test Results✅ All tests passed • 197 passed • 3 skipped • 1353s
Tests ran across 4 shards in parallel. |
Per-row Tooltip.Floating instances got stranded in their Portal when a virtual row unmounted before onMouseLeave fired — a race that occurs when the mouse moves rapidly across a TanStack Virtual list. Fix: replace one Tooltip.Floating per virtual row with a single shared Tooltip.Floating wrapping the whole <tbody>. The floating tooltip now lives on <tbody>, which never unmounts, so its Portal-rendered content can never be left open after the triggering element disappears. Row-level onMouseEnter/onMouseLeave handlers update a shared hoveredRowDescription state; the tooltip's disabled prop gates visibility so rows without a resolved URL (error-toast branch) never show a hint. A tbody-level onMouseLeave acts as a safety net to clear the description if a rapid mouse move causes a row to unmount before its own leave handler fires. Test: adds a regression test that verifies the tooltip disappears on mouseLeave (the stranded-tooltip scenario).
Verifies that the Tooltip.Floating hint appears on row hover and disappears when the mouse moves away, covering the stranded-tooltip regression introduced by the per-row Tooltip.Floating in PR #2321. Changes: - dashboard-table-linking.spec.ts: new 'Tooltip hint appears on hover and disappears on mouse-leave' test. Creates a table tile with a Search row-click action, hovers the first row to confirm the hint appears, then moves the mouse away to confirm it hides cleanly. - DashboardPage.ts: adds getFirstTableRow() and hoverFirstTableRowAndGetTooltip() page-object helpers.
P2 — correctness:
- Store hoveredVirtualIndex (row index) instead of hoveredRowDescription
(string) so the label re-derives via useMemo on every render. If the
virtualiser drops or replaces the hovered row (scroll, auto-refetch,
rapid cursor movement) the new row's action is shown immediately;
stale text from the unmounted row can never persist.
- Rows with url:null or empty description now correctly disable the
tooltip regardless of what the prior hover state was.
P2 — testing:
- Replaced the simple mouseLeave regression test with one that exercises
the actual race: hover index 0 (URL row), then enter index 1 (no-URL
row) without firing mouseLeave on index 0. Asserts tooltip hides by
inspecting the Mantine inline display style on the Portal container.
P3 — maintainability:
- label={hoveredRowDescription} — drop the ?? '' fallback that obscured
the disabled gating relationship (Mantine accepts null as ReactNode).
- disabled={!hoveredRowDescription} — guards against empty-string
descriptions that would mount a zero-width floating tooltip.
- Unconditional onMouseEnter/onMouseLeave on each <tr> with a hoisted
clearHovered useCallback, replacing the conditional handler pattern
that forked JSX unnecessarily.
- Collapse dual comment blocks into one rationale at the Tooltip.Floating
call site; the state declaration now has a single-line pointer.
- Add data-testid="row-action-hint" to the Tooltip.Floating label span
so E2E tests locate the tooltip by stable testid rather than by
hard-coupled copy strings.
Add Grafana-style threshold color rules to number tiles. Users can define an ordered list of conditions in Display Settings; the last matching rule's color wins, falling back to the static tile color, then to the default text color. Schema (common-utils): - Add ColorConditionSchema (discriminated union: gt/gte/lt/lte/between/ eq/neq/contains/startsWith/endsWith/regex) - Add colorRules to SharedChartSettingsSchema (optional, max 10) Resolver (app): - evaluateColorCondition: per-rule evaluation with type guards - resolveConditionalColor: last-match-wins, null-safe UI (app): - ColorRulesEditor: sortable rule list via @dnd-kit/sortable, per-operator value inputs, ColorSwatchInput, add/delete controls - ChartDisplaySettingsDrawer: conditional colors section gated on DisplayType.Number, below existing static color picker - EditTimeChartForm: wire colorRules through displaySettings and handleUpdateDisplaySettings - DBNumberChart: evaluate rules against raw numeric value at render time Tests: - Schema positive/negative tests (common-utils) - evaluateColorCondition + resolveConditionalColor unit tests (app) - ColorRulesEditor RTL tests (add/delete/operator/render) - DBNumberChart integration test: success/warning/error scenario No API schema or external-API changes (separate follow-up ticket).
…yout Two fixes: 1. Color not applying: ClickHouse returns UInt64 counts as JSON strings when output_format_json_quote_64bit_integers=1. The rawValue passed to resolveConditionalColor was a string like "1251132", causing all numeric operators (gte, gt, lt, etc.) to short-circuit with a false result. Coerce parseable-as-number strings to numbers before evaluation. Test added for the string-coercion path. 2. Layout: rule rows now use fixed widths and center alignment instead of stretching full-width with flex-start offsets. Operator w=80, value w=120 (single) / w=72 each (between), Add rule button left-aligned.
Replace references to a competitor's threshold model with neutral terminology: 'last-match-wins' and 'higher-priority rules go last'.
#2362 renamed the chart palette tokens from numeric (chart-1..chart-10) to hue names (chart-blue..chart-gray) and made ChartPaletteTokenSchema a strict z.enum that rejects the legacy values. This branch was authored against the old numeric tokens, so the new code's literal 'chart-1' references no longer typecheck or parse. Migrated 'chart-1' to 'chart-blue' across the new code added in this branch: - ColorRulesEditor: default new-rule color and clear-fallback. - ColorRulesEditor.test: makeRule default and the toMatchObject assertion. - utils.test (evaluateColorCondition / resolveConditionalColor describe blocks): the 12 rule literals. - common-utils/types.test (ColorConditionSchema describe block): all 30+ rule literals plus the "parses with all palette tokens" enumeration, which now lists the 10 hue names + the 3 semantic tokens. The 'chart-2' in the neq positive-case test became 'chart-orange' for variety. Legitimate pre-existing legacy-token references were left untouched: - ChartPaletteTokenSchema.parse('chart-1') / .parse('chart-10') in utils.test (still asserts the strict schema rejects legacy). - The render-time migration test in DBNumberChart.test ('chart-1' as any -> getColorFromCSSToken('chart-green')). - ColorSwatchInput.test legacy-coerce case. - dashboard.test / dashboard.remote.test legacy normalizer tests. - LEGACY_CHART_PALETTE_TOKEN_MAP and its guards-test coverage.
a5d93b8 to
19486c6
Compare
…tile-conditional-color-rules
Replace em-dashes with semicolons in two comment lines added by this branch so the prose-lint check passes for HDX-4406-owned files. No code behavior change. - packages/app/src/components/__tests__/DBNumberChart.test.tsx:445 - packages/app/src/utils.ts:621
🟡 Tier 3 — StandardIntroduces new logic, modifies core functionality, or touches areas with non-trivial risk. Why this tier:
Review process: Full human review — logic, architecture, edge cases. Stats
|
Greptile SummaryThis PR adds ordered conditional color rules to number tiles, letting users define threshold-based color overrides that evaluate last-match-wins against the tile's raw data value, with a fallback to the existing static color.
Confidence Score: 5/5Safe to merge — the feature is well-scoped, the resolver logic is correct, and tests cover all operator branches including the previously-flagged neq type-guard fix. The color resolution pipeline (schema → resolver → UI → chart) is implemented consistently end-to-end. The previously flagged neq cross-type bug and the rawValue duplication are both addressed in this HEAD. All operator branches have unit tests, the DrawerFormValues localId round-trip is clean, and the colorRules section is properly gated behind DisplayType.Number. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[DBNumberChart renders] --> B[Compute rawValue from data]
B --> C{rawValue is NaN?}
C -- Yes --> D[rawValue = undefined]
C -- No --> E{rawValue is string?}
E -- Yes --> F{parseable as number?}
F -- Yes --> G[coerce to number]
F -- No --> H[keep as string]
E -- No --> I[keep as number]
D & G & H & I --> J[resolveConditionalColor]
J --> K{rules empty or value null?}
K -- Yes --> L[return fallback = resolveChartPaletteToken config.color]
K -- No --> M[iterate rules in order]
M --> N{rule matches?}
N -- Yes --> O[match = rule.color]
N -- No --> P[match unchanged]
O & P --> Q{more rules?}
Q -- Yes --> M
Q -- No --> R[return last match]
L & R --> S{token defined?}
S -- Yes --> T[getColorFromCSSToken → CSS hex]
S -- No --> U[undefined → default text color]
Reviews (3): Last reviewed commit: "Merge branch 'main' into alex/HDX-4406-n..." | Re-trigger Greptile |
Two fixes from the automated review on #2386: - evaluateColorCondition (neq): guard on typeof so cross-type mismatches return false. Without it, a 'neq' rule with a string value (e.g. 'none') would match every numeric tile value because number !== 'someString' is always true. The contract was already documented in the eq/neq docstring but the implementation only honoured it for eq. Adds two unit tests (number vs string, string vs number) to lock the contract in. - DBNumberChart: reuse the already-computed value instead of re-walking data?.data?.[0] for rawValueRaw. The original duplication shipped because the rules path needs raw input (string/number) while formatNumber needs a numeric fallback; pulling them apart only requires turning Number.NaN into undefined so the coercion IIFE can treat 'no value' uniformly. Note: `value === Number.NaN` is a no-op in JS (NaN never equals itself), so the check uses Number.isNaN(value) instead.
Summary
This PR adds conditional color rules to number tiles. Define an ordered list of conditions; the last matching rule's color wins, so list rules in ascending priority order with the highest-priority rule last. If no rule matches, the tile falls back to its static color (set previously in #2265), then to the default text color.
The schema is intentionally generic at the shared level so a future table-tile slice can attach per-column rules without a schema change.
What changed
common-utils (schema)
ColorConditionSchema(discriminated union ofgt,gte,lt,lte,between,eq,neq,contains,startsWith,endsWith,regexoperators)colorRules(optional, max 10) toSharedChartSettingsSchemaalongside the existingcolorfieldapp (resolver)
evaluateColorCondition: evaluates a single rule against a runtime value with proper type guards (numeric ops reject strings, string ops reject numbers, bad regex returns false)resolveConditionalColor: iterates rules in order, last match wins, falls back tofallback(the static color) when nothing matchesUInt64counts as JSON strings) are coerced to numbers before rule evaluationapp (UI)
ColorRulesEditorcomponent: sortable rule list via@dnd-kit/sortable, per-operator value inputs (single number, two-number range forbetween, text-or-number foreq/neq),ColorSwatchInputper rule, add/delete buttons; "Add rule" disables at 10ChartDisplaySettingsDrawer: added "Conditional colors" section gated onDisplayType.Number, placed below the existing static color pickerEditTimeChartForm:colorRuleswired throughuseWatch,displaySettingsmemo, andhandleUpdateDisplaySettingsDBNumberChart: resolves color viaresolveConditionalColor(rawValue, config.colorRules, config.color)at render timeTests
evaluateColorConditionper-operator,resolveConditionalColorincluding last-match-wins, string coercion, and the canonical success/warning/error scenarioColorRulesEditorDBNumberChartwithcolor: 'chart-success'+ two rules confirms value 50 -> success, 200 -> warning, 1000 -> error; also covers string UInt64 inputNo changes to
packages/apischemas or external API (separate follow-up ticket, mirrors HDX-4378).Screenshots or video
How to test on Vercel preview
Preview routes: /dashboards
Steps:
References