import { useState, useEffect, useCallback } from 'react'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { agentApi } from '@/services/api'; import { Card, CardContent } from '@/components/ui/card'; 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 { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { AlertTriangle, CheckCircle, Plus, Pencil, Trash2, Loader2, RefreshCw, Bell, Cpu, MemoryStick, HardDrive, WifiOff, Network, Clock, } from 'lucide-react'; // ============ 常量 ============ const METRIC_OPTIONS = [ { value: 'cpu', label: 'CPU', icon: Cpu }, { value: 'mem', label: '内存', icon: MemoryStick }, { value: 'disk', label: '磁盘', icon: HardDrive }, { value: 'offline', label: '主机离线', icon: WifiOff }, { value: 'port_down', label: '端口不通', icon: Network }, ]; const OPERATOR_OPTIONS = [ { value: '>', label: '> 大于' }, { value: '<', label: '< 小于' }, { value: '>=', label: '≥ 大于等于' }, { value: '<=', label: '≤ 小于等于' }, { value: '==', label: '= 等于' }, ]; const SEVERITY_OPTIONS = [ { value: 'critical', label: '严重', color: 'bg-red-500/15 text-red-400 border-red-500/30' }, { value: 'warning', label: '警告', color: 'bg-amber-500/15 text-amber-400 border-amber-500/30' }, { value: 'info', label: '提示', color: 'bg-blue-500/15 text-blue-400 border-blue-500/30' }, ]; function getMetricLabel(metric: string) { return METRIC_OPTIONS.find((m) => m.value === metric)?.label || metric; } function asList(data: any): any[] { if (!data) return []; if (Array.isArray(data)) return data; if (Array.isArray(data.list)) return data.list; if (Array.isArray(data.items)) return data.items; if (Array.isArray(data.data)) return data.data; return []; } // ============ 主组件 ============ export function AgentAlerts() { const confirm = useConfirm(); const [activeTab, setActiveTab] = useState('events'); const [alerts, setAlerts] = useState([]); const [rules, setRules] = useState([]); const [loading, setLoading] = useState(true); const [resolvedView, setResolvedView] = useState(false); // 规则编辑 const [ruleOpen, setRuleOpen] = useState(false); const [editingRule, setEditingRule] = useState(null); const [ruleSaving, setRuleSaving] = useState(false); const [ruleForm, setRuleForm] = useState({ name: '', metric: 'cpu', operator: '>', threshold: '90', port: '', severity: 'warning', enabled: true, duration: '60', }); const loadData = useCallback(async () => { setLoading(true); try { const [alertsRes, rulesRes] = await Promise.all([ agentApi.listAlerts({ resolved: String(resolvedView) }), agentApi.listRules(), ]); setAlerts(asList(alertsRes)); setRules(asList(rulesRes)); } catch (err) { console.error(err); toast.error('加载告警数据失败'); } finally { setLoading(false); } }, [resolvedView]); useEffect(() => { loadData(); }, [loadData]); // ============ 告警事件 ============ const handleResolve = async (id: number) => { const ok = await confirm({ title: '恢复告警', description: '确定将此告警标记为已恢复?', confirmText: '恢复' }); if (!ok) return; try { await agentApi.resolveAlert(id); toast.success('告警已恢复'); loadData(); } catch (err: any) { toast.error(err?.message || '操作失败'); } }; // ============ 规则 CRUD ============ const resetRuleForm = () => { setRuleForm({ name: '', metric: 'cpu', operator: '>', threshold: '90', port: '', severity: 'warning', enabled: true, duration: '60' }); setEditingRule(null); }; const openCreateRule = () => { resetRuleForm(); setRuleOpen(true); }; const openEditRule = (rule: any) => { setEditingRule(rule); setRuleForm({ name: rule.name || '', metric: rule.metric || 'cpu', operator: rule.operator || '>', threshold: String(rule.threshold ?? '90'), port: rule.port ? String(rule.port) : '', severity: rule.severity || 'warning', enabled: rule.enabled !== false, duration: rule.duration ? String(rule.duration) : '60', }); setRuleOpen(true); }; const handleSaveRule = async () => { if (!ruleForm.name.trim()) { toast.error('请输入规则名称'); return; } setRuleSaving(true); try { const payload: any = { name: ruleForm.name.trim(), metric: ruleForm.metric, operator: ruleForm.operator, threshold: Number(ruleForm.threshold) || 0, severity: ruleForm.severity, enabled: ruleForm.enabled, duration: Number(ruleForm.duration) || 0, }; if (ruleForm.metric === 'port_down' && ruleForm.port) { payload.port = Number(ruleForm.port); } if (editingRule) { await agentApi.updateRule(editingRule.id, payload); toast.success('规则已更新'); } else { await agentApi.createRule(payload); toast.success('规则已创建'); } setRuleOpen(false); resetRuleForm(); loadData(); } catch (err: any) { toast.error(err?.message || '保存失败'); } finally { setRuleSaving(false); } }; const handleDeleteRule = async (id: number) => { const ok = await confirm({ title: '删除规则', description: '确定删除此告警规则?', confirmText: '删除', variant: 'destructive' }); if (!ok) return; try { await agentApi.deleteRule(id); toast.success('规则已删除'); loadData(); } catch (err: any) { toast.error(err?.message || '删除失败'); } }; const handleToggleRule = async (rule: any) => { try { await agentApi.updateRule(rule.id, { ...rule, enabled: !rule.enabled }); loadData(); } catch (err: any) { toast.error(err?.message || '操作失败'); } }; const severityBadge = (sev: string) => { const opt = SEVERITY_OPTIONS.find((s) => s.value === sev) || SEVERITY_OPTIONS[1]; return ( {opt.label} ); }; return (
{/* 页头 */}

Agent 告警

基于 Agent 上报指标的告警规则与事件

告警事件 告警规则 {/* ========== 告警事件 ========== */}
{loading ? ( 加载中... ) : alerts.length === 0 ? (

{resolvedView ? '暂无已恢复的告警' : '当前无活动告警,一切正常'}

) : (
{alerts.map((alert) => (
{severityBadge(alert.severity)} {alert.rule_name || alert.name || alert.title || '告警'}

{alert.message || alert.description || '触发告警阈值'}

{alert.hostname || '--'} 指标: {getMetricLabel(alert.metric)} {alert.value != null && ( 当前值: {alert.value} )} {alert.created_at || alert.triggered_at ? new Date(alert.created_at || alert.triggered_at).toLocaleString() : '--'}
{!resolvedView && ( )}
))}
)}
{/* ========== 告警规则 ========== */}
{loading ? ( 加载中... ) : rules.length === 0 ? (

暂无告警规则

) : (
{rules.map((rule) => { const MetricIcon = METRIC_OPTIONS.find((m) => m.value === rule.metric)?.icon || Cpu; return (
{rule.name} {severityBadge(rule.severity)} {rule.enabled === false && ( 已禁用 )}
{getMetricLabel(rule.metric)} {rule.metric !== 'offline' && ( {rule.operator} {rule.threshold}{rule.metric === 'port_down' ? '' : '%'} )} {rule.metric === 'port_down' && rule.port ? ( 端口 {rule.port} ) : null} {rule.duration ? 持续 {rule.duration}s : null}
handleToggleRule(rule)} />
); })}
)}
{/* ========== 规则编辑弹窗 ========== */} { if (!v) setRuleOpen(false); }}> {editingRule ? '编辑规则' : '新建规则'} 配置 Agent 指标告警规则
setRuleForm({ ...ruleForm, name: e.target.value })} placeholder="例如:CPU 使用率告警" className="h-9 bg-slate-800 border-slate-700 text-slate-200" />
{ruleForm.metric !== 'offline' && (
setRuleForm({ ...ruleForm, threshold: e.target.value })} className="h-9 bg-slate-800 border-slate-700 text-slate-200" />
)} {ruleForm.metric === 'port_down' && (
setRuleForm({ ...ruleForm, port: e.target.value })} placeholder="例如:8080" className="h-9 bg-slate-800 border-slate-700 text-slate-200" />
)}
setRuleForm({ ...ruleForm, duration: e.target.value })} placeholder="指标持续超限多久后告警" className="h-9 bg-slate-800 border-slate-700 text-slate-200" />
setRuleForm({ ...ruleForm, enabled: v })} />
); } export default AgentAlerts;