Files
aiops/frontend/src/pages/monitor/MiddlewareMonitor.tsx
T
attacker ecec73989b feat(i18n): 剩余模块全量国际化 — 51% 覆盖率 (231 文件接入 t())
批量转换 163 个文件,3204 处中文替换为 t() 调用:
- Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换
- Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换
- Deploy: 流水线/发布/凭证/RBAC 全部转换
- Server/Cloud/Agent/Task/Layout/UI: 全部转换
- 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys)
- tsc 编译零错误
2026-07-03 01:51:57 +08:00

289 lines
13 KiB
TypeScript

// 中间件监控 — Redis(单机/集群)/ MySQL 可视化可观测 // Logan.Li
import { useState, useEffect, useCallback, useRef } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Database, Server, Activity, Loader2, Plus, Zap, RefreshCw } from 'lucide-react';
import { middlewareMonitorApi } from '@/api';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { MwOverviewCharts } from './middleware-monitor/MwOverviewCharts';
import { MwTargetChartsPanel } from './middleware-monitor/MwTargetChartsPanel';
import { InstanceCard } from './middleware-monitor/InstanceCard';
import { MiddlewareDialog } from './middleware-monitor/MiddlewareDialog';
import type { MwTarget } from './middleware-monitor/helpers';
import { REDIS_CHARTS, MYSQL_CHARTS, evalMwHealth, worstHealthLevel } from './middleware-monitor/helpers';
import { useTranslation } from 'react-i18next';
type TabKey = 'redis' | 'mysql';
const REFRESH_INTERVAL_MS = 30_000;
interface MwListResponse { list: MwTarget[]; total: number }
export function MiddlewareMonitor() {
const { t } = useTranslation();
const confirm = useConfirm();
const [targets, setTargets] = useState<MwTarget[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState('');
const [tab, setTab] = useState<TabKey>('redis');
const [selectedRedis, setSelectedRedis] = useState<MwTarget | null>(null);
const [selectedMysql, setSelectedMysql] = useState<MwTarget | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [editTarget, setEditTarget] = useState<MwTarget | null>(null);
const [testing, setTesting] = useState<number | null>(null);
const [seeding, setSeeding] = useState(false);
const [lastRefreshAt, setLastRefreshAt] = useState<Date | null>(null);
const hasLoaded = useRef(false);
const syncSelection = useCallback((list: MwTarget[]) => {
const redis = list.filter((t) => t.description === 'redis');
const mysql = list.filter((t) => t.description === 'mysql');
setSelectedRedis((prev) => {
if (prev && redis.some((t) => t.id === prev.id)) return redis.find((t) => t.id === prev.id) || null;
return redis.find((t) => t.status === 'online') || redis[0] || null;
});
setSelectedMysql((prev) => {
if (prev && mysql.some((t) => t.id === prev.id)) return mysql.find((t) => t.id === prev.id) || null;
return mysql.find((t) => t.status === 'online') || mysql[0] || null;
});
if (redis.length === 0 && mysql.length > 0) setTab('mysql');
}, []);
const loadTargets = useCallback(async (silent = false) => {
try {
if (!silent) setLoading(true);
else setRefreshing(true);
setError('');
const data = await middlewareMonitorApi.list() as MwListResponse;
const list = data?.list || [];
setTargets(list);
syncSelection(list);
setLastRefreshAt(new Date());
hasLoaded.current = true;
} catch (err) {
if (!silent) {
setError((err as { message?: string }).message || t('common.loadFailed'));
setTargets([]);
}
} finally {
setLoading(false);
setRefreshing(false);
}
}, [syncSelection]);
useEffect(() => { loadTargets(); }, [loadTargets]);
// 30s 静默刷新,与采集周期对齐 // Logan.Li
useEffect(() => {
if (!hasLoaded.current || targets.length === 0) return;
const timer = setInterval(() => loadTargets(true), REFRESH_INTERVAL_MS);
return () => clearInterval(timer);
}, [targets.length, loadTargets]);
const redisTargets = targets.filter((t) => t.description === 'redis');
const mysqlTargets = targets.filter((t) => t.description === 'mysql');
const onlineCount = targets.filter((t) => t.status === 'online').length;
const warnCount = targets.filter((t) => worstHealthLevel(evalMwHealth(t)) !== 'ok').length;
const handleDelete = async (target: MwTarget) => {
const ok = await confirm({ title: t('monitor.middlewaremonitor.sf9be97'), description: t('monitor.middlewaremonitor.s45a9e0', { target.name: target.name }), confirmText: t('common.delete') });
if (!ok) return;
try {
await middlewareMonitorApi.delete(target.id);
toast.success(t('alert.deleted'));
loadTargets();
} catch (err) {
toast.error(t('common.deleteFailed'), { description: (err as { message?: string }).message });
}
};
const handleTest = async (target: MwTarget) => {
setTesting(target.id);
try {
const res = await middlewareMonitorApi.test(target.id) as { success: boolean; message: string };
if (res.success) { toast.success(t('aiops.llmTestSuccess'), { description: res.message }); loadTargets(true); }
else toast.error(t('aiops.llmTestFailed'), { description: res.message });
} catch (err) {
toast.error(t('monitor.middlewaremonitor.s9710d9'), { description: (err as { message?: string }).message });
} finally {
setTesting(null);
}
};
const handleSeedDefaults = async () => {
setSeeding(true);
try {
const res = await middlewareMonitorApi.seedDefaults() as { created: number; message: string; failed?: string[] };
await loadTargets();
if (res.failed?.length) toast.error(t('monitor.middlewaremonitor.s669e89'), { description: res.message });
else if (res.created > 0) toast.success(t('monitor.middlewaremonitor.s11a31e'), { description: res.message });
else toast.success(t('monitor.middlewaremonitor.s7dda0f'));
} catch (err) {
toast.error(t('deploy.toast.addFailed'), { description: (err as { message?: string }).message });
} finally {
setSeeding(false);
}
};
const handleCollectNow = async () => {
try {
const res = await middlewareMonitorApi.collectNow() as { message: string };
toast.success(t('monitor.middlewaremonitor.sc2c744'), { description: res.message });
loadTargets(true);
} catch (err) {
toast.error(t('monitor.middlewaremonitor.s5665b6'), { description: (err as { message?: string }).message });
}
};
const openCreate = (type?: TabKey) => {
setEditTarget(null);
if (type) setTab(type);
setDialogOpen(true);
};
const currentTargets = tab === 'redis' ? redisTargets : mysqlTargets;
const selected = tab === 'redis' ? selectedRedis : selectedMysql;
const setSelected = tab === 'redis' ? setSelectedRedis : setSelectedMysql;
return (
<div className="space-y-6">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h2 className="text-lg font-semibold">{t('monitor.middleware')}</h2>
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">
Redis 单机/集群 · MySQL · 30s 自动刷新
{lastRefreshAt && (
<span className="ml-2">
上次更新 {lastRefreshAt.toLocaleTimeString()}
{refreshing && t('monitor.middlewaremonitor.s4d2bc9')}
</span>
)}
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleCollectNow} className="gap-1.5">
<Zap className="h-4 w-4" />立即采集
</Button>
<Button variant="outline" size="sm" onClick={() => loadTargets(true)} disabled={refreshing} className="gap-1.5">
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />刷新
</Button>
{targets.length === 0 && (
<Button variant="outline" size="sm" onClick={handleSeedDefaults} disabled={seeding} className="gap-1.5">
{seeding ? <Loader2 className="h-4 w-4 animate-spin" /> : <Database className="h-4 w-4" />}
添加平台默认
</Button>
)}
<Button size="sm" onClick={() => openCreate()} className="gap-1.5">
<Plus className="h-4 w-4" />添加监控
</Button>
</div>
</div>
{loading && (
<div className="flex items-center justify-center py-12 text-sm text-[var(--dg-text-muted)]">
<Loader2 className="h-5 w-5 animate-spin mr-2" />加载中...
</div>
)}
{error && !loading && (
<Card className="border-red-500/30"><CardContent className="p-6 text-center dg-text-red text-sm">{error}</CardContent></Card>
)}
{!loading && !error && targets.length === 0 && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-12 text-center">
<Database className="h-12 w-12 mx-auto mb-4 text-[var(--dg-text-muted)]" />
<p className="text-sm text-[var(--dg-text-muted)]">{t('monitor.middlewaremonitor.sbbe62a')}</p>
<div className="flex justify-center gap-2 mt-4">
<Button size="sm" onClick={handleSeedDefaults} disabled={seeding}>{t('monitor.middlewaremonitor.se5caf1')}</Button>
<Button size="sm" variant="outline" onClick={() => openCreate()}>{t('monitor.middlewaremonitor.s63edd6')}</Button>
</div>
</CardContent>
</Card>
)}
{!loading && !error && targets.length > 0 && (
<>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{[
{ label: t('monitor.middlewaremonitor.s35a558'), value: targets.length, icon: Activity },
{ label: t('common.online'), value: onlineCount, icon: Database, color: 'dg-text-green' },
{ label: t('dashboard.stat.alerts'), value: warnCount, icon: Activity, color: warnCount > 0 ? 'dg-text-yellow' : 'dg-text-green' },
{ label: 'Redis', value: redisTargets.length, icon: Database, color: 'dg-text-red' },
{ label: 'MySQL', value: mysqlTargets.length, icon: Server, color: 'dg-text-blue' },
].map((item) => (
<Card key={item.label} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3 flex items-center gap-3">
<item.icon className={`h-4 w-4 ${item.color || 'text-[var(--dg-text-muted)]'}`} />
<div>
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
<p className={`text-lg font-bold ${item.color || ''}`}>{item.value}</p>
</div>
</CardContent>
</Card>
))}
</div>
<MwOverviewCharts targets={targets} />
<div className="flex gap-2">
{([
{ key: 'redis' as TabKey, label: 'Redis', count: redisTargets.length, icon: Database },
{ key: 'mysql' as TabKey, label: 'MySQL', count: mysqlTargets.length, icon: Server },
]).map((t) => (
<button
key={t.key}
type="button"
onClick={() => setTab(t.key)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
tab === t.key
? 'bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)]'
: 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]'
}`}
>
<t.icon className="h-4 w-4" />{t.label}
<span className="text-xs opacity-60">{t.count}</span>
</button>
))}
</div>
{currentTargets.length === 0 ? (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
暂无 {tab === 'redis' ? 'Redis' : 'MySQL'} 实例,
<button type="button" className="dg-text-cyan ml-1 underline" onClick={() => openCreate(tab)}>{t('monitor.middlewaremonitor.s8ec8a6')}</button>
</CardContent>
</Card>
) : (
<div className="flex flex-col xl:flex-row gap-4 items-start">
<div className="w-full xl:w-[min(260px,24%)] xl:shrink-0 space-y-2">
{currentTargets.map((t) => (
<InstanceCard
key={t.id}
target={t}
selected={selected?.id === t.id}
onSelect={() => setSelected(t)}
onEdit={() => { setEditTarget(t); setDialogOpen(true); }}
onDelete={() => handleDelete(t)}
onTest={() => handleTest(t)}
testing={testing === t.id}
/>
))}
</div>
<div className="flex-1 min-w-0 w-full">
<MwTargetChartsPanel
target={selected}
chartDefs={tab === 'redis' ? REDIS_CHARTS : MYSQL_CHARTS}
accent={tab}
/>
</div>
</div>
)}
</>
)}
<MiddlewareDialog open={dialogOpen} onOpenChange={setDialogOpen} editTarget={editTarget} onSuccess={() => loadTargets(true)} />
</div>
);
}