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:
parent
89bbeb9757
commit
47c447409b
1 changed files with 296 additions and 23 deletions
|
|
@ -1,4 +1,4 @@
|
|||
// A resource refetched on a timer: `{ data, error, refresh, setData }`.
|
||||
// A resource refetched on a timer: `{ data, error, refreshing, refresh, setData }`.
|
||||
// `data` is `null` until the first answer; a failed fetch sets `error` and
|
||||
// keeps the last good `data`; the next good answer clears `error`. The
|
||||
// request discipline (skip a tick while one is in flight, drop answers that
|
||||
|
|
@ -8,12 +8,15 @@
|
|||
// does that for a GET. It may change between renders: the latest one runs.
|
||||
// `refresh` refetches now, dropping a request in flight, for after a change
|
||||
// the page made. `setData` patches what is shown without a request.
|
||||
// `refreshing` is true while a request is in flight, and for a short minimum
|
||||
// after it starts (`createBusyHold`), for the refresh picker's spinner.
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import {
|
||||
useRefreshInterval,
|
||||
type RefreshIntervalMs,
|
||||
} from "../refresh-interval/RefreshInterval.js";
|
||||
import { createBusyHold } from "./busy-hold.js";
|
||||
import { createPoller, type Poller } from "./poller.js";
|
||||
|
||||
/** A failed request, carrying the problem the API answered with. */
|
||||
|
|
@ -43,15 +46,22 @@ export function usePolled<T>(
|
|||
): {
|
||||
data: T | null;
|
||||
error: ProblemDetails | null;
|
||||
refreshing: boolean;
|
||||
refresh: () => void;
|
||||
setData: (update: (prev: T | null) => T | null) => void;
|
||||
} {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetcherRef = useRef(fetcher);
|
||||
fetcherRef.current = fetcher;
|
||||
const pollerRef = useRef<Poller | null>(null);
|
||||
const holdRef = useRef<ReturnType<typeof createBusyHold> | null>(null);
|
||||
if (holdRef.current === null) {
|
||||
holdRef.current = createBusyHold({ show: setRefreshing });
|
||||
}
|
||||
const hold = holdRef.current;
|
||||
if (pollerRef.current === null) {
|
||||
pollerRef.current = createPoller<T>({
|
||||
fetch: (signal) => fetcherRef.current(signal),
|
||||
|
|
@ -60,12 +70,19 @@ export function usePolled<T>(
|
|||
setError(null);
|
||||
},
|
||||
onError: (e) => setError(toProblem(e)),
|
||||
onBusy: (busy) => hold.busy(busy),
|
||||
});
|
||||
}
|
||||
const poller = pollerRef.current;
|
||||
|
||||
useEffect(() => () => poller.stop(), [poller]);
|
||||
useEffect(
|
||||
() => () => {
|
||||
poller.stop();
|
||||
hold.stop();
|
||||
},
|
||||
[poller, hold],
|
||||
);
|
||||
useRefreshInterval(intervalMs, () => poller.tick());
|
||||
|
||||
return { data, error, refresh: () => poller.refetch(), setData };
|
||||
return { data, error, refreshing, refresh: () => poller.refetch(), setData };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue