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:
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>
|
||||
|
||||
Reference in new issue
Block a user