feat(i18n): 全量中英文国际化改造
- 翻译 en-US.json 中 1006 条未翻译的中文条目为英文 - 提取 30+ 组件文件的硬编码中文到 locale JSON - 新增 900+ 翻译键覆盖所有业务模块: - Layout (Sidebar, Header, Breadcrumb) - Server (ServerForm, CloudServerImport, WebShell, RdpTools) - Deploy (ReleaseManager, PipelineCreate, AppRelease) - Kubernetes (DeploymentEdit, PodExec, PodLogs, ResourceEditor) - Monitor (UnifiedQueryPanel, AlertCenter, Prometheus/Grafana Dashboard) - AIOps (predictUtils, PredictiveCostTab, AIPredictive) - System (useLdapManagement, AgentMonitor, AgentWizard) - tsc --noEmit 类型检查通过
This commit is contained in:
1 parent
76ec9387f0
commit
a9513c2056
44 files changed
+4342
-1792
No files matched your search
@@ -507,20 +507,20 @@ const DatasourceToolbar: React.FC<{
|
||||
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
||||
<Select value={activeId} onValueChange={onChange}>
|
||||
<SelectTrigger className="w-[280px] h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择数据源" />
|
||||
<SelectValue placeholder={t('monitor.datasourceToolbar.selectDatasource')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{sources.map(ds => (
|
||||
<SelectItem key={String(ds.id)} value={String(ds.id)}>
|
||||
{ds.name || ds.url}{ds.is_default ? ' · 默认' : ''}
|
||||
{ds.name || ds.url}{ds.is_default ? t('monitor.datasourceToolbar.defaultSuffix') : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-8 text-xs gap-1" onClick={onRefresh} disabled={refreshing}>
|
||||
<RefreshCw className={cn('w-3 h-3', refreshing && 'animate-spin')} />刷新数据源
|
||||
<RefreshCw className={cn('w-3 h-3', refreshing && 'animate-spin')} />{t('monitor.datasourceToolbar.refreshDatasource')}
|
||||
</Button>
|
||||
<span className="text-[11px] text-[var(--dg-text-muted)]">共 {sources.length} 个数据源,当前仅查询选中项</span>
|
||||
<span className="text-[11px] text-[var(--dg-text-muted)]">{t('monitor.datasourceToolbar.datasourceSummary', { count: sources.length })}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { prometheusProxyApi } from '@/api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface Props { dsUrl?: string; }
|
||||
@@ -37,7 +38,6 @@ const fmtBps = (b: number) => {
|
||||
if (b > 1e3) return `${(b / 1e3).toFixed(1)} KB/s`;
|
||||
return `${b.toFixed(1)} B/s`;
|
||||
};
|
||||
const fmtCores = (v: number) => v >= 1 ? `${v.toFixed(2)} 核` : `${(v * 1000).toFixed(0)} m`;
|
||||
const podName = (p: JsonData) => `${p.namespace ? `${p.namespace}/` : ''}${p.pod || 'unknown'}`;
|
||||
|
||||
const metricValue = (item?: JsonData): number => Number(item?.value ?? Number.NaN);
|
||||
@@ -65,7 +65,11 @@ function parsePrune( result: JsonData[]): JsonData[] {
|
||||
|
||||
const Box = (p: JsonData) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>;
|
||||
|
||||
const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
const fmtCores = (v: number, t: (k: string) => string) => v >= 1 ? `${v.toFixed(2)} ${t('monitor.prometheusPanel.cores')}` : `${(v * 1000).toFixed(0)} m`;
|
||||
|
||||
const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<StatStrip
|
||||
items={[
|
||||
@@ -76,18 +80,20 @@ const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
]}
|
||||
/>
|
||||
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3 max-h-60 overflow-y-auto">
|
||||
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 dg-text-purple" />节点 CPU / 内存利用率</div>
|
||||
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 dg-text-purple" />{t('monitor.prometheusPanel.nodeCpuMemUtil')}</div>
|
||||
{(d.nodeCPU || []).map((n: JsonData, i: number) => {
|
||||
const mem = (d.nodeMem || []).find((m: JsonData) => m.node === n.node);
|
||||
return <div key={i} className="flex items-center gap-3 text-[11px] mb-1.5"><span className="w-28 truncate" title={n.node}>{n.node}</span><MiniBar val={Number(n.value||0)} label={`${Number(n.value||0).toFixed(1)}%`} /><MiniBar val={Number(mem?.value||0)} label={`${Number(mem?.value||0).toFixed(1)}%`} color="bg-blue-500" /></div>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
type NodeSortField = 'name' | 'cpu' | 'mem' | 'disk' | 'rx' | 'tx';
|
||||
|
||||
const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
const { t } = useTranslation();
|
||||
const [sortField, setSortField] = useState<NodeSortField>('cpu');
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -130,8 +136,8 @@ const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]"><tr>
|
||||
{[
|
||||
{ f: 'name' as NodeSortField, l: '节点' }, { f: 'cpu' as NodeSortField, l: 'CPU' },
|
||||
{ f: 'mem' as NodeSortField, l: '内存' }, { f: 'disk' as NodeSortField, l: '磁盘' }, { f: 'rx' as NodeSortField, l: '流入' }, { f: 'tx' as NodeSortField, l: '流出' },
|
||||
{ f: 'name' as NodeSortField, l: t('monitor.prometheusPanel.node') }, { f: 'cpu' as NodeSortField, l: t('monitor.prometheusPanel.cpu') },
|
||||
{ f: 'mem' as NodeSortField, l: t('monitor.prometheusPanel.memory') }, { f: 'disk' as NodeSortField, l: t('monitor.prometheusPanel.disk') }, { f: 'rx' as NodeSortField, l: t('monitor.prometheusPanel.inbound') }, { f: 'tx' as NodeSortField, l: t('monitor.prometheusPanel.outbound') },
|
||||
].map(({ f, l }) => (
|
||||
<th key={f} className="px-3 py-2 text-left cursor-pointer select-none" onClick={() => toggleSort(f)}><div className="flex items-center gap-1">{l}<SI f={f} /></div></th>
|
||||
))}
|
||||
@@ -152,15 +158,15 @@ const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
{sorted.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>共 {sorted.length} 个节点</span>
|
||||
<span>{t('monitor.prometheusPanel.totalNodes', { count: sorted.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={50}>50/页</option>
|
||||
<option value={10}>10/{t('monitor.prometheusPanel.perPage')}</option><option value={20}>20/{t('monitor.prometheusPanel.perPage')}</option><option value={50}>50/{t('monitor.prometheusPanel.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={safePage <= 1} onClick={() => setPage(Math.max(1, safePage - 1))}>上一页</button>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={safePage <= 1} onClick={() => setPage(Math.max(1, safePage - 1))}>{t('monitor.prometheusPanel.prevPage')}</button>
|
||||
<span className="px-2 text-[var(--dg-text-primary)]">{safePage} / {totalPages}</span>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={safePage >= totalPages} onClick={() => setPage(Math.min(totalPages, safePage + 1))}>下一页</button>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={safePage >= totalPages} onClick={() => setPage(Math.min(totalPages, safePage + 1))}>{t('monitor.prometheusPanel.nextPage')}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -183,7 +189,7 @@ const SimpleRank: React.FC<{
|
||||
<CardContent className="p-3 pt-0">
|
||||
<div className="space-y-2 max-h-[320px] overflow-y-auto pr-1">
|
||||
{rows.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-[var(--dg-text-muted)]">暂无数据</div>
|
||||
<div className="py-8 text-center text-xs text-[var(--dg-text-muted)]">{t('monitor.prometheusPanel.noData')}</div>
|
||||
) : rows.slice(0, 10).map((p: JsonData, i: number) => {
|
||||
const value = Number(p.value || 0);
|
||||
return (
|
||||
@@ -226,7 +232,7 @@ const PodNetworkRank: React.FC<{ rx: JsonData[]; tx: JsonData[] }> = ({ rx, tx }
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="py-2 px-3">
|
||||
<CardTitle className="text-xs font-medium">Pod 流量网卡 Top 10</CardTitle>
|
||||
<CardTitle className="text-xs font-medium">{t('monitor.prometheusPanel.podTrafficTop10')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
@@ -234,14 +240,14 @@ const PodNetworkRank: React.FC<{ rx: JsonData[]; tx: JsonData[] }> = ({ rx, tx }
|
||||
<thead className="bg-[var(--dg-bg-tertiary)] border-y border-[var(--dg-border)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">Pod</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">流入</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">流出</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">总流量</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('monitor.prometheusPanel.inbound')}</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('monitor.prometheusPanel.outbound')}</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('monitor.prometheusPanel.totalTraffic')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={4} className="px-3 py-8 text-center text-[var(--dg-text-muted)]">暂无数据</td></tr>
|
||||
<tr><td colSpan={4} className="px-3 py-8 text-center text-[var(--dg-text-muted)]">{t('monitor.prometheusPanel.noData')}</td></tr>
|
||||
) : rows.map((p, i) => (
|
||||
<tr key={p.name} className="border-b border-[var(--dg-border)] last:border-0 hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2">
|
||||
@@ -271,8 +277,8 @@ const PodPanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
/>
|
||||
<PodNetworkRank rx={d.netRxTop || []} tx={d.netTxTop || []} />
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<SimpleRank title="Pod CPU Top 10" rows={d.cpuTop || []} valueText={fmtCores} color="bg-purple-500" />
|
||||
<SimpleRank title="Pod 内存 Top 10" rows={d.memTop || []} valueText={fmtBytes} color="bg-blue-500" />
|
||||
<SimpleRank title="Pod CPU Top 10" rows={d.cpuTop || []} valueText={(v) => fmtCores(v, t)} color="bg-purple-500" />
|
||||
<SimpleRank title={t('monitor.prometheusPanel.podMemTop10')} rows={d.memTop || []} valueText={fmtBytes} color="bg-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -365,7 +371,7 @@ const JvmPanel: React.FC<{ dsUrl?: string; fallback: JsonData }> = ({ dsUrl, fal
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Select value={application} onValueChange={setApplication}>
|
||||
<SelectTrigger className="w-[260px] h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择 Application" />
|
||||
<SelectValue placeholder={t('monitor.prometheusPanel.selectApplication')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{applications.map(app => <SelectItem key={app} value={app}>{app}</SelectItem>)}
|
||||
@@ -373,13 +379,13 @@ const JvmPanel: React.FC<{ dsUrl?: string; fallback: JsonData }> = ({ dsUrl, fal
|
||||
</Select>
|
||||
<Select value={instance} onValueChange={setInstance}>
|
||||
<SelectTrigger className="w-[280px] h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择 Instance" />
|
||||
<SelectValue placeholder={t('monitor.prometheusPanel.selectInstance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{instances.map(ins => <SelectItem key={ins} value={ins}>{ins}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{loading && <span className="text-[11px] text-[var(--dg-text-muted)] inline-flex items-center gap-1"><Loader2 className="w-3 h-3 animate-spin" />加载 JVM 指标</span>}
|
||||
{loading && <span className="text-[11px] text-[var(--dg-text-muted)] inline-flex items-center gap-1"><Loader2 className="w-3 h-3 animate-spin" />{t('monitor.prometheusPanel.loadingJvmMetrics')}</span>}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold">{uptime>86400?`${(uptime/86400).toFixed(1)}d`:`${(uptime/3600).toFixed(1)}h`}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Uptime</div></CardContent></Card>
|
||||
@@ -414,7 +420,7 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
|
||||
const data: JsonData = await prometheusProxyApi.connect(u);
|
||||
setConnected(true); setTargets(data.targets||0);
|
||||
loadAll(u);
|
||||
} catch (e) { setConnected(false); setError((e as JsonData).message||'连接失败'); }
|
||||
} catch (e) { setConnected(false); setError((e as JsonData).message||t('monitor.prometheusPanel.connectionFailed')); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
@@ -442,13 +448,13 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
|
||||
<CardHeader className="py-3 px-4 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 dg-text-purple" />
|
||||
<CardTitle className="text-sm font-medium">Prometheus 监控</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">{t('monitor.prometheusPanel.prometheusMonitor')}</CardTitle>
|
||||
{connected && <Badge className="text-[10px] bg-purple-500/10 dg-text-purple">{targets} targets</Badge>}
|
||||
{connected && <div className="ml-auto"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" />刷新</Button></div>}
|
||||
{connected && <div className="ml-auto"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" />{t('monitor.prometheusPanel.refresh')}</Button></div>}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3 pt-0">
|
||||
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-purple" /><p className="text-xs text-[var(--dg-text-muted)]">连接中…</p></div>)
|
||||
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-purple" /><p className="text-xs text-[var(--dg-text-muted)]">{t('monitor.prometheusPanel.connecting')}</p></div>)
|
||||
: error ? (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs dg-text-red"><AlertTriangle className="h-4 w-4 inline mr-1" />{error}</div>)
|
||||
: (
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
|
||||
Reference in new issue
Block a user