diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 4603eec3..d70c74ad 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -38,7 +38,9 @@ Everything it shows comes from [`swarm-controller`](../../swarm-controller/READM Pages that refresh on a timer share one polling rule: the page sends no new request while the last one is still running, so an older answer never overtakes a slow one, and a failed refresh keeps the rows already on screen under an -error. +error. While a page is fetching, its refresh picker shows a spinner in place of +the clock, for at least 0.4 seconds so a quick answer still reads as a refresh. +The Jobs page has none yet. The **wanted** badge changes the state of an agent the swarm has declared. For an agent the swarm never declared on its hive, such as one only its hive knows diff --git a/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx b/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx index fa6bf982..6f08dab0 100644 --- a/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AlertsPage.tsx @@ -75,7 +75,11 @@ function AlertsBody() { const [showAll, setShowAll] = useState(false); const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); - const { data: alerts, error } = usePolled(intervalMs, (signal) => + const { + data: alerts, + error, + refreshing, + } = usePolled(intervalMs, (signal) => fetchJson("/api/alerts", signal), ); const listed = alerts ? listedAlerts(alerts, showAll) : null; @@ -96,6 +100,7 @@ function AlertsBody() { id="alerts-refresh" value={intervalMs} onChange={setIntervalMs} + refreshing={refreshing} /> {error ? ( diff --git a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx index f1328eba..39c318e1 100644 --- a/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/ComponentsPage.tsx @@ -510,6 +510,14 @@ export function ComponentsPage() { + + undefined} + refreshing + /> +
diff --git a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx index 7b547161..27e83724 100644 --- a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx @@ -98,6 +98,7 @@ export function AgentsPage() { const { data: rows, error, + refreshing, setData: setRows, } = usePolled(intervalMs, (signal) => fetchJson("/api/agents/status", signal), @@ -131,6 +132,7 @@ export function AgentsPage() { id="agents-refresh" value={intervalMs} onChange={setIntervalMs} + refreshing={refreshing} /> ); diff --git a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx index 082a035d..13892a13 100644 --- a/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx +++ b/frontend/packages/swarm-ui/src/pages/broadcast/BroadcastComposer.tsx @@ -60,7 +60,11 @@ function storedFilter(): AgentFilter { export function BroadcastComposer() { const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); - const { data: agents, error: loadError } = usePolled(intervalMs, (signal) => + const { + data: agents, + error: loadError, + refreshing, + } = usePolled(intervalMs, (signal) => fetchJson("/api/agents/status", signal), ); const [filter, setFilterState] = useState(storedFilter); @@ -173,6 +177,7 @@ export function BroadcastComposer() { id="broadcast-refresh" value={intervalMs} onChange={setIntervalMs} + refreshing={refreshing} /> diff --git a/frontend/packages/swarm-ui/src/pages/builds/BuildsPage.tsx b/frontend/packages/swarm-ui/src/pages/builds/BuildsPage.tsx index 909325f8..0c6ff268 100644 --- a/frontend/packages/swarm-ui/src/pages/builds/BuildsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/builds/BuildsPage.tsx @@ -96,9 +96,11 @@ function BuildsBody() { const wanted = wantedRun(useSearch()); const [intervalMs, setIntervalMs] = useState(DEFAULT_INTERVAL_MS); - const { data: runs, error } = usePolled(intervalMs, (signal) => - fetchJson(RUNS, signal), - ); + const { + data: runs, + error, + refreshing, + } = usePolled(intervalMs, (signal) => fetchJson(RUNS, signal)); // Which cards are open. Survives a refresh, so a list that updates under // you does not fold the card you are reading. A linked build starts open. const [open, setOpen] = useState>( @@ -134,6 +136,7 @@ function BuildsBody() { id="builds-refresh" value={intervalMs} onChange={setIntervalMs} + refreshing={refreshing} />

diff --git a/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx b/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx index d779977e..c2bf805a 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/notifications/NotificationsPage.tsx @@ -41,18 +41,24 @@ export function NotificationRouter() { const { data: view, error, + refreshing: flowsRefreshing, 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), + const { + data: dry, + error: dryError, + refreshing: dryRefreshing, + } = usePolled(intervalMs, (signal) => fetchJson(DRY_RUN_URL, signal)); + const { + data: deliveries, + error: deliveriesError, + refreshing: deliveriesRefreshing, + } = usePolled(intervalMs, (signal) => + fetchJson(DELIVERIES_URL, signal), ); // A failed poll keeps the last answer; the counts are shown only while the // latest poll worked. @@ -102,6 +108,7 @@ export function NotificationRouter() { id="notifications-refresh" value={intervalMs} onChange={setIntervalMs} + refreshing={flowsRefreshing || dryRefreshing || deliveriesRefreshing} />