// Shell: topbar + sidebar + main const { useMemo } = React; function Topbar({ nodes, search, setSearch, live, toggleLive, currentUser, onOpenProfile }) { const counts = useMemo(() => { const c = { ok: 0, warn: 0, crit: 0, off: 0, maint: 0, unknown: 0 }; for (const n of nodes) { if (c[n.status] === undefined) c[n.status] = 0; c[n.status]++; } return c; }, [nodes]); const avgLat = useMemo(() => { const live = nodes.filter(n => n.status !== 'off'); return live.reduce((s, n) => s + n.latency, 0) / (live.length || 1); }, [nodes]); // Hamburger toggles a body class — CSS handles the rest. // Kept out of React state because it's purely a transient UI flag // and we don't need re-renders when it flips. function toggleSidebar() { document.body.classList.toggle('sb-open'); } function closeSidebar() { document.body.classList.remove('sb-open'); } // Clicking the scrim (pseudo-element covering the rest of the page // on mobile when the sidebar is open) should close the sidebar. React.useEffect(() => { function onClick(e) { if (!document.body.classList.contains('sb-open')) return; // If the click falls outside the sidebar AND outside the hamburger, // treat it as a scrim tap. if (e.target.closest('.sidebar') || e.target.closest('.hamburger')) return; closeSidebar(); } document.addEventListener('click', onClick); return () => document.removeEventListener('click', onClick); }, []); return (
МОНИТОРИНГ · СИТНОВ
{nodes.length} endpoints · prod
Live {live ? 'streaming' : 'paused'}
Up {counts.ok} Warn {counts.warn} Down {counts.crit} Off {counts.off} Maint {counts.maint}
avg {avgLat.toFixed(0)}ms
setSearch(e.target.value)} /> ⌘K
Tick
{currentUser && ( <> User {currentUser.username} {currentUser.isAdmin && admin} )}
); } function Sidebar({ view, setView, nodes, alerts, currentUser, isViewAllowed }) { const critCount = alerts.filter(a => a.severity === 'crit' && !a.ack).length; const warnCount = alerts.filter(a => a.severity === 'warn' && !a.ack).length; const allItems = [ { id: 'overview', label: 'Overview', idx: '01' }, { id: 'threed', label: '3D Fleet', idx: '02' }, { id: 'topology', label: 'Topology', idx: '03' }, { id: 'nodes', label: 'Endpoints', idx: '04', count: nodes.length }, { id: 'alerts', label: 'Alerts', idx: '05', count: critCount + warnCount }, { id: 'logs', label: 'Logs', idx: '06' }, { id: 'admin', label: 'Admin', idx: '07' }, { id: 'notifications', label: 'Notifications', idx: '08' }, { id: 'users', label: 'Users', idx: '09' }, { id: 'history', label: 'History', idx: '10' }, ]; // Filter by the current user's permissions. If the app hasn't told // us yet (no currentUser / no helper), fall back to showing all — // this branch shouldn't run because the login gate blocks it, but // we stay defensive. const allow = typeof isViewAllowed === 'function' ? isViewAllowed : () => true; const items = allItems.filter(i => allow(i.id)); const byGroup = {}; for (const n of nodes) byGroup[n.group] = (byGroup[n.group] || 0) + 1; const liveGroups = window.deriveGroups ? window.deriveGroups(nodes) : window.GROUPS; return ( ); } Object.assign(window, { Topbar, Sidebar });