[HDX-1360] feat(app): number tile static color picker#2265
Conversation
PR-1 of the issue #1360 series. The existing ColorSwatchInput (orphan in OSS since #440) used a free-form ColorInput plus a hand-picked 9-value hex array. Refactor it to a palette-only picker driven by the unified 13-token palette landed in #1627: ten categorical tokens (chart-1..chart-10) and three semantic tokens (chart-success, chart-warning, chart-error). Storing tokens (not hex) lets user color choices reflow across themes and color modes without losing contrast or breaking the WCAG ratios baked into the palette. The component remains internal in this PR. PRs 2b through 6b in the issue #1360 series wire it into ChartSeriesEditor, ChartDisplaySettingsDrawer, and the number-tile thresholds / reference-line editors. PR 7 mirrors the schema fields on the external dashboards API. PR 8 documents the feature. Highlights: - New ChartPaletteToken type + CHART_PALETTE_TOKENS, CATEGORICAL_PALETTE_TOKENS, SEMANTIC_PALETTE_TOKENS arrays, isChartPaletteToken type guard in utils.ts. - New getColorFromCSSToken(token) reads the matching --color-chart-* CSS variable, with SSR-safe fallbacks. - New resolveChartColor(token, fallbackIndex, level) for PR 2b to plug into setLineColors / formatResponseForPieChart. - ColorSwatchInput popover split into categorical / semantic sections; trigger uses --color-outline-focus for focus-visible; swatch buttons use aria-pressed; legacy non-token values are guarded to treat as unset. - Storybook stories matrix: six isolated stories (picker mechanics) and five in-context mocks (InSeriesRow, InLineChart, InStackedBar, InNumberTile, InReferenceLineEditor). The in-context stories are storybook-only mocks; they do not import the real editor components. - 13 RTL tests cover the rejection rules and keyboard nav.
🦋 Changeset detectedLatest commit: 838ce63 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
resolveChartColor and ColorSwatchInputProps had no consumers on this branch and tripped the knip CI check. Drop resolveChartColor entirely (the next PR in the issue #1360 series adds it back alongside its first consumer in ChartUtils.setLineColors), and make ColorSwatchInputProps an internal type alias (consumers infer the prop shape from typeof ColorSwatchInput).
E2E Test Results✅ All tests passed • 189 passed • 3 skipped • 1318s
Tests ran across 4 shards in parallel. |
Wire the palette picker into a real product surface. The number tile's Display Settings drawer now exposes a "Color" section that stores a palette token on the tile config; the renderer resolves the token at paint time so the choice reflows correctly across light, dark, and IDE themes. Re-scope of PR #2265: - Move the palette tokens (CHART_PALETTE_TOKENS, ChartPaletteToken, CATEGORICAL_PALETTE_TOKENS, SEMANTIC_PALETTE_TOKENS, isChartPaletteToken) from packages/app/src/utils.ts to packages/common-utils/src/types.ts so the Zod schema and the app share a single source of truth. The theme-aware CSS resolver (getColorFromCSSToken) stays in app because it depends on getComputedStyle(document.documentElement). app/utils.ts re-exports the moved symbols so existing imports keep working unchanged. - Add ChartPaletteTokenSchema = z.enum(CHART_PALETTE_TOKENS) and color: ChartPaletteTokenSchema.optional() on SharedChartSettingsSchema. Mirrors the existing numberFormat: NumberFormatSchema.optional() pattern at the same level (also a number-only field stored at shared level and gated in the UI). - Add the Color section to ChartDisplaySettingsDrawer, gated on displayType === DisplayType.Number, wired via <Controller> around <ColorSwatchInput>. - Round-trip the color through EditTimeChartForm's displaySettings memo and handleUpdateDisplaySettings. - Apply config.color in DBNumberChart via Mantine <Text c={getColorFromCSSToken(token)}>. Unknown tokens (legacy values, schema drift) fall back to the default text color via isChartPaletteToken guard. - Trim the 5 in-context Storybook stories (InSeriesRow, InLineChart, InStackedBar, InNumberTile, InReferenceLineEditor, TokensReference). The picker now has a real product surface, so the mocks are redundant. The 6 isolated stories (Default, Selected, Disabled, WithCustomLabel, AllTokensSelected, KeyboardNav) stay because they exercise picker mechanics that the in-product surface only triggers one row at a time. Adds 3 RTL tests on DBNumberChart: positive (color resolved through getColorFromCSSToken), unset (no resolution), unknown token (renderer falls back gracefully). Schema parity for the external dashboards API (packages/api/src/utils/zod.ts + convertToExternal/InternalTileChartConfig + OpenAPI regen) and customer docs on docs/use/dashboards.md ship in follow-up PRs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…watch-input # Conflicts: # packages/app/src/components/DBNumberChart.tsx
🟡 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
|
|
<!-- deep-review --> Deep Review✅ No critical issues found. The diff is tightly scoped, the palette-token store is additive-optional, the renderer is defensively guarded against legacy values, and 🟡 P2 -- recommended
🔵 P3 nitpicks (4)
Reviewers (5): ce-correctness-reviewer, ce-testing-reviewer, ce-maintainability-reviewer, ce-kieran-typescript-reviewer, ce-api-contract-reviewer. Testing gaps:
|
- Add getColorFromCSSToken unit tests covering the browser happy path,
empty CSS var fallback, getComputedStyle throw fallback, and SSR
fallback for all categorical indices (chart-1 through chart-10).
Tests note that jsdom prevents simulating window===undefined so the
throw branch covers the same paletteTokenSSRFallback code path.
- Add isChartPaletteToken tests for valid tokens, hex strings,
undefined/null/numbers, empty string, case-sensitive non-matches,
out-of-range indices, and lookalike non-tokens.
- Add ChartDisplaySettingsDrawer integration tests: assert the color
picker is present for displayType=Number and absent for Table; assert
onChange receives the selected token (categorical and semantic) when
Apply is clicked.
- Strengthen DBNumberChart color test: update mock to return a valid
hex (#00ff00) so Mantine applies it as an inline color style, then
assert toHaveStyle({color: 'rgb(0, 255, 0)'}) to lock in that the
resolved token reaches the <Text> DOM element.
|
P2 test coverage from deep review: added direct unit tests for |
Deep Review✅ No critical issues found. The change introduces a new field cleanly; the highest-severity items are external-API parity (acknowledged as follow-up 🟡 P2 -- recommended
🔵 P3 nitpicks (12)
Reviewers (9): correctness, testing, maintainability, project-standards, agent-native, learnings-researcher, kieran-typescript, api-contract, adversarial. Testing gaps:
|
P0/P1 fix from deep-review on #2265: the four pick() arrays in convertFormStateToSavedChartConfig and convertFormStateToChartConfig omitted 'color', so a user on a raw-SQL or PromQL Number tile would pick a swatch, see no preview change, and have the value silently stripped on Save. Add 'color' to all four pick() calls alongside 'numberFormat' (the analogous Number-tile-only display field) and add five round-trip tests covering the sql and promql paths.
|
P0/P1 fix: add The four commit: ade1359 |
…c gradient defs Two regressions reported against the hue-named palette refactor: 1. Stored Number-tile colors from #2265 (chart-1..chart-10) were silently dropped on dashboard load. `useDashboards` raw-casts the API response (`hdxServer('dashboards').json<Dashboard[]>()`), so `ChartPaletteTokenSchema`'s `preprocess` never runs and `isChartPaletteToken('chart-1')` resolves `tileColor` to undefined until the user re-saves the tile. Add a new `resolveChartPaletteToken(value)` helper in common-utils that accepts both current hue-named tokens and legacy chart-1..10 values. Use it in DBNumberChart (last-mile resolution before `getColorFromCSSToken`) and in ColorSwatchInput's `safeValue` guard so the picker also shows the migrated swatch when reopening a tile saved with a legacy token. 2. After unifying `COLORS` to the Observable 10 palette, HyperDX info-level <Area> fills in HDXMultiSeriesTimeChart referenced `url(#time-chart-lin-grad-<id>-00c28a)`, but the gradient <defs> block iterates `COLORS`, which no longer contains the HyperDX brand-green `#00c28a` returned by `getChartColorInfo()`. Info-level series rendered with no area fill. Build the gradient defs from the union of `COLORS` and every `lineData[i].color` actually in use, so any semantic color (`info`/`success`/`warning`/`error`) returned by the per-brand helpers is guaranteed to have a matching gradient def. Regression tests cover both bugs (resolveChartPaletteToken in guards.test.ts; DBNumberChart legacy-token render path; ColorSwatchInput legacy-token displays migrated swatch).
Address Deep Review P2 findings on the chart palette rename PR:
1. PATCH/POST validation hole — the fetch-time normalizer only healed
in-memory copies, so dashboards constructed outside the fetch path
(JSON imports via DBDashboardImportPage, presets, MCP payloads)
carried legacy `chart-1`..`chart-10` straight into the strict
server-side `ChartPaletteTokenSchema` and returned a Zod 400. Apply
`normalizeDashboardTileColors` symmetrically at write time in
`useUpdateDashboard` and `useCreateDashboard` (both local and remote
branches). This also converges the DB-side data on next save instead
of holding legacy tokens in storage forever. The normalizer is now
generic over `T extends { tiles?: Tile[] }` and returns the payload
unchanged when tiles is omitted, so partial PATCHes still work.
2. ClickStack legacy slot-flip — pre-rename ClickStack used a different
slot ordering than HyperDX (`--color-chart-1` was brand blue, not
brand green; `--color-chart-2` was orange, not blue; etc.). Since
the migration map preserves HyperDX slot ordering, any ClickStack
dashboard saved via #2265 will visually shift. We chose this
trade-off deliberately over branching the legacy map by
`detectActiveTheme()`: `LEGACY_CHART_PALETTE_TOKEN_MAP` lives in
common-utils (shared with the API), and migration is one-shot
persisted on next save — theme-branching would couple common-utils
to browser DOM state and still produce wrong results for users
whose active theme changed since the original pick. Documented the
trade-off prominently in the changeset and agent_docs so reviewers
and future maintainers can find it.
3. Remote-path test coverage — the existing dashboard test suite mocks
`IS_LOCAL_MODE: true` for the entire file, so the
`hdxServer('dashboards').json<Dashboard[]>().then(...).map(
normalizeDashboardTileColors)` branch had no coverage. Added
`dashboard.remote.test.ts` (separate file because `jest.mock` is
hoisted and per-test isolation via `jest.isolateModulesAsync` proved
unreliable for swapping the constant). Exported `fetchDashboards`
so it can be exercised directly. Tests cover: legacy migration on
the remote path, hue-named tokens passing through unchanged, and
tile identity preservation for the React reconciliation hot path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
First PR in the issue #1360 series. Adds a "Color" section to the number tile's Display Settings drawer that stores a palette token (categorical
chart-1throughchart-10, or semanticchart-success/chart-warning/chart-error). The renderer resolves the token at paint time, so the choice reflows correctly across light, dark, and IDE themes without losing contrast.Re-scoped from the original component-only plan: the picker is now wired into a real product surface (number tile -> Display Settings drawer -> renderer), so reviewers can see it work end-to-end on the Vercel preview instead of through Storybook mocks.
What ships in this PR:
CHART_PALETTE_TOKENS,ChartPaletteToken,CATEGORICAL_PALETTE_TOKENS,SEMANTIC_PALETTE_TOKENS,isChartPaletteToken) move frompackages/app/src/utils.tstopackages/common-utils/src/types.tsso the Zod schema and the app share one source of truth. The theme-aware CSS resolvergetColorFromCSSTokenstays in app (it depends ongetComputedStyle(document.documentElement)).app/utils.tsre-exports the moved symbols so existing imports keep working.ChartPaletteTokenSchema = z.enum(CHART_PALETTE_TOKENS);SharedChartSettingsSchemagainscolor: ChartPaletteTokenSchema.optional(). Mirrors the existingnumberFormat: NumberFormatSchema.optional()pattern at the same level (also a Number-only field stored at shared level and gated in the UI).ColorSwatchInput(palette-only picker, 13 tokens, theme-aware, keyboard-accessible) is unchanged from the previous revision.ChartDisplaySettingsDrawergains a Color section, gated ondisplayType === DisplayType.Number, wired via<Controller>around<ColorSwatchInput>.EditTimeChartFormround-trips the field throughdisplaySettingsandhandleUpdateDisplaySettings.DBNumberChartappliesconfig.colorvia Mantine<Text c={getColorFromCSSToken(token)}>. Unknown tokens (legacy values, schema drift) fall back to the default text color via theisChartPaletteTokenguard.Default,Selected,Disabled,WithCustomLabel,AllTokensSelected,KeyboardNav); dropped the 5 in-context mocks since the picker now has a real product surface.Follow-ups
Filed before merging so the parity work isn't lost:
coloron the number-tile schema: [HDX-1360] External API parity for number-tile color #2359.Per-series color on line / bar / pie, threshold-driven color on number, reference lines, table per-column color, and the background-chart sparkline all ship in their own follow-up PRs.
Test plan
yarn workspace @hyperdx/app jestclean (1647 / 1656 pass, 9 pre-existing skips; 3 new tests onDBNumberChartcover positive, unset, and unknown-token paths).yarn workspace @hyperdx/common-utils jestclean (957 / 957).yarn workspace @hyperdx/app lintclean.yarn workspace @hyperdx/app tsc --noEmitclean.yarn workspace @hyperdx/common-utils lintclean.yarn workspace @hyperdx/app knip: no new findings; pre-existing flags unchanged.Resolves part of #1360 (AC3, partial AC6).