Files
edgeguard-native/management-ui/src/components/Layout/Sidebar.tsx
Debian 4f887df658 feat(waf): Phase 5 — WAF-UI (per-Domain Konfiguration) — v1.2.71
- pages/WAF/index.tsx: neue WAF-Seite mit Status-Strip,
  Domänen-Tabelle (Toggle/Mode/PL) + Konfigurations-Drawer pro Domain
  (enabled, mode, paranoia_level 1-4, rule_exclusions, trusted_proxies,
  custom_rules). Quick-Toggle ohne Drawer; Hinweis: erst Detection, dann Blocking.
- App.tsx: /waf Route + lazy import
- Sidebar.tsx: WAF im Security-Bereich
- i18n EN + DE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:03:01 +02:00

182 lines
6.6 KiB
TypeScript

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,
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: <DashboardOutlined /> },
],
},
{
labelKey: 'nav.section.routing',
items: [
{ path: '/domains', labelKey: 'nav.domains', icon: <GlobalOutlined /> },
{ path: '/backends', labelKey: 'nav.backends', icon: <DatabaseOutlined /> },
{ path: '/routing-rules', labelKey: 'nav.routing', icon: <BranchesOutlined /> },
],
},
{
labelKey: 'nav.section.network',
items: [
{ path: '/networks', labelKey: 'nav.networks', icon: <ClusterOutlined /> },
{ path: '/ip-addresses', labelKey: 'nav.ipAddresses', icon: <NodeIndexOutlined /> },
{ path: '/ssl', labelKey: 'nav.ssl', icon: <SafetyCertificateOutlined /> },
{ path: '/dns', labelKey: 'nav.dns', icon: <GlobalOutlined /> },
{ path: '/ntp', labelKey: 'nav.ntp', icon: <ClockCircleOutlined /> },
],
},
{
labelKey: 'nav.section.security',
items: [
{ path: '/firewall', labelKey: 'nav.firewall', icon: <FireOutlined /> },
{ path: '/firewall/live', labelKey: 'nav.firewallLive', icon: <EyeOutlined />, child: true },
{ path: '/vpn/wireguard', labelKey: 'nav.wireguard', icon: <ThunderboltOutlined /> },
{ path: '/forward-proxy', labelKey: 'nav.forwardProxy', icon: <CloudServerOutlined /> },
{ path: '/crowdsec', labelKey: 'nav.crowdsec', icon: <RadarChartOutlined /> },
{ path: '/waf', labelKey: 'nav.waf', icon: <SafetyCertificateOutlined /> },
],
},
{
labelKey: 'nav.section.system',
items: [
{ path: '/cluster', labelKey: 'nav.cluster', icon: <ApartmentOutlined /> },
{ path: '/logs', labelKey: 'nav.logs', icon: <FileSearchOutlined /> },
{ path: '/audit', labelKey: 'nav.audit', icon: <AuditOutlined /> },
{ path: '/diagnostics', labelKey: 'nav.diagnostics', icon: <ToolOutlined /> },
{ path: '/alerts', labelKey: 'nav.alerts', icon: <BellOutlined /> },
{ path: '/backups', labelKey: 'nav.backups', icon: <DatabaseOutlined /> },
{ path: '/license', labelKey: 'nav.license', icon: <CrownOutlined /> },
{ path: '/users', labelKey: 'nav.users', icon: <TeamOutlined /> },
{ path: '/settings', labelKey: 'nav.settings', icon: <SettingOutlined /> },
],
},
]
// Sidebar-Pattern 1:1 aus netcell-webpanel (enconf) übernommen:
// - <nav> als root, dunkler Gradient + Teal/Blue-Accent
// - Section-Label-Div NEBEN dem <ul>, nicht verschachtelt
// - <Link> + pathname-Vergleich für active-State (ein <li>.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 (
<nav className={`sidebar${isOpen ? ' open' : ''}`}>
<div className="sidebar-logo">
<div className="sidebar-logo-icon">EG</div>
<span className="sidebar-logo-text">{t('app.title')}</span>
</div>
{NAV.map((section, idx) => (
<div key={section.labelKey}>
<div className={`sidebar-section${idx > 0 ? ' sidebar-section--bordered' : ''}`}>
<div className="sidebar-section-label">{t(section.labelKey)}</div>
</div>
<ul className="sidebar-menu">
{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 (
<li key={item.path} className={cls}>
<Link to={item.path} onClick={onClose}>
{item.icon}
<span>{t(item.labelKey)}</span>
</Link>
</li>
)
})}
</ul>
</div>
))}
<div className="sidebar-version">
{hostname && (
<div style={{ fontSize: 10, opacity: 0.6, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{hostname}
</div>
)}
v{version}
</div>
</nav>
)
}