- 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 回归通过
256 lines
9.3 KiB
TypeScript
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>
|
|
);
|
|
}
|