diff --git a/frontend/src/components/k8s/TopologyView.tsx b/frontend/src/components/k8s/TopologyView.tsx index 9a6da2e..4fb34b6 100644 --- a/frontend/src/components/k8s/TopologyView.tsx +++ b/frontend/src/components/k8s/TopologyView.tsx @@ -46,7 +46,7 @@ export function TopologyView() { // 编辑节点 Dialog const [editDialogOpen, setEditDialogOpen] = useState(false); const [editingNode, setEditingNode] = useState(null); - const [editForm, setEditForm] = useState({ name: '', type: 'database', note: '' }); + const [editForm, setEditForm] = useState<{ name: string; type: TopologyNode['type']; note: string }>({ name: '', type: 'database', note: '' }); const loadClusters = useCallback(async () => { try { @@ -552,7 +552,7 @@ export function TopologyView() {
- setEditForm({ ...editForm, type: v as TopologyNode['type'] })}> {CUSTOM_NODE_TYPES.map((t) => {t.label} — {t.desc})} diff --git a/frontend/src/components/monitor/alert-center/AlertCenter.tsx b/frontend/src/components/monitor/alert-center/AlertCenter.tsx index 468af5d..79f1b96 100644 --- a/frontend/src/components/monitor/alert-center/AlertCenter.tsx +++ b/frontend/src/components/monitor/alert-center/AlertCenter.tsx @@ -1,98 +1,38 @@ +// 告警中心主组件 // Logan.Li import React, { useState, useEffect } from 'react'; import { - Bell, AlertTriangle, AlertCircle, CheckCircle, XCircle, Clock, Filter, - Search, RefreshCw, Settings, Eye, Check, X, Volume2, VolumeX, Mail, - MessageSquare, Phone, User, Server, TrendingUp, TrendingDown, BarChart3, - Calendar, Download, MoreHorizontal, ChevronDown, ChevronUp, Zap, Shield, - Activity, Layers, Info, Plus, ChevronRight + AlertTriangle, AlertCircle, CheckCircle, Zap, Search, RefreshCw, + Eye, Check, VolumeX, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Card, CardContent } from '@/components/ui/card'; import { - Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter -} from '@/components/ui/dialog'; -import { - Select, SelectContent, SelectItem, SelectTrigger, SelectValue + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; -import { Separator } from '@/components/ui/separator'; -import { ScrollArea } from '@/components/ui/scroll-area'; import { toast } from 'sonner'; -import { useConfirm } from '@/components/ui/confirm-dialog'; -import { alertCenterApi, alertApi } from '@/services/api'; +import { alertCenterApi } from '@/services/api'; +import { + type Alert, type AlertRule, mapAlert, getSeverityBadge, getStatusBadge, +} from './alert-center-helpers'; import { AlertDetailDialog, AlertSilenceDialog } from './AlertCenterDetail'; - -export interface Alert { - id: string; - title: string; - description: string; - severity: 'critical' | 'warning' | 'info'; - status: 'firing' | 'resolved' | 'acknowledged' | 'suppressed'; - source: string; - service: string; - metric: string; - value: number; - threshold: number; - startedAt: string; - resolvedAt?: string; - acknowledgedBy?: string; - acknowledgedAt?: string; - runbookUrl?: string; - labels: Record; - annotations: Record; -} - -// Logan.Li — 字段名与后端 alert.go AlertRule 对齐 -interface AlertRule { - id: number; - name: string; - description: string; - enabled: boolean; - severity: string; - type?: string; - source?: string; - expression?: string; - duration?: number; - threshold?: number; - labels?: string; - annotations?: string; - notify_channels?: string; - created_at?: string; - updated_at?: string; -} - -interface NotificationChannel { - id: number; - name: string; - type: string; - enabled: boolean; - config: string; - test_status?: string; -} +import { AlertChannelsTab } from './AlertChannelsTab'; const AlertCenter: React.FC = () => { - const _confirm = useConfirm(); const [alerts, setAlerts] = useState([]); const [rules, setRules] = useState([]); - const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); - const [severityFilter, setSeverityFilter] = useState('all'); - const [statusFilter, setStatusFilter] = useState('all'); + 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 [stats, setStats] = useState({ - firing: 0, - critical: 0, - warning: 0, - resolved: 0 - }); + const [stats, setStats] = useState({ firing: 0, critical: 0, warning: 0, resolved: 0 }); useEffect(() => { fetchData(); @@ -101,39 +41,24 @@ const AlertCenter: React.FC = () => { }, []); const fetchData = async () => { - await Promise.all([fetchAlerts(), fetchRules(), fetchChannels()]); + await Promise.all([fetchAlerts(), fetchRules()]); }; + // Logan.Li — 获取告警列表 const fetchAlerts = async () => { setLoading(true); try { const res = await alertCenterApi.getAlerts(); const raw = (res.list || []) as any[]; - // Logan.Li — 后端字段映射到前端 Alert 接口 - const data: Alert[] = raw.map(a => ({ - id: String(a.id), - title: a.rule_name || a.title || `告警 #${a.id}`, - description: a.message || a.description || '', - severity: a.severity || 'info', - status: a.status || 'firing', - source: a.target || a.source || '', - service: a.service || '', - metric: a.metric || '', - value: a.value || 0, - threshold: a.threshold || 0, - startedAt: a.started_at || a.startedAt || a.created_at || new Date().toISOString(), - resolvedAt: a.resolved_at || a.resolvedAt, - labels: typeof a.labels === 'string' ? (() => { try { return JSON.parse(a.labels || '{}'); } catch { return {}; } })() : (a.labels || {}), - annotations: typeof a.annotations === 'string' ? (() => { try { return JSON.parse(a.annotations || '{}'); } catch { return {}; } })() : (a.annotations || {}), - })); + const data: Alert[] = raw.map(mapAlert); setAlerts(data); setStats({ - firing: data.filter((a: Alert) => a.status === 'firing').length, - critical: data.filter((a: Alert) => a.severity === 'critical' && a.status === 'firing').length, - warning: data.filter((a: Alert) => a.severity === 'warning' && a.status === 'firing').length, - resolved: data.filter((a: Alert) => a.status === 'resolved').length + firing: data.filter(a => a.status === 'firing').length, + critical: data.filter(a => a.severity === 'critical' && a.status === 'firing').length, + warning: data.filter(a => a.severity === 'warning' && a.status === 'firing').length, + resolved: data.filter(a => a.status === 'resolved').length, }); - } catch (error) { + } catch { toast.error('获取告警失败'); } finally { setLoading(false); @@ -144,27 +69,17 @@ const AlertCenter: React.FC = () => { try { const res = await alertCenterApi.getRules(); setRules(res.list || []); - } catch (error) { + } catch { toast.error('获取告警规则失败'); } }; - const fetchChannels = async () => { - try { - const res = await alertApi.getNotifyChannels(); - const list = (res as any)?.list || []; - setChannels(list); - } catch (error) { - toast.error('获取通知渠道失败'); - } - }; - const handleAcknowledge = async (id: string) => { try { await alertCenterApi.acknowledge(id); toast.success('告警已确认'); fetchAlerts(); - } catch (error) { + } catch { toast.error('操作失败'); } }; @@ -174,7 +89,7 @@ const AlertCenter: React.FC = () => { await alertCenterApi.resolve(id); toast.success('告警已解决'); fetchAlerts(); - } catch (error) { + } catch { toast.error('操作失败'); } }; @@ -185,7 +100,7 @@ const AlertCenter: React.FC = () => { toast.success('告警已静默'); setSilenceOpen(false); fetchAlerts(); - } catch (error) { + } catch { toast.error('操作失败'); } }; @@ -195,128 +110,11 @@ const AlertCenter: React.FC = () => { await alertCenterApi.toggleRule(ruleId); setRules(rules.map(r => String(r.id) === ruleId ? { ...r, enabled } : r)); toast.success('规则状态已更新'); - } catch (error) { + } catch { toast.error('更新失败'); } }; - // 通知渠道管理 - const [channelDialogOpen, setChannelDialogOpen] = useState(false); - const [editingChannel, setEditingChannel] = useState(null); - const [channelForm, setChannelForm] = useState({ name: '', type: 'dingtalk', config: '' }); - const [channelSaving, setChannelSaving] = useState(false); - - const openCreateChannel = () => { - setEditingChannel(null); - setChannelForm({ name: '', type: 'dingtalk', config: '' }); - setChannelDialogOpen(true); - }; - - const openEditChannel = (ch: NotificationChannel) => { - setEditingChannel(ch); - setChannelForm({ name: ch.name, type: ch.type, config: ch.config || '' }); - setChannelDialogOpen(true); - }; - - const handleSaveChannel = async () => { - if (!channelForm.name.trim()) return; - setChannelSaving(true); - try { - const payload = { name: channelForm.name.trim(), type: channelForm.type, config: channelForm.config.trim() }; - if (editingChannel) { - await alertApi.updateNotifyChannel(editingChannel.id, payload); - toast.success('渠道已更新'); - } else { - await alertApi.createNotifyChannel(payload); - toast.success('渠道已添加'); - } - setChannelDialogOpen(false); - fetchChannels(); - } catch (error) { - toast.error('保存失败'); - } finally { - setChannelSaving(false); - } - }; - - const handleDeleteChannel = async (id: number) => { - { const ok = await _confirm({ title: '删除通知渠道', description: '确定删除此通知渠道?', confirmText: '删除', variant: 'destructive' }); if (!ok) return; } - try { - await alertApi.deleteNotifyChannel(id); - toast.success('渠道已删除'); - fetchChannels(); - } catch (error) { - toast.error('删除失败'); - } - }; - - const handleTestChannel = async (id: number) => { - try { - const res = await alertApi.testNotifyChannel(id); - const status = (res as any)?.status; - if (status === 'success') { - toast.success('测试通知已发送'); - } else { - toast.warning('渠道未配置参数,请先完善配置'); - } - fetchChannels(); - } catch (error) { - toast.error('测试失败'); - } - }; - - const handleToggleChannel = async (ch: NotificationChannel) => { - try { - await alertApi.updateNotifyChannel(ch.id, { enabled: !ch.enabled }); - fetchChannels(); - } catch (error) { - toast.error('操作失败'); - } - }; - - const getChannelTypeLabel = (type: string) => { - const map: Record = { dingtalk: '钉钉', wechat: '企业微信', email: '邮件', sms: '短信', webhook: 'Webhook' }; - return map[type] || type; - }; - - const getSeverityBadge = (severity: string) => { - switch (severity) { - case 'critical': - return 严重; - case 'warning': - return 警告; - case 'info': - return 信息; - default: - return 未知; - } - }; - - const getStatusBadge = (status: string) => { - switch (status) { - case 'firing': - return 触发中; - case 'acknowledged': - return 已确认; - case 'resolved': - return 已解决; - case 'suppressed': - return 已静默; - default: - return 未知; - } - }; - - const getChannelIcon = (type: string) => { - switch (type) { - case 'email': return ; - case 'slack': return ; - case 'sms': return ; - case 'webhook': return ; - default: return ; - } - }; - const filteredAlerts = alerts.filter(alert => { const matchSearch = alert.title.toLowerCase().includes(searchQuery.toLowerCase()) || alert.description.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -391,8 +189,8 @@ const AlertCenter: React.FC = () => { 通知渠道 + {/* 告警列表 Tab */} - {/* 过滤栏 */}
@@ -427,12 +225,10 @@ const AlertCenter: React.FC = () => {
-
- -
+
{/* 告警列表 */} @@ -452,13 +248,9 @@ const AlertCenter: React.FC = () => { {loading ? ( - - 加载中... - + 加载中... ) : filteredAlerts.length === 0 ? ( - - 暂无告警 - + 暂无告警 ) : ( filteredAlerts.map((alert) => ( @@ -480,41 +272,28 @@ const AlertCenter: React.FC = () => { {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)}分钟` - } + : `${Math.floor((Date.now() - new Date(alert.startedAt).getTime()) / 60000)}分钟`}
- {alert.status === 'firing' && ( - )} {(alert.status === 'firing' || alert.status === 'acknowledged') && ( - )} -
@@ -529,14 +308,13 @@ const AlertCenter: React.FC = () => { + {/* 告警规则 Tab */}
-
- -
+
{rules.map((rule) => ( @@ -559,15 +337,15 @@ const AlertCenter: React.FC = () => { {rule.duration ? 持续 {rule.duration}秒 : null}
{(() => { - let channels: string[] = []; + let chs: string[] = []; try { const raw = rule.notify_channels; - if (raw) channels = Array.isArray(raw) ? raw : JSON.parse(raw); + if (raw) chs = Array.isArray(raw) ? raw : JSON.parse(raw); } catch { /* ignore */ } - if (!channels.length) return null; + if (!chs.length) return null; return (
- {channels.map(ch => ( + {chs.map(ch => ( {ch} ))}
@@ -587,63 +365,9 @@ const AlertCenter: React.FC = () => {
- -
-
- -
- -
- {channels.length === 0 ? ( -
- 暂无通知渠道,点击「添加渠道」创建 -
- ) : ( -
- {channels.map((channel) => ( - - openEditChannel(channel)}> -
-
- {getChannelIcon(channel.type)} - {channel.name} -
- - {channel.enabled ? '已启用' : '已禁用'} - -
-
- -
- 类型: {getChannelTypeLabel(channel.type)} - {channel.test_status === 'success' && ( - 测试通过 - )} -
-
- - - -
-
-
- ))} -
- )} + {/* 通知渠道 Tab — 自包含子组件 */} + + @@ -659,63 +383,6 @@ const AlertCenter: React.FC = () => { onOpenChange={setSilenceOpen} onSilence={handleSilence} /> - - {/* 通知渠道创建/编辑对话框 */} - - - - {editingChannel ? '编辑通知渠道' : '添加通知渠道'} - 配置告警通知渠道(钉钉/企业微信/邮件/短信/Webhook) - -
-
- - setChannelForm({ ...channelForm, name: e.target.value })} - placeholder="如:运维告警群" - /> -
-
- - -
-
- - setChannelForm({ ...channelForm, config: e.target.value })} - placeholder='{"webhook":"https://...","secret":"..."}' - className="font-mono text-sm" - /> -

- {channelForm.type === 'dingtalk' && '钉钉机器人: webhook URL + secret'} - {channelForm.type === 'wechat' && '企业微信: webhook URL'} - {channelForm.type === 'email' && '邮件: {"to":"ops@ezops.io"}'} - {channelForm.type === 'sms' && '短信: {"phones":"13800138000"}'} - {channelForm.type === 'webhook' && 'Webhook: {"url":"https://..."}'} -

-
-
- - - - -
-
); }; diff --git a/frontend/src/components/monitor/alert-center/AlertCenterDetail.tsx b/frontend/src/components/monitor/alert-center/AlertCenterDetail.tsx index 946b9fc..6141245 100644 --- a/frontend/src/components/monitor/alert-center/AlertCenterDetail.tsx +++ b/frontend/src/components/monitor/alert-center/AlertCenterDetail.tsx @@ -1,26 +1,11 @@ -import { - Check, ChevronRight, AlertCircle, AlertTriangle, Info, -} from 'lucide-react'; +import { Check, ChevronRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog'; -import type { Alert } from './AlertCenter'; - -function getSeverityBadge(severity: string) { - switch (severity) { - case 'critical': - return 严重; - case 'warning': - return 警告; - case 'info': - return 信息; - default: - return 未知; - } -} +import { type Alert, getSeverityBadge } from './alert-center-helpers'; // Logan.Li — 统一从 helpers 导入 export function AlertDetailDialog({ alert, diff --git a/frontend/src/components/monitor/alert-center/AlertChannelDialog.tsx b/frontend/src/components/monitor/alert-center/AlertChannelDialog.tsx new file mode 100644 index 0000000..794f79a --- /dev/null +++ b/frontend/src/components/monitor/alert-center/AlertChannelDialog.tsx @@ -0,0 +1,121 @@ +// 通知渠道创建/编辑对话框 // Logan.Li +import React, { useState, useEffect } from 'react'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/components/ui/select'; +import { toast } from 'sonner'; +import { alertApi } from '@/services/api'; +import type { NotificationChannel } from './alert-center-helpers'; + +const CHANNEL_TYPES = [ + { value: 'dingtalk', label: '钉钉', hint: '钉钉机器人: webhook URL + secret' }, + { value: 'wechat', label: '企业微信', hint: '企业微信: webhook URL' }, + { value: 'email', label: '邮件', hint: '邮件: {"to":"ops@ezops.io"}' }, + { value: 'sms', label: '短信', hint: '短信: {"phones":"13800138000"}' }, + { value: 'webhook', label: 'Webhook', hint: 'Webhook: {"url":"https://..."}' }, +]; + +export function AlertChannelDialog({ + open, + onOpenChange, + editingChannel, + onSaved, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + editingChannel: NotificationChannel | null; + onSaved: () => void; +}) { + const [form, setForm] = useState({ name: '', type: 'dingtalk', config: '' }); + const [saving, setSaving] = useState(false); + + // Logan.Li — 对话框打开时同步编辑数据 + useEffect(() => { + if (open) { + if (editingChannel) { + setForm({ name: editingChannel.name, type: editingChannel.type, config: editingChannel.config || '' }); + } else { + setForm({ name: '', type: 'dingtalk', config: '' }); + } + } + }, [open, editingChannel]); + + const handleSave = async () => { + if (!form.name.trim()) return; + setSaving(true); + try { + const payload = { name: form.name.trim(), type: form.type, config: form.config.trim() }; + if (editingChannel) { + await alertApi.updateNotifyChannel(editingChannel.id, payload); + toast.success('渠道已更新'); + } else { + await alertApi.createNotifyChannel(payload); + toast.success('渠道已添加'); + } + onOpenChange(false); + onSaved(); + } catch { + toast.error('保存失败'); + } finally { + setSaving(false); + } + }; + + const currentHint = CHANNEL_TYPES.find(t => t.value === form.type)?.hint || ''; + + return ( + + + + {editingChannel ? '编辑通知渠道' : '添加通知渠道'} + 配置告警通知渠道(钉钉/企业微信/邮件/短信/Webhook) + +
+
+ + setForm({ ...form, name: e.target.value })} + placeholder="如:运维告警群" + /> +
+
+ + +
+
+ + setForm({ ...form, config: e.target.value })} + placeholder='{"webhook":"https://...","secret":"..."}' + className="font-mono text-sm" + /> +

{currentHint}

+
+
+ + + + +
+
+ ); +} diff --git a/frontend/src/components/monitor/alert-center/AlertChannelsTab.tsx b/frontend/src/components/monitor/alert-center/AlertChannelsTab.tsx new file mode 100644 index 0000000..462410d --- /dev/null +++ b/frontend/src/components/monitor/alert-center/AlertChannelsTab.tsx @@ -0,0 +1,152 @@ +// 通知渠道管理 Tab(自包含:状态+API+渲染) // Logan.Li +import React, { useState, useCallback, useEffect } from 'react'; +import { Plus, RefreshCw, Zap, Settings, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { toast } from 'sonner'; +import { useConfirm } from '@/components/ui/confirm-dialog'; +import { alertApi } from '@/services/api'; +import { + type NotificationChannel, getChannelIcon, getChannelTypeLabel, +} from './alert-center-helpers'; +import { AlertChannelDialog } from './AlertChannelDialog'; + +export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey }) => { + const _confirm = useConfirm(); + const [channels, setChannels] = useState([]); + const [loading, setLoading] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingChannel, setEditingChannel] = useState(null); + + // Logan.Li — 获取渠道列表 + const fetchChannels = useCallback(async () => { + setLoading(true); + try { + const res = await alertApi.getNotifyChannels(); + const list = (res as any)?.list || []; + setChannels(list); + } catch { + toast.error('获取通知渠道失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { fetchChannels(); }, [fetchChannels, refreshKey]); + + const openCreate = () => { + setEditingChannel(null); + setDialogOpen(true); + }; + + const openEdit = (ch: NotificationChannel) => { + setEditingChannel(ch); + setDialogOpen(true); + }; + + const handleDelete = async (id: number) => { + const ok = await _confirm({ title: '删除通知渠道', description: '确定删除此通知渠道?', confirmText: '删除', variant: 'destructive' }); + if (!ok) return; + try { + await alertApi.deleteNotifyChannel(id); + toast.success('渠道已删除'); + fetchChannels(); + } catch { + toast.error('删除失败'); + } + }; + + const handleTest = async (id: number) => { + try { + const res = await alertApi.testNotifyChannel(id); + const status = (res as any)?.status; + if (status === 'success') { + toast.success('测试通知已发送'); + } else { + toast.warning('渠道未配置参数,请先完善配置'); + } + fetchChannels(); + } catch { + toast.error('测试失败'); + } + }; + + const handleToggle = async (ch: NotificationChannel) => { + try { + await alertApi.updateNotifyChannel(ch.id, { enabled: !ch.enabled }); + fetchChannels(); + } catch { + toast.error('操作失败'); + } + }; + + return ( +
+
+ + +
+ + {loading ? ( +
加载中...
+ ) : channels.length === 0 ? ( +
+ 暂无通知渠道,点击「添加渠道」创建 +
+ ) : ( +
+ {channels.map((channel) => ( + + openEdit(channel)}> +
+
+ {getChannelIcon(channel.type)} + {channel.name} +
+ + {channel.enabled ? '已启用' : '已禁用'} + +
+
+ +
+ 类型: {getChannelTypeLabel(channel.type)} + {channel.test_status === 'success' && ( + 测试通过 + )} +
+
+ + + +
+
+
+ ))} +
+ )} + + +
+ ); +}; diff --git a/frontend/src/components/monitor/alert-center/alert-center-helpers.tsx b/frontend/src/components/monitor/alert-center/alert-center-helpers.tsx new file mode 100644 index 0000000..a1932b6 --- /dev/null +++ b/frontend/src/components/monitor/alert-center/alert-center-helpers.tsx @@ -0,0 +1,127 @@ +// 告警中心共享类型与辅助函数 // Logan.Li +import React from 'react'; +import { + AlertCircle, AlertTriangle, Info, Zap, User, CheckCircle, VolumeX, + Mail, MessageSquare, Phone, Bell, +} from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; + +/** 前端 Alert 接口 */ +export interface Alert { + id: string; + title: string; + description: string; + severity: 'critical' | 'warning' | 'info'; + status: 'firing' | 'resolved' | 'acknowledged' | 'suppressed'; + source: string; + service: string; + metric: string; + value: number; + threshold: number; + startedAt: string; + resolvedAt?: string; + acknowledgedBy?: string; + acknowledgedAt?: string; + runbookUrl?: string; + labels: Record; + annotations: Record; +} + +// Logan.Li — 字段名与后端 alert.go AlertRule 对齐 +export interface AlertRule { + id: number; + name: string; + description: string; + enabled: boolean; + severity: string; + type?: string; + source?: string; + expression?: string; + duration?: number; + threshold?: number; + labels?: string; + annotations?: string; + notify_channels?: string; + created_at?: string; + updated_at?: string; +} + +export interface NotificationChannel { + id: number; + name: string; + type: string; + enabled: boolean; + config: string; + test_status?: string; +} + +// Logan.Li — 后端字段映射到前端 Alert 接口 +export function mapAlert(a: any): Alert { + return { + id: String(a.id), + title: a.rule_name || a.title || `告警 #${a.id}`, + description: a.message || a.description || '', + severity: a.severity || 'info', + status: a.status || 'firing', + source: a.target || a.source || '', + service: a.service || '', + metric: a.metric || '', + value: a.value || 0, + threshold: a.threshold || 0, + startedAt: a.started_at || a.startedAt || a.created_at || new Date().toISOString(), + resolvedAt: a.resolved_at || a.resolvedAt, + labels: typeof a.labels === 'string' + ? (() => { try { return JSON.parse(a.labels || '{}'); } catch { return {}; } })() + : (a.labels || {}), + annotations: typeof a.annotations === 'string' + ? (() => { try { return JSON.parse(a.annotations || '{}'); } catch { return {}; } })() + : (a.annotations || {}), + }; +} + +/** 严重级别 Badge */ +export function getSeverityBadge(severity: string) { + switch (severity) { + case 'critical': + return 严重; + case 'warning': + return 警告; + case 'info': + return 信息; + default: + return 未知; + } +} + +/** 告警状态 Badge */ +export function getStatusBadge(status: string) { + switch (status) { + case 'firing': + return 触发中; + case 'acknowledged': + return 已确认; + case 'resolved': + return 已解决; + case 'suppressed': + return 已静默; + default: + return 未知; + } +} + +/** 渠道类型图标 */ +export function getChannelIcon(type: string) { + switch (type) { + case 'email': return ; + case 'slack': return ; + case 'sms': return ; + case 'webhook': return ; + default: return ; + } +} + +/** 渠道类型中文标签 */ +export function getChannelTypeLabel(type: string) { + const map: Record = { dingtalk: '钉钉', wechat: '企业微信', email: '邮件', sms: '短信', webhook: 'Webhook' }; + return map[type] || type; +}