Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx
iris 8917ba27b6 swarm-ui: one polled-resource hook for the refreshing pages
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.
2026-10-21 23:05:35 +02:00

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>
);
}