feat(grafana-metric-gateway): slice 2 — rename widgets to Metric*
git mv PrometheusChartWidget→MetricChartWidget, PrometheusGaugeWidget→
MetricGaugeWidget, PrometheusMeanWidget→MetricMeanWidget (+ 3 test files,
R100 history preserved). Update registry imports/refs + barrel exports.
Adapt PrometheusMetricWidget for §3.4 Option A: read normalized {result:
[{label,points}]} series shape (last-point extraction) instead of old Prom
{resultType,result} vector. PrometheusMetricWidget NOT renamed (design §3.1).
GM-111/112/116 satisfied. All gates: 151 vitest, build exit 0, lint 0 errors.
This commit is contained in:
@@ -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<string, ServiceBinding> = {
|
||||
},
|
||||
required: ["promql"],
|
||||
},
|
||||
component: PrometheusChartWidget,
|
||||
component: MetricChartWidget,
|
||||
},
|
||||
{
|
||||
kind: "gauge",
|
||||
@@ -133,7 +133,7 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
||||
},
|
||||
required: ["promql"],
|
||||
},
|
||||
component: PrometheusGaugeWidget,
|
||||
component: MetricGaugeWidget,
|
||||
},
|
||||
{
|
||||
kind: "mean",
|
||||
@@ -156,7 +156,7 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
||||
},
|
||||
required: ["promql"],
|
||||
},
|
||||
component: PrometheusMeanWidget,
|
||||
component: MetricMeanWidget,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ interface Props {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function PrometheusChartWidget({
|
||||
export function MetricChartWidget({
|
||||
widget,
|
||||
refreshIntervalMs,
|
||||
description,
|
||||
+1
-1
@@ -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,
|
||||
+1
-1
@@ -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,
|
||||
@@ -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<string, string>;
|
||||
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 (
|
||||
<SectionCard title={widget.title} description={description}>
|
||||
@@ -52,9 +50,9 @@ export function PrometheusMetricWidget({
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{data.error}</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
) : (
|
||||
<pre className="whitespace-pre-wrap text-sm">
|
||||
{formatPrometheusValue(result)}
|
||||
{formatMetricResult(series)}
|
||||
</pre>
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
+6
-6
@@ -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<typeof useWidgets.useWidgetData>);
|
||||
}
|
||||
|
||||
describe("PrometheusChartWidget", () => {
|
||||
describe("MetricChartWidget", () => {
|
||||
it("renders skeleton while loading", () => {
|
||||
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(
|
||||
document.querySelector('[data-slot="skeleton"]'),
|
||||
).toBeInTheDocument();
|
||||
@@ -53,20 +53,20 @@ describe("PrometheusChartWidget", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricChartWidget 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, "promql is required");
|
||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(screen.getByText(/promql is required/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows empty state when no series", () => {
|
||||
mockData({ series: [] });
|
||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+7
-7
@@ -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<typeof useWidgets.useWidgetData>);
|
||||
}
|
||||
|
||||
describe("PrometheusGaugeWidget", () => {
|
||||
describe("MetricGaugeWidget", () => {
|
||||
it("renders skeleton while loading", () => {
|
||||
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||
render(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
render(<MetricGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
expect(
|
||||
document.querySelector('[data-slot="skeleton"]'),
|
||||
).toBeInTheDocument();
|
||||
@@ -48,7 +48,7 @@ describe("PrometheusGaugeWidget", () => {
|
||||
crit_at: 0.95,
|
||||
unit: "%",
|
||||
});
|
||||
render(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
render(<MetricGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
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(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
render(<MetricGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
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(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
render(<MetricGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
expect(screen.getByText(/single-series/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows empty state when no data", () => {
|
||||
mockData(null);
|
||||
render(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
render(<MetricGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+7
-7
@@ -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<typeof useWidgets.useWidgetData>);
|
||||
}
|
||||
|
||||
describe("PrometheusMeanWidget", () => {
|
||||
describe("MetricMeanWidget", () => {
|
||||
it("renders skeleton while loading", () => {
|
||||
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||
render(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
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(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
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(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(screen.getByText(/1500/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows error Alert on error", () => {
|
||||
mockData(null, "Mean requires a single-series query");
|
||||
render(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(screen.getByText(/single-series/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows empty state when no data", () => {
|
||||
mockData(null);
|
||||
render(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
render(<MetricMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user