rmm2/frontend/src/pages/Dashboard.jsx
cynfo3000 4f71bd1520 feat: Windows Agent Frontend Panel
- WindowsPanel.jsx: Uebersicht (CPU/RAM/Disk), Software (winget), System
- Dashboard: Windows-Agents mit WIN-Badge, oeffnet WindowsPanel
- Gleiche Navigation wie OPNsense/Proxmox (Sidebar-Menue)
2026-03-10 01:28:30 +01:00

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>
)
}