feat: Monitor groups and chart information

This commit is contained in:
2026-08-24 10:30:23 +00:00
parent 83cdf92575
commit 2fab784ba7
9 changed files with 299 additions and 25 deletions
+27 -4
View File
@@ -91,6 +91,7 @@ export function MonitorForm({
error?: Error | null;
}) {
const [name, setName] = useState(initial?.name ?? "");
const [group, setGroup] = useState(initial?.group ?? "");
const [type, setType] = useState<MonitorType>(initial?.type ?? "http");
const [url, setUrl] = useState(initial?.target.url ?? "");
const [host, setHost] = useState(initial?.target.host ?? "");
@@ -107,6 +108,11 @@ export function MonitorForm({
const [channelIds, setChannelIds] = useState<string[]>(initial?.channel_ids ?? []);
const { data: servers } = useQuery({ queryKey: ["servers"], queryFn: () => api.listServers() });
/* The group is free text, so the existing groups are offered as suggestions
rather than a fixed list — grouping is a label people invent, and a
select would mean adding one before it could be used. */
const { data: allMonitors } = useQuery({ queryKey: ["monitors"], queryFn: () => api.listMonitors() });
const knownGroups = Array.from(new Set((allMonitors ?? []).map((m) => m.group).filter((g): g is string => !!g))).sort();
const { data: channels } = useQuery({ queryKey: ["channels"], queryFn: () => api.listChannels() });
function handleSubmit(e: React.FormEvent) {
@@ -128,7 +134,7 @@ export function MonitorForm({
target.host = host;
target.port = port;
}
onSubmit({ name, type, target, interval_sec: intervalSec, retries, runner, enabled, channel_ids: channelIds });
onSubmit({ name, group: group.trim(), type, target, interval_sec: intervalSec, retries, runner, enabled, channel_ids: channelIds });
}
const runnerName = runner === "server" ? "the control plane" : servers?.find((s) => s.server_id === runner)?.hostname || "an agent";
@@ -141,9 +147,26 @@ export function MonitorForm({
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
<div className="grid grid-cols-1 items-start gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(320px,400px)]">
<Section title="Check" hint={typeCopy[type].target}>
<Field label="Name" help="Shown in the fleet list and in every alert this check sends.">
<input className={inputClass} value={name} onChange={(e) => setName(e.target.value)} placeholder="Billing API" required />
</Field>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)]">
<Field label="Name" help="Shown in the fleet list and in every alert this check sends.">
<input className={inputClass} value={name} onChange={(e) => setName(e.target.value)} placeholder="Billing API" required />
</Field>
<Field label="Group" help="Optional heading on the monitors page. Nothing else reads it.">
<input
className={inputClass}
value={group}
onChange={(e) => setGroup(e.target.value)}
placeholder="Production"
list="monitor-groups"
maxLength={48}
/>
<datalist id="monitor-groups">
{knownGroups.map((g) => (
<option key={g} value={g} />
))}
</datalist>
</Field>
</div>
<div>
<span className="mb-1.5 block text-sm font-medium text-text-secondary">Kind</span>
+4 -2
View File
@@ -162,7 +162,9 @@ function slotHeight(s: Slot): number {
return 55 + (s.pct - 80) * 2.2;
}
function slotTitle(s: Slot): string {
/** One line of plain text for a slot — the tape's tooltip and the chart's
* accessible name for a bar, so both read the same hour the same way. */
export function slotLabel(s: Slot): string {
const when = s.at.toLocaleString(undefined, { weekday: "short", hour: "2-digit", minute: "2-digit" });
if (s.pct === null) return `${when} · no checks ran`;
return `${when} · ${s.pct.toFixed(1)}% up · ${s.checks} checks`;
@@ -179,7 +181,7 @@ export function Tape({ slots, height = "h-9", live = true }: { slots: Slot[]; he
return (
<div className={`relative flex ${height} items-end gap-px rounded-sm bg-well p-[3px]`}>
{slots.map((s) => (
<div key={s.at.getTime()} className="flex h-full flex-1 items-end" title={slotTitle(s)}>
<div key={s.at.getTime()} className="flex h-full flex-1 items-end" title={slotLabel(s)}>
<div className={`w-full rounded-[1px] ${slotColor(s)}`} style={{ height: `${slotHeight(s)}%` }} />
</div>
))}