Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: the refresh picker shows a spinner in place of its clock while its page is fetching

usePolled reports when a request is in flight (through a new onBusy of the
poller) and holds it on for at least 0.4 seconds, so a quick answer still
reads as a refresh. The picker swaps its clock for the spinner, in the same
box so its width stays, and the spinner stands still where motion is off.
Every page with a picker passes it; a page with several polled requests shows
it while any of them runs. The Jobs page fetches through shared components
and has none yet.
This commit is contained in:
iris 2026-11-05 23:09:07 +01:00
commit 47c447409b
9 changed files with 296 additions and 23 deletions

View file

@ -75,7 +75,11 @@ function AlertsBody() {
const [showAll, setShowAll] = useState(false);
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const { data: alerts, error } = usePolled(intervalMs, (signal) =>
const {
data: alerts,
error,
refreshing,
} = usePolled(intervalMs, (signal) =>
fetchJson<Alert[]>("/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}
/>
</div>
{error ? (

View file

@ -510,6 +510,14 @@ export function ComponentsPage() {
<Sample label="editable">
<RefreshIntervalPickerSample />
</Sample>
<Sample label="refreshing">
<RefreshIntervalPicker
id="sample-refresh-interval-busy"
value={30_000}
onChange={() => undefined}
refreshing
/>
</Sample>
</Section>
<Section title="TextField">

View file

@ -98,6 +98,7 @@ export function AgentsPage() {
const {
data: rows,
error,
refreshing,
setData: setRows,
} = usePolled(intervalMs, (signal) =>
fetchJson<AgentRow[]>("/api/agents/status", signal),
@ -131,6 +132,7 @@ export function AgentsPage() {
id="agents-refresh"
value={intervalMs}
onChange={setIntervalMs}
refreshing={refreshing}
/>
</>
);

View file

@ -60,7 +60,11 @@ function storedFilter(): AgentFilter {
export function BroadcastComposer() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const { data: agents, error: loadError } = usePolled(intervalMs, (signal) =>
const {
data: agents,
error: loadError,
refreshing,
} = usePolled(intervalMs, (signal) =>
fetchJson<BroadcastAgent[]>("/api/agents/status", signal),
);
const [filter, setFilterState] = useState<AgentFilter>(storedFilter);
@ -173,6 +177,7 @@ export function BroadcastComposer() {
id="broadcast-refresh"
value={intervalMs}
onChange={setIntervalMs}
refreshing={refreshing}
/>
</div>

View file

@ -96,9 +96,11 @@ function BuildsBody() {
const wanted = wantedRun(useSearch());
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const { data: runs, error } = usePolled(intervalMs, (signal) =>
fetchJson<BuildRun[]>(RUNS, signal),
);
const {
data: runs,
error,
refreshing,
} = usePolled(intervalMs, (signal) => fetchJson<BuildRun[]>(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<ReadonlySet<string>>(
@ -134,6 +136,7 @@ function BuildsBody() {
id="builds-refresh"
value={intervalMs}
onChange={setIntervalMs}
refreshing={refreshing}
/>
</div>
<p>

View file

@ -41,18 +41,24 @@ export function NotificationRouter() {
const {
data: view,
error,
refreshing: flowsRefreshing,
refresh,
} = usePolled(intervalMs, (signal) =>
fetchJson<FlowsView>("/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<DryRun>(DRY_RUN_URL, signal),
);
const { data: deliveries, error: deliveriesError } = usePolled(
intervalMs,
(signal) => fetchJson<DeliveriesView>(DELIVERIES_URL, signal),
const {
data: dry,
error: dryError,
refreshing: dryRefreshing,
} = usePolled(intervalMs, (signal) => fetchJson<DryRun>(DRY_RUN_URL, signal));
const {
data: deliveries,
error: deliveriesError,
refreshing: deliveriesRefreshing,
} = usePolled(intervalMs, (signal) =>
fetchJson<DeliveriesView>(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}
/>
<Button variant="primary" onClick={() => draftFrom()}>
create flow

View file

@ -93,7 +93,11 @@ function RunCard({
export function ReviewRuns() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const { data: runs, error } = usePolled(intervalMs, (signal) =>
const {
data: runs,
error,
refreshing,
} = usePolled(intervalMs, (signal) =>
fetchJson<ClassifierRun[]>(RUNS, signal),
);
// Which cards are expanded. Survives a refresh, so a list that updates
@ -117,6 +121,7 @@ export function ReviewRuns() {
id="review-runs-refresh"
value={intervalMs}
onChange={setIntervalMs}
refreshing={refreshing}
/>
}
>

View file

@ -64,6 +64,7 @@ function SchedulesBody() {
const {
data: schedules,
error,
refreshing: schedulesRefreshing,
refresh,
} = usePolled(intervalMs, (signal) =>
fetchJson<Schedule[]>("/api/schedules", signal),
@ -72,7 +73,11 @@ function SchedulesBody() {
// lists, polled on the same cadence so an agent created after this page
// opened shows up. A failed load does not block the page, the cards do not
// need it; the form says the list could not load.
const { data: roster, error: rosterError } = usePolled(intervalMs, (signal) =>
const {
data: roster,
error: rosterError,
refreshing: rosterRefreshing,
} = usePolled(intervalMs, (signal) =>
fetchJson<AgentRow[]>("/api/agents/status", signal),
);
const agentNames = useMemo(() => agentNamesFrom(roster), [roster]);
@ -159,6 +164,7 @@ function SchedulesBody() {
id="schedules-refresh"
value={intervalMs}
onChange={setIntervalMs}
refreshing={schedulesRefreshing || rosterRefreshing}
/>
</div>
{error ? (

View file

@ -116,7 +116,11 @@ const DEFAULT_INTERVAL_MS: RefreshIntervalMs = 30_000;
function HiveRoster() {
const [intervalMs, setIntervalMs] =
useState<RefreshIntervalMs>(DEFAULT_INTERVAL_MS);
const { data: hives, error } = usePolled(intervalMs, (signal) =>
const {
data: hives,
error,
refreshing,
} = usePolled(intervalMs, (signal) =>
fetchJson<HiveStatus[]>("/api/hives/status", signal),
);
@ -127,6 +131,7 @@ function HiveRoster() {
id="hives-refresh"
value={intervalMs}
onChange={setIntervalMs}
refreshing={refreshing}
/>
</div>
{error ? (