feat(web): metric monitor rule builder and per-server state table

This commit is contained in:
2026-09-17 09:11:23 +00:00
parent 3cb8463dc7
commit 4158696388
7 changed files with 213 additions and 10 deletions
+41 -4
View File
@@ -7,6 +7,7 @@ import Link from "next/link";
import { api, Incident, Rollup } from "@/lib/api";
import { Button, ConfirmDialog, friendlyMessage, useToast } from "@/components/ui";
import { HeartbeatUrlPanel } from "@/components/monitors/HeartbeatUrlPanel";
import { MetricServersTable } from "@/components/monitors/MetricServersTable";
import {
Slot,
StatusChip,
@@ -18,6 +19,7 @@ import {
formatMs,
formatPct,
markIncidents,
metricCopy,
relativeTime,
slotChartColor,
slotLabel,
@@ -269,14 +271,17 @@ function Figure({ label, value, unit, tone = "" }: { label: string; value: strin
);
}
function IncidentRow({ incident }: { incident: Incident }) {
function IncidentRow({ incident, hostname }: { incident: Incident; hostname?: string }) {
const open = !incident.resolved_at;
const started = new Date(incident.started_at);
return (
<div className="grid grid-cols-[14px_minmax(0,1fr)] items-start gap-3 border-t border-border-soft px-5 py-3 first:border-t-0">
<span className={`mt-1.5 block h-2 w-2 rounded-full ${open ? "bg-danger ring-4 ring-danger/20" : "bg-text-tertiary"}`} />
<div className="min-w-0">
<p className="text-[13.5px] text-text-primary">{incident.cause || "Check failed"}</p>
<p className="text-[13.5px] text-text-primary">
{incident.cause || "Check failed"}
{hostname && <span className="ml-2 font-mono text-[11px] text-text-tertiary">· {hostname}</span>}
</p>
<p className="mt-1 flex flex-wrap gap-2 font-mono text-[11px] text-text-tertiary">
<span>
{started.toLocaleString(undefined, { weekday: "short", hour: "2-digit", minute: "2-digit" })}
@@ -380,6 +385,14 @@ export default function MonitorDetailPage() {
enabled: !!monitor && monitor.runner !== "server",
});
const { data: metricServers } = useQuery({
queryKey: ["monitors", monitorId, "servers"],
queryFn: () => api.monitorServers(monitorId),
refetchInterval: 30_000,
enabled: monitor?.type === "metric",
});
const hostnameByServerId = new Map((metricServers ?? []).map((s) => [s.server_id, s.hostname ?? s.server_id]));
const {
mutate: deleteMonitor,
isPending: isDeleting,
@@ -466,6 +479,18 @@ export default function MonitorDetailPage() {
monitor.runner === "server" ? "Control plane" : servers?.find((s) => s.server_id === monitor.runner)?.hostname || monitor.runner;
const alertChannels = (monitor.channel_ids ?? []).map((id) => channels?.find((c) => c.channel_id === id));
const isMetric = monitor.type === "metric";
let ruleSummary = "";
if (isMetric && monitor.target.metric) {
const kind = monitor.target.metric;
const copy = metricCopy[kind];
const value = copy.unit ? `${monitor.target.threshold ?? ""}${copy.unit}` : "";
const forMin = Math.round((monitor.for_sec ?? 0) / 60);
const selectorEntries = Object.entries(monitor.target.selector ?? {});
const on = selectorEntries.length > 0 ? selectorEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "all servers";
ruleSummary = `${copy.label} ${value} for ${forMin} min on ${on}`.replace(/\s+/g, " ").trim();
}
return (
<div className="p-4 sm:p-6 lg:p-8">
<Link href="/monitors" className="mb-2.5 inline-block text-sm text-text-secondary hover:text-text-primary">
@@ -593,9 +618,21 @@ export default function MonitorDetailPage() {
No incidents recorded. Every check has passed since this monitor was created.
</p>
) : (
incidents.map((inc) => <IncidentRow key={inc.incident_id} incident={inc} />)
incidents.map((inc) => (
<IncidentRow
key={inc.incident_id}
incident={inc}
hostname={inc.server_id ? hostnameByServerId.get(inc.server_id) ?? inc.server_id : undefined}
/>
))
)}
</Panel>
{isMetric && (
<Panel title="Servers" aside={ruleSummary} padded={false}>
<MetricServersTable monitorId={monitorId} />
</Panel>
)}
</div>
<div className="flex flex-col gap-5">
@@ -613,7 +650,7 @@ export default function MonitorDetailPage() {
Rotate token
</Button>
</Panel>
) : (
) : isMetric ? null : (
<Panel title="Check">
<dl className="text-[13px]">
<Row label="Runs from" value={runnerName} />
+3 -1
View File
@@ -204,7 +204,9 @@ function MonitorRow({ monitor, rollups, incidents }: MonitorRowData) {
? monitor.state.last_ping_at
? `Last ping ${relativeTime(monitor.state.last_ping_at)}`
: "Waiting for first ping"
: targetSummary(monitor)}
: monitor.type === "metric"
? (monitor.state.message ?? "Awaiting first check")
: targetSummary(monitor)}
</span>
</div>