Skip to content

feat(dashboards): per-column color on table tiles#2517

Open
alex-fedotyev wants to merge 5 commits into
mainfrom
alex/table-cell-color
Open

feat(dashboards): per-column color on table tiles#2517
alex-fedotyev wants to merge 5 commits into
mainfrom
alex/table-cell-color

Conversation

@alex-fedotyev

@alex-fedotyev alex-fedotyev commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Bring the number-tile color features to dashboard table tiles. On a builder table tile you can now set a static color on a column and layer ordered conditional rules (for example > 500 turns the cell red), the table-cell counterpart of the number-tile color picker. This extends the tile color work from #1360 to tables.

Summary

  • Add optional color and colorRules to the per-column builder config (DerivedColumnSchema), mirroring the existing per-column numberFormat. colorRules reuses the existing ColorConditionSchema (ordered, last matching rule wins, falling back to the static color).
  • Render: DBTableChart builds per-column colorByColumn / rulesByColumn lookups alongside formatByColumn, and HDXMultiSeriesTableChart resolves and applies each cell's color via the shared resolveConditionalColor and getColorFromCSSToken. Numeric-string values (ClickHouse serializes counts as strings) are coerced so numeric rules match, and unknown tokens are guarded so older configs render the default color instead of throwing.
  • Editor: a new per-column color drawer (SeriesColorDrawer) opens from a palette icon in the series row, shown only on table tiles. It reuses the existing ColorSwatchInput and ColorRulesEditor.
  • Colors persist as palette tokens, never hex, so they reflow across light and dark themes.

Screenshots

A builder table tile with a static chart-success base on the count column plus a > 50000 rule mapped to chart-error, over the demo logs. Values above the threshold render red, the rest green; the group-by column stays the default color.

table light

table dark

Why the types.ts move

To let DerivedColumnSchema reference the palette and color-condition schemas (defined lower in the file), I relocated those primitive schemas above it. This is a pure move with no logic change. The legacy-token migration helpers and the number-tile schemas stay where they were, so the relocation is kept to the minimum the ordering requires.

What's not in this PR (follow-ups)

  • Group-by / dimension column coloring (the select array addresses aggregation columns only).
  • Raw-SQL table color (no per-column schema slot to attach to).
  • String / regex operators in the rules UI (the schema accepts them; the editor emits the numeric and equality subset today, same as the number tile).
  • External API, MCP, and customer-docs parity, each as its own follow-up, matching how the number-tile color shipped.

Test plan

  • yarn lint:fix
  • make ci-lint (lint + tsc), yarn knip
  • Unit: schema round-trip (color + colorRules per operator family, plus rejections for unknown tokens and more than 10 rules); table render (static color, conditional match / no-match, numeric-string coercion, unknown-token renders default without throwing); the color drawer (swatch sets the color, rules round-trip and strip client-side ids, clear); the series editor (color control shown on table tiles, hidden otherwise, opens the drawer).
  • Manual UI verification in light and dark on the Vercel preview build (screenshots above).

Bring the number-tile color features to builder table tiles: a per-column
static palette color plus ordered conditional color rules, applied per cell
at render via the shared resolver.

- Add optional `color` and `colorRules` to `DerivedColumnSchema` (the
  per-column builder config), mirroring the existing per-column
  `numberFormat`. Move the chart palette tokens and `ColorConditionSchema`
  above `DerivedColumnSchema` so it can reference them; pure relocation, no
  logic change (the repo deliberately avoids `z.lazy` forward refs). The
  legacy-token migration helpers and number-tile schemas stay in place.
- Build per-column `colorByColumn` / `rulesByColumn` lookups in DBTableChart
  alongside `formatByColumn`, and resolve + apply each cell's color in the
  table renderer, guarding unknown tokens so legacy configs never crash.
- Add a per-column color drawer (SeriesColorDrawer) opened from the series
  row, gated to table tiles, reusing ColorSwatchInput and ColorRulesEditor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 8, 2026 8:01pm
hyperdx-storybook Ready Ready Preview, Comment Jul 8, 2026 8:01pm

Request Review

@changeset-bot

changeset-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1678fc4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Minor
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

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

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Jun 24, 2026
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

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

Why this tier:

  • Diff size: 647 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: 6
  • Production lines changed: 647 (+ 382 in test files, excluded from tier calculation)
  • Branch: alex/table-cell-color
  • 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

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the number-tile per-column color feature to builder table tiles. Users can set a static palette-token color on any aggregation column and layer up to 10 ordered conditional rules (last match wins), authored from a new SeriesColorDrawer and rendered per-cell in HDXMultiSeriesTableChart.

  • DerivedColumnSchema gains optional color and colorRules fields, reusing the existing ColorConditionSchema; the schema definitions were relocated within types.ts to satisfy forward-reference ordering with no logic change.
  • DBTableChart builds colorByColumn / rulesByColumn maps mirroring the existing formatByColumn pattern; HDXMultiSeriesTableChart resolves each cell's color via resolveConditionalColor + getColorFromCSSToken, with a numeric-string coercion guard for ClickHouse's UInt64-as-string serialisation.
  • A new SeriesColorDrawer component provides the editing UI, integrating with the series row via a palette icon gated on showColor={displayType === DisplayType.Table}.

Confidence Score: 4/5

Safe to merge after fixing the missing style={colorStyle} on the external-link cell branch in HDXMultiSeriesTableChart.

The external-link branch (action.external = true) is the only cell render path that does not apply the configured color. Every other branch — internal <Link>, error <button>, search <Link>, and plain <div> — has style={colorStyle}. A user who sets a column color and whose rows navigate to external URLs will see no color on those cells, silently contradicting the configured intent. The rest of the implementation is well-structured and mirrors existing patterns correctly.

packages/app/src/HDXMultiSeriesTableChart.tsx — the external-link branch in the cell renderer is missing style={colorStyle}.

Important Files Changed

Filename Overview
packages/app/src/HDXMultiSeriesTableChart.tsx Adds per-column color rendering via resolveConditionalColor + getColorFromCSSToken with a numeric-string coercion guard; the external-link cell branch is missing style={colorStyle} while all other branches have it.
packages/app/src/components/DBTableChart.tsx Builds colorByColumn and rulesByColumn maps mirroring formatByColumn; correctly gates on builder/non-ratio config, skips group-by columns, and declares proper memo deps.
packages/app/src/components/SeriesColorDrawer.tsx New drawer correctly isolates editing state via react-hook-form, attaches/strips localIds for dnd-kit, resets on close, and only closes on successful submit.
packages/common-utils/src/types.ts Pure relocation of palette/color-condition schemas above DerivedColumnSchema to allow forward reference; adds color and colorRules to the per-column schema. No logic changes.
packages/app/src/components/DBEditTimeChartForm/ChartSeriesEditor.tsx Adds showColor prop, guards the palette icon and SeriesColorDrawer on it, and correctly watches form state to tint the icon when a color is set.
packages/app/src/tests/HDXMultiSeriesTableChart.test.tsx Good test coverage for static color, conditional match/no-match, numeric-string coercion, and unknown-token guard; no external-link path tested.
packages/common-utils/src/tests/types.test.ts Comprehensive schema round-trip tests for all operator families, backward compatibility, token rejection, and max-rules limit.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ChartSeriesEditor\nshowColor=true for Table tiles] -->|palette icon click| B[SeriesColorDrawer\nopened]
    B -->|Apply| C[onChange: color + colorRules\nstored in DerivedColumnSchema]
    C --> D[DBTableChart\ncolorByColumn / rulesByColumn maps\nbuilt from select x meta index]
    D --> E[HDXMultiSeriesTableChart Table\nper-cell render]
    E --> F{cellValue coercion\nnumeric string to number}
    F --> G[resolveConditionalColor\nlast-match-wins over colorRules]
    G -->|match| H[rule.color token]
    G -->|no match| I[static color token / undefined]
    H --> J{isChartPaletteToken guard}
    I --> J
    J -->|known token| K[getColorFromCSSToken hex/CSS\nstyle=colorStyle applied]
    J -->|unknown/legacy| L[colorStyle = undefined\ndefault text color]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[ChartSeriesEditor\nshowColor=true for Table tiles] -->|palette icon click| B[SeriesColorDrawer\nopened]
    B -->|Apply| C[onChange: color + colorRules\nstored in DerivedColumnSchema]
    C --> D[DBTableChart\ncolorByColumn / rulesByColumn maps\nbuilt from select x meta index]
    D --> E[HDXMultiSeriesTableChart Table\nper-cell render]
    E --> F{cellValue coercion\nnumeric string to number}
    F --> G[resolveConditionalColor\nlast-match-wins over colorRules]
    G -->|match| H[rule.color token]
    G -->|no match| I[static color token / undefined]
    H --> J{isChartPaletteToken guard}
    I --> J
    J -->|known token| K[getColorFromCSSToken hex/CSS\nstyle=colorStyle applied]
    J -->|unknown/legacy| L[colorStyle = undefined\ndefault text color]
Loading

Reviews (5): Last reviewed commit: "Merge branch 'main' into alex/table-cell..." | Re-trigger Greptile

Comment thread packages/app/src/components/SeriesColorDrawer.tsx
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 228 passed • 3 skipped • 1448s

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

Tests ran across 4 shards in parallel.

View full report →

@alex-fedotyev alex-fedotyev self-assigned this Jun 24, 2026
Address review on #2517: move onClose() inside the handleSubmit success
callback in SeriesColorDrawer so closing follows a valid submit instead of
firing unconditionally. Harmless today (no validation rules) but avoids
silently discarding an in-progress edit if a resolver or required field is
added later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address testing feedback on #2517: ClickHouse serializes numeric aggregates
(count is UInt64, sums) as strings, so the cell value arrives as "15770" not
15770. The numeric color operators (gt / lt / between) require a number, so a
rule like "> 100 -> red" never matched and every cell fell back to the static
color. Coerce a numeric-looking string to a number before resolving (genuine
strings stay as-is for equality / string-match rules), mirroring DBNumberChart.

Add a render test that drives string-serialized values so the regression is
locked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Per-column color for dashboard table tiles: adds optional color / colorRules to the shared DerivedColumnSchema, resolves each cell's color in HDXMultiSeriesTableChart / DBTableChart, and adds a SeriesColorDrawer editor gated to table tiles. The types.ts relocation was verified as a pure move (identical text; no exported symbol or z.infer consumer changed), the select[i] → meta[i] mapping mirrors the existing tested formatByColumn path and degrades gracefully, and the unknown-token render guard was confirmed to fully prevent the getColorFromCSSToken throw. No happy-path crash, injection, data-loss, or auth issue was found.

✅ No critical issues found.

🟡 P2 — recommended

  • packages/common-utils/src/types.ts:413 — Table-column colorRules validates against the full ColorConditionSchema (including regex), and the render path recompiles new RegExp(rule.value) per cell across up to MAX_TABLE_ROWS (10k) rows × 10 rules, so a crafted import/internal-API config with a catastrophic-backtracking pattern freezes the tab for every viewer of the tile.
    • Fix: Validate table colorRules against a narrowed schema that excludes regex/string-match (mirroring NumberTileColorConditionSchema), and/or hoist regex compilation out of the per-cell loop with a per-rule cache.
    • api-contract, adversarial, maintainability
  • packages/app/src/components/DBTableChart.tsx:141 — The new colorByColumn / rulesByColumn map construction (ratio-config skip, group-by exclusion, select[i]meta[i].name keying) has no direct test; an off-by-one in the index mapping or a broken group-by exclusion would pass all current tests.
    • Fix: Add DBTableChart tests asserting a series color/rules land on the correct result column by index, group-by columns resolve to undefined, and ratio configs skip color entirely.
    • testing, correctness, maintainability, kieran-typescript, project-standards
  • packages/app/src/HDXMultiSeriesTableChart.tsx:196 — The ClickHouse numeric-string coercion is duplicated inline here and in DBNumberChart.tsx:292 (with a subtly different empty-string guard), so a future fix to serialization handling must touch two places and they can drift.
    • Fix: Extract a single coerceClickHouseNumeric(value): number | string | null helper next to resolveConditionalColor in utils.ts and call it from both renderers.
    • maintainability, kieran-typescript
🔵 P3 nitpicks (7)
  • packages/app/src/__tests__/HDXMultiSeriesTableChart.test.tsx:461 — The conditional-match test asserts the matched cell's color is .not.toBe('') rather than the exact token, so a regression resolving to the wrong (but non-empty) color would still pass.
    • Fix: Assert the matched cell's color equals normalizedColor('chart-error'), matching the static-color test.
    • testing
  • packages/app/src/HDXMultiSeriesTableChart.tsx:196 — The unconditional Number() coercion turns numeric-looking string cells into numbers before rule evaluation, so contains/startsWith/endsWith/regex rules can never match a numeric-string column, contradicting the adjacent comment claiming string rules still work.
    • Fix: Correct the comment, and when a string-match rule is present evaluate it against the raw string value.
    • correctness, adversarial
  • packages/app/src/HDXMultiSeriesTableChart.tsx:203Number.isFinite(Number(x)) accepts hex/scientific/octal/zero-padded literals ("0x10"→16, "1e3"→1000, "007"→7), so those string cells get mis-colored by numeric gt/lt/between rules.
    • Fix: Gate coercion behind a strict decimal pattern (e.g. /^-?\d+(\.\d+)?$/) before calling Number().
    • adversarial
  • packages/app/src/components/SeriesColorDrawer.tsx:76applyChanges has no re-entry guard; the drawer stays mounted through its close animation, so a rapid double-click on Apply double-fires onChange + onSubmit() (a duplicate chart save).
    • Fix: Disable the Apply button (or set an in-flight flag) after the first click instead of relying on the drawer disappearing.
    • julik-frontend-races
  • packages/app/src/components/SeriesColorDrawer.tsx:24 — ~40 lines of form/drawer orchestration (appliedDefaults memo, reset effect, applyChanges, clear, localId attach/strip) are copy-adapted from ChartDisplaySettingsDrawer, so the two editors can drift independently.
    • Fix: Extract a shared useColorEditorForm hook (or shared fields body) used by both drawers.
    • maintainability
  • packages/common-utils/src/types.ts:411 — Comments frame the string-match/regex operators as reserved for a "future table-tile slice," but this PR is that slice and still does not expose them in the editor, so the note misdescribes the current state.
    • Fix: Update the comments to state those operators remain unexposed capability rather than pending work.
    • maintainability
  • packages/api/src/mcp/tools/dashboards/schemas.ts — The MCP table-tile select schema omits color/colorRules, so an agent setting per-column table colors via saveDashboard/patchDashboard has them silently stripped while the call reports success (a parity gap the PR defers, but the silent success is the notable part).
    • Fix: Add optional color/colorRules to the table select schema, or reject them explicitly so agents get a clear error instead of a silent drop.
    • ce-agent-native-reviewer

Reviewers (9): correctness, testing, maintainability, project-standards, kieran-typescript, julik-frontend-races, api-contract, adversarial, ce-agent-native-reviewer.

Testing gaps:

  • No end-to-end render test for between / eq / neq / string-match operators or last-match-wins ordering (only static color and a single gt rule are covered).
  • No test for the coercion edge inputs ("0x10", "1e3", "007", " ") or for string-match rules against numeric-string cells.
  • No SeriesColorDrawer test for authoring a new rule, dnd reorder round-trip, ESC/overlay-discard, or switching displayType away from Table while the drawer is open.
  • No benchmark bounding worst-case render time for a regex rule at 10k-row scale.

kodiakhq Bot pushed a commit that referenced this pull request Jul 9, 2026
…tile (#2520)

Builds on the number-tile background sparkline (#2489, #2501) and sets up the table-cell trend follow-up tracked in HDX-4604.

## Summary

Extracts the chrome-less recharts core out of `NumberTileBackgroundChart` into a reusable `<Sparkline>` component (line / area / bar) and renders the number tile's background trend through it. This is a pure consolidation: the number tile issues the same query and draws the same faint line / area behind the value, so there is no user-visible change.

The table-cell trend feature (HDX-4604) needs the identical render shell in every numeric cell. A shared primitive is the alternative to standing up a second chart component next to this one, which is also the reuse question Mike raised on #2489 ("any reason we couldn't reuse/extend the usual time chart instead?").

## What

- New `packages/app/src/components/Sparkline.tsx`: `<Sparkline points type={'line' | 'area' | 'bar'} color height />`. Chrome-less (no axes, grid, legend, or tooltip; dots and animation off), sized to fill its parent via `ResponsiveContainer` (`height` defaults to `100%`). Renders nothing for fewer than two points. The line and area branches are lifted from `NumberTileBackgroundChart` unchanged; the bar branch follows the existing `PatternTrendChart` in `DBRowTable` and is here for the table-cell consumer that lands next.
- `NumberTileBackgroundChart.tsx` now renders `<Sparkline points={points} type={backgroundChart.type} color={color} />` inside its unchanged `aria-hidden` wrapper. Its data path (`buildSparklineTimeConfig`, `convertToTimeChartConfig`, `useQueriedChartConfig`, `formatResponseForTimeChart`) and the single-series `sparklinePointsFromGraphResults` helper are unchanged.

## Why the helper stays put

`sparklinePointsFromGraphResults` is single-series and specific to the number tile's data path. The table render reconstructs per-group trends differently, so the helper would not be shared; it stays with the number tile. Only the genuinely shared pieces (the component and the `SparklinePoint` type) live in `Sparkline.tsx`. This keeps the existing `NumberTileBackgroundChart` tests in place and avoids a dead re-export.

## No behavior change

The render is a verbatim lift, so the recharts output is identical. The diff on `NumberTileBackgroundChart.tsx` shows the same `<Area>` / `<Line>` props (stroke, `strokeOpacity` 0.5, `strokeWidth` 2, `fillOpacity` 0.15, `dataKey`, margin) moving into `<Sparkline>`, wrapped in the same `ResponsiveContainer` inside the same wrapper. The number tile passes the same resolved color and type it computed before, and keeps its own "fewer than two points" guard so the background layer is still absent when there is no trend.

## Test plan

- [x] `nx run @hyperdx/app:ci:lint` (eslint + tsc) clean
- [x] `nx run @hyperdx/app:ci:unit`: 2091 pass, 0 fail. Includes the existing number-tile render-wiring test and the `DBNumberChart` consumer test, both unchanged and green.
- [x] New `Sparkline.test.tsx`: mounts the component and asserts the line / area / bar recharts layers render in the given color, and that it renders nothing below two points.
- [x] Dev stack builds and serves the branch (app returns 200), confirming the refactor compiles and runs in the Next.js build, not only under jest.

Visual note: this is a verbatim render extraction with no visual delta, so the proof of "renders identically" is the render-block diff plus the unchanged consumer test rather than a before/after screenshot. [ui-check: allow]

## What's not in this PR (follow-up, HDX-4604)

- The table-tile "show trend sparklines" display toggle and its schema field.
- The per-cell scoped query and grouped cell render in `DBTableChart`, which consumes `<Sparkline>` (lands after #2517 so the per-column color drives the sparkline color).
- External API v2 / MCP parity and customer docs for the toggle.

No changeset: internal refactor with no user-facing behavior change. [no-changeset: allow]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants