feat: 修复服务器硬件规格(CPU核/内存/磁盘) + Top 5 资源排行
修复: - agent.go ReceiveReport 写入 cpu(核数)/memory(总内存MB)/disk(总磁盘GB) 到 servers 表 - 之前 Agent 上报只写了 cpu_usage/memory_usage/disk_usage 使用率,硬件规格始终为 0 新功能: - GET /api/v1/metrics/top?metric=cpu|memory|disk&limit=5 主机排行 API - HostMonitor 新增 Top 5 排行面板: CPU/内存/磁盘 使用率排名 - TopRankCard 组件: 排名号+主机名+进度条+百分比
This commit is contained in:
1 parent
06d7451cf9
commit
737f00169f
8 files changed
+150
-567
No files matched your search
@@ -5,7 +5,7 @@ import { useUserStore } from '@/store';
|
||||
import { asList, isOnline, timeAgo } from '@/components/agent/agentUtils';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Cpu, HardDrive, Network, Activity, Server, Loader2 } from 'lucide-react';
|
||||
import { Cpu, HardDrive, Network, Activity, Server, Loader2, TrendingUp } from 'lucide-react';
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
@@ -50,6 +50,26 @@ export function HostMonitor() {
|
||||
const [cardLoading, setCardLoading] = useState(true);
|
||||
const [chartLoading, setChartLoading] = useState(false);
|
||||
|
||||
// Top 5 排行
|
||||
const [topCPU, setTopCPU] = useState<any[]>([]);
|
||||
const [topMem, setTopMem] = useState<any[]>([]);
|
||||
const [topDisk, setTopDisk] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
import('@/services/http').then(async ({ http }) => {
|
||||
try {
|
||||
const [cpu, mem, disk] = await Promise.all([
|
||||
http.get('/metrics/top?metric=cpu&limit=5'),
|
||||
http.get('/metrics/top?metric=memory&limit=5'),
|
||||
http.get('/metrics/top?metric=disk&limit=5'),
|
||||
]);
|
||||
setTopCPU(Array.isArray(cpu) ? cpu : []);
|
||||
setTopMem(Array.isArray(mem) ? mem : []);
|
||||
setTopDisk(Array.isArray(disk) ? disk : []);
|
||||
} catch { /* 后端未就绪时静默 */ }
|
||||
});
|
||||
}, [hosts]);
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -196,6 +216,15 @@ export function HostMonitor() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Top 5 排行 */}
|
||||
{(topCPU.length > 0 || topMem.length > 0 || topDisk.length > 0) && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<TopRankCard title="🔥 CPU 使用率 Top 5" icon={<Cpu className="h-4 w-4 text-red-400" />} data={topCPU} valueKey="cpu_usage" unit="%" color="red" />
|
||||
<TopRankCard title="💾 内存使用率 Top 5" icon={<Server className="h-4 w-4 text-amber-400" />} data={topMem} valueKey="memory_usage" unit="%" color="amber" />
|
||||
<TopRankCard title="💿 磁盘使用率 Top 5" icon={<HardDrive className="h-4 w-4 text-orange-400" />} data={topDisk} valueKey="disk_usage" unit="%" color="orange" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 概览卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
@@ -374,3 +403,47 @@ function RateItem({ label, value, color }: { label: string; value: string; color
|
||||
}
|
||||
|
||||
export default HostMonitor;
|
||||
|
||||
/** Top N 排行卡片 */
|
||||
function TopRankCard({
|
||||
title, icon, data, valueKey, unit, color,
|
||||
}: {
|
||||
title: string; icon: React.ReactNode; data: any[]; valueKey: string; unit: string; color: string;
|
||||
}) {
|
||||
const barColors: Record<string, string> = { red: 'bg-red-500', amber: 'bg-amber-500', orange: 'bg-orange-500', emerald: 'bg-emerald-500' };
|
||||
const barColor = barColors[color] || 'bg-red-500';
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{icon}
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">{title}</h3>
|
||||
</div>
|
||||
{data.length === 0 ? (
|
||||
<p className="text-xs text-[var(--dg-text-muted)] py-4 text-center">暂无数据</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{data.map((item: any, i: number) => {
|
||||
const val = Number(item[valueKey] || 0);
|
||||
const barPct = Math.min(val, 100);
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-2 text-xs">
|
||||
<span className="w-4 text-[var(--dg-text-muted)] font-mono text-right">{i + 1}</span>
|
||||
<span className="w-24 truncate text-[var(--dg-text-secondary)]" title={item.hostname}>{item.hostname}</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${barColor} transition-all`}
|
||||
style={{ width: `${barPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="w-12 text-right font-mono text-[var(--dg-text-primary)]">{val.toFixed(1)}{unit}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user