Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled

This commit is contained in:
2026-10-15 13:53:04 +08:00
commit 2f9f8da48e
3299 changed files with 926841 additions and 0 deletions

View File

@@ -0,0 +1,589 @@
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;