feat: 网络双线图+告警联系人/联系组+Agent v2.0.0 + 一键报警阈值
- HostMonitor: 网络改为双线图 rx/tx 网卡带宽吞吐(双Area线), 移除单rx图 - Agent: 默认版本号从 dev 改为 v2.0.0, 重建二进制 5.2MB - 告警中心: 新增报警联系人/报警联系组 Tab, ContactDialog/ContactGroupDialog - 后端: AlertContact/AlertContactGroup 模型+CRUD, monitor_alert_contacts/groups 表 - 路由: /alerts/contacts + /alerts/contact-groups 8个REST端点 - 一键报警: 更新描述为6条规则(含TCP+进程), 切换到 /alerts/quick-setup - 测试规则: 内存>10% 触发告警已创建, 飞书通知验证通过
This commit is contained in:
1 parent
5333f6f04a
commit
e1cfbaf45e
11 files changed
+810
-16
No files matched your search
@@ -7,7 +7,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
@@ -15,13 +15,16 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { toast } from 'sonner';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { alertCenterApi, agentApi } from '@/services/api';
|
||||
import { alertCenterApi, agentApi, alertApi } from '@/services/api';
|
||||
import {
|
||||
type Alert, type AlertRule, mapAlert, getSeverityBadge, getStatusBadge,
|
||||
} from './alert-center-helpers';
|
||||
import { AlertDetailDialog, AlertSilenceDialog } from './AlertCenterDetail';
|
||||
import { AlertChannelsTab } from './AlertChannelsTab';
|
||||
import { AlertRuleDialog } from '@/components/monitor/AlertRuleDialog';
|
||||
import { ContactDialog } from './ContactDialog';
|
||||
import { ContactGroupDialog } from './ContactGroupDialog';
|
||||
import type { Contact, ContactGroup } from './alert-center-helpers';
|
||||
|
||||
const AlertCenter: React.FC = () => {
|
||||
const _confirm = useConfirm();
|
||||
@@ -40,6 +43,13 @@ const AlertCenter: React.FC = () => {
|
||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||
const [editingRule, setEditingRule] = useState<any>(null);
|
||||
const [quickSetupLoading, setQuickSetupLoading] = useState(false);
|
||||
// 联系人 & 联系组
|
||||
const [contacts, setContacts] = useState<Contact[]>([]);
|
||||
const [contactGroups, setContactGroups] = useState<ContactGroup[]>([]);
|
||||
const [contactDialogOpen, setContactDialogOpen] = useState(false);
|
||||
const [editingContact, setEditingContact] = useState<Contact | null>(null);
|
||||
const [groupDialogOpen, setGroupDialogOpen] = useState(false);
|
||||
const [editingGroup, setEditingGroup] = useState<ContactGroup | null>(null);
|
||||
// 分页
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
@@ -48,9 +58,11 @@ const AlertCenter: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
if (activeTab === 'contacts') fetchContacts();
|
||||
if (activeTab === 'groups') { fetchContacts(); fetchContactGroups(); }
|
||||
const interval = setInterval(fetchData, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [page, severityFilter, statusFilter]);
|
||||
}, [page, severityFilter, statusFilter, activeTab]);
|
||||
|
||||
const fetchData = async () => {
|
||||
await Promise.all([fetchAlerts(), fetchRules()]);
|
||||
@@ -103,6 +115,20 @@ const AlertCenter: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchContacts = async () => {
|
||||
try {
|
||||
const res: any = await alertApi.getContacts();
|
||||
setContacts((res?.data?.list || res?.list || []) as Contact[]);
|
||||
} catch { /* silent */ }
|
||||
};
|
||||
|
||||
const fetchContactGroups = async () => {
|
||||
try {
|
||||
const res: any = await alertApi.getContactGroups();
|
||||
setContactGroups((res?.data?.list || res?.list || []) as ContactGroup[]);
|
||||
} catch { /* silent */ }
|
||||
};
|
||||
|
||||
const handleAcknowledge = async (id: string) => {
|
||||
try {
|
||||
await alertCenterApi.acknowledge(id);
|
||||
@@ -151,13 +177,13 @@ const AlertCenter: React.FC = () => {
|
||||
const handleQuickSetup = async () => {
|
||||
const ok = await _confirm({
|
||||
title: '一键报警',
|
||||
description: '将为 Agent 在线的全部主机创建 CPU使用率、内存使用率、磁盘使用率、网络带宽使用率 四条默认告警规则(阈值 80%,连续 5 次触发)。',
|
||||
description: '为 Agent 在线主机创建 CPU(>80%)/内存(>80%)/磁盘(>90%)/TIME_WAIT(>15000)/CLOSE_WAIT(>200)/进程检测 6 条告警规则。',
|
||||
confirmText: '确认创建',
|
||||
});
|
||||
if (!ok) return;
|
||||
setQuickSetupLoading(true);
|
||||
try {
|
||||
const res = await agentApi.quickSetup('ecs');
|
||||
const res = await alertCenterApi.quickSetup('ecs');
|
||||
toast.success(typeof res === 'string' ? res : (res?.message || '默认告警规则创建成功'));
|
||||
fetchRules();
|
||||
} catch {
|
||||
@@ -236,10 +262,12 @@ const AlertCenter: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3 lg:w-[360px] h-9">
|
||||
<TabsList className="grid w-full grid-cols-5 lg:w-[600px] h-9">
|
||||
<TabsTrigger value="alerts" className="text-xs">告警列表</TabsTrigger>
|
||||
<TabsTrigger value="rules" className="text-xs">告警规则</TabsTrigger>
|
||||
<TabsTrigger value="channels" className="text-xs">通知渠道</TabsTrigger>
|
||||
<TabsTrigger value="contacts" className="text-xs">报警联系人</TabsTrigger>
|
||||
<TabsTrigger value="groups" className="text-xs">报警联系组</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 告警列表 Tab */}
|
||||
@@ -478,6 +506,121 @@ const AlertCenter: React.FC = () => {
|
||||
<TabsContent value="channels">
|
||||
<AlertChannelsTab refreshKey={0} />
|
||||
</TabsContent>
|
||||
|
||||
{/* 报警联系人 Tab */}
|
||||
<TabsContent value="contacts" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Button variant="outline" onClick={fetchContacts}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button onClick={() => { setEditingContact(null); setContactDialogOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加联系人
|
||||
</Button>
|
||||
</div>
|
||||
{contacts.length === 0 ? (
|
||||
<div className="text-center py-16 text-muted-foreground border border-dashed rounded-lg">
|
||||
暂无报警联系人,点击「添加联系人」创建
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{contacts.map((c) => (
|
||||
<Card key={c.id} className="hover:border-primary/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<CardTitle className="text-base">{c.name}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-1 text-sm text-muted-foreground">
|
||||
{c.phone && <div>📱 {c.phone}</div>}
|
||||
{c.email && <div>📧 {c.email}</div>}
|
||||
{c.dingtalk && <div>🔷 钉钉已配置</div>}
|
||||
{c.feishu && <div>🟢 飞书已配置</div>}
|
||||
{c.wechat && <div>💚 企业微信已配置</div>}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" className="flex-1"
|
||||
onClick={() => { setEditingContact(c); setContactDialogOpen(true); }}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={async () => {
|
||||
const ok = await _confirm({ title: '删除联系人', description: `确定删除联系人「${c.name}」?`, confirmText: '删除', variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try { await alertApi.deleteContact(c.id); toast.success('已删除'); fetchContacts(); }
|
||||
catch { toast.error('删除失败'); }
|
||||
}}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 报警联系组 Tab */}
|
||||
<TabsContent value="groups" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Button variant="outline" onClick={fetchContactGroups}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button onClick={() => { setEditingGroup(null); setGroupDialogOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加联系组
|
||||
</Button>
|
||||
</div>
|
||||
{contactGroups.length === 0 ? (
|
||||
<div className="text-center py-16 text-muted-foreground border border-dashed rounded-lg">
|
||||
暂无报警联系组,点击「添加联系组」创建
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{contactGroups.map((g) => {
|
||||
let cids: number[] = [];
|
||||
try { cids = JSON.parse(g.contact_ids || '[]'); } catch { /* ignore */ }
|
||||
const memberNames = contacts
|
||||
.filter(c => cids.includes(c.id))
|
||||
.map(c => c.name);
|
||||
return (
|
||||
<Card key={g.id} className="hover:border-primary/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<CardTitle className="text-base">{g.group_name}</CardTitle>
|
||||
<Badge variant="secondary">{cids.length} 人</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-2">
|
||||
{g.notes && <p className="text-sm text-muted-foreground">{g.notes}</p>}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{memberNames.map(name => (
|
||||
<Badge key={name} variant="outline" className="text-xs">{name}</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" className="flex-1"
|
||||
onClick={() => { setEditingGroup(g); setGroupDialogOpen(true); }}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={async () => {
|
||||
const ok = await _confirm({ title: '删除联系组', description: `确定删除联系组「${g.group_name}」?`, confirmText: '删除', variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try { await alertApi.deleteContactGroup(g.id); toast.success('已删除'); fetchContactGroups(); }
|
||||
catch { toast.error('删除失败'); }
|
||||
}}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<AlertDetailDialog
|
||||
@@ -499,6 +642,19 @@ const AlertCenter: React.FC = () => {
|
||||
notifyChannels={[]}
|
||||
onSaved={() => { handleRuleDialogClose(); fetchRules(); }}
|
||||
/>
|
||||
<ContactDialog
|
||||
open={contactDialogOpen}
|
||||
onOpenChange={setContactDialogOpen}
|
||||
editingContact={editingContact}
|
||||
onSaved={fetchContacts}
|
||||
/>
|
||||
<ContactGroupDialog
|
||||
open={groupDialogOpen}
|
||||
onOpenChange={setGroupDialogOpen}
|
||||
editingGroup={editingGroup}
|
||||
contacts={contacts}
|
||||
onSaved={() => { fetchContactGroups(); }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user