Files
aiops/frontend/src/components/monitor/alert-center/SilenceRuleTab.tsx
T
Logan.Li d19a03d427 refactor(frontend): services/api 迁至 src/api,http 迁至 utils/request
对齐 Vue 后台惯例:
- src/api/          域 API(原 services/api)
- src/utils/request.ts  HTTP 封装(原 services/http.ts)
- src/utils/export.ts  客户端导出工具
- src/utils/index.ts   工具统一出口

全局导入改为 @/api 与 @/utils/request,tsc + build 通过。
2026-06-26 00:09:07 +08:00

227 lines
8.5 KiB
TypeScript

// 告警静默规则管理 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 '@/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;