- WindowsPanel.jsx: Uebersicht (CPU/RAM/Disk), Software (winget), System - Dashboard: Windows-Agents mit WIN-Badge, oeffnet WindowsPanel - Gleiche Navigation wie OPNsense/Proxmox (Sidebar-Menue)
197 lines
7.4 KiB
JavaScript
197 lines
7.4 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import api from '../api/client'
|
|
import StatusBadge from '../components/StatusBadge'
|
|
import AgentPanel from '../components/AgentPanel'
|
|
import ProxmoxPanel from '../components/ProxmoxPanel'
|
|
import WindowsPanel from '../components/WindowsPanel'
|
|
import { Server, HardDrive, Users, Activity, AlertTriangle, Monitor } from 'lucide-react'
|
|
|
|
export default function Dashboard() {
|
|
const [agents, setAgents] = useState([])
|
|
const [customers, setCustomers] = useState([])
|
|
const [agentDetails, setAgentDetails] = useState({})
|
|
const [loading, setLoading] = useState(true)
|
|
const [selectedId, setSelectedId] = useState(null)
|
|
const [selectedType, setSelectedType] = useState(null) // 'device' | 'proxmox' | 'windows'
|
|
|
|
const reload = () => {
|
|
Promise.all([api.getAgents(), api.getCustomers()])
|
|
.then(([a, c]) => {
|
|
setAgents(a || [])
|
|
setCustomers(c || [])
|
|
// Lade Details fuer Typ-Erkennung
|
|
;(a || []).forEach((agent) => {
|
|
if (!agentDetails[agent.id]) {
|
|
api.getAgent(agent.id).then((d) => {
|
|
setAgentDetails((prev) => ({ ...prev, [agent.id]: d }))
|
|
})
|
|
}
|
|
})
|
|
})
|
|
.finally(() => setLoading(false))
|
|
}
|
|
|
|
const isProxmox = (agentId) => {
|
|
const d = agentDetails[agentId]
|
|
return d?.system_data?.proxmox?.available === true
|
|
}
|
|
const getAgentType = (agent) => {
|
|
if (isProxmox(agent.id)) return 'proxmox'
|
|
if (agent.platform === 'windows') return 'windows'
|
|
return 'device'
|
|
}
|
|
|
|
useEffect(() => {
|
|
reload()
|
|
}, [])
|
|
|
|
if (loading) {
|
|
return <div className="text-gray-500">Laden...</div>
|
|
}
|
|
|
|
const online = agents.filter((a) => a.status === 'online').length
|
|
const offline = agents.filter((a) => a.status === 'offline').length
|
|
const stale = agents.filter((a) => a.status === 'stale').length
|
|
const unassigned = agents.filter((a) => !a.customer_id).length
|
|
|
|
// Agents nach Kunde gruppieren
|
|
const customerMap = {}
|
|
customers.forEach((c) => {
|
|
customerMap[c.id] = { ...c, agents: [] }
|
|
})
|
|
agents.forEach((a) => {
|
|
if (a.customer_id && customerMap[a.customer_id]) {
|
|
customerMap[a.customer_id].agents.push(a)
|
|
}
|
|
})
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-xl font-bold text-white">Dashboard</h1>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
|
|
<StatCard icon={Server} label="Geraete" value={agents.filter(a => !isProxmox(a.id)).length} color="text-blue-400" />
|
|
<StatCard icon={HardDrive} label="Proxmox" value={agents.filter(a => isProxmox(a.id)).length} color="text-purple-400" />
|
|
<StatCard icon={Activity} label="Online" value={online} color="text-green-400" />
|
|
<StatCard icon={AlertTriangle} label="Offline" value={offline} color="text-red-400" />
|
|
<StatCard icon={Users} label="Kunden" value={customers.length} color="text-orange-400" />
|
|
</div>
|
|
|
|
{/* Agent-Liste nach Kunde */}
|
|
<div className="space-y-4">
|
|
{Object.values(customerMap)
|
|
.filter((c) => c.agents.length > 0)
|
|
.sort((a, b) => a.number.localeCompare(b.number))
|
|
.map((customer) => (
|
|
<div key={customer.id} className="bg-gray-900 rounded-lg border border-gray-800">
|
|
<div className="px-4 py-3 border-b border-gray-800 flex items-center justify-between">
|
|
<span className="text-sm font-medium text-gray-300">
|
|
<span className="text-orange-400">{customer.number}</span> — {customer.name}
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
{customer.agents.length} {customer.agents.length === 1 ? 'Geraet' : 'Geraete'}
|
|
</span>
|
|
</div>
|
|
<div className="divide-y divide-gray-800">
|
|
{customer.agents.map((agent) => (
|
|
<AgentRow key={agent.id} agent={agent} isPve={isProxmox(agent.id)} onClick={() => { setSelectedId(agent.id); setSelectedType(getAgentType(agent)) }} selected={selectedId === agent.id} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Nicht zugewiesene Agents */}
|
|
{unassigned > 0 && (
|
|
<div className="bg-gray-900 rounded-lg border border-gray-800">
|
|
<div className="px-4 py-3 border-b border-gray-800">
|
|
<span className="text-sm font-medium text-gray-500">Nicht zugewiesen</span>
|
|
</div>
|
|
<div className="divide-y divide-gray-800">
|
|
{agents
|
|
.filter((a) => !a.customer_id)
|
|
.map((agent) => (
|
|
<AgentRow key={agent.id} agent={agent} isPve={isProxmox(agent.id)} onClick={() => { setSelectedId(agent.id); setSelectedType(getAgentType(agent)) }} selected={selectedId === agent.id} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Detail Panels */}
|
|
{selectedId && selectedType === 'proxmox' && (
|
|
<ProxmoxPanel
|
|
agentId={selectedId}
|
|
customers={customers}
|
|
onClose={() => { setSelectedId(null); setSelectedType(null) }}
|
|
onReload={reload}
|
|
/>
|
|
)}
|
|
{selectedId && selectedType === 'windows' && (
|
|
<WindowsPanel
|
|
agentId={selectedId}
|
|
customers={customers}
|
|
onClose={() => { setSelectedId(null); setSelectedType(null) }}
|
|
onReload={reload}
|
|
/>
|
|
)}
|
|
{selectedId && selectedType === 'device' && (
|
|
<AgentPanel
|
|
agentId={selectedId}
|
|
customers={customers}
|
|
onClose={() => { setSelectedId(null); setSelectedType(null) }}
|
|
onReload={reload}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StatCard({ icon: Icon, label, value, color }) {
|
|
return (
|
|
<div className="bg-gray-900 rounded-lg border border-gray-800 px-4 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<Icon className={`w-5 h-5 ${color}`} />
|
|
<div>
|
|
<div className="text-2xl font-bold text-white">{value}</div>
|
|
<div className="text-xs text-gray-500">{label}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AgentRow({ agent, isPve, onClick, selected }) {
|
|
const lastHB = agent.last_heartbeat
|
|
? new Date(agent.last_heartbeat).toLocaleString('de-DE')
|
|
: '—'
|
|
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
className={`flex items-center justify-between px-4 py-2.5 hover:bg-gray-800/50 transition-colors cursor-pointer ${selected ? 'bg-gray-800/70' : ''}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<StatusBadge status={agent.status} size="dot" />
|
|
<div>
|
|
<div className="text-sm text-white inline-flex items-center gap-2">
|
|
<span className={`text-[10px] font-mono px-1 rounded ${
|
|
agent.platform === 'linux' ? 'bg-blue-900/50 text-blue-400' :
|
|
agent.platform === 'windows' ? 'bg-sky-900/50 text-sky-400' :
|
|
'bg-orange-900/50 text-orange-400'
|
|
}`}>
|
|
{agent.platform === 'linux' ? 'LNX' : agent.platform === 'windows' ? 'WIN' : 'OPN'}
|
|
</span>
|
|
{agent.name}
|
|
</div>
|
|
<div className="text-xs text-gray-500">{agent.ip}</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-xs text-gray-500">{agent.opnsense_version || '—'}</div>
|
|
<div className="text-xs text-gray-600">{lastHB}</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|