Skip to content

[HDX-4406] Number tile conditional color rules (ordered thresholds)#2386

Merged
kodiakhq[bot] merged 12 commits into
mainfrom
alex/HDX-4406-number-tile-conditional-color-rules
Jun 5, 2026
Merged

[HDX-4406] Number tile conditional color rules (ordered thresholds)#2386
kodiakhq[bot] merged 12 commits into
mainfrom
alex/HDX-4406-number-tile-conditional-color-rules

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

@alex-fedotyev alex-fedotyev commented May 30, 2026

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.

Stacking note: the branch was cut from alex/HDX-4405-fix-stranded-tooltip-in-virtual-table (PR #2380), so the file list includes 4 unrelated files from that PR: HDXMultiSeriesTableChart.tsx, HDXMultiSeriesTableChart.test.tsx, dashboard-table-linking.spec.ts, DashboardPage.ts. Those files belong to #2380 and will drop out of this diff once #2380 merges. The HDX-4406 review surface is the other 10 files.

What changed

common-utils (schema)

  • Added ColorConditionSchema (discriminated union of gt, gte, lt, lte, between, eq, neq, contains, startsWith, endsWith, regex operators)
  • Added colorRules (optional, max 10) to SharedChartSettingsSchema alongside the existing color field

app (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 to fallback (the static color) when nothing matches
  • String data values (ClickHouse returns UInt64 counts as JSON strings) are coerced to numbers before rule evaluation

app (UI)

  • New ColorRulesEditor component: sortable rule list via @dnd-kit/sortable, per-operator value inputs (single number, two-number range for between, text-or-number for eq/neq), ColorSwatchInput per rule, add/delete buttons; "Add rule" disables at 10
  • ChartDisplaySettingsDrawer: added "Conditional colors" section gated on DisplayType.Number, placed below the existing static color picker
  • EditTimeChartForm: colorRules wired through useWatch, displaySettings memo, and handleUpdateDisplaySettings
  • DBNumberChart: resolves color via resolveConditionalColor(rawValue, config.colorRules, config.color) at render time

Tests

  • Schema: positive + negative cases for all operators and array length constraints
  • Resolver: evaluateColorCondition per-operator, resolveConditionalColor including last-match-wins, string coercion, and the canonical success/warning/error scenario
  • UI: add/delete/operator shape/color swatch/render cases for ColorRulesEditor
  • Integration: DBNumberChart with color: 'chart-success' + two rules confirms value 50 -> success, 200 -> warning, 1000 -> error; also covers string UInt64 input

No changes to packages/api schemas or external API (separate follow-up ticket, mirrors HDX-4378).

Screenshots or video

Before After
Number tile has only a static color picker Number tile shows a "Conditional colors" section with add/reorder/delete rule controls below the static color picker

How to test on Vercel preview

Preview routes: /dashboards

Steps:

  1. Open or create a dashboard and add a number tile (or edit an existing one).
  2. Click the "Display Settings" button to open the drawer.
  3. Confirm the "Conditional colors" section appears below the "Color" picker.
  4. Click "Add rule" and verify a rule row appears with operator >, a value input, a color swatch, and a delete button.
  5. Change the operator to >= and set value to 100; pick the Warning color swatch.
  6. Click "Add rule" again; set operator >=, value 500; pick the Error color swatch.
  7. Click "Apply". Verify the tile persists the rules (re-open Display Settings and confirm the two rules are still there).
  8. With a tile value below 100, confirm it renders in the static (or default) color.
  9. With a value between 100 and 499, confirm warning color.
  10. With a value >= 500, confirm error color.
  11. Click "Add rule" 8 more times to reach 10 rules total. Verify the button becomes disabled.
  12. Drag a rule handle to reorder and click Apply; confirm the new order is reflected on re-open.

References

@alex-fedotyev alex-fedotyev added the ai-generated AI-generated content; review carefully before merging. label May 30, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 30, 2026

⚠️ No Changeset found

Latest commit: 16f2943

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jun 5, 2026 7:07pm
hyperdx-storybook Ready Ready Preview, Comment Jun 5, 2026 7:07pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

E2E Test Results

All tests passed • 197 passed • 3 skipped • 1353s

Status Count
✅ Passed 197
❌ Failed 0
⚠️ Flaky 5
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@alex-fedotyev alex-fedotyev changed the title [HDX-4406] Number tile conditional color rules (Grafana-style thresholds) [HDX-4406] Number tile conditional color rules (ordered thresholds) May 30, 2026
Alex Fedotyev and others added 8 commits June 5, 2026 15:51
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.
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
@alex-fedotyev alex-fedotyev changed the base branch from main to alex/HDX-4405-fix-stranded-tooltip-in-virtual-table June 5, 2026 18:02
@alex-fedotyev alex-fedotyev changed the base branch from alex/HDX-4405-fix-stranded-tooltip-in-virtual-table to main June 5, 2026 18:03
@alex-fedotyev alex-fedotyev marked this pull request as ready for review June 5, 2026 18:04
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Jun 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 787 production lines changed (Tier 2 max: < 250)
  • Cross-layer change: touches frontend (packages/app) + shared utils (packages/common-utils)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 7
  • Production lines changed: 787 (+ 1058 in test files, excluded from tier calculation)
  • Branch: alex/HDX-4406-number-tile-conditional-color-rules
  • Author: alex-fedotyev

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

This 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.

  • Schema (common-utils/types.ts): Adds ColorConditionSchema (discriminated union of 11 operators) and colorRules (max 10) to SharedChartSettingsSchema; placed at the shared level so future table-tile slices can reuse it.
  • Resolver (utils.ts): evaluateColorCondition with per-operator type guards and resolveConditionalColor (last-match-wins, short-circuits on null/empty); DBNumberChart coerces ClickHouse UInt64 strings to numbers before evaluation.
  • UI (ColorRulesEditor.tsx, ChartDisplaySettingsDrawer.tsx): Sortable rule list via dnd-kit with localIds stripped on persist; the section is gated behind displayType === DisplayType.Number.

Confidence Score: 5/5

Safe 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

Filename Overview
packages/common-utils/src/types.ts Adds ColorConditionSchema discriminated union and colorRules field to SharedChartSettingsSchema; schema constraints (finite numbers, string min/max, regex validation, max 10 rules) are correct and well-tested.
packages/app/src/utils.ts Adds evaluateColorCondition with per-operator type guards (including the neq typeof fix) and resolveConditionalColor with last-match-wins semantics; logic matches docstrings and is covered by comprehensive unit tests.
packages/app/src/components/DBNumberChart.tsx Color resolution correctly layered (rules → static color → undefined); rawValue computation reuses the existing value variable and correctly converts NaN to undefined and UInt64 strings to numbers before rule evaluation.
packages/app/src/components/ColorRulesEditor.tsx Sortable rule editor with dnd-kit; localId lifecycle (attach on load, strip on persist) is handled correctly; operator switching preserves numeric values but resets string eq/neq values to 0 (minor UX trade-off, not a bug).
packages/app/src/components/ChartDisplaySettingsDrawer.tsx ColorRulesEditor correctly integrated inside the showTileColor gate; localId round-trip through DrawerFormValues is clean; resetToDefaults correctly clears rules when resetting to defaults.
packages/app/src/components/DBEditTimeChartForm/EditTimeChartForm.tsx colorRules correctly wired through useWatch, displaySettings memo, and handleUpdateDisplaySettings with no issues.
packages/app/src/tests/utils.test.ts Comprehensive coverage of all 11 operators in evaluateColorCondition plus resolveConditionalColor edge cases including last-match-wins, null/undefined, and UInt64 string coercion.
packages/common-utils/src/tests/types.test.ts New schema test file with positive/negative cases for all operators, all palette tokens, and array length constraints.
packages/app/src/components/tests/DBNumberChart.test.tsx Integration tests using the real resolveConditionalColor resolver to verify success/warning/error thresholds and UInt64 string coercion end-to-end.
packages/app/src/components/tests/ColorRulesEditor.test.tsx UI tests covering add/delete, 10-rule cap, operator rendering, between dual inputs, and color swatch presence.

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]
Loading

Reviews (3): Last reviewed commit: "Merge branch 'main' into alex/HDX-4406-n..." | Re-trigger Greptile

Comment thread packages/app/src/utils.ts Outdated
Comment thread packages/app/src/components/DBNumberChart.tsx Outdated
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.
@kodiakhq kodiakhq Bot merged commit 3f21e2e into main Jun 5, 2026
19 checks passed
@kodiakhq kodiakhq Bot deleted the alex/HDX-4406-number-tile-conditional-color-rules branch June 5, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-generated AI-generated content; review carefully before merging. automerge review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants