import { useCallback, useEffect, useState } from 'react' import { Cpu, HardDrive, MemoryStick, Network, Server } from 'lucide-react' import RingStats from '../components/RingStats' import ResourceStatsPanel, { ChartPoint, ResourceChartConfig, StatsRangeKey, statsRanges, } from '../components/ResourceStatsPanel' import { DashboardStats, getDashboard, getHostInfo, HostInfo } from '../services/api' type HostMetricPoint = { ts: number cpu: number memory: number network: number diskIO: number } const hostHistoryKey = 'clicd_host_metric_history_v2' export default function Dashboard() { const [stats, setStats] = useState(null) const [host, setHost] = useState(null) const [history, setHistory] = useState(readHostHistory) const [range, setRange] = useState('30m') const [loading, setLoading] = useState(true) const fetchData = useCallback(async () => { try { const [dashRes, hostRes] = await Promise.all([getDashboard(), getHostInfo()]) if (dashRes.data.data) setStats(dashRes.data.data) if (hostRes.data.data) { const nextHost = hostRes.data.data setHost(nextHost) appendHostPoint(nextHost, setHistory) } } catch (err) { console.error(err) } finally { setLoading(false) } }, []) useEffect(() => { fetchData() const interval = window.setInterval(fetchData, 5000) return () => window.clearInterval(interval) }, [fetchData]) if (loading) { return (
) } const filtered = filterHistory(history, range) const memoryPct = host && host.ram.total_mb > 0 ? (host.ram.used_mb / host.ram.total_mb) * 100 : 0 const networkBps = (host?.network.rx_bps || 0) + (host?.network.tx_bps || 0) const diskIOBps = (host?.disk_io.read_bps || 0) + (host?.disk_io.write_bps || 0) const charts: ResourceChartConfig[] = [ { title: 'CPU 使用率', icon: , current: host?.cpu.usage_pct || 0, points: toChartPoints(filtered, 'cpu'), max: 100, formatValue: formatPercent, detail: `${host?.cpu.cores || 0} 核`, }, { title: '内存使用', icon: , current: memoryPct, points: toChartPoints(filtered, 'memory'), max: 100, formatValue: formatPercent, detail: `${formatMB(host?.ram.used_mb || 0)} / ${formatMB(host?.ram.total_mb || 0)}`, }, { title: '网络流量', icon: , current: networkBps, points: toChartPoints(filtered, 'network'), formatValue: formatRate, detail: `入 ${formatRate(host?.network.rx_bps || 0)} / 出 ${formatRate(host?.network.tx_bps || 0)}`, }, { title: '磁盘IO', icon: , current: diskIOBps, points: toChartPoints(filtered, 'diskIO'), formatValue: formatRate, detail: `读 ${formatRate(host?.disk_io.read_bps || 0)} / 写 ${formatRate(host?.disk_io.write_bps || 0)}`, }, ] return (

控制面板

宿主机资源状态与容器概览

} title="容器总数" value={stats?.total_containers || 0} />
{host && ( 0 ? (host.ram.used_mb / host.ram.total_mb) * 100 : 0} ramUsed={host.ram.used_mb} ramTotal={host.ram.total_mb} loadPercent={(host.load.load1 / Math.max(host.cpu.cores, 1)) * 100} diskPercent={host.disk.total_gb > 0 ? (host.disk.used_gb / host.disk.total_gb) * 100 : 0} diskUsed={host.disk.used_gb * 1024} diskTotal={host.disk.total_gb * 1024} /> )}
) } function SummaryCard({ icon, dot, title, value, muted = false, }: { icon?: JSX.Element dot?: string title: string value: number muted?: boolean }) { return (
{icon} {dot && } {title}
{value}
) } function appendHostPoint(host: HostInfo, setHistory: (updater: (prev: HostMetricPoint[]) => HostMetricPoint[]) => void) { const point: HostMetricPoint = { ts: Date.now(), cpu: clamp(host.cpu.usage_pct), memory: host.ram.total_mb > 0 ? clamp((host.ram.used_mb / host.ram.total_mb) * 100) : 0, network: (host.network.rx_bps || 0) + (host.network.tx_bps || 0), diskIO: (host.disk_io.read_bps || 0) + (host.disk_io.write_bps || 0), } setHistory((prev) => { const cutoff = Date.now() - statsRanges['1w'] const next = [...prev.filter((item) => item.ts >= cutoff), point] localStorage.setItem(hostHistoryKey, JSON.stringify(next)) return next }) } function readHostHistory(): HostMetricPoint[] { try { const raw = localStorage.getItem(hostHistoryKey) if (!raw) return [] const parsed = JSON.parse(raw) as HostMetricPoint[] const cutoff = Date.now() - statsRanges['1w'] return parsed.filter((item) => item.ts >= cutoff) } catch { return [] } } function filterHistory(history: HostMetricPoint[], range: StatsRangeKey) { const cutoff = Date.now() - statsRanges[range] return history.filter((point) => point.ts >= cutoff) } function toChartPoints>(history: HostMetricPoint[], key: T): ChartPoint[] { return history.map((point) => ({ ts: point.ts, value: Number(point[key]) || 0 })) } function clamp(value: number) { if (!Number.isFinite(value)) return 0 return Math.max(0, Math.min(value, 100)) } function formatPercent(value: number) { return `${value.toFixed(1)}%` } function formatMB(mb: number) { if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB` return `${Math.round(mb)} MB` } function formatBytes(value: number) { if (value < 1024) return `${value.toFixed(0)} B` if (value < 1024 * 1024) return `${(value / 1024).toFixed(2)} KB` return `${(value / 1024 / 1024).toFixed(2)} MB` } function formatRate(value: number) { return `${formatBytes(value)}/s` }