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:
attacker committed 2026-07-02 21:28:58 +08:00
1 parent be2cff5745
commit 0f0f2770e1
4 files changed
+388 -103

No files matched your search

+7
View File
@@ -267,6 +267,13 @@ services/api.ts (1324L)
**问题**: 平台无国际化支持,所有 UI 文案硬编码为中文,无法切换语言 **问题**: 平台无国际化支持,所有 UI 文案硬编码为中文,无法切换语言
**增量迁移进展** — 持续推进页面国际化:
| 页面 | 新增键 | 组件 |
|------|--------|------|
| Dashboard/ServerManager/AlertCenter/Approval/安全审计/Deploy/Kubernetes/Cloud/Task/Monitor | 3800+ | 已迁移 |
| **监控大屏 (GrafanaDashboard)** | 108 (monitor.dashboard.*) | GrafanaDashboard.tsx (AgentTab/ZabbixTab/PrometheusTab/DashboardTab) |
**已完成 (2026-06-27)**: **已完成 (2026-06-27)**:
- ✅ **i18n 基础设施搭建** — 安装 `i18next` + `react-i18next`,配置翻译实例 (`src/i18n/index.ts`),支持 `zh-CN` / `en-US` 双语 - ✅ **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+ 翻译键 - ✅ **翻译文件** — `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 */ /* eslint-disable @typescript-eslint/no-explicit-any */
// 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus + 自定义仪表盘 // Logan.Li // 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus + 自定义仪表盘 // Logan.Li
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
Monitor, Monitor,
@@ -52,11 +53,11 @@ import { PageEmpty } from '@/components/layout/PageEmpty';
// ═══════════ Tab 配置 ═══════════ // ═══════════ Tab 配置 ═══════════
type TabId = 'agent' | 'zabbix' | 'prometheus' | 'dashboard'; type TabId = 'agent' | 'zabbix' | 'prometheus' | 'dashboard';
const TABS: { id: TabId; label: string; icon: React.ReactNode }[] = [ const getTabs = (t: (key: string) => string): { id: TabId; label: string; icon: React.ReactNode }[] => [
{ id: 'agent', label: 'Agent 主机', icon: <Server className="w-3.5 h-3.5" /> }, { 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: '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: '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 主机监控表格 ═══════════ // ═══════════ 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'; type SortField = 'hostname' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'mem_usage' | 'disk_usage' | 'region' | 'ip';
const AgentTab: React.FC = () => { const AgentTab: React.FC = () => {
const { t } = useTranslation();
const _confirm = useConfirm(); const _confirm = useConfirm();
const navigate = useNavigate(); const navigate = useNavigate();
const [hosts, setHosts] = useState<AgentHost[]>([]); const [hosts, setHosts] = useState<AgentHost[]>([]);
@@ -88,16 +90,16 @@ const AgentTab: React.FC = () => {
// 批量卸载 // 批量卸载
const [batchUninstalling, setBatchUninstalling] = useState(false); const [batchUninstalling, setBatchUninstalling] = useState(false);
const handleBatchUninstall = async () => { 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; if (!ok) return;
setBatchUninstalling(true); setBatchUninstalling(true);
setOperating(new Set(selIds)); setOperating(new Set(selIds));
try { try {
await agentApi.batchUninstall(selIds); await agentApi.batchUninstall(selIds);
toast.success(`已提交:批量卸载 ${selIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`); toast.success(t('monitor.dashboard.agent.toast.batchUninstall', { count: selIds.length }));
setSelected(new Set()); setSelected(new Set());
setTimeout(() => { fetch(); setOperating(new Set()); }, 30000); 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); } finally { setBatchUninstalling(false); }
}; };
const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize)); const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize));
@@ -117,7 +119,7 @@ const AgentTab: React.FC = () => {
return next; return next;
}); });
} catch (e: any) { } 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 { } finally {
setLoading(false); setLoading(false);
setRefreshing(false); setRefreshing(false);
@@ -160,17 +162,17 @@ const AgentTab: React.FC = () => {
// 执行批量部署 // 执行批量部署
const handleBatchSubmit = async () => { const handleBatchSubmit = async () => {
if (!batchAck) { toast.error('请先确认并勾选「我已知悉」'); return; } if (!batchAck) { toast.error(t('monitor.dashboard.agent.toast.ackRequired')); return; }
setBatchSubmitting(true); setBatchSubmitting(true);
const token = useUserStore.getState().token; const token = useUserStore.getState().token;
const ids = selectedHosts.map(h => h.server_id); const ids = selectedHosts.map(h => h.server_id);
setOperating(new Set(ids)); setOperating(new Set(ids));
try { try {
if (batchMode === 'install') { if (batchMode === 'install') {
if (!token) { toast.error('未登录'); return; } if (!token) { toast.error(t('monitor.dashboard.agent.toast.notLogin')); return; }
// 安装:只安装无 Agent 的 // 安装:只安装无 Agent 的
const installIds = needInstall.map(h => h.server_id); 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 // 为未保存凭据的主机构造临时凭据 map;后端遇到空字段不会覆盖已有凭据 // Logan.Li
const credentials: Record<number, { username?: string; password?: string; ssh_key?: string; auth_type?: string }> = {}; 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); 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); 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()); setSelected(new Set());
setTimeout(() => { fetch(); setOperating(new Set()); }, 30000); setTimeout(() => { fetch(); setOperating(new Set()); }, 30000);
} else { } else {
// 升级:只升级在线的 // 升级:只升级在线的
const upgradeIds = needUpgrade.map(h => h.server_id); 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); 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()); setSelected(new Set());
setTimeout(() => { fetch(); setOperating(new Set()); }, 30000); setTimeout(() => { fetch(); setOperating(new Set()); }, 30000);
} }
setBatchOpen(false); setBatchOpen(false);
} catch { toast.error('提交失败'); setOperating(new Set()); } } catch { toast.error(t('monitor.dashboard.agent.toast.submitFailed')); setOperating(new Set()); }
finally { setBatchSubmitting(false); } finally { setBatchSubmitting(false); }
}; };
if (loading) return <PageLoading label="加载 Agent 主机列表..." />; if (loading) return <PageLoading label={t('monitor.dashboard.agent.loadingHosts')} />;
if (!hosts.length) return ( if (!hosts.length) return (
<PageEmpty <PageEmpty
icon={<Server className="h-8 w-8" />} icon={<Server className="h-8 w-8" />}
title="暂无服务器资产" title={t('monitor.dashboard.agent.emptyTitle')}
description="请先在「服务器管理」中添加主机,然后在此安装 Agent" description={t('monitor.dashboard.agent.emptyDesc')}
/> />
); );
return ( return (
<div> <div>
{/* 工具栏 */} {/* 工具栏 */}
<div className="flex items-center gap-2 mb-3 flex-wrap"> <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" /> <div className="flex-1" />
<Button size="sm" variant="outline" className="h-7 text-xs gap-1" onClick={() => { <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; 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); setWizardServer(picked ? { id: picked.server_id, hostname: picked.hostname, ip: picked.ip, has_auth: picked.has_auth } : null);
setWizardOpen(true); setWizardOpen(true);
}}> }}>
<Rocket className="w-3 h-3" />安装 Agent <Rocket className="w-3 h-3" />{t('monitor.dashboard.agent.installAgent')}
</Button> </Button>
{selAny && ( {selAny && (
<> <>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 dg-btn-secondary dg-text-blue" onClick={openBatchDialog}> <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>
<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}> <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>
</> </>
)} )}
<Button size="sm" variant="ghost" className="h-7 text-xs gap-1" onClick={fetch} disabled={refreshing}> <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> </Button>
</div> </div>
<AgentWizard open={wizardOpen} onOpenChange={setWizardOpen} server={wizardServer} <AgentWizard open={wizardOpen} onOpenChange={setWizardOpen} server={wizardServer}
onStart={(id) => { setOperating(prev => new Set(prev).add(id)); }} onStart={(id) => { setOperating(prev => new Set(prev).add(id)); }}
onInstalled={() => { toast.success('Agent 安装任务已提交,后台执行中…'); setWizardOpen(false); fetch(); }} onInstalled={() => { toast.success(t('monitor.dashboard.agent.toast.installed')); setWizardOpen(false); fetch(); }}
onFailed={(id) => { toast.error('安装失败'); setOperating(prev => { const next = new Set(prev); next.delete(id); return next; }); }} /> onFailed={(id) => { toast.error(t('monitor.dashboard.agent.toast.installFailed')); setOperating(prev => { const next = new Set(prev); next.delete(id); return next; }); }} />
{/* ────────── 批量部署 Agent 对话框 ────────── */} {/* ────────── 批量部署 Agent 对话框 ────────── */}
<Dialog open={batchOpen} onOpenChange={(v) => { if (!v) { setBatchOpen(false); setBatchAck(false); setBatchCreds({ username: 'root', password: '', sshKey: '', authType: 'password' }); } }}> <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> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base"> <DialogTitle className="flex items-center gap-2 text-base">
<PackageOpen className="w-5 h-5 dg-text-blue" /> <PackageOpen className="w-5 h-5 dg-text-blue" />
批量部署 Agent {t('monitor.dashboard.batch.title')}
</DialogTitle> </DialogTitle>
<DialogDescription className="dg-text-secondary text-xs"> <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> </DialogDescription>
</DialogHeader> </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)]' 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> </button>
))} ))}
</div> </div>
@@ -278,11 +280,11 @@ const AgentTab: React.FC = () => {
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead className="bg-[var(--dg-bg-tertiary)]"> <thead className="bg-[var(--dg-bg-tertiary)]">
<tr> <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">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">{t('monitor.dashboard.col.status')}</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.version')}</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.action')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <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 font-mono dg-text-secondary">{h.ip}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
{h.agent_status === 'online' {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"> : <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>} </Badge>}
</td> </td>
<td className="px-3 py-2 font-mono dg-text-secondary">{h.agent_version || '—'}</td> <td className="px-3 py-2 font-mono dg-text-secondary">{h.agent_version || '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className="dg-text-blue text-[11px]"> <span className="dg-text-blue text-[11px]">
{batchMode === 'install' ? '即将安装' : '即将升级'} {batchMode === 'install' ? t('monitor.dashboard.batch.willInstall') : t('monitor.dashboard.batch.willUpgrade')}
</span> </span>
</td> </td>
</tr> </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="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"> <div className="flex items-center gap-2">
<Info className="w-4 h-4 dg-text-blue shrink-0" /> <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>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<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.authType')}</Label>
<Select value={batchCreds.authType} onValueChange={(v: 'password' | 'key') => setBatchCreds(c => ({ ...c, authType: v }))}> <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)]"> <SelectTrigger className="h-8 text-xs dg-input text-[var(--dg-text-primary)]">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent className="dg-input text-[var(--dg-text-primary)]"> <SelectContent className="dg-input text-[var(--dg-text-primary)]">
<SelectItem value="password">密码</SelectItem> <SelectItem value="password">{t('monitor.dashboard.creds.password')}</SelectItem>
<SelectItem value="key">SSH 私钥</SelectItem> <SelectItem value="key">{t('monitor.dashboard.creds.sshKey')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<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)]" /> <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>
</div> </div>
{batchCreds.authType === 'password' ? ( {batchCreds.authType === 'password' ? (
<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.password')}</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)]" /> <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>
) : ( ) : (
<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)]" /> <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> </div>
)} )}
@@ -354,31 +356,30 @@ const AgentTab: React.FC = () => {
<Info className="w-4 h-4 dg-text-yellow mt-0.5 shrink-0" /> <Info className="w-4 h-4 dg-text-yellow mt-0.5 shrink-0" />
<div> <div>
<p className="text-xs font-medium text-amber-300 mb-1"> <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>
<p className="text-[11px] dg-text-yellow/80 leading-relaxed"> <p className="text-[11px] dg-text-yellow/80 leading-relaxed">
{batchMode === 'upgrade' && '升级前请注意以下事项:'} {batchMode === 'upgrade' ? t('monitor.dashboard.warn.upgradeNotice') : t('monitor.dashboard.warn.installNotice')}
{batchMode === 'install' && '安装前请注意以下事项:'}
</p> </p>
<ul className="mt-1 space-y-0.5 text-[11px] dg-text-secondary list-disc list-inside"> <ul className="mt-1 space-y-0.5 text-[11px] dg-text-secondary list-disc list-inside">
{batchMode === 'upgrade' && ( {batchMode === 'upgrade' && (
<> <>
<li>如果您的机器已经安装旧版 Agent,升级操作将停止旧版并替换为最新版本。</li> <li>{t('monitor.dashboard.warn.u1')}</li>
<li>如果您的机器之前安装过 Agent 但已卸载,遗留有相关采集配置,本次升级可能导致配置重新生效。升级前请确保存量废弃配置已经移除。</li> <li>{t('monitor.dashboard.warn.u2')}</li>
<li>如果您的机器采用自定义安装的 Agent(非 AIOps 提供的一键安装脚本),请不要直接使用批量升级。</li> <li>{t('monitor.dashboard.warn.u3')}</li>
<li>批量升级前,请务必先单机灰度升级,确保升级成功且不影响业务后再操作。</li> <li>{t('monitor.dashboard.warn.u4')}</li>
</> </>
)} )}
{batchMode === 'install' && ( {batchMode === 'install' && (
<> <>
<li>安装将自动检测目标主机架构(amd64/arm64),下载对应版本的 Agent 二进制并启动服务。</li> <li>{t('monitor.dashboard.warn.i1')}</li>
<li>请确保目标主机的 SSH 端口(默认 22)可访问,且已关联有效的 SSH 凭据。</li> <li>{t('monitor.dashboard.warn.i2')}</li>
<li>安装过程约 30-60 秒,期间目标主机 CPU 负载会短暂升高。</li> <li>{t('monitor.dashboard.warn.i3')}</li>
<li>若安装失败,请检查目标主机的防火墙规则和 SSH 服务状态后重试。</li> <li>{t('monitor.dashboard.warn.i4')}</li>
</> </>
)} )}
<li>操作不可回滚,请在操作前确认已备份关键配置。</li> <li>{t('monitor.dashboard.warn.c1')}</li>
<li>操作将在后台异步执行,您可以在「任务执行记录」中查看详细进度。</li> <li>{t('monitor.dashboard.warn.c2')}</li>
</ul> </ul>
</div> </div>
</div> </div>
@@ -387,13 +388,13 @@ const AgentTab: React.FC = () => {
{/* 我已知悉 勾选 */} {/* 我已知悉 勾选 */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox id="batch-ack" checked={batchAck} onCheckedChange={(c) => setBatchAck(!!c)} /> <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>
</div> </div>
<DialogFooter className="gap-2"> <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); }}> <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>
<Button <Button
className="dg-btn-primary h-8 text-xs gap-1" className="dg-btn-primary h-8 text-xs gap-1"
@@ -401,7 +402,7 @@ const AgentTab: React.FC = () => {
onClick={handleBatchSubmit} onClick={handleBatchSubmit}
> >
{batchSubmitting && <Loader2 className="w-3 h-3 animate-spin" />} {batchSubmitting && <Loader2 className="w-3 h-3 animate-spin" />}
{batchMode === 'install' ? '确认安装' : '确认升级'} {batchMode === 'install' ? t('monitor.dashboard.batch.confirmInstall') : t('monitor.dashboard.batch.confirmUpgrade')}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@@ -415,10 +416,10 @@ const AgentTab: React.FC = () => {
</th> </th>
{['hostname','agent_version','agent_status','cpu_usage','mem_usage','disk_usage','region','ip'].map(f => ( {['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)}> <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>
))} ))}
<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> <tbody>
{paged.map((h, i) => ( {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"> <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) ? ( {operating.has(h.server_id) ? (
<span className="inline-flex items-center gap-1 dg-text-blue"> <span className="inline-flex items-center gap-1 dg-text-blue">
<Loader2 className="w-3 h-3 animate-spin" /> <Loader2 className="w-3 h-3 animate-spin" />
<span className="text-[10px]">部署中</span> <span className="text-[10px]">{t('monitor.dashboard.agent.deploying')}</span>
</span> </span>
) : ( ) : (
h.agent_version || '—' h.agent_version || '—'
)} )}
</td> </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.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.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> <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"> <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" <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)}`)}> 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> </Button>
</td> </td>
</tr> </tr>
@@ -459,17 +460,17 @@ const AgentTab: React.FC = () => {
{hosts.length > pageSize && ( {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 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)]"> <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); }}> <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={10}>{`10/${t('monitor.dashboard.pagination.perPage')}`}</option>
<option value={20}>20/页</option> <option value={20}>{`20/${t('monitor.dashboard.pagination.perPage')}`}</option>
<option value={30}>30/页</option> <option value={30}>{`30/${t('monitor.dashboard.pagination.perPage')}`}</option>
</select> </select>
</div> </div>
<div className="flex items-center gap-1"> <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> <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>
</div> </div>
)} )}
@@ -480,6 +481,7 @@ const AgentTab: React.FC = () => {
// ═══════════ 数据源 Wrapper ═══════════ // ═══════════ 数据源 Wrapper ═══════════
const ZabbixTab: React.FC = () => { const ZabbixTab: React.FC = () => {
const { t } = useTranslation();
const [ds, setDs] = useState<any>(null); const [ds, setDs] = useState<any>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -488,18 +490,19 @@ const ZabbixTab: React.FC = () => {
setDs(list.find(( d: any) => d.is_default) || list[0] || null); setDs(list.find(( d: any) => d.is_default) || list[0] || null);
}).catch(() => {}).finally(() => setLoading(false)); }).catch(() => {}).finally(() => setLoading(false));
}, []); }, []);
if (loading) return <PageLoading label="加载 Zabbix 数据源..." />; if (loading) return <PageLoading label={t('monitor.dashboard.zabbix.loading')} />;
if (!ds) return ( if (!ds) return (
<PageEmpty <PageEmpty
icon={<Database className="h-8 w-8" />} icon={<Database className="h-8 w-8" />}
title="未配置 Zabbix 数据源" title={t('monitor.dashboard.zabbix.emptyTitle')}
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>} 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} />; return <ZabbixDashboard dsUrl={ds.url} dsUsername={ds.username} dsPassword={ds.password} />;
}; };
const PrometheusTab: React.FC = () => { const PrometheusTab: React.FC = () => {
const { t } = useTranslation();
const [ds, setDs] = useState<any>(null); const [ds, setDs] = useState<any>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -508,12 +511,12 @@ const PrometheusTab: React.FC = () => {
setDs(list.find(( d: any) => d.is_default) || list[0] || null); setDs(list.find(( d: any) => d.is_default) || list[0] || null);
}).catch(() => {}).finally(() => setLoading(false)); }).catch(() => {}).finally(() => setLoading(false));
}, []); }, []);
if (loading) return <PageLoading label="加载 Prometheus 数据源..." />; if (loading) return <PageLoading label={t('monitor.dashboard.prometheus.loading')} />;
if (!ds) return ( if (!ds) return (
<PageEmpty <PageEmpty
icon={<Database className="h-8 w-8" />} icon={<Database className="h-8 w-8" />}
title="未配置 Prometheus 数据源" title={t('monitor.dashboard.prometheus.emptyTitle')}
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>} 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} />; 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; } 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 DashboardTab: React.FC = () => {
const { t } = useTranslation();
const [dashboards, setDashboards] = useState<Dashboard[]>([]); const [dashboards, setDashboards] = useState<Dashboard[]>([]);
const [active, setActive] = useState<Dashboard | null>(null); const [active, setActive] = useState<Dashboard | null>(null);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
@@ -540,7 +544,7 @@ const DashboardTab: React.FC = () => {
setDashboards(list); setDashboards(list);
if (list.length > 0 && !active) setActive(list[0]); if (list.length > 0 && !active) setActive(list[0]);
} catch (e: any) { } catch (e: any) {
toast.error('刷新自定义仪表盘失败:' + (e?.message || '未知错误')); toast.error(t('monitor.dashboard.custom.toastRefreshFailed', { msg: (e?.message || t('monitor.dashboard.agent.toast.unknownError')) }));
} finally { } finally {
setRefreshing(false); setRefreshing(false);
} }
@@ -548,35 +552,35 @@ const DashboardTab: React.FC = () => {
useEffect(() => { fetch(); }, []); useEffect(() => { fetch(); }, []);
const create = async () => { const create = async () => {
if (!nd.name) { toast.error('请输入名称'); return; } if (!nd.name) { toast.error(t('monitor.dashboard.custom.toastNameRequired')); return; }
try { await dashboardApi.createDashboard({ ...nd, panels: [] }); toast.success('创建成功'); setCreateOpen(false); setNd({ name: '', description: '', datasource: 'prometheus', datasourceUrl: '' }); fetch(); } catch { toast.error('创建失败'); } 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 () => { const addPanel = async () => {
if (!np.title || !np.query || !active) return; if (!np.title || !np.query || !active) return;
try { try {
const p: Panel = { id: Date.now().toString(), ...np, datasource: active.datasource, x: 0, y: 0, options: { legend: true, tooltip: true, refreshInterval: 30 } }; 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] }); await dashboardApi.updateDashboard(active.id, { ...active, panels: [...active.panels, p] });
toast.success('面板添加成功'); setAddOpen(false); setNp({ title: '', type: 'graph', query: '', width: 6, height: 4 }); fetch(); toast.success(t('monitor.dashboard.custom.toastPanelAdded')); setAddOpen(false); setNp({ title: '', type: 'graph', query: '', width: 6, height: 4 }); fetch();
} catch { toast.error('添加失败'); } } catch { toast.error(t('monitor.dashboard.custom.toastPanelFailed')); }
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-2"> <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); }}> <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> <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 ? ' (默认)' : ''}</SelectItem>)}</SelectContent> <SelectContent>{dashboards.map(d => <SelectItem key={d.id} value={d.id}>{d.name}{d.isDefault ? ` ${t('monitor.dashboard.custom.defaultTag')}` : ''}</SelectItem>)}</SelectContent>
</Select> </Select>
<Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={fetch} disabled={refreshing}> <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> </Button>
{active && <Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setAddOpen(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" />新建</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> </div>
{active ? ( {active ? (
active.panels.length === 0 ? ( 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"> <div className="grid grid-cols-12 gap-3">
{active.panels.map(p => ( {active.panels.map(p => (
@@ -588,35 +592,37 @@ const DashboardTab: React.FC = () => {
</div> </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> <Dialog open={createOpen} onOpenChange={setCreateOpen}><DialogContent><DialogHeader><DialogTitle>{t('monitor.dashboard.custom.createTitle')}</DialogTitle></DialogHeader>
<div className="space-y-3"><div><Label>名称</Label><Input value={nd.name} onChange={e => setNd({ ...nd, name: e.target.value })} /></div> <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>数据源</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> <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)}>取消</Button><Button onClick={create}>创建</Button></DialogFooter></DialogContent></Dialog> <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> <Dialog open={addOpen} onOpenChange={setAddOpen}><DialogContent><DialogHeader><DialogTitle>{t('monitor.dashboard.custom.addPanelTitle')}</DialogTitle></DialogHeader>
<div className="space-y-3"><div><Label>标题</Label><Input value={np.title} onChange={e => setNp({ ...np, title: e.target.value })} /></div> <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>类型</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>{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>查询</Label><Input value={np.query} onChange={e => setNp({ ...np, query: e.target.value })} /></div></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)}>取消</Button><Button onClick={addPanel}>添加</Button></DialogFooter></DialogContent></Dialog> <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> </div>
); );
}; };
// ═══════════ 主组件 ═══════════ // ═══════════ 主组件 ═══════════
const GrafanaDashboard: React.FC = () => { const GrafanaDashboard: React.FC = () => {
const { t } = useTranslation();
const [tab, setTab] = useState<TabId>('agent'); const [tab, setTab] = useState<TabId>('agent');
const [fullscreen, setFullscreen] = useState(false); 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 ( return (
<PageShell <PageShell
variant="workspace" variant="workspace"
title="监控大屏" title={t('monitor.dashboard.title')}
subtitle="Agent 主机、Zabbix、Prometheus 与自定义仪表盘" subtitle={t('monitor.dashboard.subtitle')}
icon={<Monitor className="h-5 w-5" />} icon={<Monitor className="h-5 w-5" />}
tabs={pageTabs} tabs={pageTabs}
activeTab={tab} activeTab={tab}
+138 -2
View File
@@ -2434,7 +2434,143 @@
"totalItems": "{{count}} total", "totalItems": "{{count}} total",
"minutesShort": "{{count}}m", "minutesShort": "{{count}}m",
"durationSeconds": "{{count}}s duration", "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": { "task": {
"cronJob": { "cronJob": {
@@ -4456,4 +4592,4 @@
"agentCliCopied": "Copied", "agentCliCopied": "Copied",
"agentCliHint": "For Agent CLI high-frequency calls" "agentCliHint": "For Agent CLI high-frequency calls"
} }
} }
+138 -2
View File
@@ -2434,7 +2434,143 @@
"totalItems": "共 {{count}} 条", "totalItems": "共 {{count}} 条",
"minutesShort": "{{count}}分", "minutesShort": "{{count}}分",
"durationSeconds": "持续 {{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": { "task": {
"cronJob": { "cronJob": {
@@ -4456,4 +4592,4 @@
"agentCliCopied": "已复制", "agentCliCopied": "已复制",
"agentCliHint": "适合 Agent CLI 高频调用" "agentCliHint": "适合 Agent CLI 高频调用"
} }
} }