UI: Two-level navigation in AgentPanel (DATAZONE style)

This commit is contained in:
cynfo3000 2026-03-05 09:09:54 +01:00
parent 7908ae26a0
commit 31369381d0

View File

@ -6,33 +6,52 @@ import { useSettingsStore } from '../stores/settings'
import {
X, Cpu, MemoryStick, HardDrive, Clock, Network, Shield, Route,
Globe, Key, Download, Terminal, Wifi, Search, Plus, Trash2, Copy, Check, ExternalLink, Unplug, Plug, MonitorSmartphone, Ban,
FileKey, UserCheck, Database, CalendarClock, Cable, Settings, Server, GitBranch,
} from 'lucide-react'
const allTabs = [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'interfaces', label: 'Interfaces' },
{ id: 'services', label: 'Dienste' },
{ id: 'tunnels', label: 'Tunnel' },
{ id: 'vpn', label: 'VPN', platforms: ['freebsd'] },
{ id: 'wireguard', label: 'WireGuard', platforms: ['freebsd'] },
{ id: 'routes', label: 'Routen' },
{ id: 'dhcp', label: 'DHCP', platforms: ['freebsd'] },
{ id: 'certs', label: 'Zertifikate', platforms: ['freebsd'] },
{ id: 'updates', label: 'Updates', platforms: ['freebsd'] },
{ id: 'security', label: 'Sicherheit', platforms: ['freebsd'] },
{ id: 'tasks', label: 'Aufgaben', platforms: ['freebsd', 'linux'] },
{ id: 'backups', label: 'Backups' },
{ id: 'agent', label: 'Agent' },
// Two-level navigation structure (DATAZONE style)
const mainCategories = [
{ id: 'uebersicht', label: 'Uebersicht' },
{ id: 'netzwerk', label: 'Netzwerk' },
{ id: 'sicherheit', label: 'Sicherheit' },
{ id: 'daten', label: 'Daten' },
{ id: 'system', label: 'System' },
]
const subItems = {
netzwerk: [
{ id: 'interfaces', label: 'Interfaces', icon: Network },
{ id: 'vpn', label: 'VPN (WireGuard)', icon: Shield, platforms: ['freebsd'] },
{ id: 'routes', label: 'Routen', icon: GitBranch },
{ id: 'dhcp', label: 'DHCP', icon: Server, platforms: ['freebsd'] },
{ id: 'gateways', label: 'Gateways', icon: Globe },
],
sicherheit: [
{ id: 'certs', label: 'Zertifikate', icon: FileKey, platforms: ['freebsd'] },
{ id: 'security', label: 'Logins', icon: UserCheck, platforms: ['freebsd'] },
],
daten: [
{ id: 'backups', label: 'Backups', icon: Database },
{ id: 'tasks', label: 'Aufgaben', icon: CalendarClock, platforms: ['freebsd', 'linux'] },
{ id: 'tunnels', label: 'Tunnel', icon: Cable },
],
system: [
{ id: 'services', label: 'Dienste', icon: Settings },
{ id: 'updates', label: 'Updates', icon: Download, platforms: ['freebsd'] },
{ id: 'agent', label: 'Agent', icon: Cpu },
],
}
export default function AgentPanel({ agentId, customers, onClose, onReload }) {
const [data, setData] = useState(null)
const [tab, setTab] = useState('overview')
const [mainTab, setMainTab] = useState('uebersicht')
const [subTab, setSubTab] = useState('overview')
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
setTab('overview')
setMainTab('uebersicht')
setSubTab('overview')
api.getAgent(agentId).then(setData).finally(() => setLoading(false))
}, [agentId])
@ -50,7 +69,50 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) {
const { agent, system_data: sys, status } = data
const cust = agent.customer_id ? customers.find((c) => c.id === agent.customer_id) : null
const platform = agent.platform || 'freebsd'
const tabs = allTabs.filter(t => !t.platforms || t.platforms.includes(platform))
// Filter sub-items by platform and determine visible main categories
const getVisibleSubs = (catId) => {
const items = subItems[catId] || []
return items.filter(s => !s.platforms || s.platforms.includes(platform))
}
const visibleMainTabs = mainCategories.filter(cat => {
if (cat.id === 'uebersicht') return true
return getVisibleSubs(cat.id).length > 0
})
const handleMainTabChange = (catId) => {
setMainTab(catId)
if (catId === 'uebersicht') {
setSubTab('overview')
} else {
const subs = getVisibleSubs(catId)
if (subs.length > 0) setSubTab(subs[0].id)
}
}
// For "gateways" sub-tab, we render the OverviewTab's gateway section standalone
const currentSubs = mainTab !== 'uebersicht' ? getVisibleSubs(mainTab) : []
const renderContent = () => {
if (!sys) return <div className="text-gray-500">Keine Systemdaten</div>
const tab = subTab
if (tab === 'overview') return <OverviewTab sys={sys} />
if (tab === 'interfaces') return <InterfacesTab sys={sys} />
if (tab === 'services') return <ServicesTab sys={sys} />
if (tab === 'tunnels') return <TunnelTab agentId={agentId} agent={agent} />
if (tab === 'vpn') return <><VPNTab sys={sys} /><div className="mt-6"><WireGuardTab agentId={agentId} sys={sys} /></div></>
if (tab === 'routes') return <RoutesTab sys={sys} />
if (tab === 'dhcp') return <DHCPTab sys={sys} />
if (tab === 'gateways') return <GatewaysTab sys={sys} />
if (tab === 'certs') return <CertsTab sys={sys} />
if (tab === 'updates') return <UpdatesTab agentId={agentId} />
if (tab === 'security') return <SecurityTab sys={sys} />
if (tab === 'tasks') return <TasksTab agentId={agentId} />
if (tab === 'backups') return platform === 'linux' ? <PVEBackupsTab sys={sys} /> : <BackupsTab agentId={agentId} />
if (tab === 'agent') return <AgentTab agent={data?.agent} status={data?.status} platform={platform} />
return null
}
return (
<Panel onClose={onClose}>
@ -86,57 +148,59 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) {
</div>
</div>
{/* Tabs */}
{/* Main Category Tabs */}
<div className="flex gap-1 mt-4 overflow-x-auto text-xs">
{tabs.map((t) => (
{visibleMainTabs.map((cat) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`px-3 py-1.5 rounded-t whitespace-nowrap transition-colors ${
tab === t.id
? 'bg-gray-900 text-orange-400 border-t border-x border-gray-700'
: 'text-gray-400 hover:text-gray-200'
key={cat.id}
onClick={() => handleMainTabChange(cat.id)}
className={`px-4 py-1.5 whitespace-nowrap transition-colors border-b-2 ${
mainTab === cat.id
? 'text-orange-400 border-orange-400'
: 'text-gray-400 hover:text-gray-200 border-transparent'
}`}
>
{t.label}
{cat.label}
</button>
))}
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{!sys ? (
<div className="text-gray-500">Keine Systemdaten</div>
) : tab === 'overview' ? (
<OverviewTab sys={sys} />
) : tab === 'interfaces' ? (
<InterfacesTab sys={sys} />
) : tab === 'services' ? (
<ServicesTab sys={sys} />
) : tab === 'tunnels' ? (
<TunnelTab agentId={agentId} agent={agent} />
) : tab === 'vpn' ? (
<VPNTab sys={sys} />
) : tab === 'wireguard' ? (
<WireGuardTab agentId={agentId} sys={sys} />
) : tab === 'routes' ? (
<RoutesTab sys={sys} />
) : tab === 'dhcp' ? (
<DHCPTab sys={sys} />
) : tab === 'certs' ? (
<CertsTab sys={sys} />
) : tab === 'updates' ? (
<UpdatesTab agentId={agentId} />
) : tab === 'security' ? (
<SecurityTab sys={sys} />
) : tab === 'tasks' ? (
<TasksTab agentId={agentId} />
) : tab === 'backups' ? (
platform === 'linux' ? <PVEBackupsTab sys={sys} /> : <BackupsTab agentId={agentId} />
) : tab === 'agent' ? (
<AgentTab agent={data?.agent} status={data?.status} platform={platform} />
) : null}
{/* Content area with optional sub-nav */}
<div className="flex-1 flex overflow-hidden">
{/* Sub-Navigation (left sidebar) — hidden for Uebersicht */}
{mainTab !== 'uebersicht' && currentSubs.length > 0 && (
<div className="w-44 shrink-0 bg-gray-900/50 border-r border-gray-800 overflow-y-auto py-3">
<div className="px-4 mb-2">
<span className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">
{mainCategories.find(c => c.id === mainTab)?.label}
</span>
</div>
{currentSubs.map((item) => {
const Icon = item.icon
const isActive = subTab === item.id
return (
<button
key={item.id}
onClick={() => setSubTab(item.id)}
className={`w-full flex items-center gap-2.5 px-4 py-2 text-sm transition-colors ${
isActive
? 'text-orange-400 bg-orange-400/5 border-l-2 border-orange-400'
: 'text-gray-400 hover:text-white border-l-2 border-transparent'
}`}
>
{Icon && <Icon className="w-4 h-4 shrink-0" />}
<span>{item.label}</span>
</button>
)
})}
</div>
)}
{/* Main content */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{renderContent()}
</div>
</div>
</Panel>
)
@ -204,7 +268,7 @@ function Panel({ onClose, children }) {
return (
<>
<div className="fixed inset-0 z-40 bg-black/50" onClick={onClose} />
<div className="fixed inset-y-0 right-0 z-50 w-full max-w-5xl bg-gray-900 border-l border-gray-800 flex flex-col shadow-2xl animate-slide-in">
<div className="fixed inset-y-0 right-0 z-50 w-full max-w-6xl bg-gray-900 border-l border-gray-800 flex flex-col shadow-2xl animate-slide-in">
{children}
</div>
<style>{`
@ -215,6 +279,41 @@ function Panel({ onClose, children }) {
)
}
function GatewaysTab({ sys }) {
const gateways = sys.gateways || []
if (gateways.length === 0) return <div className="text-gray-500">Keine Gateways</div>
return (
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-500 text-xs border-b border-gray-700">
<th className="px-3 py-2">Name</th>
<th className="px-3 py-2">Gateway</th>
<th className="px-3 py-2">Status</th>
<th className="px-3 py-2">RTT</th>
<th className="px-3 py-2">Loss</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700/50">
{gateways.map((gw) => (
<tr key={gw.name}>
<td className="px-3 py-2 text-white">{gw.name}</td>
<td className="px-3 py-2 text-gray-400">{gw.gateway || gw.address}</td>
<td className="px-3 py-2">
<span className={gwColor(gw.status)}>
{gw.status || '—'}
</span>
</td>
<td className="px-3 py-2 text-gray-400">{gw.delay || '—'}</td>
<td className="px-3 py-2 text-gray-400">{gw.loss || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
function MetricCard({ icon: Icon, label, value, sub, bar, barColor }) {
return (
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">