feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现

F1 告警静默规则 [done]:
- 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式)
- 告警引擎 isSilenced 集成 (alert_engine.go)
- 前端 SilenceRuleTab + SilenceDialog
- 路由 /alerts/silences 已注册

F2 审计日志导出 [done]:
- 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM)
- 支持 module/action/username/startTime/endTime 筛选
- 导出限制 10000 条防溢出,自动记录审计日志
- JSON 格式向后兼容 (format=json)
- 前端 exportLogs 传递查询参数

同时包含累积的前期工作:
- 目录重构 internal/ -> controller/model/router (v0.55-v0.57)
- 后端分层架构 dto/repository/service/utils
- 审计模块全功能 (终端审计/操作审计/登录日志)
- asciicast v2 录制格式
- SFTP 文件浏览器
- StatStrip 紧凑统计条
- 在线会话自动刷新
- LiveMonitor WebSocket 诊断修复

验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
attacker committed 2026-06-25 20:16:30 +08:00
1 parent e57718998a
commit 77c234b060
314 files changed
+10050 -4065

No files matched your search

@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { StatStrip } from '@/components/ui/stat-strip';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
@@ -24,6 +25,7 @@ import { AlertDetailDialog, AlertSilenceDialog } from './AlertCenterDetail';
import { AlertRuleDialog } from '@/components/monitor/AlertRuleDialog';
import { ContactDialog } from './ContactDialog';
import { ContactGroupDialog } from './ContactGroupDialog';
import SilenceRuleTab from './SilenceRuleTab';
import type { Contact, ContactGroup } from './alert-center-helpers';
const AlertCenter: React.FC = () => {
@@ -151,8 +153,21 @@ const AlertCenter: React.FC = () => {
const handleSilence = async (id: string, duration: string) => {
try {
await alertCenterApi.silence(id, duration);
toast.success('告警已静默');
const alert = alerts.find(a => a.id === id);
const now = new Date();
const match = duration.match(/^(\d+)([hd])$/);
const num = match ? parseInt(match[1]) : 1;
const unit = match?.[2] || 'h';
const hours = unit === 'd' ? num * 24 : num;
const endTime = new Date(now.getTime() + hours * 3600_000);
await alertCenterApi.createSilence({
name: `静默: ${alert?.title || id} (${duration})`,
description: `从告警列表快速静默 — ${alert?.source || ''}`,
silence_type: 'onetime',
start_time: now.toISOString(),
end_time: endTime.toISOString(),
});
toast.success(`告警已静默 ${duration}`);
setSilenceOpen(false);
fetchAlerts();
} catch {
@@ -205,66 +220,21 @@ const AlertCenter: React.FC = () => {
return (
<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-3">
<div className="flex items-center justify-between">
<div>
<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-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-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">严重告警</p>
<p className="text-xl font-bold text-red-600">{stats.critical}</p>
</div>
<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-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">警告告警</p>
<p className="text-xl font-bold text-orange-600">{stats.warning}</p>
</div>
<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-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground">今日已解决</p>
<p className="text-xl font-bold text-green-600">{stats.resolved}</p>
</div>
<div className="p-2 bg-green-100 rounded-md">
<CheckCircle className="w-4 h-4 text-green-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* 统计 — 紧凑条 */}
<StatStrip
items={[
{ label: '触发中', value: stats.firing, color: stats.firing > 0 ? 'red' : 'muted' },
{ label: '严重', value: stats.critical, color: 'red' },
{ label: '警告', value: stats.warning, color: 'amber' },
{ label: '已解决', value: stats.resolved, color: 'emerald' },
]}
/>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4 lg:w-[480px] h-9">
<TabsList className="grid w-full grid-cols-5 lg:w-[600px] h-9">
<TabsTrigger value="alerts" className="text-xs">告警列表</TabsTrigger>
<TabsTrigger value="rules" className="text-xs">告警规则</TabsTrigger>
<TabsTrigger value="silences" className="text-xs">静默规则</TabsTrigger>
<TabsTrigger value="contacts" className="text-xs">报警联系人</TabsTrigger>
<TabsTrigger value="groups" className="text-xs">报警联系组</TabsTrigger>
</TabsList>
@@ -501,6 +471,11 @@ const AlertCenter: React.FC = () => {
)}
</TabsContent>
{/* 静默规则 Tab */}
<TabsContent value="silences">
<SilenceRuleTab />
</TabsContent>
{/* 报警联系人 Tab */}
<TabsContent value="contacts" className="space-y-4">
<div className="flex justify-between items-center">
@@ -0,0 +1,255 @@
// 告警静默规则创建/编辑弹框 // Logan.Li
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { toast } from 'sonner';
import { alertCenterApi } from '@/services/api';
// 静默类型中文标签
const SILENCE_TYPE_LABELS: Record<string, string> = {
onetime: '一次性',
daily: '每天循环',
weekly: '每周循环',
};
const WEEKDAY_LABELS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
export interface SilenceData {
id?: number;
name: string;
description: string;
silence_type: string;
start_time: string;
end_time: string;
weekdays: number[];
}
function toLocalDateTimeInput(d: Date): string {
const off = d.getTimezoneOffset();
const local = new Date(d.getTime() - off * 60000);
return local.toISOString().slice(0, 16);
}
function addHours(d: Date, hours: number): Date {
return new Date(d.getTime() + hours * 3600_000);
}
export function SilenceDialog({
open,
onOpenChange,
editingSilence,
onSaved,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingSilence: any;
onSaved: () => void;
}) {
const [saving, setSaving] = useState(false);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [silenceType, setSilenceType] = useState('onetime');
const [startTime, setStartTime] = useState('');
const [endTime, setEndTime] = useState('');
const [weekdays, setWeekdays] = useState<number[]>([]);
// 编辑时回填
useEffect(() => {
if (editingSilence) {
setName(editingSilence.name || '');
setDescription(editingSilence.description || '');
setSilenceType(editingSilence.silence_type || 'onetime');
// 解析时间
const st = editingSilence.start_time ? new Date(editingSilence.start_time) : new Date();
const et = editingSilence.end_time ? new Date(editingSilence.end_time) : addHours(new Date(), 1);
setStartTime(toLocalDateTimeInput(st));
setEndTime(toLocalDateTimeInput(et));
// 解析 weekdays
let wds: number[] = [];
if (editingSilence.weekday_list) {
wds = editingSilence.weekday_list;
} else if (editingSilence.weekdays) {
try { wds = JSON.parse(editingSilence.weekdays); } catch { /* ignore */ }
}
setWeekdays(wds);
} else {
setName('');
setDescription('');
setSilenceType('onetime');
const now = new Date();
setStartTime(toLocalDateTimeInput(now));
setEndTime(toLocalDateTimeInput(addHours(now, 2)));
setWeekdays([]);
}
}, [editingSilence, open]);
const toggleWeekday = (day: number) => {
setWeekdays(prev =>
prev.includes(day) ? prev.filter(d => d !== day) : [...prev, day]
);
};
const handleSave = async () => {
if (!name.trim()) {
toast.error('请填写规则名称');
return;
}
if (!endTime) {
toast.error('请设置结束时间');
return;
}
if (silenceType === 'weekly' && weekdays.length === 0) {
toast.error('每周静默需要选择至少一个星期');
return;
}
const payload: Record<string, unknown> = {
name: name.trim(),
description: description.trim(),
silence_type: silenceType,
start_time: startTime,
end_time: endTime,
weekdays: silenceType === 'weekly' ? weekdays : [],
};
setSaving(true);
try {
if (editingSilence) {
await alertCenterApi.updateSilence(editingSilence.id, payload);
toast.success('静默规则已更新');
} else {
await alertCenterApi.createSilence(payload);
toast.success('静默规则已创建');
}
onOpenChange(false);
onSaved();
} catch {
toast.error('保存失败');
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-lg">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{editingSilence ? '编辑静默规则' : '创建静默规则'}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
静默期间匹配的告警将不会触发通知
</DialogDescription>
</DialogHeader>
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
{/* 名称 */}
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-primary)]">规则名称 *</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="如:维护窗口静默"
className="bg-[var(--dg-bg-primary)]"
/>
</div>
{/* 描述 */}
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-primary)]">描述</Label>
<Input
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="可选备注"
className="bg-[var(--dg-bg-primary)]"
/>
</div>
{/* 静默类型 */}
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-primary)]">静默类型</Label>
<Select value={silenceType} onValueChange={setSilenceType}>
<SelectTrigger className="bg-[var(--dg-bg-primary)]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(SILENCE_TYPE_LABELS).map(([val, label]) => (
<SelectItem key={val} value={val}>{label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 时间窗口 */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-primary)]">
{silenceType === 'onetime' ? '开始时间' : '每日开始'}
</Label>
<Input
type="datetime-local"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="bg-[var(--dg-bg-primary)]"
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-primary)]">
{silenceType === 'onetime' ? '结束时间' : '每日结束'}
</Label>
<Input
type="datetime-local"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="bg-[var(--dg-bg-primary)]"
/>
</div>
</div>
{/* 星期选择(仅 weekly) */}
{silenceType === 'weekly' && (
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-primary)]">生效星期</Label>
<div className="flex flex-wrap gap-2">
{WEEKDAY_LABELS.map((label, idx) => (
<button
key={idx}
type="button"
onClick={() => toggleWeekday(idx)}
className={`px-3 py-1.5 rounded-md text-xs transition-colors ${
weekdays.includes(idx)
? 'bg-primary text-primary-foreground'
: 'bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] text-[var(--dg-text-secondary)]'
}`}
>
{label}
</button>
))}
</div>
</div>
)}
{/* 提示 */}
<div className="text-xs text-muted-foreground bg-muted/50 rounded-md p-2">
{silenceType === 'onetime' && '在指定时间窗口内,所有匹配条件的告警将被静默。窗口结束后自动恢复正常。'}
{silenceType === 'daily' && '每天在指定时段内静默告警,适合非工作时间免打扰。'}
{silenceType === 'weekly' && '仅在选定星期的指定时段内静默,适合周末维护窗口。'}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? '保存中…' : '保存'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,226 @@
// 告警静默规则管理 Tab — 列表 + 创建/编辑/删除/启停 // Logan.Li
import React, { useState, useEffect, useCallback } from 'react';
import { VolumeX, Plus, RefreshCw, Clock, Calendar, Repeat, Edit, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { alertCenterApi } from '@/services/api';
import { SilenceDialog } from './SilenceDialog';
const SILENCE_TYPE_LABELS: Record<string, string> = {
onetime: '一次性',
daily: '每天',
weekly: '每周',
};
const WEEKDAY_LABELS = ['日', '一', '二', '三', '四', '五', '六'];
function formatTime(t: string | Date): string {
if (!t) return '—';
const d = typeof t === 'string' ? new Date(t) : t;
if (isNaN(d.getTime())) return '—';
return d.toLocaleString('zh-CN', {
month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit',
});
}
const SilenceRuleTab: React.FC = () => {
const confirm = useConfirm();
const [silences, setSilences] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState<any>(null);
const fetchSilences = useCallback(async () => {
setLoading(true);
try {
const res: Record<string, any> = await alertCenterApi.getSilences();
const list = res?.list || res?.data?.list || [];
setSilences(list);
} catch {
toast.error('获取静默规则失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchSilences();
}, [fetchSilences]);
const handleAdd = () => {
setEditing(null);
setDialogOpen(true);
};
const handleEdit = (silence: any) => {
setEditing(silence);
setDialogOpen(true);
};
const handleDelete = async (silence: any) => {
const ok = await confirm({
title: '删除静默规则',
description: `确定删除「${silence.name}」?删除后该规则匹配的告警将恢复通知。`,
confirmText: '删除',
variant: 'destructive',
});
if (!ok) return;
try {
await alertCenterApi.deleteSilence(silence.id);
toast.success('已删除');
fetchSilences();
} catch {
toast.error('删除失败');
}
};
const handleToggle = async (silence: any) => {
try {
await alertCenterApi.toggleSilence(silence.id);
toast.success(silence.status === 1 ? '已禁用' : '已启用');
fetchSilences();
} catch {
toast.error('操作失败');
}
};
const isCurrentlyActive = (s: any): boolean => {
if (s.is_active !== undefined) return s.is_active;
if (s.status !== 1) return false;
const now = new Date();
const st = new Date(s.start_time);
const et = new Date(s.end_time);
return now >= st && now <= et;
};
return (
<div className="space-y-4">
{/* 操作栏 */}
<div className="flex justify-between items-center">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<VolumeX className="w-4 h-4" />
<span>共 {silences.length} 条规则</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={fetchSilences} disabled={loading}>
<RefreshCw className="w-3.5 h-3.5 mr-1" />
刷新
</Button>
<Button size="sm" onClick={handleAdd}>
<Plus className="w-3.5 h-3.5 mr-1" />
添加静默规则
</Button>
</div>
</div>
{/* 规则列表 */}
{loading && silences.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<p className="text-sm">加载中...</p>
</div>
) : silences.length === 0 ? (
<div className="text-center py-12 text-muted-foreground border border-dashed rounded-lg">
<VolumeX className="w-10 h-10 mx-auto mb-2 opacity-50" />
<p className="text-sm">暂无静默规则</p>
<p className="text-xs mt-1">点击「添加静默规则」创建维护窗口或免打扰时段</p>
</div>
) : (
<div className="grid grid-cols-1 gap-3">
{silences.map((s) => {
const active = isCurrentlyActive(s);
const enabled = s.status === 1;
const sType = s.silence_type || 'onetime';
const weekdayList: number[] = s.weekday_list || (() => {
try { return JSON.parse(s.weekdays || '[]'); } catch { return []; }
})();
return (
<Card key={s.id} className={active ? 'border-primary/50' : ''}>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
{/* 左侧信息 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
<span className="font-medium text-sm">{s.name}</span>
{/* 类型 Badge */}
<Badge variant="outline" className="text-xs">
{sType === 'onetime' && <Clock className="w-3 h-3 mr-1" />}
{sType === 'daily' && <Repeat className="w-3 h-3 mr-1" />}
{sType === 'weekly' && <Calendar className="w-3 h-3 mr-1" />}
{SILENCE_TYPE_LABELS[sType] || sType}
</Badge>
{/* 活跃状态 */}
{active && (
<Badge className="bg-green-500 text-xs">生效中</Badge>
)}
{!enabled && (
<Badge variant="secondary" className="text-xs">已禁用</Badge>
)}
</div>
{s.description && (
<p className="text-xs text-muted-foreground mb-1.5">{s.description}</p>
)}
{/* 时间窗口 */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span>
{sType === 'onetime'
? `${formatTime(s.start_time)} — ${formatTime(s.end_time)}`
: `每日 ${formatTime(s.start_time)} — ${formatTime(s.end_time)}`
}
</span>
{sType === 'weekly' && weekdayList.length > 0 && (
<span className="flex items-center gap-1">
{weekdayList.sort((a,b)=>a-b).map(d => (
<span key={d} className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-[10px]">
{WEEKDAY_LABELS[d]}
</span>
))}
</span>
)}
</div>
{/* 创建时间 */}
<div className="text-[10px] text-muted-foreground/60 mt-1">
创建于 {formatTime(s.created_at)}
</div>
</div>
{/* 右侧操作 */}
<div className="flex items-center gap-2 shrink-0">
<Switch
checked={enabled}
onCheckedChange={() => handleToggle(s)}
/>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => handleEdit(s)}>
<Edit className="w-3 h-3 mr-1" />编辑
</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs text-red-400" onClick={() => handleDelete(s)}>
<Trash2 className="w-3 h-3 mr-1" />删除
</Button>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
{/* 创建/编辑弹框 */}
<SilenceDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
editingSilence={editing}
onSaved={fetchSilences}
/>
</div>
);
};
export default SilenceRuleTab;
@@ -4,6 +4,7 @@ import { Server, Loader2, Activity, Cpu, RefreshCw, AlertTriangle, Container, Za
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { StatStrip } from '@/components/ui/stat-strip';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { http } from '@/services/http';
@@ -47,12 +48,14 @@ const Box = ( p: any) => <svg xmlns="http://www.w3.org/2000/svg" width="16" heig
const K8sPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
<div className="space-y-4">
<div className="grid grid-cols-4 gap-3">
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-2xl font-bold text-purple-400">{d.podCount?.[0]?.value || 0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Pods</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-2xl font-bold text-blue-400">{d.deployCount?.[0]?.value || 0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Deployments</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-2xl font-bold text-emerald-400">{d.nodeCount?.[0]?.value || 0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Nodes</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]"><CardContent className="p-3 text-center"><div className="text-2xl font-bold text-amber-400">{d.nsPods?.length || 0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Namespaces</div></CardContent></Card>
</div>
<StatStrip
items={[
{ label: 'Pods', value: d.podCount?.[0]?.value || 0, color: 'purple' },
{ label: 'Deployments', value: d.deployCount?.[0]?.value || 0, color: 'blue' },
{ label: 'Nodes', value: d.nodeCount?.[0]?.value || 0, color: 'emerald' },
{ label: 'Namespaces', value: d.nsPods?.length || 0, color: 'amber' },
]}
/>
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3 max-h-60 overflow-y-auto">
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 text-purple-400" />节点 CPU / 内存利用率</div>
{(d.nodeCPU || []).map(( n: any, i: number) => {
@@ -93,10 +96,12 @@ const NodePanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
const PodPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
<div className="space-y-4">
<div className="grid grid-cols-4 gap-3">
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-xl font-bold text-emerald-400">{d.running?.[0]?.value||0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Running</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-xl font-bold text-amber-400">{d.pending?.[0]?.value||0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Pending</div></CardContent></Card>
</div>
<StatStrip
items={[
{ label: 'Running', value: d.running?.[0]?.value || 0, color: 'emerald' },
{ label: 'Pending', value: d.pending?.[0]?.value || 0, color: 'amber' },
]}
/>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">CPU Top 10</div>{(d.cpuTop||[]).map(( p: any,i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><MiniBar val={Number(p.value||0)*100} label={`${Number(p.value||0).toFixed(2)}c`} color="bg-purple-500" /></div>)}</div>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">内存 Top 10</div>{(d.memTop||[]).map(( p: any,i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><span className="ml-auto">{fmtBytes(Number(p.value||0))}</span></div>)}</div>
</div>
@@ -26,6 +26,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
import { StatStrip } from '@/components/ui/stat-strip';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription
} from '@/components/ui/dialog';
@@ -260,64 +261,14 @@ const TraceManager: React.FC = () => {
return (
<div className="space-y-6">
{/* 统计卡片 */}
<div className="grid grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">总请求数</p>
<p className="text-2xl font-bold">{services.reduce((sum, s) => sum + s.requestCount, 0).toLocaleString()}</p>
</div>
<div className="p-3 bg-blue-100 rounded-lg">
<Activity className="w-5 h-5 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">平均错误率</p>
<p className="text-2xl font-bold">
{(services.reduce((sum, s) => sum + s.errorRate, 0) / (services.length || 1)).toFixed(2)}%
</p>
</div>
<div className="p-3 bg-red-100 rounded-lg">
<AlertCircle className="w-5 h-5 text-red-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">平均延迟</p>
<p className="text-2xl font-bold">
{formatDuration(services.reduce((sum, s) => sum + s.avgDuration, 0) / (services.length || 1))}
</p>
</div>
<div className="p-3 bg-green-100 rounded-lg">
<Clock className="w-5 h-5 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">服务数量</p>
<p className="text-2xl font-bold">{services.length}</p>
</div>
<div className="p-3 bg-purple-100 rounded-lg">
<Layers className="w-5 h-5 text-purple-600" />
</div>
</div>
</CardContent>
</Card>
</div>
<StatStrip
items={[
{ label: '总请求', value: services.reduce((sum, s) => sum + s.requestCount, 0).toLocaleString(), color: 'blue' },
{ label: '错误率', value: `${(services.reduce((sum, s) => sum + s.errorRate, 0) / (services.length || 1)).toFixed(2)}%`, color: 'red' },
{ label: '平均延迟', value: formatDuration(services.reduce((sum, s) => sum + s.avgDuration, 0) / (services.length || 1)), color: 'amber' },
{ label: '服务数', value: services.length, color: 'purple' },
]}
/>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3 lg:w-[400px]">