新增设备巡检模块核心功能,包括: - 巡检数据存储与调度系统 - 华为/H3C交换机及Linux服务器采集器 - 设备扩展配置与告警规则管理 - 主机详情页巡检配置入口 - 产品名称自定义设置功能 - 网络设备SSH exec通道兼容处理
590 lines
20 KiB
TypeScript
590 lines
20 KiB
TypeScript
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;
|