refactor(i18n): U24 Monitor 模块 i18n hook 迁移 — 26 个 .tsx 文件从全局 i18n.t() 迁移到 useTranslation() hook + 89 个 phantom 中文文本键替换为 monitor.lbl.* 语义化键
- 新增 monitor.lbl 翻译子命名空间 89 键(HostMonitor/Middleware/Site/SNMP/Shared)
- 26 .tsx: import i18n → useTranslation() hook + i18n.t('中文') → t('monitor.lbl.semanticKey')
- 模块级代码(TABS/METRIC_CATEGORIES)保留 i18n.t() + 语义化键
- 1 .ts helpers 文件 phantom 键替换
- zh/en 6886→6975 +89 键对齐
- tsc -b=0, npm build✓ (17.16s), ESLint=0/53/0, go build=0, Docker healthy
This commit is contained in:
1 parent
de6f9454fc
commit
d21eac9117
29 files changed
+520
-284
No files matched your search
+24
@@ -515,6 +515,7 @@ services/api.ts (1324L)
|
|||||||
| U19 | i18n hash 键清理(删除 866 个死 hash 键 + 重命名 21 个高频 live hash 键为语义化键名,zh/en 7597→6731 keys 对齐,文件 328KB→276KB (-16%),tsc -b=0,npm build✓,ESLint=0/66/0,17/17 测试通过) | P3 | **done** |
|
| U19 | i18n hash 键清理(删除 866 个死 hash 键 + 重命名 21 个高频 live hash 键为语义化键名,zh/en 7597→6731 keys 对齐,文件 328KB→276KB (-16%),tsc -b=0,npm build✓,ESLint=0/66/0,17/17 测试通过) | P3 | **done** |
|
||||||
| U20 | ESLint static-components 清零 + CredDetailDialog 内联组件修复 + i18n 批量迁移损坏修复(CredDetailDialog.tsx `Field` 内联组件提取为模块级 `CredField` — 23 处 react-hooks/static-components 警告清零;修复 8 处被批量正则损坏的 `i18n.t()` 调用(`i18n.t('>名称:</span>')` 等 → 语义化键);6 处硬编码中文清零;新增 `deploy.cred.sensitiveWarn` 键;ESLint warnings 66→43 (-35%),tsc -b=0,npm build✓ (17.29s),17/17 测试通过) | P3 | **done** |
|
| U20 | ESLint static-components 清零 + CredDetailDialog 内联组件修复 + i18n 批量迁移损坏修复(CredDetailDialog.tsx `Field` 内联组件提取为模块级 `CredField` — 23 处 react-hooks/static-components 警告清零;修复 8 处被批量正则损坏的 `i18n.t()` 调用(`i18n.t('>名称:</span>')` 等 → 语义化键);6 处硬编码中文清零;新增 `deploy.cred.sensitiveWarn` 键;ESLint warnings 66→43 (-35%),tsc -b=0,npm build✓ (17.29s),17/17 测试通过) | P3 | **done** |
|
||||||
| U23 | Task 模块 i18n 治理(CronJobManager + CronJobFormDialog + ScriptManager + RemoteExecPage 4 文件 — `import i18n` 全局调用 → `useTranslation()` hook;42 处 phantom key (`i18n.t('Chinese text')` → 语义化 `task.cronJob.*`/`task.scriptList.*`/`task.remoteExec.*` 键);28 个孤儿 hash 键清理 (cronjobformdial.s*/remoteexecpaget.s*/systemtaskspanel.s*);50+ 处硬编码 JSX 中文 → t() 调用;zh/en 6825→6886 +61 键对齐,tsc -b=0,npm build✓ (17.46s),ESLint=0/45/0,go build=0,17/17 测试通过) | P3 | **done** |
|
| U23 | Task 模块 i18n 治理(CronJobManager + CronJobFormDialog + ScriptManager + RemoteExecPage 4 文件 — `import i18n` 全局调用 → `useTranslation()` hook;42 处 phantom key (`i18n.t('Chinese text')` → 语义化 `task.cronJob.*`/`task.scriptList.*`/`task.remoteExec.*` 键);28 个孤儿 hash 键清理 (cronjobformdial.s*/remoteexecpaget.s*/systemtaskspanel.s*);50+ 处硬编码 JSX 中文 → t() 调用;zh/en 6825→6886 +61 键对齐,tsc -b=0,npm build✓ (17.46s),ESLint=0/45/0,go build=0,17/17 测试通过) | P3 | **done** |
|
||||||
|
| U24 | Monitor 模块 i18n hook 迁移(26 个 .tsx 文件从 `import i18n` 全局 → `useTranslation()` hook + 89 个 phantom 中文文本键 → `monitor.lbl.*` 语义化键;299 处 `i18n.t()` 调用 → 74 处保留(模块级/.ts 工具文件) + 225 处迁移到 `t()` hook;1 个 .ts 工具文件 helpers.ts 中文文本键替换为语义化键;zh/en 6886→6975 +89 键对齐,tsc -b=0,npm build✓ (17.16s),ESLint=0/53/0,go build=0,Docker healthy) | P3 | **done** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1501,3 +1502,26 @@ pending → in_progress → done / blocked
|
|||||||
- [x] 17/17 集成回归测试通过(零回归)
|
- [x] 17/17 集成回归测试通过(零回归)
|
||||||
- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200)
|
- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## U24: Monitor 模块 i18n hook 迁移 [done]
|
||||||
|
|
||||||
|
**问题**: `src/pages/monitor/` 目录下 26 个 .tsx 文件仍使用全局 `import i18n from '@/i18n'` + `i18n.t('Chinese text')` 模式(phantom key 问题 — 中文文本作为 i18n key,在 zh-CN locale 下「恰好工作」(返回 key 本身),但切换 en-US 时仍显示中文文本而非英文翻译)。这是 U17-U23 i18n 治理系列中最后一个大规模 phantom key 模块,涉及 299 处 `i18n.t()` 调用和 89 个唯一 phantom 中文文本键。
|
||||||
|
|
||||||
|
**已完成 (2026-07-11)**:
|
||||||
|
|
||||||
|
1. **新增 `monitor.lbl` 翻译子命名空间** — 89 个语义化翻译键覆盖 5 个子模块(HostMonitor/MiddlewareMonitor/SiteMonitor/SNMPMonitor/Shared)
|
||||||
|
2. **.tsx 文件 hook 迁移** — 26 个文件从 `import i18n` → `useTranslation()` hook + phantom 中文文本键 → `monitor.lbl.*` 语义化键
|
||||||
|
3. **模块级代码保留 `i18n.t()`** — TABS 常量/METRIC_CATEGORIES 等模块级代码保留 `i18n.t()` + 语义化键
|
||||||
|
4. **.ts 工具文件** — helpers.ts 中文文本键替换为语义化键(保留 `i18n.t()` 因 .ts 无法用 hook)
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [x] `monitor.lbl` 命名空间 89 个语义化键,zh/en 完全对齐(6975 = 6975)
|
||||||
|
- [x] 26 个 .tsx 文件迁移到 `useTranslation()` hook
|
||||||
|
- [x] 299 处 `i18n.t()` → 74 保留 + 225 迁移到 `t()` hook
|
||||||
|
- [x] `tsc -b` 零错误
|
||||||
|
- [x] `npm run build` 零错误(✓ built in 17.16s)
|
||||||
|
- [x] ESLint errors=0,any=0,unused=0
|
||||||
|
- [x] `go build` 零错误
|
||||||
|
- [x] Docker 部署 healthy
|
||||||
|
|
||||||
@@ -4492,6 +4492,97 @@
|
|||||||
"userPolicyHint": "Users without any policy are not blocked (backward compatible); once assigned, only queries matching policies are allowed.",
|
"userPolicyHint": "Users without any policy are not blocked (backward compatible); once assigned, only queries matching policies are allowed.",
|
||||||
"assign": "Assign",
|
"assign": "Assign",
|
||||||
"all": "All"
|
"all": "All"
|
||||||
|
},
|
||||||
|
"lbl": {
|
||||||
|
"unlimited": "Unlimited",
|
||||||
|
"hitRate": "Hit Rate",
|
||||||
|
"totalDevices": "Total Devices",
|
||||||
|
"instances": "Instances",
|
||||||
|
"instanceStatus": "Instance Status",
|
||||||
|
"connections": "Connections",
|
||||||
|
"online": "Online",
|
||||||
|
"fragmentationRate": "Fragmentation Rate",
|
||||||
|
"trafficSent": "Traffic Sent",
|
||||||
|
"evictedKeys": "Evicted Keys",
|
||||||
|
"noMaxMemory": "No maxmemory configured",
|
||||||
|
"standalone": "Standalone",
|
||||||
|
"middleware": "Middleware",
|
||||||
|
"hostMonitor": "Host Monitor",
|
||||||
|
"basicMonitor": "Basic Monitor",
|
||||||
|
"diskMonitor": "Disk Monitor",
|
||||||
|
"networkMonitor": "Network Monitor",
|
||||||
|
"processMonitor": "Process Monitor",
|
||||||
|
"selectHost": "Select host...",
|
||||||
|
"selectHostHint": "Select a host to view metrics",
|
||||||
|
"coreMetrics": "Core Metrics",
|
||||||
|
"allMetrics": "All Metrics",
|
||||||
|
"cpuUsage": "CPU Usage (%)",
|
||||||
|
"cpuIowait": "CPU IO Wait (%)",
|
||||||
|
"memUsage": "Memory Usage (%)",
|
||||||
|
"memAvailable": "Available Memory (GB)",
|
||||||
|
"swapUsage": "Swap Usage (%)",
|
||||||
|
"load1": "System Load (1m)",
|
||||||
|
"diskUsage": "Disk Usage (%)",
|
||||||
|
"diskRead": "Disk Read (KB/s)",
|
||||||
|
"diskWrite": "Disk Write (KB/s)",
|
||||||
|
"diskIoBusy": "Disk IO Busy (%)",
|
||||||
|
"fdUsage": "FD Usage Rate",
|
||||||
|
"fdRate": "FD Usage",
|
||||||
|
"nicRxErrors": "NIC RX Errors",
|
||||||
|
"nicTxErrors": "NIC TX Errors",
|
||||||
|
"nicBandwidth": "NIC Bandwidth",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"allPortsReport": "All discovered ports are reported, any port change triggers alert + recovery.",
|
||||||
|
"selectedOnlyReport": "Only selected port changes are reported, others are silently ignored. System process ports are not alerted by default.",
|
||||||
|
"whitelistAutoReport": "In whitelist mode, all ports are automatically reported",
|
||||||
|
"nameIpRequired": "Name and IP are required",
|
||||||
|
"updateSuccess": "Updated successfully",
|
||||||
|
"editMw": "Edit Middleware",
|
||||||
|
"addMw": "Add Middleware",
|
||||||
|
"siteProbe": "Site Probe",
|
||||||
|
"availability24h": "Availability (24h)",
|
||||||
|
"totalProbes": "Total Probes",
|
||||||
|
"editTask": "Edit Probe Task",
|
||||||
|
"addProbe": "Add Probe",
|
||||||
|
"editProbe": "Edit Probe",
|
||||||
|
"deleteProbe": "Delete Probe",
|
||||||
|
"selectTemplate": "Select Template",
|
||||||
|
"deleteMonitor": "Delete Monitor",
|
||||||
|
"partialFail": "Partial Failure",
|
||||||
|
"defaultAdded": "Default monitoring added",
|
||||||
|
"defaultExists": "Default target already exists",
|
||||||
|
"collectDone": "Collection completed",
|
||||||
|
"deleteFail": "Delete failed",
|
||||||
|
"nameAddrRequired": "Name and address are required",
|
||||||
|
"taskUpdated": "Task updated",
|
||||||
|
"taskCreated": "Probe task created",
|
||||||
|
"taskDeleted": "Task deleted",
|
||||||
|
"totalTasks": "Total Tasks",
|
||||||
|
"noTasks": "No probe tasks",
|
||||||
|
"noFilteredTasks": "No tasks match current filter",
|
||||||
|
"urlPlaceholder": "e.g., Homepage",
|
||||||
|
"probeDone": "Probe completed",
|
||||||
|
"probeFail": "Probe failed",
|
||||||
|
"metricDetail": "Metric Details",
|
||||||
|
"notDetected": "Not Detected",
|
||||||
|
"snmpTitle": "Network Device / BMC Collection & Trends",
|
||||||
|
"fillRequired": "Please fill device name, IP and select OID template",
|
||||||
|
"deviceAdded": "Device added",
|
||||||
|
"collectFail": "Collection failed",
|
||||||
|
"demoSwitch": "Core-Switch-A",
|
||||||
|
"oidTemplate": "OID Template",
|
||||||
|
"device": "Device",
|
||||||
|
"centerSubtitle": "Comprehensive infrastructure & application monitoring",
|
||||||
|
"explorerTitle": "Metrics Explorer",
|
||||||
|
"searchMetrics": "Search metrics...",
|
||||||
|
"explorerSubtitle": "Browse all metrics by category/host, supports overlay comparison",
|
||||||
|
"loadingTargets": "Loading targets...",
|
||||||
|
"selectTarget": "Please select a target",
|
||||||
|
"selectHint": "After selecting a target host, check metrics from the left panel",
|
||||||
|
"selectMetricsHint": "Please check metrics from the left panel",
|
||||||
|
"probeName": "Probe Name",
|
||||||
|
"execCommand": "Execute Command",
|
||||||
|
"refreshing": " · Refreshing..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"task": {
|
"task": {
|
||||||
|
|||||||
@@ -4492,6 +4492,97 @@
|
|||||||
"userPolicyHint": "用户未分配任何策略时不拦截(向后兼容);分配后仅允许命中策略的查询。",
|
"userPolicyHint": "用户未分配任何策略时不拦截(向后兼容);分配后仅允许命中策略的查询。",
|
||||||
"assign": "分配",
|
"assign": "分配",
|
||||||
"all": "全部"
|
"all": "全部"
|
||||||
|
},
|
||||||
|
"lbl": {
|
||||||
|
"unlimited": "无限制",
|
||||||
|
"hitRate": "命中率",
|
||||||
|
"totalDevices": "设备总数",
|
||||||
|
"instances": "监控实例",
|
||||||
|
"instanceStatus": "实例状态",
|
||||||
|
"connections": "连接",
|
||||||
|
"online": "在线",
|
||||||
|
"fragmentationRate": "碎片率",
|
||||||
|
"trafficSent": "发送流量",
|
||||||
|
"evictedKeys": "逐出 Key",
|
||||||
|
"noMaxMemory": "未配置 maxmemory",
|
||||||
|
"standalone": "单机",
|
||||||
|
"middleware": "中间件",
|
||||||
|
"hostMonitor": "主机监控",
|
||||||
|
"basicMonitor": "基础监控",
|
||||||
|
"diskMonitor": "磁盘监控",
|
||||||
|
"networkMonitor": "网络监控",
|
||||||
|
"processMonitor": "进程监控",
|
||||||
|
"selectHost": "选择主机...",
|
||||||
|
"selectHostHint": "选择主机查看监控指标",
|
||||||
|
"coreMetrics": "核心指标",
|
||||||
|
"allMetrics": "全部指标",
|
||||||
|
"cpuUsage": "CPU 使用率(%)",
|
||||||
|
"cpuIowait": "CPU IO等待(%)",
|
||||||
|
"memUsage": "内存使用率(%)",
|
||||||
|
"memAvailable": "可用内存(GB)",
|
||||||
|
"swapUsage": "Swap 使用率(%)",
|
||||||
|
"load1": "系统负载(1m)",
|
||||||
|
"diskUsage": "磁盘使用率(%)",
|
||||||
|
"diskRead": "磁盘读取(KB/s)",
|
||||||
|
"diskWrite": "磁盘写入(KB/s)",
|
||||||
|
"diskIoBusy": "磁盘IO繁忙度(%)",
|
||||||
|
"fdUsage": "FD 文件句柄使用率",
|
||||||
|
"fdRate": "FD 使用率",
|
||||||
|
"nicRxErrors": "网卡接收错误",
|
||||||
|
"nicTxErrors": "网卡发送错误",
|
||||||
|
"nicBandwidth": "网卡带宽吞吐",
|
||||||
|
"saving": "保存中…",
|
||||||
|
"allPortsReport": "所有发现的端口都上报,任意端口变化都会触发告警 + 恢复通知。",
|
||||||
|
"selectedOnlyReport": "仅上报选中端口的变化,其余端口变化静默忽略。系统进程端口默认不告警。",
|
||||||
|
"whitelistAutoReport": "白名单模式下所有端口自动上报",
|
||||||
|
"nameIpRequired": "请填写名称和 IP",
|
||||||
|
"updateSuccess": "更新成功",
|
||||||
|
"editMw": "编辑中间件",
|
||||||
|
"addMw": "添加中间件",
|
||||||
|
"siteProbe": "站点拨测",
|
||||||
|
"availability24h": "可用率(24h)",
|
||||||
|
"totalProbes": "探测次数",
|
||||||
|
"editTask": "编辑拨测任务",
|
||||||
|
"addProbe": "添加探针",
|
||||||
|
"editProbe": "编辑探针",
|
||||||
|
"deleteProbe": "删除探针",
|
||||||
|
"selectTemplate": "选择模板",
|
||||||
|
"deleteMonitor": "删除监控",
|
||||||
|
"partialFail": "部分失败",
|
||||||
|
"defaultAdded": "已添加默认监控",
|
||||||
|
"defaultExists": "默认目标已存在",
|
||||||
|
"collectDone": "采集完成",
|
||||||
|
"deleteFail": "删除失败",
|
||||||
|
"nameAddrRequired": "名称和地址不能为空",
|
||||||
|
"taskUpdated": "任务已更新",
|
||||||
|
"taskCreated": "拨测任务已创建",
|
||||||
|
"taskDeleted": "任务已删除",
|
||||||
|
"totalTasks": "总任务",
|
||||||
|
"noTasks": "暂无拨测任务",
|
||||||
|
"noFilteredTasks": "当前筛选无任务",
|
||||||
|
"urlPlaceholder": "例如:官网首页",
|
||||||
|
"probeDone": "拨测完成",
|
||||||
|
"probeFail": "拨测失败",
|
||||||
|
"metricDetail": "指标详情",
|
||||||
|
"notDetected": "未检测",
|
||||||
|
"snmpTitle": "网络设备 / BMC 采集与趋势",
|
||||||
|
"fillRequired": "请填写设备名称、IP 并选择 OID 模板",
|
||||||
|
"deviceAdded": "设备已添加",
|
||||||
|
"collectFail": "采集失败",
|
||||||
|
"demoSwitch": "核心交换机-A",
|
||||||
|
"oidTemplate": "OID 模板",
|
||||||
|
"device": "设备",
|
||||||
|
"centerSubtitle": "全面的基础设施与应用监控",
|
||||||
|
"explorerTitle": "指标浏览器",
|
||||||
|
"searchMetrics": "搜索指标...",
|
||||||
|
"explorerSubtitle": "按分类/主机浏览所有指标,支持叠加对比",
|
||||||
|
"loadingTargets": "加载监控目标...",
|
||||||
|
"selectTarget": "请选择监控目标",
|
||||||
|
"selectHint": "选择目标主机后,从左侧勾选要查看的指标",
|
||||||
|
"selectMetricsHint": "请从左侧勾选指标",
|
||||||
|
"probeName": "探针名称",
|
||||||
|
"execCommand": "执行命令",
|
||||||
|
"refreshing": " · 刷新中..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"task": {
|
"task": {
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ import { NetworkTab } from './host-monitor/tabs/NetworkTab';
|
|||||||
import { PortMonitorTab } from './host-monitor/tabs/PortMonitorTab';
|
import { PortMonitorTab } from './host-monitor/tabs/PortMonitorTab';
|
||||||
import { ProcessTab } from './host-monitor/tabs/ProcessTab';
|
import { ProcessTab } from './host-monitor/tabs/ProcessTab';
|
||||||
import type { JsonData } from '@/types/common';
|
import type { JsonData } from '@/types/common';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'basic', label: i18n.t('基础监控'), icon: <Monitor className="w-3.5 h-3.5" /> },
|
{ id: 'basic', label: i18n.t('monitor.lbl.basicMonitor'), icon: <Monitor className="w-3.5 h-3.5" /> },
|
||||||
{ id: 'disk', label: i18n.t('磁盘监控'), icon: <HardDrive className="w-3.5 h-3.5" /> },
|
{ id: 'disk', label: i18n.t('monitor.lbl.diskMonitor'), icon: <HardDrive className="w-3.5 h-3.5" /> },
|
||||||
{ id: 'network', label: i18n.t('网络监控'), icon: <Network className="w-3.5 h-3.5" /> },
|
{ id: 'network', label: i18n.t('monitor.lbl.networkMonitor'), icon: <Network className="w-3.5 h-3.5" /> },
|
||||||
{ id: 'ports', label: i18n.t('agent.portMonitor'), icon: <Server className="w-3.5 h-3.5" /> },
|
{ id: 'ports', label: i18n.t('agent.portMonitor'), icon: <Server className="w-3.5 h-3.5" /> },
|
||||||
{ id: 'process', label: i18n.t('monitor.hostProcess'), icon: <List className="w-3.5 h-3.5" /> },
|
{ id: 'process', label: i18n.t('monitor.hostProcess'), icon: <List className="w-3.5 h-3.5" /> },
|
||||||
{ id: 'system', label: i18n.t('system.systemInfo'), icon: <Info className="w-3.5 h-3.5" /> },
|
{ id: 'system', label: i18n.t('system.systemInfo'), icon: <Info className="w-3.5 h-3.5" /> },
|
||||||
@@ -39,6 +40,7 @@ const TABS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const urlHost = searchParams.get('host') || '';
|
const urlHost = searchParams.get('host') || '';
|
||||||
@@ -118,7 +120,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select value={selectedHost} onValueChange={setSelectedHost}>
|
<Select value={selectedHost} onValueChange={setSelectedHost}>
|
||||||
<SelectTrigger className="w-52 h-8 text-xs dg-input">
|
<SelectTrigger className="w-52 h-8 text-xs dg-input">
|
||||||
<SelectValue placeholder={i18n.t('选择主机...')} />
|
<SelectValue placeholder={t('monitor.lbl.selectHost')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="dg-input">
|
<SelectContent className="dg-input">
|
||||||
{hosts.map((h: JsonData) => (
|
{hosts.map((h: JsonData) => (
|
||||||
@@ -128,7 +130,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
|||||||
</Select>
|
</Select>
|
||||||
{serverInfo && (
|
{serverInfo && (
|
||||||
<span className="text-xs dg-text-muted">
|
<span className="text-xs dg-text-muted">
|
||||||
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} ${i18n.t('monitor.hostMonitorPanel.cores')}` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
|
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} ${t('monitor.hostMonitorPanel.cores')}` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -136,15 +138,15 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
|||||||
<Select value={aggType} onValueChange={(v) => setAggType(v as AggType)}>
|
<Select value={aggType} onValueChange={(v) => setAggType(v as AggType)}>
|
||||||
<SelectTrigger className="w-24 h-7 text-xs dg-input"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="w-24 h-7 text-xs dg-input"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent className="dg-input">
|
<SelectContent className="dg-input">
|
||||||
<SelectItem value="avg"> {i18n.t('选择主机...')} </SelectItem>
|
<SelectItem value="avg"> {t('monitor.lbl.selectHost')} </SelectItem>
|
||||||
<SelectItem value="max"> {i18n.t('选择主机...')} </SelectItem>
|
<SelectItem value="max"> {t('monitor.lbl.selectHost')} </SelectItem>
|
||||||
<SelectItem value="min"> {i18n.t('选择主机...')} </SelectItem>
|
<SelectItem value="min"> {t('monitor.lbl.selectHost')} </SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div className="flex flex-col items-end gap-0.5">
|
<div className="flex flex-col items-end gap-0.5">
|
||||||
<HostTimeRangePicker value={timeRange} onChange={setTimeRange} />
|
<HostTimeRangePicker value={timeRange} onChange={setTimeRange} />
|
||||||
{actualDataSpan && (
|
{actualDataSpan && (
|
||||||
<span className="text-[10px] dg-text-muted tabular-nums">{i18n.t('monitor.hostMonitorPanel.dataSpan', { span: actualDataSpan })}</span>
|
<span className="text-[10px] dg-text-muted tabular-nums">{t('monitor.hostMonitorPanel.dataSpan', { span: actualDataSpan })}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,10 +214,10 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="workspace" title={i18n.t('monitor.alertRulesTab.catHost')}
|
<PageShell variant="workspace" title={t('monitor.alertRulesTab.catHost')}
|
||||||
subtitle={selectedHost ? `当前主机: ${selectedHost}` : i18n.t('选择主机查看监控指标')}
|
subtitle={selectedHost ? `当前主机: ${selectedHost}` : t('monitor.lbl.selectHostHint')}
|
||||||
icon={<Server className="h-5 w-5" />} toolbar={toolbar}
|
icon={<Server className="h-5 w-5" />} toolbar={toolbar}
|
||||||
loading={loading && !history.length} loadingLabel={i18n.t('monitor.hostmonitortsx.sc23e802e')}
|
loading={loading && !history.length} loadingLabel={t('monitor.hostmonitortsx.sc23e802e')}
|
||||||
contentClassName="space-y-3">
|
contentClassName="space-y-3">
|
||||||
{monitorBody}
|
{monitorBody}
|
||||||
{dialogs}
|
{dialogs}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { metricsApi } from '@/api';
|
|||||||
import { PageShell } from '@/components/layout/PageShell';
|
import { PageShell } from '@/components/layout/PageShell';
|
||||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||||
import type { JsonData } from '@/types/common';
|
import type { JsonData } from '@/types/common';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
interface TargetInfo {
|
interface TargetInfo {
|
||||||
@@ -34,10 +35,11 @@ const METRIC_CATEGORIES: Record<string, { label: string; icon: React.ReactNode }
|
|||||||
disk: { label: i18n.t('monitor.dashboard.col.disk'), icon: <HardDrive className="h-4 w-4" /> },
|
disk: { label: i18n.t('monitor.dashboard.col.disk'), icon: <HardDrive className="h-4 w-4" /> },
|
||||||
network: { label: i18n.t('monitor.hostMetric.network'), icon: <Network className="h-4 w-4" /> },
|
network: { label: i18n.t('monitor.hostMetric.network'), icon: <Network className="h-4 w-4" /> },
|
||||||
middleware: { label: i18n.t('monitor.alertRulesTab.catMiddleware'), icon: <Database className="h-4 w-4" /> },
|
middleware: { label: i18n.t('monitor.alertRulesTab.catMiddleware'), icon: <Database className="h-4 w-4" /> },
|
||||||
device: { label: i18n.t('设备'), icon: <Activity className="h-4 w-4" /> },
|
device: { label: i18n.t('monitor.lbl.device'), icon: <Activity className="h-4 w-4" /> },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MetricsExplorer() {
|
export function MetricsExplorer() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [targets, setTargets] = useState<TargetInfo[]>([]);
|
const [targets, setTargets] = useState<TargetInfo[]>([]);
|
||||||
const [metricNames, setMetricNames] = useState<string[]>([]);
|
const [metricNames, setMetricNames] = useState<string[]>([]);
|
||||||
const [selectedTarget, setSelectedTarget] = useState('');
|
const [selectedTarget, setSelectedTarget] = useState('');
|
||||||
@@ -109,7 +111,7 @@ export function MetricsExplorer() {
|
|||||||
<div className="flex items-center gap-4 flex-wrap w-full">
|
<div className="flex items-center gap-4 flex-wrap w-full">
|
||||||
<Select value={selectedTarget} onValueChange={setSelectedTarget}>
|
<Select value={selectedTarget} onValueChange={setSelectedTarget}>
|
||||||
<SelectTrigger className="w-64 dg-input">
|
<SelectTrigger className="w-64 dg-input">
|
||||||
<SelectValue placeholder={loading ? i18n.t('monitor.logPanel.logResults.loading') : i18n.t('加载中...')} />
|
<SelectValue placeholder={loading ? t('monitor.logPanel.logResults.loading') : t('common.loading')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="dg-input max-h-80">
|
<SelectContent className="dg-input max-h-80">
|
||||||
{targets.map(t => (
|
{targets.map(t => (
|
||||||
@@ -126,7 +128,7 @@ export function MetricsExplorer() {
|
|||||||
<Search className="absolute left-2.5 top-2 h-4 w-4 dg-text-muted" />
|
<Search className="absolute left-2.5 top-2 h-4 w-4 dg-text-muted" />
|
||||||
<Input
|
<Input
|
||||||
className="pl-8 dg-input h-9"
|
className="pl-8 dg-input h-9"
|
||||||
placeholder={i18n.t('搜索指标...')}
|
placeholder={t('monitor.lbl.searchMetrics')}
|
||||||
value={searchKw}
|
value={searchKw}
|
||||||
onChange={(e) => setSearchKw(e.target.value)}
|
onChange={(e) => setSearchKw(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -140,19 +142,19 @@ export function MetricsExplorer() {
|
|||||||
return (
|
return (
|
||||||
<PageShell
|
<PageShell
|
||||||
variant="workspace"
|
variant="workspace"
|
||||||
title={i18n.t('指标浏览器')}
|
title={t('monitor.lbl.explorerTitle')}
|
||||||
subtitle={i18n.t('按分类/主机浏览所有指标,支持叠加对比')}
|
subtitle={i18n.t('monitor.lbl.explorerSubtitle')}
|
||||||
icon={<Activity className="h-5 w-5" />}
|
icon={<Activity className="h-5 w-5" />}
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
loadingLabel={i18n.t('加载监控目标...')}
|
loadingLabel={i18n.t('monitor.lbl.loadingTargets')}
|
||||||
contentClassName="space-y-6"
|
contentClassName="space-y-6"
|
||||||
>
|
>
|
||||||
{!selectedTarget ? (
|
{!selectedTarget ? (
|
||||||
<PageEmpty
|
<PageEmpty
|
||||||
icon={<Filter className="h-12 w-12" />}
|
icon={<Filter className="h-12 w-12" />}
|
||||||
title={i18n.t('请选择监控目标')}
|
title={i18n.t('monitor.lbl.selectTarget')}
|
||||||
description={i18n.t('选择目标主机后,从左侧勾选要查看的指标')}
|
description={i18n.t('monitor.lbl.selectHint')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
@@ -160,7 +162,7 @@ export function MetricsExplorer() {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<BarChart3 className="h-4 w-4 dg-text-muted" />
|
<BarChart3 className="h-4 w-4 dg-text-muted" />
|
||||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {i18n.t('monitor.metricsexplorer.s3ec50388')} </h3>
|
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {t('monitor.metricsexplorer.s3ec50388')} </h3>
|
||||||
<span className="text-xs dg-text-muted ml-auto">{selectedMetrics.length} 已选</span>
|
<span className="text-xs dg-text-muted ml-auto">{selectedMetrics.length} 已选</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
||||||
@@ -205,9 +207,9 @@ export function MetricsExplorer() {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{selectedMetrics.length === 0 ? (
|
{selectedMetrics.length === 0 ? (
|
||||||
<PageEmpty icon={<Plus className="h-8 w-8" />} title={i18n.t('请从左侧勾选指标')} className="py-16" />
|
<PageEmpty icon={<Plus className="h-8 w-8" />} title={i18n.t('monitor.lbl.selectMetricsHint')} className="py-16" />
|
||||||
) : sortedTimes.length === 0 ? (
|
) : sortedTimes.length === 0 ? (
|
||||||
<PageEmpty title={i18n.t('aiops.assistantNoData')} className="py-16" />
|
<PageEmpty title={t('aiops.assistantNoData')} className="py-16" />
|
||||||
) : (
|
) : (
|
||||||
<div className="h-80 overflow-x-auto">
|
<div className="h-80 overflow-x-auto">
|
||||||
<svg viewBox={`0 0 ${Math.max(sortedTimes.length * 4, 400)} 300`} className="w-full h-full min-w-[400px]">
|
<svg viewBox={`0 0 ${Math.max(sortedTimes.length * 4, 400)} 300`} className="w-full h-full min-w-[400px]">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { InstanceCard } from './middleware-monitor/InstanceCard';
|
|||||||
import { MiddlewareDialog } from './middleware-monitor/MiddlewareDialog';
|
import { MiddlewareDialog } from './middleware-monitor/MiddlewareDialog';
|
||||||
import type { MwTarget } from './middleware-monitor/helpers';
|
import type { MwTarget } from './middleware-monitor/helpers';
|
||||||
import { REDIS_CHARTS, MYSQL_CHARTS, evalMwHealth, worstHealthLevel } from './middleware-monitor/helpers';
|
import { REDIS_CHARTS, MYSQL_CHARTS, evalMwHealth, worstHealthLevel } from './middleware-monitor/helpers';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type TabKey = 'redis' | 'mysql';
|
type TabKey = 'redis' | 'mysql';
|
||||||
const REFRESH_INTERVAL_MS = 30_000;
|
const REFRESH_INTERVAL_MS = 30_000;
|
||||||
@@ -20,6 +20,7 @@ const REFRESH_INTERVAL_MS = 30_000;
|
|||||||
interface MwListResponse { list: MwTarget[]; total: number }
|
interface MwListResponse { list: MwTarget[]; total: number }
|
||||||
|
|
||||||
export function MiddlewareMonitor() {
|
export function MiddlewareMonitor() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [targets, setTargets] = useState<MwTarget[]>([]);
|
const [targets, setTargets] = useState<MwTarget[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -62,7 +63,7 @@ export function MiddlewareMonitor() {
|
|||||||
hasLoaded.current = true;
|
hasLoaded.current = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
setError((err as { message?: string }).message || i18n.t('kubernetes.msg.loadFailed'));
|
setError((err as { message?: string }).message || t('kubernetes.msg.loadFailed'));
|
||||||
setTargets([]);
|
setTargets([]);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -86,14 +87,14 @@ export function MiddlewareMonitor() {
|
|||||||
const warnCount = targets.filter((t) => worstHealthLevel(evalMwHealth(t)) !== 'ok').length;
|
const warnCount = targets.filter((t) => worstHealthLevel(evalMwHealth(t)) !== 'ok').length;
|
||||||
|
|
||||||
const handleDelete = async (target: MwTarget) => {
|
const handleDelete = async (target: MwTarget) => {
|
||||||
const ok = await confirm({ title: i18n.t('删除监控'), description: `确认删除 "${target.name}"?`, confirmText: i18n.t('kubernetes.storage.delete2') });
|
const ok = await confirm({ title: t('monitor.lbl.deleteMonitor'), description: `确认删除 "${target.name}"?`, confirmText: t('kubernetes.storage.delete2') });
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await middlewareMonitorApi.delete(target.id);
|
await middlewareMonitorApi.delete(target.id);
|
||||||
toast.success(i18n.t('monitor.silenceRules.deleted'));
|
toast.success(t('monitor.silenceRules.deleted'));
|
||||||
loadTargets();
|
loadTargets();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(i18n.t('kubernetes.msg.deleteFailed2'), { description: (err as { message?: string }).message });
|
toast.error(t('kubernetes.msg.deleteFailed2'), { description: (err as { message?: string }).message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,10 +102,10 @@ export function MiddlewareMonitor() {
|
|||||||
setTesting(target.id);
|
setTesting(target.id);
|
||||||
try {
|
try {
|
||||||
const res = await middlewareMonitorApi.test(target.id) as { success: boolean; message: string };
|
const res = await middlewareMonitorApi.test(target.id) as { success: boolean; message: string };
|
||||||
if (res.success) { toast.success(i18n.t('aiops.llmTestSuccess'), { description: res.message }); loadTargets(true); }
|
if (res.success) { toast.success(t('aiops.llmTestSuccess'), { description: res.message }); loadTargets(true); }
|
||||||
else toast.error(i18n.t('aiops.llmTestFailed'), { description: res.message });
|
else toast.error(t('aiops.llmTestFailed'), { description: res.message });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(i18n.t('monitor.alertChannels.testFailed'), { description: (err as { message?: string }).message });
|
toast.error(t('monitor.alertChannels.testFailed'), { description: (err as { message?: string }).message });
|
||||||
} finally {
|
} finally {
|
||||||
setTesting(null);
|
setTesting(null);
|
||||||
}
|
}
|
||||||
@@ -115,11 +116,11 @@ export function MiddlewareMonitor() {
|
|||||||
try {
|
try {
|
||||||
const res = await middlewareMonitorApi.seedDefaults() as { created: number; message: string; failed?: string[] };
|
const res = await middlewareMonitorApi.seedDefaults() as { created: number; message: string; failed?: string[] };
|
||||||
await loadTargets();
|
await loadTargets();
|
||||||
if (res.failed?.length) toast.error(i18n.t('部分失败'), { description: res.message });
|
if (res.failed?.length) toast.error(t('monitor.lbl.partialFail'), { description: res.message });
|
||||||
else if (res.created > 0) toast.success(i18n.t('已添加默认监控'), { description: res.message });
|
else if (res.created > 0) toast.success(t('monitor.lbl.defaultAdded'), { description: res.message });
|
||||||
else toast.success(i18n.t('默认目标已存在'));
|
else toast.success(t('monitor.lbl.defaultExists'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(i18n.t('monitor.dashboard.custom.toastPanelFailed'), { description: (err as { message?: string }).message });
|
toast.error(t('monitor.dashboard.custom.toastPanelFailed'), { description: (err as { message?: string }).message });
|
||||||
} finally {
|
} finally {
|
||||||
setSeeding(false);
|
setSeeding(false);
|
||||||
}
|
}
|
||||||
@@ -128,10 +129,10 @@ export function MiddlewareMonitor() {
|
|||||||
const handleCollectNow = async () => {
|
const handleCollectNow = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await middlewareMonitorApi.collectNow() as { message: string };
|
const res = await middlewareMonitorApi.collectNow() as { message: string };
|
||||||
toast.success(i18n.t('采集完成'), { description: res.message });
|
toast.success(t('monitor.lbl.collectDone'), { description: res.message });
|
||||||
loadTargets(true);
|
loadTargets(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(i18n.t('删除失败'), { description: (err as { message?: string }).message });
|
toast.error(t('monitor.lbl.deleteFail'), { description: (err as { message?: string }).message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,13 +150,13 @@ export function MiddlewareMonitor() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold"> {i18n.t('monitor.alertHelper.srcMiddleware')} </h2>
|
<h2 className="text-lg font-semibold"> {t('monitor.alertHelper.srcMiddleware')} </h2>
|
||||||
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">
|
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">
|
||||||
Redis 单机/集群 · MySQL · 30s 自动刷新
|
Redis 单机/集群 · MySQL · 30s 自动刷新
|
||||||
{lastRefreshAt && (
|
{lastRefreshAt && (
|
||||||
<span className="ml-2">
|
<span className="ml-2">
|
||||||
上次更新 {lastRefreshAt.toLocaleTimeString()}
|
上次更新 {lastRefreshAt.toLocaleTimeString()}
|
||||||
{refreshing && i18n.t(' · 刷新中...')}
|
{refreshing && t('monitor.lbl.refreshing')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@@ -193,10 +194,10 @@ export function MiddlewareMonitor() {
|
|||||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-12 text-center">
|
||||||
<Database className="h-12 w-12 mx-auto mb-4 text-[var(--dg-text-muted)]" />
|
<Database className="h-12 w-12 mx-auto mb-4 text-[var(--dg-text-muted)]" />
|
||||||
<p className="text-sm text-[var(--dg-text-muted)]"> {i18n.t('monitor.middlewaremonit.sbbe62a10')} </p>
|
<p className="text-sm text-[var(--dg-text-muted)]"> {t('monitor.middlewaremonit.sbbe62a10')} </p>
|
||||||
<div className="flex justify-center gap-2 mt-4">
|
<div className="flex justify-center gap-2 mt-4">
|
||||||
<Button size="sm" onClick={handleSeedDefaults} disabled={seeding}> {i18n.t('monitor.middlewaremonit.se5caf12b')} </Button>
|
<Button size="sm" onClick={handleSeedDefaults} disabled={seeding}> {t('monitor.middlewaremonit.se5caf12b')} </Button>
|
||||||
<Button size="sm" variant="outline" onClick={() => openCreate()}> {i18n.t('monitor.middlewaremonit.s63edd678')} </Button>
|
<Button size="sm" variant="outline" onClick={() => openCreate()}> {t('monitor.middlewaremonit.s63edd678')} </Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -206,9 +207,9 @@ export function MiddlewareMonitor() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||||
{[
|
{[
|
||||||
{ label: i18n.t('监控实例'), value: targets.length, icon: Activity },
|
{ label: t('monitor.lbl.instances'), value: targets.length, icon: Activity },
|
||||||
{ label: i18n.t('agent.agentOnline'), value: onlineCount, icon: Database, color: 'dg-text-green' },
|
{ label: t('agent.agentOnline'), value: onlineCount, icon: Database, color: 'dg-text-green' },
|
||||||
{ label: i18n.t('monitor.hostDashboard.alert'), value: warnCount, icon: Activity, color: warnCount > 0 ? 'dg-text-yellow' : 'dg-text-green' },
|
{ label: t('monitor.hostDashboard.alert'), value: warnCount, icon: Activity, color: warnCount > 0 ? 'dg-text-yellow' : 'dg-text-green' },
|
||||||
{ label: 'Redis', value: redisTargets.length, icon: Database, color: 'dg-text-red' },
|
{ label: 'Redis', value: redisTargets.length, icon: Database, color: 'dg-text-red' },
|
||||||
{ label: 'MySQL', value: mysqlTargets.length, icon: Server, color: 'dg-text-blue' },
|
{ label: 'MySQL', value: mysqlTargets.length, icon: Server, color: 'dg-text-blue' },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
@@ -251,7 +252,7 @@ export function MiddlewareMonitor() {
|
|||||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
|
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
|
||||||
暂无 {tab === 'redis' ? 'Redis' : 'MySQL'} 实例,
|
暂无 {tab === 'redis' ? 'Redis' : 'MySQL'} 实例,
|
||||||
<button type="button" className="dg-text-cyan ml-1 underline" onClick={() => openCreate(tab)}> {i18n.t('monitor.middlewaremonit.s8ec8a6e8')} </button>
|
<button type="button" className="dg-text-cyan ml-1 underline" onClick={() => openCreate(tab)}> {t('monitor.middlewaremonit.s8ec8a6e8')} </button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ import { HostMonitor } from './HostMonitor';
|
|||||||
import { MiddlewareMonitor } from './MiddlewareMonitor';
|
import { MiddlewareMonitor } from './MiddlewareMonitor';
|
||||||
import { SNMPMonitor } from './SNMPMonitor';
|
import { SNMPMonitor } from './SNMPMonitor';
|
||||||
import { SiteMonitor } from './SiteMonitor';
|
import { SiteMonitor } from './SiteMonitor';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
type TabId = 'host' | 'middleware' | 'snmp' | 'site';
|
type TabId = 'host' | 'middleware' | 'snmp' | 'site';
|
||||||
|
|
||||||
const tabs: { id: TabId; name: string; description: string }[] = [
|
const tabs: { id: TabId; name: string; description: string }[] = [
|
||||||
{ id: 'host', name: i18n.t('monitor.alertRulesTab.catHost'), description: i18n.t('主机监控') },
|
{ id: 'host', name: i18n.t('monitor.alertRulesTab.catHost'), description: i18n.t('monitor.lbl.hostMonitor') },
|
||||||
{ id: 'middleware', name: i18n.t('monitor.alertRulesTab.catMiddleware'), description: i18n.t('中间件') },
|
{ id: 'middleware', name: i18n.t('monitor.alertRulesTab.catMiddleware'), description: i18n.t('monitor.lbl.middleware') },
|
||||||
{ id: 'snmp', name: 'SNMP', description: i18n.t('网络设备 / BMC 采集与趋势') },
|
{ id: 'snmp', name: 'SNMP', description: i18n.t('monitor.lbl.snmpTitle') },
|
||||||
{ id: 'site', name: i18n.t('monitor.alertRulesTab.catSite'), description: i18n.t('站点拨测') },
|
{ id: 'site', name: i18n.t('monitor.alertRulesTab.catSite'), description: i18n.t('monitor.lbl.siteProbe') },
|
||||||
];
|
];
|
||||||
|
|
||||||
const TAB_IDS: TabId[] = ['host', 'middleware', 'snmp', 'site'];
|
const TAB_IDS: TabId[] = ['host', 'middleware', 'snmp', 'site'];
|
||||||
@@ -25,6 +26,7 @@ function parseTab(raw: string | null): TabId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MonitorCenter() {
|
export function MonitorCenter() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [activeTab, setActiveTab] = useState<TabId>(() => parseTab(searchParams.get('tab')));
|
const [activeTab, setActiveTab] = useState<TabId>(() => parseTab(searchParams.get('tab')));
|
||||||
|
|
||||||
@@ -43,8 +45,8 @@ export function MonitorCenter() {
|
|||||||
return (
|
return (
|
||||||
<PageShell
|
<PageShell
|
||||||
variant="list"
|
variant="list"
|
||||||
title={i18n.t('monitor.title')}
|
title={t('monitor.title')}
|
||||||
subtitle={current?.description ?? i18n.t('全面的基础设施与应用监控')}
|
subtitle={current?.description ?? t('monitor.lbl.centerSubtitle')}
|
||||||
icon={<Activity className="h-5 w-5" />}
|
icon={<Activity className="h-5 w-5" />}
|
||||||
tabs={tabs.map((t) => ({ id: t.id, label: t.name }))}
|
tabs={tabs.map((t) => ({ id: t.id, label: t.name }))}
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { SnmpOverviewCharts } from './snmp-monitor/SnmpOverviewCharts';
|
|||||||
import { SnmpDeviceChartsPanel } from './snmp-monitor/SnmpDeviceChartsPanel';
|
import { SnmpDeviceChartsPanel } from './snmp-monitor/SnmpDeviceChartsPanel';
|
||||||
import { SnmpTemplatesCollapsible, type SnmpTemplate } from './snmp-monitor/SnmpTemplatesCollapsible';
|
import { SnmpTemplatesCollapsible, type SnmpTemplate } from './snmp-monitor/SnmpTemplatesCollapsible';
|
||||||
import type { SnmpDevice, SnmpMetric, SnmpOverviewItem } from './snmp-monitor/helpers';
|
import type { SnmpDevice, SnmpMetric, SnmpOverviewItem } from './snmp-monitor/helpers';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface SnmpStats { total: number; online: number; abnormal: number }
|
interface SnmpStats { total: number; online: number; abnormal: number }
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ const keyMetrics = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function SNMPMonitor() {
|
export function SNMPMonitor() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [stats, setStats] = useState<SnmpStats>({ total: 0, online: 0, abnormal: 0 });
|
const [stats, setStats] = useState<SnmpStats>({ total: 0, online: 0, abnormal: 0 });
|
||||||
const [devices, setDevices] = useState<SnmpDevice[]>([]);
|
const [devices, setDevices] = useState<SnmpDevice[]>([]);
|
||||||
const [overview, setOverview] = useState<SnmpOverviewItem[]>([]);
|
const [overview, setOverview] = useState<SnmpOverviewItem[]>([]);
|
||||||
@@ -105,7 +106,7 @@ export function SNMPMonitor() {
|
|||||||
|
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
if (!form.name.trim() || !form.ip.trim() || !form.template_id) {
|
if (!form.name.trim() || !form.ip.trim() || !form.template_id) {
|
||||||
toast.error(i18n.t('请填写设备名称、IP 并选择 OID 模板'));
|
toast.error(t('monitor.lbl.fillRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@@ -118,12 +119,12 @@ export function SNMPMonitor() {
|
|||||||
template_id: Number(form.template_id),
|
template_id: Number(form.template_id),
|
||||||
vendor: tpl?.vendor || '', device_type: tpl?.device_type || 'switch',
|
vendor: tpl?.vendor || '', device_type: tpl?.device_type || 'switch',
|
||||||
});
|
});
|
||||||
toast.success(i18n.t('设备已添加'));
|
toast.success(t('monitor.lbl.deviceAdded'));
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setForm({ ...emptyForm });
|
setForm({ ...emptyForm });
|
||||||
refresh();
|
refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error((e as { message?: string }).message || i18n.t('monitor.dashboard.custom.toastPanelFailed'));
|
toast.error((e as { message?: string }).message || t('monitor.dashboard.custom.toastPanelFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -137,7 +138,7 @@ export function SNMPMonitor() {
|
|||||||
toast.success(`${d.name} 采集成功`);
|
toast.success(`${d.name} 采集成功`);
|
||||||
refresh();
|
refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error((err as { message?: string }).message || i18n.t('采集失败'));
|
toast.error((err as { message?: string }).message || t('monitor.lbl.collectFail'));
|
||||||
refresh();
|
refresh();
|
||||||
} finally {
|
} finally {
|
||||||
setCollectingId(null);
|
setCollectingId(null);
|
||||||
@@ -163,18 +164,18 @@ export function SNMPMonitor() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
await snmpApi.deleteDevice(id);
|
await snmpApi.deleteDevice(id);
|
||||||
toast.success(i18n.t('monitor.silenceRules.deleted'));
|
toast.success(t('monitor.silenceRules.deleted'));
|
||||||
if (selectedDevice?.id === id) setSelectedDevice(null);
|
if (selectedDevice?.id === id) setSelectedDevice(null);
|
||||||
refresh();
|
refresh();
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(i18n.t('kubernetes.msg.deleteFailed2'));
|
toast.error(t('kubernetes.msg.deleteFailed2'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusBadge = (status?: string) => {
|
const statusBadge = (status?: string) => {
|
||||||
if (status === 'online') return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30"> {i18n.t('agent.agentOnline')} </Badge>;
|
if (status === 'online') return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30"> {t('agent.agentOnline')} </Badge>;
|
||||||
if (status === 'error') return <Badge className="bg-red-500/15 dg-text-red border-red-500/30"> {i18n.t('agent.heartbeatAbnormal')} </Badge>;
|
if (status === 'error') return <Badge className="bg-red-500/15 dg-text-red border-red-500/30"> {t('agent.heartbeatAbnormal')} </Badge>;
|
||||||
return <Badge className="bg-[var(--dg-bg-active)]/15 dg-text-secondary border-[var(--dg-border)]"> {i18n.t('agent.agentOffline')} </Badge>;
|
return <Badge className="bg-[var(--dg-bg-active)]/15 dg-text-secondary border-[var(--dg-border)]"> {t('agent.agentOffline')} </Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -197,13 +198,13 @@ export function SNMPMonitor() {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-md bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<DialogContent className="sm:max-w-md bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle> {i18n.t('monitor.snmpmonitortsx.s29b90990')} </DialogTitle>
|
<DialogTitle> {t('monitor.snmpmonitortsx.s29b90990')} </DialogTitle>
|
||||||
<DialogDescription> {i18n.t('monitor.snmpmonitortsx.s11ea7d0c')} </DialogDescription>
|
<DialogDescription> {t('monitor.snmpmonitortsx.s11ea7d0c')} </DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-3 py-2">
|
<div className="grid gap-3 py-2">
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label> {i18n.t('monitor.snmpmonitortsx.s9f694f60')} </Label>
|
<Label> {t('monitor.snmpmonitortsx.s9f694f60')} </Label>
|
||||||
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder={i18n.t('核心交换机-A')} />
|
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder={t('monitor.lbl.demoSwitch')} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
@@ -211,14 +212,14 @@ export function SNMPMonitor() {
|
|||||||
<Input value={form.ip} onChange={(e) => setForm({ ...form, ip: e.target.value })} placeholder="192.168.1.1" />
|
<Input value={form.ip} onChange={(e) => setForm({ ...form, ip: e.target.value })} placeholder="192.168.1.1" />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label> {i18n.t('system.ldapPort')} </Label>
|
<Label> {t('system.ldapPort')} </Label>
|
||||||
<Input type="number" value={form.port} onChange={(e) => setForm({ ...form, port: Number(e.target.value) || 161 })} />
|
<Input type="number" value={form.port} onChange={(e) => setForm({ ...form, port: Number(e.target.value) || 161 })} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label>OID 模板</Label>
|
<Label>OID 模板</Label>
|
||||||
<Select value={form.template_id} onValueChange={(v) => setForm({ ...form, template_id: v })}>
|
<Select value={form.template_id} onValueChange={(v) => setForm({ ...form, template_id: v })}>
|
||||||
<SelectTrigger><SelectValue placeholder={i18n.t('pipeline.selectTemplate')} /></SelectTrigger>
|
<SelectTrigger><SelectValue placeholder={t('pipeline.selectTemplate')} /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{templates.map((t) => (
|
{templates.map((t) => (
|
||||||
<SelectItem key={t.id} value={String(t.id)}>{t.name} ({t.vendor})</SelectItem>
|
<SelectItem key={t.id} value={String(t.id)}>{t.name} ({t.vendor})</SelectItem>
|
||||||
@@ -244,7 +245,7 @@ export function SNMPMonitor() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setDialogOpen(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
|
<Button variant="outline" onClick={() => setDialogOpen(false)}> {t('rbac.jobRole.cancel')} </Button>
|
||||||
<Button onClick={handleAdd} disabled={submitting}>
|
<Button onClick={handleAdd} disabled={submitting}>
|
||||||
{submitting && <Loader2 className="h-4 w-4 animate-spin mr-1" />}确认
|
{submitting && <Loader2 className="h-4 w-4 animate-spin mr-1" />}确认
|
||||||
</Button>
|
</Button>
|
||||||
@@ -257,10 +258,10 @@ export function SNMPMonitor() {
|
|||||||
{/* 紧凑统计 */}
|
{/* 紧凑统计 */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||||
{[
|
{[
|
||||||
{ label: i18n.t('设备总数'), value: stats.total, color: '' },
|
{ label: t('monitor.lbl.totalDevices'), value: stats.total, color: '' },
|
||||||
{ label: i18n.t('agent.agentOnline'), value: stats.online, color: 'dg-text-green' },
|
{ label: t('agent.agentOnline'), value: stats.online, color: 'dg-text-green' },
|
||||||
{ label: i18n.t('agent.heartbeatAbnormal'), value: stats.abnormal, color: 'dg-text-red' },
|
{ label: t('agent.heartbeatAbnormal'), value: stats.abnormal, color: 'dg-text-red' },
|
||||||
{ label: i18n.t('OID 模板'), value: templates.length, color: 'dg-text-cyan' },
|
{ label: t('monitor.lbl.oidTemplate'), value: templates.length, color: 'dg-text-cyan' },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<div key={item.label} className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-3 py-2 text-center">
|
<div key={item.label} className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-3 py-2 text-center">
|
||||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
||||||
@@ -293,9 +294,9 @@ export function SNMPMonitor() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="text-xs"> {i18n.t('kubernetes.secret.secretName')} </TableHead>
|
<TableHead className="text-xs"> {t('kubernetes.secret.secretName')} </TableHead>
|
||||||
<TableHead className="text-xs">IP</TableHead>
|
<TableHead className="text-xs">IP</TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('system.terminalStatus')} </TableHead>
|
<TableHead className="text-xs"> {t('system.terminalStatus')} </TableHead>
|
||||||
<TableHead className="w-24" />
|
<TableHead className="w-24" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -351,7 +352,7 @@ export function SNMPMonitor() {
|
|||||||
<DialogDescription>{metricsDevice?.ip} · {templateName(metricsDevice?.template_id || 0)}</DialogDescription>
|
<DialogDescription>{metricsDevice?.ip} · {templateName(metricsDevice?.template_id || 0)}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{metricsLoading ? (
|
{metricsLoading ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground"> {i18n.t('monitor.logPanel.logResults.loading')} </div>
|
<div className="py-8 text-center text-sm text-muted-foreground"> {t('monitor.logPanel.logResults.loading')} </div>
|
||||||
) : metrics.length === 0 ? (
|
) : metrics.length === 0 ? (
|
||||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||||
暂无指标,请先采集
|
暂无指标,请先采集
|
||||||
@@ -372,7 +373,7 @@ export function SNMPMonitor() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead> {i18n.t('cloud.colMetric')} </TableHead><TableHead> {i18n.t('monitor.logIndex')} </TableHead><TableHead>值</TableHead><TableHead>OID</TableHead>
|
<TableHead> {t('cloud.colMetric')} </TableHead><TableHead> {t('monitor.logIndex')} </TableHead><TableHead>值</TableHead><TableHead>OID</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useConfirm } from '@/components/ui/confirm-dialog';
|
|||||||
import type { JsonData } from '@/types/common';
|
import type { JsonData } from '@/types/common';
|
||||||
import { SiteProtocolPieChart } from './site-monitor/SiteProtocolPieChart';
|
import { SiteProtocolPieChart } from './site-monitor/SiteProtocolPieChart';
|
||||||
import { SiteTaskDetailPage } from './site-monitor/SiteTaskDetailPage';
|
import { SiteTaskDetailPage } from './site-monitor/SiteTaskDetailPage';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import {
|
import {
|
||||||
PROBE_TYPE_OPTIONS, type ProbeType, type SiteStats, type SiteTask,
|
PROBE_TYPE_OPTIONS, type ProbeType, type SiteStats, type SiteTask,
|
||||||
@@ -50,6 +51,7 @@ function alertStatusBadge(status?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SiteMonitor() {
|
export function SiteMonitor() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const taskIdParam = searchParams.get('task');
|
const taskIdParam = searchParams.get('task');
|
||||||
const [stats, setStats] = useState<SiteStats | null>(null);
|
const [stats, setStats] = useState<SiteStats | null>(null);
|
||||||
@@ -137,7 +139,7 @@ export function SiteMonitor() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!form.name.trim() || !form.url.trim()) { toast.error(i18n.t('名称和地址不能为空')); return; }
|
if (!form.name.trim() || !form.url.trim()) { toast.error(t('monitor.lbl.nameAddrRequired')); return; }
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const payload = {
|
const payload = {
|
||||||
name: form.name.trim(), url: form.url.trim(), type: form.type.toLowerCase(),
|
name: form.name.trim(), url: form.url.trim(), type: form.type.toLowerCase(),
|
||||||
@@ -146,34 +148,34 @@ export function SiteMonitor() {
|
|||||||
expected_status_code: Number(form.expected_status_code) || 200,
|
expected_status_code: Number(form.expected_status_code) || 200,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
if (editingTaskId) { await siteApi.updateTask(editingTaskId, payload); toast.success(i18n.t('任务已更新')); }
|
if (editingTaskId) { await siteApi.updateTask(editingTaskId, payload); toast.success(t('monitor.lbl.taskUpdated')); }
|
||||||
else { await siteApi.createTask(payload); toast.success(i18n.t('拨测任务已创建')); }
|
else { await siteApi.createTask(payload); toast.success(t('monitor.lbl.taskCreated')); }
|
||||||
setFormOpen(false); setEditingTaskId(null);
|
setFormOpen(false); setEditingTaskId(null);
|
||||||
loadTasks(); loadStats();
|
loadTasks(); loadStats();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error((e as JsonData).message || (editingTaskId ? i18n.t('kubernetes.deployment.imageUpdateFailed') : i18n.t('monitor.dashboard.custom.toastCreateFailed')));
|
toast.error((e as JsonData).message || (editingTaskId ? t('kubernetes.deployment.imageUpdateFailed') : t('monitor.dashboard.custom.toastCreateFailed')));
|
||||||
} finally { setSaving(false); }
|
} finally { setSaving(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (task: SiteTask) => {
|
const handleDelete = async (task: SiteTask) => {
|
||||||
const ok = await _confirm({ title: i18n.t('monitor.deleteProbe'), description: `确定删除「${task.name}」吗?`, variant: 'destructive' });
|
const ok = await _confirm({ title: t('monitor.deleteProbe'), description: `确定删除「${task.name}」吗?`, variant: 'destructive' });
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await siteApi.deleteTask(task.id);
|
await siteApi.deleteTask(task.id);
|
||||||
if (taskIdParam === String(task.id)) {
|
if (taskIdParam === String(task.id)) {
|
||||||
closeDetail();
|
closeDetail();
|
||||||
}
|
}
|
||||||
toast.success(i18n.t('任务已删除'));
|
toast.success(t('monitor.lbl.taskDeleted'));
|
||||||
loadTasks(); loadStats();
|
loadTasks(); loadStats();
|
||||||
} catch (e) { toast.error((e as JsonData).message || i18n.t('kubernetes.msg.deleteFailed2')); }
|
} catch (e) { toast.error((e as JsonData).message || t('kubernetes.msg.deleteFailed2')); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle = async (task: SiteTask) => {
|
const handleToggle = async (task: SiteTask) => {
|
||||||
try {
|
try {
|
||||||
await siteApi.toggleTask(task.id, !task.enabled);
|
await siteApi.toggleTask(task.id, !task.enabled);
|
||||||
toast.success(task.enabled ? i18n.t('cloud.stopped') : i18n.t('aiops.llmEnabled'));
|
toast.success(task.enabled ? t('cloud.stopped') : t('aiops.llmEnabled'));
|
||||||
loadTasks(); loadStats();
|
loadTasks(); loadStats();
|
||||||
} catch (e) { toast.error((e as JsonData).message || i18n.t('cloud.operationFailed')); }
|
} catch (e) { toast.error((e as JsonData).message || t('cloud.operationFailed')); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const targetMeta = probeTypeMeta[form.type];
|
const targetMeta = probeTypeMeta[form.type];
|
||||||
@@ -192,8 +194,8 @@ export function SiteMonitor() {
|
|||||||
if (!detailTask) {
|
if (!detailTask) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 text-center py-16">
|
<div className="space-y-4 text-center py-16">
|
||||||
<p className="text-sm text-[var(--dg-text-muted)]"> {i18n.t('monitor.sitemonitortsx.s62a0023a')} </p>
|
<p className="text-sm text-[var(--dg-text-muted)]"> {t('monitor.sitemonitortsx.s62a0023a')} </p>
|
||||||
<Button variant="outline" size="sm" onClick={closeDetail}> {i18n.t('monitor.siteTask.backToList')} </Button>
|
<Button variant="outline" size="sm" onClick={closeDetail}> {t('monitor.siteTask.backToList')} </Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -210,7 +212,7 @@ export function SiteMonitor() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold"> {i18n.t('monitor.alertRulesTab.catSite')} </h2>
|
<h2 className="text-lg font-semibold"> {t('monitor.alertRulesTab.catSite')} </h2>
|
||||||
<p className="text-xs text-[var(--dg-text-muted)]">HTTP Web 拨测 · SSL 证书检测 · TCP / DNS / PING</p>
|
<p className="text-xs text-[var(--dg-text-muted)]">HTTP Web 拨测 · SSL 证书检测 · TCP / DNS / PING</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -227,11 +229,11 @@ export function SiteMonitor() {
|
|||||||
<div className="xl:col-span-8 space-y-4">
|
<div className="xl:col-span-8 space-y-4">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||||
{[
|
{[
|
||||||
{ label: i18n.t('总任务'), value: stats?.total_tasks ?? '--' },
|
{ label: t('monitor.lbl.totalTasks'), value: stats?.total_tasks ?? '--' },
|
||||||
{ label: i18n.t('agent.heartbeatNormal'), value: stats?.up_count ?? '--', color: 'dg-text-green' },
|
{ label: t('agent.heartbeatNormal'), value: stats?.up_count ?? '--', color: 'dg-text-green' },
|
||||||
{ label: i18n.t('agent.heartbeatAbnormal'), value: stats?.down_count ?? '--', color: 'dg-text-red' },
|
{ label: t('agent.heartbeatAbnormal'), value: stats?.down_count ?? '--', color: 'dg-text-red' },
|
||||||
{ label: i18n.t('可用率(24h)'), value: availStr, color: 'dg-text-blue' },
|
{ label: t('monitor.lbl.availability24h'), value: availStr, color: 'dg-text-blue' },
|
||||||
{ label: i18n.t('探测次数'), value: probeCountStr },
|
{ label: t('monitor.lbl.totalProbes'), value: probeCountStr },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<div key={item.label} className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-3 py-2 text-center">
|
<div key={item.label} className="rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-3 py-2 text-center">
|
||||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
||||||
@@ -245,8 +247,8 @@ export function SiteMonitor() {
|
|||||||
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
|
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Activity className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
<Activity className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||||
<h3 className="text-sm font-medium"> {i18n.t('monitor.sitemonitortsx.s30319e1c')} </h3>
|
<h3 className="text-sm font-medium"> {t('monitor.sitemonitortsx.s30319e1c')} </h3>
|
||||||
<span className="text-[10px] text-[var(--dg-text-muted)]"> {i18n.t('monitor.sitemonitortsx.sa5e6b654')} </span>
|
<span className="text-[10px] text-[var(--dg-text-muted)]"> {t('monitor.sitemonitortsx.sa5e6b654')} </span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{FILTER_TYPES.map((type) => {
|
{FILTER_TYPES.map((type) => {
|
||||||
@@ -263,7 +265,7 @@ export function SiteMonitor() {
|
|||||||
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] border border-transparent hover:text-[var(--dg-text-primary)]'
|
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] border border-transparent hover:text-[var(--dg-text-primary)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{type === 'ALL' ? i18n.t('system.auditAll') : type} {count > 0 && count}
|
{type === 'ALL' ? t('system.auditAll') : type} {count > 0 && count}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -272,19 +274,19 @@ export function SiteMonitor() {
|
|||||||
|
|
||||||
{filteredTasks.length === 0 ? (
|
{filteredTasks.length === 0 ? (
|
||||||
<div className="text-center py-10 text-sm text-[var(--dg-text-muted)]">
|
<div className="text-center py-10 text-sm text-[var(--dg-text-muted)]">
|
||||||
{tasks.length === 0 ? i18n.t('暂无拨测任务') : i18n.t('当前筛选无任务')}
|
{tasks.length === 0 ? t('monitor.lbl.noTasks') : t('monitor.lbl.noFilteredTasks')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||||
<TableHead className="text-xs"> {i18n.t('kubernetes.secret.secretName')} </TableHead>
|
<TableHead className="text-xs"> {t('kubernetes.secret.secretName')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('system.credentialType')} </TableHead>
|
<TableHead className="text-xs"> {t('system.credentialType')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('monitor.siteTask.frequency')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.siteTask.frequency')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('全部')} </TableHead>
|
<TableHead className="text-xs"> {t('common.all')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('全部')} </TableHead>
|
<TableHead className="text-xs"> {t('common.all')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('system.userActive')} </TableHead>
|
<TableHead className="text-xs"> {t('system.userActive')} </TableHead>
|
||||||
<TableHead className="text-xs w-24" />
|
<TableHead className="text-xs w-24" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -343,16 +345,16 @@ export function SiteMonitor() {
|
|||||||
<Dialog open={formOpen} onOpenChange={(open) => { setFormOpen(open); if (!open) setEditingTaskId(null); }}>
|
<Dialog open={formOpen} onOpenChange={(open) => { setFormOpen(open); if (!open) setEditingTaskId(null); }}>
|
||||||
<DialogContent className="sm:max-w-md bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<DialogContent className="sm:max-w-md bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editingTaskId ? i18n.t('monitor.editProbe') : i18n.t('monitor.addProbe')}</DialogTitle>
|
<DialogTitle>{editingTaskId ? t('monitor.editProbe') : t('monitor.addProbe')}</DialogTitle>
|
||||||
<DialogDescription className="text-[var(--dg-text-muted)]"> {i18n.t('monitor.sitemonitortsx.s6173cf9a')} </DialogDescription>
|
<DialogDescription className="text-[var(--dg-text-muted)]"> {t('monitor.sitemonitortsx.s6173cf9a')} </DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-2">
|
<div className="grid gap-4 py-2">
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label> {i18n.t('automation.cronJobName')} </Label>
|
<Label> {t('automation.cronJobName')} </Label>
|
||||||
<Input placeholder={i18n.t('例如:官网首页')} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
<Input placeholder={t('monitor.lbl.urlPlaceholder')} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label> {i18n.t('保存')} </Label>
|
<Label> {t('common.save')} </Label>
|
||||||
<Select value={form.type} onValueChange={(v) => {
|
<Select value={form.type} onValueChange={(v) => {
|
||||||
const t = v as ProbeType;
|
const t = v as ProbeType;
|
||||||
setForm({ ...form, type: t, frequency_sec: editingTaskId ? form.frequency_sec : defaultProbeFrequency(t) });
|
setForm({ ...form, type: t, frequency_sec: editingTaskId ? form.frequency_sec : defaultProbeFrequency(t) });
|
||||||
@@ -370,30 +372,30 @@ export function SiteMonitor() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label> {i18n.t('保存')} </Label>
|
<Label> {t('common.save')} </Label>
|
||||||
<Input type="number" min={MIN_PROBE_FREQUENCY[form.type]} max={86400} value={form.frequency_sec}
|
<Input type="number" min={MIN_PROBE_FREQUENCY[form.type]} max={86400} value={form.frequency_sec}
|
||||||
onChange={(e) => setForm({ ...form, frequency_sec: Number(e.target.value) || defaultProbeFrequency(form.type) })} />
|
onChange={(e) => setForm({ ...form, frequency_sec: Number(e.target.value) || defaultProbeFrequency(form.type) })} />
|
||||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{probeFrequencyHint(form.type)}</p>
|
<p className="text-[10px] text-[var(--dg-text-muted)]">{probeFrequencyHint(form.type)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label> {i18n.t('保存')} </Label>
|
<Label> {t('common.save')} </Label>
|
||||||
<Input type="number" min={1} max={300} value={form.timeout_sec}
|
<Input type="number" min={1} max={300} value={form.timeout_sec}
|
||||||
onChange={(e) => setForm({ ...form, timeout_sec: Number(e.target.value) || 10 })} />
|
onChange={(e) => setForm({ ...form, timeout_sec: Number(e.target.value) || 10 })} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{form.type === 'HTTP' && (
|
{form.type === 'HTTP' && (
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label> {i18n.t('保存')} </Label>
|
<Label> {t('common.save')} </Label>
|
||||||
<Input type="number" min={100} max={599} value={form.expected_status_code}
|
<Input type="number" min={100} max={599} value={form.expected_status_code}
|
||||||
onChange={(e) => setForm({ ...form, expected_status_code: Number(e.target.value) || 200 })} />
|
onChange={(e) => setForm({ ...form, expected_status_code: Number(e.target.value) || 200 })} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => { setFormOpen(false); setEditingTaskId(null); }} disabled={saving}> {i18n.t('rbac.jobRole.cancel')} </Button>
|
<Button variant="outline" onClick={() => { setFormOpen(false); setEditingTaskId(null); }} disabled={saving}> {t('rbac.jobRole.cancel')} </Button>
|
||||||
<Button onClick={handleSave} disabled={saving}>
|
<Button onClick={handleSave} disabled={saving}>
|
||||||
{saving && <Loader2 className="h-4 w-4 animate-spin mr-1" />}
|
{saving && <Loader2 className="h-4 w-4 animate-spin mr-1" />}
|
||||||
{editingTaskId ? i18n.t('task.script.save') : i18n.t('保存')}
|
{editingTaskId ? t('task.script.save') : t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ import {
|
|||||||
import { DEFAULT_PROBE_FORM, type ProbeForm } from './types';
|
import { DEFAULT_PROBE_FORM, type ProbeForm } from './types';
|
||||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface HealthProbeSectionProps {
|
interface HealthProbeSectionProps {
|
||||||
selectedHost: string;
|
selectedHost: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const { data: probes = [], isFetching, refetch } = useHealthProbes(selectedHost);
|
const { data: probes = [], isFetching, refetch } = useHealthProbes(selectedHost);
|
||||||
@@ -69,15 +70,15 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
|||||||
setProbeDialogOpen(false);
|
setProbeDialogOpen(false);
|
||||||
qc.invalidateQueries({ queryKey: queryKeys.hostMonitor.probes(selectedHost) });
|
qc.invalidateQueries({ queryKey: queryKeys.hostMonitor.probes(selectedHost) });
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(i18n.t('kubernetes.msg.saveFailed'));
|
toast.error(t('kubernetes.msg.saveFailed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteProbe = async (p: Record<string, unknown>) => {
|
const handleDeleteProbe = async (p: Record<string, unknown>) => {
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: i18n.t('删除探针'),
|
title: t('monitor.lbl.deleteProbe'),
|
||||||
description: `确定删除探针「${p.name}」?`,
|
description: `确定删除探针「${p.name}」?`,
|
||||||
confirmText: i18n.t('kubernetes.storage.delete2'),
|
confirmText: t('kubernetes.storage.delete2'),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -89,7 +90,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
|||||||
<>
|
<>
|
||||||
<TabsContent value="health_probes" className="mt-3 space-y-3">
|
<TabsContent value="health_probes" className="mt-3 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]"> {i18n.t('无限制')} </h3>
|
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]"> {t('monitor.lbl.unlimited')} </h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="sm" variant="outline" className="h-7 text-[11px]" onClick={() => refetch()} disabled={isFetching}>
|
<Button size="sm" variant="outline" className="h-7 text-[11px]" onClick={() => refetch()} disabled={isFetching}>
|
||||||
<RefreshCw className={cn('w-3 h-3 mr-1', isFetching && 'animate-spin')} />刷新
|
<RefreshCw className={cn('w-3 h-3 mr-1', isFetching && 'animate-spin')} />刷新
|
||||||
@@ -102,7 +103,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
|||||||
{probes.length === 0 ? (
|
{probes.length === 0 ? (
|
||||||
<Card className="bg-[var(--dg-bg-secondary)] border-dashed border-[var(--dg-border)]">
|
<Card className="bg-[var(--dg-bg-secondary)] border-dashed border-[var(--dg-border)]">
|
||||||
<CardContent className="p-8 text-center text-xs text-[var(--dg-text-muted)]">
|
<CardContent className="p-8 text-center text-xs text-[var(--dg-text-muted)]">
|
||||||
暂无健康探针,点击i18n.t('添加探针')创建
|
暂无健康探针,点击t('monitor.lbl.addProbe')创建
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
@@ -140,8 +141,8 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
|||||||
<div className="truncate font-mono">{String(p.exec_command)}</div>
|
<div className="truncate font-mono">{String(p.exec_command)}</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span> {i18n.t('无限制')} <span className={p.last_status === 'up' ? 'dg-text-green' : p.last_status === 'down' ? 'dg-text-red' : 'dg-text-secondary'}>
|
<span> {t('monitor.lbl.unlimited')} <span className={p.last_status === 'up' ? 'dg-text-green' : p.last_status === 'down' ? 'dg-text-red' : 'dg-text-secondary'}>
|
||||||
{p.last_status ? String(p.last_status).toUpperCase() : i18n.t('未检测')}
|
{p.last_status ? String(p.last_status).toUpperCase() : t('monitor.lbl.notDetected')}
|
||||||
</span></span>
|
</span></span>
|
||||||
{Number(p.last_latency_ms) > 0 && <span>延迟: {String(p.last_latency_ms)}ms</span>}
|
{Number(p.last_latency_ms) > 0 && <span>延迟: {String(p.last_latency_ms)}ms</span>}
|
||||||
{Number(p.fail_count) > 0 && <span className="dg-text-red">连续失败: {String(p.fail_count)}次</span>}
|
{Number(p.fail_count) > 0 && <span className="dg-text-red">连续失败: {String(p.fail_count)}次</span>}
|
||||||
@@ -159,23 +160,23 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
|||||||
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-lg">
|
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-[var(--dg-text-primary)]">
|
<DialogTitle className="text-[var(--dg-text-primary)]">
|
||||||
{editingProbe ? i18n.t('编辑探针') : i18n.t('添加探针')}
|
{editingProbe ? t('monitor.lbl.editProbe') : t('monitor.lbl.addProbe')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||||
{editingProbe ? i18n.t('编辑探针') : i18n.t('添加探针')}
|
{editingProbe ? t('monitor.lbl.editProbe') : t('monitor.lbl.addProbe')}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
|
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('>探针名称 <span className=')} <span className="dg-text-red">*</span></label>
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('>探针名称 <span className=')} <span className="dg-text-red">*</span></label>
|
||||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.name} onChange={e => setProbeForm({ ...probeForm, name: e.target.value })} placeholder={i18n.t('如:cex-agent 健康检查')} />
|
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.name} onChange={e => setProbeForm({ ...probeForm, name: e.target.value })} placeholder={t('如:cex-agent 健康检查')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('agent.installTarget')} </label>
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('agent.installTarget')} </label>
|
||||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.hostname} onChange={e => setProbeForm({ ...probeForm, hostname: e.target.value })} />
|
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.hostname} onChange={e => setProbeForm({ ...probeForm, hostname: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('kubernetes.container.probeType')} </label>
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('kubernetes.container.probeType')} </label>
|
||||||
<select className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.probe_type} onChange={e => setProbeForm({ ...probeForm, probe_type: e.target.value })}>
|
<select className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.probe_type} onChange={e => setProbeForm({ ...probeForm, probe_type: e.target.value })}>
|
||||||
<option value="http_get">HTTP GET</option>
|
<option value="http_get">HTTP GET</option>
|
||||||
<option value="tcp_socket">TCP Socket</option>
|
<option value="tcp_socket">TCP Socket</option>
|
||||||
@@ -188,9 +189,9 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
|||||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">HTTP URL <span className="dg-text-red">*</span></label>
|
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">HTTP URL <span className="dg-text-red">*</span></label>
|
||||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_url} onChange={e => setProbeForm({ ...probeForm, http_url: e.target.value })} placeholder="http://localhost:8080/health" /></div>
|
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_url} onChange={e => setProbeForm({ ...probeForm, http_url: e.target.value })} placeholder="http://localhost:8080/health" /></div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('无限制')} </label>
|
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('monitor.lbl.unlimited')} </label>
|
||||||
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_expect_code} onChange={e => setProbeForm({ ...probeForm, http_expect_code: Number(e.target.value) })} /></div>
|
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_expect_code} onChange={e => setProbeForm({ ...probeForm, http_expect_code: Number(e.target.value) })} /></div>
|
||||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('无限制')} </label>
|
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('monitor.lbl.unlimited')} </label>
|
||||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_expect_body} onChange={e => setProbeForm({ ...probeForm, http_expect_body: e.target.value })} /></div>
|
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_expect_body} onChange={e => setProbeForm({ ...probeForm, http_expect_body: e.target.value })} /></div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -206,24 +207,24 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{probeForm.probe_type === 'exec_command' && (
|
{probeForm.probe_type === 'exec_command' && (
|
||||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('aiops.assistantCommand')} <span className="dg-text-red">*</span></label>
|
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('aiops.assistantCommand')} <span className="dg-text-red">*</span></label>
|
||||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs font-mono text-[var(--dg-text-primary)]" value={probeForm.exec_command} onChange={e => setProbeForm({ ...probeForm, exec_command: e.target.value })} placeholder="pgrep -x mysqld" /></div>
|
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs font-mono text-[var(--dg-text-primary)]" value={probeForm.exec_command} onChange={e => setProbeForm({ ...probeForm, exec_command: e.target.value })} placeholder="pgrep -x mysqld" /></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('automation.remoteTimeout')} </label>
|
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('automation.remoteTimeout')} </label>
|
||||||
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.timeout} onChange={e => setProbeForm({ ...probeForm, timeout: Number(e.target.value) })} /></div>
|
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.timeout} onChange={e => setProbeForm({ ...probeForm, timeout: Number(e.target.value) })} /></div>
|
||||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('>执行命令 <span className=')} </label>
|
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('>执行命令 <span className=')} </label>
|
||||||
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.interval} onChange={e => setProbeForm({ ...probeForm, interval: Number(e.target.value) })} /></div>
|
<input type="number" className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.interval} onChange={e => setProbeForm({ ...probeForm, interval: Number(e.target.value) })} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {i18n.t('aiops.llmModelDesc')} </label>
|
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block"> {t('aiops.llmModelDesc')} </label>
|
||||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.description} onChange={e => setProbeForm({ ...probeForm, description: e.target.value })} /></div>
|
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.description} onChange={e => setProbeForm({ ...probeForm, description: e.target.value })} /></div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setProbeDialogOpen(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
|
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setProbeDialogOpen(false)}> {t('rbac.jobRole.cancel')} </Button>
|
||||||
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={handleSaveProbe} disabled={!probeForm.name || !probeForm.hostname}>
|
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={handleSaveProbe} disabled={!probeForm.name || !probeForm.hostname}>
|
||||||
{editingProbe ? i18n.t('task.script.save') : i18n.t('rbac.jobRole.create')}
|
{editingProbe ? t('task.script.save') : t('rbac.jobRole.create')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type TimeRangePresetId,
|
type TimeRangePresetId,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { presetToRange, toDatetimeLocalValue, formatRangeSummary } from './timeRange';
|
import { presetToRange, toDatetimeLocalValue, formatRangeSummary } from './timeRange';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: HostMonitorTimeRange;
|
value: HostMonitorTimeRange;
|
||||||
@@ -17,6 +17,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HostTimeRangePicker({ value, onChange }: Props) {
|
export function HostTimeRangePicker({ value, onChange }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [customOpen, setCustomOpen] = useState(false);
|
const [customOpen, setCustomOpen] = useState(false);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const [customStart, setCustomStart] = useState(toDatetimeLocalValue(new Date(now.getTime() - 3600_000)));
|
const [customStart, setCustomStart] = useState(toDatetimeLocalValue(new Date(now.getTime() - 3600_000)));
|
||||||
@@ -45,7 +46,7 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
|
|||||||
const e = start.getTime() <= end.getTime() ? end : start;
|
const e = start.getTime() <= end.getTime() ? end : start;
|
||||||
onChange({
|
onChange({
|
||||||
preset: 'custom',
|
preset: 'custom',
|
||||||
label: i18n.t('aiops.providerCustom'),
|
label: t('aiops.providerCustom'),
|
||||||
start: s.toISOString(),
|
start: s.toISOString(),
|
||||||
end: e.toISOString(),
|
end: e.toISOString(),
|
||||||
buckets: Math.min(120, Math.max(36, Math.ceil((e.getTime() - s.getTime()) / 60_000 / 2))),
|
buckets: Math.min(120, Math.max(36, Math.ceil((e.getTime() - s.getTime()) / 60_000 / 2))),
|
||||||
@@ -86,10 +87,10 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
{customOpen && (
|
{customOpen && (
|
||||||
<div className="absolute right-0 top-full mt-1 z-50 w-72 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-primary)] p-3 shadow-lg">
|
<div className="absolute right-0 top-full mt-1 z-50 w-72 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-primary)] p-3 shadow-lg">
|
||||||
<p className="text-xs font-medium text-[var(--dg-text-primary)] mb-2"> {i18n.t('无限制')} </p>
|
<p className="text-xs font-medium text-[var(--dg-text-primary)] mb-2"> {t('monitor.lbl.unlimited')} </p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] dg-text-muted"> {i18n.t('common.start')} </label>
|
<label className="text-[10px] dg-text-muted"> {t('common.start')} </label>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={customStart}
|
value={customStart}
|
||||||
@@ -98,7 +99,7 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] dg-text-muted"> {i18n.t('无限制')} </label>
|
<label className="text-[10px] dg-text-muted"> {t('monitor.lbl.unlimited')} </label>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={customEnd}
|
value={customEnd}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// 端口策略对话框 // Logan.Li
|
// 端口策略对话框 // Logan.Li
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { parsePortList } from '@/utils/validate';
|
import { parsePortList } from '@/utils/validate';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
@@ -27,16 +27,17 @@ export function PortPolicyDialog({
|
|||||||
onSave,
|
onSave,
|
||||||
saving,
|
saving,
|
||||||
}: PortPolicyDialogProps) {
|
}: PortPolicyDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-md">
|
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-[var(--dg-text-primary)]"> {i18n.t('monitor.portpolicydialo.s868e3dd0')} </DialogTitle>
|
<DialogTitle className="text-[var(--dg-text-primary)]"> {t('monitor.portpolicydialo.s868e3dd0')} </DialogTitle>
|
||||||
<DialogDescription className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.portpolicydialo.scd27ad47')} </DialogDescription>
|
<DialogDescription className="text-[var(--dg-text-secondary)]"> {t('monitor.portpolicydialo.scd27ad47')} </DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block"> {i18n.t('monitor.portpolicydialo.s7824bdd1')} </label>
|
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block"> {t('monitor.portpolicydialo.s7824bdd1')} </label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 dg-text-blue' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 dg-text-blue' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||||
@@ -53,8 +54,8 @@ export function PortPolicyDialog({
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1.5 leading-relaxed">
|
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1.5 leading-relaxed">
|
||||||
{portPolicyMode === 'whitelist'
|
{portPolicyMode === 'whitelist'
|
||||||
? i18n.t('所有发现的端口都上报,任意端口变化都会触发告警 + 恢复通知。')
|
? t('monitor.lbl.allPortsReport')
|
||||||
: i18n.t('仅上报选中端口的变化,其余端口变化静默忽略(不告警)。系统进程端口(sshd/rpcbind/systemd等)永远忽略。')}
|
: t('monitor.lbl.selectedOnlyReport')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ export function PortPolicyDialog({
|
|||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
onWhitelistChange(parsePortList(val));
|
onWhitelistChange(parsePortList(val));
|
||||||
}}
|
}}
|
||||||
placeholder={portPolicyMode === 'whitelist' ? i18n.t('白名单模式下所有端口自动上报') : '80,443,3306,6379'}
|
placeholder={portPolicyMode === 'whitelist' ? t('monitor.lbl.whitelistAutoReport') : '80,443,3306,6379'}
|
||||||
readOnly={portPolicyMode === 'whitelist'}
|
readOnly={portPolicyMode === 'whitelist'}
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
|
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
|
||||||
@@ -83,14 +84,14 @@ export function PortPolicyDialog({
|
|||||||
|
|
||||||
<div className="bg-amber-500/5 border border-amber-500/20 rounded p-2.5">
|
<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">
|
<p className="text-[10px] text-amber-300/80 leading-relaxed">
|
||||||
⚠️ 无论选择哪种模式,以下系统进程端口<b> {i18n.t('monitor.portpolicydialo.s2ff12d51')} </b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
|
⚠️ 无论选择哪种模式,以下系统进程端口<b> {t('monitor.portpolicydialo.s2ff12d51')} </b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => onOpenChange(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
|
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => onOpenChange(false)}> {t('rbac.jobRole.cancel')} </Button>
|
||||||
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={onSave} disabled={saving}>
|
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={onSave} disabled={saving}>
|
||||||
{saving ? i18n.t('monitor.silenceDialog.saving') : i18n.t('保存中…')}
|
{saving ? t('monitor.silenceDialog.saving') : t('monitor.lbl.saving')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { ChartToolbar } from './ChartToolbar';
|
import { ChartToolbar } from './ChartToolbar';
|
||||||
import { MonitorChartTooltip } from '../shared/monitorChartTheme';
|
import { MonitorChartTooltip } from '../shared/monitorChartTheme';
|
||||||
import { ChartRangeHint } from '../shared/ChartRangeHint';
|
import { ChartRangeHint } from '../shared/ChartRangeHint';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface ChartSeries {
|
export interface ChartSeries {
|
||||||
dataKey: string;
|
dataKey: string;
|
||||||
@@ -192,6 +192,7 @@ export const ZoomableAreaChart = forwardRef<ZoomableChartHandle, Props>(function
|
|||||||
onZoomedChange,
|
onZoomedChange,
|
||||||
referenceLines,
|
referenceLines,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const uid = useId().replace(/:/g, '');
|
const uid = useId().replace(/:/g, '');
|
||||||
const gradPrefix = `grad-${chartId}-${uid}`;
|
const gradPrefix = `grad-${chartId}-${uid}`;
|
||||||
|
|
||||||
@@ -316,11 +317,11 @@ export const ZoomableAreaChart = forwardRef<ZoomableChartHandle, Props>(function
|
|||||||
<DialogContent className={dialogClass}>
|
<DialogContent className={dialogClass}>
|
||||||
<DialogHeader className="shrink-0 flex-row items-start justify-between gap-4 space-y-0 pr-10">
|
<DialogHeader className="shrink-0 flex-row items-start justify-between gap-4 space-y-0 pr-10">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<DialogTitle className="text-base truncate">{title || i18n.t('指标详情')}</DialogTitle>
|
<DialogTitle className="text-base truncate">{title || t('monitor.lbl.metricDetail')}</DialogTitle>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p className="text-xs dg-text-muted mt-0.5 truncate">{subtitle}</p>
|
<p className="text-xs dg-text-muted mt-0.5 truncate">{subtitle}</p>
|
||||||
)}
|
)}
|
||||||
<DialogDescription className="sr-only"> {i18n.t('选择主机...')} </DialogDescription>
|
<DialogDescription className="sr-only"> {t('monitor.lbl.selectHost')} </DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
<ChartToolbar
|
<ChartToolbar
|
||||||
size="md"
|
size="md"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Cpu, Clock, Server, HardDrive, Activity } from 'lucide-react';
|
import { Cpu, Clock, Server, HardDrive, Activity } from 'lucide-react';
|
||||||
import type { AggType, BucketPoint, HostRecord, RefLine } from '../types';
|
import type { AggType, BucketPoint, HostRecord, RefLine } from '../types';
|
||||||
import { MetricChartCard } from '../MetricChartCard';
|
import { MetricChartCard } from '../MetricChartCard';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface BasicTabProps {
|
export interface BasicTabProps {
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -16,19 +16,20 @@ export interface BasicTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BasicTab({ metrics, latest, currentVal, aggType, pctRefLines }: BasicTabProps) {
|
export function BasicTab({ metrics, latest, currentVal, aggType, pctRefLines }: BasicTabProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<MetricChartCard title={i18n.t('CPU 使用率(%)')} value={currentVal('cpu')} icon={<Cpu />} iconTone="blue"
|
<MetricChartCard title={t('monitor.lbl.cpuUsage')} value={currentVal('cpu')} icon={<Cpu />} iconTone="blue"
|
||||||
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||||
<MetricChartCard title={i18n.t('CPU IO等待(%)')} value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock />} iconTone="orange"
|
<MetricChartCard title={t('monitor.lbl.cpuIowait')} value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock />} iconTone="orange"
|
||||||
data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||||
<MetricChartCard title={i18n.t('内存使用率(%)')} value={currentVal('mem')} icon={<Server />} iconTone="green"
|
<MetricChartCard title={t('monitor.lbl.memUsage')} value={currentVal('mem')} icon={<Server />} iconTone="green"
|
||||||
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||||
<MetricChartCard title={i18n.t('可用内存(GB)')} value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server />} iconTone="green"
|
<MetricChartCard title={t('monitor.lbl.memAvailable')} value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server />} iconTone="green"
|
||||||
data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title={i18n.t('Swap 使用率(%)')} value={currentVal('swap')} icon={<HardDrive />} iconTone="red"
|
<MetricChartCard title={t('monitor.lbl.swapUsage')} value={currentVal('swap')} icon={<HardDrive />} iconTone="red"
|
||||||
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||||
<MetricChartCard title={i18n.t('系统负载(1m)')} value={currentVal('load1')} icon={<Activity />} iconTone="purple"
|
<MetricChartCard title={t('monitor.lbl.load1')} value={currentVal('load1')} icon={<Activity />} iconTone="purple"
|
||||||
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
import type { JsonData } from '@/types/common';
|
import type { JsonData } from '@/types/common';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export function ContextSwitchCard({ csInfo }: { csInfo: JsonData }) {
|
export function ContextSwitchCard({ csInfo }: { csInfo: JsonData }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
@@ -47,7 +48,7 @@ export function ContextSwitchCard({ csInfo }: { csInfo: JsonData }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]"> {i18n.t('无限制')} </div>
|
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]"> {t('monitor.lbl.unlimited')} </div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { AggType, BucketPoint, RefLine } from '../types';
|
|||||||
import type { JsonData } from '@/types/common';
|
import type { JsonData } from '@/types/common';
|
||||||
import { MetricChartCard } from '../MetricChartCard';
|
import { MetricChartCard } from '../MetricChartCard';
|
||||||
import { parseJSON } from '../utils';
|
import { parseJSON } from '../utils';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface DiskTabProps {
|
export interface DiskTabProps {
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -19,6 +19,7 @@ export interface DiskTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DiskTab({ metrics, latest, currentVal, aggType, pctRefLines, diskIOInfo }: DiskTabProps) {
|
export function DiskTab({ metrics, latest, currentVal, aggType, pctRefLines, diskIOInfo }: DiskTabProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const inodeVal = (() => {
|
const inodeVal = (() => {
|
||||||
const dArr = Array.isArray(parseJSON(latest?.disk_info)) ? parseJSON(latest?.disk_info) : [];
|
const dArr = Array.isArray(parseJSON(latest?.disk_info)) ? parseJSON(latest?.disk_info) : [];
|
||||||
const root = dArr.find((d: JsonData) => d.mount === '/') || dArr[0];
|
const root = dArr.find((d: JsonData) => d.mount === '/') || dArr[0];
|
||||||
@@ -27,15 +28,15 @@ export function DiskTab({ metrics, latest, currentVal, aggType, pctRefLines, dis
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<MetricChartCard title={i18n.t('磁盘使用率(%)')} value={currentVal('disk')} icon={<HardDrive />} iconTone="orange"
|
<MetricChartCard title={t('monitor.lbl.diskUsage')} value={currentVal('disk')} icon={<HardDrive />} iconTone="orange"
|
||||||
data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||||
<MetricChartCard title={i18n.t('Swap 使用率(%)')} value={inodeVal} icon={<HardDrive />} iconTone="purple"
|
<MetricChartCard title={t('monitor.lbl.swapUsage')} value={inodeVal} icon={<HardDrive />} iconTone="purple"
|
||||||
data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||||
<MetricChartCard title={i18n.t('磁盘读取(KB/s)')} value={currentVal('ioR')} icon={<Disc />} iconTone="blue"
|
<MetricChartCard title={t('monitor.lbl.diskRead')} value={currentVal('ioR')} icon={<Disc />} iconTone="blue"
|
||||||
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title={i18n.t('磁盘写入(KB/s)')} value={currentVal('ioW')} icon={<Disc />} iconTone="green"
|
<MetricChartCard title={t('monitor.lbl.diskWrite')} value={currentVal('ioW')} icon={<Disc />} iconTone="green"
|
||||||
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title={i18n.t('磁盘IO繁忙度(%)')} value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc />} iconTone="red"
|
<MetricChartCard title={t('monitor.lbl.diskIoBusy')} value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc />} iconTone="red"
|
||||||
data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { MetricIconBox } from '../MetricIconBox';
|
import { MetricIconBox } from '../MetricIconBox';
|
||||||
import { ChartToolbar } from '../ChartToolbar';
|
import { ChartToolbar } from '../ChartToolbar';
|
||||||
import { ZoomableAreaChart, type ZoomableChartHandle } from '../ZoomableAreaChart';
|
import { ZoomableAreaChart, type ZoomableChartHandle } from '../ZoomableAreaChart';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface FDUsageCardProps {
|
export interface FDUsageCardProps {
|
||||||
fdInfo: JsonData;
|
fdInfo: JsonData;
|
||||||
@@ -19,6 +19,7 @@ export interface FDUsageCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FDUsageCard({ fdInfo, fdMetrics, aggType, fdChartRef, fdChartZoomed, setFdChartZoomed }: FDUsageCardProps) {
|
export function FDUsageCard({ fdInfo, fdMetrics, aggType, fdChartRef, fdChartZoomed, setFdChartZoomed }: FDUsageCardProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
@@ -39,15 +40,15 @@ export function FDUsageCard({ fdInfo, fdMetrics, aggType, fdChartRef, fdChartZoo
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-4 mb-3 text-xs">
|
<div className="flex items-center gap-4 mb-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[var(--dg-text-muted)]"> {i18n.t('deploy.toast.assigned')} </span>
|
<span className="text-[var(--dg-text-muted)]"> {t('deploy.toast.assigned')} </span>
|
||||||
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.allocated.toLocaleString()}</span>
|
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.allocated.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[var(--dg-text-muted)]"> {i18n.t('monitor.metricCatalog.colMax')} </span>
|
<span className="text-[var(--dg-text-muted)]"> {t('monitor.metricCatalog.colMax')} </span>
|
||||||
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.max === -1 ? i18n.t('无限制') : fdInfo.max?.toLocaleString() || '—'}</span>
|
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.max === -1 ? t('monitor.lbl.unlimited') : fdInfo.max?.toLocaleString() || '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[var(--dg-text-muted)]"> {i18n.t('server.usage')} </span>
|
<span className="text-[var(--dg-text-muted)]"> {t('server.usage')} </span>
|
||||||
<span className={`font-mono font-semibold ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'dg-text-purple'}`}>
|
<span className={`font-mono font-semibold ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'dg-text-purple'}`}>
|
||||||
{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}
|
{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}
|
||||||
</span>
|
</span>
|
||||||
@@ -57,11 +58,11 @@ export function FDUsageCard({ fdInfo, fdMetrics, aggType, fdChartRef, fdChartZoo
|
|||||||
<ZoomableAreaChart
|
<ZoomableAreaChart
|
||||||
ref={fdChartRef}
|
ref={fdChartRef}
|
||||||
data={fdMetrics.map((d) => ({ ...d, value: d[aggType] }))}
|
data={fdMetrics.map((d) => ({ ...d, value: d[aggType] }))}
|
||||||
series={[{ dataKey: 'value', color: '#a78bfa', label: i18n.t('FD 使用率') }]}
|
series={[{ dataKey: 'value', color: '#a78bfa', label: t('monitor.lbl.fdRate') }]}
|
||||||
yDomain={[0, 'auto']}
|
yDomain={[0, 'auto']}
|
||||||
unit="%"
|
unit="%"
|
||||||
chartId="fd-usage"
|
chartId="fd-usage"
|
||||||
title={i18n.t('网卡带宽吞吐')}
|
title={t('monitor.lbl.nicBandwidth')}
|
||||||
subtitle={fdInfo?.usage_pct != null ? `当前 ${fdInfo.usage_pct.toFixed(1)}%` : undefined}
|
subtitle={fdInfo?.usage_pct != null ? `当前 ${fdInfo.usage_pct.toFixed(1)}%` : undefined}
|
||||||
height={160}
|
height={160}
|
||||||
toolbarPlacement="header"
|
toolbarPlacement="header"
|
||||||
@@ -71,7 +72,7 @@ export function FDUsageCard({ fdInfo, fdMetrics, aggType, fdChartRef, fdChartZoo
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]"> {i18n.t('无限制')} </div>
|
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]"> {t('monitor.lbl.unlimited')} </div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { JsonData } from '@/types/common';
|
|||||||
import { MetricChartCard } from '../MetricChartCard';
|
import { MetricChartCard } from '../MetricChartCard';
|
||||||
import { DualLineChart } from '../DualLineChart';
|
import { DualLineChart } from '../DualLineChart';
|
||||||
import { formatBPS } from '../utils';
|
import { formatBPS } from '../utils';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface NetworkTabProps {
|
export interface NetworkTabProps {
|
||||||
metrics: {
|
metrics: {
|
||||||
@@ -19,31 +19,32 @@ export interface NetworkTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkTab({ metrics, netInfo, currentVal, aggType }: NetworkTabProps) {
|
export function NetworkTab({ metrics, netInfo, currentVal, aggType }: NetworkTabProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const connTotal = netInfo ? String((netInfo.tcp_established || 0) + (netInfo.tcp_time_wait || 0) + (netInfo.tcp_close_wait || 0)) : '--';
|
const connTotal = netInfo ? String((netInfo.tcp_established || 0) + (netInfo.tcp_time_wait || 0) + (netInfo.tcp_close_wait || 0)) : '--';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||||
<MetricChartCard title="TCP EST" value={currentVal('tcp')} icon={<Network />} iconTone="purple"
|
<MetricChartCard title="TCP EST" value={currentVal('tcp')} icon={<Network />} iconTone="purple"
|
||||||
data={metrics.tcpEst} color="#8b5cf6" unit={i18n.t('monitor.hostMonitorPanel.portUnit')} domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.tcpEst} color="#8b5cf6" unit={t('monitor.hostMonitorPanel.portUnit')} domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock />} iconTone="yellow"
|
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock />} iconTone="yellow"
|
||||||
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title="TCP CLOSE_WAIT" value={netInfo?.tcp_close_wait != null ? String(netInfo.tcp_close_wait) : '--'} icon={<Clock />} iconTone="red"
|
<MetricChartCard title="TCP CLOSE_WAIT" value={netInfo?.tcp_close_wait != null ? String(netInfo.tcp_close_wait) : '--'} icon={<Clock />} iconTone="red"
|
||||||
data={metrics.tcpCW} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.tcpCW} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title={i18n.t('内存使用率(%)')} value={connTotal} icon={<Network />} iconTone="cyan"
|
<MetricChartCard title={t('monitor.lbl.memUsage')} value={connTotal} icon={<Network />} iconTone="cyan"
|
||||||
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
</div>
|
</div>
|
||||||
<DualLineChart
|
<DualLineChart
|
||||||
title={i18n.t('主机监控')}
|
title={t('monitor.lbl.hostMonitor')}
|
||||||
currentRx={netInfo?.net_rx_bps != null ? formatBPS(netInfo.net_rx_bps) : '--'}
|
currentRx={netInfo?.net_rx_bps != null ? formatBPS(netInfo.net_rx_bps) : '--'}
|
||||||
currentTx={netInfo?.net_tx_bps != null ? formatBPS(netInfo.net_tx_bps) : '--'}
|
currentTx={netInfo?.net_tx_bps != null ? formatBPS(netInfo.net_tx_bps) : '--'}
|
||||||
rxData={metrics.rx} txData={metrics.tx}
|
rxData={metrics.rx} txData={metrics.tx}
|
||||||
aggType={aggType} unit="KB/s"
|
aggType={aggType} unit="KB/s"
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||||
<MetricChartCard title={i18n.t('网卡接收错误')} value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle />} iconTone="red"
|
<MetricChartCard title={t('monitor.lbl.nicRxErrors')} value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle />} iconTone="red"
|
||||||
data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
<MetricChartCard title={i18n.t('网卡发送错误')} value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle />} iconTone="orange"
|
<MetricChartCard title={t('monitor.lbl.nicTxErrors')} value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle />} iconTone="orange"
|
||||||
data={metrics.txErr} color="#f97316" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
data={metrics.txErr} color="#f97316" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Server } from 'lucide-react';
|
import { Server } from 'lucide-react';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface PortMonitorTabProps {
|
export interface PortMonitorTabProps {
|
||||||
portInfo: JsonData[];
|
portInfo: JsonData[];
|
||||||
@@ -14,6 +14,7 @@ export interface PortMonitorTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PortMonitorTab({ portInfo, portPolicyMode, portWhitelist, onOpenPortPolicy }: PortMonitorTabProps) {
|
export function PortMonitorTab({ portInfo, portPolicyMode, portWhitelist, onOpenPortPolicy }: PortMonitorTabProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
@@ -44,10 +45,10 @@ export function PortMonitorTab({ portInfo, portPolicyMode, portWhitelist, onOpen
|
|||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
|
<tr className="text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
|
||||||
<th className="py-1.5 text-left font-medium"> {i18n.t('system.ldapPort')} </th>
|
<th className="py-1.5 text-left font-medium"> {t('system.ldapPort')} </th>
|
||||||
<th className="py-1.5 text-left font-medium"> {i18n.t('kubernetes.service.protocol')} </th>
|
<th className="py-1.5 text-left font-medium"> {t('kubernetes.service.protocol')} </th>
|
||||||
<th className="py-1.5 text-left font-medium"> {i18n.t('进程监控')} </th>
|
<th className="py-1.5 text-left font-medium"> {t('monitor.lbl.processMonitor')} </th>
|
||||||
<th className="py-1.5 text-right font-medium"> {i18n.t('system.terminalStatus')} </th>
|
<th className="py-1.5 text-right font-medium"> {t('system.terminalStatus')} </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
import type { JsonData } from '@/types/common';
|
import type { JsonData } from '@/types/common';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { List } from 'lucide-react';
|
import { List } from 'lucide-react';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export function ProcessTable({ processTop }: { processTop: JsonData[] }) {
|
export function ProcessTable({ processTop }: { processTop: JsonData[] }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
@@ -20,11 +21,11 @@ export function ProcessTable({ processTop }: { processTop: JsonData[] }) {
|
|||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
|
<tr className="text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
|
||||||
<th className="py-1.5 text-left font-medium w-[40%]"> {i18n.t('进程监控')} </th>
|
<th className="py-1.5 text-left font-medium w-[40%]"> {t('monitor.lbl.processMonitor')} </th>
|
||||||
<th className="py-1.5 text-right font-medium w-[8%]">PID</th>
|
<th className="py-1.5 text-right font-medium w-[8%]">PID</th>
|
||||||
<th className="py-1.5 text-right font-medium w-[16%]"> {i18n.t('进程监控')} </th>
|
<th className="py-1.5 text-right font-medium w-[16%]"> {t('monitor.lbl.processMonitor')} </th>
|
||||||
<th className="py-1.5 text-right font-medium w-[16%]">CPU使用率(%)</th>
|
<th className="py-1.5 text-right font-medium w-[16%]">CPU使用率(%)</th>
|
||||||
<th className="py-1.5 text-right font-medium w-[20%]"> {i18n.t('进程监控')} </th>
|
<th className="py-1.5 text-right font-medium w-[20%]"> {t('monitor.lbl.processMonitor')} </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -41,7 +42,7 @@ export function ProcessTable({ processTop }: { processTop: JsonData[] }) {
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
className="opacity-0 group-hover/cmd:opacity-100 transition-opacity p-0.5 hover:dg-text-cyan shrink-0"
|
className="opacity-0 group-hover/cmd:opacity-100 transition-opacity p-0.5 hover:dg-text-cyan shrink-0"
|
||||||
title={i18n.t('FD 文件句柄使用率')}
|
title={t('monitor.lbl.fdUsage')}
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Loader2 } from 'lucide-react';
|
|||||||
import { middlewareMonitorApi } from '@/api';
|
import { middlewareMonitorApi } from '@/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { MwTarget } from './helpers';
|
import type { MwTarget } from './helpers';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -20,6 +20,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }: Props) {
|
export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
name: '', type: 'redis' as 'redis' | 'mysql', ip: '', port: 6379,
|
name: '', type: 'redis' as 'redis' | 'mysql', ip: '', port: 6379,
|
||||||
@@ -46,7 +47,7 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!form.name.trim() || !form.ip.trim()) {
|
if (!form.name.trim() || !form.ip.trim()) {
|
||||||
toast.error(i18n.t('请填写名称和 IP'));
|
toast.error(t('monitor.lbl.nameIpRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -63,15 +64,15 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
|||||||
}
|
}
|
||||||
if (editTarget) {
|
if (editTarget) {
|
||||||
await middlewareMonitorApi.update(editTarget.id, payload);
|
await middlewareMonitorApi.update(editTarget.id, payload);
|
||||||
toast.success(i18n.t('更新成功'));
|
toast.success(t('monitor.lbl.updateSuccess'));
|
||||||
} else {
|
} else {
|
||||||
await middlewareMonitorApi.create(payload);
|
await middlewareMonitorApi.create(payload);
|
||||||
toast.success(i18n.t('monitor.dashboard.custom.toastCreateSuccess'));
|
toast.success(t('monitor.dashboard.custom.toastCreateSuccess'));
|
||||||
}
|
}
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onSuccess();
|
onSuccess();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(editTarget ? i18n.t('kubernetes.deployment.imageUpdateFailed') : i18n.t('monitor.dashboard.custom.toastCreateFailed'), { description: (err as { message?: string }).message });
|
toast.error(editTarget ? t('kubernetes.deployment.imageUpdateFailed') : t('monitor.dashboard.custom.toastCreateFailed'), { description: (err as { message?: string }).message });
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -81,12 +82,12 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-md">
|
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editTarget ? i18n.t('编辑中间件') : i18n.t('添加中间件')}</DialogTitle>
|
<DialogTitle>{editTarget ? t('monitor.lbl.editMw') : t('monitor.lbl.addMw')}</DialogTitle>
|
||||||
<DialogDescription> {i18n.t('监控实例')} </DialogDescription>
|
<DialogDescription> {t('monitor.lbl.instances')} </DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
|
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label> {i18n.t('system.credentialType')} </Label>
|
<Label> {t('system.credentialType')} </Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{(['redis', 'mysql'] as const).map((t) => (
|
{(['redis', 'mysql'] as const).map((t) => (
|
||||||
<button key={t} type="button" onClick={() => setForm((f) => ({ ...f, type: t, port: t === 'redis' ? 6379 : 3306 }))}
|
<button key={t} type="button" onClick={() => setForm((f) => ({ ...f, type: t, port: t === 'redis' ? 6379 : 3306 }))}
|
||||||
@@ -98,9 +99,9 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
|||||||
</div>
|
</div>
|
||||||
{form.type === 'redis' && (
|
{form.type === 'redis' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label> {i18n.t('deploy.form.deployMode')} </Label>
|
<Label> {t('deploy.form.deployMode')} </Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{[{ value: 'standalone' as const, label: i18n.t('单机') }, { value: 'cluster' as const, label: i18n.t('k8s.topology.typeCluster') }].map((opt) => (
|
{[{ value: 'standalone' as const, label: t('monitor.lbl.standalone') }, { value: 'cluster' as const, label: t('k8s.topology.typeCluster') }].map((opt) => (
|
||||||
<button key={opt.value} type="button" onClick={() => setForm((f) => ({ ...f, redis_mode: opt.value }))}
|
<button key={opt.value} type="button" onClick={() => setForm((f) => ({ ...f, redis_mode: opt.value }))}
|
||||||
className={`flex-1 py-2 rounded-lg text-sm ${form.redis_mode === opt.value ? 'bg-blue-600 text-white' : 'bg-[var(--dg-bg-tertiary)]'}`}>
|
className={`flex-1 py-2 rounded-lg text-sm ${form.redis_mode === opt.value ? 'bg-blue-600 text-white' : 'bg-[var(--dg-bg-tertiary)]'}`}>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@@ -110,7 +111,7 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label> {i18n.t('deploy.form.nameStar')} </Label>
|
<Label> {t('deploy.form.nameStar')} </Label>
|
||||||
<Input value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} />
|
<Input value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
@@ -119,7 +120,7 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
|||||||
<Input value={form.ip} onChange={(e) => setForm((f) => ({ ...f, ip: e.target.value }))} />
|
<Input value={form.ip} onChange={(e) => setForm((f) => ({ ...f, ip: e.target.value }))} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label> {i18n.t('system.ldapPort')} </Label>
|
<Label> {t('system.ldapPort')} </Label>
|
||||||
<Input type="number" value={form.port} onChange={(e) => setForm((f) => ({ ...f, port: Number(e.target.value) }))} />
|
<Input type="number" value={form.port} onChange={(e) => setForm((f) => ({ ...f, port: Number(e.target.value) }))} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,21 +131,21 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
|||||||
{form.type === 'mysql' && (
|
{form.type === 'mysql' && (
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label> {i18n.t('system.userName')} </Label>
|
<Label> {t('system.userName')} </Label>
|
||||||
<Input value={form.username} onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))} />
|
<Input value={form.username} onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label> {i18n.t('k8s.topology.typeDatabase')} </Label>
|
<Label> {t('k8s.topology.typeDatabase')} </Label>
|
||||||
<Input value={form.database} onChange={(e) => setForm((f) => ({ ...f, database: e.target.value }))} />
|
<Input value={form.database} onChange={(e) => setForm((f) => ({ ...f, database: e.target.value }))} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
|
<Button variant="outline" onClick={() => onOpenChange(false)}> {t('rbac.jobRole.cancel')} </Button>
|
||||||
<Button onClick={handleSubmit} disabled={saving}>
|
<Button onClick={handleSubmit} disabled={saving}>
|
||||||
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||||
{editTarget ? i18n.t('task.script.save') : i18n.t('rbac.jobRole.create')}
|
{editTarget ? t('task.script.save') : t('rbac.jobRole.create')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { MwTarget } from './helpers';
|
|||||||
import { fmtHitRate, fmtVal } from './helpers';
|
import { fmtHitRate, fmtVal } from './helpers';
|
||||||
import { ChartExpandShell } from '../shared/ChartExpandShell';
|
import { ChartExpandShell } from '../shared/ChartExpandShell';
|
||||||
import { MonitorChartTooltip, MONITOR_PIE_STROKE, MonitorPieTooltip } from '../shared/monitorChartTheme';
|
import { MonitorChartTooltip, MONITOR_PIE_STROKE, MonitorPieTooltip } from '../shared/monitorChartTheme';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
targets: MwTarget[];
|
targets: MwTarget[];
|
||||||
@@ -21,12 +21,13 @@ function shortName(name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MwOverviewCharts({ targets }: Props) {
|
export function MwOverviewCharts({ targets }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const statusData = useMemo(() => {
|
const statusData = useMemo(() => {
|
||||||
const online = targets.filter((t) => t.status === 'online').length;
|
const online = targets.filter((t) => t.status === 'online').length;
|
||||||
const offline = targets.length - online;
|
const offline = targets.length - online;
|
||||||
return [
|
return [
|
||||||
{ name: i18n.t('agent.agentOnline'), value: online, fill: 'var(--dg-green)' },
|
{ name: t('agent.agentOnline'), value: online, fill: 'var(--dg-green)' },
|
||||||
{ name: i18n.t('agent.agentOffline'), value: offline, fill: 'var(--dg-red)' },
|
{ name: t('agent.agentOffline'), value: offline, fill: 'var(--dg-red)' },
|
||||||
].filter((d) => d.value > 0);
|
].filter((d) => d.value > 0);
|
||||||
}, [targets]);
|
}, [targets]);
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ export function MwOverviewCharts({ targets }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3">
|
||||||
<OverviewChartCard title={i18n.t('实例状态')} icon={<PieChartIcon className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={statusData.length === 0}>
|
<OverviewChartCard title={t('monitor.lbl.instanceStatus')} icon={<PieChartIcon className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={statusData.length === 0}>
|
||||||
{(h) => (
|
{(h) => (
|
||||||
<ResponsiveContainer width="100%" height={h}>
|
<ResponsiveContainer width="100%" height={h}>
|
||||||
<PieChart>
|
<PieChart>
|
||||||
@@ -67,13 +68,13 @@ export function MwOverviewCharts({ targets }: Props) {
|
|||||||
)}
|
)}
|
||||||
</OverviewChartCard>
|
</OverviewChartCard>
|
||||||
|
|
||||||
<OverviewChartCard title={i18n.t('在线')} icon={<Percent className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={redisHitData.length === 0}>
|
<OverviewChartCard title={t('common.online')} icon={<Percent className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={redisHitData.length === 0}>
|
||||||
{(h) => (
|
{(h) => (
|
||||||
<ResponsiveContainer width="100%" height={h}>
|
<ResponsiveContainer width="100%" height={h}>
|
||||||
<BarChart data={redisHitData} layout="vertical" margin={{ left: 0, right: 8 }}>
|
<BarChart data={redisHitData} layout="vertical" margin={{ left: 0, right: 8 }}>
|
||||||
<XAxis type="number" domain={[0, 100]} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickFormatter={(v) => `${v}%`} />
|
<XAxis type="number" domain={[0, 100]} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickFormatter={(v) => `${v}%`} />
|
||||||
<YAxis type="category" dataKey="name" width={64} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickLine={false} axisLine={false} />
|
<YAxis type="category" dataKey="name" width={64} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickLine={false} axisLine={false} />
|
||||||
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtHitRate(v / 100)} nameFormatter={() => i18n.t('命中率')} />} />
|
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtHitRate(v / 100)} nameFormatter={() => t('monitor.lbl.hitRate')} />} />
|
||||||
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
||||||
{redisHitData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
{redisHitData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
||||||
</Bar>
|
</Bar>
|
||||||
@@ -97,13 +98,13 @@ export function MwOverviewCharts({ targets }: Props) {
|
|||||||
)}
|
)}
|
||||||
</OverviewChartCard>
|
</OverviewChartCard>
|
||||||
|
|
||||||
<OverviewChartCard title={i18n.t('monitor.mwoverviewchart.s07d3e014')} icon={<BarChart3 className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={mysqlConnData.length === 0}>
|
<OverviewChartCard title={t('monitor.mwoverviewchart.s07d3e014')} icon={<BarChart3 className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={mysqlConnData.length === 0}>
|
||||||
{(h) => (
|
{(h) => (
|
||||||
<ResponsiveContainer width="100%" height={h}>
|
<ResponsiveContainer width="100%" height={h}>
|
||||||
<BarChart data={mysqlConnData} layout="vertical" margin={{ left: 0, right: 8 }}>
|
<BarChart data={mysqlConnData} layout="vertical" margin={{ left: 0, right: 8 }}>
|
||||||
<XAxis type="number" tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} />
|
<XAxis type="number" tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} />
|
||||||
<YAxis type="category" dataKey="name" width={64} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickLine={false} axisLine={false} />
|
<YAxis type="category" dataKey="name" width={64} tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} tickLine={false} axisLine={false} />
|
||||||
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtVal(v)} nameFormatter={() => i18n.t('连接')} />} />
|
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtVal(v)} nameFormatter={() => t('monitor.lbl.connections')} />} />
|
||||||
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
||||||
{mysqlConnData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
{mysqlConnData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
||||||
</Bar>
|
</Bar>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { MwTrendChart } from './MwTrendChart';
|
|||||||
import { MwGaugeBar } from './MwGaugeBar';
|
import { MwGaugeBar } from './MwGaugeBar';
|
||||||
import { MwHealthStrip } from './MwHealthStrip';
|
import { MwHealthStrip } from './MwHealthStrip';
|
||||||
import { ChartExpandShell } from '../shared/ChartExpandShell';
|
import { ChartExpandShell } from '../shared/ChartExpandShell';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
target: MwTarget | null;
|
target: MwTarget | null;
|
||||||
@@ -27,6 +27,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [timeRange, setTimeRange] = useState<MwTimeRange>('6h');
|
const [timeRange, setTimeRange] = useState<MwTimeRange>('6h');
|
||||||
const [historyMap, setHistoryMap] = useState<Record<string, Array<{ timestamp: number; value: number }>>>({});
|
const [historyMap, setHistoryMap] = useState<Record<string, Array<{ timestamp: number; value: number }>>>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -92,15 +93,15 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setShowAll((v) => !v)}>
|
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setShowAll((v) => !v)}>
|
||||||
{showAll ? i18n.t('核心指标') : i18n.t('全部指标')}
|
{showAll ? t('monitor.lbl.coreMetrics') : t('monitor.lbl.allMetrics')}
|
||||||
</Button>
|
</Button>
|
||||||
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as MwTimeRange)}>
|
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as MwTimeRange)}>
|
||||||
<SelectTrigger className="h-7 w-[92px] text-xs"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="h-7 w-[92px] text-xs"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1h"> {i18n.t('monitor.helpersts.sf972b7b1')} </SelectItem>
|
<SelectItem value="1h"> {t('monitor.helpersts.sf972b7b1')} </SelectItem>
|
||||||
<SelectItem value="6h"> {i18n.t('monitor.siteProtocol.rangeDefault')} </SelectItem>
|
<SelectItem value="6h"> {t('monitor.siteProtocol.rangeDefault')} </SelectItem>
|
||||||
<SelectItem value="24h"> {i18n.t('monitor.helpersts.s6e99b786')} </SelectItem>
|
<SelectItem value="24h"> {t('monitor.helpersts.s6e99b786')} </SelectItem>
|
||||||
<SelectItem value="7d"> {i18n.t('monitor.helpersts.sd765efc4')} </SelectItem>
|
<SelectItem value="7d"> {t('monitor.helpersts.sd765efc4')} </SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
|
||||||
@@ -116,20 +117,20 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
|||||||
{accent === 'redis' && (
|
{accent === 'redis' && (
|
||||||
<>
|
<>
|
||||||
<MwGaugeBar
|
<MwGaugeBar
|
||||||
label={i18n.t('命中率')}
|
label={t('monitor.lbl.hitRate')}
|
||||||
value={(m.redis_hit_rate || 0) * 100}
|
value={(m.redis_hit_rate || 0) * 100}
|
||||||
display={fmtHitRate(m.redis_hit_rate)}
|
display={fmtHitRate(m.redis_hit_rate)}
|
||||||
level={m.redis_hit_rate > 0 ? gaugeToneLow((m.redis_hit_rate || 0) * 100, 90, 80) : 'ok'}
|
level={m.redis_hit_rate > 0 ? gaugeToneLow((m.redis_hit_rate || 0) * 100, 90, 80) : 'ok'}
|
||||||
/>
|
/>
|
||||||
<MwGaugeBar
|
<MwGaugeBar
|
||||||
label={i18n.t('命中率')}
|
label={t('monitor.lbl.hitRate')}
|
||||||
value={memPct ?? (m.redis_used_memory > 0 ? 50 : 0)}
|
value={memPct ?? (m.redis_used_memory > 0 ? 50 : 0)}
|
||||||
display={fmtMemory(m.redis_used_memory)}
|
display={fmtMemory(m.redis_used_memory)}
|
||||||
level={memPct != null ? gaugeTone(memPct, 80, 90) : 'ok'}
|
level={memPct != null ? gaugeTone(memPct, 80, 90) : 'ok'}
|
||||||
hint={memPct != null ? `已用 ${memPct.toFixed(0)}%` : i18n.t('未配置 maxmemory')}
|
hint={memPct != null ? `已用 ${memPct.toFixed(0)}%` : t('monitor.lbl.noMaxMemory')}
|
||||||
/>
|
/>
|
||||||
<MwGaugeBar
|
<MwGaugeBar
|
||||||
label={i18n.t('kubernetes.topology.connections')}
|
label={t('kubernetes.topology.connections')}
|
||||||
value={Math.min(100, (m.redis_connected_clients || 0) / 2)}
|
value={Math.min(100, (m.redis_connected_clients || 0) / 2)}
|
||||||
display={fmtVal(m.redis_connected_clients)}
|
display={fmtVal(m.redis_connected_clients)}
|
||||||
level={(m.redis_blocked_clients || 0) > 0 ? 'warn' : 'ok'}
|
level={(m.redis_blocked_clients || 0) > 0 ? 'warn' : 'ok'}
|
||||||
@@ -140,19 +141,19 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
|||||||
{accent === 'mysql' && (
|
{accent === 'mysql' && (
|
||||||
<>
|
<>
|
||||||
<MwGaugeBar
|
<MwGaugeBar
|
||||||
label={i18n.t('命中率')}
|
label={t('monitor.lbl.hitRate')}
|
||||||
value={(m.mysql_buffer_hit_rate || 0) * 100}
|
value={(m.mysql_buffer_hit_rate || 0) * 100}
|
||||||
display={fmtHitRate(m.mysql_buffer_hit_rate)}
|
display={fmtHitRate(m.mysql_buffer_hit_rate)}
|
||||||
level={m.mysql_buffer_hit_rate > 0 ? gaugeToneLow((m.mysql_buffer_hit_rate || 0) * 100, 95, 90) : 'ok'}
|
level={m.mysql_buffer_hit_rate > 0 ? gaugeToneLow((m.mysql_buffer_hit_rate || 0) * 100, 95, 90) : 'ok'}
|
||||||
/>
|
/>
|
||||||
<MwGaugeBar
|
<MwGaugeBar
|
||||||
label={i18n.t('命中率')}
|
label={t('monitor.lbl.hitRate')}
|
||||||
value={connPct ?? 0}
|
value={connPct ?? 0}
|
||||||
display={`${fmtVal(m.mysql_connections)} / ${fmtVal(m.mysql_max_connections)}`}
|
display={`${fmtVal(m.mysql_connections)} / ${fmtVal(m.mysql_max_connections)}`}
|
||||||
level={connPct != null ? gaugeTone(connPct, 80, 90) : 'ok'}
|
level={connPct != null ? gaugeTone(connPct, 80, 90) : 'ok'}
|
||||||
/>
|
/>
|
||||||
<MwGaugeBar
|
<MwGaugeBar
|
||||||
label={i18n.t('命中率')}
|
label={t('monitor.lbl.hitRate')}
|
||||||
value={Math.min(100, (m.mysql_threads_running || 0) * 5)}
|
value={Math.min(100, (m.mysql_threads_running || 0) * 5)}
|
||||||
display={fmtVal(m.mysql_threads_running)}
|
display={fmtVal(m.mysql_threads_running)}
|
||||||
level={(m.mysql_threads_running || 0) > 20 ? 'warn' : 'ok'}
|
level={(m.mysql_threads_running || 0) > 20 ? 'warn' : 'ok'}
|
||||||
@@ -167,16 +168,16 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
|||||||
{accent === 'redis' ? (
|
{accent === 'redis' ? (
|
||||||
<>
|
<>
|
||||||
<SummaryTile label="QPS" value={fmtVal(m.redis_qps, 1)} />
|
<SummaryTile label="QPS" value={fmtVal(m.redis_qps, 1)} />
|
||||||
<SummaryTile label={i18n.t('碎片率')} value={fmtVal(m.redis_db_size)} />
|
<SummaryTile label={t('monitor.lbl.fragmentationRate')} value={fmtVal(m.redis_db_size)} />
|
||||||
<SummaryTile label={i18n.t('碎片率')} value={fmtVal(m.redis_evicted_keys)} warn={(m.redis_evicted_keys || 0) > 0} />
|
<SummaryTile label={t('monitor.lbl.fragmentationRate')} value={fmtVal(m.redis_evicted_keys)} warn={(m.redis_evicted_keys || 0) > 0} />
|
||||||
<SummaryTile label={i18n.t('逐出 Key')} value={m.redis_mem_fragmentation_ratio ? fmtVal(m.redis_mem_fragmentation_ratio, 2) : '--'} />
|
<SummaryTile label={t('monitor.lbl.evictedKeys')} value={m.redis_mem_fragmentation_ratio ? fmtVal(m.redis_mem_fragmentation_ratio, 2) : '--'} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SummaryTile label="QPS" value={fmtVal(m.mysql_qps, 1)} />
|
<SummaryTile label="QPS" value={fmtVal(m.mysql_qps, 1)} />
|
||||||
<SummaryTile label={i18n.t('发送流量')} value={fmtMemory(m.mysql_bytes_received)} />
|
<SummaryTile label={t('monitor.lbl.trafficSent')} value={fmtMemory(m.mysql_bytes_received)} />
|
||||||
<SummaryTile label={i18n.t('发送流量')} value={fmtMemory(m.mysql_bytes_sent)} />
|
<SummaryTile label={t('monitor.lbl.trafficSent')} value={fmtMemory(m.mysql_bytes_sent)} />
|
||||||
<SummaryTile label={i18n.t('agent.agentUptime')} value={m.mysql_uptime ? `${Math.floor(m.mysql_uptime / 3600)}h` : '--'} />
|
<SummaryTile label={t('agent.agentUptime')} value={m.mysql_uptime ? `${Math.floor(m.mysql_uptime / 3600)}h` : '--'} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const MYSQL_PRIMARY_KEYS = ['mysql_qps', 'mysql_connections', 'mysql_buff
|
|||||||
export const REDIS_CHARTS: MwChartDef[] = [
|
export const REDIS_CHARTS: MwChartDef[] = [
|
||||||
{ key: 'redis_qps', label: 'QPS', color: 'var(--dg-blue)', unit: 'ops/s' },
|
{ key: 'redis_qps', label: 'QPS', color: 'var(--dg-blue)', unit: 'ops/s' },
|
||||||
{ key: 'redis_used_memory', label: i18n.t('内存占用'), color: 'var(--dg-red)', formatter: fmtMemory },
|
{ key: 'redis_used_memory', label: i18n.t('内存占用'), color: 'var(--dg-red)', formatter: fmtMemory },
|
||||||
{ key: 'redis_hit_rate', label: i18n.t('命中率'), color: 'var(--dg-green)', percent: true },
|
{ key: 'redis_hit_rate', label: i18n.t('monitor.lbl.hitRate'), color: 'var(--dg-green)', percent: true },
|
||||||
{ key: 'redis_connected_clients', label: i18n.t('kubernetes.topology.connections'), color: 'var(--dg-cyan)' },
|
{ key: 'redis_connected_clients', label: i18n.t('kubernetes.topology.connections'), color: 'var(--dg-cyan)' },
|
||||||
{ key: 'redis_db_size', label: i18n.t('慢查询速率'), color: 'var(--dg-purple)' },
|
{ key: 'redis_db_size', label: i18n.t('慢查询速率'), color: 'var(--dg-purple)' },
|
||||||
{ key: 'redis_blocked_clients', label: i18n.t('阻塞客户端'), color: 'var(--dg-orange)' },
|
{ key: 'redis_blocked_clients', label: i18n.t('阻塞客户端'), color: 'var(--dg-orange)' },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// 图表选区提示 — 仅缩放后显示恢复入口 // Logan.Li
|
// 图表选区提示 — 仅缩放后显示恢复入口 // Logan.Li
|
||||||
import { ZoomOut } from 'lucide-react';
|
import { ZoomOut } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string | null;
|
label: string | null;
|
||||||
@@ -10,10 +10,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChartRangeHint({ label, zoomed, onReset }: Props) {
|
export function ChartRangeHint({ label, zoomed, onReset }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
if (!zoomed) return null;
|
if (!zoomed) return null;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between text-[10px] text-[var(--dg-text-muted)] mt-1 px-0.5">
|
<div className="flex items-center justify-between text-[10px] text-[var(--dg-text-muted)] mt-1 px-0.5">
|
||||||
<span> {i18n.t('monitor.chartrangehintt.s39ce9bad')} <span className="text-[var(--dg-text-primary)] font-mono">{label}</span></span>
|
<span> {t('monitor.chartrangehintt.s39ce9bad')} <span className="text-[var(--dg-text-primary)] font-mono">{label}</span></span>
|
||||||
<Button type="button" variant="ghost" size="sm" className="h-6 text-[10px] px-2" onClick={onReset}>
|
<Button type="button" variant="ghost" size="sm" className="h-6 text-[10px] px-2" onClick={onReset}>
|
||||||
<ZoomOut className="h-3 w-3 mr-1" />恢复全量
|
<ZoomOut className="h-3 w-3 mr-1" />恢复全量
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { PieChart as PieChartIcon } from 'lucide-react';
|
import { PieChart as PieChartIcon } from 'lucide-react';
|
||||||
import { PROBE_TYPE_OPTIONS, normalizeProbeType } from './helpers';
|
import { PROBE_TYPE_OPTIONS, normalizeProbeType } from './helpers';
|
||||||
import type { SiteTask } from './helpers';
|
import type { SiteTask } from './helpers';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tasks: SiteTask[];
|
tasks: SiteTask[];
|
||||||
@@ -21,6 +21,7 @@ const PROTOCOL_COLORS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function SiteProtocolPieChart({ tasks }: Props) {
|
export function SiteProtocolPieChart({ tasks }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const protocolData = useMemo(() => {
|
const protocolData = useMemo(() => {
|
||||||
const counts: Record<string, number> = {};
|
const counts: Record<string, number> = {};
|
||||||
for (const t of PROBE_TYPE_OPTIONS) counts[t] = 0;
|
for (const t of PROBE_TYPE_OPTIONS) counts[t] = 0;
|
||||||
@@ -36,7 +37,7 @@ export function SiteProtocolPieChart({ tasks }: Props) {
|
|||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||||
<CardContent className="p-6 text-center text-sm text-[var(--dg-text-muted)]"> {i18n.t('可用率(24h)')} </CardContent>
|
<CardContent className="p-6 text-center text-sm text-[var(--dg-text-muted)]"> {t('monitor.lbl.availability24h')} </CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,7 @@ export function SiteProtocolPieChart({ tasks }: Props) {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<PieChartIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
<PieChartIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {i18n.t('可用率(24h)')} </h3>
|
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {t('monitor.lbl.availability24h')} </h3>
|
||||||
<span className="text-[10px] text-[var(--dg-text-muted)]">共 {tasks.length} 个任务</span>
|
<span className="text-[10px] text-[var(--dg-text-muted)]">共 {tasks.length} 个任务</span>
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
SITE_TIME_RANGE_HOURS, SITE_TIME_RANGE_LABELS,
|
SITE_TIME_RANGE_HOURS, SITE_TIME_RANGE_LABELS,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { SiteProtocolObs } from './observability/SiteProtocolObs';
|
import { SiteProtocolObs } from './observability/SiteProtocolObs';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -48,6 +49,7 @@ function httpCodeBadge(code?: number, expected = 200) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [results, setResults] = useState<SiteResult[]>([]);
|
const [results, setResults] = useState<SiteResult[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [probing, setProbing] = useState(false);
|
const [probing, setProbing] = useState(false);
|
||||||
@@ -77,11 +79,11 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
|||||||
setProbing(true);
|
setProbing(true);
|
||||||
try {
|
try {
|
||||||
await siteApi.probeNow(task.id);
|
await siteApi.probeNow(task.id);
|
||||||
toast.success(i18n.t('拨测完成'));
|
toast.success(t('monitor.lbl.probeDone'));
|
||||||
loadResults();
|
loadResults();
|
||||||
onProbed();
|
onProbed();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error((e as { message?: string }).message || i18n.t('拨测失败'));
|
toast.error((e as { message?: string }).message || t('monitor.lbl.probeFail'));
|
||||||
} finally {
|
} finally {
|
||||||
setProbing(false);
|
setProbing(false);
|
||||||
}
|
}
|
||||||
@@ -117,41 +119,41 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
|||||||
case 'HTTP':
|
case 'HTTP':
|
||||||
return (
|
return (
|
||||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||||
<TableHead className="text-xs"> {i18n.t('system.auditTime')} </TableHead>
|
<TableHead className="text-xs"> {t('system.auditTime')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('system.terminalStatus')} </TableHead>
|
<TableHead className="text-xs"> {t('system.terminalStatus')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('monitor.siteProtocol.http.statusCodeName')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.siteProtocol.http.statusCodeName')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('monitor.siteProtocol.latency.latencyName')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.siteProtocol.latency.latencyName')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('cloud.colDescription')} </TableHead>
|
<TableHead className="text-xs"> {t('cloud.colDescription')} </TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
case 'SSL':
|
case 'SSL':
|
||||||
return (
|
return (
|
||||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||||
<TableHead className="text-xs"> {i18n.t('system.auditTime')} </TableHead>
|
<TableHead className="text-xs"> {t('system.auditTime')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('编辑拨测任务')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.lbl.editTask')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('编辑拨测任务')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.lbl.editTask')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('monitor.siteProtocol.ssl.daysName')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.siteProtocol.ssl.daysName')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('monitor.siteProtocol.ssl.issuer')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.siteProtocol.ssl.issuer')} </TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
case 'TCP':
|
case 'TCP':
|
||||||
return (
|
return (
|
||||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||||
<TableHead className="text-xs"> {i18n.t('system.auditTime')} </TableHead>
|
<TableHead className="text-xs"> {t('system.auditTime')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('monitor.siteProtocol.tcp.connected')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.siteProtocol.tcp.connected')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('monitor.siteProtocol.latency.latencyName')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.siteProtocol.latency.latencyName')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('编辑拨测任务')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.lbl.editTask')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('cloud.colDescription')} </TableHead>
|
<TableHead className="text-xs"> {t('cloud.colDescription')} </TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||||
<TableHead className="text-xs"> {i18n.t('system.auditTime')} </TableHead>
|
<TableHead className="text-xs"> {t('system.auditTime')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('system.terminalStatus')} </TableHead>
|
<TableHead className="text-xs"> {t('system.terminalStatus')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('monitor.siteProtocol.latency.latencyName')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.siteProtocol.latency.latencyName')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('编辑拨测任务')} </TableHead>
|
<TableHead className="text-xs"> {t('monitor.lbl.editTask')} </TableHead>
|
||||||
<TableHead className="text-xs"> {i18n.t('cloud.colDescription')} </TableHead>
|
<TableHead className="text-xs"> {t('cloud.colDescription')} </TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -188,7 +190,7 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
|||||||
<TableCell>{statusBadge(r.status)}</TableCell>
|
<TableCell>{statusBadge(r.status)}</TableCell>
|
||||||
<TableCell className="text-xs">{fmtMs(r.response_time || r.total_time)}</TableCell>
|
<TableCell className="text-xs">{fmtMs(r.response_time || r.total_time)}</TableCell>
|
||||||
<TableCell className="text-xs text-[var(--dg-text-muted)] truncate max-w-[120px]">{r.remote_ip || '--'}</TableCell>
|
<TableCell className="text-xs text-[var(--dg-text-muted)] truncate max-w-[120px]">{r.remote_ip || '--'}</TableCell>
|
||||||
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[160px] truncate">{r.error_message || (r.status === 'success' ? i18n.t('monitor.siteProtocol.tcp.connected') : '--')}</TableCell>
|
<TableCell className="text-xs text-[var(--dg-text-muted)] max-w-[160px] truncate">{r.error_message || (r.status === 'success' ? t('monitor.siteProtocol.tcp.connected') : '--')}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@@ -223,10 +225,10 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
|||||||
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as SiteTimeRange)}>
|
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as SiteTimeRange)}>
|
||||||
<SelectTrigger className="h-7 w-[92px] text-xs"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="h-7 w-[92px] text-xs"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1h"> {i18n.t('探测次数')} </SelectItem>
|
<SelectItem value="1h"> {t('monitor.lbl.totalProbes')} </SelectItem>
|
||||||
<SelectItem value="6h"> {i18n.t('monitor.siteProtocol.rangeDefault')} </SelectItem>
|
<SelectItem value="6h"> {t('monitor.siteProtocol.rangeDefault')} </SelectItem>
|
||||||
<SelectItem value="24h"> {i18n.t('探测次数')} </SelectItem>
|
<SelectItem value="24h"> {t('monitor.lbl.totalProbes')} </SelectItem>
|
||||||
<SelectItem value="7d"> {i18n.t('探测次数')} </SelectItem>
|
<SelectItem value="7d"> {t('monitor.lbl.totalProbes')} </SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadResults} disabled={loading}>
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadResults} disabled={loading}>
|
||||||
@@ -259,7 +261,7 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
|||||||
<SiteProtocolObs task={task} results={results} rangeLabel={rangeLabel} />
|
<SiteProtocolObs task={task} results={results} rangeLabel={rangeLabel} />
|
||||||
<div className={`mt-4 overflow-x-auto ${fullPage ? 'max-h-80' : 'max-h-56'}`}>
|
<div className={`mt-4 overflow-x-auto ${fullPage ? 'max-h-80' : 'max-h-56'}`}>
|
||||||
{results.length === 0 ? (
|
{results.length === 0 ? (
|
||||||
<p className="text-center py-6 text-sm text-[var(--dg-text-muted)]"> {i18n.t('可用率(24h)')} </p>
|
<p className="text-center py-6 text-sm text-[var(--dg-text-muted)]"> {t('monitor.lbl.availability24h')} </p>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>{renderTableHead()}</TableHeader>
|
<TableHeader>{renderTableHead()}</TableHeader>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
SNMP_TIME_RANGE_HOURS, fmtMetricValue, isSnmpCounterMetric, metricColor, pickSeriesByCategory,
|
SNMP_TIME_RANGE_HOURS, fmtMetricValue, isSnmpCounterMetric, metricColor, pickSeriesByCategory,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { SnmpMetricTrendChart } from './SnmpMetricTrendChart';
|
import { SnmpMetricTrendChart } from './SnmpMetricTrendChart';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
device: SnmpDevice | null;
|
device: SnmpDevice | null;
|
||||||
@@ -30,6 +30,7 @@ interface ChartBlock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [timeRange, setTimeRange] = useState<SnmpTimeRange>('24h');
|
const [timeRange, setTimeRange] = useState<SnmpTimeRange>('24h');
|
||||||
const [series, setSeries] = useState<SnmpMetricSeries[]>([]);
|
const [series, setSeries] = useState<SnmpMetricSeries[]>([]);
|
||||||
const [charts, setCharts] = useState<ChartBlock[]>([]);
|
const [charts, setCharts] = useState<ChartBlock[]>([]);
|
||||||
@@ -115,9 +116,9 @@ export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="6h"> {i18n.t('monitor.siteProtocol.rangeDefault')} </SelectItem>
|
<SelectItem value="6h"> {t('monitor.siteProtocol.rangeDefault')} </SelectItem>
|
||||||
<SelectItem value="24h"> {i18n.t('选择模板')} </SelectItem>
|
<SelectItem value="24h"> {t('monitor.lbl.selectTemplate')} </SelectItem>
|
||||||
<SelectItem value="7d"> {i18n.t('选择模板')} </SelectItem>
|
<SelectItem value="7d"> {t('monitor.lbl.selectTemplate')} </SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
|
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
|
||||||
@@ -129,9 +130,9 @@ export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
|||||||
|
|
||||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||||
{[
|
{[
|
||||||
{ label: i18n.t('设备总数'), value: latestCpu ? fmtMetricValue(latestCpu.latest_value, latestCpu.unit || '%') : '--' },
|
{ label: t('monitor.lbl.totalDevices'), value: latestCpu ? fmtMetricValue(latestCpu.latest_value, latestCpu.unit || '%') : '--' },
|
||||||
{ label: i18n.t('设备总数'), value: latestMem ? fmtMetricValue(latestMem.latest_value, latestMem.unit || '%') : '--' },
|
{ label: t('monitor.lbl.totalDevices'), value: latestMem ? fmtMetricValue(latestMem.latest_value, latestMem.unit || '%') : '--' },
|
||||||
{ label: i18n.t('设备总数'), value: i18n.t('设备总数') },
|
{ label: t('monitor.lbl.totalDevices'), value: t('monitor.lbl.totalDevices') },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<div key={item.label} className="rounded-lg bg-[var(--dg-bg-tertiary)] p-3 text-center">
|
<div key={item.label} className="rounded-lg bg-[var(--dg-bg-tertiary)] p-3 text-center">
|
||||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { BarChart3, PieChart as PieChartIcon } from 'lucide-react';
|
|||||||
import type { SnmpDevice, SnmpOverviewItem } from './helpers';
|
import type { SnmpDevice, SnmpOverviewItem } from './helpers';
|
||||||
import { fmtMetricValue, statusColor } from './helpers';
|
import { fmtMetricValue, statusColor } from './helpers';
|
||||||
import { MonitorChartTooltip, MONITOR_PIE_STROKE, MonitorPieTooltip } from '../shared/monitorChartTheme';
|
import { MonitorChartTooltip, MONITOR_PIE_STROKE, MonitorPieTooltip } from '../shared/monitorChartTheme';
|
||||||
import i18n from '@/i18n';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
devices: SnmpDevice[];
|
devices: SnmpDevice[];
|
||||||
@@ -17,14 +17,15 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SnmpOverviewCharts({ devices, overview }: Props) {
|
export function SnmpOverviewCharts({ devices, overview }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const statusData = useMemo(() => {
|
const statusData = useMemo(() => {
|
||||||
const online = devices.filter((d) => d.status === 'online').length;
|
const online = devices.filter((d) => d.status === 'online').length;
|
||||||
const error = devices.filter((d) => d.status === 'error').length;
|
const error = devices.filter((d) => d.status === 'error').length;
|
||||||
const offline = devices.length - online - error;
|
const offline = devices.length - online - error;
|
||||||
return [
|
return [
|
||||||
{ name: i18n.t('agent.agentOnline'), value: online, fill: 'var(--dg-green)' },
|
{ name: t('agent.agentOnline'), value: online, fill: 'var(--dg-green)' },
|
||||||
{ name: i18n.t('agent.heartbeatAbnormal'), value: error, fill: 'var(--dg-red)' },
|
{ name: t('agent.heartbeatAbnormal'), value: error, fill: 'var(--dg-red)' },
|
||||||
{ name: i18n.t('agent.agentOffline'), value: offline, fill: 'var(--dg-text-muted)' },
|
{ name: t('agent.agentOffline'), value: offline, fill: 'var(--dg-text-muted)' },
|
||||||
].filter((d) => d.value > 0);
|
].filter((d) => d.value > 0);
|
||||||
}, [devices]);
|
}, [devices]);
|
||||||
|
|
||||||
@@ -48,10 +49,10 @@ export function SnmpOverviewCharts({ devices, overview }: Props) {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<BarChart3 className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
<BarChart3 className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {i18n.t('monitor.snmpoverviewcha.s64f5a4da')} </h3>
|
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {t('monitor.snmpoverviewcha.s64f5a4da')} </h3>
|
||||||
</div>
|
</div>
|
||||||
{cpuData.length === 0 ? (
|
{cpuData.length === 0 ? (
|
||||||
<p className="text-xs text-[var(--dg-text-muted)] py-8 text-center"> {i18n.t('monitor.snmpoverviewcha.s1f441890')} </p>
|
<p className="text-xs text-[var(--dg-text-muted)] py-8 text-center"> {t('monitor.snmpoverviewcha.s1f441890')} </p>
|
||||||
) : (
|
) : (
|
||||||
<ResponsiveContainer width="100%" height={Math.max(160, cpuData.length * 36)}>
|
<ResponsiveContainer width="100%" height={Math.max(160, cpuData.length * 36)}>
|
||||||
<BarChart data={cpuData} layout="vertical" margin={{ top: 0, right: 16, left: 0, bottom: 0 }}>
|
<BarChart data={cpuData} layout="vertical" margin={{ top: 0, right: 16, left: 0, bottom: 0 }}>
|
||||||
@@ -99,7 +100,7 @@ export function SnmpOverviewCharts({ devices, overview }: Props) {
|
|||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<PieChartIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
<PieChartIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {i18n.t('monitor.snmpoverviewcha.sbb0ae97f')} </h3>
|
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]"> {t('monitor.snmpoverviewcha.sbb0ae97f')} </h3>
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={180}>
|
<ResponsiveContainer width="100%" height={180}>
|
||||||
<PieChart>
|
<PieChart>
|
||||||
|
|||||||
Reference in new issue
Block a user