Replace Grafana iframe panel with server-side chart widget
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.
This commit is contained in:
@@ -2,7 +2,7 @@ import type { ComponentType } from "react";
|
||||
import { AlertmanagerAlertsWidget } from "../widgets/AlertmanagerAlertsWidget";
|
||||
import { BackupsWidget } from "../widgets/BackupsWidget";
|
||||
import { GrafanaLinkWidget } from "../widgets/GrafanaLinkWidget";
|
||||
import { GrafanaPanelWidget } from "../widgets/GrafanaPanelWidget";
|
||||
import { GrafanaChartWidget } from "../widgets/GrafanaChartWidget";
|
||||
import { JellyfinWidget } from "../widgets/JellyfinWidget";
|
||||
import { JellyfinNowPlayingWidget } from "../widgets/JellyfinNowPlayingWidget";
|
||||
import { PrometheusMetricWidget } from "../widgets/PrometheusMetricWidget";
|
||||
@@ -89,27 +89,37 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
||||
component: GrafanaLinkWidget,
|
||||
},
|
||||
{
|
||||
kind: "panel",
|
||||
name: "Panel embed",
|
||||
description: "Embed a Grafana panel directly.",
|
||||
refreshIntervalMs: 0,
|
||||
kind: "chart",
|
||||
name: "Chart",
|
||||
description: "Live time-series chart from a Grafana datasource query.",
|
||||
refreshIntervalMs: 60_000,
|
||||
defaultConfig: {
|
||||
dashboard_uid: "",
|
||||
panel_id: 1,
|
||||
datasource_uid: "prometheus",
|
||||
query: "",
|
||||
from_ts: "now-1h",
|
||||
to_ts: "now",
|
||||
interval_ms: 30_000,
|
||||
max_data_points: 100,
|
||||
},
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
dashboard_uid: { type: "string" },
|
||||
panel_id: { type: "integer" },
|
||||
datasource_uid: {
|
||||
type: "string",
|
||||
description: "Grafana datasource UID (e.g. 'prometheus')",
|
||||
},
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Query expression (e.g. PromQL)",
|
||||
},
|
||||
from_ts: { type: "string" },
|
||||
to_ts: { type: "string" },
|
||||
interval_ms: { type: "integer" },
|
||||
max_data_points: { type: "integer" },
|
||||
},
|
||||
required: ["dashboard_uid", "panel_id"],
|
||||
required: ["query"],
|
||||
},
|
||||
component: GrafanaPanelWidget,
|
||||
component: GrafanaChartWidget,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user