refactor: 标准化项目结构 frontend/ + backend/ + SQLite + run.sh
- 目录重构: app/ -> frontend/,与 backend/ 并列 - 数据库: 默认 SQLite(零配置),修复 DSN() bug - run.sh: 一键启动前后端,支持 start/stop/restart/status - Makefile: install/dev/build/lint/clean 快捷命令 - vite.config.ts: 配置 dev server 端口 5173 + API 代理到 8080 - config.yaml: SQLite 默认配置,开箱即用 - docs/: 完整开发文档(项目结构/API/数据库/规范/部署) - 修复: GORM foreign key 关联声明 - 修复: go get gorm.io/driver/sqlite 依赖
This commit is contained in:
1 parent
7d6674bddd
commit
97c4207ff8
132 files changed
+1572
-34
No files matched your search
@@ -0,0 +1,631 @@
|
||||
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
|
||||
} 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 {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
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 { alertCenterApi } from '@/services/api';
|
||||
|
||||
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<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
}
|
||||
|
||||
interface AlertRule {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
severity: 'critical' | 'warning' | 'info';
|
||||
query: string;
|
||||
duration: string;
|
||||
threshold: number;
|
||||
labels: string[];
|
||||
notificationChannels: string[];
|
||||
lastEvaluated: string;
|
||||
}
|
||||
|
||||
interface NotificationChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'email' | 'slack' | 'webhook' | 'sms' | 'pagerduty';
|
||||
enabled: boolean;
|
||||
config: Record<string, string>;
|
||||
}
|
||||
|
||||
const AlertCenter: React.FC = () => {
|
||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||
const [rules, setRules] = useState<AlertRule[]>([]);
|
||||
const [channels, setChannels] = useState<NotificationChannel[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [severityFilter, setSeverityFilter] = useState<string>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [activeTab, setActiveTab] = useState('alerts');
|
||||
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [silenceOpen, setSilenceOpen] = useState(false);
|
||||
const [stats, setStats] = useState({
|
||||
firing: 0,
|
||||
critical: 0,
|
||||
warning: 0,
|
||||
resolved: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
await Promise.all([fetchAlerts(), fetchRules(), fetchChannels()]);
|
||||
};
|
||||
|
||||
const fetchAlerts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await alertCenterApi.getAlerts();
|
||||
const data = res.data || [];
|
||||
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
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error('获取告警失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRules = async () => {
|
||||
try {
|
||||
const res = await alertCenterApi.getRules();
|
||||
setRules(res.data || []);
|
||||
} catch (error) {
|
||||
toast.error('获取告警规则失败');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchChannels = async () => {
|
||||
try {
|
||||
const res = await alertCenterApi.getChannels();
|
||||
setChannels(res.data || []);
|
||||
} catch (error) {
|
||||
toast.error('获取通知渠道失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAcknowledge = async (id: string) => {
|
||||
try {
|
||||
await alertCenterApi.acknowledge(id);
|
||||
toast.success('告警已确认');
|
||||
fetchAlerts();
|
||||
} catch (error) {
|
||||
toast.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleResolve = async (id: string) => {
|
||||
try {
|
||||
await alertCenterApi.resolve(id);
|
||||
toast.success('告警已解决');
|
||||
fetchAlerts();
|
||||
} catch (error) {
|
||||
toast.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSilence = async (id: string, duration: string) => {
|
||||
try {
|
||||
await alertCenterApi.silence(id, duration);
|
||||
toast.success('告警已静默');
|
||||
setSilenceOpen(false);
|
||||
fetchAlerts();
|
||||
} catch (error) {
|
||||
toast.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRule = async (ruleId: string, enabled: boolean) => {
|
||||
try {
|
||||
await alertCenterApi.toggleRule(ruleId, enabled);
|
||||
setRules(rules.map(r => r.id === ruleId ? { ...r, enabled } : r));
|
||||
toast.success('规则状态已更新');
|
||||
} catch (error) {
|
||||
toast.error('更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityBadge = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return <Badge variant="destructive" className="gap-1"><AlertCircle className="w-3 h-3" />严重</Badge>;
|
||||
case 'warning':
|
||||
return <Badge variant="default" className="bg-orange-500 gap-1"><AlertTriangle className="w-3 h-3" />警告</Badge>;
|
||||
case 'info':
|
||||
return <Badge variant="default" className="bg-blue-500 gap-1"><Info className="w-3 h-3" />信息</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">未知</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'firing':
|
||||
return <Badge variant="destructive" className="gap-1 animate-pulse"><Zap className="w-3 h-3" />触发中</Badge>;
|
||||
case 'acknowledged':
|
||||
return <Badge variant="default" className="bg-yellow-500 gap-1"><User className="w-3 h-3" />已确认</Badge>;
|
||||
case 'resolved':
|
||||
return <Badge variant="default" className="bg-green-500 gap-1"><CheckCircle className="w-3 h-3" />已解决</Badge>;
|
||||
case 'suppressed':
|
||||
return <Badge variant="secondary" className="gap-1"><VolumeX className="w-3 h-3" />已静默</Badge>;
|
||||
default:
|
||||
return <Badge variant="secondary">未知</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getChannelIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'email': return <Mail className="w-4 h-4" />;
|
||||
case 'slack': return <MessageSquare className="w-4 h-4" />;
|
||||
case 'sms': return <Phone className="w-4 h-4" />;
|
||||
case 'webhook': return <Zap className="w-4 h-4" />;
|
||||
default: return <Bell className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAlerts = alerts.filter(alert => {
|
||||
const matchSearch = alert.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
alert.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
alert.source.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchSeverity = severityFilter === 'all' || alert.severity === severityFilter;
|
||||
const matchStatus = statusFilter === 'all' || alert.status === statusFilter;
|
||||
return matchSearch && matchSeverity && matchStatus;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className={stats.firing > 0 ? 'border-red-500 border-2' : ''}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">触发中</p>
|
||||
<p className={`text-2xl font-bold ${stats.firing > 0 ? 'text-red-600' : ''}`}>{stats.firing}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-lg ${stats.firing > 0 ? 'bg-red-100' : 'bg-gray-100'}`}>
|
||||
<Zap className={`w-5 h-5 ${stats.firing > 0 ? 'text-red-600' : 'text-gray-600'}`} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">严重告警</p>
|
||||
<p className="text-2xl font-bold text-red-600">{stats.critical}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-red-100 rounded-lg">
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">警告告警</p>
|
||||
<p className="text-2xl font-bold text-orange-600">{stats.warning}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-orange-100 rounded-lg">
|
||||
<AlertTriangle className="w-5 h-5 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">今日已解决</p>
|
||||
<p className="text-2xl font-bold text-green-600">{stats.resolved}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-100 rounded-lg">
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3 lg:w-[400px]">
|
||||
<TabsTrigger value="alerts">告警列表</TabsTrigger>
|
||||
<TabsTrigger value="rules">告警规则</TabsTrigger>
|
||||
<TabsTrigger value="channels">通知渠道</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="alerts" className="space-y-4">
|
||||
{/* 过滤栏 */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-[300px]">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索告警标题、描述、来源..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Select value={severityFilter} onValueChange={setSeverityFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="级别" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部级别</SelectItem>
|
||||
<SelectItem value="critical">严重</SelectItem>
|
||||
<SelectItem value="warning">警告</SelectItem>
|
||||
<SelectItem value="info">信息</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="firing">触发中</SelectItem>
|
||||
<SelectItem value="acknowledged">已确认</SelectItem>
|
||||
<SelectItem value="resolved">已解决</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={fetchAlerts}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => toast.info('批量操作功能开发中')}>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
批量确认
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 告警列表 */}
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">告警信息</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">级别</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">状态</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">来源</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">持续时间</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">加载中...</td>
|
||||
</tr>
|
||||
) : filteredAlerts.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-muted-foreground">暂无告警</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredAlerts.map((alert) => (
|
||||
<tr key={alert.id} className="hover:bg-muted/50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium">{alert.title}</div>
|
||||
<div className="text-sm text-muted-foreground line-clamp-1">{alert.description}</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
{Object.entries(alert.labels).slice(0, 3).map(([k, v]) => (
|
||||
<Badge key={k} variant="outline" className="text-xs">{k}={v}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">{getSeverityBadge(alert.severity)}</td>
|
||||
<td className="px-4 py-3">{getStatusBadge(alert.status)}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<div>{alert.source}</div>
|
||||
<div className="text-muted-foreground">{alert.service}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{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)}分钟`
|
||||
}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => { setSelectedAlert(alert); setDetailOpen(true); }}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
{alert.status === 'firing' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAcknowledge(alert.id)}
|
||||
>
|
||||
<Check className="w-4 h-4 text-blue-600" />
|
||||
</Button>
|
||||
)}
|
||||
{(alert.status === 'firing' || alert.status === 'acknowledged') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleResolve(alert.id)}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => { setSelectedAlert(alert); setSilenceOpen(true); }}
|
||||
>
|
||||
<VolumeX className="w-4 h-4 text-orange-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="rules" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={fetchRules}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={() => toast.info('创建规则功能开发中')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建规则
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{rules.map((rule) => (
|
||||
<Card key={rule.id}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="font-medium">{rule.name}</span>
|
||||
{getSeverityBadge(rule.severity)}
|
||||
<Badge variant={rule.enabled ? 'default' : 'secondary'}>
|
||||
{rule.enabled ? '已启用' : '已禁用'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-2">{rule.description}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span className="font-mono bg-muted px-2 py-1 rounded">{rule.query}</span>
|
||||
<span>持续 {rule.duration}</span>
|
||||
<span>阈值: {rule.threshold}</span>
|
||||
</div>
|
||||
<div className="flex gap-1 mt-2">
|
||||
{rule.notificationChannels.map(ch => (
|
||||
<Badge key={ch} variant="outline" className="text-xs">{ch}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={rule.enabled}
|
||||
onCheckedChange={(checked) => toggleRule(rule.id, checked)}
|
||||
/>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="channels" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={fetchChannels}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={() => toast.info('添加渠道功能开发中')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加渠道
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{channels.map((channel) => (
|
||||
<Card key={channel.id}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{getChannelIcon(channel.type)}
|
||||
<CardTitle className="text-base">{channel.name}</CardTitle>
|
||||
</div>
|
||||
<Badge variant={channel.enabled ? 'default' : 'secondary'}>
|
||||
{channel.enabled ? '已启用' : '已禁用'}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="text-sm text-muted-foreground capitalize mb-4">
|
||||
类型: {channel.type}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
配置
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Zap className="w-4 h-4 mr-2" />
|
||||
测试
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 告警详情对话框 */}
|
||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{selectedAlert?.title}
|
||||
{selectedAlert && getSeverityBadge(selectedAlert.severity)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedAlert && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-muted-foreground">描述</Label>
|
||||
<p className="mt-1">{selectedAlert.description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-muted-foreground">来源</Label>
|
||||
<p className="font-medium">{selectedAlert.source}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">服务</Label>
|
||||
<p className="font-medium">{selectedAlert.service}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">指标</Label>
|
||||
<p className="font-medium">{selectedAlert.metric}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">当前值 / 阈值</Label>
|
||||
<p className="font-medium">{selectedAlert.value} / {selectedAlert.threshold}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">开始时间</Label>
|
||||
<p className="font-medium">{selectedAlert.startedAt}</p>
|
||||
</div>
|
||||
{selectedAlert.resolvedAt && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">解决时间</Label>
|
||||
<p className="font-medium">{selectedAlert.resolvedAt}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">标签</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{Object.entries(selectedAlert.labels).map(([k, v]) => (
|
||||
<Badge key={k} variant="outline">{k}={v}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{selectedAlert.runbookUrl && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">处理手册</Label>
|
||||
<a href={selectedAlert.runbookUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline block mt-1">
|
||||
查看处理手册 →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailOpen(false)}>关闭</Button>
|
||||
{selectedAlert?.status === 'firing' && (
|
||||
<Button onClick={() => { handleAcknowledge(selectedAlert.id); setDetailOpen(false); }}>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
确认告警
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 静默对话框 */}
|
||||
<Dialog open={silenceOpen} onOpenChange={setSilenceOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>静默告警</DialogTitle>
|
||||
<DialogDescription>选择静默持续时间</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
{['1h', '4h', '8h', '24h', '7d'].map((duration) => (
|
||||
<Button
|
||||
key={duration}
|
||||
variant="outline"
|
||||
className="w-full justify-between"
|
||||
onClick={() => selectedAlert && handleSilence(selectedAlert.id, duration)}
|
||||
>
|
||||
<span>静默 {duration}</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertCenter;
|
||||
Reference in new issue
Block a user