// 告警中心主组件 — TanStack Query 编排 // Logan.Li import React, { useState, useEffect, useMemo } from 'react'; import { cn } from '@/lib/utils'; import { AlertTriangle, CheckCircle, Search, RefreshCw, Eye, Check, VolumeX, Plus, } 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 { toast } from 'sonner'; import { PageShell } from '@/components/layout/PageShell'; import { PageEmpty } from '@/components/layout/PageEmpty'; import { PageLoading } from '@/components/layout/PageLoading'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { alertCenterApi, alertApi } from '@/api'; import { useAlerts, useUnifiedAlertRules, useMetricCatalog, useAlertMutations } from '@/hooks/api'; import { type Alert, getSeverityBadge, getStatusBadge, getChannelTypeLabel, } from './alert-center-helpers'; import { AlertDetailDialog, AlertSilenceDialog } from './AlertCenterDetail'; import { MetricCatalogTab } from './MetricCatalogTab'; import { AlertRulesTab } from './AlertRulesTab'; import { ContactDialog } from './ContactDialog'; import { ContactGroupDialog } from './ContactGroupDialog'; import { NotifyPolicyTab } from './NotifyPolicyTab'; import type { UnifiedAlertRule } from '@/api'; import type { Contact, ContactGroup } from './alert-center-helpers'; import { useTranslation } from 'react-i18next'; const AlertCenter: React.FC = () => { const { t } = useTranslation(); const _confirm = useConfirm(); const [searchQuery, setSearchQuery] = useState(''); const [severityFilter, setSeverityFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [sourceFilter, setSourceFilter] = useState('all'); const [activeTab, setActiveTab] = useState('alerts'); const [selectedAlert, setSelectedAlert] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [silenceOpen, setSilenceOpen] = useState(false); // 联系人 & 联系组 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, source_type: sourceFilter, }), [page, pageSize, severityFilter, statusFilter, sourceFilter]); const { data: alertData, isLoading: loading, isFetching: fetchingAlerts, refetch: refetchAlerts } = useAlerts(alertParams); const { data: rules = [], isFetching: fetchingRules, refetch: refetchRules } = useUnifiedAlertRules('all'); const { data: catalog = [] } = useMetricCatalog(); const { ackMutation, resolveMutation, quickSetupMutation, monitorQuickSetupMutation, toggleAgentRuleMutation, deleteAgentRuleMutation, deleteMonitorRuleMutation, toggleMonitorRuleMutation, } = 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(() => { // eslint-disable-next-line react-hooks/immutability if (activeTab === 'contacts') fetchContacts(); // eslint-disable-next-line react-hooks/immutability if (activeTab === 'groups') { fetchContacts(); fetchContactGroups(); } }, [activeTab]); const fetchContacts = async () => { try { const res = await alertApi.getContacts() as { list?: Contact[] }; setContacts(res?.list || []); } catch { /* silent */ } }; const fetchContactGroups = async () => { try { const res = await alertApi.getContactGroups() as { list?: ContactGroup[] }; setContactGroups(res?.list || []); } catch { /* silent */ } }; const handleAcknowledge = async (id: string) => { try { await ackMutation.mutateAsync(id); toast.success(t('alert.acknowledged')); } catch { toast.error(t('alert.operationFailed')); } }; const handleResolve = async (id: string) => { try { await resolveMutation.mutateAsync(id); toast.success(t('alert.resolvedToast')); } catch { toast.error(t('alert.operationFailed')); } }; 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: `${t('monitor.alertCenterPanel.silencePrefix')} ${alert?.title || id} (${duration})`, description: `${t('alert.silenceDesc')} — ${alert?.source || ''}`, silence_type: 'onetime', start_time: now.toISOString(), end_time: endTime.toISOString(), }); toast.success(t('monitor.alertCenterPanel.silencedFor', { duration })); setSilenceOpen(false); refetchAlerts(); } catch { toast.error(t('alert.operationFailed')); } }; const handleQuickSetup = async () => { const ok = await _confirm({ title: t('alert.quickSetup'), description: t('alert.quickSetupDesc'), confirmText: t('alert.quickSetupConfirm'), }); if (!ok) return; try { const res = await quickSetupMutation.mutateAsync('ecs'); toast.success(typeof res === 'string' ? res : ((res as { message?: string })?.message || t('alert.quickSetupSuccess'))); } catch { toast.error(t('alert.quickSetupFailed')); } }; const handleDeleteAgentRule = async (rule: UnifiedAlertRule) => { const ok = await _confirm({ title: t('alert.deleteRule'), description: `${rule.name}`, confirmText: t('common.delete'), variant: 'destructive' }); if (!ok) return; try { await deleteAgentRuleMutation.mutateAsync(rule.id); toast.success(t('alert.deleted')); } catch { toast.error(t('alert.deleteFailed')); } }; const handleDeleteMonitorRule = async (rule: UnifiedAlertRule) => { const ok = await _confirm({ title: t('alert.deleteRule'), description: `${rule.name}`, confirmText: t('common.delete'), variant: 'destructive' }); if (!ok) return; try { await deleteMonitorRuleMutation.mutateAsync(rule.id); toast.success(t('alert.deleted')); } catch { toast.error(t('alert.deleteFailed')); } }; const handleFilterChange = (type: 'severity' | 'status' | 'source', value: string) => { if (type === 'severity') setSeverityFilter(value); else if (type === 'status') setStatusFilter(value); else setSourceFilter(value); setPage(1); }; const pageTabs = [ { id: 'alerts', label: t('alert.tab.alerts') }, { id: 'rules', label: t('alert.tab.rules') }, { id: 'metrics', label: t('monitor.alertCenterPanel.metricCatalog') }, { id: 'contacts', label: t('alert.tab.contacts') }, { id: 'groups', label: t('alert.tab.groups') }, { id: 'notify', label: t('monitor.alertCenterPanel.notifyPolicy') }, ]; const alertsToolbar = activeTab === 'alerts' ? (
setSearchQuery(e.target.value)} className="pl-8 h-8 text-xs" />
{t('alert.totalItems', { count: total })}
) : undefined; // eslint-disable-next-line react-hooks/purity const nowMs = Date.now(); return ( } tabs={pageTabs} activeTab={activeTab} onTabChange={setActiveTab} toolbar={alertsToolbar} metrics={( 0 ? 'red' : 'muted' }, { label: t('alert.stat.critical'), value: stats.critical, color: 'red' }, { label: t('alert.stat.warning'), value: stats.warning, color: 'amber' }, { label: t('alert.stat.resolved'), value: stats.resolved, color: 'emerald' }, ]} /> )} actions={activeTab === 'rules' ? ( ) : activeTab === 'contacts' ? ( ) : activeTab === 'groups' ? ( <> ) : undefined} > {activeTab === 'alerts' && (
{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) => ( )) )}
{t('alert.table.alertInfo')} {t('alert.table.severity')} {t('alert.table.status')} {t('alert.table.source')} {t('alert.table.duration')} {t('alert.table.actions')}
{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 ? t('alert.minutesShort', { count: Math.floor((new Date(alert.resolvedAt).getTime() - new Date(alert.startedAt).getTime()) / 60000) }) : t('alert.minutesShort', { count: Math.floor((nowMs - new Date(alert.startedAt).getTime()) / 60000) })}
{alert.status === 'firing' && ( )} {(alert.status === 'firing' || alert.status === 'acknowledged') && ( )}
{/* 分页控件 */} {totalPages > 1 && (
{t('alert.pageInfo', { 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 ( ); })}
)}
)} {activeTab === 'rules' && ( refetchRules()} onToggleAgent={(id, enabled) => toggleAgentRuleMutation.mutate({ id, enabled })} onDeleteAgent={handleDeleteAgentRule} onDeleteMonitor={handleDeleteMonitorRule} onToggleMonitor={(id, enabled) => toggleMonitorRuleMutation.mutate({ id, enabled })} onQuickSetupHost={handleQuickSetup} onQuickSetupMonitor={(cat) => monitorQuickSetupMutation.mutate(cat)} quickSetupPending={quickSetupMutation.isPending || monitorQuickSetupMutation.isPending} /> )} {activeTab === 'metrics' && } {activeTab === 'contacts' && ( 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} )}
))}
) )} {activeTab === 'notify' && } {activeTab === 'groups' && ( 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} {t('alert.peopleCount', { count: cids.length })}
{g.notes &&

{g.notes}

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