批量转换 163 个文件,3204 处中文替换为 t() 调用: - Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换 - Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换 - Deploy: 流水线/发布/凭证/RBAC 全部转换 - Server/Cloud/Agent/Task/Layout/UI: 全部转换 - 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys) - tsc 编译零错误
289 lines
13 KiB
TypeScript
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>
|
|
);
|
|
} |