// 告警中心主组件 — TanStack Query 编排 // Logan.Li import React, { useState, useEffect, useMemo } from 'react'; import { AlertTriangle, CheckCircle, Search, RefreshCw, Eye, Check, VolumeX, Plus, Rocket, } from 'lucide-react'; 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'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { alertCenterApi, alertApi } from '@/api'; import { useAlerts, useAgentAlertRules, useAlertMutations } from '@/hooks/api'; import { type Alert, type AlertRule, getSeverityBadge, getStatusBadge, getChannelTypeLabel, } from './alert-center-helpers'; 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 = () => { const _confirm = useConfirm(); const [searchQuery, setSearchQuery] = useState(''); const [severityFilter, setSeverityFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [activeTab, setActiveTab] = useState('alerts'); const [selectedAlert, setSelectedAlert] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [silenceOpen, setSilenceOpen] = useState(false); // 规则弹框 const [ruleDialogOpen, setRuleDialogOpen] = useState(false); const [editingRule, setEditingRule] = useState(null); // 联系人 & 联系组 const [contacts, setContacts] = useState([]); const [contactGroups, setContactGroups] = useState([]); const [contactDialogOpen, setContactDialogOpen] = useState(false); const [editingContact, setEditingContact] = useState(null); const [groupDialogOpen, setGroupDialogOpen] = useState(false); const [editingGroup, setEditingGroup] = useState(null); // 分页 const [page, setPage] = useState(1); const [pageSize] = useState(10); const alertParams = useMemo(() => ({ page, page_size: pageSize, severity: severityFilter, status: statusFilter, }), [page, pageSize, severityFilter, statusFilter]); const { data: alertData, isLoading: loading, refetch: refetchAlerts } = useAlerts(alertParams); const { data: rules = [], refetch: refetchRules } = useAgentAlertRules(); const { ackMutation, resolveMutation, quickSetupMutation, toggleAgentRuleMutation, deleteAgentRuleMutation, } = useAlertMutations(); const alerts = alertData?.list ?? []; const total = alertData?.total ?? 0; const stats = alertData?.stats ?? { firing: 0, critical: 0, warning: 0, resolved: 0 }; const totalPages = Math.max(1, Math.ceil(total / pageSize)); useEffect(() => { if (activeTab === 'contacts') fetchContacts(); if (activeTab === 'groups') { fetchContacts(); fetchContactGroups(); } }, [activeTab]); const fetchContacts = async () => { try { const res: Record = await alertApi.getContacts(); setContacts((res?.data?.list || res?.list || []) as Contact[]); } catch { /* silent */ } }; const fetchContactGroups = async () => { try { const res: Record = await alertApi.getContactGroups(); setContactGroups((res?.data?.list || res?.list || []) as ContactGroup[]); } catch { /* silent */ } }; const handleAcknowledge = async (id: string) => { try { await ackMutation.mutateAsync(id); toast.success('告警已确认'); } catch { toast.error('操作失败'); } }; const handleResolve = async (id: string) => { try { await resolveMutation.mutateAsync(id); toast.success('告警已解决'); } catch { toast.error('操作失败'); } }; const handleSilence = async (id: string, duration: string) => { try { 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); refetchAlerts(); } catch { toast.error('操作失败'); } }; const toggleRule = async (ruleId: string, enabled: boolean) => { try { await toggleAgentRuleMutation.mutateAsync({ id: Number(ruleId), enabled }); toast.success('规则状态已更新'); } catch { toast.error('更新失败'); } }; const handleOpenAddRule = () => { setEditingRule(null); setRuleDialogOpen(true); }; const handleEditRule = (rule: AlertRule) => { setEditingRule(rule); setRuleDialogOpen(true); }; const handleRuleDialogClose = () => { setRuleDialogOpen(false); setEditingRule(null); }; const handleQuickSetup = async () => { const ok = await _confirm({ title: '一键报警', description: '为 Agent 在线主机创建 CPU(>80%)/内存(>80%)/磁盘(>90%)/TIME_WAIT(>15000)/CLOSE_WAIT(>200)/进程检测 6 条告警规则。', confirmText: '确认创建', }); if (!ok) return; try { const res = await quickSetupMutation.mutateAsync('ecs'); toast.success(typeof res === 'string' ? res : ((res as { message?: string })?.message || '默认告警规则创建成功')); } catch { toast.error('创建失败,请检查 Agent 主机是否在线'); } }; const handleDeleteRule = async (rule: AlertRule) => { const ok = await _confirm({ title: '删除规则', description: `确定删除规则「${rule.name}」?`, confirmText: '删除', variant: 'destructive' }); if (!ok) return; try { await deleteAgentRuleMutation.mutateAsync(Number(rule.id)); toast.success('已删除'); } catch { toast.error('删除失败'); } }; const handleFilterChange = (type: 'severity' | 'status', value: string) => { if (type === 'severity') setSeverityFilter(value); else setStatusFilter(value); setPage(1); // 过滤条件变化时重置到第一页 }; return (
{/* 统计 — 紧凑条 */} 0 ? 'red' : 'muted' }, { label: '严重', value: stats.critical, color: 'red' }, { label: '警告', value: stats.warning, color: 'amber' }, { label: '已解决', value: stats.resolved, color: 'emerald' }, ]} /> 告警列表 告警规则 静默规则 报警联系人 报警联系组 {/* 告警列表 Tab */}
setSearchQuery(e.target.value)} className="pl-8 h-8 text-xs" />
共 {total} 条
{/* 告警表格 — 紧凑 */}
{loading ? ( ) : alerts.length === 0 ? ( ) : ( 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) => ( )) )}
告警信息 级别 状态 来源 持续 操作
加载中...
暂无告警
{alert.title}
{alert.description}
{Object.entries(alert.labels).slice(0, 2).map(([k, v]) => ( {k}={v} ))}
{getSeverityBadge(alert.severity)} {getStatusBadge(alert.status)}
{alert.source}
{alert.service}
{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)}分`}
{alert.status === 'firing' && ( )} {(alert.status === 'firing' || alert.status === 'acknowledged') && ( )}
{/* 分页控件 */} {totalPages > 1 && (
第 {page}/{totalPages} 页,共 {total} 条
{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 ( ); })}
)}
{/* 告警规则 Tab */}
{rules.length === 0 ? (

暂无告警规则

点击「添加规则」或「一键报警」快速创建

) : (
{rules.map((rule) => (
{rule.name} {getSeverityBadge(rule.severity)} {rule.enabled ? '已启用' : '已禁用'}

{rule.description}

{rule.expression && ( {rule.expression} )} {rule.duration ? 持续 {rule.duration}秒 : null}
{(() => { let chs: string[] = []; try { const raw = rule.notify_channels; if (raw) chs = Array.isArray(raw) ? raw : JSON.parse(raw); } catch { /* ignore */ } if (!chs.length) return null; return (
{chs.map(ch => ( {ch} ))}
); })()}
toggleRule(String(rule.id), checked)} />
))}
)}
{/* 静默规则 Tab */} {/* 报警联系人 Tab */}

报警联系人

{contacts.length === 0 ? (
暂无报警联系人,点击「添加联系人」创建
) : (
{contacts.map((c) => (
{c.name}
{c.phone &&
📱 {c.phone}
} {c.email &&
📧 {c.email}
}
{c.channel_type && ( {getChannelTypeLabel(c.channel_type)} )} {c.webhook_url && ( {c.webhook_url} )}
))}
)}
{/* 报警联系组 Tab */}
{contactGroups.length === 0 ? (
暂无报警联系组,点击「添加联系组」创建
) : (
{contactGroups.map((g) => { let cids: number[] = []; try { cids = JSON.parse(g.contact_ids || '[]'); } catch { /* ignore */ } const memberNames = contacts .filter(c => cids.includes(c.id)) .map(c => c.name); return (
{g.group_name} {cids.length} 人
{g.notes &&

{g.notes}

}
{memberNames.map(name => ( {name} ))}
); })}
)}
{ handleRuleDialogClose(); refetchRules(); }} /> { fetchContactGroups(); }} />
); }; export default AlertCenter;