批量转换 163 个文件,3204 处中文替换为 t() 调用: - Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换 - Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换 - Deploy: 流水线/发布/凭证/RBAC 全部转换 - Server/Cloud/Agent/Task/Layout/UI: 全部转换 - 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys) - tsc 编译零错误
137 lines
5.6 KiB
TypeScript
137 lines
5.6 KiB
TypeScript
// SNMP 概览图表 — 设备状态分布 + CPU 对比 // Logan.Li
|
|
import { useMemo } from 'react';
|
|
import {
|
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
|
|
ResponsiveContainer, Cell, PieChart, Pie,
|
|
} from 'recharts';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { BarChart3, PieChart as PieChartIcon } from 'lucide-react';
|
|
import type { SnmpDevice, SnmpOverviewItem } from './helpers';
|
|
import { fmtMetricValue, statusColor } from './helpers';
|
|
import { MonitorChartTooltip, MONITOR_PIE_STROKE, MonitorPieTooltip } from '../shared/monitorChartTheme';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface Props {
|
|
devices: SnmpDevice[];
|
|
overview: SnmpOverviewItem[];
|
|
}
|
|
|
|
export function SnmpOverviewCharts({ devices, overview }: Props) {
|
|
const { t } = useTranslation();
|
|
const statusData = useMemo(() => {
|
|
const online = devices.filter((d) => d.status === 'online').length;
|
|
const error = devices.filter((d) => d.status === 'error').length;
|
|
const offline = devices.length - online - error;
|
|
return [
|
|
{ name: t('common.online'), value: online, fill: 'var(--dg-green)' },
|
|
{ name: t('common.abnormal'), value: error, fill: 'var(--dg-red)' },
|
|
{ name: t('common.offline'), value: offline, fill: 'var(--dg-text-muted)' },
|
|
].filter((d) => d.value > 0);
|
|
}, [devices]);
|
|
|
|
const cpuData = useMemo(() => {
|
|
return overview
|
|
.filter((o) => o.cpu != null && o.cpu > 0)
|
|
.map((o) => ({
|
|
name: o.name.length > 10 ? `${o.name.slice(0, 10)}…` : o.name,
|
|
fullName: o.name,
|
|
cpu: o.cpu ?? 0,
|
|
fill: statusColor(o.status),
|
|
}))
|
|
.sort((a, b) => b.cpu - a.cpu);
|
|
}, [overview]);
|
|
|
|
if (devices.length === 0) return null;
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
|
<Card className="lg:col-span-2 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<BarChart3 className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
|
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">{t('monitor.snmpoverviewcharts.s64f5a4')}</h3>
|
|
</div>
|
|
{cpuData.length === 0 ? (
|
|
<p className="text-xs text-[var(--dg-text-muted)] py-8 text-center">{t('monitor.snmpoverviewcharts.s1f4418')}</p>
|
|
) : (
|
|
<ResponsiveContainer width="100%" height={Math.max(160, cpuData.length * 36)}>
|
|
<BarChart data={cpuData} layout="vertical" margin={{ top: 0, right: 16, left: 0, bottom: 0 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--dg-border)" horizontal={false} />
|
|
<XAxis
|
|
type="number"
|
|
domain={[0, 100]}
|
|
tick={{ fontSize: 10, fill: 'var(--dg-text-muted)' }}
|
|
tickLine={false}
|
|
axisLine={{ stroke: 'var(--dg-border)' }}
|
|
tickFormatter={(v: number) => `${v}%`}
|
|
/>
|
|
<YAxis
|
|
type="category"
|
|
dataKey="name"
|
|
width={72}
|
|
tick={{ fontSize: 10, fill: 'var(--dg-text-muted)' }}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
/>
|
|
<Tooltip
|
|
content={(
|
|
<MonitorChartTooltip
|
|
valueFormatter={(v) => fmtMetricValue(v, '%')}
|
|
nameFormatter={() => 'CPU'}
|
|
/>
|
|
)}
|
|
labelFormatter={(_, payload) => {
|
|
const item = payload?.[0]?.payload as { fullName?: string } | undefined;
|
|
return item?.fullName ?? '';
|
|
}}
|
|
/>
|
|
<Bar dataKey="cpu" radius={[0, 4, 4, 0]} maxBarSize={20} isAnimationActive={false}>
|
|
{cpuData.map((entry) => (
|
|
<Cell key={entry.fullName} fill={entry.fill} />
|
|
))}
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<PieChartIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
|
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">{t('monitor.snmpoverviewcharts.sbb0ae9')}</h3>
|
|
</div>
|
|
<ResponsiveContainer width="100%" height={180}>
|
|
<PieChart>
|
|
<Pie
|
|
data={statusData}
|
|
dataKey="value"
|
|
nameKey="name"
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={42}
|
|
outerRadius={68}
|
|
paddingAngle={2}
|
|
isAnimationActive={false}
|
|
>
|
|
{statusData.map((entry) => (
|
|
<Cell key={entry.name} fill={entry.fill} stroke={MONITOR_PIE_STROKE} strokeWidth={2} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip content={<MonitorPieTooltip />} />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
<div className="flex flex-wrap justify-center gap-3 mt-1">
|
|
{statusData.map((d) => (
|
|
<div key={d.name} className="flex items-center gap-1.5 text-xs text-[var(--dg-text-muted)]">
|
|
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: d.fill }} />
|
|
{d.name} {d.value}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
} |