refactor(frontend): extract useImageUpdates hook from EditorLayout (#831)

EditorLayout owned the stack-image-update state plus a 5-minute
polling interval as part of a 30-line useEffect that already
juggled six other concerns (selected file, active view, stacks
refresh, auto-update settings, git-source pending, …). The image-
update slice has clean boundaries: it depends on activeNode.id,
mutates one state object, and is otherwise unrelated to the rest
of the effect.

Move it into a dedicated hook at frontend/src/hooks/useImageUpdates.ts.
The hook owns the stackUpdates state, runs an initial fetch on
activeNode.id change, schedules the 5-minute poll, and exposes a
refresh() callback for the four manual-trigger sites
(deploy success, image-update action, manual registry-refresh
poll). The hook destructure aliases refresh to fetchImageUpdates so
existing call sites in EditorLayout don't need to be renamed.

This is the first slice of audit finding 1.6 (EditorLayout 3129-line
refactor); the next slice is useFleetNotifications.
This commit is contained in:
Anso
2026-04-28 10:30:22 -04:00
committed by GitHub
parent 46fae21e67
commit 3f2ff47c94
2 changed files with 48 additions and 18 deletions
+4 -18
View File
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useMemo, useCallback, Suspense } from 'rea
type Theme = 'light' | 'dark' | 'auto';
import { Editor } from '@/lib/monacoLoader';
import { useImageUpdates } from '@/hooks/useImageUpdates';
import TerminalComponent from './Terminal';
import ErrorBoundary from './ErrorBoundary';
import HomeDashboard from './HomeDashboard';
@@ -354,7 +355,7 @@ export default function EditorLayout() {
// Image update checker state
const [stackUpdates, setStackUpdates] = useState<Record<string, boolean>>({});
const { stackUpdates, refresh: fetchImageUpdates } = useImageUpdates(activeNode?.id);
const [autoUpdateSettings, setAutoUpdateSettings] = useState<Record<string, boolean>>({});
const isAdmiral = license?.variant === 'admiral';
@@ -824,13 +825,10 @@ export default function EditorLayout() {
}
refreshStacks();
fetchImageUpdates();
// Image-update fetching + 5-minute poll are owned by useImageUpdates,
// which mirrors this effect's activeNode.id dependency.
fetchAutoUpdateSettings();
refreshGitSourcePending();
// Poll for image update results every 5 minutes so background checks are picked up
const imageUpdateInterval = setInterval(fetchImageUpdates, 5 * 60 * 1000);
return () => clearInterval(imageUpdateInterval);
}, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps
const fetchNotifications = async () => {
@@ -878,18 +876,6 @@ export default function EditorLayout() {
return () => clearInterval(id);
}, []);
const fetchImageUpdates = async () => {
try {
const res = await apiFetch('/image-updates');
if (res.ok) {
const data = await res.json();
setStackUpdates(data);
}
} catch (e: unknown) {
console.error('[ImageUpdates] fetch failed:', e);
}
};
const fetchAutoUpdateSettings = async () => {
try {
const res = await apiFetch('/stacks/auto-update-settings');
+44
View File
@@ -0,0 +1,44 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { apiFetch } from '@/lib/api';
const IMAGE_UPDATE_POLL_MS = 5 * 60 * 1000;
/**
* Owns the stack-image-update state and its 5-minute background poll.
* Re-fetches whenever `activeNodeId` changes; consumers can also call
* `refresh()` to force a refetch (e.g. after a deploy or a manual
* registry-check trigger).
*
* Extracted from EditorLayout so the polling lifecycle and its state
* live next to each other instead of being spread across a 3000-line
* component. The dependency on `apiFetch` keeps the call routed
* through the active-node header just like before.
*/
export function useImageUpdates(activeNodeId: number | undefined) {
const [stackUpdates, setStackUpdates] = useState<Record<string, boolean>>({});
const refresh = useCallback(async () => {
try {
const res = await apiFetch('/image-updates');
if (res.ok) {
const data = await res.json() as Record<string, boolean>;
setStackUpdates(data);
}
} catch (e: unknown) {
console.error('[ImageUpdates] fetch failed:', e);
}
}, []);
// Pin the interval to the latest closure without retriggering it on
// every render the way putting `refresh` into the deps array would.
const refreshRef = useRef(refresh);
refreshRef.current = refresh;
useEffect(() => {
void refreshRef.current();
const id = setInterval(() => { void refreshRef.current(); }, IMAGE_UPDATE_POLL_MS);
return () => clearInterval(id);
}, [activeNodeId]);
return { stackUpdates, refresh };
}