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