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
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Sparkles, Brain, Loader2 } from 'lucide-react';
|
||||
import { AlertCircle, TrendingUp, CheckCircle, FileText, Terminal, Play } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AlertItem, AIAnalysis } from './analyzerHelpers';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
@@ -15,6 +16,7 @@ interface AIAnalysisResultProps {
|
||||
}
|
||||
|
||||
export function AIAnalysisResult({ selectedAlert, isAnalyzing, analysis, onAutoFix }: AIAnalysisResultProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card className="dg-card flex flex-col">
|
||||
<CardHeader className="border-b border-[var(--dg-border)] pb-3">
|
||||
@@ -91,7 +93,7 @@ export function AIAnalysisResult({ selectedAlert, isAnalyzing, analysis, onAutoF
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-[var(--dg-text-secondary)] flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 dg-text-blue" />
|
||||
证据链
|
||||
{t('common.evidenceChain')}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{analysis.evidence.slice(0, 5).map((item, idx) => (
|
||||
|
||||
@@ -178,7 +178,7 @@ export function buildCapacityInsightPrompt(forecasts: CapacityForecast[]): strin
|
||||
if (forecasts.length === 0) return '';
|
||||
const lines = forecasts.map((f) => {
|
||||
const risk = f.daysUntilThreshold >= 999 ? i18n.t('aiops.capacity.noRisk') : i18n.t('aiops.predictutilsts.daysUntilThreshold', { days: f.daysUntilThreshold });
|
||||
return `- ${f.resource}:当前 ${f.currentUsage}% → 7天后预测 ${f.predictedUsage}%(阈值 ${f.threshold}%),${risk}`;
|
||||
return i18n.t('aiops.capacity.summary', { resource: f.resource, current: f.currentUsage, predicted: f.predictedUsage, threshold: f.threshold, risk });
|
||||
});
|
||||
return [
|
||||
i18n.t('aiops.capacity.expertPrompt'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JsonData } from "@/types/common";
|
||||
import i18n from "@/i18n";
|
||||
// Agent 监控相关工具函数(供组件与 API 层共享) // Logan.Li
|
||||
|
||||
/** 在线判定:3 分钟内有上报视为在线 */
|
||||
@@ -15,9 +16,9 @@ export function timeAgo(ts: unknown): string {
|
||||
const t = new Date(ts as string | number | Date).getTime();
|
||||
if (Number.isNaN(t)) return '--';
|
||||
const diff = Math.floor((Date.now() - t) / 1000);
|
||||
if (diff < 60) return `${diff}秒前`;
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
|
||||
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 new Date(ts as string | number | Date).toLocaleString();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ export function formatDistanceToNow(timestamp: number): string {
|
||||
if (seconds < 60) {
|
||||
return i18n.t('task.cronJob.time.justNow');
|
||||
} else if (minutes < 60) {
|
||||
return `${minutes}分钟前`;
|
||||
return i18n.t('common.duration.minAgo', { count: minutes });
|
||||
} else if (hours < 24) {
|
||||
return `${hours}小时前`;
|
||||
return i18n.t('common.duration.hourAgo', { count: hours });
|
||||
} else if (days < 30) {
|
||||
return `${days}天前`;
|
||||
return i18n.t('common.duration.dayAgo', { count: days });
|
||||
} else {
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
}
|
||||
|
||||
@@ -181,7 +181,10 @@
|
||||
"minSec": "{{m}}m {{s}}s",
|
||||
"hour": "{{count}}h",
|
||||
"hourMin": "{{h}}h {{m}}m",
|
||||
"minAgo": "{{count}} min ago"
|
||||
"minAgo": "{{count}} min ago",
|
||||
"secAgo": "{{count}}s ago",
|
||||
"hourAgo": "{{count}}h ago",
|
||||
"dayAgo": "{{count}}d ago"
|
||||
},
|
||||
"ebCaughtError": "[ErrorBoundary] Caught error:",
|
||||
"ebCopyError": "Copy Error",
|
||||
@@ -623,7 +626,15 @@
|
||||
"fillIfMultiContainer": "(required for multi-container)",
|
||||
"confirmDeleteType": "Delete {{type}} \"{{name}}\"?",
|
||||
"unitCount": "",
|
||||
"fieldValues": "{{count}} values"
|
||||
"fieldValues": "{{count}} values",
|
||||
"selected": "{{count}} selected",
|
||||
"responseParseError": "Response parse failed",
|
||||
"rdpActiveSessions": "RDP Active Sessions",
|
||||
"rdpHistorySessions": "RDP History Sessions",
|
||||
"rdpSessionCount": "RDP sessions: {{count}}",
|
||||
"evidenceChain": "Evidence Chain",
|
||||
"last1Hour": "Last 1 hour",
|
||||
"currentHost": "Current host: {{host}}"
|
||||
},
|
||||
"menu": {
|
||||
"aiAgentCli": "Agent CLI",
|
||||
@@ -1478,7 +1489,15 @@
|
||||
"general": "General Approval"
|
||||
},
|
||||
"workflowSubtitle": "Configure multi-level approval workflows for resource, application, and permission requests",
|
||||
"workflowTitle": "Approval Workflow Config"
|
||||
"workflowTitle": "Approval Workflow Config",
|
||||
"provider": {
|
||||
"aliyun": "Alibaba Cloud",
|
||||
"tencent": "Tencent Cloud",
|
||||
"huawei": "Huawei Cloud"
|
||||
},
|
||||
"permType": {
|
||||
"bastion": "Bastion"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"csvHeader": "Time,User,Command,Output Size,Session ID,Critical",
|
||||
@@ -4423,7 +4442,16 @@
|
||||
"snmpVersion": "SNMP Version",
|
||||
"snmpNoDevice": "No SNMP devices. Click \"Add Device\" to start monitoring.",
|
||||
"snmpCollectMetrics": "Collected Metrics",
|
||||
"snmpNoMetrics": "No metrics yet. Please collect first."
|
||||
"snmpNoMetrics": "No metrics yet. Please collect first.",
|
||||
"noTrendData": "No trend data",
|
||||
"noTrendDataWaiting": "No trend data, waiting for collection",
|
||||
"selectInstanceHint": "Select an instance to view trends and health",
|
||||
"clickDeviceHint": "Click a device row to view CPU / Memory / Temperature / Fan trends",
|
||||
"noMetricsHint": "No visualizable metrics, please complete at least one collection",
|
||||
"usedHint": "Used {{value}}",
|
||||
"clusterNodesHint": "Cluster {{value}} nodes",
|
||||
"slowQueriesHint": "Slow queries {{value}}",
|
||||
"pointCount": "{{min}} — {{max}} · {{count}} pts"
|
||||
},
|
||||
"logAlertBell": {
|
||||
"linked": "Alert Policy Linked",
|
||||
@@ -4813,7 +4841,8 @@
|
||||
"neverReport": "Never report",
|
||||
"filterMode": "Filter Mode",
|
||||
"title": "Port Monitoring Policy",
|
||||
"desc": "Configure Agent port reporting filter rules"
|
||||
"desc": "Configure Agent port reporting filter rules",
|
||||
"systemProcessDesc": "⚠️ Regardless of mode, the following system process ports {{neverReport}}: sshd, rpcbind, master, systemd, dbus-daemon, rsyslogd, etc. Automatically skipped during collection."
|
||||
},
|
||||
"postgresql": "PostgreSQL",
|
||||
"prevPage": "Previous",
|
||||
@@ -5004,7 +5033,15 @@
|
||||
"probeDesc": "Configure probe tasks by protocol type, the system will automatically probe at intervals",
|
||||
"taskNotFound": "Probe task does not exist or has been deleted",
|
||||
"clickToObserve": "Click to view details",
|
||||
"alerting": "Alerting"
|
||||
"alerting": "Alerting",
|
||||
"recommendFreq": "Recommended {{freq}}, min {{minFreq}}",
|
||||
"seconds": "{{count}}s",
|
||||
"minutes": "{{count}}min",
|
||||
"hours": "{{count}}h",
|
||||
"certDays": "{{count}}d",
|
||||
"certRemaining": "Certificate remaining {{days}} days",
|
||||
"checkFailed": "{{label}} failed",
|
||||
"latencyFluctuation": "{{label}} latency fluctuation"
|
||||
},
|
||||
"siteProbe": "Site Probe",
|
||||
"siteProbeTitle": "Site Probe Tasks",
|
||||
@@ -7933,7 +7970,8 @@
|
||||
"expertPrompt": "You are an AIOps capacity planning expert. Based on the following server resource capacity forecast data, provide a comprehensive risk assessment and actionable operations recommendations:",
|
||||
"resCpu": "Server CPU Compute",
|
||||
"resMemory": "Server Memory",
|
||||
"resDisk": "Storage Disk Space"
|
||||
"resDisk": "Storage Disk Space",
|
||||
"summary": "- {{resource}}: Current {{current}}% → 7-day forecast {{predicted}}% (threshold {{threshold}}%), {{risk}}"
|
||||
},
|
||||
"capacityPlanPrompt": "Based on the following host resource usage, provide cost optimization and capacity planning advice (identify idle/overloaded hosts; suggest scale-down/scale-up)",
|
||||
"capacityPlanning": "Capacity Planning",
|
||||
|
||||
@@ -181,7 +181,10 @@
|
||||
"minSec": "{{m}}分{{s}}秒",
|
||||
"hour": "{{count}}时",
|
||||
"hourMin": "{{h}}时{{m}}分",
|
||||
"minAgo": "{{count}}分钟前"
|
||||
"minAgo": "{{count}}分钟前",
|
||||
"secAgo": "{{count}}秒前",
|
||||
"hourAgo": "{{count}}小时前",
|
||||
"dayAgo": "{{count}}天前"
|
||||
},
|
||||
"ebCaughtError": "[ErrorBoundary] 捕获到错误:",
|
||||
"ebCopyError": "复制错误",
|
||||
@@ -623,7 +626,15 @@
|
||||
"fillIfMultiContainer": "(多容器时填写)",
|
||||
"confirmDeleteType": "确认删除 {{type}} \"{{name}}\"?",
|
||||
"unitCount": "个",
|
||||
"fieldValues": "{{count}} 个值"
|
||||
"fieldValues": "{{count}} 个值",
|
||||
"selected": "{{count}} 已选",
|
||||
"responseParseError": "响应解析失败",
|
||||
"rdpActiveSessions": "RDP 在线会话",
|
||||
"rdpHistorySessions": "RDP 历史会话",
|
||||
"rdpSessionCount": "RDP 会话共 {{count}} 条",
|
||||
"evidenceChain": "证据链",
|
||||
"last1Hour": "最近 1 小时",
|
||||
"currentHost": "当前主机: {{host}}"
|
||||
},
|
||||
"menu": {
|
||||
"aiAgentCli": "Agent CLI",
|
||||
@@ -1478,7 +1489,15 @@
|
||||
"general": "通用审批"
|
||||
},
|
||||
"workflowSubtitle": "为资源申请、应用申请、权限审批配置多级审批流程",
|
||||
"workflowTitle": "审批流程配置"
|
||||
"workflowTitle": "审批流程配置",
|
||||
"provider": {
|
||||
"aliyun": "阿里云",
|
||||
"tencent": "腾讯云",
|
||||
"huawei": "华为云"
|
||||
},
|
||||
"permType": {
|
||||
"bastion": "堡垒机"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"csvHeader": "时间,用户,命令,输出大小,会话ID,高危",
|
||||
@@ -4423,7 +4442,16 @@
|
||||
"snmpVersion": "SNMP 版本",
|
||||
"snmpNoDevice": "暂无 SNMP 设备,点击「添加设备」开始监控",
|
||||
"snmpCollectMetrics": "采集指标",
|
||||
"snmpNoMetrics": "暂无指标,请先采集"
|
||||
"snmpNoMetrics": "暂无指标,请先采集",
|
||||
"noTrendData": "暂无趋势数据",
|
||||
"noTrendDataWaiting": "暂无趋势数据,等待后台采集",
|
||||
"selectInstanceHint": "选择左侧实例查看趋势与健康度",
|
||||
"clickDeviceHint": "点击设备行查看 CPU / 内存 / 温度 / 风扇趋势图",
|
||||
"noMetricsHint": "暂无可视化指标,请先完成至少一次采集",
|
||||
"usedHint": "已用 {{value}}",
|
||||
"clusterNodesHint": "集群 {{value}} 节点",
|
||||
"slowQueriesHint": "慢查询 {{value}}",
|
||||
"pointCount": "{{min}} — {{max}} · {{count}} 点"
|
||||
},
|
||||
"logAlertBell": {
|
||||
"linked": "已关联告警策略",
|
||||
@@ -4813,7 +4841,8 @@
|
||||
"neverReport": "永远不上报",
|
||||
"filterMode": "过滤模式",
|
||||
"title": "端口监控策略",
|
||||
"desc": "配置 Agent 端口上报过滤规则"
|
||||
"desc": "配置 Agent 端口上报过滤规则",
|
||||
"systemProcessDesc": "⚠️ 无论选择哪种模式,以下系统进程端口 {{neverReport}}:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。"
|
||||
},
|
||||
"postgresql": "PostgreSQL",
|
||||
"prevPage": "上一页",
|
||||
@@ -5004,7 +5033,15 @@
|
||||
"probeDesc": "按协议类型配置探活任务,系统将按间隔自动拨测",
|
||||
"taskNotFound": "拨测任务不存在或已删除",
|
||||
"clickToObserve": "点击可观测进入详情页",
|
||||
"alerting": "告警中"
|
||||
"alerting": "告警中",
|
||||
"recommendFreq": "推荐 {{freq}},最小 {{minFreq}}",
|
||||
"seconds": "{{count}}秒",
|
||||
"minutes": "{{count}}分钟",
|
||||
"hours": "{{count}}小时",
|
||||
"certDays": "{{count}}天",
|
||||
"certRemaining": "证书剩余 {{days}} 天",
|
||||
"checkFailed": "{{label}}失败",
|
||||
"latencyFluctuation": "{{label}}延迟波动"
|
||||
},
|
||||
"siteProbe": "站点拨测",
|
||||
"siteProbeTitle": "站点拨测任务",
|
||||
@@ -7933,7 +7970,8 @@
|
||||
"expertPrompt": "你是 AIOps 容量规划专家。基于以下服务器资源容量预测数据,给出综合风险评估与可执行的运维建议:",
|
||||
"resCpu": "服务器CPU算力",
|
||||
"resMemory": "服务器内存",
|
||||
"resDisk": "存储磁盘空间"
|
||||
"resDisk": "存储磁盘空间",
|
||||
"summary": "- {{resource}}:当前 {{current}}% → 7天后预测 {{predicted}}%(阈值 {{threshold}}%),{{risk}}"
|
||||
},
|
||||
"capacityPlanPrompt": "根据以下主机资源使用数据,提供成本优化和容量规划建议(识别闲置/过载主机,给出缩容/扩容建议)",
|
||||
"capacityPlanning": "容量规划",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ async function request<T = any>(
|
||||
try {
|
||||
json = await response.json();
|
||||
} catch {
|
||||
throw new Error(`HTTP ${response.status}: 响应解析失败`);
|
||||
throw new Error(`HTTP ${response.status}: ${i18n.t('common.responseParseError')}`);
|
||||
}
|
||||
|
||||
if (!response.ok || (json.code && json.code !== 200)) {
|
||||
|
||||
Reference in new issue
Block a user