diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 55dcdf94..138bda13 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -5,14 +5,9 @@ import { Editor } from '@/lib/monacoLoader'; import { useImageUpdates } from '@/hooks/useImageUpdates'; import TerminalComponent from './Terminal'; import ErrorBoundary from './ErrorBoundary'; -import HomeDashboard from './HomeDashboard'; import type { NotificationItem } from './dashboard/types'; import BashExecModal from './BashExecModal'; import LazyBoundary from './LazyBoundary'; -import { Skeleton } from '@/components/ui/skeleton'; -import { AdmiralGate } from './AdmiralGate'; -import { CapabilityGate } from './CapabilityGate'; -import ResourcesView from './ResourcesView'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter, DialogTrigger } from './ui/dialog'; @@ -37,58 +32,24 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSepara import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { TopBar } from './TopBar'; import { cn } from '@/lib/utils'; -import { SettingsPage } from './settings/SettingsPage'; import type { SectionId } from './settings/types'; +import { ViewRouter } from './EditorLayout/ViewRouter'; import { StackAlertSheet } from './StackAlertSheet'; import { StackAutoHealSheet } from '@/components/StackAutoHealSheet'; import { GitSourcePanel } from './stack/GitSourcePanel'; -import { AppStoreView } from './AppStoreView'; import { LogViewer } from './LogViewer'; import StructuredLogViewer from './StructuredLogViewer'; import StackAnatomyPanel from './StackAnatomyPanel'; import { Sparkline } from './ui/sparkline'; import type { ScheduleTaskPrefill } from './ScheduledOperationsView'; -// Paid-tier views and the security-history overlay are loaded on demand. -// Their internal PaidGate / AdmiralGate / CapabilityGate wrappers render -// the upsell or capability-missing card with blurred children rather than -// short-circuiting, so a tier-locked or capability-missing operator -// opening one of these tabs still triggers the chunk fetch to render the -// blurred preview. The gate-short-circuit fix lives in a follow-up -// branch. What this lazy split closes is the much larger initial-bundle -// leak: every Community user used to download the full FleetView, -// AuditLogView, etc. on first page load even if they never clicked those -// tabs. After this change, the chunks fetch only on tab open. -// -// GlobalObservabilityView is a free-tier feature with no internal gate; -// it is split here purely for the bundle-size win, not for IP protection. -const HostConsole = lazy(() => import('./HostConsole')); -const GlobalObservabilityView = lazy(() => - import('./GlobalObservabilityView').then(m => ({ default: m.GlobalObservabilityView })), -); -const FleetView = lazy(() => - import('./FleetView').then(m => ({ default: m.FleetView })), -); -const AuditLogView = lazy(() => - import('./AuditLogView').then(m => ({ default: m.AuditLogView })), -); +// SecurityHistoryView is the only lazy-loaded view that lives outside +// the ViewRouter switch — it renders as an overlay sheet wired into the +// settings flow, not as a top-level tab. The other tab-level lazy views +// (HostConsole, FleetView, AuditLogView, etc.) live inside ViewRouter. const SecurityHistoryView = lazy(() => import('./SecurityHistoryView').then(m => ({ default: m.SecurityHistoryView })), ); -const ScheduledOperationsView = lazy(() => import('./ScheduledOperationsView')); -const AutoUpdateReadinessView = lazy(() => import('./AutoUpdateReadinessView')); - -// Sized for the main workspace area (flex-1 with p-6 padding). Visible -// only during the brief window between an unlocked view's chunk request -// and its first render. -function ViewSkeleton() { - return ( -
- - -
- ); -} import { SENCHO_NAVIGATE_EVENT } from './NodeManager'; import type { SenchoNavigateDetail } from './NodeManager'; import { NodeSwitcher } from './NodeSwitcher'; @@ -2512,26 +2473,34 @@ export default function EditorLayout() { {/* Main Workspace */}
- {activeView === 'settings' ? ( - - ) : activeView === 'templates' ? ( - { refreshStacks(); loadFile(stackName); }} /> - ) : activeView === 'resources' ? ( - - ) : activeView === 'host-console' ? ( - - - - }> - setActiveView(selectedFile ? 'editor' : 'dashboard')} /> - - - - - ) : !isLoading && selectedFile && activeView === 'editor' ? ( + { refreshStacks(); loadFile(stackName); }} + onHostConsoleClose={() => setActiveView(selectedFile ? 'editor' : 'dashboard')} + onFleetNavigateToNode={(nodeId, stackName) => { + const node = nodes.find(n => n.id === nodeId); + if (node) { + if (activeNode?.id === nodeId) { + loadFile(stackName); + } else { + pendingStackLoadRef.current = stackName; + setActiveNode(node); + } + } + }} + filterNodeId={filterNodeId} + onClearScheduledOpsFilter={() => setFilterNodeId(null)} + schedulePrefill={schedulePrefill} + onPrefillConsumed={handlePrefillConsumed} + notifications={notifications} + onNavigateToStack={(stackFile) => { loadFile(stackFile); }} + onOpenSettingsSection={(section) => handleOpenSettings(section)} + onClearNotifications={clearAllNotifications} + renderEditor={() => (
{/* Left column: identity + health strip + logs, stacked */} @@ -3066,67 +3035,8 @@ export default function EditorLayout() { )}
- ) : activeView === 'global-observability' ? ( - - }> - - - - ) : activeView === 'fleet' ? ( - - - }> - { - const node = nodes.find(n => n.id === nodeId); - if (node) { - if (activeNode?.id === nodeId) { - loadFile(stackName); - } else { - pendingStackLoadRef.current = stackName; - setActiveNode(node); - } - } - }} /> - - - - ) : activeView === 'audit-log' ? ( - - - }> - - - - - ) : activeView === 'auto-updates' ? ( - - - }> - - - - - ) : activeView === 'scheduled-ops' ? ( - - - }> - setFilterNodeId(null)} - prefill={schedulePrefill} - onPrefillConsumed={handlePrefillConsumed} - /> - - - - ) : ( - { loadFile(stackFile); }} - onOpenSettingsSection={(section) => handleOpenSettings(section)} - notifications={notifications} - onClearNotifications={clearAllNotifications} - /> - )} + )} + />
diff --git a/frontend/src/components/EditorLayout/ViewRouter.tsx b/frontend/src/components/EditorLayout/ViewRouter.tsx new file mode 100644 index 00000000..eaa81fc8 --- /dev/null +++ b/frontend/src/components/EditorLayout/ViewRouter.tsx @@ -0,0 +1,205 @@ +import { Suspense, lazy, type ReactNode } from 'react'; +import { Skeleton } from '@/components/ui/skeleton'; +import { AdmiralGate } from '../AdmiralGate'; +import { CapabilityGate } from '../CapabilityGate'; +import LazyBoundary from '../LazyBoundary'; +import { SettingsPage } from '../settings/SettingsPage'; +import type { SectionId } from '../settings/types'; +import { AppStoreView } from '../AppStoreView'; +import ResourcesView from '../ResourcesView'; +import HomeDashboard from '../HomeDashboard'; +import type { NotificationItem } from '../dashboard/types'; +import type { ScheduleTaskPrefill } from '../ScheduledOperationsView'; + +// Paid-tier views and the security-history overlay are loaded on demand. +// Their internal PaidGate / AdmiralGate / CapabilityGate wrappers render +// the upsell or capability-missing card with blurred children rather than +// short-circuiting, so a tier-locked or capability-missing operator +// opening one of these tabs still triggers the chunk fetch to render the +// blurred preview. What this lazy split closes is the much larger +// initial-bundle leak: every Community user used to download the full +// FleetView, AuditLogView, etc. on first page load even if they never +// clicked those tabs. After this change, the chunks fetch only on tab +// open. +// +// GlobalObservabilityView is a free-tier feature with no internal gate; +// it is split here purely for the bundle-size win, not for IP protection. +const HostConsole = lazy(() => import('../HostConsole')); +const GlobalObservabilityView = lazy(() => + import('../GlobalObservabilityView').then(m => ({ default: m.GlobalObservabilityView })), +); +const FleetView = lazy(() => + import('../FleetView').then(m => ({ default: m.FleetView })), +); +const AuditLogView = lazy(() => + import('../AuditLogView').then(m => ({ default: m.AuditLogView })), +); +const ScheduledOperationsView = lazy(() => import('../ScheduledOperationsView')); +const AutoUpdateReadinessView = lazy(() => import('../AutoUpdateReadinessView')); + +// Sized for the main workspace area (flex-1 with p-6 padding). Visible +// only during the brief window between an unlocked view's chunk request +// and its first render. +function ViewSkeleton() { + return ( +
+ + +
+ ); +} + +function LazyView({ children }: { children: ReactNode }) { + return ( + + }> + {children} + + + ); +} + +export type ActiveView = + | 'dashboard' + | 'editor' + | 'host-console' + | 'resources' + | 'templates' + | 'global-observability' + | 'fleet' + | 'audit-log' + | 'scheduled-ops' + | 'auto-updates' + | 'settings'; + +export interface ViewRouterProps { + activeView: ActiveView; + selectedFile: string | null; + isLoading: boolean; + settingsSection: SectionId; + onSettingsSectionChange: (section: SectionId) => void; + onTemplateDeploySuccess: (stackName: string) => void; + onHostConsoleClose: () => void; + onFleetNavigateToNode: (nodeId: number, stackName: string) => void; + filterNodeId: number | null; + onClearScheduledOpsFilter: () => void; + schedulePrefill: ScheduleTaskPrefill | null; + onPrefillConsumed: () => void; + notifications: NotificationItem[]; + onNavigateToStack: (stackFile: string) => void; + onOpenSettingsSection: (section: SectionId) => void; + onClearNotifications: () => void; + // Render slot for the inline editor view. Kept as a callback so the + // (large) editor JSX is only allocated when activeView === 'editor', + // not on every parent render that lands on a different view. + renderEditor: () => ReactNode; +} + +export function ViewRouter({ + activeView, + selectedFile, + isLoading, + settingsSection, + onSettingsSectionChange, + onTemplateDeploySuccess, + onHostConsoleClose, + onFleetNavigateToNode, + filterNodeId, + onClearScheduledOpsFilter, + schedulePrefill, + onPrefillConsumed, + notifications, + onNavigateToStack, + onOpenSettingsSection, + onClearNotifications, + renderEditor, +}: ViewRouterProps): ReactNode { + if (activeView === 'settings') { + return ( + + ); + } + if (activeView === 'templates') { + return ; + } + if (activeView === 'resources') { + return ; + } + if (activeView === 'host-console') { + return ( + + + + + + + + ); + } + // Fall-through: when activeView === 'editor' but selectedFile is + // null or the stack is still loading, drop through to the default + // HomeDashboard render below. This matches the pre-extraction + // behavior of the conditional ternary chain in EditorLayout.tsx. + if (!isLoading && selectedFile && activeView === 'editor') { + return renderEditor(); + } + if (activeView === 'global-observability') { + return ( + + + + ); + } + if (activeView === 'fleet') { + return ( + + + + + + ); + } + if (activeView === 'audit-log') { + return ( + + + + + + ); + } + if (activeView === 'auto-updates') { + return ( + + + + + + ); + } + if (activeView === 'scheduled-ops') { + return ( + + + + + + ); + } + return ( + + ); +}