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 {
|
import {
|
||||||
X, Cpu, MemoryStick, HardDrive, Clock, Network, Shield, Route,
|
X, Cpu, MemoryStick, HardDrive, Clock, Network, Shield, Route,
|
||||||
Globe, Key, Download, Terminal, Wifi, Search, Plus, Trash2, Copy, Check, ExternalLink, Unplug, Plug, MonitorSmartphone, Ban,
|
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'
|
} from 'lucide-react'
|
||||||
|
|
||||||
const allTabs = [
|
// Two-level navigation structure (DATAZONE style)
|
||||||
{ id: 'overview', label: 'Uebersicht' },
|
const mainCategories = [
|
||||||
{ id: 'interfaces', label: 'Interfaces' },
|
{ id: 'uebersicht', label: 'Uebersicht' },
|
||||||
{ id: 'services', label: 'Dienste' },
|
{ id: 'netzwerk', label: 'Netzwerk' },
|
||||||
{ id: 'tunnels', label: 'Tunnel' },
|
{ id: 'sicherheit', label: 'Sicherheit' },
|
||||||
{ id: 'vpn', label: 'VPN', platforms: ['freebsd'] },
|
{ id: 'daten', label: 'Daten' },
|
||||||
{ id: 'wireguard', label: 'WireGuard', platforms: ['freebsd'] },
|
{ id: 'system', label: 'System' },
|
||||||
{ 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' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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 }) {
|
export default function AgentPanel({ agentId, customers, onClose, onReload }) {
|
||||||
const [data, setData] = useState(null)
|
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)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setTab('overview')
|
setMainTab('uebersicht')
|
||||||
|
setSubTab('overview')
|
||||||
api.getAgent(agentId).then(setData).finally(() => setLoading(false))
|
api.getAgent(agentId).then(setData).finally(() => setLoading(false))
|
||||||
}, [agentId])
|
}, [agentId])
|
||||||
|
|
||||||
@ -50,7 +69,50 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) {
|
|||||||
const { agent, system_data: sys, status } = data
|
const { agent, system_data: sys, status } = data
|
||||||
const cust = agent.customer_id ? customers.find((c) => c.id === agent.customer_id) : null
|
const cust = agent.customer_id ? customers.find((c) => c.id === agent.customer_id) : null
|
||||||
const platform = agent.platform || 'freebsd'
|
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 (
|
return (
|
||||||
<Panel onClose={onClose}>
|
<Panel onClose={onClose}>
|
||||||
@ -86,57 +148,59 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Main Category Tabs */}
|
||||||
<div className="flex gap-1 mt-4 overflow-x-auto text-xs">
|
<div className="flex gap-1 mt-4 overflow-x-auto text-xs">
|
||||||
{tabs.map((t) => (
|
{visibleMainTabs.map((cat) => (
|
||||||
<button
|
<button
|
||||||
key={t.id}
|
key={cat.id}
|
||||||
onClick={() => setTab(t.id)}
|
onClick={() => handleMainTabChange(cat.id)}
|
||||||
className={`px-3 py-1.5 rounded-t whitespace-nowrap transition-colors ${
|
className={`px-4 py-1.5 whitespace-nowrap transition-colors border-b-2 ${
|
||||||
tab === t.id
|
mainTab === cat.id
|
||||||
? 'bg-gray-900 text-orange-400 border-t border-x border-gray-700'
|
? 'text-orange-400 border-orange-400'
|
||||||
: 'text-gray-400 hover:text-gray-200'
|
: 'text-gray-400 hover:text-gray-200 border-transparent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t.label}
|
{cat.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content area with optional sub-nav */}
|
||||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
{!sys ? (
|
{/* Sub-Navigation (left sidebar) — hidden for Uebersicht */}
|
||||||
<div className="text-gray-500">Keine Systemdaten</div>
|
{mainTab !== 'uebersicht' && currentSubs.length > 0 && (
|
||||||
) : tab === 'overview' ? (
|
<div className="w-44 shrink-0 bg-gray-900/50 border-r border-gray-800 overflow-y-auto py-3">
|
||||||
<OverviewTab sys={sys} />
|
<div className="px-4 mb-2">
|
||||||
) : tab === 'interfaces' ? (
|
<span className="text-[10px] uppercase tracking-wider text-gray-500 font-semibold">
|
||||||
<InterfacesTab sys={sys} />
|
{mainCategories.find(c => c.id === mainTab)?.label}
|
||||||
) : tab === 'services' ? (
|
</span>
|
||||||
<ServicesTab sys={sys} />
|
</div>
|
||||||
) : tab === 'tunnels' ? (
|
{currentSubs.map((item) => {
|
||||||
<TunnelTab agentId={agentId} agent={agent} />
|
const Icon = item.icon
|
||||||
) : tab === 'vpn' ? (
|
const isActive = subTab === item.id
|
||||||
<VPNTab sys={sys} />
|
return (
|
||||||
) : tab === 'wireguard' ? (
|
<button
|
||||||
<WireGuardTab agentId={agentId} sys={sys} />
|
key={item.id}
|
||||||
) : tab === 'routes' ? (
|
onClick={() => setSubTab(item.id)}
|
||||||
<RoutesTab sys={sys} />
|
className={`w-full flex items-center gap-2.5 px-4 py-2 text-sm transition-colors ${
|
||||||
) : tab === 'dhcp' ? (
|
isActive
|
||||||
<DHCPTab sys={sys} />
|
? 'text-orange-400 bg-orange-400/5 border-l-2 border-orange-400'
|
||||||
) : tab === 'certs' ? (
|
: 'text-gray-400 hover:text-white border-l-2 border-transparent'
|
||||||
<CertsTab sys={sys} />
|
}`}
|
||||||
) : tab === 'updates' ? (
|
>
|
||||||
<UpdatesTab agentId={agentId} />
|
{Icon && <Icon className="w-4 h-4 shrink-0" />}
|
||||||
) : tab === 'security' ? (
|
<span>{item.label}</span>
|
||||||
<SecurityTab sys={sys} />
|
</button>
|
||||||
) : tab === 'tasks' ? (
|
)
|
||||||
<TasksTab agentId={agentId} />
|
})}
|
||||||
) : tab === 'backups' ? (
|
</div>
|
||||||
platform === 'linux' ? <PVEBackupsTab sys={sys} /> : <BackupsTab agentId={agentId} />
|
)}
|
||||||
) : tab === 'agent' ? (
|
|
||||||
<AgentTab agent={data?.agent} status={data?.status} platform={platform} />
|
{/* Main content */}
|
||||||
) : null}
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
@ -204,7 +268,7 @@ function Panel({ onClose, children }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-40 bg-black/50" onClick={onClose} />
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<style>{`
|
<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 }) {
|
function MetricCard({ icon: Icon, label, value, sub, bar, barColor }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
|
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user