feat: 监控告警体系统一重构 + 自动化任务调度引擎 + 监控大屏拆分
告警中心 - 新增统一告警(alert_unified)、监控告警规则(monitor_alert_rule)、指标目录(alert_metric_catalog) - 通知策略与消息分发(alert_notify_policy/dispatch/message)、静默规则 - 告警引擎按阈值/SNMP 拆分(alert_engine_monitor/snmp) 监控采集 - SNMP 常量/模板与历史指标(snmp templates/constants、snmp_metrics_history、snmp_collect) - 中间件监控 Redis 采集与种子(middleware_monitor_redis/seed) - 站点探测 pkg(siteprobe) + 站点视图(site_view) 自动化任务 - cron 调度器(cronscheduler)、脚本运行器(scriptrunner)、系统任务(systask)及运行记录(cron_job_run、system_task) 审计与凭证 - 审计设置(audit_setting) + 历史清理(auditcleaner) 前端 - 监控页面(host/middleware/site/snmp)拆分为子模块并大屏化 - 告警中心多 Tab(规则/指标目录/通知策略/静默)、任务管理组件重构、Agent CLI Markdown 渲染 - 移除旧脚本卡片/详情/日志组件,统一为 ScriptTable/ScriptCodeEditor/TerminalOutput Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
092d0e4b9e
commit
471353e3d7
190 files changed
+16640
-3484
No files matched your search
@@ -0,0 +1,178 @@
|
||||
// SNMP 设备可观测面板 — 多指标趋势图 // Logan.Li
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Activity, LineChart, Loader2, RefreshCw } from 'lucide-react';
|
||||
import { snmpApi } from '@/api';
|
||||
import type {
|
||||
SnmpDevice, SnmpHistoryPoint, SnmpMetricSeries, SnmpTimeRange,
|
||||
} from './helpers';
|
||||
import {
|
||||
SNMP_TIME_RANGE_HOURS, fmtMetricValue, isSnmpCounterMetric, metricColor, pickSeriesByCategory,
|
||||
} from './helpers';
|
||||
import { SnmpMetricTrendChart } from './SnmpMetricTrendChart';
|
||||
|
||||
interface Props {
|
||||
device: SnmpDevice | null;
|
||||
templateName: string;
|
||||
}
|
||||
|
||||
interface ChartBlock {
|
||||
series: SnmpMetricSeries;
|
||||
data: SnmpHistoryPoint[];
|
||||
unit?: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||
const [timeRange, setTimeRange] = useState<SnmpTimeRange>('24h');
|
||||
const [series, setSeries] = useState<SnmpMetricSeries[]>([]);
|
||||
const [charts, setCharts] = useState<ChartBlock[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadCharts = useCallback(async () => {
|
||||
if (!device) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const seriesRes = await snmpApi.getDeviceMetricSeries(device.id) as { list?: SnmpMetricSeries[] };
|
||||
const allSeries = seriesRes?.list || [];
|
||||
setSeries(allSeries);
|
||||
const picked = pickSeriesByCategory(allSeries);
|
||||
const hours = SNMP_TIME_RANGE_HOURS[timeRange];
|
||||
|
||||
const blocks = await Promise.all(
|
||||
picked.map(async (s) => {
|
||||
try {
|
||||
const histRes = await snmpApi.getDeviceMetricHistory(device.id, {
|
||||
metric_name: s.name,
|
||||
index: s.index || undefined,
|
||||
hours,
|
||||
}) as { list?: SnmpHistoryPoint[]; rate_unit?: string };
|
||||
const unit = histRes?.rate_unit || (isSnmpCounterMetric(s.name) ? 'bps' : s.unit);
|
||||
return {
|
||||
series: s,
|
||||
data: histRes?.list || [],
|
||||
unit,
|
||||
loading: false,
|
||||
};
|
||||
} catch {
|
||||
return { series: s, data: [], loading: false };
|
||||
}
|
||||
}),
|
||||
);
|
||||
setCharts(blocks);
|
||||
} catch {
|
||||
setSeries([]);
|
||||
setCharts([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [device, timeRange]);
|
||||
|
||||
useEffect(() => { loadCharts(); }, [loadCharts]);
|
||||
|
||||
// 30s 静默刷新趋势 // Logan.Li
|
||||
useEffect(() => {
|
||||
if (!device) return;
|
||||
const timer = setInterval(loadCharts, 30_000);
|
||||
return () => clearInterval(timer);
|
||||
}, [device, loadCharts]);
|
||||
|
||||
if (!device) {
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
|
||||
点击设备行查看 CPU / 内存 / 温度 / 风扇趋势图
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const latestCpu = series.find((s) => s.name.includes('cpu'));
|
||||
const latestMem = series.find((s) => s.name.includes('mem'));
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between gap-2 mb-4 flex-wrap">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Activity className="h-4 w-4 text-[var(--dg-text-muted)] shrink-0" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)] truncate">
|
||||
可观测 · {device.name}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-[10px] border-[var(--dg-border)] shrink-0">
|
||||
{templateName}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as SnmpTimeRange)}>
|
||||
<SelectTrigger className="h-7 w-[88px] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="6h">近 6 小时</SelectItem>
|
||||
<SelectItem value="24h">近 24 小时</SelectItem>
|
||||
<SelectItem value="7d">近 7 天</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
|
||||
<RefreshCw className={`h-3 w-3 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||
{[
|
||||
{ label: '最新 CPU', value: latestCpu ? fmtMetricValue(latestCpu.latest_value, latestCpu.unit || '%') : '--' },
|
||||
{ label: '最新内存', value: latestMem ? fmtMetricValue(latestMem.latest_value, latestMem.unit || '%') : '--' },
|
||||
{ label: '数据保留', value: '7 天' },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="rounded-lg bg-[var(--dg-bg-tertiary)] p-3 text-center">
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
||||
<p className="text-sm font-mono font-medium text-[var(--dg-text-primary)] mt-0.5">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="py-12 flex items-center justify-center text-sm text-[var(--dg-text-muted)]">
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />加载趋势...
|
||||
</div>
|
||||
) : charts.length === 0 ? (
|
||||
<div className="py-12 text-center text-sm text-[var(--dg-text-muted)]">
|
||||
暂无可视化指标,请先完成至少一次采集
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{charts.map((block) => (
|
||||
<div
|
||||
key={`${block.series.name}-${block.series.index || ''}`}
|
||||
className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<LineChart className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />
|
||||
<span className="text-xs font-medium text-[var(--dg-text-primary)]">{block.series.label}</span>
|
||||
<span className="text-xs font-mono text-[var(--dg-text-muted)] ml-auto">
|
||||
{block.data.length > 0
|
||||
? fmtMetricValue(block.data[block.data.length - 1].value, block.unit)
|
||||
: fmtMetricValue(block.series.latest_value, block.series.unit)}
|
||||
</span>
|
||||
</div>
|
||||
<SnmpMetricTrendChart
|
||||
data={block.data}
|
||||
color={metricColor(block.series.name)}
|
||||
unit={block.unit}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user