swarm-ui: test which alert instances the alerts page lists
The firing-only filter and the helpers beside it move out of the page component into a plain module, with tests for the state match, the never-active time and the row key.
This commit is contained in:
parent
6da252aa20
commit
b39e095aa8
3 changed files with 104 additions and 26 deletions
|
|
@ -15,40 +15,16 @@ import {
|
|||
type RefreshIntervalMs,
|
||||
} from "../ui/refresh-interval/RefreshInterval.js";
|
||||
import { Table, type TableColumn } from "../ui/table/Table.js";
|
||||
|
||||
interface Alert {
|
||||
name: string;
|
||||
// Grafana's state for the instance: `Alerting`, `Pending`, `NoData` or
|
||||
// `Error`.
|
||||
state: string;
|
||||
active_at: string | null;
|
||||
summary: string | null;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
import { activeSinceMs, alertKey, firingAlerts, type Alert } from "./alerts.js";
|
||||
|
||||
// Grafana's alert-groups view, where the instances this page leaves out are.
|
||||
const GRAFANA_ALERTS_URL =
|
||||
"https://grafana.constellation.darkest.space/alerting/groups";
|
||||
|
||||
const FIRING = "Alerting";
|
||||
|
||||
// Versioned: the state column used to be a filter, and a saved "not Pending"
|
||||
// selection under the old key would now hide every row.
|
||||
const FILTERS_STORAGE_KEY = "swarm-ui:alerts:table-filters:v2";
|
||||
|
||||
// Grafana reports an instance that never became active with the zero time,
|
||||
// which `Date.parse` reads as year 1.
|
||||
function activeSinceMs(a: Alert): number | null {
|
||||
if (a.active_at === null) return null;
|
||||
const ms = Date.parse(a.active_at);
|
||||
return Number.isNaN(ms) || ms <= 0 ? null : ms;
|
||||
}
|
||||
|
||||
// A row needs a key that is unique among instances of the same rule.
|
||||
function alertKey(a: Alert): string {
|
||||
return JSON.stringify(Object.entries(a.labels).sort());
|
||||
}
|
||||
|
||||
const COLUMNS: TableColumn<Alert>[] = [
|
||||
{
|
||||
key: "name",
|
||||
|
|
@ -93,7 +69,7 @@ const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
|
|||
export function AlertsPage() {
|
||||
const [alerts, setAlerts] = useState<Alert[] | null>(null);
|
||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||
const firing = alerts?.filter((a) => a.state === FIRING) ?? null;
|
||||
const firing = alerts ? firingAlerts(alerts) : null;
|
||||
const [intervalMs, setIntervalMs] =
|
||||
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
|
||||
|
||||
|
|
|
|||
34
frontend/packages/swarm-ui/src/pages/alerts.ts
Normal file
34
frontend/packages/swarm-ui/src/pages/alerts.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// What the alerts page lists, apart from how it draws it: the shape of one
|
||||
// instance from swarm-controller's `GET /api/alerts` and which of them count
|
||||
// as firing.
|
||||
|
||||
export interface Alert {
|
||||
name: string;
|
||||
// Grafana's state for the instance: `Alerting`, `Pending`, `NoData` or
|
||||
// `Error`.
|
||||
state: string;
|
||||
active_at: string | null;
|
||||
summary: string | null;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
const FIRING = "Alerting";
|
||||
|
||||
// The instances the page shows: the ones Grafana says are firing now. Pending,
|
||||
// no-data and errored ones are left to Grafana's own list.
|
||||
export function firingAlerts(alerts: Alert[]): Alert[] {
|
||||
return alerts.filter((a) => a.state === FIRING);
|
||||
}
|
||||
|
||||
// Grafana reports an instance that never became active with the zero time,
|
||||
// which `Date.parse` reads as year 1.
|
||||
export function activeSinceMs(a: Alert): number | null {
|
||||
if (a.active_at === null) return null;
|
||||
const ms = Date.parse(a.active_at);
|
||||
return Number.isNaN(ms) || ms <= 0 ? null : ms;
|
||||
}
|
||||
|
||||
// A row needs a key that is unique among instances of the same rule.
|
||||
export function alertKey(a: Alert): string {
|
||||
return JSON.stringify(Object.entries(a.labels).sort());
|
||||
}
|
||||
68
frontend/packages/swarm-ui/test/alerts.test.mjs
Normal file
68
frontend/packages/swarm-ui/test/alerts.test.mjs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Which alert instances the alerts page lists. Run by `npm test`, which Node
|
||||
// runs with its built-in test runner and type stripping, so the `.ts` module
|
||||
// is imported as it is.
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { activeSinceMs, alertKey, firingAlerts } from "../src/pages/alerts.ts";
|
||||
|
||||
const alert = (name, state, extra = {}) => ({
|
||||
name,
|
||||
state,
|
||||
active_at: null,
|
||||
summary: null,
|
||||
labels: { instance: name },
|
||||
...extra,
|
||||
});
|
||||
|
||||
test("only instances in the Alerting state are listed", () => {
|
||||
const all = [
|
||||
alert("a", "Alerting"),
|
||||
alert("b", "Pending"),
|
||||
alert("c", "NoData"),
|
||||
alert("d", "Error"),
|
||||
alert("e", "Alerting"),
|
||||
];
|
||||
assert.deepEqual(
|
||||
firingAlerts(all).map((a) => a.name),
|
||||
["a", "e"],
|
||||
);
|
||||
});
|
||||
|
||||
test("nothing firing leaves the list empty, and the input is not changed", () => {
|
||||
const all = [alert("a", "Pending"), alert("b", "Error")];
|
||||
assert.deepEqual(firingAlerts(all), []);
|
||||
assert.equal(all.length, 2);
|
||||
assert.deepEqual(firingAlerts([]), []);
|
||||
});
|
||||
|
||||
test("the state match is exact, so an unknown or lower-case state is left out", () => {
|
||||
assert.deepEqual(firingAlerts([alert("a", "alerting"), alert("b", "")]), []);
|
||||
});
|
||||
|
||||
test("an instance that never became active has no active-since time", () => {
|
||||
assert.equal(activeSinceMs(alert("a", "Alerting")), null);
|
||||
assert.equal(
|
||||
activeSinceMs(
|
||||
alert("a", "Alerting", { active_at: "0001-01-01T00:00:00Z" }),
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
activeSinceMs(alert("a", "Alerting", { active_at: "not a date" })),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
activeSinceMs(
|
||||
alert("a", "Alerting", { active_at: "2026-10-18T08:00:00Z" }),
|
||||
),
|
||||
Date.parse("2026-10-18T08:00:00Z"),
|
||||
);
|
||||
});
|
||||
|
||||
test("instances of one rule get different keys, whatever the label order", () => {
|
||||
const a = alert("r", "Alerting", { labels: { x: "1", y: "2" } });
|
||||
const b = alert("r", "Alerting", { labels: { y: "2", x: "1" } });
|
||||
const c = alert("r", "Alerting", { labels: { x: "1", y: "3" } });
|
||||
assert.equal(alertKey(a), alertKey(b));
|
||||
assert.notEqual(alertKey(a), alertKey(c));
|
||||
});
|
||||
Loading…
Reference in a new issue