swarm-ui: show only firing alerts on the alerts page and link to Grafana's list
The page listed every non-normal instance. It now keeps the ones in the Alerting state and links to Grafana's alert-groups view for the pending, no-data and error ones.
This commit is contained in:
parent
b5d2e310ba
commit
842b37dc33
2 changed files with 33 additions and 24 deletions
|
|
@ -14,7 +14,7 @@ domain, covers host-level detail for one hive.
|
||||||
| `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
| `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps; create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
||||||
| `/agents/<name>/terminal` | one agent's live terminal |
|
| `/agents/<name>/terminal` | one agent's live terminal |
|
||||||
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
||||||
| `/alerts` | the alerts the swarm's Grafana holds now (firing, pending, no data, error), read through the controller with a Viewer service-account token |
|
| `/alerts` | the alerts of the swarm's Grafana that are firing now, with a link to Grafana's full alert list for the pending, no-data and error ones; read through the controller with a Viewer service-account token |
|
||||||
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
|
||||||
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
| `/schedules` | the scheduled prompts: add, edit, pause, resume and remove |
|
||||||
| `/broadcast` | send one message to several agents at once, picked from a grid of their icons |
|
| `/broadcast` | send one message to several agents at once, picked from a grid of their icons |
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
// <AlertsPage> — `/alerts`' content: the alerts the swarm's Grafana holds
|
// <AlertsPage> — `/alerts`' content: the alerts of the swarm's Grafana that
|
||||||
// now, from swarm-controller's `GET /api/alerts`. Same shape as `HivesPage`:
|
// are firing now, from swarm-controller's `GET /api/alerts` (which also lists
|
||||||
// one fetch on a `RefreshIntervalPicker` cadence, rendered as a `Table`.
|
// pending, no-data and errored instances; those stay in Grafana). Same shape
|
||||||
|
// as `HivesPage`: one fetch on a `RefreshIntervalPicker` cadence, rendered as
|
||||||
|
// a `Table`.
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||||
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
import { Panel } from "../ui/panel/Panel.js";
|
import { Panel } from "../ui/panel/Panel.js";
|
||||||
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
|
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -17,19 +19,18 @@ import { Table, type TableColumn } from "../ui/table/Table.js";
|
||||||
interface Alert {
|
interface Alert {
|
||||||
name: string;
|
name: string;
|
||||||
// Grafana's state for the instance: `Alerting`, `Pending`, `NoData` or
|
// Grafana's state for the instance: `Alerting`, `Pending`, `NoData` or
|
||||||
// `Error`. Anything else renders neutral.
|
// `Error`.
|
||||||
state: string;
|
state: string;
|
||||||
active_at: string | null;
|
active_at: string | null;
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
labels: Record<string, string>;
|
labels: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATE_TONE: Record<string, BadgeTone> = {
|
// Grafana's alert-groups view, where the instances this page leaves out are.
|
||||||
Alerting: "negative",
|
const GRAFANA_ALERTS_URL =
|
||||||
Error: "negative",
|
"https://grafana.constellation.darkest.space/alerting/groups";
|
||||||
NoData: "warning",
|
|
||||||
Pending: "warning",
|
const FIRING = "Alerting";
|
||||||
};
|
|
||||||
|
|
||||||
// Grafana reports an instance that never became active with the zero time,
|
// Grafana reports an instance that never became active with the zero time,
|
||||||
// which `Date.parse` reads as year 1.
|
// which `Date.parse` reads as year 1.
|
||||||
|
|
@ -59,7 +60,7 @@ const COLUMNS: TableColumn<Alert>[] = [
|
||||||
const since = activeSinceMs(a);
|
const since = activeSinceMs(a);
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
tone={STATE_TONE[a.state] ?? "neutral"}
|
tone="negative"
|
||||||
value={
|
value={
|
||||||
since !== null ? (
|
since !== null ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -72,9 +73,7 @@ const COLUMNS: TableColumn<Alert>[] = [
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
sortBy: (a) => a.state,
|
sortBy: (a) => activeSinceMs(a) ?? 0,
|
||||||
filterValues: (a) => [a.state],
|
|
||||||
filterMode: "multiselect",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "summary",
|
key: "summary",
|
||||||
|
|
@ -90,6 +89,7 @@ const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
||||||
export function AlertsPage() {
|
export function AlertsPage() {
|
||||||
const [alerts, setAlerts] = useState<Alert[] | null>(null);
|
const [alerts, setAlerts] = useState<Alert[] | null>(null);
|
||||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||||
|
const firing = alerts?.filter((a) => a.state === FIRING) ?? null;
|
||||||
const [intervalMs, setIntervalMs] =
|
const [intervalMs, setIntervalMs] =
|
||||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||||
|
|
||||||
|
|
@ -110,11 +110,20 @@ export function AlertsPage() {
|
||||||
title="alerts"
|
title="alerts"
|
||||||
icon="🚨"
|
icon="🚨"
|
||||||
actions={
|
actions={
|
||||||
<RefreshIntervalPicker
|
<>
|
||||||
id="alerts-refresh"
|
<a
|
||||||
value={intervalMs}
|
href={GRAFANA_ALERTS_URL}
|
||||||
onChange={setIntervalMs}
|
target="_blank"
|
||||||
/>
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
all alerts in Grafana
|
||||||
|
</a>
|
||||||
|
<RefreshIntervalPicker
|
||||||
|
id="alerts-refresh"
|
||||||
|
value={intervalMs}
|
||||||
|
onChange={setIntervalMs}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{error ? (
|
{error ? (
|
||||||
|
|
@ -124,11 +133,11 @@ export function AlertsPage() {
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!error && alerts === null ? <p>loading…</p> : null}
|
{!error && alerts === null ? <p>loading…</p> : null}
|
||||||
{alerts && alerts.length === 0 ? <p>no alerts.</p> : null}
|
{firing && firing.length === 0 ? <p>nothing is alerting.</p> : null}
|
||||||
{alerts && alerts.length > 0 ? (
|
{firing && firing.length > 0 ? (
|
||||||
<Table
|
<Table
|
||||||
columns={COLUMNS}
|
columns={COLUMNS}
|
||||||
rows={alerts}
|
rows={firing}
|
||||||
rowKey={alertKey}
|
rowKey={alertKey}
|
||||||
storageKey="swarm-ui:alerts:table-filters"
|
storageKey="swarm-ui:alerts:table-filters"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue