diff --git a/frontend/src/components/agent/AgentAlerts.tsx b/frontend/src/components/agent/AgentAlerts.tsx index 16e07bf..68d59f2 100644 --- a/frontend/src/components/agent/AgentAlerts.tsx +++ b/frontend/src/components/agent/AgentAlerts.tsx @@ -1,29 +1,13 @@ +// Agent 告警主组件:告警事件 Tab + 告警规则 Tab + 规则编辑弹窗 // Logan.Li 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, @@ -34,51 +18,15 @@ import { 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 []; -} - -// ============ 主组件 ============ +import { + METRIC_OPTIONS, + getMetricLabel, + asList, + severityBadge, +} from './agentAlertHelper'; +import { AgentRuleDialog } from './AgentRuleDialog'; export function AgentAlerts() { const confirm = useConfirm(); @@ -88,20 +36,9 @@ export function AgentAlerts() { 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); @@ -138,67 +75,16 @@ export function AgentAlerts() { }; // ============ 规则 CRUD ============ - const resetRuleForm = () => { - setRuleForm({ name: '', metric: 'cpu', operator: '>', threshold: '90', port: '', severity: 'warning', enabled: true, duration: '60' }); - setEditingRule(null); - }; - const openCreateRule = () => { - resetRuleForm(); + setEditingRule(null); 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; @@ -220,16 +106,6 @@ export function AgentAlerts() { } }; - const severityBadge = (sev: string) => { - const opt = SEVERITY_OPTIONS.find((s) => s.value === sev) || SEVERITY_OPTIONS[1]; - return ( - - - {opt.label} - - ); - }; - return (
{/* 页头 */} @@ -422,119 +298,12 @@ export function AgentAlerts() { {/* ========== 规则编辑弹窗 ========== */} - { 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 })} /> -
-
- - - - - -
-
+
); } diff --git a/frontend/src/components/agent/AgentRuleDialog.tsx b/frontend/src/components/agent/AgentRuleDialog.tsx new file mode 100644 index 0000000..f3a43fd --- /dev/null +++ b/frontend/src/components/agent/AgentRuleDialog.tsx @@ -0,0 +1,219 @@ +// Agent 告警规则编辑/新建弹窗(自包含表单状态,从 AgentAlerts.tsx 提取) // Logan.Li +import { useState, useEffect } from 'react'; +import { toast } from 'sonner'; +import { agentApi } from '@/services/api'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Loader2 } from 'lucide-react'; +import { METRIC_OPTIONS, OPERATOR_OPTIONS, SEVERITY_OPTIONS } from './agentAlertHelper'; + +interface Props { + open: boolean; + onOpenChange: (v: boolean) => void; + editingRule: any; + onSuccess: () => void; +} + +// 规则表单默认值 // Logan.Li +const DEFAULT_FORM = { + name: '', + metric: 'cpu', + operator: '>', + threshold: '90', + port: '', + severity: 'warning', + enabled: true, + duration: '60', +}; + +export function AgentRuleDialog({ open, onOpenChange, editingRule, onSuccess }: Props) { + const [ruleForm, setRuleForm] = useState({ ...DEFAULT_FORM }); + const [saving, setSaving] = useState(false); + + // 弹窗打开时填充表单:编辑回填 / 新建重置 // Logan.Li + useEffect(() => { + if (!open) return; + if (editingRule) { + setRuleForm({ + name: editingRule.name || '', + metric: editingRule.metric || 'cpu', + operator: editingRule.operator || '>', + threshold: String(editingRule.threshold ?? '90'), + port: editingRule.port ? String(editingRule.port) : '', + severity: editingRule.severity || 'warning', + enabled: editingRule.enabled !== false, + duration: editingRule.duration ? String(editingRule.duration) : '60', + }); + } else { + setRuleForm({ ...DEFAULT_FORM }); + } + }, [open, editingRule]); + + const handleSave = async () => { + if (!ruleForm.name.trim()) { + toast.error('请输入规则名称'); + return; + } + setSaving(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('规则已创建'); + } + onOpenChange(false); + onSuccess(); + } catch (err: any) { + toast.error(err?.message || '保存失败'); + } finally { + setSaving(false); + } + }; + + return ( + { if (!v) onOpenChange(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 })} /> +
+
+ + + + + +
+
+ ); +} diff --git a/frontend/src/components/agent/agentAlertHelper.tsx b/frontend/src/components/agent/agentAlertHelper.tsx new file mode 100644 index 0000000..b6c21f1 --- /dev/null +++ b/frontend/src/components/agent/agentAlertHelper.tsx @@ -0,0 +1,54 @@ +// Agent 告警常量与辅助函数(从 AgentAlerts.tsx 提取) // Logan.Li +import { Badge } from '@/components/ui/badge'; +import { AlertTriangle, Cpu, MemoryStick, HardDrive, WifiOff, Network } from 'lucide-react'; + +// 监控指标选项(含图标,告警事件/规则/编辑弹窗共用) // Logan.Li +export 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 }, +]; + +// 比较运算符选项 // Logan.Li +export const OPERATOR_OPTIONS = [ + { value: '>', label: '> 大于' }, + { value: '<', label: '< 小于' }, + { value: '>=', label: '≥ 大于等于' }, + { value: '<=', label: '≤ 小于等于' }, + { value: '==', label: '= 等于' }, +]; + +// 严重程度选项(含徽章配色) // Logan.Li +export 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' }, +]; + +// 获取指标中文标签 // Logan.Li +export function getMetricLabel(metric: string) { + return METRIC_OPTIONS.find((m) => m.value === metric)?.label || metric; +} + +// 兼容多种列表响应格式(数组 / {list} / {items} / {data}) // Logan.Li +export 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 []; +} + +// 严重程度徽章(告警事件 + 规则列表共用) // Logan.Li +export function severityBadge(sev: string) { + const opt = SEVERITY_OPTIONS.find((s) => s.value === sev) || SEVERITY_OPTIONS[1]; + return ( + + + {opt.label} + + ); +}