5dad98231f
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.
121 lines
3.0 KiB
TypeScript
121 lines
3.0 KiB
TypeScript
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { SectionCard } from "../components/SectionCard";
|
|
import { useWidgetData } from "../hooks/useWidgets";
|
|
import type { WidgetInstance } from "../types";
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
} from "recharts";
|
|
|
|
interface Props {
|
|
widget: WidgetInstance;
|
|
refreshIntervalMs: number;
|
|
description?: string;
|
|
}
|
|
|
|
interface SeriesPoint {
|
|
t: number;
|
|
v: number | null;
|
|
}
|
|
|
|
interface ChartSeries {
|
|
label: string;
|
|
points: SeriesPoint[];
|
|
}
|
|
|
|
/** Merge multiple time-series into a single recharts-friendly array. */
|
|
function mergeSeries(series: ChartSeries[]): Record<string, unknown>[] {
|
|
const map = new Map<number, Record<string, unknown>>();
|
|
for (const s of series) {
|
|
for (const p of s.points) {
|
|
const existing = map.get(p.t) ?? { time: p.t };
|
|
existing[s.label] = p.v;
|
|
map.set(p.t, existing);
|
|
}
|
|
}
|
|
return [...map.values()].sort(
|
|
(a, b) => (a.time as number) - (b.time as number),
|
|
);
|
|
}
|
|
|
|
function formatTime(ms: number): string {
|
|
return new Date(ms).toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
const CHART_COLORS = [
|
|
"var(--color-chart-1)",
|
|
"var(--color-chart-2)",
|
|
"var(--color-chart-3)",
|
|
"var(--color-chart-4)",
|
|
"var(--color-chart-5)",
|
|
];
|
|
|
|
export function PrometheusChartWidget({
|
|
widget,
|
|
refreshIntervalMs,
|
|
description,
|
|
}: Props) {
|
|
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
|
const series = data?.data?.series as ChartSeries[] | undefined;
|
|
|
|
return (
|
|
<SectionCard title={widget.title} description={description}>
|
|
{isLoading && !data ? (
|
|
<Skeleton className="h-[300px] w-full" />
|
|
) : data?.error ? (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{data.error}</AlertDescription>
|
|
</Alert>
|
|
) : series && series.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<LineChart data={mergeSeries(series)}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis
|
|
dataKey="time"
|
|
tickFormatter={formatTime}
|
|
tick={{ fontSize: 11 }}
|
|
className="fill-muted-foreground"
|
|
/>
|
|
<YAxis tick={{ fontSize: 11 }} className="fill-muted-foreground" />
|
|
<Tooltip
|
|
labelFormatter={(label) => formatTime(Number(label))}
|
|
contentStyle={{
|
|
backgroundColor: "hsl(var(--popover))",
|
|
border: "1px solid hsl(var(--border))",
|
|
borderRadius: "0.5rem",
|
|
color: "hsl(var(--popover-foreground))",
|
|
}}
|
|
/>
|
|
{series.map((s, i) => (
|
|
<Line
|
|
key={s.label}
|
|
type="monotone"
|
|
dataKey={s.label}
|
|
stroke={CHART_COLORS[i % CHART_COLORS.length]}
|
|
dot={false}
|
|
strokeWidth={2}
|
|
connectNulls
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<Alert>
|
|
<AlertDescription>
|
|
No data. Check your PromQL query and window in the widget config.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</SectionCard>
|
|
);
|
|
}
|