- 151 个 hash-style flat key (s+hex 后缀) 全部重命名为语义化 nested key
- monitor 58 个: alertChannelDialog/alertManager/alertRulesTab/chartRangeHint/
esIndexPicker/helpers/hostMonitor/logAlertSpanel/logClusterPanel/logDingAssistant/
logQueryToolbar/logRbac/lokiLabelBrowse/metricsExplorer/middlewareMonit/
mwOverviewChart/portPolicyDialog/prometheusDashb/queryHistoryPan/savedQueriesPan/
siteMonitor/snmpMonitor/snmpOverviewChart/zabbixDashboard/zabbixDatasource
- kubernetes 42 个: deploymentBundle/deploymentEditAdvanced/deploymentEditService/
deploymentView/deploymentWorkload/eventsPage/kubernetesCluster/namespaceManage/
namespaceResource/podExecTerminal/podFileBrowser/podLogsViewer/podMetricsPage/
resourceVisualEditor/workloadViewPage
- aiops 17 个: agentCliMessage/aiAnalysisResult/analyzerHelpers/assistant/predictUtils
- agent 12 个: agentAlerts/agentMonitor/hostDetailDialog
- cloud 13 个: billingRecordDialog/billingRuleDialog/platformManager
- pipeline 6 个: buildNodesManage/deployRbac/deployTargetField
- system 3 个: auditSetting
- 58 个源文件中的 147 处 t()/i18n.t() 调用更新为语义化键路径
- 3 个命名空间冲突修复 (monitor.hostMonitor/agent.agentMonitor/aiops.aiAnalysisResult
原 string 值提升为 .title 子键)
- React Hooks 警告修复 (53→43, -10):
- exhaustive-deps 4→0: useMemo/useCallback 添加 t 到 deps 数组
- preserve-manual-memoization 5→0: 变量遮蔽修复 (filter(t)→filter(tg))
- 1 个冗余 eslint-disable 指令移除 (CronJobManager)
- zh/en 7480=7480 完全对齐, 0 个 hash key 残留
- tsc -b=0, npm build✓ (17.59s), ESLint errors=0/warnings=43/any=0
- go build=0, 17/17 集成测试通过, Docker healthy
143 lines
6.8 KiB
TypeScript
143 lines
6.8 KiB
TypeScript
// 中间件概览图表 — 状态 + 命中率 + QPS/连接(支持放大)// Logan.Li
|
|
import { useMemo, type ReactNode } from 'react';
|
|
import {
|
|
BarChart, Bar, XAxis, YAxis, Tooltip,
|
|
ResponsiveContainer, Cell, PieChart, Pie,
|
|
} from 'recharts';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { BarChart3, PieChart as PieChartIcon, Percent } from 'lucide-react';
|
|
import type { MwTarget } from './helpers';
|
|
import { fmtHitRate, fmtVal } from './helpers';
|
|
import { ChartExpandShell } from '../shared/ChartExpandShell';
|
|
import { MonitorChartTooltip, MONITOR_PIE_STROKE, MonitorPieTooltip } from '../shared/monitorChartTheme';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface Props {
|
|
targets: MwTarget[];
|
|
}
|
|
|
|
function shortName(name: string) {
|
|
return name.length > 10 ? `${name.slice(0, 10)}…` : name;
|
|
}
|
|
|
|
export function MwOverviewCharts({ targets }: Props) {
|
|
const { t } = useTranslation();
|
|
const statusData = useMemo(() => {
|
|
const online = targets.filter((tg) => tg.status === 'online').length;
|
|
const offline = targets.length - online;
|
|
return [
|
|
{ name: t('agent.agentOnline'), value: online, fill: 'var(--dg-green)' },
|
|
{ name: t('agent.agentOffline'), value: offline, fill: 'var(--dg-red)' },
|
|
].filter((d) => d.value > 0);
|
|
}, [targets, t]);
|
|
|
|
const redisHitData = useMemo(() => targets
|
|
.filter((tg) => tg.description === 'redis' && tg.latest_metrics?.redis_hit_rate > 0)
|
|
.map((tg) => ({
|
|
name: shortName(tg.name), fullName: tg.name,
|
|
value: (tg.latest_metrics.redis_hit_rate || 0) * 100,
|
|
fill: (tg.latest_metrics.redis_hit_rate || 0) >= 0.9 ? 'var(--dg-green)' : 'var(--dg-yellow)',
|
|
}))
|
|
.sort((a, b) => a.value - b.value), [targets]);
|
|
|
|
const redisQpsData = useMemo(() => targets
|
|
.filter((tg) => tg.description === 'redis' && tg.latest_metrics?.redis_qps > 0)
|
|
.map((tg) => ({ name: shortName(tg.name), fullName: tg.name, value: tg.latest_metrics.redis_qps, fill: 'var(--dg-blue)' }))
|
|
.sort((a, b) => b.value - a.value), [targets]);
|
|
|
|
const mysqlConnData = useMemo(() => targets
|
|
.filter((tg) => tg.description === 'mysql' && tg.latest_metrics?.mysql_connections > 0)
|
|
.map((tg) => ({ name: shortName(tg.name), fullName: tg.name, value: tg.latest_metrics.mysql_connections, fill: 'var(--dg-blue)' }))
|
|
.sort((a, b) => b.value - a.value), [targets]);
|
|
|
|
if (targets.length === 0) return null;
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3">
|
|
<OverviewChartCard title={t('monitor.lbl.instanceStatus')} icon={<PieChartIcon className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={statusData.length === 0}>
|
|
{(h) => (
|
|
<ResponsiveContainer width="100%" height={h}>
|
|
<PieChart>
|
|
<Pie data={statusData} dataKey="value" nameKey="name" cx="50%" cy="50%"
|
|
innerRadius={h * 0.22} outerRadius={h * 0.34} paddingAngle={2} isAnimationActive={false}>
|
|
{statusData.map((e) => <Cell key={e.name} fill={e.fill} stroke={MONITOR_PIE_STROKE} strokeWidth={2} />)}
|
|
</Pie>
|
|
<Tooltip content={<MonitorPieTooltip />} />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</OverviewChartCard>
|
|
|
|
<OverviewChartCard title={t('common.online')} icon={<Percent className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={redisHitData.length === 0}>
|
|
{(h) => (
|
|
<ResponsiveContainer width="100%" height={h}>
|
|
<BarChart data={redisHitData} layout="vertical" margin={{ left: 0, right: 8 }}>
|
|
<XAxis type="number" domain={[0, 100]} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickFormatter={(v) => `${v}%`} />
|
|
<YAxis type="category" dataKey="name" width={64} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickLine={false} axisLine={false} />
|
|
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtHitRate(v / 100)} nameFormatter={() => t('monitor.lbl.hitRate')} />} />
|
|
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
|
{redisHitData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</OverviewChartCard>
|
|
|
|
<OverviewChartCard title="Redis QPS" icon={<BarChart3 className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={redisQpsData.length === 0}>
|
|
{(h) => (
|
|
<ResponsiveContainer width="100%" height={h}>
|
|
<BarChart data={redisQpsData} layout="vertical" margin={{ left: 0, right: 8 }}>
|
|
<XAxis type="number" tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} />
|
|
<YAxis type="category" dataKey="name" width={64} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickLine={false} axisLine={false} />
|
|
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtVal(v, 1)} nameFormatter={() => 'QPS'} />} />
|
|
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
|
{redisQpsData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</OverviewChartCard>
|
|
|
|
<OverviewChartCard title={t('monitor.lbl.mysqlConnections')} icon={<BarChart3 className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={mysqlConnData.length === 0}>
|
|
{(h) => (
|
|
<ResponsiveContainer width="100%" height={h}>
|
|
<BarChart data={mysqlConnData} layout="vertical" margin={{ left: 0, right: 8 }}>
|
|
<XAxis type="number" tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} />
|
|
<YAxis type="category" dataKey="name" width={64} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickLine={false} axisLine={false} />
|
|
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtVal(v)} nameFormatter={() => t('monitor.lbl.connections')} />} />
|
|
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
|
{mysqlConnData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</OverviewChartCard>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OverviewChartCard({
|
|
title, icon, empty, children,
|
|
}: {
|
|
title: string;
|
|
icon: ReactNode;
|
|
empty?: boolean;
|
|
children: (height: number) => ReactNode;
|
|
}) {
|
|
return (
|
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
<CardContent className="p-3">
|
|
<ChartExpandShell
|
|
title={title}
|
|
icon={icon}
|
|
className="bg-transparent p-0"
|
|
compactHeight={140}
|
|
expandedHeight={360}
|
|
empty={empty}
|
|
>
|
|
{children}
|
|
</ChartExpandShell>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} |