Files
aiops/frontend/src/components/monitor/AlertManager.tsx
T
attacker c68aef4444 feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)
- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
2026-06-26 21:13:15 +08:00

256 lines
9.3 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Plus, Settings, Bell, AlertTriangle, Clock, CheckCircle } from 'lucide-react';
import {
ResponsiveContainer,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
} from 'recharts';
import { alertApi } from '@/api';
import { AlertEventsTab } from './AlertEventsTab';
import { AlertRulesTab } from './AlertRulesTab';
import { AlertRuleDialog } from './AlertRuleDialog';
import { AlertChannelDialog } from './AlertChannelDialog';
import type { JsonData } from '@/types/common';
export function AlertManager() {
const [activeTab, setActiveTab] = useState('events');
const [alerts, setAlerts] = useState<JsonData[]>([]);
const [rules, setRules] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true);
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<JsonData>(null);
const [channelDialogOpen, setChannelDialogOpen] = useState(false);
const [notifyChannels, setNotifyChannels] = useState<JsonData[]>([]);
const loadData = async () => {
try {
setLoading(true);
const [alertsData, rulesData, channelsData] = await Promise.all([
alertApi.getAlerts(),
alertApi.getRules(),
alertApi.getNotifyChannels(),
]);
setAlerts((alertsData as JsonData)?.list || []);
setRules((rulesData as JsonData)?.list || (Array.isArray(rulesData) ? rulesData : []));
setNotifyChannels((channelsData as JsonData)?.list || []);
} catch (error) {
console.error('Failed to load alert data:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
const openCreateRule = () => {
setEditingRule(null);
setRuleDialogOpen(true);
};
const openEditRule = (rule: JsonData) => {
setEditingRule(rule);
setRuleDialogOpen(true);
};
// 告警趋势数据
const alertTrendData = [
{ time: '00:00', critical: 2, warning: 5, info: 8 },
{ time: '04:00', critical: 1, warning: 3, info: 6 },
{ time: '08:00', critical: 3, warning: 7, info: 12 },
{ time: '12:00', critical: 5, warning: 10, info: 15 },
{ time: '16:00', critical: 4, warning: 8, info: 10 },
{ time: '20:00', critical: 3, warning: 6, info: 9 },
{ time: '24:00', critical: 2, warning: 4, info: 7 },
];
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">监控告警</h1>
<p className="text-sm text-slate-500 mt-1">告警规则管理与告警事件处理</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
onClick={() => setChannelDialogOpen(true)}
>
<Settings className="h-4 w-4 mr-2" />
通知配置
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreateRule}>
<Plus className="h-4 w-4 mr-2" />
新建规则
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">告警规则</p>
<p className="text-2xl font-bold text-slate-100">{rules.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
<Bell className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">触发中</p>
<p className="text-2xl font-bold text-red-400">
{alerts.filter((e) => e.status === 'firing').length}
</p>
</div>
<div className="p-3 rounded-lg bg-red-500/10 text-red-400">
<AlertTriangle className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">今日告警</p>
<p className="text-2xl font-bold text-amber-400">{alerts.length}</p>
</div>
<div className="p-3 rounded-lg bg-amber-500/10 text-amber-400">
<Clock className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">已恢复</p>
<p className="text-2xl font-bold text-emerald-400">
{alerts.filter((e) => e.status === 'resolved').length}
</p>
</div>
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
<CheckCircle className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Alert Trend Chart */}
<Card className="bg-slate-900 border-slate-800">
<CardHeader className="pb-2">
<CardTitle className="text-slate-100">告警趋势</CardTitle>
<CardDescription className="text-slate-500">过去24小时告警统计</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={alertTrendData}>
<defs>
<linearGradient id="colorCritical" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.3} />
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorWarning" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.3} />
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="time" stroke="#475569" fontSize={12} />
<YAxis stroke="#475569" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#0f172a',
border: '1px solid #1e293b',
borderRadius: '6px',
color: '#f8fafc',
}}
/>
<Area
type="monotone"
dataKey="critical"
stroke="#ef4444"
fillOpacity={1}
fill="url(#colorCritical)"
name="严重"
/>
<Area
type="monotone"
dataKey="warning"
stroke="#f59e0b"
fillOpacity={1}
fill="url(#colorWarning)"
name="警告"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="bg-slate-800 border border-slate-700">
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
告警事件
</TabsTrigger>
<TabsTrigger value="rules" className="data-[state=active]:bg-slate-700">
告警规则
</TabsTrigger>
</TabsList>
<TabsContent value="events">
<AlertEventsTab alerts={alerts} loading={loading} onReload={loadData} />
</TabsContent>
<TabsContent value="rules">
<AlertRulesTab
rules={rules}
notifyChannels={notifyChannels}
onReload={loadData}
onEditRule={openEditRule}
/>
</TabsContent>
</Tabs>
{/* Dialogs */}
<AlertRuleDialog
open={ruleDialogOpen}
onOpenChange={setRuleDialogOpen}
editingRule={editingRule}
notifyChannels={notifyChannels}
onSaved={loadData}
/>
<AlertChannelDialog
open={channelDialogOpen}
onOpenChange={setChannelDialogOpen}
notifyChannels={notifyChannels}
onChannelsChanged={loadData}
/>
</div>
);
}