feat(observability): service discovery, health cards, alertmanager widget
Slice 3 of observability-service-registry (frontend). The Observability
page discovers Grafana from the service registry instead of env vars,
adds Grafana + Prometheus health cards, and ships an alertmanager
active_alerts dashboard widget.
- types: added GrafanaStatus + PrometheusStatus; added optional
service_id/error to AlertmanagerStatus.
- api/client.ts + hooks/useObservability.ts: fetchGrafanaStatus,
fetchPrometheusStatus, useGrafanaStatus, usePrometheusStatus.
- widgets/AlertmanagerAlertsWidget.tsx (new): presentational widget
consuming the active_alerts summary shape (total/by_severity/alerts);
exported from widgets/index.ts.
- integrations/registry.ts: alertmanager binding (active_alerts kind,
30s refresh, optional severity_filter); registry.test.ts updated to
6 service types incl alertmanager + a resolve test.
- components/ObservabilityPage.tsx: removed
import.meta.env.VITE_GRAFANA_URL; derive GRAFANA_BASE_URL from the
first enabled grafana service via useServiceInstances("grafana");
added Grafana + Prometheus HealthCards (up/not-configured/unreachable)
with QueryError retry blocks; machine dashboard shows a "No Grafana
service configured" empty-state linking to /services when none is set.
npm run build (tsc -b + vite) clean; 0 lint errors; 72 frontend tests
pass. Reviewed fresh-context (read-only): no blockers.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SectionCard } from "../components/SectionCard";
|
||||
import { useWidgetData } from "../hooks/useWidgets";
|
||||
import type {
|
||||
AlertmanagerAlert,
|
||||
AlertmanagerAlertSummary,
|
||||
WidgetInstance,
|
||||
} from "../types";
|
||||
|
||||
interface Props {
|
||||
widget: WidgetInstance;
|
||||
refreshIntervalMs: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
function severityVariant(
|
||||
severity: string,
|
||||
): "default" | "secondary" | "destructive" | "outline" {
|
||||
switch (severity.toLowerCase()) {
|
||||
case "critical":
|
||||
return "destructive";
|
||||
case "warning":
|
||||
return "default";
|
||||
case "info":
|
||||
return "secondary";
|
||||
default:
|
||||
return "outline";
|
||||
}
|
||||
}
|
||||
|
||||
export function AlertmanagerAlertsWidget({
|
||||
widget,
|
||||
refreshIntervalMs,
|
||||
description,
|
||||
}: Props) {
|
||||
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
||||
const summary = data?.data as AlertmanagerAlertSummary | undefined;
|
||||
const alerts: AlertmanagerAlert[] = summary?.alerts ?? [];
|
||||
|
||||
return (
|
||||
<SectionCard title={widget.title} description={description}>
|
||||
{isLoading && !data ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-full" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
</div>
|
||||
) : data?.error ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{data.error}</AlertDescription>
|
||||
</Alert>
|
||||
) : summary ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">{summary.total}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Firing alert{summary.total === 1 ? "" : "s"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{Object.entries(summary.by_severity).map(([sev, count]) => (
|
||||
<Badge key={sev} variant={severityVariant(sev)}>
|
||||
{sev}: {count}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{alerts.length > 0 ? (
|
||||
<ul className="space-y-1.5">
|
||||
{alerts.slice(0, 5).map((a, idx) => (
|
||||
<li
|
||||
key={`${a.name}-${idx}`}
|
||||
className="flex items-center justify-between gap-2 rounded-md border px-2 py-1 text-sm"
|
||||
>
|
||||
<span className="truncate">{a.name}</span>
|
||||
<Badge variant={severityVariant(a.severity)}>
|
||||
{a.severity}
|
||||
</Badge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Everything looks quiet.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { AlertmanagerAlertsWidget } from "./AlertmanagerAlertsWidget";
|
||||
export { BackupsWidget } from "./BackupsWidget";
|
||||
export { GrafanaLinkWidget } from "./GrafanaLinkWidget";
|
||||
export { JellyfinWidget } from "./JellyfinWidget";
|
||||
|
||||
Reference in New Issue
Block a user