From 6469d73dbfbe886b7aaa5d93417ea8c7cf30f81e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 10 May 2026 07:37:51 +0000 Subject: [PATCH] Add description field to dashboards - Add optional description field to DashboardSchema in common-utils - Add description to Mongoose dashboard model - Add description to frontend Dashboard type - Include description in external API v2 (create, update, get, list) - Include description in MCP tools (get, save) - Add editable description below dashboard name on detail page - Show description on dashboard listing page (grid & list cards) - Add description field to dashboard import form - Include description in dashboard export/import template flow - Update OpenAPI documentation for description field Co-authored-by: Mike Shi --- packages/api/openapi.json | 18 +++ .../src/mcp/tools/dashboards/getDashboard.ts | 1 + .../src/mcp/tools/dashboards/saveDashboard.ts | 28 +++- packages/api/src/models/dashboard.ts | 1 + .../src/routers/external-api/v2/dashboards.ts | 23 ++++ .../external-api/v2/utils/dashboards.ts | 3 + packages/app/src/DBDashboardImportPage.tsx | 14 ++ packages/app/src/DBDashboardPage.tsx | 130 ++++++++++++++++-- .../Dashboards/DashboardsListPage.tsx | 13 +- packages/app/src/dashboard.ts | 1 + packages/common-utils/src/core/utils.ts | 2 + packages/common-utils/src/types.ts | 1 + 12 files changed, 218 insertions(+), 17 deletions(-) diff --git a/packages/api/openapi.json b/packages/api/openapi.json index 265b09de1b..bfa3dd056a 100644 --- a/packages/api/openapi.json +++ b/packages/api/openapi.json @@ -2011,6 +2011,12 @@ "maxLength": 1024, "example": "Service Overview" }, + "description": { + "type": "string", + "description": "Optional description of the dashboard", + "maxLength": 40000, + "example": "Monitors key service health metrics including latency and error rates" + }, "tiles": { "type": "array", "description": "List of tiles/charts in the dashboard", @@ -2073,6 +2079,12 @@ "description": "Dashboard name.", "example": "New Dashboard" }, + "description": { + "type": "string", + "maxLength": 40000, + "description": "Optional description of the dashboard.", + "example": "Monitors key service health metrics" + }, "tiles": { "type": "array", "description": "List of tiles/charts to include in the dashboard.", @@ -2134,6 +2146,12 @@ "description": "Dashboard name.", "example": "Updated Dashboard Name" }, + "description": { + "type": "string", + "maxLength": 40000, + "description": "Optional description of the dashboard.", + "example": "Updated description for this dashboard" + }, "tiles": { "type": "array", "items": { diff --git a/packages/api/src/mcp/tools/dashboards/getDashboard.ts b/packages/api/src/mcp/tools/dashboards/getDashboard.ts index ee9b2d73a8..58391d507d 100644 --- a/packages/api/src/mcp/tools/dashboards/getDashboard.ts +++ b/packages/api/src/mcp/tools/dashboards/getDashboard.ts @@ -41,6 +41,7 @@ export function registerGetDashboard( const output = dashboards.map(d => ({ id: d._id.toString(), name: d.name, + ...(d.description ? { description: d.description } : {}), tags: d.tags, ...(frontendUrl ? { url: `${frontendUrl}/dashboards/${d._id}` } : {}), })); diff --git a/packages/api/src/mcp/tools/dashboards/saveDashboard.ts b/packages/api/src/mcp/tools/dashboards/saveDashboard.ts index c7ce7ef5cf..42b47361de 100644 --- a/packages/api/src/mcp/tools/dashboards/saveDashboard.ts +++ b/packages/api/src/mcp/tools/dashboards/saveDashboard.ts @@ -47,6 +47,11 @@ export function registerSaveDashboard( 'Dashboard ID. Omit to create a new dashboard, provide to update an existing one.', ), name: z.string().describe('Dashboard name'), + description: z + .string() + .max(40000) + .optional() + .describe('Optional description of the dashboard'), tiles: mcpTilesParam, tags: z.array(z.string()).optional().describe('Dashboard tags'), }), @@ -54,12 +59,19 @@ export function registerSaveDashboard( withToolTracing( 'hyperdx_save_dashboard', context, - async ({ id: dashboardId, name, tiles: inputTiles, tags }) => { + async ({ + id: dashboardId, + name, + description, + tiles: inputTiles, + tags, + }) => { if (!dashboardId) { return createDashboard({ teamId, frontendUrl, name, + description, inputTiles, tags, }); @@ -69,6 +81,7 @@ export function registerSaveDashboard( frontendUrl, dashboardId, name, + description, inputTiles, tags, }); @@ -83,17 +96,20 @@ async function createDashboard({ teamId, frontendUrl, name, + description, inputTiles, tags, }: { teamId: string; frontendUrl: string | undefined; name: string; + description: string | undefined; inputTiles: unknown[]; tags: string[] | undefined; }) { const parsed = createDashboardBodySchema.safeParse({ name, + description, tiles: inputTiles, tags, }); @@ -149,6 +165,9 @@ async function createDashboard({ const newDashboard = await new Dashboard({ name: parsed.data.name, + ...(parsed.data.description !== undefined + ? { description: parsed.data.description } + : {}), tiles: internalTiles, tags: tags && uniq(tags), filters: filtersWithIds, @@ -184,6 +203,7 @@ async function updateDashboard({ frontendUrl, dashboardId, name, + description, inputTiles, tags, }: { @@ -191,6 +211,7 @@ async function updateDashboard({ frontendUrl: string | undefined; dashboardId: string; name: string; + description: string | undefined; inputTiles: unknown[]; tags: string[] | undefined; }) { @@ -203,6 +224,7 @@ async function updateDashboard({ const parsed = updateDashboardBodySchema.safeParse({ name, + description, tiles: inputTiles, tags, }); @@ -278,6 +300,10 @@ async function updateDashboard({ tags: tags && uniq(tags), }; + if (description !== undefined) { + setPayload.description = description; + } + if (filters !== undefined) { setPayload.filters = convertExternalFiltersToInternal( filters, diff --git a/packages/api/src/models/dashboard.ts b/packages/api/src/models/dashboard.ts index fa24833cae..c8320b1cbe 100644 --- a/packages/api/src/models/dashboard.ts +++ b/packages/api/src/models/dashboard.ts @@ -23,6 +23,7 @@ export default mongoose.model( type: String, required: true, }, + description: { type: String, required: false }, tiles: { type: mongoose.Schema.Types.Mixed, required: true }, team: { type: mongoose.Schema.Types.ObjectId, ref: 'Team' }, tags: { diff --git a/packages/api/src/routers/external-api/v2/dashboards.ts b/packages/api/src/routers/external-api/v2/dashboards.ts index 22dcdd9e78..215411ada2 100644 --- a/packages/api/src/routers/external-api/v2/dashboards.ts +++ b/packages/api/src/routers/external-api/v2/dashboards.ts @@ -1073,6 +1073,11 @@ async function getSourceConnectionMismatches( * description: Dashboard name * maxLength: 1024 * example: "Service Overview" + * description: + * type: string + * description: Optional description of the dashboard + * maxLength: 40000 + * example: "Monitors key service health metrics including latency and error rates" * tiles: * type: array * description: List of tiles/charts in the dashboard @@ -1119,6 +1124,11 @@ async function getSourceConnectionMismatches( * maxLength: 1024 * description: Dashboard name. * example: "New Dashboard" + * description: + * type: string + * maxLength: 40000 + * description: Optional description of the dashboard. + * example: "Monitors key service health metrics" * tiles: * type: array * description: List of tiles/charts to include in the dashboard. @@ -1165,6 +1175,11 @@ async function getSourceConnectionMismatches( * maxLength: 1024 * description: Dashboard name. * example: "Updated Dashboard Name" + * description: + * type: string + * maxLength: 40000 + * description: Optional description of the dashboard. + * example: "Updated description for this dashboard" * tiles: * type: array * items: @@ -1305,6 +1320,7 @@ router.get('/', async (req, res, next) => { { _id: 1, name: 1, + description: 1, tiles: 1, tags: 1, filters: 1, @@ -1422,6 +1438,7 @@ router.get( { _id: 1, name: 1, + description: 1, tiles: 1, tags: 1, filters: 1, @@ -1589,6 +1606,7 @@ router.post( const { name, + description, tiles, tags, filters, @@ -1635,6 +1653,7 @@ router.post( const newDashboard = await new Dashboard({ name, + ...(description !== undefined ? { description } : {}), tiles: internalTiles, tags: tags && uniq(tags), filters: filtersWithIds, @@ -1812,6 +1831,7 @@ router.put( const { name, + description, tiles, tags, filters, @@ -1869,6 +1889,9 @@ router.put( tiles: internalTiles, tags: tags && uniq(tags), }; + if (description !== undefined) { + setPayload.description = description; + } if (filters !== undefined) { setPayload.filters = convertExternalFiltersToInternal( filters, diff --git a/packages/api/src/routers/external-api/v2/utils/dashboards.ts b/packages/api/src/routers/external-api/v2/utils/dashboards.ts index 504cf85cbe..660ce47325 100644 --- a/packages/api/src/routers/external-api/v2/utils/dashboards.ts +++ b/packages/api/src/routers/external-api/v2/utils/dashboards.ts @@ -71,6 +71,7 @@ export function isConfigTile( export type ExternalDashboard = { id: string; name: string; + description?: string; tiles: ExternalDashboardTileWithId[]; tags?: string[]; filters?: ExternalDashboardFilterWithId[]; @@ -310,6 +311,7 @@ export function convertToExternalDashboard( return { id: dashboard._id.toString(), name: dashboard.name, + ...(dashboard.description ? { description: dashboard.description } : {}), tiles: dashboard.tiles .map(convertTileToExternalChart) .filter(t => t !== undefined), @@ -576,6 +578,7 @@ export function resolveSavedQueryLanguage(params: { const dashboardBodyBaseShape = { name: z.string().max(1024), + description: z.string().max(40000).optional(), tiles: externalDashboardTileListSchema, tags: tagsSchema, savedQuery: z.string().nullable().optional(), diff --git a/packages/app/src/DBDashboardImportPage.tsx b/packages/app/src/DBDashboardImportPage.tsx index 5534928126..864cb21213 100644 --- a/packages/app/src/DBDashboardImportPage.tsx +++ b/packages/app/src/DBDashboardImportPage.tsx @@ -201,6 +201,7 @@ function FileSelection({ const MappingForm = z.object({ dashboardName: z.string().min(1), + dashboardDescription: z.string().max(40000).optional().default(''), tags: z.array(z.string()), sourceMappings: z.array(z.string()), connectionMappings: z.array(z.string()), @@ -221,6 +222,7 @@ function Mapping({ input }: { input: DashboardTemplate }) { resolver: zodResolver(MappingForm), defaultValues: { dashboardName: input.name, + dashboardDescription: input.description ?? '', tags: input.tags ?? [], sourceMappings: input.tiles.map(() => ''), connectionMappings: input.tiles.map(() => ''), @@ -397,6 +399,7 @@ function Mapping({ input }: { input: DashboardTemplate }) { tiles: zippedTiles, filters: zippedFilters, name: data.dashboardName, + description: data.dashboardDescription || undefined, tags: data.tags, }); let _dashboardId = dashboardId; @@ -440,6 +443,17 @@ function Mapping({ input }: { input: DashboardTemplate }) { /> )} /> + ( + + )} + /> void; +}) { + const [editing, setEditing] = useState(false); + const [editedDescription, setEditedDescription] = useState(description); + const { hovered, ref } = useHover(); + + const cancelEditing = () => { + setEditedDescription(description); + setEditing(false); + }; + + if (editing) { + return ( + +
{ + e.preventDefault(); + onSave(editedDescription.trim()); + setEditing(false); + }} + onKeyDown={e => { + if (e.key === 'Escape') { + cancelEditing(); + } + }} + onBlur={e => { + if (!e.currentTarget.contains(e.relatedTarget)) { + cancelEditing(); + } + }} + > +