import { useState, useEffect } from 'react'; import Editor from '@monaco-editor/react'; import TerminalComponent from './Terminal'; import ErrorBoundary from './ErrorBoundary'; import HomeDashboard from './HomeDashboard'; import BashExecModal from './BashExecModal'; import HostConsole from './HostConsole'; import ResourcesView from './ResourcesView'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger } from './ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog'; import { Tabs, TabsList, TabsTrigger } from './ui/tabs'; import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; import { Badge } from './ui/badge'; import { Plus, Trash2, Play, Square, Save, Terminal, RotateCw, CloudDownload, Pencil, X, Home, LogOut, ExternalLink, Bell, Settings, MoreVertical, BellRing, Rocket, HardDrive, ScrollText, Activity, Server } from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; import { apiFetch } from '@/lib/api'; import { toast } from 'sonner'; import { Label } from './ui/label'; import { Command, CommandInput, CommandList, CommandItem } from './ui/command'; import { ScrollArea } from './ui/scroll-area'; import { Skeleton } from './ui/skeleton'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; import { HoverCard, HoverCardContent, HoverCardTrigger } from './ui/hover-card'; import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { SettingsModal } from './SettingsModal'; import { StackAlertSheet } from './StackAlertSheet'; import { AppStoreView } from './AppStoreView'; import { LogViewer } from './LogViewer'; import { GlobalObservabilityView } from './GlobalObservabilityView'; import { useNodes } from '@/context/NodeContext'; interface ContainerInfo { Id: string; Names: string[]; State: string; Status?: string; Ports?: { PrivatePort: number, PublicPort: number }[]; } interface StackStatus { [key: string]: 'running' | 'exited' | 'unknown'; } const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; export default function EditorLayout() { const { logout } = useAuth(); const { nodes, activeNode, setActiveNode } = useNodes(); const [files, setFiles] = useState([]); const [selectedFile, setSelectedFile] = useState(null); const [content, setContent] = useState(''); const [originalContent, setOriginalContent] = useState(''); const [envContent, setEnvContent] = useState(''); const [originalEnvContent, setOriginalEnvContent] = useState(''); const [envExists, setEnvExists] = useState(false); const [envFiles, setEnvFiles] = useState([]); const [selectedEnvFile, setSelectedEnvFile] = useState(''); const [containers, setContainers] = useState([]); const [containerStats, setContainerStats] = useState>({}); const [activeTab, setActiveTab] = useState<'compose' | 'env'>('compose'); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [newStackName, setNewStackName] = useState(''); const [stackToDelete, setStackToDelete] = useState(null); const [isLoading, setIsLoading] = useState(false); const [loadingAction, setLoadingAction] = useState(null); const [isFileLoading, setIsFileLoading] = useState(false); const [isDarkMode, setIsDarkMode] = useState(() => { const saved = localStorage.getItem('sencho-theme'); if (saved !== null) { return saved === 'dark'; } return true; // Default to dark mode }); const [activeView, setActiveView] = useState<'dashboard' | 'editor' | 'host-console' | 'resources' | 'templates' | 'global-observability'>('dashboard'); const [isEditing, setIsEditing] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [stackStatuses, setStackStatuses] = useState({}); // Bash exec modal state const [bashModalOpen, setBashModalOpen] = useState(false); const [selectedContainer, setSelectedContainer] = useState<{ id: string; name: string } | null>(null); // LogViewer state const [logViewerOpen, setLogViewerOpen] = useState(false); const [logContainer, setLogContainer] = useState<{ id: string; name: string } | null>(null); // Image update checker state const [stackUpdates, setStackUpdates] = useState>({}); // Notifications & Settings state const [notifications, setNotifications] = useState([]); const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [alertSheetOpen, setAlertSheetOpen] = useState(false); const [alertSheetStack, setAlertSheetStack] = useState(''); const openAlertSheet = (stackName: string) => { setAlertSheetStack(stackName); setAlertSheetOpen(true); }; // Theme toggle effect useEffect(() => { const html = document.documentElement; if (isDarkMode) { html.classList.add('dark'); localStorage.setItem('sencho-theme', 'dark'); } else { html.classList.remove('dark'); localStorage.setItem('sencho-theme', 'light'); } }, [isDarkMode]); const refreshStacks = async (background = false) => { if (!background) setIsLoading(true); try { const res = await apiFetch('/stacks'); if (!res.ok) { setFiles([]); return; } const data = await res.json(); const fileList: string[] = Array.isArray(data) ? data : []; setFiles(fileList); // Fetch status for each stack const statuses: StackStatus = {}; for (const file of fileList) { try { const containersRes = await apiFetch(`/stacks/${file}/containers`); const containers = await containersRes.json(); const hasRunning = Array.isArray(containers) && containers.some((c: ContainerInfo) => c.State === 'running'); statuses[file] = hasRunning ? 'running' : (Array.isArray(containers) && containers.length > 0 ? 'exited' : 'unknown'); } catch { statuses[file] = 'unknown'; } } setStackStatuses(statuses); } catch (error) { console.error('Failed to refresh stacks:', error); setFiles([]); } finally { setIsLoading(false); } }; // Notification polling - independent of active node, runs once on mount useEffect(() => { fetchNotifications(); const notificationInterval = setInterval(fetchNotifications, 5000); return () => clearInterval(notificationInterval); }, []); // eslint-disable-line react-hooks/exhaustive-deps // Re-fetch stacks whenever the active node changes (or becomes available on mount). // Also clears any stale editor/container state that belonged to the previous node. useEffect(() => { if (!activeNode) return; setSelectedFile(null); setContent(''); setOriginalContent(''); setEnvContent(''); setOriginalEnvContent(''); setContainers([]); setIsEditing(false); setActiveView('dashboard'); refreshStacks(); fetchImageUpdates(); }, [activeNode?.id]); // eslint-disable-line react-hooks/exhaustive-deps const fetchNotifications = async () => { try { const res = await apiFetch('/notifications'); if (res.ok) { const data = await res.json(); setNotifications(data); } } catch (e) { } }; const fetchImageUpdates = async () => { try { const res = await apiFetch('/image-updates'); if (res.ok) { const data = await res.json(); setStackUpdates(data); } } catch (e) { } }; const markAllRead = async () => { try { await apiFetch('/notifications/read', { method: 'POST' }); fetchNotifications(); } catch (e) { } }; const deleteNotification = async (id: number) => { try { await apiFetch(`/notifications/${id}`, { method: 'DELETE' }); fetchNotifications(); } catch (e) { } }; const clearAllNotifications = async () => { try { await apiFetch('/notifications', { method: 'DELETE' }); fetchNotifications(); } catch (e) { } }; useEffect(() => { const wsMap: Record = {}; (containers || []).forEach(container => { if (!container?.Id) return; try { const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const activeNodeId = localStorage.getItem('sencho-active-node') || ''; const ws = new WebSocket(`${wsProtocol}//${window.location.host}/ws${activeNodeId ? `?nodeId=${activeNodeId}` : ''}`); wsMap[container.Id] = ws; ws.onopen = () => ws.send(JSON.stringify({ action: 'streamStats', containerId: container.Id, nodeId: activeNodeId || undefined })); ws.onmessage = (event) => { try { const data = JSON.parse(event.data); // Skip initial empty chunks where stats fields are missing if (!data.cpu_stats?.cpu_usage || !data.precpu_stats?.cpu_usage || !data.memory_stats?.usage) return; const cpuDelta = data.cpu_stats.cpu_usage.total_usage - data.precpu_stats.cpu_usage.total_usage; const systemDelta = (data.cpu_stats.system_cpu_usage || 0) - (data.precpu_stats.system_cpu_usage || 0); const onlineCpus = data.cpu_stats.online_cpus || 1; const cpuPercent = systemDelta > 0 ? ((cpuDelta / systemDelta) * onlineCpus * 100).toFixed(2) : '0.00'; const ramUsage = (data.memory_stats.usage / (1024 * 1024)).toFixed(2) + ' MB'; let currentRx = 0; let currentTx = 0; if (data.networks) { Object.values(data.networks).forEach((net: any) => { currentRx += net.rx_bytes || 0; currentTx += net.tx_bytes || 0; }); } setContainerStats(prev => { const prevStat = prev[container.Id]; // Calculate rate if we have a previous value const rxRate = prevStat?.lastRx !== undefined ? Math.max(0, currentRx - prevStat.lastRx) : 0; const txRate = prevStat?.lastTx !== undefined ? Math.max(0, currentTx - prevStat.lastTx) : 0; const netIO = `${formatBytes(rxRate)}/s ↓ / ${formatBytes(txRate)}/s ↑`; // Check if values actually changed to prevent infinite re-renders const newCpu = cpuPercent + '%'; if (prevStat && prevStat.cpu === newCpu && prevStat.ram === ramUsage && prevStat.lastRx === currentRx && prevStat.lastTx === currentTx) { return prev; } return { ...prev, [container.Id]: { cpu: newCpu, ram: ramUsage, net: netIO, lastRx: currentRx, lastTx: currentTx } }; }); } catch { // Ignore parse errors } }; } catch { // Ignore WebSocket errors } }); return () => { Object.values(wsMap).forEach(ws => { try { ws.close(); } catch { // Ignore close errors } }); }; }, [containers]); const loadFile = async (filename: string) => { if (!filename) return; setIsFileLoading(true); setIsEditing(false); // Reset to view mode when loading a new file try { const res = await apiFetch(`/stacks/${filename}`); const text = await res.text(); setSelectedFile(filename); setActiveView('editor'); setContent(text || ''); setOriginalContent(text || ''); // Load env files try { const envsRes = await apiFetch(`/stacks/${filename}/envs`); if (envsRes.ok) { const { envFiles } = await envsRes.json(); if (envFiles && envFiles.length > 0) { setEnvFiles(envFiles); const firstFile = envFiles[0]; setSelectedEnvFile(firstFile); setEnvExists(true); // Load specific env file content const envContentRes = await apiFetch(`/stacks/${filename}/env?file=${encodeURIComponent(firstFile)}`); if (envContentRes.ok) { const envText = await envContentRes.text(); setEnvContent(envText || ''); setOriginalEnvContent(envText || ''); } else { setEnvContent(''); setOriginalEnvContent(''); } } else { setEnvFiles([]); setSelectedEnvFile(''); setEnvContent(''); setOriginalEnvContent(''); setEnvExists(false); } } else { setEnvFiles([]); setSelectedEnvFile(''); setEnvContent(''); setOriginalEnvContent(''); setEnvExists(false); } } catch { setEnvFiles([]); setSelectedEnvFile(''); setEnvContent(''); setOriginalEnvContent(''); setEnvExists(false); } // Load containers try { const containersRes = await apiFetch(`/stacks/${filename}/containers`); const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); } catch (error) { console.error('Failed to load containers:', error); setContainers([]); } } catch (error) { console.error('Failed to load file:', error); setSelectedFile(null); setContent(''); setOriginalContent(''); setEnvContent(''); setOriginalEnvContent(''); setContainers([]); } finally { setIsFileLoading(false); } }; const changeEnvFile = async (file: string) => { setSelectedEnvFile(file); setIsFileLoading(true); try { const res = await apiFetch(`/stacks/${selectedFile}/env?file=${encodeURIComponent(file)}`); const text = await res.text(); setEnvContent(text || ''); setOriginalEnvContent(text || ''); } catch (e) { console.error('Failed to switch env file', e); } finally { setIsFileLoading(false); } }; const saveFile = async () => { if (!selectedFile) return; const currentContent = activeTab === 'compose' ? (content || '') : (envContent || ''); const endpoint = activeTab === 'compose' ? `/stacks/${selectedFile}` : `/stacks/${selectedFile}/env?file=${encodeURIComponent(selectedEnvFile)}`; try { const response = await apiFetch(endpoint, { method: 'PUT', body: JSON.stringify({ content: currentContent }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.text()}`); } // Update original content after save if (activeTab === 'compose') { setOriginalContent(content); } else { setOriginalEnvContent(envContent); } setIsEditing(false); toast.success('File saved successfully!'); } catch (error) { console.error('Failed to save file:', error); toast.error(`Failed to save file: ${(error as Error).message}`); } }; const handleSaveAndDeploy = async (e: React.MouseEvent) => { await saveFile(); await deployStack(e); }; const discardChanges = () => { if (activeTab === 'compose') { setContent(originalContent); } else { setEnvContent(originalEnvContent); } setIsEditing(false); }; const enterEditMode = () => { setIsEditing(true); }; const deployStack = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!selectedFile || loadingAction !== null) return; const stackName = selectedFile.replace(/\.(yml|yaml)$/, ''); setLoadingAction('deploy'); try { const response = await apiFetch(`/stacks/${stackName}/deploy`, { method: 'POST', }); if (!response.ok) { const errText = await response.text(); throw new Error(errText || 'Deploy failed'); } toast.success("Stack deployed successfully!"); // Refresh containers after deploy const containersRes = await apiFetch(`/stacks/${stackName}/containers`); const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); await refreshStacks(true); } catch (error: any) { console.error('Failed to deploy:', error); toast.error(error.message || 'Failed to deploy stack'); } finally { setLoadingAction(null); } }; const stopStack = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!selectedFile || loadingAction !== null) return; const stackName = selectedFile.replace(/\.(yml|yaml)$/, ''); setLoadingAction('stop'); try { const response = await apiFetch(`/stacks/${stackName}/stop`, { method: 'POST', }); if (!response.ok) { const errText = await response.text(); throw new Error(errText || 'Stop failed'); } toast.success('Stack stopped successfully!'); // Refresh containers after stop const containersRes = await apiFetch(`/stacks/${stackName}/containers`); const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); await refreshStacks(true); } catch (error: any) { console.error('Failed to stop:', error); toast.error(error.message || 'Failed to stop stack'); } finally { setLoadingAction(null); } }; const restartStack = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!selectedFile || loadingAction !== null) return; const stackName = selectedFile.replace(/\.(yml|yaml)$/, ''); setLoadingAction('restart'); try { const response = await apiFetch(`/stacks/${stackName}/restart`, { method: 'POST', }); if (!response.ok) { const errText = await response.text(); throw new Error(errText || 'Restart failed'); } toast.success('Stack restarted successfully!'); // Refresh containers after restart const containersRes = await apiFetch(`/stacks/${stackName}/containers`); const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); await refreshStacks(true); } catch (error: any) { console.error('Failed to restart:', error); toast.error(error.message || 'Failed to restart stack'); } finally { setLoadingAction(null); } }; const updateStack = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!selectedFile || loadingAction !== null) return; const stackName = selectedFile.replace(/\.(yml|yaml)$/, ''); setLoadingAction('update'); try { const response = await apiFetch(`/stacks/${stackName}/update`, { method: 'POST', }); if (!response.ok) { const errText = await response.text(); throw new Error(errText || 'Update failed'); } toast.success('Stack updated successfully!'); // Refresh containers after update const containersRes = await apiFetch(`/stacks/${stackName}/containers`); const conts = await containersRes.json(); setContainers(Array.isArray(conts) ? conts : []); await refreshStacks(true); } catch (error: any) { console.error('Failed to update:', error); toast.error(error.message || 'Failed to update stack'); } finally { setLoadingAction(null); } }; const deleteStack = async () => { if (!stackToDelete) return; setLoadingAction('delete'); try { const response = await apiFetch(`/stacks/${stackToDelete}`, { method: 'DELETE', }); if (!response.ok) { const errText = await response.text(); throw new Error(errText || 'Failed to delete stack'); } toast.success('Stack deleted successfully!'); setDeleteDialogOpen(false); setStackToDelete(null); if (selectedFile === stackToDelete) { setSelectedFile(null); setContent(''); setOriginalContent(''); setEnvContent(''); setOriginalEnvContent(''); setEnvExists(false); setContainers([]); setIsEditing(false); } await refreshStacks(); } catch (error: any) { console.error('Failed to delete stack:', error); toast.error(error.message || 'Failed to delete stack'); } finally { setLoadingAction(null); } }; const handleCreateStack = async () => { if (!newStackName.trim()) return; // Send stackName directly (no .yml extension - backend creates directory) const stackName = newStackName.trim(); try { const response = await apiFetch('/stacks', { method: 'POST', body: JSON.stringify({ stackName }), }); if (!response.ok) { if (response.status === 409) { throw new Error('Stack already exists'); } else if (response.status === 400) { throw new Error('Invalid stack name (use alphanumeric characters and hyphens only)'); } throw new Error('Failed to create stack'); } setCreateDialogOpen(false); setNewStackName(''); await refreshStacks(); // Auto-load the new stack in the editor pane await loadFile(stackName); } catch (error: any) { console.error('Failed to create stack:', error); toast.error(error.message || 'Failed to create stack'); } }; const openBashModal = (containerId: string, containerName: string) => { setSelectedContainer({ id: containerId, name: containerName }); setBashModalOpen(true); }; const closeBashModal = () => { setBashModalOpen(false); setSelectedContainer(null); }; const openLogViewer = (containerId: string, containerName: string) => { setLogContainer({ id: containerId, name: containerName }); setLogViewerOpen(true); }; const closeLogViewer = () => { setLogViewerOpen(false); setLogContainer(null); }; // Safe container list with fallback const safeContainers = containers || []; // Safe content strings with fallback const safeContent = content || ''; const safeEnvContent = envContent || ''; // Stack state booleans for dynamic button rendering const isRunning = safeContainers?.some(c => c.State === 'running'); // Stack name is now the same as selectedFile (no extension to strip) const stackName = selectedFile || ''; // Filter files based on search query const filteredFiles = files.filter(file => { return file.toLowerCase().includes(searchQuery.toLowerCase()); }); // Get display name for stack (now just returns the name as-is since no extension) const getDisplayName = (stackName: string) => { return stackName; }; const getContainerBadge = (container: ContainerInfo) => { const status = (container.Status || '').toLowerCase(); const state = (container.State || '').toLowerCase(); if (status.includes('(unhealthy)') || state === 'exited' || state === 'dead') { return { variant: 'destructive' as const, text: container.State }; } if (status.includes('(starting)')) { return { variant: 'secondary' as const, text: container.State }; } return { variant: 'default' as const, text: container.State }; }; return (
{/* Left Sidebar (Stacks) */}
{/* Branding Header */}
Sencho Logo

Sencho

Logout
{/* Node Switcher */} {nodes.length > 1 && (
)} {/* Create Stack Button */}
Create New Stack
setNewStackName(e.target.value)} />
{/* Search Input & Stack List */}

STACKS

{isLoading ? (
) : ( (filteredFiles || []).map(file => ( loadFile(file)} className={`justify-start rounded-lg mb-1 cursor-pointer hover:bg-muted group ${selectedFile === file ? '!bg-accent !text-accent-foreground' : ''}`} >
{getDisplayName(file)} {stackUpdates[file] && ( )}
e.stopPropagation()}> openAlertSheet(file)}> Alerts
)) )}
{/* Main Content Area */}
{/* Top Header Bar */}
{/* Node Context Pill — visible only when a remote node is active */}
{activeNode?.type === 'remote' ? (
{activeNode.name}
) : (
{activeNode?.name ?? 'Local'}
)}
{/* Home Button */} {/* Console Toggle */} {/* Resources Toggle */} {/* App Store Toggle */} {/* Global Observability Toggle */} {/* Settings Modal Toggle */} {/* Notifications Popover */}

Notifications

{notifications.filter(n => !n.is_read).length > 0 && ( )} {notifications.length > 0 && ( )}
{notifications.length === 0 ? (
No notifications
) : (
{notifications.map((notif: any) => (
{notif.level} {new Date(notif.timestamp).toLocaleString()}

{notif.message}

{/* Delete individual notification button */}
))}
)}
{/* end right-side buttons */}
{/* Main Workspace */}
{activeView === 'templates' ? ( { refreshStacks(); loadFile(stackName); }} /> ) : activeView === 'resources' ? ( ) : activeView === 'host-console' ? ( setActiveView(selectedFile ? 'editor' : 'dashboard')} /> ) : !isLoading && selectedFile && activeView === 'editor' ? (
{/* Left Column (Command Center & Terminal) */}
{/* Command Center Card */}
{/* Stack Name */} {stackName} {/* Action Bar */}
{isRunning ? ( <> ) : ( )}
{/* Containers List */}

CONTAINERS

{safeContainers.length === 0 ? (
No containers running for this stack.
) : (
{safeContainers.map(container => { let mainPort: number | undefined; if (container.Ports && container.Ports.length > 0) { const WEB_UI_PORTS = [32400, 8989, 7878, 9696, 5055, 8080, 80, 443, 3000, 9000]; const IGNORE_PORTS = [1900, 53, 22]; // 1. Match typical Web UI Private ports let match = container.Ports.find(p => WEB_UI_PORTS.includes(p.PrivatePort)); // 2. Match typical Web UI Public ports if (!match) match = container.Ports.find(p => WEB_UI_PORTS.includes(p.PublicPort)); // 3. Fallback to any port not in ignore list if (!match) match = container.Ports.find(p => !IGNORE_PORTS.includes(p.PrivatePort) && !IGNORE_PORTS.includes(p.PublicPort)); mainPort = (match || container.Ports[0]).PublicPort; } return (
{getContainerBadge(container).text || 'unknown'}

Container Status

{container?.Status || 'No status details available'}

CPU: {container.State === 'running' ? (containerStats[container?.Id]?.cpu || 'N/A') : '0.00%'} | RAM: {container.State === 'running' ? (containerStats[container?.Id]?.ram || 'N/A') : '0.00 MB'} | NET: {container.State === 'running' ? (containerStats[container?.Id]?.net || '0 B ↓ / 0 B ↑') : '0 B/s ↓ / 0 B/s ↑'}
{mainPort && ( Open App ({mainPort}) )} View Live Logs Open Bash Terminal
); })}
)}
{/* Terminal Section */}

Terminal

{/* Right Column (The Editor) */}
setActiveTab(value as 'compose' | 'env')}> compose.yaml .env {activeTab === 'env' && envFiles.length > 1 && ( )}
{!isEditing ? ( ) : ( <> )}
{activeTab === 'env' && (
Variables defined here are automatically available for substitution in your compose.yaml (e.g., ${'{}'}VAR). To pass them directly into your container, you must add env_file: - .env to your service definition.
)}
{!isFileLoading && ( { if (!isEditing) return; // Prevent changes in view mode if (activeTab === 'compose') { setContent(value || ''); } else { setEnvContent(value || ''); } }} options={{ minimap: { enabled: false }, fontSize: 14, padding: { top: 10 }, scrollBeyondLastLine: false, readOnly: !isEditing, }} /> )} {isFileLoading && (
Loading...
)}
) : activeView === 'global-observability' ? ( ) : ( )}
{/* Delete Confirmation Dialog */} Delete Stack Are you sure you want to delete {stackToDelete}? This action cannot be undone. setDeleteDialogOpen(false)}>Cancel Delete {/* Bash Exec Modal */} {selectedContainer && ( )} {/* LogViewer Modal */} {logContainer && ( )} {/* Settings Modal */} setSettingsModalOpen(false)} isDarkMode={isDarkMode} setIsDarkMode={setIsDarkMode} /> {/* Stack Alert Sheet */} setAlertSheetOpen(false)} stackName={alertSheetStack} />
); }