4f8cf4cc63
## Summary The Runs list now updates live without requiring a page refresh. Status changes and other run fields are updated in place while runs are executing. When new runs matching the current filters are created, a "New runs created" refresh button appears above the list. Root runs now show a live child-run status breakdown directly in the status tooltip. ### List live update - Visible runs update in place while they are still running. - A "New runs created" refresh button appears when new matching runs are detected. - Polling stops when all visible runs have finished and a refresh button is already shown. - Polling pauses when the browser tab is not visible. - Runs list status updates and new-run detection share a single runs/live polling path. ### Child-status tooltip - Root run tooltips now display a breakdown of child run statuses. - Child statuses are loaded when the tooltip opens (after a 400ms hover delay). - The tooltip stays up to date while child runs are still changing state. - Handles cases where child runs continue running after their parent run has completed, or have not yet been created. ### Supporting changes - Added hidden-tab awareness to polling. - Added safeguards around polling inputs (`runIds` deduping and limits). ## Test plan - [x] pnpm run typecheck --filter webapp passes - [x] cd apps/webapp && pnpm run test ./test/presenters/mapRunToLiveFields.test.ts --run passes - [x] cd apps/webapp && pnpm run test ./test/runsRepository.part2.test.ts --run -t "hasNewRuns" passes ### Manual smoke: - [x] Active runs update without a page refresh. - [x] A new matching run shows the refresh banner and the banner actions work as expected. - [x] Root run tooltips show live child-status updates and stop polling once child runs settle. --------- Co-authored-by: Ekaterina Bulatova <kathiekiwi@Ekaterinas-MacBook-Pro.local>
70 lines
1.8 KiB
TypeScript
70 lines
1.8 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
|
|
type UseIntervalOptions = {
|
|
/** If passed, will refresh every interval MS */
|
|
interval?: number;
|
|
onLoad?: boolean;
|
|
onFocus?: boolean;
|
|
disabled?: boolean;
|
|
/** Skip interval ticks while the document tab is hidden */
|
|
pauseWhenHidden?: boolean;
|
|
callback: () => void;
|
|
};
|
|
|
|
export function useInterval({
|
|
interval,
|
|
onLoad = true,
|
|
onFocus = true,
|
|
disabled = false,
|
|
pauseWhenHidden = false,
|
|
callback,
|
|
}: UseIntervalOptions) {
|
|
// Always keep the latest callback in a ref so the effects below
|
|
// never close over a stale version.
|
|
const latestCallback = useRef(callback);
|
|
useEffect(() => {
|
|
latestCallback.current = callback;
|
|
}, [callback]);
|
|
|
|
// On interval
|
|
useEffect(() => {
|
|
if (!interval || interval <= 0 || disabled) return;
|
|
|
|
const intervalId = setInterval(() => {
|
|
if (pauseWhenHidden && document.visibilityState !== "visible") {
|
|
return;
|
|
}
|
|
latestCallback.current();
|
|
}, interval);
|
|
|
|
return () => clearInterval(intervalId);
|
|
}, [interval, disabled, pauseWhenHidden]);
|
|
|
|
// On focus
|
|
useEffect(() => {
|
|
if (!onFocus || disabled) return;
|
|
|
|
const handleFocus = () => {
|
|
if (document.visibilityState === "visible") {
|
|
latestCallback.current();
|
|
}
|
|
};
|
|
|
|
// Revalidate when the page becomes visible
|
|
document.addEventListener("visibilitychange", handleFocus);
|
|
// Revalidate when the window gains focus
|
|
window.addEventListener("focus", handleFocus);
|
|
|
|
return () => {
|
|
document.removeEventListener("visibilitychange", handleFocus);
|
|
window.removeEventListener("focus", handleFocus);
|
|
};
|
|
}, [onFocus, disabled]);
|
|
|
|
// On load
|
|
useEffect(() => {
|
|
if (disabled || !onLoad) return;
|
|
latestCallback.current();
|
|
}, [disabled, onLoad]);
|
|
}
|