feat(i18n): 增量国际化迁移 — deploy/kubernetes/monitor/task 全模块 + 补全 28 缺失翻译键

迁移范围:
- deploy 模块 (PipelineEditor/PipelineDetailDialog/PipelineFormDialog/PipelineRunDialog/ReleaseCreateDialog/ReleaseDetailDialog/ReleaseManager/ToolConfigTab/JobRoleCreateDialog 等 14 文件)
- kubernetes 模块 (YamlEditorToolbar/DeploymentHpaTab/DeploymentLogsTab/DeploymentPodsTab/DnsHostSection/SchedulingSection/EventsTab/HPACronHPATab/HistoryTab/PodsTab/ServicesTab/ActionModals 等 12 文件)
- monitor 模块 (AlertEventsTab/AlertCenterDetail/DatasourcePage/LogAlertBell/LogCenter/LogCenterArchBanner/StatChartPanel/LogRoleCreateDialog 等 8 文件)
- task 模块 (CronJobStatsPanel/CronJobTable/CronJobViewDialog/ExecutionHistoryDialog 4 文件)
- layout (PageEmpty/PageLoading 2 文件)
- site-monitor/snmp-monitor 模块 (6 文件)

翻译键: 3975 → 4003 (+28 缺失键补全), 22 命名空间, zh-CN/en-US 完全对齐
新增命名空间: agent/aiops/cloud/kubernetes/monitor/pipeline/rbac/system/task/ui

验证: go build✅ tsc -b=0 npm build✅ (20.78s) 17/17 集成测试通过
This commit is contained in:
attacker committed 2026-07-02 12:39:36 +08:00
1 parent eddf01dd79
commit 63e4c220d4
59 files changed
+6713 -641

No files matched your search

@@ -1,4 +1,5 @@
// 监控图表工具栏 — 固定卡片/弹框右上角 // Logan.Li
import { useTranslation } from 'react-i18next';
import { Maximize2, ZoomOut } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
@@ -14,6 +15,7 @@ interface Props {
export function ChartToolbar({
zoomed, onExpand, onResetZoom, size = 'sm', className,
}: Props) {
const { t } = useTranslation();
const btn = size === 'md' ? 'h-8 w-8' : 'h-7 w-7';
const icon = size === 'md' ? 'h-4 w-4' : 'h-3.5 w-3.5';
@@ -25,7 +27,7 @@ export function ChartToolbar({
variant="ghost"
size="icon"
className={cn(btn, 'dg-text-secondary hover:dg-text-primary hover:bg-[var(--dg-bg-hover)]')}
title="重置缩放"
title={t('monitor.chartToolbar.resetZoom')}
onClick={onResetZoom}
>
<ZoomOut className={icon} />
@@ -36,7 +38,7 @@ export function ChartToolbar({
variant="ghost"
size="icon"
className={cn(btn, 'dg-text-secondary hover:dg-text-blue hover:bg-[var(--dg-bg-hover)]')}
title="放大查看"
title={t('monitor.chartToolbar.expand')}
onClick={onExpand}
>
<Maximize2 className={icon} />
@@ -1,4 +1,5 @@
// 主机核心指标条 — CPU/内存/磁盘/网络/磁盘IO 一览 // Logan.Li
import { useTranslation } from 'react-i18next';
import { Cpu, HardDrive, Network, Disc, Server } from 'lucide-react';
import type { JsonData } from '@/types/common';
import { formatBPS } from './utils';
@@ -17,6 +18,7 @@ function tone(val: number, warn: number, crit: number): string {
}
export function HostCoreMetricsStrip({ latest, netInfo, diskIOInfo, onJump }: Props) {
const { t } = useTranslation();
const cpu = Number(latest?.cpu_usage ?? latest?.cpu ?? 0);
const mem = Number(latest?.memory_usage ?? latest?.mem_usage ?? latest?.mem ?? 0);
const disk = Number(latest?.disk_usage ?? latest?.disk ?? 0);
@@ -27,10 +29,10 @@ export function HostCoreMetricsStrip({ latest, netInfo, diskIOInfo, onJump }: Pr
const items = [
{ tab: 'basic', icon: Cpu, label: 'CPU', value: `${cpu.toFixed(1)}%`, cls: tone(cpu, 80, 90) },
{ tab: 'basic', icon: Server, label: '内存', value: `${mem.toFixed(1)}%`, cls: tone(mem, 85, 90) },
{ tab: 'disk', icon: HardDrive, label: '磁盘', value: `${disk.toFixed(1)}%`, cls: tone(disk, 80, 90) },
{ tab: 'network', icon: Network, label: '网络', value: `↓${rx} ↑${tx}`, cls: 'text-[var(--dg-text-primary)]' },
{ tab: 'disk', icon: Disc, label: '磁盘IO', value: `↓${ioR} ↑${ioW}`, cls: 'text-[var(--dg-text-primary)]' },
{ tab: 'basic', icon: Server, label: t('monitor.hostMetric.memory'), value: `${mem.toFixed(1)}%`, cls: tone(mem, 85, 90) },
{ tab: 'disk', icon: HardDrive, label: t('monitor.hostMetric.disk'), value: `${disk.toFixed(1)}%`, cls: tone(disk, 80, 90) },
{ tab: 'network', icon: Network, label: t('monitor.hostMetric.network'), value: `↓${rx} ↑${tx}`, cls: 'text-[var(--dg-text-primary)]' },
{ tab: 'disk', icon: Disc, label: t('monitor.hostMetric.diskIO'), value: `↓${ioR} ↑${ioW}`, cls: 'text-[var(--dg-text-primary)]' },
];
return (
@@ -1,5 +1,6 @@
// 指标图表卡片 — 工具栏固定标题栏右上角 // Logan.Li
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { ReactNode } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { ZoomableAreaChart, type ZoomableChartHandle, type ChartReferenceLine } from './ZoomableAreaChart';
@@ -27,6 +28,7 @@ function safeChartId(title: string) {
export function MetricChartCard({
title, value, icon, iconTone = 'blue', data, color, unit, domain, aggType, referenceLines,
}: MetricChartCardProps) {
const { t } = useTranslation();
const chartRef = useRef<ZoomableChartHandle>(null);
const [zoomed, setZoomed] = useState(false);
const chartData = data.map((d) => ({ ...d, value: d[aggType] }));
@@ -60,7 +62,7 @@ export function MetricChartCard({
unit={unit}
chartId={safeChartId(title)}
title={title}
subtitle={`当前 ${value}${unit ? ` ${unit}` : ''}`}
subtitle={`${t('monitor.metricChart.currentPrefix')} ${value}${unit ? ` ${unit}` : ''}`}
height={188}
toolbarPlacement="header"
onZoomedChange={setZoomed}
@@ -1,4 +1,5 @@
// 系统信息 Tab 内容 // Logan.Li
import { useTranslation } from 'react-i18next';
import { Activity, HardDrive } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { TabsContent } from '@/components/ui/tabs';
@@ -16,6 +17,7 @@ interface SystemInfoSectionProps {
export function SystemInfoSection({
selectedHost, serverInfo, latest, heartbeatInfo, portInfoCount,
}: SystemInfoSectionProps) {
const { t } = useTranslation();
const disk = latest?.disk_info ? parseJSON(latest.disk_info) : null;
const parts = disk ? (Array.isArray(disk) ? disk : (disk.partitions || [])) : [];
@@ -25,18 +27,18 @@ export function SystemInfoSection({
<CardContent className="p-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3 text-xs">
{[
['主机名', serverInfo?.hostname || selectedHost],
['IP 地址', serverInfo?.ip || '—'],
['操作系统', serverInfo?.os || '—'],
['内核版本', serverInfo?.kernel || '—'],
['CPU 核心数', serverInfo?.cpu_cores || '—'],
['总内存', serverInfo?.mem_total ? `${serverInfo.mem_total.toFixed(0)} GB` : '—'],
['已用内存', serverInfo?.mem_used ? `${serverInfo.mem_used.toFixed(1)} GB` : '—'],
['运行时长', serverInfo?.uptime ? `${Math.floor(serverInfo.uptime / 86400)}天 ${Math.floor((serverInfo.uptime % 86400) / 3600)}时` : '—'],
['Agent 版本', heartbeatInfo?.version || serverInfo?.agent_version || '—'],
['Agent 序列号', heartbeatInfo?.report_seq != null ? String(heartbeatInfo.report_seq) : '—'],
['监听端口数', portInfoCount > 0 ? `${portInfoCount}` : '—'],
['Agent RSS', heartbeatInfo?.self_rss_mb ? `${heartbeatInfo.self_rss_mb.toFixed(1)} MB` : '—'],
[t('monitor.systemInfo.hostname'), serverInfo?.hostname || selectedHost],
[t('monitor.systemInfo.ipAddress'), serverInfo?.ip || '—'],
[t('monitor.systemInfo.os'), serverInfo?.os || '—'],
[t('monitor.systemInfo.kernel'), serverInfo?.kernel || '—'],
[t('monitor.systemInfo.cpuCores'), serverInfo?.cpu_cores || '—'],
[t('monitor.systemInfo.memTotal'), serverInfo?.mem_total ? `${serverInfo.mem_total.toFixed(0)} GB` : '—'],
[t('monitor.systemInfo.memUsed'), serverInfo?.mem_used ? `${serverInfo.mem_used.toFixed(1)} GB` : '—'],
[t('monitor.systemInfo.uptime'), serverInfo?.uptime ? `${Math.floor(serverInfo.uptime / 86400)}${t('monitor.systemInfo.dayUnit')} ${Math.floor((serverInfo.uptime % 86400) / 3600)}${t('monitor.systemInfo.hourUnit')}` : '—'],
[t('monitor.systemInfo.agentVersion'), heartbeatInfo?.version || serverInfo?.agent_version || '—'],
[t('monitor.systemInfo.agentSeq'), heartbeatInfo?.report_seq != null ? String(heartbeatInfo.report_seq) : '—'],
[t('monitor.systemInfo.portCount'), portInfoCount > 0 ? `${portInfoCount}` : '—'],
[t('monitor.systemInfo.agentRss'), heartbeatInfo?.self_rss_mb ? `${heartbeatInfo.self_rss_mb.toFixed(1)} MB` : '—'],
].map(([label, val]) => (
<div key={label as string}>
<span className="text-[var(--dg-text-muted)]">{label}:</span>
@@ -52,7 +54,7 @@ export function SystemInfoSection({
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<Activity className="h-4 w-4 text-[var(--dg-text-muted)]" />
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">系统负载 (Load Average)</h3>
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">{t('monitor.systemInfo.loadAvgTitle')}</h3>
</div>
<div className="flex items-center gap-6 text-xs">
<div><span className="text-[var(--dg-text-muted)]">1min </span><span className="text-sm font-bold text-[var(--dg-text-primary)]">{latest.load1 ?? '--'}</span></div>
@@ -66,7 +68,7 @@ export function SystemInfoSection({
{parts.length > 0 && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-2 flex items-center gap-2"><HardDrive className="h-4 w-4" />磁盘分区</h3>
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-2 flex items-center gap-2"><HardDrive className="h-4 w-4" />{t('monitor.systemInfo.diskPartition')}</h3>
<div className="space-y-1.5">
{parts.map((p: JsonData, i: number) => (
<div key={i} className="flex items-center gap-2 text-xs">
@@ -84,4 +86,4 @@ export function SystemInfoSection({
)}
</TabsContent>
);
}
}
@@ -1,5 +1,6 @@
// 中间件实例卡片 — 紧凑列表项,详情见右侧可观测面板 // Logan.Li
import { Button } from '@/components/ui/button';
import { useTranslation } from 'react-i18next';
import { AlertTriangle, Loader2, Pencil, Trash2, Zap } from 'lucide-react';
import type { MwTarget } from './helpers';
import {
@@ -18,6 +19,7 @@ interface Props {
}
export function InstanceCard({ target, selected, onSelect, onEdit, onDelete, onTest, testing }: Props) {
const { t } = useTranslation();
const m = target.latest_metrics || {};
const isRedis = target.description === 'redis';
const issues = evalMwHealth(target);
@@ -55,20 +57,20 @@ export function InstanceCard({ target, selected, onSelect, onEdit, onDelete, onT
) : (
<>
<MetricPill label="QPS" value={fmtVal(m.mysql_qps, 1)} />
<MetricPill label="线程" value={fmtVal(m.mysql_threads_running)} />
<MetricPill label={t('monitor.mwInstance.threads')} value={fmtVal(m.mysql_threads_running)} />
</>
)}
</div>
<div className="flex items-center gap-0.5" onClick={(e) => e.stopPropagation()}>
<Button variant="ghost" size="sm" className="h-6 px-1.5 text-[10px]" onClick={onTest} disabled={testing}>
{testing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Zap className="h-3 w-3" />}测试
{testing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Zap className="h-3 w-3" />}{t('monitor.mwInstance.test')}
</Button>
<Button variant="ghost" size="sm" className="h-6 px-1.5 text-[10px]" onClick={onEdit}>
<Pencil className="h-3 w-3" />编辑
<Pencil className="h-3 w-3" />{t('common.edit')}
</Button>
<Button variant="ghost" size="sm" className="h-6 px-1.5 text-[10px] dg-text-red" onClick={onDelete}>
<Trash2 className="h-3 w-3" />删除
<Trash2 className="h-3 w-3" />{t('common.delete')}
</Button>
</div>
</button>
@@ -1,4 +1,5 @@
// 中间件健康告警条 // Logan.Li
import { useTranslation } from 'react-i18next';
import { AlertTriangle, CheckCircle2 } from 'lucide-react';
import type { MwHealthIssue } from './helpers';
import { worstHealthLevel, healthLevelColor } from './helpers';
@@ -8,6 +9,7 @@ interface Props {
}
export function MwHealthStrip({ issues }: Props) {
const { t } = useTranslation();
const level = worstHealthLevel(issues);
const color = healthLevelColor(level);
@@ -15,7 +17,7 @@ export function MwHealthStrip({ issues }: Props) {
return (
<div className="flex items-center gap-2 rounded-lg border border-emerald-500/20 bg-emerald-500/5 px-3 py-2 text-xs">
<CheckCircle2 className="h-3.5 w-3.5 dg-text-green shrink-0" />
<span className="text-[var(--dg-text-muted)]">指标正常,无告警</span>
<span className="text-[var(--dg-text-muted)]">{t('monitor.mwHealth.allNormal')}</span>
</div>
);
}
@@ -1,5 +1,6 @@
// 监控图表放大壳 — 复用主机监控 ChartToolbar + 宽屏弹框 // Logan.Li
import { useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle,
} from '@/components/ui/dialog';
@@ -28,6 +29,7 @@ export function ChartExpandShell({
children,
}: Props) {
const [expanded, setExpanded] = useState(false);
const { t } = useTranslation();
return (
<>
@@ -40,7 +42,7 @@ export function ChartExpandShell({
{!empty && <ChartToolbar onExpand={() => setExpanded(true)} />}
</div>
{empty ? (
<p className="text-xs text-center py-10 text-[var(--dg-text-muted)]">暂无数据</p>
<p className="text-xs text-center py-10 text-[var(--dg-text-muted)]">{t('common.noData')}</p>
) : (
children(compactHeight)
)}
@@ -50,7 +52,7 @@ export function ChartExpandShell({
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle className="text-base">{title}</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">放大查看监控趋势与指标细节</DialogDescription>
<DialogDescription className="text-[var(--dg-text-muted)]">{t('monitor.chartExpand.description')}</DialogDescription>
</DialogHeader>
<div className="pt-2">{children(expandedHeight)}</div>
</DialogContent>
@@ -1,4 +1,5 @@
// 站点拨测图表容器 — 详情页全宽展示,无放大弹框 // Logan.Li
import { useTranslation } from 'react-i18next';
import type { ReactNode } from 'react';
interface Props {
@@ -16,6 +17,7 @@ export function SiteChartCard({
empty = false,
children,
}: Props) {
const { t } = useTranslation();
return (
<div className="rounded-lg bg-[var(--dg-bg-tertiary)] p-3">
<div className="flex items-center gap-2 mb-2 min-w-0">
@@ -23,7 +25,7 @@ export function SiteChartCard({
<span className="text-xs text-[var(--dg-text-muted)] truncate">{title}</span>
</div>
{empty ? (
<p className="text-xs text-center py-12 text-[var(--dg-text-muted)]">暂无数据</p>
<p className="text-xs text-center py-12 text-[var(--dg-text-muted)]">{t('common.noData')}</p>
) : (
children(height)
)}
@@ -1,4 +1,5 @@
// 拨测任务详情页 — 可观测指标与历史记录(非弹框)// Logan.Li
import { useTranslation } from 'react-i18next';
import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -13,19 +14,20 @@ interface Props {
}
export function SiteTaskDetailPage({ task, onBack, onProbed }: Props) {
const { t } = useTranslation();
const probeType = normalizeProbeType(task.type);
return (
<div className="space-y-4">
<div className="flex items-start gap-3 flex-wrap">
<Button variant="outline" size="sm" className="h-8 shrink-0" onClick={onBack}>
<ArrowLeft className="h-3.5 w-3.5 mr-1" />返回列表
<ArrowLeft className="h-3.5 w-3.5 mr-1" />{t('monitor.siteTask.backToList')}
</Button>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)] truncate">{task.name}</h2>
<Badge variant="outline" className="text-[10px] shrink-0">{probeType}</Badge>
<span className="text-[10px] text-[var(--dg-text-muted)] shrink-0">频率 {fmtFrequency(task.frequency_sec)}</span>
<span className="text-[10px] text-[var(--dg-text-muted)] shrink-0">{t('monitor.siteTask.frequency')} {fmtFrequency(task.frequency_sec)}</span>
</div>
<p className="text-xs font-mono text-[var(--dg-text-muted)] truncate mt-0.5">{task.url}</p>
</div>
@@ -1,4 +1,5 @@
// 拨测异常事件时间线 // Logan.Li
import { useTranslation } from 'react-i18next';
import { AlertTriangle, Info, XCircle } from 'lucide-react';
import type { ProbeEvent } from './analytics';
@@ -20,12 +21,14 @@ const toneMap = {
error: 'dg-text-red',
};
export function SiteEventTimeline({ title, events, emptyText = '近期无异常事件' }: Props) {
export function SiteEventTimeline({ title, events, emptyText }: Props) {
const { t } = useTranslation();
const resolvedEmpty = emptyText ?? t('monitor.siteEvent.defaultEmpty');
return (
<div className="rounded-lg bg-[var(--dg-bg-tertiary)] p-3">
<p className="text-xs text-[var(--dg-text-muted)] mb-2">{title}</p>
{events.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] py-4 text-center">{emptyText}</p>
<p className="text-xs text-[var(--dg-text-muted)] py-4 text-center">{resolvedEmpty}</p>
) : (
<ul className="space-y-1.5 max-h-36 overflow-y-auto">
{events.map((e) => {
@@ -3,6 +3,7 @@ import {
Area, Bar, BarChart, CartesianGrid, Cell, ComposedChart, Line,
ReferenceArea, ReferenceLine, Tooltip, XAxis, YAxis,
} from 'recharts';
import { useTranslation } from 'react-i18next';
import { Globe, Network, Radio, Shield, Wifi } from 'lucide-react';
import type { SiteResult, SiteTask } from '../helpers';
import { fmtMs, fmtSslDays, normalizeProbeType } from '../helpers';
@@ -35,16 +36,18 @@ function SelectionArea({ left, right }: { left: string; right: string }) {
);
}
function HttpObs({ task, results, rangeLabel = '近 6 小时' }: Props) {
function HttpObs({ task, results, rangeLabel }: Props) {
const { t } = useTranslation();
const rl = rangeLabel ?? t('monitor.siteProtocol.rangeDefault');
const s = analyzeHttpResults(results, task);
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{[
{ label: `期望 ${s.expectedCode}`, value: s.okCount, tone: 'dg-text-green' },
{ label: `${t('monitor.siteProtocol.http.expectedPrefix')} ${s.expectedCode}`, value: s.okCount, tone: 'dg-text-green' },
{ label: '4xx', value: s.err4xx, tone: 'dg-text-yellow' },
{ label: '5xx', value: s.err5xx, tone: 'dg-text-red' },
{ label: '超时/其他', value: s.otherErr, tone: 'dg-text-red' },
{ label: t('monitor.siteProtocol.http.timeoutOther'), value: s.otherErr, tone: 'dg-text-red' },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-secondary)] rounded-lg px-3 py-2 text-center">
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
@@ -54,7 +57,7 @@ function HttpObs({ task, results, rangeLabel = '近 6 小时' }: Props) {
</div>
<SiteChartCard
icon={<Globe className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
title={`HTTP 状态码时序(${rangeLabel})`}
title={`${t('monitor.siteProtocol.http.statusCodeTitle')}(${rl})`}
empty={s.chart.length === 0}
>
{(h) => (
@@ -64,8 +67,8 @@ function HttpObs({ task, results, rangeLabel = '近 6 小时' }: Props) {
<CartesianGrid strokeDasharray="3 3" stroke={MONITOR_CHART_GRID} vertical={false} />
<XAxis dataKey="time" tick={MONITOR_CHART_TICK} tickLine={false} axisLine={{ stroke: MONITOR_CHART_GRID }} interval="preserveStartEnd" />
<YAxis tick={MONITOR_CHART_TICK} tickLine={false} axisLine={false} width={36} domain={[0, 'auto']} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => `HTTP ${v}`} nameFormatter={() => '状态码'} />} />
<ReferenceLine y={s.expectedCode} stroke="var(--dg-green)" strokeDasharray="4 4" label={{ value: `期望${s.expectedCode}`, fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => `HTTP ${v}`} nameFormatter={() => t('monitor.siteProtocol.http.statusCodeName')} />} />
<ReferenceLine y={s.expectedCode} stroke="var(--dg-green)" strokeDasharray="4 4" label={{ value: `${t('monitor.siteProtocol.http.expectedPrefix')}${s.expectedCode}`, fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Bar dataKey="code" radius={[2, 2, 0, 0]} maxBarSize={14} isAnimationActive={false}>
{displayData.map((d, i) => (
<Cell key={`${d.time}-${i}`} fill={d.ok ? 'var(--dg-green)' : d.code >= 500 ? 'var(--dg-red)' : d.code >= 400 ? '#eab308' : 'var(--dg-red)'} />
@@ -77,22 +80,23 @@ function HttpObs({ task, results, rangeLabel = '近 6 小时' }: Props) {
</MonitorRangeChartFrame>
)}
</SiteChartCard>
<SiteEventTimeline title="4xx / 5xx / 超时事件" events={s.events} />
<SiteEventTimeline title={t('monitor.siteProtocol.http.timelineTitle')} events={s.events} />
</div>
);
}
function SslObs({ task, results }: Props) {
const { t } = useTranslation();
const s = analyzeSslResults(results, task);
const sslInfo = fmtSslDays(s.days);
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{[
{ label: '证书到期', value: s.expiry || '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: '剩余天数', value: sslInfo.text, tone: sslInfo.tone },
{ label: '签发者', value: s.issuer ? (s.issuer.length > 16 ? `${s.issuer.slice(0, 16)}…` : s.issuer) : '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: '握手延迟', value: fmtMs(s.handshakeMs), tone: 'text-[var(--dg-text-primary)]' },
{ label: t('monitor.siteProtocol.ssl.expiry'), value: s.expiry || '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: t('monitor.siteProtocol.ssl.remainingDays'), value: sslInfo.text, tone: sslInfo.tone },
{ label: t('monitor.siteProtocol.ssl.issuer'), value: s.issuer ? (s.issuer.length > 16 ? `${s.issuer.slice(0, 16)}…` : s.issuer) : '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: t('monitor.siteProtocol.ssl.handshakeLatency'), value: fmtMs(s.handshakeMs), tone: 'text-[var(--dg-text-primary)]' },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-secondary)] rounded-lg px-3 py-2 text-center">
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
@@ -102,7 +106,7 @@ function SslObs({ task, results }: Props) {
</div>
<SiteChartCard
icon={<Shield className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
title="证书剩余天数趋势"
title={t('monitor.siteProtocol.ssl.daysTrendTitle')}
height={220}
empty={s.chart.length === 0}
>
@@ -113,9 +117,9 @@ function SslObs({ task, results }: Props) {
<CartesianGrid strokeDasharray="3 3" stroke={MONITOR_CHART_GRID} vertical={false} />
<XAxis dataKey="time" tick={MONITOR_CHART_TICK} tickLine={false} axisLine={{ stroke: MONITOR_CHART_GRID }} />
<YAxis tick={MONITOR_CHART_TICK} tickLine={false} axisLine={false} width={32} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => `${v} 天`} nameFormatter={() => '剩余'} />} />
<ReferenceLine y={30} stroke="#eab308" strokeDasharray="4 4" label={{ value: '30天', fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<ReferenceLine y={7} stroke="var(--dg-red)" strokeDasharray="4 4" label={{ value: '7天', fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => `${v} ${t('monitor.siteProtocol.ssl.daysUnit')}`} nameFormatter={() => t('monitor.siteProtocol.ssl.daysName')} />} />
<ReferenceLine y={30} stroke="#eab308" strokeDasharray="4 4" label={{ value: t('monitor.siteProtocol.ssl.ref30days'), fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<ReferenceLine y={7} stroke="var(--dg-red)" strokeDasharray="4 4" label={{ value: t('monitor.siteProtocol.ssl.ref7days'), fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Line type="monotone" dataKey="days" stroke="var(--dg-blue)" strokeWidth={2} dot={false} isAnimationActive={false} />
<SelectionArea left={refLeft} right={refRight} />
</ComposedChart>
@@ -123,24 +127,26 @@ function SslObs({ task, results }: Props) {
</MonitorRangeChartFrame>
)}
</SiteChartCard>
<SiteEventTimeline title="证书 / 握手异常" events={s.events} />
<SiteEventTimeline title={t('monitor.siteProtocol.ssl.timelineTitle')} events={s.events} />
</div>
);
}
function LatencyObs({ results, mode, rangeLabel = '近 6 小时' }: Props & { mode: 'PING' | 'DNS' }) {
function LatencyObs({ results, mode, rangeLabel }: Props & { mode: 'PING' | 'DNS' }) {
const { t } = useTranslation();
const rl = rangeLabel ?? t('monitor.siteProtocol.rangeDefault');
const s = mode === 'PING' ? analyzePingResults(results) : analyzeDnsResults(results);
const label = mode === 'PING' ? 'PING 延迟波动' : 'DNS 解析延迟';
const label = mode === 'PING' ? t('monitor.siteProtocol.latency.pingLabel') : t('monitor.siteProtocol.latency.dnsLabel');
const Icon = mode === 'PING' ? Wifi : Radio;
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{[
{ label: '平均', value: fmtMs(s.avg) },
{ label: '最小', value: fmtMs(s.min) },
{ label: '最大', value: fmtMs(s.max) },
{ label: t('monitor.siteProtocol.latency.avg'), value: fmtMs(s.avg) },
{ label: t('monitor.siteProtocol.latency.min'), value: fmtMs(s.min) },
{ label: t('monitor.siteProtocol.latency.max'), value: fmtMs(s.max) },
{ label: 'P95', value: fmtMs(s.p95) },
{ label: '波动次数', value: String(s.spikeCount), tone: s.spikeCount > 0 ? 'dg-text-yellow' : undefined },
{ label: t('monitor.siteProtocol.latency.spikeCount'), value: String(s.spikeCount), tone: s.spikeCount > 0 ? 'dg-text-yellow' : undefined },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-secondary)] rounded-lg px-3 py-2 text-center">
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
@@ -150,7 +156,7 @@ function LatencyObs({ results, mode, rangeLabel = '近 6 小时' }: Props & { mo
</div>
<SiteChartCard
icon={<Icon className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
title={`${label}(${rangeLabel})`}
title={`${label}(${rl})`}
empty={s.chart.length === 0}
>
{(h) => (
@@ -160,8 +166,8 @@ function LatencyObs({ results, mode, rangeLabel = '近 6 小时' }: Props & { mo
<CartesianGrid strokeDasharray="3 3" stroke={MONITOR_CHART_GRID} vertical={false} />
<XAxis dataKey="time" tick={MONITOR_CHART_TICK} tickLine={false} axisLine={{ stroke: MONITOR_CHART_GRID }} interval="preserveStartEnd" />
<YAxis tick={MONITOR_CHART_TICK} tickLine={false} axisLine={false} width={42} tickFormatter={(v: number) => (v >= 1000 ? `${(v / 1000).toFixed(1)}s` : `${v}ms`)} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v, name) => (name === 'latency' ? fmtMs(v) : String(v))} nameFormatter={(name) => (name === 'latency' ? '延迟' : name)} />} />
<ReferenceLine y={s.avg} stroke="var(--dg-blue)" strokeDasharray="4 4" label={{ value: '均值', fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v, name) => (name === 'latency' ? fmtMs(v) : String(v))} nameFormatter={(name) => (name === 'latency' ? t('monitor.siteProtocol.latency.latencyName') : name)} />} />
<ReferenceLine y={s.avg} stroke="var(--dg-blue)" strokeDasharray="4 4" label={{ value: t('monitor.siteProtocol.latency.avgLabel'), fontSize: 10, fill: 'var(--dg-text-muted)' }} />
<Line type="monotone" dataKey="latency" stroke="var(--dg-blue)" strokeWidth={2} dot={false} isAnimationActive={false} />
<SelectionArea left={refLeft} right={refRight} />
</ComposedChart>
@@ -169,20 +175,22 @@ function LatencyObs({ results, mode, rangeLabel = '近 6 小时' }: Props & { mo
</MonitorRangeChartFrame>
)}
</SiteChartCard>
<SiteEventTimeline title="延迟尖峰 / 失败事件" events={s.events} />
<SiteEventTimeline title={t('monitor.siteProtocol.latency.timelineTitle')} events={s.events} />
</div>
);
}
function TcpObs({ results, rangeLabel = '近 6 小时' }: Props) {
function TcpObs({ results, rangeLabel }: Props) {
const { t } = useTranslation();
const rl = rangeLabel ?? t('monitor.siteProtocol.rangeDefault');
const s = analyzeTcpResults(results);
return (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{[
{ label: `连通率(${rangeLabel})`, value: s.upRate, tone: 'dg-text-green' },
{ label: '不通次数', value: String(s.failCount), tone: s.failCount > 0 ? 'dg-text-red' : 'text-[var(--dg-text-primary)]' },
{ label: '最近失败', value: s.lastFailAt || '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: `${t('monitor.siteProtocol.tcp.upRate')}(${rl})`, value: s.upRate, tone: 'dg-text-green' },
{ label: t('monitor.siteProtocol.tcp.failCount'), value: String(s.failCount), tone: s.failCount > 0 ? 'dg-text-red' : 'text-[var(--dg-text-primary)]' },
{ label: t('monitor.siteProtocol.tcp.lastFail'), value: s.lastFailAt || '--', tone: 'text-[var(--dg-text-primary)]' },
].map((item) => (
<div key={item.label} className="bg-[var(--dg-bg-secondary)] rounded-lg px-3 py-2 text-center">
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
@@ -192,7 +200,7 @@ function TcpObs({ results, rangeLabel = '近 6 小时' }: Props) {
</div>
<SiteChartCard
icon={<Network className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
title="TCP 连通状态时序"
title={t('monitor.siteProtocol.tcp.statusTitle')}
height={200}
empty={s.chart.length === 0}
>
@@ -202,7 +210,7 @@ function TcpObs({ results, rangeLabel = '近 6 小时' }: Props) {
<ComposedChart data={displayData} margin={{ top: 4, right: 8, left: 0, bottom: 4 }} {...chartHandlers}>
<XAxis dataKey="time" tick={MONITOR_CHART_TICK} tickLine={false} axisLine={{ stroke: MONITOR_CHART_GRID }} interval="preserveStartEnd" />
<YAxis hide domain={[0, 100]} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => (v >= 100 ? '连通' : '不通')} nameFormatter={() => '状态'} />} />
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => (v >= 100 ? t('monitor.siteProtocol.tcp.connected') : t('monitor.siteProtocol.tcp.disconnected'))} nameFormatter={() => t('monitor.siteProtocol.tcp.statusName')} />} />
<Area type="stepAfter" dataKey="up" stroke="var(--dg-green)" fill="var(--dg-green)" fillOpacity={0.2} isAnimationActive={false} />
<SelectionArea left={refLeft} right={refRight} />
</ComposedChart>
@@ -210,7 +218,7 @@ function TcpObs({ results, rangeLabel = '近 6 小时' }: Props) {
</MonitorRangeChartFrame>
)}
</SiteChartCard>
<SiteEventTimeline title="TCP 不通时间点" events={s.events} emptyText="近期 TCP 均连通" />
<SiteEventTimeline title={t('monitor.siteProtocol.tcp.timelineTitle')} events={s.events} emptyText={t('monitor.siteProtocol.tcp.emptyText')} />
</div>
);
}
@@ -222,4 +230,4 @@ export function SiteProtocolObs({ task, results, rangeLabel }: Props) {
if (type === 'DNS') return <LatencyObs task={task} results={results} mode="DNS" rangeLabel={rangeLabel} />;
if (type === 'TCP') return <TcpObs task={task} results={results} rangeLabel={rangeLabel} />;
return <HttpObs task={task} results={results} rangeLabel={rangeLabel} />;
}
}
@@ -1,5 +1,6 @@
// SNMP 内置 OID 模板 — 默认折叠,不占用主监控区域 // Logan.Li
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronDown, Radio } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
@@ -25,6 +26,7 @@ interface Props {
export function SnmpTemplatesCollapsible({ templates }: Props) {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
return (
<Collapsible open={open} onOpenChange={setOpen}>
@@ -35,9 +37,9 @@ export function SnmpTemplatesCollapsible({ templates }: Props) {
>
<span className="flex items-center gap-2 text-xs text-[var(--dg-text-muted)]">
<Radio className="h-3.5 w-3.5" />
<span className="text-[var(--dg-text-primary)] font-medium">内置 OID 模板</span>
<Badge variant="outline" className="text-[10px] h-5">{templates.length} 个</Badge>
<span className="hidden sm:inline">H3C / 华为 / Cisco / 锐捷 / Dell / 浪潮</span>
<span className="text-[var(--dg-text-primary)] font-medium">{t('monitor.snmp.builtinTemplates')}</span>
<Badge variant="outline" className="text-[10px] h-5">{t('monitor.snmp.templateCount', { count: templates.length })}</Badge>
<span className="hidden sm:inline">{t('monitor.snmp.vendors')}</span>
</span>
<ChevronDown className={cn('h-4 w-4 shrink-0 text-[var(--dg-text-muted)] transition-transform', open && 'rotate-180')} />
</button>
@@ -65,7 +67,7 @@ export function SnmpTemplatesCollapsible({ templates }: Props) {
</div>
))}
{templates.length === 0 && (
<p className="text-xs text-[var(--dg-text-muted)] col-span-2 py-4 text-center">模板尚未初始化,重启服务后自动种子</p>
<p className="text-xs text-[var(--dg-text-muted)] col-span-2 py-4 text-center">{t('monitor.snmp.templatesNotInit')}</p>
)}
</div>
</div>