Skip to content

feat(external-api): round-trip dashboard containers, tabs, and tile container/tab refs#2201

Open
alex-fedotyev wants to merge 3 commits intomainfrom
alex/HDX-2150-external-api-containers-tabs
Open

feat(external-api): round-trip dashboard containers, tabs, and tile container/tab refs#2201
alex-fedotyev wants to merge 3 commits intomainfrom
alex/HDX-2150-external-api-containers-tabs

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

Summary

PR #2015 added a dashboard organization layer (containers with optional tabs, plus per-tile containerId and 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 on /api/v2/dashboards. Dashboards saved without containers round-trip unchanged.

Closes #2150. Follow-up to #2015 (commit 7665fbe).

What's in scope

  • Dashboard body Zod schema gains containers: DashboardContainer[]? (imported from @hyperdx/common-utils) and the tile schema gains containerId? and tabId?.
  • convertToExternalDashboard now emits containers (only when at least one is present, so dashboards without the layer round-trip with the field absent).
  • convertTileToExternalChart and convertToInternalTileConfig propagate containerId and tabId. The legacy series-format translator in externalApi.ts also propagates them so both code paths preserve the fields.
  • The containers: 1 projection is added to the Mongoose find and findOne calls.
  • New cross-field validation on the body schema:
    • container ids unique within a dashboard
    • tab ids unique within a container
    • tile containerId resolves to a real container
    • tile tabId resolves to a tab inside that container
    • tile tabId requires containerId to be set
  • OpenAPI JSDoc additions for DashboardContainer, DashboardContainerTab, the new tile fields, and the new dashboard field on Dashboard / CreateDashboardRequest / UpdateDashboardRequest. openapi.json regenerated.
  • A changeset entry.

Out of scope

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/app
  • yarn knip (no new unused exports)
  • Integration: yarn jest dashboards.test.ts -t "Containers and tabs" — all 8 new tests pass
  • Integration: full yarn jest dashboards.test.ts — 86/86 tests pass (no regressions in old or new format suites)
  • Integration: 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.json regenerated and committed; spectral lint passes

…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).
@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 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 5, 2026 8:50pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

🦋 Changeset detected

Latest commit: 6884d0a

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

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Minor
@hyperdx/app 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

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Building Building Preview, Comment May 5, 2026 8:19pm

Request Review

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label May 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Critical-path files (2):
    • packages/api/src/routers/external-api/v2/dashboards.ts
    • packages/api/src/routers/external-api/v2/utils/dashboards.ts

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 5
  • Production lines changed: 288 (+ 361 in test files, excluded from tier calculation)
  • Branch: alex/HDX-2150-external-api-containers-tabs
  • Author: alex-fedotyev

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

PR Review

✅ No critical issues found.

The implementation is clean, additive, and well-tested. A few minor observations:

  • ⚠️ containers: [] persists as empty array in MongoDB → When a PUT sends containers: [] to clear containers, the DB stores [] but reads back as absent (because convertToExternalDashboard only emits the field when length > 0). This is functionally correct from the API perspective but leaves a stale [] in the DB. A $unset when the array is empty would be cleaner, though this is not a bug.

  • ℹ️ Double iteration over containers in buildDashboardBodySchema → The uniqueness check for container IDs and the tab ID uniqueness check are two separate forEach loops. These could be merged into one pass. Minor efficiency nit, not a correctness issue.

  • Validation is thorough: unknown containerId, unknown tabId, tabId without containerId, duplicate container IDs, duplicate tab IDs within a container — all covered with tests.

  • Both code paths handled: the new config-format path (convertTileToExternalChart/convertToInternalTileConfig) and the legacy series-format path (translateExternalChartToTileConfig in externalApi.ts) both propagate containerId/tabId.

  • Projections updated in both GET / and GET /:id; findOneAndUpdate in PUT uses { new: true } without a restricting projection so the returned document includes containers.

  • Test coverage: 8 integration tests covering round-trips, backward compat, and all validation rejection cases. 86/86 existing tests unaffected.

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

github-actions Bot commented May 5, 2026

E2E Test Results

All tests passed • 159 passed • 3 skipped • 1168s

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

Tests ran across 4 shards in parallel.

View full report →

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

External Dashboards API: expose container + tab fields

1 participant