feat: Monitor groups and chart information
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user