feat(prometheus-direct-charting): slice 1 — prom range query + chart rebrand
Add PrometheusWidgetSource._fetch_chart hitting /api/v1/query_range directly (SC-101..104). New shared helpers in widgets/prometheus_range.py: step_for_window (window preset -> step, ~200pts) and normalize_prometheus_matrix (extracted label/dedup rule, retargeted at Prom matrix, robust to malformed data). Chart widget kind moved grafana->prometheus in both registries; GrafanaChartWidget renamed -> PrometheusChartWidget (git mv, recharts body preserved). Grafana binding/service untouched (removed in slice 3). Backend: 298 pytest pass, ruff clean. Frontend: 128 vitest pass, build+lint green.
This commit is contained in:
@@ -23,6 +23,9 @@ describe("service registry", () => {
|
||||
it("binds widget kinds per service", () => {
|
||||
expect(SERVICE_REGISTRY.grafana.widgets.map((w) => w.kind)).toEqual([
|
||||
"link",
|
||||
]);
|
||||
expect(SERVICE_REGISTRY.prometheus.widgets.map((w) => w.kind)).toEqual([
|
||||
"metric",
|
||||
"chart",
|
||||
]);
|
||||
expect(SERVICE_REGISTRY.alertmanager.widgets.map((w) => w.kind)).toEqual([
|
||||
|
||||
@@ -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 { GrafanaChartWidget } from "../widgets/GrafanaChartWidget";
|
||||
import { PrometheusChartWidget } from "../widgets/PrometheusChartWidget";
|
||||
import { JellyfinWidget } from "../widgets/JellyfinWidget";
|
||||
import { JellyfinNowPlayingWidget } from "../widgets/JellyfinNowPlayingWidget";
|
||||
import { PrometheusMetricWidget } from "../widgets/PrometheusMetricWidget";
|
||||
@@ -88,39 +88,6 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
||||
},
|
||||
component: GrafanaLinkWidget,
|
||||
},
|
||||
{
|
||||
kind: "chart",
|
||||
name: "Chart",
|
||||
description: "Live time-series chart from a Grafana datasource query.",
|
||||
refreshIntervalMs: 60_000,
|
||||
defaultConfig: {
|
||||
datasource_uid: "prometheus",
|
||||
query: "",
|
||||
from_ts: "now-1h",
|
||||
to_ts: "now",
|
||||
interval_ms: 30_000,
|
||||
max_data_points: 100,
|
||||
},
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
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: ["query"],
|
||||
},
|
||||
component: GrafanaChartWidget,
|
||||
},
|
||||
],
|
||||
},
|
||||
prometheus: {
|
||||
@@ -141,6 +108,28 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
||||
},
|
||||
component: PrometheusMetricWidget,
|
||||
},
|
||||
{
|
||||
kind: "chart",
|
||||
name: "Chart",
|
||||
description: "Multi-series line chart from a PromQL range query.",
|
||||
refreshIntervalMs: 60_000,
|
||||
defaultConfig: { promql: "", window: "1h" },
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
promql: {
|
||||
type: "string",
|
||||
description: "PromQL range query expression",
|
||||
},
|
||||
window: {
|
||||
type: "string",
|
||||
description: "Time window preset (1h, 6h, 24h, 7d)",
|
||||
},
|
||||
},
|
||||
required: ["promql"],
|
||||
},
|
||||
component: PrometheusChartWidget,
|
||||
},
|
||||
],
|
||||
},
|
||||
jellyfin: {
|
||||
|
||||
+2
-2
@@ -59,7 +59,7 @@ const CHART_COLORS = [
|
||||
"var(--color-chart-5)",
|
||||
];
|
||||
|
||||
export function GrafanaChartWidget({
|
||||
export function PrometheusChartWidget({
|
||||
widget,
|
||||
refreshIntervalMs,
|
||||
description,
|
||||
@@ -111,7 +111,7 @@ export function GrafanaChartWidget({
|
||||
) : (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
No data. Check your query and datasource_uid in the widget config.
|
||||
No data. Check your PromQL query and window in the widget config.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { GrafanaChartWidget } from "../GrafanaChartWidget";
|
||||
import { PrometheusChartWidget } from "../PrometheusChartWidget";
|
||||
import type { WidgetInstance } from "../../types";
|
||||
import * as useWidgets from "../../hooks/useWidgets";
|
||||
|
||||
@@ -29,7 +29,7 @@ function mockData(data: unknown, error?: string) {
|
||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||
}
|
||||
|
||||
describe("GrafanaChartWidget", () => {
|
||||
describe("PrometheusChartWidget", () => {
|
||||
it("renders a chart with series data", () => {
|
||||
mockData({
|
||||
series: [
|
||||
@@ -42,20 +42,20 @@ describe("GrafanaChartWidget", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
render(<GrafanaChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
// recharts renders an SVG; the title from SectionCard should be present.
|
||||
expect(screen.getByText("CPU Usage")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows error Alert on error", () => {
|
||||
mockData(null, "Grafana api_key is required for chart queries");
|
||||
render(<GrafanaChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(screen.getByText(/api_key is required/i)).toBeInTheDocument();
|
||||
mockData(null, "promql is required");
|
||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(screen.getByText(/promql is required/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows empty state when no series", () => {
|
||||
mockData({ series: [] });
|
||||
render(<GrafanaChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
export { AlertmanagerAlertsWidget } from "./AlertmanagerAlertsWidget";
|
||||
export { BackupsWidget } from "./BackupsWidget";
|
||||
export { GrafanaLinkWidget } from "./GrafanaLinkWidget";
|
||||
export { GrafanaChartWidget } from "./GrafanaChartWidget";
|
||||
export { PrometheusChartWidget } from "./PrometheusChartWidget";
|
||||
export { JellyfinWidget } from "./JellyfinWidget";
|
||||
export { PrometheusMetricWidget } from "./PrometheusMetricWidget";
|
||||
export { SshTaskWidget } from "./SshTaskWidget";
|
||||
|
||||
Reference in New Issue
Block a user