feat(i18n): 监控大屏 GrafanaDashboard 全量国际化 — 108 翻译键 (monitor.dashboard.*) + 4 组件子Tab全量迁移
- GrafanaDashboard.tsx (640L) 全量迁移: AgentTab/ZabbixTab/PrometheusTab/DashboardTab 四个组件
- 新增 monitor.dashboard.* 命名空间 (108 键): tab/agent/batch/confirm/creds/warn/status/col/pagination/zabbix/prometheus/custom
- 硬编码中文 UI 文案 → t() 翻译键: Tab标签/工具栏/批量部署对话框/确认框/toast消息/表格列头/状态Badge/凭据表单/警告文案/分页/空状态
- TABS 常量改 getTabs(t) 函数模式(支持语言切换实时更新)
- zh-CN/en-US 完全对齐: 4201 键
- tsc -b=0, npm build ✅ (19.77s), ESLint any=37 unused=0, 17/17 集成测试通过
This commit is contained in:
1 parent
be2cff5745
commit
0f0f2770e1
4 files changed
+386
-101
No files matched your search
@@ -267,6 +267,13 @@ services/api.ts (1324L)
|
||||
|
||||
**问题**: 平台无国际化支持,所有 UI 文案硬编码为中文,无法切换语言
|
||||
|
||||
**增量迁移进展** — 持续推进页面国际化:
|
||||
|
||||
| 页面 | 新增键 | 组件 |
|
||||
|------|--------|------|
|
||||
| Dashboard/ServerManager/AlertCenter/Approval/安全审计/Deploy/Kubernetes/Cloud/Task/Monitor | 3800+ | 已迁移 |
|
||||
| **监控大屏 (GrafanaDashboard)** | 108 (monitor.dashboard.*) | GrafanaDashboard.tsx (AgentTab/ZabbixTab/PrometheusTab/DashboardTab) |
|
||||
|
||||
**已完成 (2026-06-27)**:
|
||||
- ✅ **i18n 基础设施搭建** — 安装 `i18next` + `react-i18next`,配置翻译实例 (`src/i18n/index.ts`),支持 `zh-CN` / `en-US` 双语
|
||||
- ✅ **翻译文件** — `src/locales/zh-CN.json`(中文基础)+ `src/locales/en-US.json`(英文翻译),覆盖 5 大命名空间 (common/menu/header/login/dashboard),120+ 翻译键
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus + 自定义仪表盘 // Logan.Li
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
|
||||
Monitor,
|
||||
@@ -52,11 +53,11 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
|
||||
// ═══════════ Tab 配置 ═══════════
|
||||
type TabId = 'agent' | 'zabbix' | 'prometheus' | 'dashboard';
|
||||
const TABS: { id: TabId; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: 'agent', label: 'Agent 主机', icon: <Server className="w-3.5 h-3.5" /> },
|
||||
const getTabs = (t: (key: string) => string): { id: TabId; label: string; icon: React.ReactNode }[] => [
|
||||
{ id: 'agent', label: t('monitor.dashboard.tab.agent'), icon: <Server className="w-3.5 h-3.5" /> },
|
||||
{ id: 'zabbix', label: 'Zabbix', icon: <Activity className="w-3.5 h-3.5" /> },
|
||||
{ id: 'prometheus', label: 'Prometheus', icon: <Activity className="w-3.5 h-3.5" /> },
|
||||
{ id: 'dashboard', label: '自定义', icon: <Monitor className="w-3.5 h-3.5" /> },
|
||||
{ id: 'dashboard', label: t('monitor.dashboard.tab.dashboard'), icon: <Monitor className="w-3.5 h-3.5" /> },
|
||||
];
|
||||
|
||||
// ═══════════ Agent 主机监控表格 ═══════════
|
||||
@@ -64,6 +65,7 @@ interface AgentHost { server_id: number; hostname: string; ip: string; cpu_usage
|
||||
type SortField = 'hostname' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'mem_usage' | 'disk_usage' | 'region' | 'ip';
|
||||
|
||||
const AgentTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const _confirm = useConfirm();
|
||||
const navigate = useNavigate();
|
||||
const [hosts, setHosts] = useState<AgentHost[]>([]);
|
||||
@@ -88,16 +90,16 @@ const AgentTab: React.FC = () => {
|
||||
// 批量卸载
|
||||
const [batchUninstalling, setBatchUninstalling] = useState(false);
|
||||
const handleBatchUninstall = async () => {
|
||||
const ok = await _confirm({ title: '批量卸载 Agent', description: `确定卸载选中的 ${selIds.length} 台主机的 AIOps Agent?卸载将在后台异步执行。`, confirmText: '卸载', variant: 'destructive' });
|
||||
const ok = await _confirm({ title: t('monitor.dashboard.confirm.uninstallTitle'), description: t('monitor.dashboard.confirm.uninstallDesc', { count: selIds.length }), confirmText: t('monitor.dashboard.confirm.uninstall'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
setBatchUninstalling(true);
|
||||
setOperating(new Set(selIds));
|
||||
try {
|
||||
await agentApi.batchUninstall(selIds);
|
||||
toast.success(`已提交:批量卸载 ${selIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
|
||||
toast.success(t('monitor.dashboard.agent.toast.batchUninstall', { count: selIds.length }));
|
||||
setSelected(new Set());
|
||||
setTimeout(() => { fetch(); setOperating(new Set()); }, 30000);
|
||||
} catch { toast.error('提交失败'); setOperating(new Set()); }
|
||||
} catch { toast.error(t('monitor.dashboard.agent.toast.submitFailed')); setOperating(new Set()); }
|
||||
finally { setBatchUninstalling(false); }
|
||||
};
|
||||
const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize));
|
||||
@@ -117,7 +119,7 @@ const AgentTab: React.FC = () => {
|
||||
return next;
|
||||
});
|
||||
} catch (e: any) {
|
||||
toast.error('刷新 Agent 主机列表失败:' + (e?.message || '未知错误'));
|
||||
toast.error(t('monitor.dashboard.agent.toast.refreshFailed', { msg: (e?.message || t('monitor.dashboard.agent.toast.unknownError')) }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -160,17 +162,17 @@ const AgentTab: React.FC = () => {
|
||||
|
||||
// 执行批量部署
|
||||
const handleBatchSubmit = async () => {
|
||||
if (!batchAck) { toast.error('请先确认并勾选「我已知悉」'); return; }
|
||||
if (!batchAck) { toast.error(t('monitor.dashboard.agent.toast.ackRequired')); return; }
|
||||
setBatchSubmitting(true);
|
||||
const token = useUserStore.getState().token;
|
||||
const ids = selectedHosts.map(h => h.server_id);
|
||||
setOperating(new Set(ids));
|
||||
try {
|
||||
if (batchMode === 'install') {
|
||||
if (!token) { toast.error('未登录'); return; }
|
||||
if (!token) { toast.error(t('monitor.dashboard.agent.toast.notLogin')); return; }
|
||||
// 安装:只安装无 Agent 的
|
||||
const installIds = needInstall.map(h => h.server_id);
|
||||
if (installIds.length === 0) { toast.warning('所选主机均已安装 Agent,无需安装'); return; }
|
||||
if (installIds.length === 0) { toast.warning(t('monitor.dashboard.agent.toast.allInstalled')); return; }
|
||||
// 为未保存凭据的主机构造临时凭据 map;后端遇到空字段不会覆盖已有凭据 // Logan.Li
|
||||
const credentials: Record<number, { username?: string; password?: string; ssh_key?: string; auth_type?: string }> = {};
|
||||
const needCreds = batchCreds.username && (batchCreds.authType === 'key' ? batchCreds.sshKey : batchCreds.password);
|
||||
@@ -185,63 +187,63 @@ const AgentTab: React.FC = () => {
|
||||
}
|
||||
}
|
||||
const res = await serverApi.batchInstallAgent(installIds, token, Object.keys(credentials).length ? credentials : undefined);
|
||||
toast.success(`已提交:批量安装 ${res?.total ?? installIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
|
||||
toast.success(t('monitor.dashboard.agent.toast.batchInstall', { count: res?.total ?? installIds.length }));
|
||||
setSelected(new Set());
|
||||
setTimeout(() => { fetch(); setOperating(new Set()); }, 30000);
|
||||
} else {
|
||||
// 升级:只升级在线的
|
||||
const upgradeIds = needUpgrade.map(h => h.server_id);
|
||||
if (upgradeIds.length === 0) { toast.warning('所选主机中没有在线 Agent,无法升级'); return; }
|
||||
if (upgradeIds.length === 0) { toast.warning(t('monitor.dashboard.agent.toast.noOnline')); return; }
|
||||
const res = await agentApi.batchUpgrade(upgradeIds);
|
||||
toast.success(res?.message || `已提交:批量升级 ${upgradeIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
|
||||
toast.success(res?.message || t('monitor.dashboard.agent.toast.batchUpgrade', { count: upgradeIds.length }));
|
||||
setSelected(new Set());
|
||||
setTimeout(() => { fetch(); setOperating(new Set()); }, 30000);
|
||||
}
|
||||
setBatchOpen(false);
|
||||
} catch { toast.error('提交失败'); setOperating(new Set()); }
|
||||
} catch { toast.error(t('monitor.dashboard.agent.toast.submitFailed')); setOperating(new Set()); }
|
||||
finally { setBatchSubmitting(false); }
|
||||
};
|
||||
|
||||
if (loading) return <PageLoading label="加载 Agent 主机列表..." />;
|
||||
if (loading) return <PageLoading label={t('monitor.dashboard.agent.loadingHosts')} />;
|
||||
if (!hosts.length) return (
|
||||
<PageEmpty
|
||||
icon={<Server className="h-8 w-8" />}
|
||||
title="暂无服务器资产"
|
||||
description="请先在「服务器管理」中添加主机,然后在此安装 Agent"
|
||||
title={t('monitor.dashboard.agent.emptyTitle')}
|
||||
description={t('monitor.dashboard.agent.emptyDesc')}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{/* 工具栏 */}
|
||||
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
||||
{selAny && <span className="text-xs text-[var(--dg-text-secondary)]">已选 {selected.size} 项</span>}
|
||||
{selAny && <span className="text-xs text-[var(--dg-text-secondary)]">{t('monitor.dashboard.agent.selected', { count: selected.size })}</span>}
|
||||
<div className="flex-1" />
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs gap-1" onClick={() => {
|
||||
const picked = selAny ? hosts.find(h => selected.has(String(h.server_id))) : null;
|
||||
setWizardServer(picked ? { id: picked.server_id, hostname: picked.hostname, ip: picked.ip, has_auth: picked.has_auth } : null);
|
||||
setWizardOpen(true);
|
||||
}}>
|
||||
<Rocket className="w-3 h-3" />安装 Agent
|
||||
<Rocket className="w-3 h-3" />{t('monitor.dashboard.agent.installAgent')}
|
||||
</Button>
|
||||
{selAny && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 dg-btn-secondary dg-text-blue" onClick={openBatchDialog}>
|
||||
<PackageOpen className="w-3 h-3" />批量部署 ({selected.size})
|
||||
<PackageOpen className="w-3 h-3" />{t('monitor.dashboard.agent.batchDeploy')} ({selected.size})
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-red-500/30 dg-text-red hover:bg-red-500/10" onClick={handleBatchUninstall} disabled={batchUninstalling}>
|
||||
<Trash2 className="w-3 h-3" />批量卸载 ({selected.size})
|
||||
<Trash2 className="w-3 h-3" />{t('monitor.dashboard.agent.batchUninstall')} ({selected.size})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button size="sm" variant="ghost" className="h-7 text-xs gap-1" onClick={fetch} disabled={refreshing}>
|
||||
<RefreshCw className={cn('w-3 h-3', refreshing && 'animate-spin')} />刷新
|
||||
<RefreshCw className={cn('w-3 h-3', refreshing && 'animate-spin')} />{t('monitor.dashboard.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AgentWizard open={wizardOpen} onOpenChange={setWizardOpen} server={wizardServer}
|
||||
onStart={(id) => { setOperating(prev => new Set(prev).add(id)); }}
|
||||
onInstalled={() => { toast.success('Agent 安装任务已提交,后台执行中…'); setWizardOpen(false); fetch(); }}
|
||||
onFailed={(id) => { toast.error('安装失败'); setOperating(prev => { const next = new Set(prev); next.delete(id); return next; }); }} />
|
||||
onInstalled={() => { toast.success(t('monitor.dashboard.agent.toast.installed')); setWizardOpen(false); fetch(); }}
|
||||
onFailed={(id) => { toast.error(t('monitor.dashboard.agent.toast.installFailed')); setOperating(prev => { const next = new Set(prev); next.delete(id); return next; }); }} />
|
||||
|
||||
{/* ────────── 批量部署 Agent 对话框 ────────── */}
|
||||
<Dialog open={batchOpen} onOpenChange={(v) => { if (!v) { setBatchOpen(false); setBatchAck(false); setBatchCreds({ username: 'root', password: '', sshKey: '', authType: 'password' }); } }}>
|
||||
@@ -249,10 +251,10 @@ const AgentTab: React.FC = () => {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<PackageOpen className="w-5 h-5 dg-text-blue" />
|
||||
批量部署 Agent
|
||||
{t('monitor.dashboard.batch.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="dg-text-secondary text-xs">
|
||||
选中 {selectedHosts.length} 台主机(需安装: {needInstall.length},待升级: {needUpgrade.length})
|
||||
{t('monitor.dashboard.batch.summary', { selected: selectedHosts.length, install: needInstall.length, upgrade: needUpgrade.length })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -268,7 +270,7 @@ const AgentTab: React.FC = () => {
|
||||
batchMode === m ? 'dg-btn-primary text-white shadow-sm' : 'dg-text-secondary hover:text-[var(--dg-text-primary)]'
|
||||
}`}
|
||||
>
|
||||
{m === 'install' ? `⚡ 安装 (${needInstall.length})` : `🔄 升级到最新版 (${needUpgrade.length})`}
|
||||
{m === 'install' ? t('monitor.dashboard.batch.install', { count: needInstall.length }) : t('monitor.dashboard.batch.upgrade', { count: needUpgrade.length })}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -278,11 +280,11 @@ const AgentTab: React.FC = () => {
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">主机名</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">{t('monitor.dashboard.col.hostname')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">IP</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">状态</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">当前版本</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">操作</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">{t('monitor.dashboard.col.status')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">{t('monitor.dashboard.col.version')}</th>
|
||||
<th className="px-3 py-2 text-left font-medium dg-text-secondary">{t('monitor.dashboard.col.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -292,15 +294,15 @@ const AgentTab: React.FC = () => {
|
||||
<td className="px-3 py-2 font-mono dg-text-secondary">{h.ip}</td>
|
||||
<td className="px-3 py-2">
|
||||
{h.agent_status === 'online'
|
||||
? <Badge className="text-[10px] bg-emerald-500/20 dg-text-green">在线</Badge>
|
||||
? <Badge className="text-[10px] bg-emerald-500/20 dg-text-green">{t('monitor.dashboard.status.online')}</Badge>
|
||||
: <Badge className="text-[10px] bg-[var(--dg-bg-active)]/20 dg-text-secondary">
|
||||
{h.agent_status === 'not_installed' ? '未安装' : h.agent_status || '离线'}
|
||||
{h.agent_status === 'not_installed' ? t('monitor.dashboard.status.notInstalled') : h.agent_status || t('monitor.dashboard.status.offline')}
|
||||
</Badge>}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-mono dg-text-secondary">{h.agent_version || '—'}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className="dg-text-blue text-[11px]">
|
||||
{batchMode === 'install' ? '即将安装' : '即将升级'}
|
||||
{batchMode === 'install' ? t('monitor.dashboard.batch.willInstall') : t('monitor.dashboard.batch.willUpgrade')}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -314,34 +316,34 @@ const AgentTab: React.FC = () => {
|
||||
<div className="rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/50 p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="w-4 h-4 dg-text-blue shrink-0" />
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">以下主机未保存 SSH 凭据,请提供临时凭据用于批量安装(仅本次使用):</span>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)]">{t('monitor.dashboard.creds.hint')}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">认证方式</Label>
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">{t('monitor.dashboard.creds.authType')}</Label>
|
||||
<Select value={batchCreds.authType} onValueChange={(v: 'password' | 'key') => setBatchCreds(c => ({ ...c, authType: v }))}>
|
||||
<SelectTrigger className="h-8 text-xs dg-input text-[var(--dg-text-primary)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input text-[var(--dg-text-primary)]">
|
||||
<SelectItem value="password">密码</SelectItem>
|
||||
<SelectItem value="key">SSH 私钥</SelectItem>
|
||||
<SelectItem value="password">{t('monitor.dashboard.creds.password')}</SelectItem>
|
||||
<SelectItem value="key">{t('monitor.dashboard.creds.sshKey')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">用户名</Label>
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">{t('monitor.dashboard.creds.username')}</Label>
|
||||
<Input value={batchCreds.username} onChange={e => setBatchCreds(c => ({ ...c, username: e.target.value }))} placeholder="root" className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
{batchCreds.authType === 'password' ? (
|
||||
<div>
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">密码</Label>
|
||||
<Input type="password" value={batchCreds.password} onChange={e => setBatchCreds(c => ({ ...c, password: e.target.value }))} placeholder="输入 SSH 密码" className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" />
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">{t('monitor.dashboard.creds.password')}</Label>
|
||||
<Input type="password" value={batchCreds.password} onChange={e => setBatchCreds(c => ({ ...c, password: e.target.value }))} placeholder={t('monitor.dashboard.creds.passwordPlaceholder')} className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">SSH 私钥</Label>
|
||||
<Label className="text-[11px] dg-text-secondary mb-1 block">{t('monitor.dashboard.creds.sshKey')}</Label>
|
||||
<textarea value={batchCreds.sshKey} onChange={e => setBatchCreds(c => ({ ...c, sshKey: e.target.value }))} placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" className="w-full h-24 rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-[var(--dg-text-primary)] text-xs p-2 resize-none focus:outline-none focus:ring-1 focus:ring-[var(--dg-blue)]" />
|
||||
</div>
|
||||
)}
|
||||
@@ -354,31 +356,30 @@ const AgentTab: React.FC = () => {
|
||||
<Info className="w-4 h-4 dg-text-yellow mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-amber-300 mb-1">
|
||||
{batchMode === 'install' ? '本次安装' : '本次升级'},主机监控探针将部署 AIOps Agent。AIOps Agent 是 AIOps 平台推出的新一代数据采集器,提供主机监控指标采集和可观测数据采集能力。
|
||||
{(batchMode === 'install' ? t('monitor.dashboard.warn.thisInstall') : t('monitor.dashboard.warn.thisUpgrade')) + t('monitor.dashboard.warn.intro')}
|
||||
</p>
|
||||
<p className="text-[11px] dg-text-yellow/80 leading-relaxed">
|
||||
{batchMode === 'upgrade' && '升级前请注意以下事项:'}
|
||||
{batchMode === 'install' && '安装前请注意以下事项:'}
|
||||
{batchMode === 'upgrade' ? t('monitor.dashboard.warn.upgradeNotice') : t('monitor.dashboard.warn.installNotice')}
|
||||
</p>
|
||||
<ul className="mt-1 space-y-0.5 text-[11px] dg-text-secondary list-disc list-inside">
|
||||
{batchMode === 'upgrade' && (
|
||||
<>
|
||||
<li>如果您的机器已经安装旧版 Agent,升级操作将停止旧版并替换为最新版本。</li>
|
||||
<li>如果您的机器之前安装过 Agent 但已卸载,遗留有相关采集配置,本次升级可能导致配置重新生效。升级前请确保存量废弃配置已经移除。</li>
|
||||
<li>如果您的机器采用自定义安装的 Agent(非 AIOps 提供的一键安装脚本),请不要直接使用批量升级。</li>
|
||||
<li>批量升级前,请务必先单机灰度升级,确保升级成功且不影响业务后再操作。</li>
|
||||
<li>{t('monitor.dashboard.warn.u1')}</li>
|
||||
<li>{t('monitor.dashboard.warn.u2')}</li>
|
||||
<li>{t('monitor.dashboard.warn.u3')}</li>
|
||||
<li>{t('monitor.dashboard.warn.u4')}</li>
|
||||
</>
|
||||
)}
|
||||
{batchMode === 'install' && (
|
||||
<>
|
||||
<li>安装将自动检测目标主机架构(amd64/arm64),下载对应版本的 Agent 二进制并启动服务。</li>
|
||||
<li>请确保目标主机的 SSH 端口(默认 22)可访问,且已关联有效的 SSH 凭据。</li>
|
||||
<li>安装过程约 30-60 秒,期间目标主机 CPU 负载会短暂升高。</li>
|
||||
<li>若安装失败,请检查目标主机的防火墙规则和 SSH 服务状态后重试。</li>
|
||||
<li>{t('monitor.dashboard.warn.i1')}</li>
|
||||
<li>{t('monitor.dashboard.warn.i2')}</li>
|
||||
<li>{t('monitor.dashboard.warn.i3')}</li>
|
||||
<li>{t('monitor.dashboard.warn.i4')}</li>
|
||||
</>
|
||||
)}
|
||||
<li>操作不可回滚,请在操作前确认已备份关键配置。</li>
|
||||
<li>操作将在后台异步执行,您可以在「任务执行记录」中查看详细进度。</li>
|
||||
<li>{t('monitor.dashboard.warn.c1')}</li>
|
||||
<li>{t('monitor.dashboard.warn.c2')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,13 +388,13 @@ const AgentTab: React.FC = () => {
|
||||
{/* 我已知悉 勾选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="batch-ack" checked={batchAck} onCheckedChange={(c) => setBatchAck(!!c)} />
|
||||
<label htmlFor="batch-ack" className="text-xs text-[var(--dg-text-secondary)] cursor-pointer select-none">我已知悉以上风险和注意事项</label>
|
||||
<label htmlFor="batch-ack" className="text-xs text-[var(--dg-text-secondary)] cursor-pointer select-none">{t('monitor.dashboard.batch.ack')}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] h-8 text-xs" onClick={() => { setBatchOpen(false); setBatchAck(false); }}>
|
||||
取消
|
||||
{t('monitor.dashboard.batch.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="dg-btn-primary h-8 text-xs gap-1"
|
||||
@@ -401,7 +402,7 @@ const AgentTab: React.FC = () => {
|
||||
onClick={handleBatchSubmit}
|
||||
>
|
||||
{batchSubmitting && <Loader2 className="w-3 h-3 animate-spin" />}
|
||||
{batchMode === 'install' ? '确认安装' : '确认升级'}
|
||||
{batchMode === 'install' ? t('monitor.dashboard.batch.confirmInstall') : t('monitor.dashboard.batch.confirmUpgrade')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -415,10 +416,10 @@ const AgentTab: React.FC = () => {
|
||||
</th>
|
||||
{['hostname','agent_version','agent_status','cpu_usage','mem_usage','disk_usage','region','ip'].map(f => (
|
||||
<th key={f} className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] cursor-pointer hover:text-[var(--dg-text-primary)] select-none whitespace-nowrap" onClick={() => toggle(f as SortField)}>
|
||||
<div className="flex items-center gap-1">{{hostname:'主机名',agent_version:'Agent版本',agent_status:'状态',cpu_usage:'CPU',mem_usage:'内存',disk_usage:'磁盘',region:'区域',ip:'IP'}[f]}<SI f={f as SortField} /></div>
|
||||
<div className="flex items-center gap-1">{{hostname:t('monitor.dashboard.col.hostname'),agent_version:t('monitor.dashboard.col.agentVersion'),agent_status:t('monitor.dashboard.col.status'),cpu_usage:t('monitor.dashboard.col.cpu'),mem_usage:t('monitor.dashboard.col.memory'),disk_usage:t('monitor.dashboard.col.disk'),region:t('monitor.dashboard.col.region'),ip:'IP'}[f]}<SI f={f as SortField} /></div>
|
||||
</th>
|
||||
))}
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] whitespace-nowrap">操作</th></tr></thead>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] whitespace-nowrap">{t('monitor.dashboard.col.action')}</th></tr></thead>
|
||||
<tbody>
|
||||
{paged.map((h, i) => (
|
||||
<tr key={h.hostname + i} className="border-b border-[var(--dg-border)] last:border-0 hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
@@ -434,13 +435,13 @@ const AgentTab: React.FC = () => {
|
||||
{operating.has(h.server_id) ? (
|
||||
<span className="inline-flex items-center gap-1 dg-text-blue">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span className="text-[10px]">部署中</span>
|
||||
<span className="text-[10px]">{t('monitor.dashboard.agent.deploying')}</span>
|
||||
</span>
|
||||
) : (
|
||||
h.agent_version || '—'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5">{h.agent_status==='online'?<Badge className="text-[10px] bg-emerald-500/20 dg-text-green"><Wifi className="w-3 h-3 mr-1"/>在线</Badge>:h.agent_status==='not_installed'?<Badge className="text-[10px] bg-amber-500/20 dg-text-yellow">未安装</Badge>:<Badge className="text-[10px] bg-[var(--dg-bg-active)]/10 dg-text-secondary"><WifiOff className="w-3 h-3 mr-1"/>离线</Badge>}</td>
|
||||
<td className="px-3 py-2.5">{h.agent_status==='online'?<Badge className="text-[10px] bg-emerald-500/20 dg-text-green"><Wifi className="w-3 h-3 mr-1"/>{t('monitor.dashboard.status.online')}</Badge>:h.agent_status==='not_installed'?<Badge className="text-[10px] bg-amber-500/20 dg-text-yellow">{t('monitor.dashboard.status.notInstalled')}</Badge>:<Badge className="text-[10px] bg-[var(--dg-bg-active)]/10 dg-text-secondary"><WifiOff className="w-3 h-3 mr-1"/>{t('monitor.dashboard.status.offline')}</Badge>}</td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.cpu_usage||0), `${Number(h.cpu_usage||0).toFixed(1)}%`)}</td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.mem_usage||0), `${Number(h.mem_usage||0).toFixed(1)}%`)}</td>
|
||||
<td className="px-3 py-2.5">{bar(Number(h.disk_usage||0), `${Number(h.disk_usage||0).toFixed(1)}%`)}</td>
|
||||
@@ -449,7 +450,7 @@ const AgentTab: React.FC = () => {
|
||||
<td className="px-3 py-2.5">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-blue hover:bg-[var(--dg-bg-hover)] px-2"
|
||||
onClick={() => navigate(`/host-monitor?host=${encodeURIComponent(h.hostname)}`)}>
|
||||
<BarChart3 className="w-3.5 h-3.5 mr-1" />图表
|
||||
<BarChart3 className="w-3.5 h-3.5 mr-1" />{t('monitor.dashboard.agent.chart')}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -459,17 +460,17 @@ const AgentTab: React.FC = () => {
|
||||
{hosts.length > pageSize && (
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-xs">
|
||||
<div className="flex items-center gap-2 text-[var(--dg-text-muted)]">
|
||||
<span>共 {hosts.length} 条</span>
|
||||
<span>{t('monitor.dashboard.pagination.total', { count: hosts.length })}</span>
|
||||
<select className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded px-2 py-0.5 text-xs" value={pageSize} onChange={e => { setPageSize(Number(e.target.value)); setPage(1); }}>
|
||||
<option value={10}>10/页</option>
|
||||
<option value={20}>20/页</option>
|
||||
<option value={30}>30/页</option>
|
||||
<option value={10}>{`10/${t('monitor.dashboard.pagination.perPage')}`}</option>
|
||||
<option value={20}>{`20/${t('monitor.dashboard.pagination.perPage')}`}</option>
|
||||
<option value={30}>{`30/${t('monitor.dashboard.pagination.perPage')}`}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={page <= 1} onClick={() => setPage(p => p - 1)}>上一页</button>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={page <= 1} onClick={() => setPage(p => p - 1)}>{t('monitor.dashboard.pagination.prev')}</button>
|
||||
<span className="px-2 text-[var(--dg-text-primary)]">{page} / {totalPages}</span>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>下一页</button>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>{t('monitor.dashboard.pagination.next')}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -480,6 +481,7 @@ const AgentTab: React.FC = () => {
|
||||
|
||||
// ═══════════ 数据源 Wrapper ═══════════
|
||||
const ZabbixTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [ds, setDs] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
@@ -488,18 +490,19 @@ const ZabbixTab: React.FC = () => {
|
||||
setDs(list.find(( d: any) => d.is_default) || list[0] || null);
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
}, []);
|
||||
if (loading) return <PageLoading label="加载 Zabbix 数据源..." />;
|
||||
if (loading) return <PageLoading label={t('monitor.dashboard.zabbix.loading')} />;
|
||||
if (!ds) return (
|
||||
<PageEmpty
|
||||
icon={<Database className="h-8 w-8" />}
|
||||
title="未配置 Zabbix 数据源"
|
||||
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a>}
|
||||
title={t('monitor.dashboard.zabbix.emptyTitle')}
|
||||
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">{t('monitor.dashboard.zabbix.goConfig')} <ExternalLink className="w-3 h-3" /></a>}
|
||||
/>
|
||||
);
|
||||
return <ZabbixDashboard dsUrl={ds.url} dsUsername={ds.username} dsPassword={ds.password} />;
|
||||
};
|
||||
|
||||
const PrometheusTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [ds, setDs] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
@@ -508,12 +511,12 @@ const PrometheusTab: React.FC = () => {
|
||||
setDs(list.find(( d: any) => d.is_default) || list[0] || null);
|
||||
}).catch(() => {}).finally(() => setLoading(false));
|
||||
}, []);
|
||||
if (loading) return <PageLoading label="加载 Prometheus 数据源..." />;
|
||||
if (loading) return <PageLoading label={t('monitor.dashboard.prometheus.loading')} />;
|
||||
if (!ds) return (
|
||||
<PageEmpty
|
||||
icon={<Database className="h-8 w-8" />}
|
||||
title="未配置 Prometheus 数据源"
|
||||
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a>}
|
||||
title={t('monitor.dashboard.prometheus.emptyTitle')}
|
||||
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">{t('monitor.dashboard.zabbix.goConfig')} <ExternalLink className="w-3 h-3" /></a>}
|
||||
/>
|
||||
);
|
||||
return <PrometheusDashboard dsUrl={ds.url} />;
|
||||
@@ -524,6 +527,7 @@ interface Dashboard { id: string; name: string; description: string; datasource:
|
||||
export interface Panel { id: string; title: string; type: string; query: string; datasource: string; width: number; height: number; x: number; y: number; options?: any; }
|
||||
|
||||
const DashboardTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
|
||||
const [active, setActive] = useState<Dashboard | null>(null);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
@@ -540,7 +544,7 @@ const DashboardTab: React.FC = () => {
|
||||
setDashboards(list);
|
||||
if (list.length > 0 && !active) setActive(list[0]);
|
||||
} catch (e: any) {
|
||||
toast.error('刷新自定义仪表盘失败:' + (e?.message || '未知错误'));
|
||||
toast.error(t('monitor.dashboard.custom.toastRefreshFailed', { msg: (e?.message || t('monitor.dashboard.agent.toast.unknownError')) }));
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
@@ -548,35 +552,35 @@ const DashboardTab: React.FC = () => {
|
||||
useEffect(() => { fetch(); }, []);
|
||||
|
||||
const create = async () => {
|
||||
if (!nd.name) { toast.error('请输入名称'); return; }
|
||||
try { await dashboardApi.createDashboard({ ...nd, panels: [] }); toast.success('创建成功'); setCreateOpen(false); setNd({ name: '', description: '', datasource: 'prometheus', datasourceUrl: '' }); fetch(); } catch { toast.error('创建失败'); }
|
||||
if (!nd.name) { toast.error(t('monitor.dashboard.custom.toastNameRequired')); return; }
|
||||
try { await dashboardApi.createDashboard({ ...nd, panels: [] }); toast.success(t('monitor.dashboard.custom.toastCreateSuccess')); setCreateOpen(false); setNd({ name: '', description: '', datasource: 'prometheus', datasourceUrl: '' }); fetch(); } catch { toast.error(t('monitor.dashboard.custom.toastCreateFailed')); }
|
||||
};
|
||||
const addPanel = async () => {
|
||||
if (!np.title || !np.query || !active) return;
|
||||
try {
|
||||
const p: Panel = { id: Date.now().toString(), ...np, datasource: active.datasource, x: 0, y: 0, options: { legend: true, tooltip: true, refreshInterval: 30 } };
|
||||
await dashboardApi.updateDashboard(active.id, { ...active, panels: [...active.panels, p] });
|
||||
toast.success('面板添加成功'); setAddOpen(false); setNp({ title: '', type: 'graph', query: '', width: 6, height: 4 }); fetch();
|
||||
} catch { toast.error('添加失败'); }
|
||||
toast.success(t('monitor.dashboard.custom.toastPanelAdded')); setAddOpen(false); setNp({ title: '', type: 'graph', query: '', width: 6, height: 4 }); fetch();
|
||||
} catch { toast.error(t('monitor.dashboard.custom.toastPanelFailed')); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={active?.id} onValueChange={id => { const d = dashboards.find(x => x.id === id); if (d) setActive(d); }}>
|
||||
<SelectTrigger className="w-[240px] h-8 text-xs"><SelectValue placeholder="选择仪表盘" /></SelectTrigger>
|
||||
<SelectContent>{dashboards.map(d => <SelectItem key={d.id} value={d.id}>{d.name}{d.isDefault ? ' (默认)' : ''}</SelectItem>)}</SelectContent>
|
||||
<SelectTrigger className="w-[240px] h-8 text-xs"><SelectValue placeholder={t('monitor.dashboard.custom.selectDashboard')} /></SelectTrigger>
|
||||
<SelectContent>{dashboards.map(d => <SelectItem key={d.id} value={d.id}>{d.name}{d.isDefault ? ` ${t('monitor.dashboard.custom.defaultTag')}` : ''}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={fetch} disabled={refreshing}>
|
||||
<RefreshCw className={cn('w-3 h-3 mr-1', refreshing && 'animate-spin')} />刷新
|
||||
<RefreshCw className={cn('w-3 h-3 mr-1', refreshing && 'animate-spin')} />{t('monitor.dashboard.refresh')}
|
||||
</Button>
|
||||
{active && <Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setAddOpen(true)}><Plus className="w-3 h-3 mr-1" />添加面板</Button>}
|
||||
<Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setCreateOpen(true)}><Plus className="w-3 h-3 mr-1" />新建</Button>
|
||||
{active && <Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setAddOpen(true)}><Plus className="w-3 h-3 mr-1" />{t('monitor.dashboard.custom.addPanel')}</Button>}
|
||||
<Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setCreateOpen(true)}><Plus className="w-3 h-3 mr-1" />{t('monitor.dashboard.custom.create')}</Button>
|
||||
</div>
|
||||
|
||||
{active ? (
|
||||
active.panels.length === 0 ? (
|
||||
<PageEmpty icon={<BarChart3 className="w-10 h-10" />} title="暂无面板" className="py-16" />
|
||||
<PageEmpty icon={<BarChart3 className="w-10 h-10" />} title={t('monitor.dashboard.custom.emptyNoPanels')} className="py-16" />
|
||||
) : (
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
{active.panels.map(p => (
|
||||
@@ -588,35 +592,37 @@ const DashboardTab: React.FC = () => {
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<PageEmpty icon={<Monitor className="w-10 h-10" />} title="暂无仪表盘" className="py-16" />
|
||||
<PageEmpty icon={<Monitor className="w-10 h-10" />} title={t('monitor.dashboard.custom.emptyNoDashboards')} className="py-16" />
|
||||
)}
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}><DialogContent><DialogHeader><DialogTitle>新建仪表盘</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3"><div><Label>名称</Label><Input value={nd.name} onChange={e => setNd({ ...nd, name: e.target.value })} /></div>
|
||||
<div><Label>数据源</Label><Select value={nd.datasource} onValueChange={v => setNd({ ...nd, datasource: v as any })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="prometheus">Prometheus</SelectItem><SelectItem value="zabbix">Zabbix</SelectItem><SelectItem value="skywalking">SkyWalking</SelectItem><SelectItem value="custom">自定义</SelectItem></SelectContent></Select></div></div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setCreateOpen(false)}>取消</Button><Button onClick={create}>创建</Button></DialogFooter></DialogContent></Dialog>
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}><DialogContent><DialogHeader><DialogTitle>{t('monitor.dashboard.custom.createTitle')}</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3"><div><Label>{t('monitor.dashboard.custom.name')}</Label><Input value={nd.name} onChange={e => setNd({ ...nd, name: e.target.value })} /></div>
|
||||
<div><Label>{t('monitor.dashboard.custom.datasource')}</Label><Select value={nd.datasource} onValueChange={v => setNd({ ...nd, datasource: v as any })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="prometheus">Prometheus</SelectItem><SelectItem value="zabbix">Zabbix</SelectItem><SelectItem value="skywalking">SkyWalking</SelectItem><SelectItem value="custom">Custom</SelectItem></SelectContent></Select></div></div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setCreateOpen(false)}>{t('monitor.dashboard.custom.cancel')}</Button><Button onClick={create}>{t('monitor.dashboard.custom.createBtn')}</Button></DialogFooter></DialogContent></Dialog>
|
||||
|
||||
<Dialog open={addOpen} onOpenChange={setAddOpen}><DialogContent><DialogHeader><DialogTitle>添加面板</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3"><div><Label>标题</Label><Input value={np.title} onChange={e => setNp({ ...np, title: e.target.value })} /></div>
|
||||
<div><Label>类型</Label><Select value={np.type} onValueChange={v => setNp({ ...np, type: v as any })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="graph">折线图</SelectItem><SelectItem value="stat">数值</SelectItem><SelectItem value="gauge">仪表盘</SelectItem><SelectItem value="table">表格</SelectItem></SelectContent></Select></div>
|
||||
<div><Label>查询</Label><Input value={np.query} onChange={e => setNp({ ...np, query: e.target.value })} /></div></div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setAddOpen(false)}>取消</Button><Button onClick={addPanel}>添加</Button></DialogFooter></DialogContent></Dialog>
|
||||
<Dialog open={addOpen} onOpenChange={setAddOpen}><DialogContent><DialogHeader><DialogTitle>{t('monitor.dashboard.custom.addPanelTitle')}</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3"><div><Label>{t('monitor.dashboard.custom.panelTitle')}</Label><Input value={np.title} onChange={e => setNp({ ...np, title: e.target.value })} /></div>
|
||||
<div><Label>{t('monitor.dashboard.custom.panelType')}</Label><Select value={np.type} onValueChange={v => setNp({ ...np, type: v as any })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="graph">{t('monitor.dashboard.custom.typeGraph')}</SelectItem><SelectItem value="stat">{t('monitor.dashboard.custom.typeStat')}</SelectItem><SelectItem value="gauge">{t('monitor.dashboard.custom.typeGauge')}</SelectItem><SelectItem value="table">{t('monitor.dashboard.custom.typeTable')}</SelectItem></SelectContent></Select></div>
|
||||
<div><Label>{t('monitor.dashboard.custom.panelQuery')}</Label><Input value={np.query} onChange={e => setNp({ ...np, query: e.target.value })} /></div></div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setAddOpen(false)}>{t('monitor.dashboard.custom.cancel')}</Button><Button onClick={addPanel}>{t('monitor.dashboard.custom.addBtn')}</Button></DialogFooter></DialogContent></Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ═══════════ 主组件 ═══════════
|
||||
const GrafanaDashboard: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = useState<TabId>('agent');
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
|
||||
const pageTabs = TABS.map((item) => ({ id: item.id, label: item.label }));
|
||||
const tabs = getTabs(t);
|
||||
const pageTabs = tabs.map((item) => ({ id: item.id, label: item.label }));
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title="监控大屏"
|
||||
subtitle="Agent 主机、Zabbix、Prometheus 与自定义仪表盘"
|
||||
title={t('monitor.dashboard.title')}
|
||||
subtitle={t('monitor.dashboard.subtitle')}
|
||||
icon={<Monitor className="h-5 w-5" />}
|
||||
tabs={pageTabs}
|
||||
activeTab={tab}
|
||||
|
||||
@@ -2434,7 +2434,143 @@
|
||||
"totalItems": "{{count}} total",
|
||||
"minutesShort": "{{count}}m",
|
||||
"durationSeconds": "{{count}}s duration",
|
||||
"peopleCount": "{{count}} people"
|
||||
"peopleCount": "{{count}} people",
|
||||
"dashboard": {
|
||||
"title": "Monitoring Dashboard",
|
||||
"subtitle": "Agent hosts, Zabbix, Prometheus and custom dashboards",
|
||||
"tab": {
|
||||
"agent": "Agent Hosts",
|
||||
"dashboard": "Custom"
|
||||
},
|
||||
"refresh": "Refresh",
|
||||
"loading": "Loading...",
|
||||
"agent": {
|
||||
"loadingHosts": "Loading agent host list...",
|
||||
"emptyTitle": "No server assets",
|
||||
"emptyDesc": "Please add hosts in Server Management first, then install Agent here",
|
||||
"selected": "{{count}} selected",
|
||||
"installAgent": "Install Agent",
|
||||
"batchDeploy": "Batch Deploy",
|
||||
"batchUninstall": "Batch Uninstall",
|
||||
"deploying": "Deploying",
|
||||
"chart": "Chart",
|
||||
"toast": {
|
||||
"installed": "Agent installation submitted, running in background…",
|
||||
"installFailed": "Installation failed",
|
||||
"refreshFailed": "Failed to refresh agent host list: {{msg}}",
|
||||
"unknownError": "Unknown error",
|
||||
"submitFailed": "Submission failed",
|
||||
"batchUninstall": "Submitted: batch uninstalling Agent on {{count}} hosts, running in background, auto-refresh in 30s",
|
||||
"batchInstall": "Submitted: batch installing Agent on {{count}} hosts, running in background, auto-refresh in 30s",
|
||||
"batchUpgrade": "Submitted: batch upgrading Agent on {{count}} hosts, running in background, auto-refresh in 30s",
|
||||
"allInstalled": "All selected hosts already have Agent installed",
|
||||
"noOnline": "No online Agent among selected hosts, cannot upgrade",
|
||||
"ackRequired": "Please check \"I acknowledge\" first",
|
||||
"notLogin": "Not logged in"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"uninstallTitle": "Batch Uninstall Agent",
|
||||
"uninstallDesc": "Confirm uninstalling AIOps Agent on {{count}} selected hosts? Uninstall will run asynchronously in the background.",
|
||||
"uninstall": "Uninstall"
|
||||
},
|
||||
"batch": {
|
||||
"title": "Batch Deploy Agent",
|
||||
"summary": "{{selected}} hosts selected (need install: {{install}}, upgrade: {{upgrade}})",
|
||||
"install": "⚡ Install ({{count}})",
|
||||
"upgrade": "🔄 Upgrade to latest ({{count}})",
|
||||
"willInstall": "Will Install",
|
||||
"willUpgrade": "Will Upgrade",
|
||||
"ack": "I acknowledge the above risks and precautions",
|
||||
"cancel": "Cancel",
|
||||
"confirmInstall": "Confirm Install",
|
||||
"confirmUpgrade": "Confirm Upgrade"
|
||||
},
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"notInstalled": "Not Installed"
|
||||
},
|
||||
"col": {
|
||||
"hostname": "Hostname",
|
||||
"version": "Current Version",
|
||||
"status": "Status",
|
||||
"action": "Action",
|
||||
"agentVersion": "Agent Version",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memory",
|
||||
"disk": "Disk",
|
||||
"region": "Region"
|
||||
},
|
||||
"creds": {
|
||||
"hint": "The following hosts have no saved SSH credentials. Please provide temporary credentials for batch installation (this time only):",
|
||||
"authType": "Auth Method",
|
||||
"password": "Password",
|
||||
"sshKey": "SSH Private Key",
|
||||
"username": "Username",
|
||||
"passwordPlaceholder": "Enter SSH password"
|
||||
},
|
||||
"warn": {
|
||||
"thisInstall": "This installation",
|
||||
"thisUpgrade": "This upgrade",
|
||||
"intro": ", the host monitoring probe will deploy AIOps Agent. AIOps Agent is the next-generation data collector, providing host monitoring metric collection and observability data collection.",
|
||||
"upgradeNotice": "Please note the following before upgrading:",
|
||||
"installNotice": "Please note the following before installation:",
|
||||
"u1": "If your machine already has an older Agent installed, the upgrade will stop the old version and replace it with the latest.",
|
||||
"u2": "If your machine previously installed Agent but has been uninstalled, leftover collection configs may become active again. Please ensure obsolete configs are removed before upgrading.",
|
||||
"u3": "If your machine uses a custom-installed Agent (not the AIOps one-click install script), do not use batch upgrade directly.",
|
||||
"u4": "Before batch upgrading, please test single-machine canary upgrade first.",
|
||||
"i1": "Installation will auto-detect target host architecture (amd64/arm64), download the corresponding Agent binary and start the service.",
|
||||
"i2": "Please ensure the target host's SSH port (default 22) is accessible and valid SSH credentials are associated.",
|
||||
"i3": "Installation takes about 30-60 seconds, during which the target host's CPU load will briefly spike.",
|
||||
"i4": "If installation fails, check the target host's firewall rules and SSH service status, then retry.",
|
||||
"c1": "This operation is irreversible. Please ensure critical configs are backed up before proceeding.",
|
||||
"c2": "The operation will run asynchronously in the background. You can view detailed progress in Task Execution Records."
|
||||
},
|
||||
"pagination": {
|
||||
"total": "{{count}} total",
|
||||
"prev": "Previous",
|
||||
"next": "Next",
|
||||
"perPage": "page"
|
||||
},
|
||||
"zabbix": {
|
||||
"loading": "Loading Zabbix datasource...",
|
||||
"emptyTitle": "No Zabbix datasource configured",
|
||||
"goConfig": "Go to configure"
|
||||
},
|
||||
"prometheus": {
|
||||
"loading": "Loading Prometheus datasource...",
|
||||
"emptyTitle": "No Prometheus datasource configured"
|
||||
},
|
||||
"custom": {
|
||||
"selectDashboard": "Select dashboard",
|
||||
"defaultTag": "(Default)",
|
||||
"addPanel": "Add Panel",
|
||||
"create": "Create New",
|
||||
"emptyNoPanels": "No panels",
|
||||
"emptyNoDashboards": "No dashboards",
|
||||
"createTitle": "Create Dashboard",
|
||||
"name": "Name",
|
||||
"datasource": "Datasource",
|
||||
"cancel": "Cancel",
|
||||
"createBtn": "Create",
|
||||
"addPanelTitle": "Add Panel",
|
||||
"panelTitle": "Title",
|
||||
"panelType": "Type",
|
||||
"panelQuery": "Query",
|
||||
"typeGraph": "Line Chart",
|
||||
"typeStat": "Stat",
|
||||
"typeGauge": "Gauge",
|
||||
"typeTable": "Table",
|
||||
"addBtn": "Add",
|
||||
"toastRefreshFailed": "Failed to refresh custom dashboards: {{msg}}",
|
||||
"toastNameRequired": "Please enter a name",
|
||||
"toastCreateSuccess": "Created successfully",
|
||||
"toastCreateFailed": "Creation failed",
|
||||
"toastPanelAdded": "Panel added successfully",
|
||||
"toastPanelFailed": "Failed to add"
|
||||
}
|
||||
}
|
||||
},
|
||||
"task": {
|
||||
"cronJob": {
|
||||
|
||||
@@ -2434,7 +2434,143 @@
|
||||
"totalItems": "共 {{count}} 条",
|
||||
"minutesShort": "{{count}}分",
|
||||
"durationSeconds": "持续 {{count}}秒",
|
||||
"peopleCount": "{{count}} 人"
|
||||
"peopleCount": "{{count}} 人",
|
||||
"dashboard": {
|
||||
"title": "监控大屏",
|
||||
"subtitle": "Agent 主机、Zabbix、Prometheus 与自定义仪表盘",
|
||||
"tab": {
|
||||
"agent": "Agent 主机",
|
||||
"dashboard": "自定义"
|
||||
},
|
||||
"refresh": "刷新",
|
||||
"loading": "加载中...",
|
||||
"agent": {
|
||||
"loadingHosts": "加载 Agent 主机列表...",
|
||||
"emptyTitle": "暂无服务器资产",
|
||||
"emptyDesc": "请先在「服务器管理」中添加主机,然后在此安装 Agent",
|
||||
"selected": "已选 {{count}} 项",
|
||||
"installAgent": "安装 Agent",
|
||||
"batchDeploy": "批量部署",
|
||||
"batchUninstall": "批量卸载",
|
||||
"deploying": "部署中",
|
||||
"chart": "图表",
|
||||
"toast": {
|
||||
"installed": "Agent 安装任务已提交,后台执行中…",
|
||||
"installFailed": "安装失败",
|
||||
"refreshFailed": "刷新 Agent 主机列表失败:{{msg}}",
|
||||
"unknownError": "未知错误",
|
||||
"submitFailed": "提交失败",
|
||||
"batchUninstall": "已提交:批量卸载 {{count}} 台主机的 Agent,后台执行中,30秒后自动刷新",
|
||||
"batchInstall": "已提交:批量安装 {{count}} 台主机的 Agent,后台执行中,30秒后自动刷新",
|
||||
"batchUpgrade": "已提交:批量升级 {{count}} 台主机的 Agent,后台执行中,30秒后自动刷新",
|
||||
"allInstalled": "所选主机均已安装 Agent,无需安装",
|
||||
"noOnline": "所选主机中没有在线 Agent,无法升级",
|
||||
"ackRequired": "请先确认并勾选「我已知悉」",
|
||||
"notLogin": "未登录"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"uninstallTitle": "批量卸载 Agent",
|
||||
"uninstallDesc": "确定卸载选中的 {{count}} 台主机的 AIOps Agent?卸载将在后台异步执行。",
|
||||
"uninstall": "卸载"
|
||||
},
|
||||
"batch": {
|
||||
"title": "批量部署 Agent",
|
||||
"summary": "选中 {{selected}} 台主机(需安装: {{install}},待升级: {{upgrade}})",
|
||||
"install": "⚡ 安装 ({{count}})",
|
||||
"upgrade": "🔄 升级到最新版 ({{count}})",
|
||||
"willInstall": "即将安装",
|
||||
"willUpgrade": "即将升级",
|
||||
"ack": "我已知悉以上风险和注意事项",
|
||||
"cancel": "取消",
|
||||
"confirmInstall": "确认安装",
|
||||
"confirmUpgrade": "确认升级"
|
||||
},
|
||||
"status": {
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"notInstalled": "未安装"
|
||||
},
|
||||
"col": {
|
||||
"hostname": "主机名",
|
||||
"version": "当前版本",
|
||||
"status": "状态",
|
||||
"action": "操作",
|
||||
"agentVersion": "Agent版本",
|
||||
"cpu": "CPU",
|
||||
"memory": "内存",
|
||||
"disk": "磁盘",
|
||||
"region": "区域"
|
||||
},
|
||||
"creds": {
|
||||
"hint": "以下主机未保存 SSH 凭据,请提供临时凭据用于批量安装(仅本次使用):",
|
||||
"authType": "认证方式",
|
||||
"password": "密码",
|
||||
"sshKey": "SSH 私钥",
|
||||
"username": "用户名",
|
||||
"passwordPlaceholder": "输入 SSH 密码"
|
||||
},
|
||||
"warn": {
|
||||
"thisInstall": "本次安装",
|
||||
"thisUpgrade": "本次升级",
|
||||
"intro": ",主机监控探针将部署 AIOps Agent。AIOps Agent 是 AIOps 平台推出的新一代数据采集器,提供主机监控指标采集和可观测数据采集能力。",
|
||||
"upgradeNotice": "升级前请注意以下事项:",
|
||||
"installNotice": "安装前请注意以下事项:",
|
||||
"u1": "如果您的机器已经安装旧版 Agent,升级操作将停止旧版并替换为最新版本。",
|
||||
"u2": "如果您的机器之前安装过 Agent 但已卸载,遗留有相关采集配置,本次升级可能导致配置重新生效。升级前请确保存量废弃配置已经移除。",
|
||||
"u3": "如果您的机器采用自定义安装的 Agent(非 AIOps 提供的一键安装脚本),请不要直接使用批量升级。",
|
||||
"u4": "批量升级前,请务必先单机灰度升级,确保升级成功且不影响业务后再操作。",
|
||||
"i1": "安装将自动检测目标主机架构(amd64/arm64),下载对应版本的 Agent 二进制并启动服务。",
|
||||
"i2": "请确保目标主机的 SSH 端口(默认 22)可访问,且已关联有效的 SSH 凭据。",
|
||||
"i3": "安装过程约 30-60 秒,期间目标主机 CPU 负载会短暂升高。",
|
||||
"i4": "若安装失败,请检查目标主机的防火墙规则和 SSH 服务状态后重试。",
|
||||
"c1": "操作不可回滚,请在操作前确认已备份关键配置。",
|
||||
"c2": "操作将在后台异步执行,您可以在「任务执行记录」中查看详细进度。"
|
||||
},
|
||||
"pagination": {
|
||||
"total": "共 {{count}} 条",
|
||||
"prev": "上一页",
|
||||
"next": "下一页",
|
||||
"perPage": "页"
|
||||
},
|
||||
"zabbix": {
|
||||
"loading": "加载 Zabbix 数据源...",
|
||||
"emptyTitle": "未配置 Zabbix 数据源",
|
||||
"goConfig": "前往配置"
|
||||
},
|
||||
"prometheus": {
|
||||
"loading": "加载 Prometheus 数据源...",
|
||||
"emptyTitle": "未配置 Prometheus 数据源"
|
||||
},
|
||||
"custom": {
|
||||
"selectDashboard": "选择仪表盘",
|
||||
"defaultTag": "(默认)",
|
||||
"addPanel": "添加面板",
|
||||
"create": "新建",
|
||||
"emptyNoPanels": "暂无面板",
|
||||
"emptyNoDashboards": "暂无仪表盘",
|
||||
"createTitle": "新建仪表盘",
|
||||
"name": "名称",
|
||||
"datasource": "数据源",
|
||||
"cancel": "取消",
|
||||
"createBtn": "创建",
|
||||
"addPanelTitle": "添加面板",
|
||||
"panelTitle": "标题",
|
||||
"panelType": "类型",
|
||||
"panelQuery": "查询",
|
||||
"typeGraph": "折线图",
|
||||
"typeStat": "数值",
|
||||
"typeGauge": "仪表盘",
|
||||
"typeTable": "表格",
|
||||
"addBtn": "添加",
|
||||
"toastRefreshFailed": "刷新自定义仪表盘失败:{{msg}}",
|
||||
"toastNameRequired": "请输入名称",
|
||||
"toastCreateSuccess": "创建成功",
|
||||
"toastCreateFailed": "创建失败",
|
||||
"toastPanelAdded": "面板添加成功",
|
||||
"toastPanelFailed": "添加失败"
|
||||
}
|
||||
}
|
||||
},
|
||||
"task": {
|
||||
"cronJob": {
|
||||
|
||||
Reference in new issue
Block a user