The iframe-based 'panel' widget didn't work: the browser couldn't
authenticate against the OIDC-protected Grafana (Authentik), and
iframes can't carry Bearer tokens or share cross-origin session
cookies. Result: blank iframe or login redirect.
Replace it with a 'chart' widget that queries Grafana's datasource
API server-side:
Backend (GrafanaWidgetSource): POSTs to /api/ds/query with the stored
api_key (which bypasses OIDC), using the widget's configured PromQL
query, datasource_uid, time range, and resolution. Normalizes Grafana's
frame-based response into a simple {series: [{label, points: [{t, v}]}]}
shape. The api_key is never exposed to the browser.
Frontend (GrafanaChartWidget): renders the series data as a recharts
LineChart with dark-mode-aware colors (Tailwind --chart-* tokens),
responsive container, custom tooltip, and per-series lines. Loading
skeleton, error Alert, and empty state. recharts ^3.9.2 added.
The 'link' widget kind (deep-link URL) is unchanged. The 'panel' kind
and GrafanaPanelWidget are fully removed.
Backend: 279 tests pass (+1 net: -2 panel + 3 chart). Frontend: 127
tests pass (net 0: -3 panel + 3 chart). Lint/build green both sides.
6.9 KiB
Grafana Chart Widget — worker output
Files changed (10 files, ~400 lines)
| File | Status | Lines |
|---|---|---|
backend/src/media_library_viewer_api/integrations/grafana.py |
modified | +12/-12 (panel→chart config + kind) |
backend/src/media_library_viewer_api/widgets/sources.py |
modified | +70/-12 (chart query adapter replaces panel URL logic) |
backend/tests/test_widgets.py |
modified | +55/-20 (3 new chart tests replace 2 panel tests) |
backend/tests/test_services.py |
modified | +2/-2 (grafana widget-kind + API-metadata assertions) |
frontend/src/widgets/GrafanaChartWidget.tsx |
new | 100 |
frontend/src/widgets/__tests__/GrafanaChartWidget.test.tsx |
new | 57 |
frontend/src/widgets/GrafanaPanelWidget.tsx |
deleted | -50 |
frontend/src/widgets/__tests__/GrafanaPanelWidget.test.tsx |
deleted | -72 |
frontend/src/integrations/registry.ts |
modified | +24/-14 (chart binding replaces panel) |
frontend/src/integrations/registry.test.ts |
modified | +1/-1 (panel→chart) |
frontend/src/widgets/index.ts |
modified | +1/-0 (export GrafanaChartWidget) |
frontend/package.json + package-lock.json |
modified | +1 dep (recharts ^3.9.2) |
recharts version installed: ^3.9.2
Grafana /api/ds/query request/response shape
Request (POST):
{
"queries": [{
"datasource": {"uid": "prometheus", "type": "prometheus"},
"expr": "rate(cpu[5m])",
"format": "time_series",
"intervalMs": 30000,
"maxDataPoints": 100,
"refId": "A"
}],
"from": "now-1h",
"to": "now"
}
Headers: Authorization: Bearer {api_key}, Content-Type: application/json
Response (abbreviated):
{
"results": {
"A": {
"frames": [{
"data": { "values": [[1000, 2000], [0.5, 0.8]] },
"schema": { "fields": [{"name":"Time"}, {"name":"cpu_usage"}] }
}]
}
}
}
Series normalization logic
Iterates results[*].frames[]. For each frame with values having >=2 arrays (timestamps + values), extracts the series label from schema.fields[-1].name and zips timestamps+values into [{t: int, v: float|null}]. Returns {"series": [{"label": "...", "points": [...]}]}.
Frontend chart rendering
GrafanaChartWidget fetches widget data, extracts data.series, merges all series by timestamp into a single recharts data array ([{time, cpu_usage: 0.5, mem: 0.3}, ...]), and renders a <LineChart> with one <Line> per series. Uses Tailwind CSS variables (--chart-1 through --chart-5) for colors so it respects dark mode. Includes loading skeleton, error Alert, and empty-state Alert.
Validation
cd backend && .venv/bin/ruff check . && .venv/bin/python -m pytest → 279 passed, ruff clean
cd frontend && npm run lint && npm run build && npm run test → 127 passed, lint/build clean
Deviations
- No deviations from spec. The
linkwidget kind is unchanged. Thepanelkind is fully replaced bychart. - recharts
labelFormattertype workaround. Recharts 3.x typeslabelFormatteras(label: ReactNode, ...) => ReactNode, not(number) => string. Wrapped with(label) => formatTime(Number(label))to satisfy TS strict.
skill_resolution
none — no project/user SKILL.md paths were injected; no .atl/skill-registry.md found.
Residual risks
- The chart widget assumes the Grafana datasource is Prometheus-type (hardcoded
"type": "prometheus"in the query body). If the user has a non-Prometheus datasource (InfluxDB, etc.), the query body format may need adjustment. Thedatasource_uidis configurable but thetypeis not. - recharts is ~45KB gzipped added to the bundle.
{
"criteriaSatisfied": [
{
"id": "criterion-1",
"status": "satisfied",
"evidence": "Replaces the broken iframe panel widget with a server-side chart query widget. Backend queries /api/ds/query with stored api_key; frontend renders recharts LineChart. No iframe, no browser auth, no CORS. The link widget kind is unchanged. 279 backend + 127 frontend tests pass; lint/build green both sides."
}
],
"changedFiles": [
"backend/src/media_library_viewer_api/integrations/grafana.py",
"backend/src/media_library_viewer_api/widgets/sources.py",
"backend/tests/test_widgets.py",
"backend/tests/test_services.py",
"frontend/src/widgets/GrafanaChartWidget.tsx",
"frontend/src/widgets/__tests__/GrafanaChartWidget.test.tsx",
"frontend/src/widgets/GrafanaPanelWidget.tsx (deleted)",
"frontend/src/widgets/__tests__/GrafanaPanelWidget.test.tsx (deleted)",
"frontend/src/integrations/registry.ts",
"frontend/src/integrations/registry.test.ts",
"frontend/src/widgets/index.ts",
"frontend/package.json"
],
"testsAddedOrUpdated": [
"backend/tests/test_widgets.py",
"backend/tests/test_services.py",
"frontend/src/widgets/__tests__/GrafanaChartWidget.test.tsx",
"frontend/src/integrations/registry.test.ts"
],
"commandsRun": [
{ "command": "cd backend && .venv/bin/ruff check .", "result": "passed", "summary": "All checks passed" },
{ "command": "cd backend && .venv/bin/python -m pytest tests/ -q", "result": "passed", "summary": "279 passed, 2 pre-existing warnings" },
{ "command": "cd frontend && npm run lint", "result": "passed", "summary": "0 errors, 0 warnings" },
{ "command": "cd frontend && npm run build", "result": "passed", "summary": "tsc + vite build clean" },
{ "command": "cd frontend && npm run test", "result": "passed", "summary": "39 files / 127 tests passed" }
],
"validationOutput": [
"Backend ruff clean; 279 tests pass (was 278; -2 panel + 3 chart = +1 net).",
"Frontend eslint clean; tsc + vite build clean; 127 tests pass (-3 panel + 3 chart = net 0).",
"GrafanaWidgetSource._fetch_chart POSTs to /api/ds/query with Bearer token; normalizes response to {series:[{label,points}]}",
"GrafanaChartWidget renders recharts LineChart with dark-mode CSS variable colors.",
"link widget kind unchanged; panel widget kind fully removed."
],
"residualRisks": [
"Chart query body hardcodes datasource type 'prometheus' — non-Prometheus datasources (InfluxDB etc.) may need a type field on the config.",
"recharts adds ~45KB gzipped to the frontend bundle."
],
"noStagedFiles": true,
"diffSummary": "~400 lines: replaces Grafana panel iframe widget with server-side datasource-query chart widget. Backend: /api/ds/query POST with api_key + series normalization (70 lines). Frontend: recharts LineChart component with dark-mode support (100 lines). 3 backend + 3 frontend tests. recharts ^3.9.2 installed.",
"reviewFindings": [
"no blockers"
],
"manualNotes": "recharts labelFormatter type workaround: recharts 3.x types it as (ReactNode) => ReactNode, not (number) => string. Wrapped with Number() cast. The link widget kind is fully preserved. The panel widget kind and all its code/tests are fully deleted."
}