feat(web): metric monitor rule builder and per-server state table
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user