// 中间件实例可观测面板 — 批量拉取 + 健康度 + 仪表盘 // Logan.Li import { useCallback, useEffect, useMemo, 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 { middlewareMonitorApi } from '@/api'; import type { MwChartDef, MwTarget, MwTimeRange } from './helpers'; import { MW_TIME_RANGE_HOURS, fmtMetricDisplay, fmtMemory, fmtHitRate, fmtVal, fmtLastSeen, mysqlConnPct, redisMemoryPct, redisModeLabel, groupHistoryByMetric, counterToDelta, evalMwHealth, gaugeTone, gaugeToneLow, REDIS_PRIMARY_KEYS, MYSQL_PRIMARY_KEYS, } from './helpers'; import { MwTrendChart } from './MwTrendChart'; import { MwGaugeBar } from './MwGaugeBar'; import { MwHealthStrip } from './MwHealthStrip'; import { ChartExpandShell } from '../shared/ChartExpandShell'; import i18n from '@/i18n'; interface Props { target: MwTarget | null; chartDefs: MwChartDef[]; accent: 'redis' | 'mysql'; } export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) { const [timeRange, setTimeRange] = useState('6h'); const [historyMap, setHistoryMap] = useState>>({}); const [loading, setLoading] = useState(false); const [showAll, setShowAll] = useState(false); const primaryKeys = accent === 'redis' ? REDIS_PRIMARY_KEYS : MYSQL_PRIMARY_KEYS; const visibleDefs = useMemo( () => (showAll ? chartDefs : chartDefs.filter((d) => primaryKeys.includes(d.key))), [showAll, chartDefs, primaryKeys], ); const loadCharts = useCallback(async () => { if (!target) return; setLoading(true); try { const res = await middlewareMonitorApi.getMetrics(target.id, { hours: MW_TIME_RANGE_HOURS[timeRange], }) as { list?: Array<{ metric_name: string; timestamp: number; value: number }> }; setHistoryMap(groupHistoryByMetric(res?.list || [])); } catch { setHistoryMap({}); } finally { setLoading(false); } }, [target, timeRange]); useEffect(() => { loadCharts(); }, [loadCharts]); // 30s 静默刷新趋势 // Logan.Li useEffect(() => { if (!target) return; const timer = setInterval(loadCharts, 30_000); return () => clearInterval(timer); }, [target, loadCharts]); if (!target) { return ( 选择左侧实例查看趋势与健康度 ); } const m = target.latest_metrics || {}; const memPct = redisMemoryPct(m); const connPct = mysqlConnPct(m); const healthIssues = evalMwHealth(target); return (

可观测 · {target.name}

{target.ip}:{target.port} {accent === 'redis' && ( {redisModeLabel(target.config_public?.redis_mode)} )} 更新 {fmtLastSeen(target.last_seen_at)}
{/* 仪表盘 */}
{accent === 'redis' && ( <> 0 ? gaugeToneLow((m.redis_hit_rate || 0) * 100, 90, 80) : 'ok'} /> 0 ? 50 : 0)} display={fmtMemory(m.redis_used_memory)} level={memPct != null ? gaugeTone(memPct, 80, 90) : 'ok'} hint={memPct != null ? `已用 ${memPct.toFixed(0)}%` : i18n.t('monitor.mwtargetchartsp.s1c1ce805')} /> 0 ? 'warn' : 'ok'} hint={m.redis_cluster_known_nodes ? `集群 ${fmtVal(m.redis_cluster_known_nodes)} 节点` : undefined} /> )} {accent === 'mysql' && ( <> 0 ? gaugeToneLow((m.mysql_buffer_hit_rate || 0) * 100, 95, 90) : 'ok'} /> 20 ? 'warn' : 'ok'} hint={`慢查询 ${fmtVal(m.mysql_slow_queries)}`} /> )}
{/* 核心摘要 */}
{accent === 'redis' ? ( <> 0} /> ) : ( <> )}
{loading ? (
加载趋势...
) : (
{visibleDefs.map((def) => { const series = def.counterDelta ? counterToDelta(historyMap[def.key] || []) : (historyMap[def.key] || []); return ( } compactHeight={150} expandedHeight={400} empty={series.length === 0} > {(h) => ( )} ); })}
)}
); } function SummaryTile({ label, value, warn }: { label: string; value: string; warn?: boolean }) { return (

{label}

{value}

); }