diff --git a/frontend/src/integrations/registry.ts b/frontend/src/integrations/registry.ts index 5fbc1e3..6e138af 100644 --- a/frontend/src/integrations/registry.ts +++ b/frontend/src/integrations/registry.ts @@ -1,9 +1,9 @@ import type { ComponentType } from "react"; import { AlertmanagerAlertsWidget } from "../widgets/AlertmanagerAlertsWidget"; import { BackupsWidget } from "../widgets/BackupsWidget"; -import { PrometheusChartWidget } from "../widgets/PrometheusChartWidget"; -import { PrometheusGaugeWidget } from "../widgets/PrometheusGaugeWidget"; -import { PrometheusMeanWidget } from "../widgets/PrometheusMeanWidget"; +import { MetricChartWidget } from "../widgets/MetricChartWidget"; +import { MetricGaugeWidget } from "../widgets/MetricGaugeWidget"; +import { MetricMeanWidget } from "../widgets/MetricMeanWidget"; import { JellyfinWidget } from "../widgets/JellyfinWidget"; import { JellyfinNowPlayingWidget } from "../widgets/JellyfinNowPlayingWidget"; import { PrometheusMetricWidget } from "../widgets/PrometheusMetricWidget"; @@ -109,7 +109,7 @@ export const SERVICE_REGISTRY: Record = { }, required: ["promql"], }, - component: PrometheusChartWidget, + component: MetricChartWidget, }, { kind: "gauge", @@ -133,7 +133,7 @@ export const SERVICE_REGISTRY: Record = { }, required: ["promql"], }, - component: PrometheusGaugeWidget, + component: MetricGaugeWidget, }, { kind: "mean", @@ -156,7 +156,7 @@ export const SERVICE_REGISTRY: Record = { }, required: ["promql"], }, - component: PrometheusMeanWidget, + component: MetricMeanWidget, }, ], }, diff --git a/frontend/src/widgets/PrometheusChartWidget.tsx b/frontend/src/widgets/MetricChartWidget.tsx similarity index 96% rename from frontend/src/widgets/PrometheusChartWidget.tsx rename to frontend/src/widgets/MetricChartWidget.tsx index d1f54dd..b93234b 100644 --- a/frontend/src/widgets/PrometheusChartWidget.tsx +++ b/frontend/src/widgets/MetricChartWidget.tsx @@ -12,7 +12,7 @@ interface Props { description?: string; } -export function PrometheusChartWidget({ +export function MetricChartWidget({ widget, refreshIntervalMs, description, diff --git a/frontend/src/widgets/PrometheusGaugeWidget.tsx b/frontend/src/widgets/MetricGaugeWidget.tsx similarity index 98% rename from frontend/src/widgets/PrometheusGaugeWidget.tsx rename to frontend/src/widgets/MetricGaugeWidget.tsx index 79a2f39..2130942 100644 --- a/frontend/src/widgets/PrometheusGaugeWidget.tsx +++ b/frontend/src/widgets/MetricGaugeWidget.tsx @@ -37,7 +37,7 @@ function formatValue(value: number, unit?: string | null): string { return unit ? `${formatted} ${unit}` : formatted; } -export function PrometheusGaugeWidget({ +export function MetricGaugeWidget({ widget, refreshIntervalMs, description, diff --git a/frontend/src/widgets/PrometheusMeanWidget.tsx b/frontend/src/widgets/MetricMeanWidget.tsx similarity index 97% rename from frontend/src/widgets/PrometheusMeanWidget.tsx rename to frontend/src/widgets/MetricMeanWidget.tsx index bed8a7a..c02f224 100644 --- a/frontend/src/widgets/PrometheusMeanWidget.tsx +++ b/frontend/src/widgets/MetricMeanWidget.tsx @@ -27,7 +27,7 @@ function formatMean(value: number, unit?: string | null): string { return unit ? `${formatted} ${unit}` : formatted; } -export function PrometheusMeanWidget({ +export function MetricMeanWidget({ widget, refreshIntervalMs, description, diff --git a/frontend/src/widgets/PrometheusMetricWidget.tsx b/frontend/src/widgets/PrometheusMetricWidget.tsx index dc617a2..93b1040 100644 --- a/frontend/src/widgets/PrometheusMetricWidget.tsx +++ b/frontend/src/widgets/PrometheusMetricWidget.tsx @@ -10,30 +10,28 @@ interface Props { description?: string; } -type PromQLResult = { - resultType?: string; - result?: unknown; +type SeriesPoint = { + t: number; + v: number | null; }; -type PromQLVectorSample = { - metric?: Record; - value?: [number, string]; +type Series = { + label: string; + points: SeriesPoint[]; }; -function formatPrometheusValue(result: PromQLResult | undefined): string { - if (!result) return "No data"; - if (result.resultType === "scalar" && Array.isArray(result.result)) { - return String(result.result[1] ?? "No data"); +function formatMetricResult(series: Series[] | undefined): string { + if (!series || series.length === 0) return "No data"; + if (series.length === 1) { + const last = [...series[0].points].reverse().find((p) => p.v !== null); + return last ? String(last.v) : "No data"; } - if ( - result.resultType === "vector" && - Array.isArray(result.result) && - result.result.length > 0 - ) { - const first = result.result[0] as PromQLVectorSample; - if (first.value) return String(first.value[1]); - } - return JSON.stringify(result, null, 2); + return series + .map((s) => { + const last = [...s.points].reverse().find((p) => p.v !== null); + return `${s.label}: ${last ? last.v : "n/a"}`; + }) + .join("\n"); } export function PrometheusMetricWidget({ @@ -42,7 +40,7 @@ export function PrometheusMetricWidget({ description, }: Props) { const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs); - const result = data?.data?.result as PromQLResult | undefined; + const series = data?.data?.result as Series[] | undefined; return ( @@ -52,9 +50,9 @@ export function PrometheusMetricWidget({ {data.error} - ) : ( + ) : (
-					{formatPrometheusValue(result)}
+					{formatMetricResult(series)}
 				
)}
diff --git a/frontend/src/widgets/__tests__/PrometheusChartWidget.test.tsx b/frontend/src/widgets/__tests__/MetricChartWidget.test.tsx similarity index 80% rename from frontend/src/widgets/__tests__/PrometheusChartWidget.test.tsx rename to frontend/src/widgets/__tests__/MetricChartWidget.test.tsx index 19ad2b7..71769ae 100644 --- a/frontend/src/widgets/__tests__/PrometheusChartWidget.test.tsx +++ b/frontend/src/widgets/__tests__/MetricChartWidget.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from "vitest"; import { render, screen } from "@testing-library/react"; -import { PrometheusChartWidget } from "../PrometheusChartWidget"; +import { MetricChartWidget } from "../MetricChartWidget"; import type { WidgetInstance } from "../../types"; import * as useWidgets from "../../hooks/useWidgets"; @@ -29,13 +29,13 @@ function mockData(data: unknown, error?: string) { } as unknown as ReturnType); } -describe("PrometheusChartWidget", () => { +describe("MetricChartWidget", () => { it("renders skeleton while loading", () => { vi.mocked(useWidgets.useWidgetData).mockReturnValue({ data: undefined, isLoading: true, } as unknown as ReturnType); - render(); + render(); expect( document.querySelector('[data-slot="skeleton"]'), ).toBeInTheDocument(); @@ -53,20 +53,20 @@ describe("PrometheusChartWidget", () => { }, ], }); - render(); + render(); // 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, "promql is required"); - render(); + render(); expect(screen.getByText(/promql is required/i)).toBeInTheDocument(); }); it("shows empty state when no series", () => { mockData({ series: [] }); - render(); + render(); expect(screen.getByText(/No data/i)).toBeInTheDocument(); }); }); diff --git a/frontend/src/widgets/__tests__/PrometheusGaugeWidget.test.tsx b/frontend/src/widgets/__tests__/MetricGaugeWidget.test.tsx similarity index 80% rename from frontend/src/widgets/__tests__/PrometheusGaugeWidget.test.tsx rename to frontend/src/widgets/__tests__/MetricGaugeWidget.test.tsx index 8b97e0f..1f53036 100644 --- a/frontend/src/widgets/__tests__/PrometheusGaugeWidget.test.tsx +++ b/frontend/src/widgets/__tests__/MetricGaugeWidget.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from "vitest"; import { render, screen } from "@testing-library/react"; -import { PrometheusGaugeWidget } from "../PrometheusGaugeWidget"; +import { MetricGaugeWidget } from "../MetricGaugeWidget"; import type { WidgetInstance } from "../../types"; import * as useWidgets from "../../hooks/useWidgets"; @@ -29,13 +29,13 @@ function mockData(data: unknown, error?: string) { } as unknown as ReturnType); } -describe("PrometheusGaugeWidget", () => { +describe("MetricGaugeWidget", () => { it("renders skeleton while loading", () => { vi.mocked(useWidgets.useWidgetData).mockReturnValue({ data: undefined, isLoading: true, } as unknown as ReturnType); - render(); + render(); expect( document.querySelector('[data-slot="skeleton"]'), ).toBeInTheDocument(); @@ -48,7 +48,7 @@ describe("PrometheusGaugeWidget", () => { crit_at: 0.95, unit: "%", }); - render(); + render(); expect(screen.getByText("CPU Gauge")).toBeInTheDocument(); expect(screen.getByText(/0.75 %/)).toBeInTheDocument(); // Threshold labels present when bands are set. @@ -58,7 +58,7 @@ describe("PrometheusGaugeWidget", () => { it("renders a gauge without threshold bands (single color)", () => { mockData({ value: 42, unit: "req/s" }); - render(); + render(); expect(screen.getByText(/42 req\/s/)).toBeInTheDocument(); // No threshold labels when bands are absent. expect(screen.queryByText(/warn/i)).not.toBeInTheDocument(); @@ -66,13 +66,13 @@ describe("PrometheusGaugeWidget", () => { it("shows error Alert on error", () => { mockData(null, "Gauge requires a single-series query"); - render(); + render(); expect(screen.getByText(/single-series/i)).toBeInTheDocument(); }); it("shows empty state when no data", () => { mockData(null); - render(); + render(); expect(screen.getByText(/No data/i)).toBeInTheDocument(); }); }); diff --git a/frontend/src/widgets/__tests__/PrometheusMeanWidget.test.tsx b/frontend/src/widgets/__tests__/MetricMeanWidget.test.tsx similarity index 77% rename from frontend/src/widgets/__tests__/PrometheusMeanWidget.test.tsx rename to frontend/src/widgets/__tests__/MetricMeanWidget.test.tsx index 448d518..1efa39a 100644 --- a/frontend/src/widgets/__tests__/PrometheusMeanWidget.test.tsx +++ b/frontend/src/widgets/__tests__/MetricMeanWidget.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from "vitest"; import { render, screen } from "@testing-library/react"; -import { PrometheusMeanWidget } from "../PrometheusMeanWidget"; +import { MetricMeanWidget } from "../MetricMeanWidget"; import type { WidgetInstance } from "../../types"; import * as useWidgets from "../../hooks/useWidgets"; @@ -29,13 +29,13 @@ function mockData(data: unknown, error?: string) { } as unknown as ReturnType); } -describe("PrometheusMeanWidget", () => { +describe("MetricMeanWidget", () => { it("renders skeleton while loading", () => { vi.mocked(useWidgets.useWidgetData).mockReturnValue({ data: undefined, isLoading: true, } as unknown as ReturnType); - render(); + render(); expect( document.querySelector('[data-slot="skeleton"]'), ).toBeInTheDocument(); @@ -43,26 +43,26 @@ describe("PrometheusMeanWidget", () => { it("renders the mean value with unit", () => { mockData({ value: 23.5, unit: "%" }); - render(); + render(); expect(screen.getByText("Avg CPU")).toBeInTheDocument(); expect(screen.getByText(/23.5 %/)).toBeInTheDocument(); }); it("renders the mean value without unit", () => { mockData({ value: 1500, unit: null }); - render(); + render(); expect(screen.getByText(/1500/)).toBeInTheDocument(); }); it("shows error Alert on error", () => { mockData(null, "Mean requires a single-series query"); - render(); + render(); expect(screen.getByText(/single-series/i)).toBeInTheDocument(); }); it("shows empty state when no data", () => { mockData(null); - render(); + render(); expect(screen.getByText(/No data/i)).toBeInTheDocument(); }); }); diff --git a/frontend/src/widgets/index.ts b/frontend/src/widgets/index.ts index 398ed38..22d0ab3 100644 --- a/frontend/src/widgets/index.ts +++ b/frontend/src/widgets/index.ts @@ -1,8 +1,8 @@ export { AlertmanagerAlertsWidget } from "./AlertmanagerAlertsWidget"; export { BackupsWidget } from "./BackupsWidget"; -export { PrometheusChartWidget } from "./PrometheusChartWidget"; -export { PrometheusGaugeWidget } from "./PrometheusGaugeWidget"; -export { PrometheusMeanWidget } from "./PrometheusMeanWidget"; +export { MetricChartWidget } from "./MetricChartWidget"; +export { MetricGaugeWidget } from "./MetricGaugeWidget"; +export { MetricMeanWidget } from "./MetricMeanWidget"; export { JellyfinWidget } from "./JellyfinWidget"; export { JellyfinNowPlayingWidget } from "./JellyfinNowPlayingWidget"; export { PrometheusMetricWidget } from "./PrometheusMetricWidget";