Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx
iris 599747bacb swarm-ui: alerts and the issue report are folded cards on a reports tab
The alerts and issues tabs are one reports tab in the nav. Each page, unchanged
inside, is a card that starts shut and reads nothing until opened; the alerts
header's link, button and refresh picker move into the card's body. /alerts and
/issues redirect to /reports. Docs follow.
2026-10-26 23:04:32 +01:00

134 lines
3.9 KiB
TypeScript

// <AlertsCard> — a card of the reports tab: 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 `HivesCard`: 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 { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.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;
function AlertsBody() {
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 (
<>
<div class="ui-alerts-bar">
<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}
/>
</div>
{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}
</>
);
}
// A folded card on the reports tab: read and polled only once it is opened.
export function AlertsCard() {
return (
<LazyDisclosure
title="alerts"
icon="🚨"
hint="what the swarm's Grafana holds now"
>
<AlertsBody />
</LazyDisclosure>
);
}