Files
NetMesh/components/inspection/AlertRulesPage.tsx
zhaolei c64e00d835 feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括:
- 巡检数据存储与调度系统
- 华为/H3C交换机及Linux服务器采集器
- 设备扩展配置与告警规则管理
- 主机详情页巡检配置入口
- 产品名称自定义设置功能
- 网络设备SSH exec通道兼容处理
2026-10-09 10:37:48 +08:00

590 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Plus, Trash2, Edit2, AlertTriangle, Bell, Settings2, Power, PowerOff } from "lucide-react";
import React, { useEffect, useState, useCallback } from "react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Badge } from "../ui/badge";
import { Switch } from "../ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "../ui/dialog";
import { Label } from "../ui/label";
import {
inspectionAlertRuleApi,
inspectionCatalogApi,
type AlertRule,
type AlertLevel,
type Operator,
type DeviceCatalog,
} from "../../infrastructure/inspection/inspectionApi";
import { toast } from "../ui/toast";
const LEVEL_OPTIONS: { value: AlertLevel; label: string; color: string }[] = [
{ value: "critical", label: "严重", color: "text-red-500 bg-red-500/10 border-red-500/30" },
{ value: "warning", label: "警告", color: "text-orange-500 bg-orange-500/10 border-orange-500/30" },
{ value: "info", label: "提示", color: "text-blue-500 bg-blue-500/10 border-blue-500/30" },
];
const OPERATOR_OPTIONS: { value: Operator; label: string }[] = [
{ value: ">", label: "大于 ( > )" },
{ value: "<", label: "小于 ( < )" },
{ value: ">=", label: "大于等于 ( >= )" },
{ value: "<=", label: "小于等于 ( <= )" },
{ value: "==", label: "等于 ( == )" },
{ value: "!=", label: "不等于 ( != )" },
];
const COMMON_METRICS = [
{ value: "cpu_usage", label: "CPU 使用率 (%)", group: "通用" },
{ value: "memory_usage", label: "内存使用率 (%)", group: "通用" },
{ value: "disk_usage", label: "磁盘使用率 (%)", group: "通用" },
{ value: "load_1m", label: "系统负载 (1分钟)", group: "通用" },
{ value: "online", label: "设备在线状态", group: "通用" },
{ value: "temperature", label: "设备温度 (°C)", group: "网络设备" },
{ value: "power_failure_count", label: "电源故障数", group: "网络设备" },
{ value: "fan_failure_count", label: "风扇故障数", group: "网络设备" },
{ value: "port_down_count", label: "端口 Down 数", group: "网络设备" },
{ value: "port_total_count", label: "端口总数", group: "网络设备" },
];
const SCOPE_OPTIONS = [
{ value: "all", label: "全部设备" },
{ value: "tier", label: "按设备分级" },
{ value: "catalog", label: "按设备类型" },
{ value: "device", label: "指定设备" },
];
export function AlertRulesPage() {
const [rules, setRules] = useState<AlertRule[]>([]);
const [catalogs, setCatalogs] = useState<DeviceCatalog[]>([]);
const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<AlertRule | null>(null);
// 筛选条件
const [filterLevel, setFilterLevel] = useState<"all" | AlertLevel>("all");
const [filterScope, setFilterScope] = useState<"all" | "preset" | "custom">("all");
const [searchText, setSearchText] = useState("");
// 表单状态
const [form, setForm] = useState({
name: "",
metric: "cpu_usage",
operator: ">" as Operator,
threshold: "80",
level: "warning" as AlertLevel,
duration: 1,
scope_type: "all" as "all" | "tier" | "catalog" | "device",
scope_value: "" as string,
enabled: true,
});
const loadRules = useCallback(async () => {
setLoading(true);
try {
const [list, catList] = await Promise.all([
inspectionAlertRuleApi.list(),
inspectionCatalogApi.list(),
]);
setRules(list);
setCatalogs(catList);
} catch (e: any) {
console.error("[AlertRules] 加载失败:", e);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadRules();
}, [loadRules]);
const handleCreate = () => {
setEditingRule(null);
setForm({
name: "",
metric: "cpu_usage",
operator: ">",
threshold: "80",
level: "warning",
duration: 1,
scope_type: "all",
scope_value: "",
enabled: true,
});
setDialogOpen(true);
};
const handleEdit = (rule: AlertRule) => {
setEditingRule(rule);
setForm({
name: rule.name,
metric: rule.metric,
operator: rule.operator,
threshold: rule.threshold,
level: rule.level,
duration: rule.duration,
scope_type: rule.scope_type as any,
scope_value: Array.isArray(rule.scope_value)
? rule.scope_value.join(", ")
: rule.scope_value || "",
enabled: rule.enabled,
});
setDialogOpen(true);
};
const handleSave = async () => {
if (!form.name.trim()) {
toast.error("请输入规则名称");
return;
}
if (!form.threshold.trim()) {
toast.error("请输入阈值");
return;
}
try {
const data: Partial<AlertRule> = {
name: form.name,
metric: form.metric,
operator: form.operator,
threshold: form.threshold,
level: form.level,
duration: form.duration,
scope_type: form.scope_type,
scope_value:
form.scope_type === "all"
? null
: form.scope_type === "device"
? form.scope_value.split(",").map((s) => s.trim()).filter(Boolean)
: form.scope_value || null,
enabled: form.enabled,
};
if (editingRule) {
await inspectionAlertRuleApi.update(editingRule.id, data);
toast.success("规则已更新");
} else {
await inspectionAlertRuleApi.create(data);
toast.success("规则已创建");
}
setDialogOpen(false);
loadRules();
} catch (e: any) {
toast.error("保存失败: " + e.message);
}
};
const handleToggle = async (ruleId: string, enabled: boolean) => {
try {
await inspectionAlertRuleApi.toggle(ruleId, enabled);
toast.success(enabled ? "已启用" : "已停用");
loadRules();
} catch (e: any) {
toast.error("操作失败");
}
};
const handleDelete = async (ruleId: string) => {
if (!confirm("确定要删除这条规则吗?")) return;
try {
await inspectionAlertRuleApi.remove(ruleId);
toast.success("已删除");
loadRules();
} catch (e: any) {
toast.error("删除失败");
}
};
const getLevelBadge = (level: AlertLevel) => {
const opt = LEVEL_OPTIONS.find((l) => l.value === level);
return (
<Badge variant="outline" className={opt?.color}>
{opt?.label}
</Badge>
);
};
const getScopeLabel = (rule: AlertRule) => {
switch (rule.scope_type) {
case "all":
return "全部设备";
case "tier":
const tierLabels: Record<string, string> = {
core: "核心级",
aggregation: "重要级",
access: "一般",
};
return `分级: ${tierLabels[String(rule.scope_value)] || rule.scope_value}`;
case "catalog":
const cat = catalogs.find((c) => c.id === rule.scope_value);
return `类型: ${cat?.name || rule.scope_value}`;
case "device":
return `指定设备 (${Array.isArray(rule.scope_value) ? rule.scope_value.length : 0})`;
default:
return rule.scope_type;
}
};
// 筛选后的数据
const filteredRules = (rules ?? []).filter((r) => {
if (filterLevel !== "all" && r.level !== filterLevel) return false;
if (filterScope === "preset" && !r.preset) return false;
if (filterScope === "custom" && r.preset) return false;
if (searchText) {
const s = searchText.toLowerCase();
if (!r.name.toLowerCase().includes(s) && !r.metric.toLowerCase().includes(s)) {
return false;
}
}
return true;
});
return (
<div className="h-full flex flex-col">
{/* 顶部工具栏 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border/60">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center">
<AlertTriangle size={18} className="text-primary" />
</div>
<div>
<h2 className="text-lg font-semibold">告警规则配置</h2>
<p className="text-sm text-muted-foreground">
定义设备巡检的告警触发条件(共 {(rules?.length ?? 0)} 条)
</p>
</div>
</div>
<Button onClick={handleCreate}>
<Plus size={16} className="mr-2" />
新增规则
</Button>
</div>
{/* 筛选栏 */}
<div className="flex items-center gap-3 px-6 py-3 border-b border-border/40 bg-muted/20">
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">级别:</span>
<div className="flex gap-1">
{[
{ value: "all", label: "全部" },
{ value: "critical", label: "严重" },
{ value: "warning", label: "警告" },
{ value: "info", label: "提示" },
].map((opt) => (
<button
key={opt.value}
onClick={() => setFilterLevel(opt.value as any)}
className={`px-2.5 py-1 text-xs rounded-md transition-colors ${
filterLevel === opt.value
? "bg-primary text-primary-foreground"
: "bg-background hover:bg-accent text-muted-foreground"
}`}
>
{opt.label}
</button>
))}
</div>
</div>
<div className="w-px h-4 bg-border/60" />
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">分类:</span>
<div className="flex gap-1">
{[
{ value: "all", label: "全部" },
{ value: "preset", label: "预置" },
{ value: "custom", label: "自定义" },
].map((opt) => (
<button
key={opt.value}
onClick={() => setFilterScope(opt.value as any)}
className={`px-2.5 py-1 text-xs rounded-md transition-colors ${
filterScope === opt.value
? "bg-primary text-primary-foreground"
: "bg-background hover:bg-accent text-muted-foreground"
}`}
>
{opt.label}
</button>
))}
</div>
</div>
<div className="flex-1" />
<div className="relative w-56">
<Input
placeholder="搜索规则名称..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="h-8 text-xs"
/>
</div>
</div>
{/* 规则列表 */}
<div className="flex-1 overflow-auto p-6">
{loading ? (
<div className="text-center text-muted-foreground py-8">加载中...</div>
) : filteredRules.length === 0 ? (
<div className="text-center text-muted-foreground py-12">
<Bell size={40} className="mx-auto mb-3 opacity-40" />
<p>暂无符合条件的告警规则</p>
<p className="text-sm">点击右上角新增规则开始配置</p>
</div>
) : (
<div className="space-y-2">
{filteredRules.map((rule) => (
<div
key={rule.id}
className="flex items-center gap-4 p-4 border border-border/60 rounded-lg hover:bg-accent/30 transition-colors"
>
{/* 启用开关 */}
<div className="flex flex-col items-center gap-1">
<Switch
checked={rule.enabled}
onCheckedChange={(v) => handleToggle(rule.id, v)}
/>
<span className="text-[10px] text-muted-foreground">
{rule.enabled ? "启用" : "停用"}
</span>
</div>
{/* 规则信息 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{rule.name}</span>
{getLevelBadge(rule.level)}
{rule.preset && (
<Badge variant="secondary" className="text-xs">
预置
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground flex items-center gap-3">
<span>
{COMMON_METRICS.find((m) => m.value === rule.metric)?.label || rule.metric}
</span>
<span className="font-mono">{rule.operator}</span>
<span>{rule.threshold}</span>
<span className="text-border">|</span>
<span>{getScopeLabel(rule)}</span>
{rule.duration > 1 && (
<>
<span className="text-border">|</span>
<span>持续 {rule.duration} 次</span>
</>
)}
</div>
</div>
{/* 操作按钮 */}
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEdit(rule)}
title="编辑"
>
<Edit2 size={15} />
</Button>
{!rule.preset && (
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(rule.id)}
title="删除"
className="text-destructive hover:text-destructive"
>
<Trash2 size={15} />
</Button>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* 编辑弹窗 */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{editingRule ? "编辑告警规则" : "新增告警规则"}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
{/* 规则名称 */}
<div className="space-y-2">
<Label>规则名称</Label>
<Input
placeholder="例如:CPU 使用率过高"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
</div>
{/* 指标 + 操作符 + 阈值 */}
<div className="grid grid-cols-3 gap-3">
<div className="space-y-2">
<Label>指标</Label>
<Select value={form.metric} onValueChange={(v) => setForm({ ...form, metric: v })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{COMMON_METRICS.map((m) => (
<SelectItem key={m.value} value={m.value}>
{m.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>条件</Label>
<Select
value={form.operator}
onValueChange={(v) => setForm({ ...form, operator: v as Operator })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{OPERATOR_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>阈值</Label>
<Input
placeholder="80"
value={form.threshold}
onChange={(e) => setForm({ ...form, threshold: e.target.value })}
/>
</div>
</div>
{/* 告警级别 */}
<div className="space-y-2">
<Label>告警级别</Label>
<div className="flex gap-2">
{LEVEL_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => setForm({ ...form, level: opt.value })}
className={`flex-1 py-2 px-3 rounded-md border text-sm font-medium transition-colors ${
form.level === opt.value
? `border-primary/50 ${opt.color}`
: "border-border hover:border-primary/30"
}`}
>
{opt.label}
</button>
))}
</div>
</div>
{/* 适用范围 */}
<div className="space-y-2">
<Label>适用范围</Label>
<div className="flex gap-2">
<Select
value={form.scope_type}
onValueChange={(v) => setForm({ ...form, scope_type: v as any })}
>
<SelectTrigger className="flex-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SCOPE_OPTIONS.map((s) => (
<SelectItem key={s.value} value={s.value}>
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
{form.scope_type !== "all" && (
<Select
value={form.scope_value}
onValueChange={(v) => setForm({ ...form, scope_value: v })}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="请选择" />
</SelectTrigger>
<SelectContent>
{form.scope_type === "catalog" &&
catalogs.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
{form.scope_type === "tier" && (
<>
<SelectItem value="core">核心级</SelectItem>
<SelectItem value="aggregation">重要级</SelectItem>
<SelectItem value="access">一般</SelectItem>
</>
)}
</SelectContent>
</Select>
)}
</div>
{form.scope_type === "device" && (
<Input
placeholder="输入设备 ID,多个用逗号分隔"
value={form.scope_value}
onChange={(e) => setForm({ ...form, scope_value: e.target.value })}
className="mt-1"
/>
)}
</div>
{/* 持续次数 */}
<div className="space-y-2">
<Label>持续次数(防抖动)</Label>
<div className="flex items-center gap-3">
<Input
type="number"
min={1}
max={10}
value={form.duration}
onChange={(e) =>
setForm({ ...form, duration: parseInt(e.target.value, 10) || 1 })
}
className="w-24"
/>
<span className="text-sm text-muted-foreground">
连续触发 N 次后才产生告警,避免瞬时波动误报
</span>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)}>
取消
</Button>
<Button onClick={handleSave}>{editingRule ? "保存修改" : "创建规则"}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
export default AlertRulesPage;