Files
DECNET/decnet_web/src/components/Layout.tsx

146 lines
5.1 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { NavLink } from 'react-router-dom';
import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut, Server, Archive, Package, Network, ChevronDown, ChevronRight, HardDrive, Boxes, UserPlus } from 'lucide-react';
import './Layout.css';
interface LayoutProps {
children: React.ReactNode;
onLogout: () => void;
onSearch: (q: string) => void;
}
const Layout: React.FC<LayoutProps> = ({ children, onLogout, onSearch }) => {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [search, setSearch] = useState('');
const [systemActive, setSystemActive] = useState(false);
const handleSearchSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSearch(search);
};
useEffect(() => {
const onStats = (e: Event) => {
const stats = (e as CustomEvent).detail;
setSystemActive(stats.deployed_deckies > 0);
};
window.addEventListener('decnet:stats', onStats);
return () => window.removeEventListener('decnet:stats', onStats);
}, []);
return (
<div className="layout-container">
{/* Sidebar */}
<aside className={`sidebar ${sidebarOpen ? 'open' : 'closed'}`}>
<div className="sidebar-header">
<Activity size={24} className="violet-accent" />
{sidebarOpen && <span className="logo-text">DECNET</span>}
<button className="toggle-btn" onClick={() => setSidebarOpen(!sidebarOpen)}>
{sidebarOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
<nav className="sidebar-nav">
<NavItem to="/" icon={<LayoutDashboard size={20} />} label="Dashboard" open={sidebarOpen} />
<NavItem to="/fleet" icon={<Server size={20} />} label="Decoy Fleet" open={sidebarOpen} />
<NavItem to="/live-logs" icon={<Terminal size={20} />} label="Live Logs" open={sidebarOpen} />
<NavItem to="/bounty" icon={<Archive size={20} />} label="Bounty" open={sidebarOpen} />
<NavItem to="/attackers" icon={<Activity size={20} />} label="Attackers" open={sidebarOpen} />
<NavGroup label="SWARM" icon={<Network size={20} />} open={sidebarOpen}>
<NavItem to="/swarm/hosts" icon={<HardDrive size={18} />} label="SWARM Hosts" open={sidebarOpen} indent />
<NavItem to="/swarm/deckies" icon={<Boxes size={18} />} label="SWARM Deckies" open={sidebarOpen} indent />
<NavItem to="/swarm-updates" icon={<Package size={18} />} label="Remote Updates" open={sidebarOpen} indent />
<NavItem to="/swarm/enroll" icon={<UserPlus size={18} />} label="Agent Enrollment" open={sidebarOpen} indent />
</NavGroup>
<NavItem to="/config" icon={<Settings size={20} />} label="Config" open={sidebarOpen} />
</nav>
<div className="sidebar-footer">
<button className="logout-btn" onClick={onLogout}>
<LogOut size={20} />
{sidebarOpen && <span>Logout</span>}
</button>
</div>
</aside>
{/* Main Content Area */}
<main className="main-content">
{/* Topbar */}
<header className="topbar">
<form onSubmit={handleSearchSubmit} className="search-container">
<Search size={18} className="search-icon" />
<input
type="text"
placeholder="Search logs, deckies, IPs..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</form>
<div className="topbar-status">
<span className="matrix-text" style={{ color: systemActive ? 'var(--text-color)' : 'var(--accent-color)' }}>
SYSTEM: {systemActive ? 'ACTIVE' : 'INACTIVE'}
</span>
</div>
</header>
{/* Dynamic Content */}
<div className="content-viewport">
{children}
</div>
</main>
</div>
);
};
interface NavItemProps {
to: string;
icon: React.ReactNode;
label: string;
open: boolean;
indent?: boolean;
}
const NavItem: React.FC<NavItemProps> = ({ to, icon, label, open, indent }) => (
<NavLink
to={to}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''} ${indent ? 'nav-subitem' : ''}`}
end={to === '/'}
>
{icon}
{open && <span className="nav-label">{label}</span>}
</NavLink>
);
interface NavGroupProps {
label: string;
icon: React.ReactNode;
open: boolean;
children: React.ReactNode;
}
const NavGroup: React.FC<NavGroupProps> = ({ label, icon, open, children }) => {
const [expanded, setExpanded] = useState(true);
return (
<div className="nav-group">
<button
type="button"
className="nav-item nav-group-toggle"
onClick={() => setExpanded((v) => !v)}
>
{icon}
{open && (
<>
<span className="nav-label">{label}</span>
<span className="nav-group-chevron">
{expanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</span>
</>
)}
</button>
{expanded && <div className="nav-group-children">{children}</div>}
</div>
);
};
export default Layout;