refactor(i18n): U41 残余硬编码中文清零 — 工具函数+审计RDP标签+AIOps分析+Monitor残余+审批选项
18 文件 ~35 处 UI 硬编码中文→t()/i18n.t() 调用: - agent-utils.ts timeAgo() 秒前/分钟前/小时前 - utils.ts formatDistanceToNow() 分钟前/小时前/天前 - request.ts 响应解析失败错误消息 - ActiveSessionsPanel/HistorySessionsPanel RDP 在线/历史会话标签 - MetricsExplorer 清除/已选/最近1小时 - HostMonitor 当前主机 subtitle - AIAnalysisResult 证据链 (新增 useTranslation hook) - capacity-predict.ts 容量预测摘要模板 - ApprovalResourcePage 阿里云/腾讯云/华为云 - ApprovalPermissionPage 堡垒机 - site-monitor/helpers.ts 频率/时间/证书天数格式化 - site-monitor/analytics.ts 证书剩余/检查失败/延迟波动事件 - middleware-monitor MwTargetChartsPanel/MwTrendChart/helpers.ts - snmp-monitor SnmpMetricTrendChart/SnmpDeviceChartsPanel - host-monitor/utils.ts 图表点数标签 + PortPolicyDialog 系统进程说明 新增 34 翻译键 (common.duration.secAgo/hourAgo/dayAgo + common.selected/ responseParseError/rdpActiveSessions/rdpHistorySessions/rdpSessionCount/ evidenceChain/last1Hour/currentHost + monitor.lbl 10键 + monitor.siteMonitor 8键 + approval.provider 3键 + approval.permType 1键 + monitor.portPolicyDialog.systemProcessDesc + aiops.capacity.summary) zh/en 7813→7847 对齐; tsc -b=0, npm build✓ (17.31s), ESLint=0/43/any=0, go build=0, 17/17 测试通过
This commit is contained in:
1 parent
4e2ba77aa8
commit
a7838aaf65
23 files changed
+140
-57
No files matched your search
@@ -6,7 +6,7 @@ function ApprovalPermissionPage() {
|
||||
const { t } = useTranslation();
|
||||
const PERMISSION_FIELDS: FormField[] = [
|
||||
{ key: 'perm_type', label: t('approval.type.permission'), type: 'select', options: [
|
||||
'堡垒机', 'VPN', 'Log', 'DB', 'SSH', 'K8s',
|
||||
t('approval.permType.bastion'), 'VPN', 'Log', 'DB', 'SSH', 'K8s',
|
||||
], required: true },
|
||||
{ key: 'account', label: 'Account', type: 'text', placeholder: 'logan.li', required: true },
|
||||
{ key: 'reason', label: 'Reason', type: 'textarea', placeholder: '', required: true },
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ApprovalCategoryPage, type FormField } from './ApprovalCategoryPage';
|
||||
function ApprovalResourcePage() {
|
||||
const { t } = useTranslation();
|
||||
const RESOURCE_FIELDS: FormField[] = [
|
||||
{ key: 'provider', label: t('approval.resourceTitle'), type: 'select', options: ['阿里云', '腾讯云', '华为云', 'AWS'], required: true },
|
||||
{ key: 'provider', label: t('approval.resourceTitle'), type: 'select', options: [t('approval.provider.aliyun'), t('approval.provider.tencent'), t('approval.provider.huawei'), 'AWS'], required: true },
|
||||
{ key: 'region', label: 'Region', type: 'text', placeholder: 'cn-hangzhou', required: true },
|
||||
{ key: 'spec', label: 'Spec', type: 'select', options: ['2C4G', '4C8G', '8C16G', '16C32G', '32C64G'], required: true },
|
||||
{ key: 'disk', label: 'Disk(GB)', type: 'number', defaultValue: 40, required: true },
|
||||
|
||||
@@ -319,7 +319,7 @@ export function ActiveSessionsPanel({ userToggledOff, onUserToggledOff }: Active
|
||||
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-0">
|
||||
<div className="px-4 py-2 border-b border-[var(--dg-border)] text-xs font-semibold dg-text-secondary">RDP 在线会话</div>
|
||||
<div className="px-4 py-2 border-b border-[var(--dg-border)] text-xs font-semibold dg-text-secondary">{t('common.rdpActiveSessions')}</div>
|
||||
{rdpQuery.isLoading ? (
|
||||
<div className="flex items-center justify-center py-10"><div className="w-7 h-7 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" /></div>
|
||||
) : rdpSessions.length === 0 ? (
|
||||
|
||||
@@ -248,7 +248,7 @@ export function HistorySessionsPanel() {
|
||||
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-0">
|
||||
<div className="px-4 py-2 border-b border-[var(--dg-border)] text-xs font-semibold dg-text-secondary">RDP 历史会话</div>
|
||||
<div className="px-4 py-2 border-b border-[var(--dg-border)] text-xs font-semibold dg-text-secondary">{t('common.rdpHistorySessions')}</div>
|
||||
{rdpQuery.isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="w-7 h-7 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
@@ -303,7 +303,7 @@ export function HistorySessionsPanel() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-[var(--dg-border)]">
|
||||
<span className="text-xs dg-text-muted">RDP 会话共 {rdpTotal} 条</span>
|
||||
<span className="text-xs dg-text-muted">{t('common.rdpSessionCount', { count: rdpTotal })}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" disabled={rdpPage <= 1} onClick={() => setRdpPage((p) => p - 1)}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
|
||||
@@ -215,7 +215,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
|
||||
return (
|
||||
<PageShell variant="workspace" title={t('monitor.alertRulesTab.catHost')}
|
||||
subtitle={selectedHost ? `当前主机: ${selectedHost}` : t('monitor.lbl.selectHostHint')}
|
||||
subtitle={selectedHost ? t('common.currentHost', { host: selectedHost }) : t('monitor.lbl.selectHostHint')}
|
||||
icon={<Server className="h-5 w-5" />} toolbar={toolbar}
|
||||
loading={loading && !history.length} loadingLabel={t('monitor.hostMonitor.loadingData')}
|
||||
contentClassName="space-y-3">
|
||||
|
||||
@@ -134,7 +134,7 @@ export function MetricsExplorer() {
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => { setSelectedMetrics([]); setQueryResults([]); }}>
|
||||
清除
|
||||
{t('common.clear')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -163,7 +163,7 @@ export function MetricsExplorer() {
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<BarChart3 className="h-4 w-4 dg-text-muted" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {t('monitor.metricsExplorer.metricList')} </h3>
|
||||
<span className="text-xs dg-text-muted ml-auto">{selectedMetrics.length} 已选</span>
|
||||
<span className="text-xs dg-text-muted ml-auto">{t('common.selected', { count: selectedMetrics.length })}</span>
|
||||
</div>
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
{Object.entries(grouped).map(([cat, names]) => (
|
||||
@@ -203,7 +203,7 @@ export function MetricsExplorer() {
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Activity className="h-4 w-4 dg-text-muted" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">
|
||||
{target?.name || ''} — 最近 1 小时
|
||||
{target?.name || ''} — {t('common.last1Hour')}
|
||||
</h3>
|
||||
</div>
|
||||
{selectedMetrics.length === 0 ? (
|
||||
|
||||
@@ -84,7 +84,7 @@ export function PortPolicyDialog({
|
||||
|
||||
<div className="bg-amber-500/5 border border-amber-500/20 rounded p-2.5">
|
||||
<p className="text-[10px] text-amber-300/80 leading-relaxed">
|
||||
⚠️ 无论选择哪种模式,以下系统进程端口<b> {t('monitor.portPolicyDialog.neverReport')} </b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
|
||||
{t('monitor.portPolicyDialog.systemProcessDesc', { neverReport: t('monitor.portPolicyDialog.neverReport') })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// 主机监控工具函数 // Logan.Li
|
||||
import i18n from '@/i18n';
|
||||
import type { BucketPoint, HostMonitorTimeRange } from './types';
|
||||
import type { JsonData } from '@/types/common';
|
||||
/** 按时间跨度格式化 X 轴标签(避免 7 天范围只显示时分) // Logan.Li */
|
||||
@@ -175,7 +176,7 @@ export function getActualDataSpan(history: JsonData[], _range: HostMonitorTimeRa
|
||||
const min = Math.min(...ts);
|
||||
const max = Math.max(...ts);
|
||||
const spanMin = Math.max(1, Math.round((max - min) / 60));
|
||||
return `${formatAxisTime(min, spanMin)} — ${formatAxisTime(max, spanMin)} · ${history.length} 点`;
|
||||
return i18n.t('monitor.lbl.pointCount', { min: formatAxisTime(min, spanMin), max: formatAxisTime(max, spanMin), count: history.length });
|
||||
}
|
||||
|
||||
export function getCurrentVal(latest: JsonData, netInfo: JsonData, diskIOInfo: JsonData, swapInfo: JsonData, fdInfo: JsonData,
|
||||
|
||||
@@ -67,7 +67,7 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] h-full">
|
||||
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
|
||||
选择左侧实例查看趋势与健康度
|
||||
{t('monitor.lbl.selectInstanceHint')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -127,14 +127,14 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||
value={memPct ?? (m.redis_used_memory > 0 ? 50 : 0)}
|
||||
display={fmtMemory(m.redis_used_memory)}
|
||||
level={memPct != null ? gaugeTone(memPct, 80, 90) : 'ok'}
|
||||
hint={memPct != null ? `已用 ${memPct.toFixed(0)}%` : t('monitor.lbl.noMaxMemory')}
|
||||
hint={memPct != null ? t('monitor.lbl.usedHint', { value: `${memPct.toFixed(0)}%` }) : t('monitor.lbl.noMaxMemory')}
|
||||
/>
|
||||
<MwGaugeBar
|
||||
label={t('kubernetes.topology.connections')}
|
||||
value={Math.min(100, (m.redis_connected_clients || 0) / 2)}
|
||||
display={fmtVal(m.redis_connected_clients)}
|
||||
level={(m.redis_blocked_clients || 0) > 0 ? 'warn' : 'ok'}
|
||||
hint={m.redis_cluster_known_nodes ? `集群 ${fmtVal(m.redis_cluster_known_nodes)} 节点` : undefined}
|
||||
hint={m.redis_cluster_known_nodes ? t('monitor.lbl.clusterNodesHint', { value: fmtVal(m.redis_cluster_known_nodes) }) : undefined}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -157,7 +157,7 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||
value={Math.min(100, (m.mysql_threads_running || 0) * 5)}
|
||||
display={fmtVal(m.mysql_threads_running)}
|
||||
level={(m.mysql_threads_running || 0) > 20 ? 'warn' : 'ok'}
|
||||
hint={`慢查询 ${fmtVal(m.mysql_slow_queries)}`}
|
||||
hint={t('monitor.lbl.slowQueriesHint', { value: fmtVal(m.mysql_slow_queries) })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// 中间件指标趋势图 — 光标拖拽选区 // Logan.Li
|
||||
import { useMemo } from 'react';
|
||||
import i18n from '@/i18n';
|
||||
import {
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
ReferenceArea,
|
||||
@@ -41,7 +42,7 @@ export function MwTrendChart({
|
||||
if (chartData.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center text-xs text-[var(--dg-text-muted)]" style={{ height }}>
|
||||
暂无趋势数据
|
||||
{i18n.t('monitor.lbl.noTrendData')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -245,8 +245,8 @@ export function fmtLastSeen(iso?: string): string {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '--';
|
||||
const diff = Math.floor((Date.now() - d.getTime()) / 1000);
|
||||
if (diff < 60) return `${diff}s 前`;
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m 前`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h 前`;
|
||||
if (diff < 60) return i18n.t('common.duration.secAgo', { count: diff });
|
||||
if (diff < 3600) return i18n.t('common.duration.minAgo', { count: Math.floor(diff / 60) });
|
||||
if (diff < 86400) return i18n.t('common.duration.hourAgo', { count: Math.floor(diff / 3600) });
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
@@ -95,15 +95,15 @@ export const probeTypeMeta: Record<ProbeType, { label: string; placeholder: stri
|
||||
|
||||
export function probeFrequencyHint(type: ProbeType): string {
|
||||
const sec = defaultProbeFrequency(type);
|
||||
return `推荐 ${fmtFrequency(sec)},最小 ${fmtFrequency(MIN_PROBE_FREQUENCY[type])}`;
|
||||
return i18n.t('monitor.siteMonitor.recommendFreq', { freq: fmtFrequency(sec), minFreq: fmtFrequency(MIN_PROBE_FREQUENCY[type]) });
|
||||
}
|
||||
|
||||
/** 拨测频率展示 // Logan.Li */
|
||||
export function fmtFrequency(sec?: number): string {
|
||||
if (!sec) return '--';
|
||||
if (sec < 60) return `${sec}秒`;
|
||||
if (sec < 3600) return `${Math.round(sec / 60)}分钟`;
|
||||
return `${(sec / 3600).toFixed(1)}小时`;
|
||||
if (sec < 60) return i18n.t('monitor.siteMonitor.seconds', { count: sec });
|
||||
if (sec < 3600) return i18n.t('monitor.siteMonitor.minutes', { count: Math.round(sec / 60) });
|
||||
return i18n.t('monitor.siteMonitor.hours', { count: (sec / 3600).toFixed(1) });
|
||||
}
|
||||
|
||||
/** 可用率 / 响应时间组合展示 // Logan.Li */
|
||||
@@ -147,9 +147,9 @@ export function isSslExpiringSoon(days?: number, threshold = SSL_EXPIRY_WARN_DAY
|
||||
export function fmtSslDays(days?: number): { text: string; tone: string } {
|
||||
if (days == null) return { text: '--', tone: 'text-[var(--dg-text-muted)]' };
|
||||
if (days < 0) return { text: i18n.t('system.expired2'), tone: 'dg-text-red' };
|
||||
if (days <= 7) return { text: `${days}天`, tone: 'dg-text-red' };
|
||||
if (days <= 30) return { text: `${days}天`, tone: 'dg-text-yellow' };
|
||||
return { text: `${days}天`, tone: 'dg-text-green' };
|
||||
if (days <= 7) return { text: i18n.t('monitor.siteMonitor.certDays', { count: days }), tone: 'dg-text-red' };
|
||||
if (days <= 30) return { text: i18n.t('monitor.siteMonitor.certDays', { count: days }), tone: 'dg-text-yellow' };
|
||||
return { text: i18n.t('monitor.siteMonitor.certDays', { count: days }), tone: 'dg-text-green' };
|
||||
}
|
||||
|
||||
/** 归一化拨测协议类型 // Logan.Li */
|
||||
|
||||
@@ -130,7 +130,7 @@ export function analyzeSslResults(results: SiteResult[], task: SiteTask): SslObs
|
||||
if (r.status !== 'success') {
|
||||
pushEvent(events, r, i18n.t('monitor.lbl.siteRequestTimeout'), 'error', r.error_message);
|
||||
} else if (r.ssl_days_remaining != null && r.ssl_days_remaining < 30) {
|
||||
pushEvent(events, r, `证书剩余 ${r.ssl_days_remaining} 天`, r.ssl_days_remaining <= 7 ? 'error' : 'warn', expiry);
|
||||
pushEvent(events, r, i18n.t('monitor.siteMonitor.certRemaining', { days: r.ssl_days_remaining }), r.ssl_days_remaining <= 7 ? 'error' : 'warn', expiry);
|
||||
}
|
||||
}
|
||||
const chart = tooltipOrder(results)
|
||||
@@ -158,10 +158,10 @@ function analyzeLatencyResults(results: SiteResult[], label: string): LatencyObs
|
||||
const latency = r.response_time ?? r.total_time ?? 0;
|
||||
const failed = r.status !== 'success';
|
||||
const spike = !failed && latency > spikeThreshold;
|
||||
if (failed) pushEvent(events, r, `${label}失败`, 'error', r.error_message || r.remote_ip);
|
||||
if (failed) pushEvent(events, r, i18n.t('monitor.siteMonitor.checkFailed', { label }), 'error', r.error_message || r.remote_ip);
|
||||
else if (spike) {
|
||||
spikeCount++;
|
||||
pushEvent(events, r, `${label}延迟波动`, 'warn', fmtMs(latency));
|
||||
pushEvent(events, r, i18n.t('monitor.siteMonitor.latencyFluctuation', { label }), 'warn', fmtMs(latency));
|
||||
}
|
||||
return { time: fmtTime(r.checked_at).slice(-5), latency, failed, spike };
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
|
||||
点击设备行查看 CPU / 内存 / 温度 / 风扇趋势图
|
||||
{t('monitor.lbl.clickDeviceHint')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -104,7 +104,7 @@ export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Activity className="h-4 w-4 text-[var(--dg-text-muted)] shrink-0" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)] truncate">
|
||||
可观测 · {device.name}
|
||||
{t('monitor.lbl.observability')} · {device.name}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-[10px] border-[var(--dg-border)] shrink-0">
|
||||
{templateName}
|
||||
@@ -123,7 +123,7 @@ export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
|
||||
<RefreshCw className={`h-3 w-3 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,7 +147,7 @@ export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||
</div>
|
||||
) : charts.length === 0 ? (
|
||||
<div className="py-12 text-center text-sm text-[var(--dg-text-muted)]">
|
||||
暂无可视化指标,请先完成至少一次采集
|
||||
{t('monitor.lbl.noMetricsHint')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SNMP 指标趋势折线图 — 光标拖拽选区 // Logan.Li
|
||||
import { useMemo } from 'react';
|
||||
import i18n from '@/i18n';
|
||||
import {
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
ReferenceArea,
|
||||
@@ -30,7 +31,7 @@ export function SnmpMetricTrendChart({ data, color, unit, height = 160 }: Props)
|
||||
if (chartData.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center text-xs text-[var(--dg-text-muted)]" style={{ height }}>
|
||||
暂无趋势数据,等待后台采集
|
||||
{i18n.t('monitor.lbl.noTrendDataWaiting')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user