111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
/**
|
|
* Prometheus Metrics tab (spec R2.4, R8.2).
|
|
*
|
|
* Instance-scoped tab showing Prometheus service health.
|
|
* usePrometheusStatus is scoped by instance.id; usePrometheusTargets
|
|
* stays global (returns Node Exporter scrape targets for external Prom).
|
|
*/
|
|
import { Radio } from "lucide-react";
|
|
import {
|
|
usePrometheusStatus,
|
|
usePrometheusTargets,
|
|
} from "../../hooks/useObservability";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import type { PrometheusTarget, ServiceInstance } from "../../types";
|
|
|
|
function TargetsTable({ targets }: { targets: PrometheusTarget[] }) {
|
|
return (
|
|
<div className="space-y-3">
|
|
{targets.map((target, idx) => (
|
|
<div key={idx} className="rounded-lg border p-3">
|
|
<div className="font-mono text-sm">{target.targets.join(", ")}</div>
|
|
{target.labels && Object.keys(target.labels).length > 0 && (
|
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
{Object.entries(target.labels).map(([key, value]) => (
|
|
<Badge key={key} variant="outline" className="text-[10px]">
|
|
{key}: {value}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function MetricsTab({ instance }: { instance: ServiceInstance }) {
|
|
const {
|
|
data: status,
|
|
isLoading: statusLoading,
|
|
error: statusError,
|
|
} = usePrometheusStatus(instance.id);
|
|
const {
|
|
data: targets,
|
|
isLoading: targetsLoading,
|
|
error: targetsError,
|
|
} = usePrometheusTargets();
|
|
|
|
const statusDetail = status?.up
|
|
? status.version
|
|
? `version ${status.version}`
|
|
: "reachable"
|
|
: statusLoading
|
|
? "checking…"
|
|
: "unreachable";
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Radio className="h-4 w-4" />
|
|
Prometheus {statusDetail}
|
|
</div>
|
|
|
|
{statusError && (
|
|
<Alert variant="destructive">
|
|
<AlertTitle>Failed to reach Prometheus</AlertTitle>
|
|
<AlertDescription>{statusError.message}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Radio className="h-4 w-4" />
|
|
Node Exporter Targets ({targets?.length ?? 0})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{targetsLoading ? (
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-16 w-full" />
|
|
<Skeleton className="h-16 w-full" />
|
|
</div>
|
|
) : !targets || targets.length === 0 ? (
|
|
<div className="flex h-full min-h-[160px] flex-col items-center justify-center gap-2 rounded-md border p-6 text-center">
|
|
<Radio className="h-8 w-8 text-muted-foreground" />
|
|
<div className="font-medium">No Node Exporter targets</div>
|
|
<div className="max-w-md text-sm text-muted-foreground">
|
|
Enable Node Exporter on an SSH machine in Settings to populate
|
|
Prometheus scrape targets.
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<TargetsTable targets={targets} />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{targetsError && (
|
|
<Alert variant="destructive">
|
|
<AlertTitle>Failed to load targets</AlertTitle>
|
|
<AlertDescription>{targetsError.message}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|