UI: Two-level navigation in AgentPanel (DATAZONE style)
This commit is contained in:
parent
7908ae26a0
commit
31369381d0
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user