feat: Agent采集uptime, 后端持久化kernel/uptime/diskIO, 前端磁盘BPS+IOPS展示

- Agent: system.go 添加 readUptime(), SystemStat 携带 Uptime
- Backend: Server 模型新增 Kernel 字段, ReceiveReport 持久化 kernel+uptime
- Backend: /agent/latest 返回 kernel/uptime/port_info/disk_io_info/disk_total
- Frontend: 磁盘监控改为 BPS+IOPS (4张卡片), 系统信息修复内核/运行时长/监听端口/磁盘容量
- 图表单位从 MB/s 改为 KB/s, 添加 formatBPS 工具函数
This commit is contained in:
attacker committed 2026-06-19 01:10:00 +00:00
1 parent 31c2e338a3
commit 5949ec5d0c
48 files changed
+2182 -956

No files matched your search

@@ -40,25 +40,36 @@ const AlertCenter: React.FC = () => {
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<any>(null);
const [quickSetupLoading, setQuickSetupLoading] = useState(false);
// 分页
const [page, setPage] = useState(1);
const [pageSize] = useState(10);
const [total, setTotal] = useState(0);
const totalPages = Math.max(1, Math.ceil(total / pageSize));
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, []);
}, [page, severityFilter, statusFilter]);
const fetchData = async () => {
await Promise.all([fetchAlerts(), fetchRules()]);
};
// Logan.Li — 获取告警列表
// Logan.Li — 获取告警列表(分页)
const fetchAlerts = async () => {
setLoading(true);
try {
const res = await alertCenterApi.getAlerts();
const raw = (res.list || []) as any[];
const res: any = await alertCenterApi.getAlerts({
page,
page_size: pageSize,
severity: severityFilter !== 'all' ? severityFilter : undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
});
const raw = (res.list || res.data?.list || []) as any[];
const data: Alert[] = raw.map(mapAlert);
setAlerts(data);
setTotal(res.total || res.data?.total || data.length);
setStats({
firing: data.filter(a => a.status === 'firing').length,
critical: data.filter(a => a.severity === 'critical' && a.status === 'firing').length,
@@ -160,67 +171,64 @@ const AlertCenter: React.FC = () => {
try { await agentApi.deleteRule(Number(rule.id)); toast.success('已删除'); fetchRules(); } catch { toast.error('删除失败'); }
};
const filteredAlerts = alerts.filter(alert => {
const matchSearch = alert.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
alert.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
alert.source.toLowerCase().includes(searchQuery.toLowerCase());
const matchSeverity = severityFilter === 'all' || alert.severity === severityFilter;
const matchStatus = statusFilter === 'all' || alert.status === statusFilter;
return matchSearch && matchSeverity && matchStatus;
});
const handleFilterChange = (type: 'severity' | 'status', value: string) => {
if (type === 'severity') setSeverityFilter(value);
else setStatusFilter(value);
setPage(1); // 过滤条件变化时重置到第一页
};
return (
<div className="space-y-6">
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<div className="space-y-3">
{/* 统计卡片 — 紧凑 */}
<div className="grid grid-cols-4 gap-3">
<Card className={stats.firing > 0 ? 'border-red-500 border-2' : ''}>
<CardContent className="p-4">
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">触发中</p>
<p className={`text-2xl font-bold ${stats.firing > 0 ? 'text-red-600' : ''}`}>{stats.firing}</p>
<p className="text-xs text-muted-foreground">触发中</p>
<p className={`text-xl font-bold ${stats.firing > 0 ? 'text-red-600' : ''}`}>{stats.firing}</p>
</div>
<div className={`p-3 rounded-lg ${stats.firing > 0 ? 'bg-red-100' : 'bg-gray-100'}`}>
<Zap className={`w-5 h-5 ${stats.firing > 0 ? 'text-red-600' : 'text-gray-600'}`} />
<div className={`p-2 rounded-md ${stats.firing > 0 ? 'bg-red-100' : 'bg-gray-100'}`}>
<Zap className={`w-4 h-4 ${stats.firing > 0 ? 'text-red-600' : 'text-gray-600'}`} />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">严重告警</p>
<p className="text-2xl font-bold text-red-600">{stats.critical}</p>
<p className="text-xs text-muted-foreground">严重告警</p>
<p className="text-xl font-bold text-red-600">{stats.critical}</p>
</div>
<div className="p-3 bg-red-100 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-600" />
<div className="p-2 bg-red-100 rounded-md">
<AlertCircle className="w-4 h-4 text-red-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">警告告警</p>
<p className="text-2xl font-bold text-orange-600">{stats.warning}</p>
<p className="text-xs text-muted-foreground">警告告警</p>
<p className="text-xl font-bold text-orange-600">{stats.warning}</p>
</div>
<div className="p-3 bg-orange-100 rounded-lg">
<AlertTriangle className="w-5 h-5 text-orange-600" />
<div className="p-2 bg-orange-100 rounded-md">
<AlertTriangle className="w-4 h-4 text-orange-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">今日已解决</p>
<p className="text-2xl font-bold text-green-600">{stats.resolved}</p>
<p className="text-xs text-muted-foreground">今日已解决</p>
<p className="text-xl font-bold text-green-600">{stats.resolved}</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<CheckCircle className="w-5 h-5 text-green-600" />
<div className="p-2 bg-green-100 rounded-md">
<CheckCircle className="w-4 h-4 text-green-600" />
</div>
</div>
</CardContent>
@@ -228,27 +236,27 @@ const AlertCenter: React.FC = () => {
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3 lg:w-[400px]">
<TabsTrigger value="alerts">告警列表</TabsTrigger>
<TabsTrigger value="rules">告警规则</TabsTrigger>
<TabsTrigger value="channels">通知渠道</TabsTrigger>
<TabsList className="grid w-full grid-cols-3 lg:w-[360px] h-9">
<TabsTrigger value="alerts" className="text-xs">告警列表</TabsTrigger>
<TabsTrigger value="rules" className="text-xs">告警规则</TabsTrigger>
<TabsTrigger value="channels" className="text-xs">通知渠道</TabsTrigger>
</TabsList>
{/* 告警列表 Tab */}
<TabsContent value="alerts" className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-2 flex-1 min-w-[300px]">
<TabsContent value="alerts" className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2 flex-1 min-w-[280px]">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
<Input
placeholder="搜索告警标题、描述、来源..."
placeholder="搜索告警..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
className="pl-8 h-8 text-xs"
/>
</div>
<Select value={severityFilter} onValueChange={setSeverityFilter}>
<SelectTrigger className="w-[120px]">
<Select value={severityFilter} onValueChange={(v) => handleFilterChange('severity', v)}>
<SelectTrigger className="w-[100px] h-8 text-xs">
<SelectValue placeholder="级别" />
</SelectTrigger>
<SelectContent>
@@ -258,8 +266,8 @@ const AlertCenter: React.FC = () => {
<SelectItem value="info">信息</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[120px]">
<Select value={statusFilter} onValueChange={(v) => handleFilterChange('status', v)}>
<SelectTrigger className="w-[100px] h-8 text-xs">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
@@ -270,76 +278,85 @@ const AlertCenter: React.FC = () => {
</SelectContent>
</Select>
</div>
<Button variant="outline" onClick={fetchAlerts}>
<RefreshCw className="w-4 h-4 mr-2" />
刷新
</Button>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span>共 {total} 条</span>
<Button variant="outline" size="sm" onClick={fetchAlerts} className="h-7 text-xs px-2">
<RefreshCw className="w-3 h-3 mr-1" />刷新
</Button>
</div>
</div>
{/* 告警列表 */}
{/* 告警表格 — 紧凑 */}
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<table className="w-full text-sm">
<thead className="bg-muted/50">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">告警信息</th>
<th className="px-4 py-3 text-left text-sm font-medium">级别</th>
<th className="px-4 py-3 text-left text-sm font-medium">状态</th>
<th className="px-4 py-3 text-left text-sm font-medium">来源</th>
<th className="px-4 py-3 text-left text-sm font-medium">持续时间</th>
<th className="px-4 py-3 text-left text-sm font-medium">操作</th>
<th className="px-3 py-2 text-left text-xs font-medium">告警信息</th>
<th className="px-3 py-2 text-left text-xs font-medium w-16">级别</th>
<th className="px-3 py-2 text-left text-xs font-medium w-16">状态</th>
<th className="px-3 py-2 text-left text-xs font-medium w-24">来源</th>
<th className="px-3 py-2 text-left text-xs font-medium w-20">持续</th>
<th className="px-3 py-2 text-left text-xs font-medium w-28">操作</th>
</tr>
</thead>
<tbody className="divide-y">
{loading ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">加载中...</td></tr>
) : filteredAlerts.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">暂无告警</td></tr>
<tr><td colSpan={6} className="px-3 py-6 text-center text-xs text-muted-foreground">加载中...</td></tr>
) : alerts.length === 0 ? (
<tr><td colSpan={6} className="px-3 py-6 text-center text-xs text-muted-foreground">暂无告警</td></tr>
) : (
filteredAlerts.map((alert) => (
alerts.filter(alert => {
// 客户端也保留关键词搜索过滤
if (!searchQuery) return true;
const q = searchQuery.toLowerCase();
return alert.title.toLowerCase().includes(q) ||
alert.description.toLowerCase().includes(q) ||
alert.source.toLowerCase().includes(q);
}).map((alert) => (
<tr key={alert.id} className="hover:bg-muted/50">
<td className="px-4 py-3">
<div className="font-medium">{alert.title}</div>
<div className="text-sm text-muted-foreground line-clamp-1">{alert.description}</div>
<div className="flex gap-1 mt-1">
{Object.entries(alert.labels).slice(0, 3).map(([k, v]) => (
<Badge key={k} variant="outline" className="text-xs">{k}={v}</Badge>
<td className="px-3 py-2">
<div className="font-medium text-xs">{alert.title}</div>
<div className="text-xs text-muted-foreground line-clamp-1">{alert.description}</div>
<div className="flex gap-1 mt-0.5">
{Object.entries(alert.labels).slice(0, 2).map(([k, v]) => (
<Badge key={k} variant="outline" className="text-[10px] px-1 py-0">{k}={v}</Badge>
))}
</div>
</td>
<td className="px-4 py-3">{getSeverityBadge(alert.severity)}</td>
<td className="px-4 py-3">{getStatusBadge(alert.status)}</td>
<td className="px-4 py-3 text-sm">
<td className="px-3 py-2">{getSeverityBadge(alert.severity)}</td>
<td className="px-3 py-2">{getStatusBadge(alert.status)}</td>
<td className="px-3 py-2 text-xs">
<div>{alert.source}</div>
<div className="text-muted-foreground">{alert.service}</div>
<div className="text-muted-foreground text-[10px]">{alert.service}</div>
</td>
<td className="px-4 py-3 text-sm text-muted-foreground">
<td className="px-3 py-2 text-xs text-muted-foreground">
{alert.status === 'resolved' && alert.resolvedAt
? `${Math.floor((new Date(alert.resolvedAt).getTime() - new Date(alert.startedAt).getTime()) / 60000)}分钟`
: `${Math.floor((Date.now() - new Date(alert.startedAt).getTime()) / 60000)}分钟`}
? `${Math.floor((new Date(alert.resolvedAt).getTime() - new Date(alert.startedAt).getTime()) / 60000)}分`
: `${Math.floor((Date.now() - new Date(alert.startedAt).getTime()) / 60000)}分`}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm"
<td className="px-3 py-2">
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="sm" className="h-7 w-7 p-0"
onClick={() => { setSelectedAlert(alert); setDetailOpen(true); }}>
<Eye className="w-4 h-4" />
<Eye className="w-3.5 h-3.5" />
</Button>
{alert.status === 'firing' && (
<Button variant="ghost" size="sm"
<Button variant="ghost" size="sm" className="h-7 w-7 p-0"
onClick={() => handleAcknowledge(alert.id)}>
<Check className="w-4 h-4 text-blue-600" />
<Check className="w-3.5 h-3.5 text-blue-600" />
</Button>
)}
{(alert.status === 'firing' || alert.status === 'acknowledged') && (
<Button variant="ghost" size="sm"
<Button variant="ghost" size="sm" className="h-7 w-7 p-0"
onClick={() => handleResolve(alert.id)}>
<CheckCircle className="w-4 h-4 text-green-600" />
<CheckCircle className="w-3.5 h-3.5 text-green-600" />
</Button>
)}
<Button variant="ghost" size="sm"
<Button variant="ghost" size="sm" className="h-7 w-7 p-0"
onClick={() => { setSelectedAlert(alert); setSilenceOpen(true); }}>
<VolumeX className="w-4 h-4 text-orange-600" />
<VolumeX className="w-3.5 h-3.5 text-orange-600" />
</Button>
</div>
</td>
@@ -349,6 +366,34 @@ const AlertCenter: React.FC = () => {
</tbody>
</table>
</div>
{/* 分页控件 */}
{totalPages > 1 && (
<div className="flex items-center justify-between px-3 py-2 border-t text-xs">
<span className="text-muted-foreground">第 {page}/{totalPages} 页,共 {total} 条</span>
<div className="flex items-center gap-1">
<Button variant="outline" size="sm" className="h-7 text-xs px-2"
disabled={page <= 1} onClick={() => setPage(p => p - 1)}>
上一页
</Button>
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
const start = Math.max(1, Math.min(page - 2, totalPages - 4));
const pn = start + i;
if (pn > totalPages) return null;
return (
<Button key={pn} variant={pn === page ? 'default' : 'outline'} size="sm"
className="h-7 min-w-[28px] text-xs px-1"
onClick={() => setPage(pn)}>
{pn}
</Button>
);
})}
<Button variant="outline" size="sm" className="h-7 text-xs px-2"
disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>
下一页
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>