Files
aiops/frontend/src/pages/monitor/snmp-monitor/SnmpDeviceChartsPanel.tsx
T
attacker d21eac9117 refactor(i18n): U24 Monitor 模块 i18n hook 迁移 — 26 个 .tsx 文件从全局 i18n.t() 迁移到 useTranslation() hook + 89 个 phantom 中文文本键替换为 monitor.lbl.* 语义化键
- 新增 monitor.lbl 翻译子命名空间 89 键(HostMonitor/Middleware/Site/SNMP/Shared)
- 26 .tsx: import i18n → useTranslation() hook + i18n.t('中文') → t('monitor.lbl.semanticKey')
- 模块级代码(TABS/METRIC_CATEGORIES)保留 i18n.t() + 语义化键
- 1 .ts helpers 文件 phantom 键替换
- zh/en 6886→6975 +89 键对齐
- tsc -b=0, npm build✓ (17.16s), ESLint=0/53/0, go build=0, Docker healthy
2026-07-11 20:15:44 +08:00

180 lines
7.2 KiB
TypeScript

// 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';
import { useTranslation } from 'react-i18next';
interface Props {
device: SnmpDevice | null;
templateName: string;
}
interface ChartBlock {
series: SnmpMetricSeries;
data: SnmpHistoryPoint[];
unit?: string;
loading: boolean;
}
export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
const { t } = useTranslation();
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"> {t('monitor.siteProtocol.rangeDefault')} </SelectItem>
<SelectItem value="24h"> {t('monitor.lbl.selectTemplate')} </SelectItem>
<SelectItem value="7d"> {t('monitor.lbl.selectTemplate')} </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: t('monitor.lbl.totalDevices'), value: latestCpu ? fmtMetricValue(latestCpu.latest_value, latestCpu.unit || '%') : '--' },
{ label: t('monitor.lbl.totalDevices'), value: latestMem ? fmtMetricValue(latestMem.latest_value, latestMem.unit || '%') : '--' },
{ label: t('monitor.lbl.totalDevices'), value: t('monitor.lbl.totalDevices') },
].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>
);
}