Skip to content

[HDX-1360] feat(app): number tile static color picker#2265

Merged
elizabetdev merged 8 commits into
mainfrom
alex/HDX-1360-color-swatch-input
May 28, 2026
Merged

[HDX-1360] feat(app): number tile static color picker#2265
elizabetdev merged 8 commits into
mainfrom
alex/HDX-1360-color-swatch-input

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

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

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-1 through chart-10, or semantic chart-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:

  • Palette tokens (CHART_PALETTE_TOKENS, ChartPaletteToken, CATEGORICAL_PALETTE_TOKENS, SEMANTIC_PALETTE_TOKENS, isChartPaletteToken) move from packages/app/src/utils.ts to packages/common-utils/src/types.ts so the Zod schema and the app share one source of truth. The theme-aware CSS resolver getColorFromCSSToken stays in app (it depends on getComputedStyle(document.documentElement)). app/utils.ts re-exports the moved symbols so existing imports keep working.
  • New ChartPaletteTokenSchema = z.enum(CHART_PALETTE_TOKENS); SharedChartSettingsSchema gains color: ChartPaletteTokenSchema.optional(). 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).
  • ColorSwatchInput (palette-only picker, 13 tokens, theme-aware, keyboard-accessible) is unchanged from the previous revision.
  • ChartDisplaySettingsDrawer gains a Color section, gated on displayType === DisplayType.Number, wired via <Controller> around <ColorSwatchInput>.
  • EditTimeChartForm round-trips the field through displaySettings and handleUpdateDisplaySettings.
  • DBNumberChart applies config.color via Mantine <Text c={getColorFromCSSToken(token)}>. Unknown tokens (legacy values, schema drift) fall back to the default text color via the isChartPaletteToken guard.
  • Storybook stories trimmed: kept the 6 isolated stories (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:

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 jest clean (1647 / 1656 pass, 9 pre-existing skips; 3 new tests on DBNumberChart cover positive, unset, and unknown-token paths).
  • yarn workspace @hyperdx/common-utils jest clean (957 / 957).
  • yarn workspace @hyperdx/app lint clean.
  • yarn workspace @hyperdx/app tsc --noEmit clean.
  • yarn workspace @hyperdx/common-utils lint clean.
  • yarn workspace @hyperdx/app knip: no new findings; pre-existing flags unchanged.
  • Prose-lint clean.
  • Tier predictor: Tier 3 (819 prod lines, 4 prod files; Tier 2 max is 250).
  • Vercel preview: drive the number-tile drawer, pick a categorical and a semantic token, save, reload, verify persistence and theme reflow (light + dark).

Resolves part of #1360 (AC3, partial AC6).

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-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

🦋 Changeset detected

Latest commit: 838ce63

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

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

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

@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 28, 2026 11:27am

Request Review

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).
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

E2E Test Results

All tests passed • 189 passed • 3 skipped • 1318s

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

Tests ran across 4 shards in parallel.

View full report →

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>
@alex-fedotyev alex-fedotyev changed the title [HDX-1360] feat(app): refactor ColorSwatchInput to palette tokens [HDX-1360] feat(app): number tile static color picker May 28, 2026
…watch-input

# Conflicts:
#	packages/app/src/components/DBNumberChart.tsx
@alex-fedotyev alex-fedotyev marked this pull request as ready for review May 28, 2026 03:10
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label May 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

🟡 Tier 3 — Standard

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

Why this tier:

  • Diff size: 561 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: 9
  • Production lines changed: 561 (+ 541 in test files, excluded from tier calculation)
  • Branch: alex/HDX-1360-color-swatch-input
  • 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.

@github-actions
Copy link
Copy Markdown
Contributor

<!-- 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 ColorSwatchInput's only existing caller is the drawer that this PR also updates — so the breaking API change on the component carries no orphaned consumers.

🟡 P2 -- recommended

  • packages/app/src/utils.ts:499 -- getColorFromCSSToken and paletteTokenSSRFallback add four new branches (SSR fallback, getComputedStyle throw, empty CSS-var, populated CSS-var) with semantic-token cases plus a categorical Number(token.slice(...)) parse, and none are exercised by a direct unit test; the only coverage is via a jest.mock in DBNumberChart.test.tsx that replaces the function entirely.
    • Fix: Add unit tests in packages/app/src/__tests__/utils.test.ts covering each semantic token, a categorical token, the SSR branch, the getComputedStyle throw, and the empty-string fallback.
    • ce-testing-reviewer, ce-kieran-typescript-reviewer
  • packages/common-utils/src/types.ts:839 -- isChartPaletteToken is the runtime safety net for legacy values at two render-time call sites (ColorSwatchInput.safeValue, DBNumberChart.tileColor) but has no direct tests, so a regression that loosens the guard (e.g. dropping the typeof value === 'string' check) would not be caught.
    • Fix: Add tests in packages/common-utils/src/__tests__ covering valid tokens, hex strings, undefined/null/numbers, empty string, and case-sensitive non-matches.
    • ce-testing-reviewer
  • packages/app/src/components/ChartDisplaySettingsDrawer.tsx:124 -- The new showTileColor = displayType === DisplayType.Number gate and the Controller-wrapped ColorSwatchInput, plus the round-trip through EditTimeChartForm.tsx:226-543 (useWatchdisplaySettings memo → handleUpdateDisplaySettingssetValue('color', ...)), have no integration test; a refactor that drops 'color' from any of those three lists would silently break the picker.
    • Fix: Add an integration test that mounts the drawer with displayType=Number, picks a token, asserts onChange fires with {color: 'chart-…', …}, and a negative test that the picker is absent for displayType=Table.
    • ce-testing-reviewer, ce-kieran-typescript-reviewer
  • packages/app/src/components/__tests__/DBNumberChart.test.tsx:307 -- The three new color tests only assert that the mocked getColorFromCSSToken was (or was not) called and that '1234' renders; they never verify the resolved color reaches the <Text c={...}> DOM element, so a regression that drops the c prop or passes undefined would still pass.
    • Fix: Add an assertion against the rendered element's color (e.g. toHaveStyle({color: 'resolved(chart-success)'}) or the Mantine data-* attribute) to lock in the contract that the resolved token is applied.
    • ce-testing-reviewer
🔵 P3 nitpicks (4)
  • packages/common-utils/src/types.ts:828 -- CATEGORICAL_PALETTE_TOKENS = CHART_PALETTE_TOKENS.slice(0, 10) and SEMANTIC_PALETTE_TOKENS = CHART_PALETTE_TOKENS.slice(10) rely on magic indices that silently desync if CHART_PALETTE_TOKENS is reordered or extended; the cast also widens to a plain readonly ChartPaletteToken[] (losing tuple-literal narrowing).
    • Fix: Define the two subset tuples literally and compose CHART_PALETTE_TOKENS = [...CATEGORICAL_PALETTE_TOKENS, ...SEMANTIC_PALETTE_TOKENS] as const so the split is self-describing.
    • ce-maintainability-reviewer, ce-kieran-typescript-reviewer
  • packages/app/src/components/ColorSwatchInput.test.tsx:184 -- The 'keyboard activates the trigger and selects a swatch with Enter' test fires fireEvent.keyDown(trigger, {key: 'Enter'}) and then fireEvent.click(trigger); the click does all the work, so the test passes even if Enter activation is removed entirely.
    • Fix: Drop fireEvent.click and use userEvent.keyboard('{Enter}') after focusing, or rename the test to reflect that it asserts click activation.
    • ce-testing-reviewer, ce-correctness-reviewer
  • packages/app/src/components/ColorSwatchInput.test.tsx:174 -- 'closes the popover after a selection' asserts only that the swatch test-id is absent, which would also be true if the dropdown re-mounted for another reason.
    • Fix: Also assert expect(trigger).toHaveAttribute('aria-expanded', 'false') to verify the popover-closed semantics directly.
  • packages/app/src/utils.ts:519 -- paletteTokenSSRFallback's default branch parses Number(token.slice('chart-'.length)); a future non-numeric categorical token (e.g. chart-neutral) would produce NaN, COLORS[NaN] is undefined, and the ?? COLORS[0] silently masks the gap.
    • Fix: Replace the loose default with an exhaustiveness check (e.g. default: const _: never = token; return COLORS[0];) so a future token forces a deliberate update.
    • ce-kieran-typescript-reviewer

Reviewers (5): ce-correctness-reviewer, ce-testing-reviewer, ce-maintainability-reviewer, ce-kieran-typescript-reviewer, ce-api-contract-reviewer.

Testing gaps:

  • paletteTokenSSRFallback semantic and categorical branches have no direct coverage.
  • The drawer color section and the form's color round-trip are untested end-to-end.
  • No regression test that an external-API payload carrying color is either accepted (post-parity follow-up [HDX-1360] External API parity for number-tile color #2359) or rejected explicitly — today it is silently stripped.

- 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.
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

P2 test coverage from deep review: added direct unit tests for getColorFromCSSToken (browser happy path, empty-CSS-var fallback, getComputedStyle-throw fallback, and all categorical indices), isChartPaletteToken guard tests (13 cases covering valid tokens, hex strings, null/undefined/numbers, case-sensitive mismatches, and out-of-range indices), a new ChartDisplaySettingsDrawer integration test verifying the picker appears for Number tiles and absent for Table tiles plus that onChange fires with the selected token, and a strengthened DBNumberChart color test that asserts the resolved color reaches the DOM element inline style. Commit: 1bf59d5.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

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 #2359), a theme-blind fallback, and test gaps that protect the new behavior. None block merge.

🟡 P2 -- recommended

  • packages/api/src/utils/zod.ts:309 -- The externalDashboardNumberChartConfigSchema and the SQL counterpart at :299 do not declare color; the schema's .transform(...schema.parse(data)) re-parses through the sub-schema and silently strips unknown keys, so a Number tile written through /api/v2/dashboards or MCP saveDashboard loses any color a user set in the UI without raising an error.
    • Fix: Mirror color: ChartPaletteTokenSchema.optional() onto both external Number-tile schemas (and the MCP mcpNumberTileSchema in packages/api/src/mcp/tools/dashboards/schemas.ts) so the new field crosses the external boundary.
    • correctness, api-contract, adversarial, agent-native
  • packages/api/src/routers/external-api/v2/utils/dashboards.ts:265 -- The DisplayType.Number branch of convertToExternalTileChartConfig hand-rolls displayType / sourceId / select / numberFormat and never copies color, so GET /api/v2/dashboards and MCP getDashboard always return Number tiles without color even when one was saved through the UI -- the GET→PUT round-trip silently erases user state.
    • Fix: Forward color: config.color from both the SQL Number branch (:175) and the structured branch (:265) so external reads expose the field, then add a round-trip test covering save-UI → GET-external → PUT-external → reload-UI.
    • api-contract, adversarial, agent-native
  • packages/app/src/utils.ts:519 -- paletteTokenSSRFallback hardcodes the HyperDX CHART_PALETTE hexes for chart-success/warning/error (and indexes into the HyperDX COLORS array for categorical tokens), so a ClickStack-themed user hitting the SSR branch or the getComputedStyle-throw branch sees HyperDX green where the theme defines --color-chart-success as #3ca951 -- a brand mismatch the token system was supposed to prevent.
    • Fix: Either call detectActiveTheme() and pick the matching CHART_PALETTE table, or return '' and let Mantine's default text color cover the fallback path so themes can't drift.
    • correctness, maintainability, project-standards, adversarial
  • packages/common-utils/src/types.ts:832 -- CATEGORICAL_PALETTE_TOKENS = CHART_PALETTE_TOKENS.slice(0, 10) as readonly ChartPaletteToken[] (and SEMANTIC_PALETTE_TOKENS = ...slice(10)) encodes the categorical/semantic split as a positional index with no type-level link to the array's contents, so inserting or reordering a token in CHART_PALETTE_TOKENS silently misclassifies it in the picker grid and in paletteTokenSSRFallback's numeric-parse branch.
    • Fix: Declare CATEGORICAL_PALETTE_TOKENS and SEMANTIC_PALETTE_TOKENS as separate as const tuples and build CHART_PALETTE_TOKENS = [...CATEGORICAL_PALETTE_TOKENS, ...SEMANTIC_PALETTE_TOKENS] as const, making the split the source of truth.
    • maintainability, kieran-typescript
  • packages/app/src/components/ColorSwatchInput.test.tsx:180 -- The "keyboard activates the trigger and selects a swatch with Enter" test fires keyDown('Enter') and then explicitly calls fireEvent.click(trigger)/fireEvent.click(firstSwatch), so the keydown event is dead -- the assertion passes purely on the click path even though the test name claims keyboard coverage; a regression that breaks Enter activation would not be caught.
    • Fix: Replace with await user.keyboard('{Enter}') from @testing-library/user-event after focusing the trigger and the swatch, or rename the test so it doesn't claim keyboard semantics it isn't testing.
  • packages/app/src/components/__tests__/ChartDisplaySettingsDrawer.test.tsx:26 -- All three new color tests start from settings: {} and never open the drawer with a preset color, so a regression in applyDefaultSettings or in the Controller/reset wiring that fails to surface a saved color on initial render would pass CI.
    • Fix: Add a test that renders with settings: { color: 'chart-2' } and asserts the trigger shows Color 2 and the swatch popover marks color-swatch-option-chart-2 with aria-pressed=true.
  • packages/app/src/components/__tests__/ChartDisplaySettingsDrawer.test.tsx:62 -- The Clear → Apply integration is not covered: ColorSwatchInput's clear button is unit-tested in isolation, but no test confirms that clicking Clear inside the drawer and pressing Apply propagates { color: undefined } through react-hook-form's Controller to onChange.
    • Fix: Add a test that renders with settings: { color: 'chart-1' }, opens the popover, clicks color-swatch-input-clear, clicks Apply, and asserts onChange.mock.calls[0][0].color is undefined.
🔵 P3 nitpicks (12)
  • packages/app/src/utils.ts:486 -- getColorFromCSSToken and getColorFromCSSVariable duplicate the SSR-guard / getComputedStyle / trim / fallback structure with only the variable name and fallback differing.
    • Fix: Extract a private readCssVar(name: string): string | undefined helper that both functions delegate to.
    • maintainability
  • packages/app/src/utils.ts:421 -- app/utils.ts re-exports CHART_PALETTE_TOKENS / ChartPaletteToken / isChartPaletteToken / CATEGORICAL_PALETTE_TOKENS / SEMANTIC_PALETTE_TOKENS from @hyperdx/common-utils/dist/types as a transitional barrel, creating two valid import paths for the same symbols.
    • Fix: Either drop the re-export and update the small set of importers to pull from @hyperdx/common-utils/dist/types directly, or add a tracked // TODO(<owner>): so the migration shape is time-boxed.
    • maintainability
  • packages/app/src/components/DBNumberChart.tsx:98 -- getColorFromCSSToken(config.color) runs in the component body on every render and performs a synchronous getComputedStyle(document.documentElement).getPropertyValue(...); cheap per tile but multiplied across a dashboard with many Number tiles.
    • Fix: Wrap in useMemo(() => ..., [config.color]).
    • maintainability
  • packages/app/src/components/ChartEditor/__tests__/utils.test.ts:1331 -- The new color round-trip tests cover only the configType: 'sql' and configType: 'promql' branches; the third if (source) builder branch (which carries color implicitly through omit + spread) has no test pinning the contract, so a future refactor to an explicit pick would silently drop color.
    • Fix: Add a builder-branch test that calls the converter with a builder form state plus color: 'chart-1' and asserts the resulting config retains color.
    • correctness, testing
  • packages/app/src/components/ChartEditor/utils.ts:83 -- The two converters add 'color' to four separate pick(form, [...]) lists for sql/promql while the builder branch carries it implicitly; a future shared field will need four matching edits or it will silently strip on three of the four paths.
    • Fix: Extract SHARED_CHART_PICK_KEYS / SHARED_RAW_SQL_PICK_KEYS / SHARED_PROMQL_PICK_KEYS as as const arrays so all call sites reuse one source of truth, or switch the sql/promql branches to spread-then-omit to match builder.
    • adversarial
  • packages/app/src/components/__tests__/DBNumberChart.test.tsx:333 -- The "unset" and "unknown token" tests assert only that getColorFromCSSToken was not called and that the <Text> rendered; they do not assert the rendered element has no inline color style, so a regression that hardcodes a fallback color would not be caught.
    • Fix: Add expect(textEl.style.color).toBe('') (or assert the style attribute does not contain color) to both tests.
    • testing
  • packages/app/src/components/ChartDisplaySettingsDrawer.tsx:67 -- applyDefaultSettings passes color: settings.color through unfiltered, so if a non-token value ever reached the schema (legacy data, schema drift, manual mongo edit) the form would hold the unsafe value invisibly while the picker shows "unselected".
    • Fix: Filter at the source: color: isChartPaletteToken(settings.color) ? settings.color : undefined.
    • adversarial
  • packages/app/src/utils.ts:531 -- The default: arm of paletteTokenSSRFallback runs Number(token.slice('chart-'.length)) - 1 even though token still has the full ChartPaletteToken union type; correctness is preserved only by the ?? COLORS[0] tail.
    • Fix: Branch on a typed isCategoricalPaletteToken guard (paired with the split tuples above) so future additions to the semantic set force an explicit case.
    • kieran-typescript
  • packages/app/src/components/ColorSwatchInput.test.tsx:1 -- The new test file is a sibling of ColorSwatchInput.tsx while the other two new component test files in this PR live under packages/app/src/components/__tests__/; the CLAUDE.md testing convention prefers the __tests__/ location.
    • Fix: Move the file to packages/app/src/components/__tests__/ColorSwatchInput.test.tsx.
    • project-standards
  • packages/app/src/components/ColorSwatchInput.tsx:48 -- The JSDoc and the comment at packages/common-utils/src/types.ts:792 both point readers to notes/repo-conventions/hyperdx/tile-styling.md for the design rationale, but no such file exists in the working tree.
    • Fix: Either add the referenced doc or remove the dead pointer; agent_docs/data_viz_colors.md and agent_docs/themes.md already cover most of the rationale.
  • .changeset/number-tile-static-color.md:1 -- The changeset lists only '@hyperdx/app': patch even though packages/common-utils/src/types.ts gains new exports (CHART_PALETTE_TOKENS, ChartPaletteToken, ChartPaletteTokenSchema, isChartPaletteToken, CATEGORICAL_PALETTE_TOKENS, SEMANTIC_PALETTE_TOKENS) and a new color schema field.
    • Fix: Add '@hyperdx/common-utils': patch to the changeset so downstream consumers see the bump.
    • project-standards
  • packages/common-utils/src/types.ts:856 -- The "consider if they need to be made to the external API schema as well" comment now sits directly above an unmirrored field; a future contributor has no inline signal that color is the existing exception.
    • Fix: Add a one-line // TODO(#2359): not yet wired through the external API. next to the new color field so the gap is visible at the call site.
    • api-contract

Reviewers (9): correctness, testing, maintainability, project-standards, agent-native, learnings-researcher, kieran-typescript, api-contract, adversarial.

Testing gaps:

  • No external-API regression test for color strip-on-write / drop-on-read on Number tiles.
  • No drawer test for initial-value rendering of color or for the Clear → Apply round-trip.
  • Builder branch of convertFormState* converters has no color round-trip test.
  • getColorFromCSSToken SSR branch (typeof window === 'undefined') is covered only indirectly via the getComputedStyle-throws path.
  • No theme-switch test asserting that resolved colors change when document.documentElement toggles between the hyperdx and clickstack theme classes.

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.
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

P0/P1 fix: add color to sql/promql chart config converters

The four pick() arrays in convertFormStateToSavedChartConfig and convertFormStateToChartConfig now include 'color' alongside 'numberFormat' for the sql and promql branches. Adds 5 round-trip tests.

commit: ade1359

@elizabetdev elizabetdev merged commit a44fa21 into main May 28, 2026
19 checks passed
elizabetdev added a commit that referenced this pull request May 28, 2026
…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).
elizabetdev added a commit that referenced this pull request May 29, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants