import { Suspense, lazy, type ReactNode } from 'react'; import { Skeleton } from '@/components/ui/skeleton'; import { useAuth } from '@/context/AuthContext'; import { PaidGate } from '../PaidGate'; import { CapabilityGate } from '../CapabilityGate'; import { HubOnlyGate } from '../HubOnlyGate'; 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'; import type { MuteRuleDraft } from '@/lib/muteRules'; import type { ActiveView } from './hooks/useViewNavigationState'; import type { SecurityTab, FleetTab } from '@/lib/events'; // Paid-tier views are loaded on demand. Their internal PaidGate / // 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')); const SecurityView = lazy(() => import('../SecurityView').then(m => ({ default: m.SecurityView })), ); // 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 }; 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; muteRulePrefill: MuteRuleDraft | null; onMutePrefillConsumed: () => void; notifications: NotificationItem[]; onNavigateToStack: (stackFile: string) => void; onOpenSettingsSection: (section: SectionId) => void; onOpenMuteRulesWithPrefill?: (draft: MuteRuleDraft) => void; onClearNotifications: () => void; securityTab: SecurityTab; onSecurityTabChange: (tab: SecurityTab) => void; fleetUpdatesIntent?: { tab: 'nodes' | 'changelog' } | null; onFleetUpdatesIntentConsumed?: () => void; fleetTab?: FleetTab | null; onFleetTabConsumed?: () => 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, muteRulePrefill, onMutePrefillConsumed, notifications, onNavigateToStack, onOpenSettingsSection, onOpenMuteRulesWithPrefill, onClearNotifications, securityTab, onSecurityTabChange, fleetUpdatesIntent, onFleetUpdatesIntentConsumed, fleetTab, onFleetTabConsumed, renderEditor, }: ViewRouterProps): ReactNode { const { can } = useAuth(); if (activeView === 'settings') { return ( ); } if (activeView === 'templates') { return ; } if (activeView === 'resources') { return ; } if (activeView === 'security') { // Node-scoped (not hub-only): scan/scanner data follows the active node // like Resources. The page itself is Community; per-tab gates handle // capability-missing nodes and the local-control governance tabs. return ( ); } if (activeView === 'host-console') { // Mirror the backend RBAC gate (system:console, admin-only). The nav // item is already admin-gated; this stops a non-admin who reaches the // view another way from mounting a console that the server will 403. if (!can('system:console')) return null; 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 ( ); }