Watch
0
0
Fork
You've already forked hyperhive
0

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.
This commit is contained in:
iris 2026-10-26 21:16:05 +01:00 • committed by mara
commit 599747bacb
4 changed files with 96 additions and 43 deletions

View file

@ -1,4 +1,4 @@
// <AlertsPage> — `/alerts`' content: the alerts the swarm's Grafana holds
// <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,
@ -7,7 +7,7 @@ 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 { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
import { RelativeTime } from "../ui/relative-time/RelativeTime.js";
import {
RefreshIntervalPicker,
@ -71,7 +71,7 @@ const COLUMNS: TableColumn<Alert>[] = [
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
export function AlertsPage() {
function AlertsBody() {
const [showAll, setShowAll] = useState(false);
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
@ -82,31 +82,22 @@ export function AlertsPage() {
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}
/>
</>
}
>
<>
<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"
@ -125,6 +116,19 @@ export function AlertsPage() {
storageKey="swarm-ui:alerts:table-filters"
/>
) : null}
</Panel>
</>
);
}
// 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>
);
}

View file

@ -1,4 +1,4 @@
// <IssueReportPage> — mara's custom issue report: browse open issues
// <IssueReportCard> — a card of the reports tab: mara's custom issue report: browse open issues
// across every repo that has one, sortable/filterable her own way, with
// "hide blocked (open dependency)" as the headline filter and a rank
// column for how many other open issues depend on each one. Backed by
@ -32,7 +32,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { useLocalSetting } from "@hive/shared/settings-storage.js";
import { Panel } from "../ui/panel/Panel.js";
import { LazyDisclosure } from "../ui/disclosure/LazyDisclosure.js";
import { CheckboxField } from "../ui/checkbox-field/CheckboxField.js";
import { SelectField } from "../ui/select-field/SelectField.js";
import { Table, type TableColumn } from "../ui/table/Table.js";
@ -128,7 +128,7 @@ function SortHeader({
);
}
export function IssueReportPage() {
function IssueReportBody() {
const [repos, setRepos] = useState<string[] | null>(null);
const [repoFilter, setRepoFilter] = useLocalSetting(
REPO_FILTER_KEY,
@ -370,7 +370,7 @@ export function IssueReportPage() {
];
return (
<Panel title="issue report" icon="📋">
<>
<div class="issue-report-controls">
<SelectField
id="issue-report-repo"
@ -408,6 +408,20 @@ export function IssueReportPage() {
storageKey={TABLE_FILTERS_KEY}
/>
) : null}
</Panel>
</>
);
}
// A folded card on the reports tab: the repo list and the report are read only
// once it is opened.
export function IssueReportCard() {
return (
<LazyDisclosure
title="issue report"
icon="📋"
hint="open issues across every repo, ranked by what they block"
>
<IssueReportBody />
</LazyDisclosure>
);
}

View file

@ -0,0 +1,16 @@
/* The reports tab: the folded cards, one gap apart. */
.ui-reports-page {
display: flex;
flex-direction: column;
gap: 1em;
}
/* The bar at the top of the alerts card: the Grafana link, the show-more
button and the refresh picker, to the right. */
.ui-alerts-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.5em 0.75em;
margin-block-end: 0.5em;
}

View file

@ -0,0 +1,15 @@
// <ReportsPage> — `/reports`: what the swarm reports about itself, as two
// folded cards: the alerts Grafana holds now, and mara's issue report. Neither
// reads anything until it is opened.
import { AlertsCard } from "../AlertsPage.js";
import { IssueReportCard } from "../IssueReportPage.js";
import "./ReportsPage.css";
export function ReportsPage() {
return (
<div class="ui-reports-page">
<AlertsCard />
<IssueReportCard />
</div>
);
}