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.
134 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|