refactor(frontend): EditorLayout final shell (B4-7) (#906)

* refactor(frontend): extract useOverlayState hook from EditorLayout

* refactor(frontend): extract useStackActions hook and wire useOverlayState into EditorLayout

* refactor(frontend): fix quality issues in useStackActions post-review

* fix(frontend): fix interval leak, RunResult contract, yml hardcode, and loadFile length in useStackActions

* refactor(frontend): extract useSidebarContextMenu hook from EditorLayout

* refactor(frontend): extract ShellOverlays component from EditorLayout

* refactor(frontend): relocate Monaco layout effect and log-viewer event listener out of EditorLayout

The Monaco tab-switch layout effect is now self-contained in EditorView,
alongside its monacoEditorRef. The SENCHO_OPEN_LOGS_EVENT listener moves
into useOverlayState, where openLogViewer lives. EditorLayout is left with
the two coordination effects that depend on cross-hook state.
This commit is contained in:
Anso
2026-05-04 07:43:02 -04:00
committed by GitHub
parent 34fcb2591f
commit 1cf996142b
7 changed files with 1491 additions and 1102 deletions
@@ -0,0 +1,102 @@
import { useState, useCallback, useEffect } from 'react';
import { SENCHO_OPEN_LOGS_EVENT } from '@/lib/events';
import type { SenchoOpenLogsDetail } from '@/lib/events';
import type { PolicyBlockPayload } from '../../stack/PolicyBlockDialog';
import type { Node } from '@/context/NodeContext';
type DiffPreview = {
mode: 'save' | 'save-and-deploy';
language: 'yaml' | 'ini';
original: string;
modified: string;
fileName: string;
};
type PolicyBlock = { stackName: string; payload: PolicyBlockPayload };
type Container = { id: string; name: string };
export function useOverlayState() {
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [stackToDelete, setStackToDelete] = useState<string | null>(null);
const openDeleteDialog = useCallback((stackName: string) => {
setStackToDelete(stackName);
setDeleteDialogOpen(true);
}, []);
const closeDeleteDialog = useCallback(() => {
setDeleteDialogOpen(false);
setStackToDelete(null);
}, []);
const [pendingUnsavedLoad, setPendingUnsavedLoad] = useState<string | null>(null);
const [pendingUnsavedNode, setPendingUnsavedNode] = useState<Node | null>(null);
const [bashModalOpen, setBashModalOpen] = useState(false);
const [selectedContainer, setSelectedContainer] = useState<Container | null>(null);
const openBashModal = useCallback((container: Container) => {
setSelectedContainer(container);
setBashModalOpen(true);
}, []);
const closeBashModal = useCallback(() => {
setBashModalOpen(false);
setSelectedContainer(null);
}, []);
const [logViewerOpen, setLogViewerOpen] = useState(false);
const [logContainer, setLogContainer] = useState<Container | null>(null);
const openLogViewer = useCallback((container: Container) => {
setLogContainer(container);
setLogViewerOpen(true);
}, []);
const closeLogViewer = useCallback(() => {
setLogViewerOpen(false);
setLogContainer(null);
}, []);
// Listen for topology click-to-logs events and open the log viewer.
// openLogViewer is stable (useCallback with empty deps), so this effect
// mounts/unmounts once and never re-registers.
useEffect(() => {
const handler = (e: Event) => {
const { containerId, containerName } = (e as CustomEvent<SenchoOpenLogsDetail>).detail;
openLogViewer({ id: containerId, name: containerName });
};
window.addEventListener(SENCHO_OPEN_LOGS_EVENT, handler);
return () => window.removeEventListener(SENCHO_OPEN_LOGS_EVENT, handler);
}, [openLogViewer]); // openLogViewer is stable (useCallback with empty deps)
const [alertSheetOpen, setAlertSheetOpen] = useState(false);
const [alertSheetStack, setAlertSheetStack] = useState('');
const [autoHealStackName, setAutoHealStackName] = useState<string | null>(null);
const openAlertSheet = useCallback((stackName: string, autoHeal?: string | null) => {
setAlertSheetStack(stackName);
setAutoHealStackName(autoHeal ?? null);
setAlertSheetOpen(true);
}, []);
const closeAlertSheet = useCallback(() => setAlertSheetOpen(false), []);
const [policyBlock, setPolicyBlock] = useState<PolicyBlock | null>(null);
const [policyBypassing, setPolicyBypassing] = useState(false);
const [stackMisconfigScanId, setStackMisconfigScanId] = useState<number | null>(null);
const [diffPreview, setDiffPreview] = useState<DiffPreview | null>(null);
const [diffPreviewConfirming, setDiffPreviewConfirming] = useState(false);
return {
createDialogOpen, setCreateDialogOpen,
deleteDialogOpen, stackToDelete, openDeleteDialog, closeDeleteDialog,
pendingUnsavedLoad, setPendingUnsavedLoad,
pendingUnsavedNode, setPendingUnsavedNode,
bashModalOpen, selectedContainer, openBashModal, closeBashModal,
logViewerOpen, logContainer, openLogViewer, closeLogViewer,
alertSheetOpen, alertSheetStack, autoHealStackName, openAlertSheet, closeAlertSheet,
setAutoHealStackName,
policyBlock, setPolicyBlock, policyBypassing, setPolicyBypassing,
stackMisconfigScanId, setStackMisconfigScanId,
diffPreview, setDiffPreview, diffPreviewConfirming, setDiffPreviewConfirming,
} as const;
}
export type OverlayState = ReturnType<typeof useOverlayState>;