// — 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. // // A flow draws as its graph, read-only. A built-in flow has a switch and can // be duplicated; a custom flow is edited on its own page (). // A new flow and a duplicate both open that page on a draft: nothing is created // until it is saved there. import { useState } from "preact/hooks"; import { useLocation } from "wouter-preact"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { Button } from "../../ui/button/Button.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 { Deliveries } from "./Deliveries.js"; import { NEW_FLOW_PATH } from "./FlowDraft.js"; import { FlowGraph } from "./FlowGraph.js"; import { withBuiltin, type FlowsView } from "./FlowTypes.js"; import { DELIVERIES_URL, DRY_RUN_URL, hitsOf, unavailableText, type Deliveries as DeliveriesView, type DryRun, } from "./RouterLive.js"; import "./NotificationsPage.css"; const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000; export function NotificationsPage() { const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); const { data: view, error, refresh, } = usePolled(intervalMs, (signal) => fetchJson("/api/notify/flows", signal), ); // The router's own answers. Either is 503 while the running router predates // it, which the page says in place of the section. const { data: dry, error: dryError } = usePolled(intervalMs, (signal) => fetchJson(DRY_RUN_URL, signal), ); const { data: deliveries, error: deliveriesError } = usePolled( intervalMs, (signal) => fetchJson(DELIVERIES_URL, signal), ); // A failed poll keeps the last answer; the counts are shown only while the // latest poll worked. const shown = dryError ? null : dry; const [pending, setPending] = useState(false); const [actionError, setActionError] = useState(null); const [, navigate] = useLocation(); // Switch one built-in on or off: the whole document goes back with the // revision this page read (0 for none), 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(); } } // Open the editor on a draft: empty, or a copy of the flow `from`. const draftFrom = (from?: string) => navigate( from === undefined ? NEW_FLOW_PATH : `${NEW_FLOW_PATH}?from=${encodeURIComponent(from)}`, ); return ( } > {error ? ( ) : null} {actionError ? ( ) : null} {!error && view === null ?

loading…

: null} {view ? (
{dryError ? (

{unavailableText("hit counts", dryError)}

) : dry ? (

counts: events that reached each node, of the newest {dry.events}

) : null}

built-in flows

{view.builtins.map((flow) => { const on = !view.disabled_builtins.includes(flow.id); return (
{flow.name} setBuiltin(flow.id, checked)} />
); })}

custom flows

{view.flows.length === 0 ?

no custom flows

: null} {view.flows.map((flow) => (
{flow.name} {flow.enabled ? "on" : "off"}
))}

recent deliveries

{deliveriesError ? (

{unavailableText("the deliveries log", deliveriesError)}

) : deliveries ? ( ) : (

loading…

)}
) : null}
); }