Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: show the notification router's flows and switch built-ins off

This commit is contained in:
atlas 2026-10-24 03:51:33 +02:00 • committed by mara
commit 0512862b3d

View file

@ -0,0 +1,144 @@
// <NotificationsPage> — the notification router's flows. Backed by
// swarm-controller's `GET/PUT /api/notify/flows`; the controller is the only
// writer of the stored document and the router reads it every few seconds.
//
// Read-only except for one switch per built-in flow. A flow draws as an
// indented tree, the shape the API validates it to. Custom flows are not
// editable here: they arrive through the API.
import { useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Card } from "../../ui/card/Card.js";
import { CheckboxField } from "../../ui/checkbox-field/CheckboxField.js";
import { Panel } from "../../ui/panel/Panel.js";
import {
RefreshIntervalPicker,
type RefreshIntervalMs,
} from "../../ui/refresh-interval/RefreshInterval.js";
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
import {
describeNode,
treeRows,
withBuiltin,
type Flow,
type FlowsView,
} from "./FlowTypes.js";
import "./NotificationsPage.css";
const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
function FlowTree({ flow }: { flow: Flow }) {
return (
<ul class="notify-flow-tree">
{treeRows(flow).map(({ node, depth }) => (
<li
class="notify-node"
key={node.id}
style={{ paddingLeft: `${depth * 1.5}em` }}
>
<span class="notify-node-kind">{node.kind}</span>
<span class="notify-node-text">{describeNode(node)}</span>
</li>
))}
</ul>
);
}
export function NotificationsPage() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const {
data: view,
error,
refresh,
} = usePolled(intervalMs, (signal) =>
fetchJson<FlowsView>("/api/notify/flows", signal),
);
const [pending, setPending] = useState(false);
const [actionError, setActionError] = useState<ProblemDetails | null>(null);
// Switch one built-in on or off: the whole document goes back with the
// revision this page read, so a change made elsewhere since is refused
// rather than overwritten.
async function setBuiltin(id: string, enabled: boolean) {
if (!view) return;
setPending(true);
setActionError(null);
try {
const r = await fetch("/api/notify/flows", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({
revision: view.revision,
disabled_builtins: withBuiltin(view.disabled_builtins, id, enabled),
flows: view.flows,
}),
});
if (!r.ok) setActionError(await readApiError(r));
} catch (err) {
setActionError({ detail: String(err) });
} finally {
setPending(false);
refresh();
}
}
return (
<Panel
title="notifications"
icon="🔔"
actions={
<RefreshIntervalPicker
id="notifications-refresh"
value={intervalMs}
onChange={setIntervalMs}
/>
}
>
{error ? (
<ApiErrorPanel context="failed to load the flows" problem={error} />
) : null}
{actionError ? (
<ApiErrorPanel
context="the flows were not changed"
problem={actionError}
/>
) : null}
{!error && view === null ? <p>loading…</p> : null}
{view ? (
<div class="notify-flows">
<h3>built-in flows</h3>
{view.builtins.map((flow) => {
const on = !view.disabled_builtins.includes(flow.id);
return (
<Card key={flow.id} class={on ? "" : "notify-off"}>
<div class="notify-flow-head">
<span class="notify-flow-name">{flow.name}</span>
<CheckboxField
id={`builtin-${flow.id}`}
label={on ? "on" : "off"}
checked={on}
disabled={pending}
onChange={(checked) => setBuiltin(flow.id, checked)}
/>
</div>
<FlowTree flow={flow} />
</Card>
);
})}
<h3>custom flows</h3>
{view.flows.length === 0 ? <p>no custom flows</p> : null}
{view.flows.map((flow) => (
<Card key={flow.id} class={flow.enabled ? "" : "notify-off"}>
<div class="notify-flow-head">
<span class="notify-flow-name">{flow.name}</span>
<span>{flow.enabled ? "on" : "off"}</span>
</div>
<FlowTree flow={flow} />
</Card>
))}
</div>
) : null}
</Panel>
);
}