Add Dashboards UI: PromQL-backed bar/line/pie panels saved per profile (ticket 014)

Backend: replace Profile.dashboards' opaque JSON stub with typed
Dashboard/Panel structs, add ProfileStore CRUD (create/save/list/delete
dashboard) and matching /api/settings/profiles/:name/dashboards[/:id] routes,
with tests covering the HTTP round trip and persistence across a simulated
restart.

Frontend: add recharts (lightweight, React 19-compatible) and a Dashboards
component with a dashboard builder (create dashboards, add/remove/edit panels
with a PromQL query + chart type), live per-panel previews that respect the
active time range and show malformed-query errors inline instead of crashing,
and load/save wired to the new profile-backed persistence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 13:29:45 +01:00
co-authored by Claude Sonnet 5
parent ae21a7a0ce
commit 41362aa3d8
9 changed files with 1445 additions and 15 deletions
+66
View File
@@ -98,6 +98,29 @@ export interface ProfileSummary {
dashboard_count: number
}
/// One chart-ready time series returned by `GET /api/query` (ticket 013):
/// a label combination plus its `(timestamp_ms, value)` points, sorted by
/// timestamp. See `PromqlSeriesDto` on the backend.
export interface PromqlSeries {
labels: Record<string, string>
points: [number, number][]
}
export type ChartType = 'bar' | 'line' | 'pie'
export interface Panel {
id: string
title: string
promql: string
chart_type: ChartType
}
export interface Dashboard {
id: string
name: string
panels: Panel[]
}
async function getJson<T>(path: string): Promise<T> {
const res = await fetch(path)
if (!res.ok) {
@@ -265,3 +288,46 @@ export function deleteProfile(name: string): Promise<void> {
export function activateProfile(name: string): Promise<Settings> {
return sendJson(`/api/settings/profiles/${encodeURIComponent(name)}/activate`, 'POST')
}
/// Evaluates a PromQL query (ticket 013) via `GET /api/query`. Throws on a
/// malformed query (`400` with `{ "error": "..." }`), so callers should catch
/// and surface the message inline (e.g. per-panel) rather than letting it
/// crash the page.
export function fetchPromqlSeries(params: {
promql: string
startMs?: number
endMs?: number
}): Promise<PromqlSeries[]> {
const search = new URLSearchParams()
search.set('promql', params.promql)
if (params.startMs !== undefined) search.set('start_ms', String(params.startMs))
if (params.endMs !== undefined) search.set('end_ms', String(params.endMs))
return getJson(`/api/query?${search.toString()}`)
}
export function fetchDashboards(profileName: string): Promise<Dashboard[]> {
return getJson(`/api/settings/profiles/${encodeURIComponent(profileName)}/dashboards`)
}
export function createDashboard(profileName: string, name: string): Promise<Dashboard> {
return sendJson(`/api/settings/profiles/${encodeURIComponent(profileName)}/dashboards`, 'POST', { name })
}
export function saveDashboard(
profileName: string,
dashboardId: string,
update: { name: string; panels: Panel[] },
): Promise<Dashboard> {
return sendJson(
`/api/settings/profiles/${encodeURIComponent(profileName)}/dashboards/${encodeURIComponent(dashboardId)}`,
'PUT',
update,
)
}
export function deleteDashboard(profileName: string, dashboardId: string): Promise<void> {
return sendJson(
`/api/settings/profiles/${encodeURIComponent(profileName)}/dashboards/${encodeURIComponent(dashboardId)}`,
'DELETE',
)
}