import { Link, useLocation } from 'react-router-dom' import type { ReactNode } from 'react' import { useQuery } from '@tanstack/react-query' import apiClient, { isEnvelope } from '../../api/client' import { ApartmentOutlined, BellOutlined, BranchesOutlined, ClockCircleOutlined, CloudServerOutlined, ClusterOutlined, CrownOutlined, DashboardOutlined, EyeOutlined, FileSearchOutlined, AuditOutlined, DatabaseOutlined, FireOutlined, GlobalOutlined, IdcardOutlined, NodeIndexOutlined, RadarChartOutlined, SafetyCertificateOutlined, SettingOutlined, TeamOutlined, ThunderboltOutlined, ToolOutlined, } from '@ant-design/icons' import { useTranslation } from 'react-i18next' interface SidebarProps { isOpen: boolean onClose?: () => void } interface NavItem { path: string labelKey: string icon: ReactNode child?: boolean // visuell eingerückt unter dem Parent-Item } interface NavSection { labelKey: string items: NavItem[] } const NAV: NavSection[] = [ { labelKey: 'nav.section.overview', items: [ { path: '/dashboard', labelKey: 'nav.dashboard', icon: }, ], }, { labelKey: 'nav.section.routing', items: [ { path: '/domains', labelKey: 'nav.domains', icon: }, { path: '/backends', labelKey: 'nav.backends', icon: }, { path: '/routing-rules', labelKey: 'nav.routing', icon: }, ], }, { labelKey: 'nav.section.network', items: [ { path: '/networks', labelKey: 'nav.networks', icon: }, { path: '/ip-addresses', labelKey: 'nav.ipAddresses', icon: }, { path: '/ssl', labelKey: 'nav.ssl', icon: }, { path: '/dns', labelKey: 'nav.dns', icon: }, { path: '/dhcp', labelKey: 'nav.dhcp', icon: }, { path: '/ntp', labelKey: 'nav.ntp', icon: }, ], }, { labelKey: 'nav.section.security', items: [ { path: '/firewall', labelKey: 'nav.firewall', icon: }, { path: '/firewall/live', labelKey: 'nav.firewallLive', icon: , child: true }, { path: '/vpn/wireguard', labelKey: 'nav.wireguard', icon: }, { path: '/forward-proxy', labelKey: 'nav.forwardProxy', icon: }, { path: '/crowdsec', labelKey: 'nav.crowdsec', icon: }, { path: '/waf', labelKey: 'nav.waf', icon: }, { path: '/radius', labelKey: 'nav.radius', icon: }, ], }, { labelKey: 'nav.section.system', items: [ { path: '/cluster', labelKey: 'nav.cluster', icon: }, { path: '/logs', labelKey: 'nav.logs', icon: }, { path: '/audit', labelKey: 'nav.audit', icon: }, { path: '/diagnostics', labelKey: 'nav.diagnostics', icon: }, { path: '/alerts', labelKey: 'nav.alerts', icon: }, { path: '/backups', labelKey: 'nav.backups', icon: }, { path: '/license', labelKey: 'nav.license', icon: }, { path: '/users', labelKey: 'nav.users', icon: }, { path: '/settings', labelKey: 'nav.settings', icon: }, ], }, ] // Sidebar-Pattern 1:1 aus netcell-webpanel (enconf) übernommen: // - als root, dunkler Gradient + Teal/Blue-Accent // - Section-Label-Div NEBEN dem , nicht verschachtelt // - + pathname-Vergleich für active-State (ein .active::before // rendert den Akzent-Stab links + tint die Item-Background) // CSS lebt in styles/enterprise.css (.sidebar*). export default function Sidebar({ isOpen, onClose }: SidebarProps) { const { t } = useTranslation() const location = useLocation() // Version aus /system/health ziehen damit nach jedem Self-Upgrade // automatisch die neue Versionsnummer in der Sidebar erscheint — // ohne Hardcode, der beim Vergessen den Eindruck erweckt das Update // sei nicht durchgelaufen. const { data: health } = useQuery({ queryKey: ['system', 'health'], queryFn: async () => { const r = await apiClient.get('/system/health') return isEnvelope(r.data) ? (r.data.data as { version?: string; hostname?: string }) : { version: '' } }, refetchInterval: 60_000, refetchOnWindowFocus: true, staleTime: 30_000, }) const version = health?.version || '…' const hostname = health?.hostname || null return ( EG {t('app.title')} {NAV.map((section, idx) => ( 0 ? ' sidebar-section--bordered' : ''}`}> {t(section.labelKey)} {section.items.map((item) => { // exact-match → der genauere Pfad gewinnt; sonst würde // /firewall den /firewall/live-Eintrag als „active" // mitmarkieren. Sibling-Pfade müssen sich gegenseitig // ausschließen. const hasMoreSpecificSibling = section.items.some( (other) => other.path !== item.path && other.path.startsWith(item.path + '/'), ) const isActive = hasMoreSpecificSibling ? location.pathname === item.path : location.pathname === item.path || location.pathname.startsWith(item.path + '/') const cls = 'sidebar-menu-item' + (isActive ? ' active' : '') + (item.child ? ' sidebar-menu-item--child' : '') return ( {item.icon} {t(item.labelKey)} ) })} ))} {hostname && ( {hostname} )} v{version} ) }