144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
// <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>
|
|
);
|
|
}
|