Each polling page repeated its own fetch-and-set code on a fixed timer, so a slow answer could be overtaken by the next tick, an older response could overwrite a newer one, and nothing aborted a request when the page went away. usePolled skips a tick while a request is in flight, aborts on unmount or refresh, ignores answers from dropped requests, and keeps the last good data under an error. The alerts, hives, agents, schedules, builds, review runs and broadcast pages and the subagent list use it.
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
// <AlertsPage> — `/alerts`' content: the alerts the swarm's Grafana holds
|
|
// now, from swarm-controller's `GET /api/alerts`. Only the firing ones show at
|
|
// first; a button in the header adds the pending, no-data and errored ones.
|
|
// Same shape as `HivesPage`: one fetch on a `RefreshIntervalPicker` cadence,
|
|
// rendered as a `Table`.
|
|
import { useState } from "preact/hooks";
|
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
|
import { Button } from "../ui/button/Button.js";
|
|
import { Panel } from "../ui/panel/Panel.js";
|
|
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
|
|
import {
|
|
RefreshIntervalPicker,
|
|
type RefreshIntervalMs,
|
|
} from "../ui/refresh-interval/RefreshInterval.js";
|
|
import { fetchJson, usePolled } from "../ui/polled/usePolled.js";
|
|
import { Table, type TableColumn } from "../ui/table/Table.js";
|
|
import { activeSinceMs, alertKey, listedAlerts, type Alert } from "./alerts.js";
|
|
|
|
const STATE_TONE: Record<string, BadgeTone> = {
|
|
Alerting: "negative",
|
|
Error: "negative",
|
|
NoData: "warning",
|
|
Pending: "warning",
|
|
};
|
|
|
|
// Grafana's own alert list, for what this page does not show.
|
|
const GRAFANA_ALERTS_URL =
|
|
"https://grafana.constellation.darkest.space/alerting/groups";
|
|
|
|
const COLUMNS: TableColumn<Alert>[] = [
|
|
{
|
|
key: "name",
|
|
header: "alert",
|
|
render: (a) => a.name,
|
|
sortBy: (a) => a.name,
|
|
filterValue: (a) => a.name,
|
|
},
|
|
{
|
|
key: "state",
|
|
header: "state",
|
|
render: (a) => {
|
|
const since = activeSinceMs(a);
|
|
return (
|
|
<Badge
|
|
tone={STATE_TONE[a.state] ?? "neutral"}
|
|
value={
|
|
since !== null ? (
|
|
<>
|
|
{a.state} (<RelativeTime epochMs={since} />)
|
|
</>
|
|
) : (
|
|
a.state
|
|
)
|
|
}
|
|
/>
|
|
);
|
|
},
|
|
sortBy: (a) => a.state,
|
|
filterValues: (a) => [a.state],
|
|
filterMode: "multiselect",
|
|
},
|
|
{
|
|
key: "summary",
|
|
header: "summary",
|
|
render: (a) => a.summary ?? "—",
|
|
sortBy: (a) => a.summary ?? "",
|
|
filterValue: (a) => a.summary ?? "",
|
|
},
|
|
];
|
|
|
|
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
|
|
|
export function AlertsPage() {
|
|
const [showAll, setShowAll] = useState(false);
|
|
const [intervalMs, setIntervalMs] =
|
|
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
|
const { data: alerts, error } = usePolled(intervalMs, (signal) =>
|
|
fetchJson<Alert[]>("/api/alerts", signal),
|
|
);
|
|
const listed = alerts ? listedAlerts(alerts, showAll) : null;
|
|
const hidden = alerts && listed ? alerts.length - listed.length : 0;
|
|
|
|
return (
|
|
<Panel
|
|
title="alerts"
|
|
icon="🚨"
|
|
actions={
|
|
<>
|
|
<a
|
|
href={GRAFANA_ALERTS_URL}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Grafana's alert list
|
|
</a>
|
|
{showAll || hidden > 0 ? (
|
|
<Button onClick={() => setShowAll(!showAll)}>
|
|
{showAll ? "only firing" : `show ${hidden} more`}
|
|
</Button>
|
|
) : null}
|
|
<RefreshIntervalPicker
|
|
id="alerts-refresh"
|
|
value={intervalMs}
|
|
onChange={setIntervalMs}
|
|
/>
|
|
</>
|
|
}
|
|
>
|
|
{error ? (
|
|
<ApiErrorPanel
|
|
context="failed to load Grafana's alerts"
|
|
problem={error}
|
|
/>
|
|
) : null}
|
|
{!error && alerts === null ? <p>loading…</p> : null}
|
|
{listed && listed.length === 0 ? (
|
|
<p>{showAll ? "no alerts." : "nothing is alerting."}</p>
|
|
) : null}
|
|
{listed && listed.length > 0 ? (
|
|
<Table
|
|
columns={COLUMNS}
|
|
rows={listed}
|
|
rowKey={alertKey}
|
|
storageKey="swarm-ui:alerts:table-filters"
|
|
/>
|
|
) : null}
|
|
</Panel>
|
|
);
|
|
}
|