feat(external-api): round-trip dashboard containers, tabs, and tile container/tab refs#2201
feat(external-api): round-trip dashboard containers, tabs, and tile container/tab refs#2201alex-fedotyev wants to merge 3 commits intomainfrom
Conversation
…ontainer/tab refs (#2150) PR #2015 added a dashboard organization layer (containers with optional tabs, tiles join a container via containerId and a tab via tabId) but the v2 external API was not updated to round-trip the new fields. External integrations that build dashboards programmatically had no way to use the new layer. This wires the full set of fields through CREATE / GET / LIST / UPDATE. Dashboards saved without containers round-trip unchanged (Mongoose returns an empty array for missing containers, so the conversion only emits the field when at least one container is present). The body schema validates that: - container ids are unique within the dashboard - tab ids are unique within their container - tile.containerId resolves to a real container - tile.tabId resolves to a tab inside the tile's container - tile.tabId requires tile.containerId to be set Tests cover create + get round-trip, update round-trip with re-homing tiles and dropping a container, optional-field defaults, all five validation rejections, and the no-containers backward-compat case. The conversion utilities also pick up containerId / tabId on the tile itself: convertToInternalTileConfig now extends its pick list (was the specific bug v2 of the plan missed) and the legacy series translator in externalApi.ts also propagates the fields so both code paths preserve them. Refs #2150, follows up on #2015 (7665fbe).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 6884d0a 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.
|
🔴 Tier 4 — CriticalTouches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
PR Review✅ No critical issues found. The implementation is clean, additive, and well-tested. A few minor observations:
|
Empty-string values previously passed per-field validation and only hit the cross-field check (no container has id ''). Adding .min(1) matches the shared DashboardContainerSchema pattern and surfaces a field-level error instead.
E2E Test Results✅ All tests passed • 159 passed • 3 skipped • 1168s
Tests ran across 4 shards in parallel. |
After reading notes/principles/external-api-audit.md and walking through the UI surface (useDashboardContainers.tsx, DashboardContainer.tsx, GroupTabBar.tsx), three gaps were caught that the initial implementation missed. - OpenAPI parity: TileBase.containerId / tabId now declare minLength: 1 to match the Zod schema's z.string().min(1).optional(). The Zod fix landed in the previous commit but the OpenAPI didn't pick up the constraint until JSDoc was updated and openapi.json regenerated. - Test gap: explicit empty containers: [] now has its own round-trip test. The conversion normalizes [] back to absent on read (the existing length-guard makes this work), but the behavior wasn't asserted. - Test gap: tile.containerId or tile.tabId set to an empty string is now explicitly rejected. Previously this would have failed cross-field validation only because no real container has id "", not because the tile-level rule fired. UI invariants the API stays permissive about (auto-fixed by the UI rather than rejected) are documented in the per-feature code map under notes/repo-conventions/hyperdx/dashboards-containers-tabs.md in the workspace.
Summary
PR #2015 added a dashboard organization layer (containers with optional tabs, plus per-tile
containerIdandtabId) but the v2 external API was not updated to round-trip the new fields. External integrations that build dashboards programmatically had no way to use the new layer.This wires the full set of fields through
CREATE/GET/LIST/UPDATEon/api/v2/dashboards. Dashboards saved without containers round-trip unchanged.Closes #2150. Follow-up to #2015 (commit 7665fbe).
What's in scope
containers: DashboardContainer[]?(imported from@hyperdx/common-utils) and the tile schema gainscontainerId?andtabId?.convertToExternalDashboardnow emitscontainers(only when at least one is present, so dashboards without the layer round-trip with the field absent).convertTileToExternalChartandconvertToInternalTileConfigpropagatecontainerIdandtabId. The legacyseries-format translator inexternalApi.tsalso propagates them so both code paths preserve the fields.containers: 1projection is added to the MongoosefindandfindOnecalls.containerIdresolves to a real containertabIdresolves to a tab inside that containertabIdrequirescontainerIdto be setDashboardContainer,DashboardContainerTab, the new tile fields, and the new dashboard field onDashboard/CreateDashboardRequest/UpdateDashboardRequest.openapi.jsonregenerated.Out of scope
type: 'section'discriminator removed in refactor: Unify section/group into single Group with collapsible/bordered options #2015 (commit 7665fbe). Not emitted, not validated against. Legacy documents parse cleanly via Zod's strip-unknown.repeatfield (future work per refactor: Unify section/group into single Group with collapsible/bordered options #2015 review notes).hyperdx_save_dashboardtool is unchanged in this PR. Its inlineinputSchemadoes not yet exposecontainers, and adding it there is its own follow-up; doing both at once would tangle two surfaces.onClickare tracked separately.Tier
The triage classifier marks
packages/api/src/routers/external-api/v2/*as critical-path, so this lands as Tier 4 by directory rule, even though the diff is small (~284 prod lines) and additive. Splitting further would separate the body schema, the conversion utilities, and the route wiring from each other and not actually reduce review burden. Happy to break this up if there's a preferred way to slice it.Test plan
yarn ci:lint(lint + tsc + spectral) on@hyperdx/common-utils,@hyperdx/api,@hyperdx/appyarn knip(no new unused exports)yarn jest dashboards.test.ts -t "Containers and tabs"— all 8 new tests passyarn jest dashboards.test.ts— 86/86 tests pass (no regressions in old or new format suites)yarn jest src/mcp/__tests__/dashboards.test.ts— 19/19 MCP dashboard tests pass (the MCP body schema shares with the external API body schema, so this confirms the new validations don't break the MCP path)openapi.jsonregenerated and committed; spectral lint passes