Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: one polled-resource hook for the refreshing pages

Each polling page repeated its own fetch-and-set code on a fixed timer,
so a slow answer could be overtaken by the next tick, an older response
could overwrite a newer one, and nothing aborted a request when the page
went away.

usePolled skips a tick while a request is in flight, aborts on unmount
or refresh, ignores answers from dropped requests, and keeps the last
good data under an error. The alerts, hives, agents, schedules, builds,
review runs and broadcast pages and the subagent list use it.
This commit is contained in:
iris 2026-10-21 22:03:59 +02:00 • committed by mara
commit 8917ba27b6
12 changed files with 274 additions and 160 deletions

View file

@ -28,7 +28,6 @@
// here just says which one to show.
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 { Badge } from "@hive/shared/badge.js";
import { LinkIcon } from "@hive/shared/icons.js";
import { useAgentActions } from "../../agent/roster/AgentActions.js";
@ -42,9 +41,9 @@ import { Panel } from "../../ui/panel/Panel.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import {
RefreshIntervalPicker,
useRefreshInterval,
type RefreshIntervalMs,
} from "../../ui/refresh-interval/RefreshInterval.js";
import { fetchJson, usePolled } from "../../ui/polled/usePolled.js";
import { SplitView } from "../../ui/split-view/SplitView.js";
import { type TableColumn } from "../../ui/table/Table.js";
import { CreateAgentForm } from "../CreateAgentForm.js";
@ -62,10 +61,15 @@ type ViewMode = "cards" | "table";
const VIEW_MODE_KEY = "swarm-ui:agents:view-mode";
export function AgentsPage() {
const [rows, setRows] = useState<AgentRow[] | null>(null);
const [error, setError] = useState<ProblemDetails | null>(null);
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const {
data: rows,
error,
setData: setRows,
} = usePolled(intervalMs, (signal) =>
fetchJson<AgentRow[]>("/api/agents/status", signal),
);
// Lazy initializer (not a mount effect) so the very first render
// already reflects the stored choice — no default-then-flip flash.
const [viewMode, setViewModeState] = useState<ViewMode>(() => {
@ -116,23 +120,6 @@ export function AgentsPage() {
const [detailTargetName, setDetailTargetName] = useState<string | null>(null);
const detailTarget = rows?.find((r) => r.name === detailTargetName) ?? null;
async function refresh() {
const res = await fetch("/api/agents/status");
if (!res.ok) {
setError(await readApiError(res));
return;
}
setRows((await res.json()) as AgentRow[]);
// A refresh that succeeds clears a previous failure — otherwise a
// transient error would sit on screen forever after the data itself
// has recovered.
setError(null);
}
useRefreshInterval(intervalMs, () => {
refresh().catch((e: unknown) => setError({ detail: String(e) }));
});
// `FilterableView`'s `columns` — the full field set (including
// per-column sort/filter, which drives both the table's own popovers
// and the card view's filter bar now — see that component's own