Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/hdx-2150-deep-review-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@hyperdx/api': patch
'@hyperdx/common-utils': patch
---

External Dashboards API: tighten validation around container/tab references
on the v2 dashboards routes.

- Cap tile `containerId` and `tabId` at 256 characters to mirror the
internal `DashboardContainer` schema and the `DASHBOARD_CONTAINER_ID_MAX`
constant, now exported from `@hyperdx/common-utils`.
- Cap a single dashboard payload at 500 tiles via the new
`DASHBOARD_MAX_TILES` constant to keep one request from pushing tens of
MB into Mongo.
- Treat empty-string `containerId` / `tabId` on legacy Mongo docs as
absent on read, so dashboards predating the containers feature still
round-trip through the external schema's `min(1)` cap.
- Extract the cross-tile container/tab consistency check into a shared
`validateDashboardContainersConsistency` helper so the canonical
schema and the request body schema agree on what a valid payload is.
- OpenAPI now publishes the matching `maxLength` and `maxItems` bounds
on `DashboardContainer.id`, `DashboardContainerTab.id`, the
`containers` array, and the request `tiles` array.
32 changes: 32 additions & 0 deletions .changeset/hdx-2150-deep-review-roundtrip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@hyperdx/api': patch
'@hyperdx/common-utils': patch
---

External Dashboards API: fix `PUT` round-trip when the request body omits
`containers`, and self-heal orphan `containerId` / `tabId` references on
read.

- Move tile-level container/tab reference resolution out of the request
body schema and into the `POST` and `PUT` handlers, so a `PUT` whose
body omits `containers` validates tile refs against the existing
dashboard's containers (the documented "preserve on omit" branch)
rather than against an empty fallback. Without this, a `PUT` that
changes only `tiles` while keeping a tile homed in a real preserved
container was rejected with `Tile references unknown containerId`.
- Split the shared validation helper into a structure-only pass
(`validateDashboardContainersStructure`) and a tile-ref pass
(`validateDashboardTileContainerRefs`) on
`@hyperdx/common-utils`. The composite
`validateDashboardContainersConsistency` now wraps both, so existing
callers keep their current behavior.
- On read, drop `tile.containerId` / `tile.tabId` when the ref does not
resolve to a container (or tab) in the same dashboard. A pre-existing
doc with an orphan ref now round-trips on `GET` as if the ref were
absent, so the next `PUT` validates instead of failing with
`Tile references unknown containerId`. Each drop is logged with the
dashboard id, tile id, and the offending ref.
- Document in the OpenAPI `PUT /api/v2/dashboards/{id}` description that
the endpoint does not support optimistic concurrency. Concurrent PUTs
may silently overwrite each other; clients should serialize edits to
a given dashboard.
13 changes: 13 additions & 0 deletions .changeset/hdx-2150-external-api-containers-tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@hyperdx/api': minor
---

External Dashboards API now round-trips the new dashboard organization
layer added in #2015: `containers` on the dashboard, optional `tabs` on each
container, and `containerId` / `tabId` on each tile. Create, get, list, and
update all preserve the structure. The body validates that tile
`containerId` references resolve to a real container, that tile `tabId`
references resolve to a tab inside that container, and that tab ids are
unique within a container. Container id uniqueness is already enforced by
the shared schema. Dashboards saved without `containers` round-trip
unchanged.
114 changes: 113 additions & 1 deletion packages/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1809,6 +1809,78 @@
}
}
},
"DashboardContainerTab": {
"type": "object",
"description": "A single tab inside a dashboard container. Tiles join a tab via tabId.",
"required": [
"id",
"title"
],
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Unique identifier for the tab within its container.",
"example": "errors"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Display title for the tab.",
"example": "Errors"
}
}
},
"DashboardContainer": {
"type": "object",
"description": "A grouping container for tiles on a dashboard. Tiles join a container via containerId.",
"required": [
"id",
"title",
"collapsed"
],
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Unique identifier for the container within the dashboard.",
"example": "service-health"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Display title for the container.",
"example": "Service Health"
},
"collapsed": {
"type": "boolean",
"description": "Persisted default collapse state. Per-viewer state lives in the URL.",
"example": false
},
"collapsible": {
"type": "boolean",
"description": "Whether the user can collapse the group. Defaults to true.",
"example": true
},
"bordered": {
"type": "boolean",
"description": "Whether to show a visual border around the group. Defaults to true.",
"example": true
},
"tabs": {
"type": "array",
"description": "Optional tabs. 2+ entries renders a tab bar; 0-1 entries renders a plain group header. Tiles join a tab via tabId.",
"maxItems": 20,
"items": {
"$ref": "#/components/schemas/DashboardContainerTab"
}
}
}
},
"TileBase": {
"type": "object",
"description": "Common fields shared by tile input and output",
Expand Down Expand Up @@ -1854,6 +1926,20 @@
"config": {
"$ref": "#/components/schemas/TileConfig",
"description": "Chart configuration for the tile. The displayType field determines which variant is used. Replaces the deprecated \"series\" and \"asRatio\" fields."
},
"containerId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "References a DashboardContainer by id. Tiles without containerId render in the default ungrouped area.",
"example": "service-health"
},
"tabId": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "References a tab inside the tile's container by id. Requires containerId to be set, and the container to declare a matching tab.",
"example": "errors"
}
}
},
Expand Down Expand Up @@ -2057,6 +2143,14 @@
"items": {
"$ref": "#/components/schemas/SavedFilterValue"
}
},
"containers": {
"type": "array",
"description": "Optional grouping containers. Each tile may join a container via tile.containerId, and a tab inside it via tile.tabId.",
"maxItems": 50,
"items": {
"$ref": "#/components/schemas/DashboardContainer"
}
}
}
},
Expand All @@ -2076,6 +2170,7 @@
"tiles": {
"type": "array",
"description": "List of tiles/charts to include in the dashboard.",
"maxItems": 500,
"items": {
"$ref": "#/components/schemas/TileInput"
}
Expand Down Expand Up @@ -2118,6 +2213,14 @@
"items": {
"$ref": "#/components/schemas/SavedFilterValue"
}
},
"containers": {
"type": "array",
"description": "Optional grouping containers. Each tile may join a container via tile.containerId, and a tab inside it via tile.tabId.",
"maxItems": 50,
"items": {
"$ref": "#/components/schemas/DashboardContainer"
}
}
}
},
Expand All @@ -2136,6 +2239,7 @@
},
"tiles": {
"type": "array",
"maxItems": 500,
"items": {
"$ref": "#/components/schemas/TileInput"
},
Expand Down Expand Up @@ -2180,6 +2284,14 @@
"items": {
"$ref": "#/components/schemas/SavedFilterValue"
}
},
"containers": {
"type": "array",
"description": "Optional grouping containers. Each tile may join a container via tile.containerId, and a tab inside it via tile.tabId.",
"maxItems": 50,
"items": {
"$ref": "#/components/schemas/DashboardContainer"
}
}
}
},
Expand Down Expand Up @@ -4170,7 +4282,7 @@
},
"put": {
"summary": "Update Dashboard",
"description": "Updates an existing dashboard",
"description": "Updates an existing dashboard.\n\n**Concurrency:** This endpoint does not support optimistic\nconcurrency control. Concurrent PUT requests for the same\ndashboard may silently overwrite each other, which can leave\norphan tile-to-container references on layout-shape edits.\nClients should serialize edits to a given dashboard.\n",
"operationId": "updateDashboard",
"tags": [
"Dashboards"
Expand Down
Loading
Loading