feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括: - 巡检数据存储与调度系统 - 华为/H3C交换机及Linux服务器采集器 - 设备扩展配置与告警规则管理 - 主机详情页巡检配置入口 - 产品名称自定义设置功能 - 网络设备SSH exec通道兼容处理
This commit is contained in:
528
components/inspection/AlertCenterPage.tsx
Normal file
528
components/inspection/AlertCenterPage.tsx
Normal file
@@ -0,0 +1,528 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
MessageSquare,
|
||||
RefreshCw,
|
||||
Filter,
|
||||
Info,
|
||||
AlertOctagon,
|
||||
Bell,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Input } from "../ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "../ui/dialog";
|
||||
import { Label } from "../ui/label";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import {
|
||||
inspectionAlertApi,
|
||||
type AlertRecord,
|
||||
type AlertLevel,
|
||||
type AlertRecordStatus,
|
||||
type AlertStats,
|
||||
} from "../../infrastructure/inspection/inspectionApi";
|
||||
import { toast } from "../ui/toast";
|
||||
|
||||
const LEVEL_COLORS: Record<AlertLevel, { bg: string; text: string; border: string; dot: string }> = {
|
||||
critical: {
|
||||
bg: "bg-red-500/10",
|
||||
text: "text-red-500",
|
||||
border: "border-red-500/30",
|
||||
dot: "bg-red-500",
|
||||
},
|
||||
warning: {
|
||||
bg: "bg-orange-500/10",
|
||||
text: "text-orange-500",
|
||||
border: "border-orange-500/30",
|
||||
dot: "bg-orange-500",
|
||||
},
|
||||
info: {
|
||||
bg: "bg-blue-500/10",
|
||||
text: "text-blue-500",
|
||||
border: "border-blue-500/30",
|
||||
dot: "bg-blue-500",
|
||||
},
|
||||
};
|
||||
|
||||
const LEVEL_LABELS: Record<AlertLevel, string> = {
|
||||
critical: "严重",
|
||||
warning: "警告",
|
||||
info: "提示",
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<AlertRecordStatus, string> = {
|
||||
active: "待处理",
|
||||
acknowledged: "已确认",
|
||||
resolved: "已恢复",
|
||||
closed: "已关闭",
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<AlertRecordStatus, string> = {
|
||||
active: "text-red-500",
|
||||
acknowledged: "text-orange-500",
|
||||
resolved: "text-green-500",
|
||||
closed: "text-muted-foreground",
|
||||
};
|
||||
|
||||
export function AlertCenterPage() {
|
||||
const [alerts, setAlerts] = useState<AlertRecord[]>([]);
|
||||
const [stats, setStats] = useState<AlertStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedAlert, setSelectedAlert] = useState<AlertRecord | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [noteDialogOpen, setNoteDialogOpen] = useState(false);
|
||||
const [noteText, setNoteText] = useState("");
|
||||
|
||||
// 筛选
|
||||
const [filterLevel, setFilterLevel] = useState<string>("all");
|
||||
const [filterStatus, setFilterStatus] = useState<string>("all");
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<"active" | "all">("active");
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statsData, alertsData] = await Promise.all([
|
||||
inspectionAlertApi.getStats(),
|
||||
activeTab === "active"
|
||||
? inspectionAlertApi.getActive()
|
||||
: inspectionAlertApi.list({ limit: 200 }),
|
||||
]);
|
||||
setStats(statsData);
|
||||
setAlerts(alertsData);
|
||||
} catch (e: any) {
|
||||
console.error("[AlertCenter] 加载失败:", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
// 筛选后的数据
|
||||
const filteredAlerts = (alerts ?? []).filter((a) => {
|
||||
if (filterLevel !== "all" && a.level !== filterLevel) return false;
|
||||
if (filterStatus !== "all" && a.status !== filterStatus) return false;
|
||||
if (searchText) {
|
||||
const s = searchText.toLowerCase();
|
||||
if (
|
||||
!a.device_name.toLowerCase().includes(s) &&
|
||||
!a.rule_name.toLowerCase().includes(s) &&
|
||||
!a.message.toLowerCase().includes(s)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const handleViewDetail = (alert: AlertRecord) => {
|
||||
setSelectedAlert(alert);
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
const handleAcknowledge = async () => {
|
||||
if (!selectedAlert) return;
|
||||
try {
|
||||
await inspectionAlertApi.acknowledge(selectedAlert.id, "operator");
|
||||
toast.success("已确认告警");
|
||||
setDetailOpen(false);
|
||||
loadData();
|
||||
} catch (e: any) {
|
||||
toast.error("操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleResolve = async () => {
|
||||
if (!selectedAlert) return;
|
||||
try {
|
||||
await inspectionAlertApi.resolve(selectedAlert.id, "manual", "operator");
|
||||
toast.success("已恢复告警");
|
||||
setDetailOpen(false);
|
||||
loadData();
|
||||
} catch (e: any) {
|
||||
toast.error("操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!selectedAlert) return;
|
||||
setNoteText("");
|
||||
setNoteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseConfirm = async () => {
|
||||
if (!selectedAlert) return;
|
||||
try {
|
||||
await inspectionAlertApi.close(selectedAlert.id, noteText);
|
||||
toast.success("已关闭告警");
|
||||
setNoteDialogOpen(false);
|
||||
setDetailOpen(false);
|
||||
loadData();
|
||||
} catch (e: any) {
|
||||
toast.error("操作失败");
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (ts: number) => {
|
||||
return new Date(ts).toLocaleString();
|
||||
};
|
||||
|
||||
const getTimeAgo = (ts: number) => {
|
||||
const diff = Date.now() - ts;
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return "刚刚";
|
||||
if (mins < 60) return `${mins} 分钟前`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return `${hours} 小时前`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days} 天前`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* 顶部统计 */}
|
||||
<div className="px-6 py-4 border-b border-border/60">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-red-500/10 flex items-center justify-center">
|
||||
<Bell size={18} className="text-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">告警中心</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
监控设备巡检产生的告警
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={loadData}>
|
||||
<RefreshCw size={14} className="mr-2" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="p-3 rounded-lg border border-red-500/30 bg-red-500/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertOctagon size={16} className="text-red-500" />
|
||||
<span className="text-sm text-muted-foreground">严重告警</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-red-500 mt-1">
|
||||
{stats?.active_critical ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-orange-500/30 bg-orange-500/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle size={16} className="text-orange-500" />
|
||||
<span className="text-sm text-muted-foreground">警告</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-orange-500 mt-1">
|
||||
{stats?.active_warning ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-blue-500/30 bg-blue-500/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info size={16} className="text-blue-500" />
|
||||
<span className="text-sm text-muted-foreground">提示</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-500 mt-1">
|
||||
{stats?.active_info ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-green-500/30 bg-green-500/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span className="text-sm text-muted-foreground">已恢复</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-green-500 mt-1">
|
||||
{stats?.resolved ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 筛选栏 */}
|
||||
<div className="flex items-center gap-3 px-6 py-3 border-b border-border/60">
|
||||
<div className="flex bg-secondary/50 rounded-md p-0.5">
|
||||
<button
|
||||
onClick={() => setActiveTab("active")}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
activeTab === "active"
|
||||
? "bg-background shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
待处理
|
||||
{stats && stats.active > 0 && (
|
||||
<Badge className="ml-2" variant="secondary">
|
||||
{stats.active}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("all")}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
activeTab === "all"
|
||||
? "bg-background shadow-sm font-medium"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<Select value={filterLevel} onValueChange={setFilterLevel}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="级别" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部级别</SelectItem>
|
||||
<SelectItem value="critical">严重</SelectItem>
|
||||
<SelectItem value="warning">警告</SelectItem>
|
||||
<SelectItem value="info">提示</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{activeTab === "all" && (
|
||||
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="active">待处理</SelectItem>
|
||||
<SelectItem value="acknowledged">已确认</SelectItem>
|
||||
<SelectItem value="resolved">已恢复</SelectItem>
|
||||
<SelectItem value="closed">已关闭</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<Input
|
||||
placeholder="搜索设备/规则..."
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="w-48"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 告警列表 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="text-center text-muted-foreground py-12">加载中...</div>
|
||||
) : filteredAlerts.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-16">
|
||||
<CheckCircle size={40} className="mx-auto mb-3 text-green-500 opacity-40" />
|
||||
<p className="text-sm">暂无告警记录</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{activeTab === "active" ? "所有设备运行正常" : "还没有产生过告警"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border/60">
|
||||
{filteredAlerts.map((alert) => {
|
||||
const colors = LEVEL_COLORS[alert.level];
|
||||
return (
|
||||
<div
|
||||
key={alert.id}
|
||||
onClick={() => handleViewDetail(alert)}
|
||||
className="flex items-center gap-4 px-6 py-3 hover:bg-accent/30 cursor-pointer transition-colors"
|
||||
>
|
||||
{/* 级别指示点 */}
|
||||
<div className={`w-2 h-2 rounded-full ${colors.dot}`} />
|
||||
|
||||
{/* 主要内容 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<span className="font-medium truncate">{alert.device_name}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${colors.bg} ${colors.text} ${colors.border} border`}
|
||||
>
|
||||
{LEVEL_LABELS[alert.level]}
|
||||
</Badge>
|
||||
<span className={`text-xs ${STATUS_COLORS[alert.status]}`}>
|
||||
{STATUS_LABELS[alert.status]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground truncate">
|
||||
{alert.rule_name} — {alert.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 时间 */}
|
||||
<div className="text-right shrink-0">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{getTimeAgo(alert.triggered_at)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground/60">
|
||||
{formatTime(alert.triggered_at)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChevronRight size={16} className="text-muted-foreground shrink-0" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 详情弹窗 */}
|
||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
{selectedAlert && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${LEVEL_COLORS[selectedAlert.level].dot}`}
|
||||
/>
|
||||
告警详情
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">设备名称</div>
|
||||
<div className="text-sm font-medium">{selectedAlert.device_name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">告警级别</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${LEVEL_COLORS[selectedAlert.level].bg} ${
|
||||
LEVEL_COLORS[selectedAlert.level].text
|
||||
} ${LEVEL_COLORS[selectedAlert.level].border} border`}
|
||||
>
|
||||
{LEVEL_LABELS[selectedAlert.level]}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">规则名称</div>
|
||||
<div className="text-sm">{selectedAlert.rule_name}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">告警消息</div>
|
||||
<div className="text-sm p-2 bg-secondary/30 rounded-md">
|
||||
{selectedAlert.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">当前值</div>
|
||||
<div className="text-sm font-mono">{selectedAlert.metric_value}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">阈值</div>
|
||||
<div className="text-sm font-mono">{selectedAlert.threshold}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">触发时间</div>
|
||||
<div className="text-sm">{formatTime(selectedAlert.triggered_at)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">当前状态</div>
|
||||
<div className={`text-sm font-medium ${STATUS_COLORS[selectedAlert.status]}`}>
|
||||
{STATUS_LABELS[selectedAlert.status]}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedAlert.note && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">备注</div>
|
||||
<div className="text-sm text-muted-foreground">{selectedAlert.note}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex justify-between">
|
||||
<Button variant="outline" onClick={() => setDetailOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
{selectedAlert.status === "active" && (
|
||||
<Button variant="outline" onClick={handleAcknowledge}>
|
||||
<MessageSquare size={14} className="mr-2" />
|
||||
确认
|
||||
</Button>
|
||||
)}
|
||||
{(selectedAlert.status === "active" ||
|
||||
selectedAlert.status === "acknowledged") && (
|
||||
<Button variant="outline" onClick={handleResolve}>
|
||||
<CheckCircle size={14} className="mr-2" />
|
||||
恢复
|
||||
</Button>
|
||||
)}
|
||||
{selectedAlert.status !== "closed" && (
|
||||
<Button variant="destructive" onClick={handleClose}>
|
||||
<XCircle size={14} className="mr-2" />
|
||||
关闭
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 关闭备注弹窗 */}
|
||||
<Dialog open={noteDialogOpen} onOpenChange={setNoteDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>关闭告警</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<Label>关闭原因(可选)</Label>
|
||||
<Textarea
|
||||
placeholder="请输入关闭原因或处理备注..."
|
||||
value={noteText}
|
||||
onChange={(e) => setNoteText(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setNoteDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleCloseConfirm}>
|
||||
确认关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AlertCenterPage;
|
||||
Reference in New Issue
Block a user