feat: live-refresh stack detail container and health state (#1712)

* feat: live-refresh stack detail container and health state

Keep the open stack's container cards in sync with Docker via state-invalidate events and a visibility-aware poll, without reloading compose, env, or logs.

* fix: remove unused _ms parameter from visibilityInterval mock

Fixes the @typescript-eslint/no-unused-vars ESLint error in CI lint job.

* fix: stop stack detail live-refresh when leaving the editor

Gate poll and invalidate handling on editor visibility, refresh the
current selection after a mid-flight stack switch, and skip starting
visibilityInterval when the tab is already hidden.

* fix: avoid return in finally for stack detail live-refresh

Satisfy no-unsafe-finally by gating the trailing refresh with a positive
condition instead of early returns inside the finally block.
This commit is contained in:
Anso
2026-07-28 09:24:18 -04:00
committed by GitHub
parent 681ecc7047
commit 60092dd462
11 changed files with 946 additions and 16 deletions
+16 -3
View File
@@ -17,6 +17,7 @@ import { useUrlSync } from './EditorLayout/hooks/useUrlSync';
import { shouldClearPendingDetailStack } from './EditorLayout/mobile-pending-detail';
import { useOverlayState } from './EditorLayout/hooks/useOverlayState';
import { useStackActions, NODE_SWITCH_PENDING_TOKEN } from './EditorLayout/hooks/useStackActions';
import { useSelectedStackLiveRefresh } from './EditorLayout/hooks/useSelectedStackLiveRefresh';
import { useTheme } from '@/hooks/use-theme';
import { ThemeQuickSwitch } from './theme/ThemeQuickSwitch';
import { useNotifications } from './EditorLayout/hooks/useNotifications';
@@ -296,6 +297,16 @@ export default function EditorLayout() {
// Wire the ref now that stackActions is available
resetEditorStateRef.current = stackActions.resetEditorState;
const { syncStale: containersSyncStale, retrySync: retryContainersSync } = useSelectedStackLiveRefresh({
selectedFile,
activeNodeId: activeNode?.id,
isDetailVisible: activeView === 'editor',
containers,
composeContent: content,
containersLoadStatus,
refreshSelectedContainers: stackActions.refreshSelectedContainers,
});
// A failed health gate routes into the existing recovery affordance: record
// a failure for the stack so RecoveryChip/RecoveryPanel offer the same
// explicit, user-confirmed rollback as any failed operation. Keyed by gate
@@ -623,6 +634,8 @@ export default function EditorLayout() {
containersLoadStatus={containersLoadStatus}
containersLoadError={containersLoadError}
onRetryContainersLoad={() => { void stackActions.retryContainersLoad(); }}
containersSyncStale={containersSyncStale}
onRetrySync={retryContainersSync}
containerStats={containerStats}
containerStatsError={containerStatsError}
content={content}
@@ -684,10 +697,10 @@ export default function EditorLayout() {
onRefreshState={async () => {
if (!selectedFile) return;
const name = selectedFile.replace(/\.(yml|yaml)$/, '');
const ok = await stackActions.refreshSelectedContainers(name, selectedFile);
const outcome = await stackActions.refreshSelectedContainers(name, selectedFile);
await refreshStacks(true);
if (ok) toast.success('Refreshed container state.');
else toast.error('Could not refresh container state.');
if (outcome === 'ok') toast.success('Refreshed container state.');
else if (outcome === 'failed') toast.error('Could not refresh container state.');
}}
onDismissRecovery={() => { if (selectedFile) dismissActionResult(selectedFile); }}
panelStartedAt={panelStartedAt}