Files
edgeguard-native/management-ui/src/components/Layout/Sidebar.tsx
Debian dbc14a24a4 feat(backup): Restore-Pfad — POST /backups/:id/restore + UI
backup.Service.Restore(id) schreibt /var/lib/edgeguard/restore.sh
und dispatcht via `sudo systemd-run --unit=edgeguard-restore.service`.
Skript-Ablauf:
  1. tar -xzf der Backup-Datei → /var/lib/edgeguard/restore-tmp
  2. state-files (setup.json/license/jwt/node.conf/acme-account) per
     cp -a zurück, chown edgeguard
  3. systemctl stop edgeguard-api + scheduler (DB-Connections freigeben)
  4. sudo -u postgres psql -f dump.sql (--clean droppt + recreated)
  5. edgeguard-ctl render-config (haproxy/nft/squid/unbound/chrony)
  6. systemctl start edgeguard-api + scheduler
  7. rm -rf restore-tmp + restore.sh

UI: pro Backup-Row neuer Restore-Button mit Popconfirm. Beim Trigger
zeigt sich das vertraute Fullscreen-Overlay (Klassen .update-modal*
re-used) mit 4 Steps (Extract / DB-Restore / Render / Restart) + Live-
Timer. Health-Poll alle 3s detektiert API-Restart + reload. Safety-
Timeout 3 min für große DB-Dumps.

postinst: sudoers für `systemd-run --unit=edgeguard-restore.service
--description=... --collect bash /var/lib/edgeguard/restore.sh` +
zugehöriges `systemctl reset-failed`. Pfad fix damit kein Wildcard
nötig wird.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:22:55 +02:00

129 lines
4.0 KiB
TypeScript

import { Link, useLocation } from 'react-router-dom'
import type { ReactNode } from 'react'
import {
ApartmentOutlined,
ClockCircleOutlined,
CloudServerOutlined,
ClusterOutlined,
CrownOutlined,
DashboardOutlined,
FileSearchOutlined,
DatabaseOutlined,
FireOutlined,
GlobalOutlined,
NodeIndexOutlined,
SafetyCertificateOutlined,
SettingOutlined,
ThunderboltOutlined,
} from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
interface SidebarProps {
isOpen: boolean
onClose?: () => void
}
interface NavItem {
path: string
labelKey: string
icon: ReactNode
}
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 /> },
],
},
{
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: '/vpn/wireguard', labelKey: 'nav.wireguard', icon: <ThunderboltOutlined /> },
{ path: '/forward-proxy', labelKey: 'nav.forwardProxy', icon: <CloudServerOutlined /> },
],
},
{
labelKey: 'nav.section.system',
items: [
{ path: '/cluster', labelKey: 'nav.cluster', icon: <ApartmentOutlined /> },
{ path: '/logs', labelKey: 'nav.logs', icon: <FileSearchOutlined /> },
{ path: '/backups', labelKey: 'nav.backups', icon: <DatabaseOutlined /> },
{ path: '/license', labelKey: 'nav.license', icon: <CrownOutlined /> },
{ path: '/settings', labelKey: 'nav.settings', icon: <SettingOutlined /> },
],
},
]
const VERSION = '1.0.65'
// 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()
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) => (
<div key={section.labelKey}>
<div className="sidebar-section">
<div className="sidebar-section-label">{t(section.labelKey)}</div>
</div>
<ul className="sidebar-menu">
{section.items.map((item) => {
const isActive = location.pathname === item.path
|| location.pathname.startsWith(item.path + '/')
return (
<li
key={item.path}
className={`sidebar-menu-item${isActive ? ' active' : ''}`}
>
<Link to={item.path} onClick={onClose}>
{item.icon}
<span>{t(item.labelKey)}</span>
</Link>
</li>
)
})}
</ul>
</div>
))}
<div className="sidebar-version">v{VERSION}</div>
</nav>
)
}