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 type { ComponentType } from "react";
|
||||||
import { AlertmanagerAlertsWidget } from "../widgets/AlertmanagerAlertsWidget";
|
import { AlertmanagerAlertsWidget } from "../widgets/AlertmanagerAlertsWidget";
|
||||||
import { BackupsWidget } from "../widgets/BackupsWidget";
|
import { BackupsWidget } from "../widgets/BackupsWidget";
|
||||||
import { PrometheusChartWidget } from "../widgets/PrometheusChartWidget";
|
import { MetricChartWidget } from "../widgets/MetricChartWidget";
|
||||||
import { PrometheusGaugeWidget } from "../widgets/PrometheusGaugeWidget";
|
import { MetricGaugeWidget } from "../widgets/MetricGaugeWidget";
|
||||||
import { PrometheusMeanWidget } from "../widgets/PrometheusMeanWidget";
|
import { MetricMeanWidget } from "../widgets/MetricMeanWidget";
|
||||||
import { JellyfinWidget } from "../widgets/JellyfinWidget";
|
import { JellyfinWidget } from "../widgets/JellyfinWidget";
|
||||||
import { JellyfinNowPlayingWidget } from "../widgets/JellyfinNowPlayingWidget";
|
import { JellyfinNowPlayingWidget } from "../widgets/JellyfinNowPlayingWidget";
|
||||||
import { PrometheusMetricWidget } from "../widgets/PrometheusMetricWidget";
|
import { PrometheusMetricWidget } from "../widgets/PrometheusMetricWidget";
|
||||||
@@ -109,7 +109,7 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
|||||||
},
|
},
|
||||||
required: ["promql"],
|
required: ["promql"],
|
||||||
},
|
},
|
||||||
component: PrometheusChartWidget,
|
component: MetricChartWidget,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "gauge",
|
kind: "gauge",
|
||||||
@@ -133,7 +133,7 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
|||||||
},
|
},
|
||||||
required: ["promql"],
|
required: ["promql"],
|
||||||
},
|
},
|
||||||
component: PrometheusGaugeWidget,
|
component: MetricGaugeWidget,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "mean",
|
kind: "mean",
|
||||||
@@ -156,7 +156,7 @@ export const SERVICE_REGISTRY: Record<string, ServiceBinding> = {
|
|||||||
},
|
},
|
||||||
required: ["promql"],
|
required: ["promql"],
|
||||||
},
|
},
|
||||||
component: PrometheusMeanWidget,
|
component: MetricMeanWidget,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrometheusChartWidget({
|
export function MetricChartWidget({
|
||||||
widget,
|
widget,
|
||||||
refreshIntervalMs,
|
refreshIntervalMs,
|
||||||
description,
|
description,
|
||||||
+1
-1
@@ -37,7 +37,7 @@ function formatValue(value: number, unit?: string | null): string {
|
|||||||
return unit ? `${formatted} ${unit}` : formatted;
|
return unit ? `${formatted} ${unit}` : formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrometheusGaugeWidget({
|
export function MetricGaugeWidget({
|
||||||
widget,
|
widget,
|
||||||
refreshIntervalMs,
|
refreshIntervalMs,
|
||||||
description,
|
description,
|
||||||
+1
-1
@@ -27,7 +27,7 @@ function formatMean(value: number, unit?: string | null): string {
|
|||||||
return unit ? `${formatted} ${unit}` : formatted;
|
return unit ? `${formatted} ${unit}` : formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrometheusMeanWidget({
|
export function MetricMeanWidget({
|
||||||
widget,
|
widget,
|
||||||
refreshIntervalMs,
|
refreshIntervalMs,
|
||||||
description,
|
description,
|
||||||
@@ -10,30 +10,28 @@ interface Props {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromQLResult = {
|
type SeriesPoint = {
|
||||||
resultType?: string;
|
t: number;
|
||||||
result?: unknown;
|
v: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PromQLVectorSample = {
|
type Series = {
|
||||||
metric?: Record<string, string>;
|
label: string;
|
||||||
value?: [number, string];
|
points: SeriesPoint[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatPrometheusValue(result: PromQLResult | undefined): string {
|
function formatMetricResult(series: Series[] | undefined): string {
|
||||||
if (!result) return "No data";
|
if (!series || series.length === 0) return "No data";
|
||||||
if (result.resultType === "scalar" && Array.isArray(result.result)) {
|
if (series.length === 1) {
|
||||||
return String(result.result[1] ?? "No data");
|
const last = [...series[0].points].reverse().find((p) => p.v !== null);
|
||||||
|
return last ? String(last.v) : "No data";
|
||||||
}
|
}
|
||||||
if (
|
return series
|
||||||
result.resultType === "vector" &&
|
.map((s) => {
|
||||||
Array.isArray(result.result) &&
|
const last = [...s.points].reverse().find((p) => p.v !== null);
|
||||||
result.result.length > 0
|
return `${s.label}: ${last ? last.v : "n/a"}`;
|
||||||
) {
|
})
|
||||||
const first = result.result[0] as PromQLVectorSample;
|
.join("\n");
|
||||||
if (first.value) return String(first.value[1]);
|
|
||||||
}
|
|
||||||
return JSON.stringify(result, null, 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrometheusMetricWidget({
|
export function PrometheusMetricWidget({
|
||||||
@@ -42,7 +40,7 @@ export function PrometheusMetricWidget({
|
|||||||
description,
|
description,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
||||||
const result = data?.data?.result as PromQLResult | undefined;
|
const series = data?.data?.result as Series[] | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard title={widget.title} description={description}>
|
<SectionCard title={widget.title} description={description}>
|
||||||
@@ -52,9 +50,9 @@ export function PrometheusMetricWidget({
|
|||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>{data.error}</AlertDescription>
|
<AlertDescription>{data.error}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<pre className="whitespace-pre-wrap text-sm">
|
<pre className="whitespace-pre-wrap text-sm">
|
||||||
{formatPrometheusValue(result)}
|
{formatMetricResult(series)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { PrometheusChartWidget } from "../PrometheusChartWidget";
|
import { MetricChartWidget } from "../MetricChartWidget";
|
||||||
import type { WidgetInstance } from "../../types";
|
import type { WidgetInstance } from "../../types";
|
||||||
import * as useWidgets from "../../hooks/useWidgets";
|
import * as useWidgets from "../../hooks/useWidgets";
|
||||||
|
|
||||||
@@ -29,13 +29,13 @@ function mockData(data: unknown, error?: string) {
|
|||||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("PrometheusChartWidget", () => {
|
describe("MetricChartWidget", () => {
|
||||||
it("renders skeleton while loading", () => {
|
it("renders skeleton while loading", () => {
|
||||||
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
||||||
data: undefined,
|
data: undefined,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
render(<MetricChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||||
expect(
|
expect(
|
||||||
document.querySelector('[data-slot="skeleton"]'),
|
document.querySelector('[data-slot="skeleton"]'),
|
||||||
).toBeInTheDocument();
|
).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.
|
// recharts renders an SVG; the title from SectionCard should be present.
|
||||||
expect(screen.getByText("CPU Usage")).toBeInTheDocument();
|
expect(screen.getByText("CPU Usage")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows error Alert on error", () => {
|
it("shows error Alert on error", () => {
|
||||||
mockData(null, "promql is required");
|
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();
|
expect(screen.getByText(/promql is required/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows empty state when no series", () => {
|
it("shows empty state when no series", () => {
|
||||||
mockData({ series: [] });
|
mockData({ series: [] });
|
||||||
render(<PrometheusChartWidget widget={widget} refreshIntervalMs={60000} />);
|
render(<MetricChartWidget widget={widget} refreshIntervalMs={60000} />);
|
||||||
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+7
-7
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { PrometheusGaugeWidget } from "../PrometheusGaugeWidget";
|
import { MetricGaugeWidget } from "../MetricGaugeWidget";
|
||||||
import type { WidgetInstance } from "../../types";
|
import type { WidgetInstance } from "../../types";
|
||||||
import * as useWidgets from "../../hooks/useWidgets";
|
import * as useWidgets from "../../hooks/useWidgets";
|
||||||
|
|
||||||
@@ -29,13 +29,13 @@ function mockData(data: unknown, error?: string) {
|
|||||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("PrometheusGaugeWidget", () => {
|
describe("MetricGaugeWidget", () => {
|
||||||
it("renders skeleton while loading", () => {
|
it("renders skeleton while loading", () => {
|
||||||
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
||||||
data: undefined,
|
data: undefined,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||||
render(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
render(<MetricGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||||
expect(
|
expect(
|
||||||
document.querySelector('[data-slot="skeleton"]'),
|
document.querySelector('[data-slot="skeleton"]'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -48,7 +48,7 @@ describe("PrometheusGaugeWidget", () => {
|
|||||||
crit_at: 0.95,
|
crit_at: 0.95,
|
||||||
unit: "%",
|
unit: "%",
|
||||||
});
|
});
|
||||||
render(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
render(<MetricGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||||
expect(screen.getByText("CPU Gauge")).toBeInTheDocument();
|
expect(screen.getByText("CPU Gauge")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/0.75 %/)).toBeInTheDocument();
|
expect(screen.getByText(/0.75 %/)).toBeInTheDocument();
|
||||||
// Threshold labels present when bands are set.
|
// Threshold labels present when bands are set.
|
||||||
@@ -58,7 +58,7 @@ describe("PrometheusGaugeWidget", () => {
|
|||||||
|
|
||||||
it("renders a gauge without threshold bands (single color)", () => {
|
it("renders a gauge without threshold bands (single color)", () => {
|
||||||
mockData({ value: 42, unit: "req/s" });
|
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();
|
expect(screen.getByText(/42 req\/s/)).toBeInTheDocument();
|
||||||
// No threshold labels when bands are absent.
|
// No threshold labels when bands are absent.
|
||||||
expect(screen.queryByText(/warn/i)).not.toBeInTheDocument();
|
expect(screen.queryByText(/warn/i)).not.toBeInTheDocument();
|
||||||
@@ -66,13 +66,13 @@ describe("PrometheusGaugeWidget", () => {
|
|||||||
|
|
||||||
it("shows error Alert on error", () => {
|
it("shows error Alert on error", () => {
|
||||||
mockData(null, "Gauge requires a single-series query");
|
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();
|
expect(screen.getByText(/single-series/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows empty state when no data", () => {
|
it("shows empty state when no data", () => {
|
||||||
mockData(null);
|
mockData(null);
|
||||||
render(<PrometheusGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
render(<MetricGaugeWidget widget={widget} refreshIntervalMs={30000} />);
|
||||||
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+7
-7
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { PrometheusMeanWidget } from "../PrometheusMeanWidget";
|
import { MetricMeanWidget } from "../MetricMeanWidget";
|
||||||
import type { WidgetInstance } from "../../types";
|
import type { WidgetInstance } from "../../types";
|
||||||
import * as useWidgets from "../../hooks/useWidgets";
|
import * as useWidgets from "../../hooks/useWidgets";
|
||||||
|
|
||||||
@@ -29,13 +29,13 @@ function mockData(data: unknown, error?: string) {
|
|||||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("PrometheusMeanWidget", () => {
|
describe("MetricMeanWidget", () => {
|
||||||
it("renders skeleton while loading", () => {
|
it("renders skeleton while loading", () => {
|
||||||
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
vi.mocked(useWidgets.useWidgetData).mockReturnValue({
|
||||||
data: undefined,
|
data: undefined,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
} as unknown as ReturnType<typeof useWidgets.useWidgetData>);
|
||||||
render(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
render(<MetricMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||||
expect(
|
expect(
|
||||||
document.querySelector('[data-slot="skeleton"]'),
|
document.querySelector('[data-slot="skeleton"]'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -43,26 +43,26 @@ describe("PrometheusMeanWidget", () => {
|
|||||||
|
|
||||||
it("renders the mean value with unit", () => {
|
it("renders the mean value with unit", () => {
|
||||||
mockData({ value: 23.5, 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("Avg CPU")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/23.5 %/)).toBeInTheDocument();
|
expect(screen.getByText(/23.5 %/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the mean value without unit", () => {
|
it("renders the mean value without unit", () => {
|
||||||
mockData({ value: 1500, unit: null });
|
mockData({ value: 1500, unit: null });
|
||||||
render(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
render(<MetricMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||||
expect(screen.getByText(/1500/)).toBeInTheDocument();
|
expect(screen.getByText(/1500/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows error Alert on error", () => {
|
it("shows error Alert on error", () => {
|
||||||
mockData(null, "Mean requires a single-series query");
|
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();
|
expect(screen.getByText(/single-series/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows empty state when no data", () => {
|
it("shows empty state when no data", () => {
|
||||||
mockData(null);
|
mockData(null);
|
||||||
render(<PrometheusMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
render(<MetricMeanWidget widget={widget} refreshIntervalMs={60000} />);
|
||||||
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
expect(screen.getByText(/No data/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
export { AlertmanagerAlertsWidget } from "./AlertmanagerAlertsWidget";
|
export { AlertmanagerAlertsWidget } from "./AlertmanagerAlertsWidget";
|
||||||
export { BackupsWidget } from "./BackupsWidget";
|
export { BackupsWidget } from "./BackupsWidget";
|
||||||
export { PrometheusChartWidget } from "./PrometheusChartWidget";
|
export { MetricChartWidget } from "./MetricChartWidget";
|
||||||
export { PrometheusGaugeWidget } from "./PrometheusGaugeWidget";
|
export { MetricGaugeWidget } from "./MetricGaugeWidget";
|
||||||
export { PrometheusMeanWidget } from "./PrometheusMeanWidget";
|
export { MetricMeanWidget } from "./MetricMeanWidget";
|
||||||
export { JellyfinWidget } from "./JellyfinWidget";
|
export { JellyfinWidget } from "./JellyfinWidget";
|
||||||
export { JellyfinNowPlayingWidget } from "./JellyfinNowPlayingWidget";
|
export { JellyfinNowPlayingWidget } from "./JellyfinNowPlayingWidget";
|
||||||
export { PrometheusMetricWidget } from "./PrometheusMetricWidget";
|
export { PrometheusMetricWidget } from "./PrometheusMetricWidget";
|
||||||
|
|||||||
Reference in New Issue
Block a user