feat(i18n): 剩余模块全量国际化 — 51% 覆盖率 (231 文件接入 t())
批量转换 163 个文件,3204 处中文替换为 t() 调用: - Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换 - Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换 - Deploy: 流水线/发布/凭证/RBAC 全部转换 - Server/Cloud/Agent/Task/Layout/UI: 全部转换 - 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys) - tsc 编译零错误
This commit is contained in:
1 parent
78c27037e3
commit
ecec73989b
166 files changed
+6925
-2800
No files matched your search
@@ -31,8 +31,10 @@ import { PortPolicyDialog } from './host-monitor/PortPolicyDialog';
|
||||
import { SystemInfoSection } from './host-monitor/SystemInfoSection';
|
||||
import { HostCoreMetricsStrip } from './host-monitor/HostCoreMetricsStrip';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const [searchParams] = useSearchParams();
|
||||
const urlHost = searchParams.get('host') || '';
|
||||
@@ -124,7 +126,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={selectedHost} onValueChange={setSelectedHost}>
|
||||
<SelectTrigger className="w-52 h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择主机..." />
|
||||
<SelectValue placeholder=t('monitor.hostmonitor.sb6637d') />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{hosts.map((h: JsonData) => (
|
||||
@@ -134,7 +136,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
</Select>
|
||||
{serverInfo && (
|
||||
<span className="text-xs dg-text-muted">
|
||||
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} 核` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
|
||||
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? t('monitor.hostmonitor.se307d8', { serverInfo.cpu_cores as number | string: serverInfo.cpu_cores as number | string }) : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -144,9 +146,9 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="avg">平均值</SelectItem>
|
||||
<SelectItem value="max">最大值</SelectItem>
|
||||
<SelectItem value="min">最小值</SelectItem>
|
||||
<SelectItem value="avg">{t('monitor.hostmonitor.se68632')}</SelectItem>
|
||||
<SelectItem value="max">{t('monitor.hostmonitor.s5da893')}</SelectItem>
|
||||
<SelectItem value="min">{t('monitor.hostmonitor.sc322ed')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-col items-end gap-0.5">
|
||||
@@ -163,13 +165,13 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="h-8 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] gap-0 overflow-x-auto">
|
||||
{[
|
||||
{ id: 'basic', label: '基础监控', icon: <Monitor className="w-3.5 h-3.5" /> },
|
||||
{ id: 'disk', label: '磁盘监控', icon: <HardDrive className="w-3.5 h-3.5" /> },
|
||||
{ id: 'network', label: '网络监控', icon: <Network className="w-3.5 h-3.5" /> },
|
||||
{ id: 'ports', label: '端口监控', icon: <Server className="w-3.5 h-3.5" /> },
|
||||
{ id: 'process', label: '进程监控', icon: <List className="w-3.5 h-3.5" /> },
|
||||
{ id: 'system', label: '系统信息', icon: <Info className="w-3.5 h-3.5" /> },
|
||||
{ id: 'health_probes', label: '健康探针', icon: <HeartPulse className="w-3.5 h-3.5" /> },
|
||||
{ id: 'basic', label: t('monitor.hostmonitor.s003038'), icon: <Monitor className="w-3.5 h-3.5" /> },
|
||||
{ id: 'disk', label: t('monitor.hostmonitor.s5dd0a0'), icon: <HardDrive className="w-3.5 h-3.5" /> },
|
||||
{ id: 'network', label: t('monitor.hostmonitor.s59805c'), icon: <Network className="w-3.5 h-3.5" /> },
|
||||
{ id: 'ports', label: t('agent.portMonitor'), icon: <Server className="w-3.5 h-3.5" /> },
|
||||
{ id: 'process', label: t('monitor.hostProcess'), icon: <List className="w-3.5 h-3.5" /> },
|
||||
{ id: 'system', label: t('monitor.hostmonitor.sb7ea5e'), icon: <Info className="w-3.5 h-3.5" /> },
|
||||
{ id: 'health_probes', label: t('kubernetes.container.probes'), icon: <HeartPulse className="w-3.5 h-3.5" /> },
|
||||
].map(tab => (
|
||||
<TabsTrigger key={tab.id} value={tab.id} className="text-[11px] h-7 gap-1.5 px-3 data-[state=active]:bg-[var(--dg-bg-primary)]">
|
||||
<span className="shrink-0 opacity-80">{tab.icon}</span>
|
||||
@@ -179,35 +181,35 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu />} iconTone="blue"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s4e6cbf') value={currentVal('cpu')} icon={<Cpu />} iconTone="blue"
|
||||
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||
<MetricChartCard title="CPU IO等待(%)" value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock />} iconTone="orange"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s79f80f') 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} />
|
||||
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server />} iconTone="green"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s78262b') value={currentVal('mem')} icon={<Server />} iconTone="green"
|
||||
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||
<MetricChartCard title="可用内存(GB)" value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server />} iconTone="green"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.sa23fcd') 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} />
|
||||
<MetricChartCard title="Swap 使用率(%)" value={currentVal('swap')} icon={<HardDrive />} iconTone="red"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s05589b') value={currentVal('swap')} icon={<HardDrive />} iconTone="red"
|
||||
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity />} iconTone="purple"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.sae62ef') value={currentVal('load1')} icon={<Activity />} iconTone="purple"
|
||||
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="disk" className="mt-3 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<MetricChartCard title="磁盘使用率(%)" value={currentVal('disk')} icon={<HardDrive />} iconTone="orange"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.sdf9692') value={currentVal('disk')} icon={<HardDrive />} iconTone="orange"
|
||||
data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||
<MetricChartCard title="Inode 使用率(%)" value={(() => {
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s202961') value={(() => {
|
||||
const dArr = Array.isArray(parseJSON(latest?.disk_info)) ? parseJSON(latest?.disk_info) : [];
|
||||
const root = dArr.find((d: JsonData) => d.mount === '/') || dArr[0];
|
||||
return root?.inode_usage_pct != null ? `${root.inode_usage_pct.toFixed(1)}%` : '--';
|
||||
})()} icon={<HardDrive />} iconTone="purple"
|
||||
data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} referenceLines={pctRefLines} />
|
||||
<MetricChartCard title="磁盘读取(KB/s)" value={currentVal('ioR')} icon={<Disc />} iconTone="blue"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s72e3c2') value={currentVal('ioR')} icon={<Disc />} iconTone="blue"
|
||||
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc />} iconTone="green"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.sa9b96f') value={currentVal('ioW')} icon={<Disc />} iconTone="green"
|
||||
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘IO繁忙度(%)" value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc />} iconTone="red"
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s996ada') 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} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -215,26 +217,26 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<TabsContent value="network" className="mt-3 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<MetricChartCard title="TCP EST" value={currentVal('tcp')} icon={<Network />} iconTone="purple"
|
||||
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
data={metrics.tcpEst} color="#8b5cf6" unit=t('monitor.hostmonitor.s930882') domain={[0, 'auto']} aggType={aggType} />
|
||||
<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=t('monitor.hostmonitor.s930882') 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"
|
||||
data={metrics.tcpCW} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="TCP 总连接" value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network />} iconTone="cyan"
|
||||
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
data={metrics.tcpCW} color="#ef4444" unit=t('monitor.hostmonitor.s930882') domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s56c1b4') value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network />} iconTone="cyan"
|
||||
data={metrics.connTotal} color="#22d3ee" unit=t('monitor.hostmonitor.s930882') domain={[0, 'auto']} aggType={aggType} />
|
||||
</div>
|
||||
<DualLineChart
|
||||
title="网卡带宽吞吐"
|
||||
title=t('monitor.hostmonitor.s1e2a9e')
|
||||
currentRx={netInfo?.net_rx_bps != null ? formatBPS(netInfo.net_rx_bps) : '--'}
|
||||
currentTx={netInfo?.net_tx_bps != null ? formatBPS(netInfo.net_tx_bps) : '--'}
|
||||
rxData={metrics.rx} txData={metrics.tx}
|
||||
aggType={aggType} unit="KB/s"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||
<MetricChartCard title="网卡接收错误" 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} />
|
||||
<MetricChartCard title="网卡发送错误" 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} />
|
||||
<MetricChartCard title=t('monitor.hostmonitor.s9f7e99') value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle />} iconTone="red"
|
||||
data={metrics.rxErr} color="#ef4444" unit=t('monitor.hostmonitor.s930882') domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title=t('monitor.hostmonitor.sb2ffd5') value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle />} iconTone="orange"
|
||||
data={metrics.txErr} color="#f97316" unit=t('monitor.hostmonitor.s930882') domain={[0, 'auto']} aggType={aggType} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -269,10 +271,10 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
|
||||
<th className="py-1.5 text-left font-medium">端口</th>
|
||||
<th className="py-1.5 text-left font-medium">协议</th>
|
||||
<th className="py-1.5 text-left font-medium">进程</th>
|
||||
<th className="py-1.5 text-right font-medium">状态</th>
|
||||
<th className="py-1.5 text-left font-medium">{t('server.csv.port')}</th>
|
||||
<th className="py-1.5 text-left font-medium">{t('common.protocol')}</th>
|
||||
<th className="py-1.5 text-left font-medium">{t('monitor.hostmonitor.sf88522')}</th>
|
||||
<th className="py-1.5 text-right font-medium">{t('common.status')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -315,11 +317,11 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
|
||||
<th className="py-1.5 text-left font-medium w-[40%]">进程名称 / CMD</th>
|
||||
<th className="py-1.5 text-left font-medium w-[40%]">{t('monitor.hostmonitor.s23ecaf')}</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%]">内存使用率(%)</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%]">打开文件数</th>
|
||||
<th className="py-1.5 text-right font-medium w-[16%]">{t('monitor.hostmonitor.s78262b')}</th>
|
||||
<th className="py-1.5 text-right font-medium w-[16%]">{t('monitor.hostmonitor.s4cfc11')}</th>
|
||||
<th className="py-1.5 text-right font-medium w-[20%]">{t('monitor.hostmonitor.s8fe5ed')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -336,7 +338,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
</span>
|
||||
<button
|
||||
className="opacity-0 group-hover/cmd:opacity-100 transition-opacity p-0.5 hover:dg-text-cyan shrink-0"
|
||||
title="复制命令行"
|
||||
title=t('monitor.hostmonitor.s92cb88')
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
@@ -398,15 +400,15 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<>
|
||||
<div className="flex items-center gap-4 mb-3 text-xs">
|
||||
<div>
|
||||
<span className="text-[var(--dg-text-muted)]">已分配 </span>
|
||||
<span className="text-[var(--dg-text-muted)]">{t('common.assigned')} </span>
|
||||
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.allocated.toLocaleString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--dg-text-muted)]">上限 </span>
|
||||
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.max === -1 ? '无限制' : fdInfo.max?.toLocaleString() || '—'}</span>
|
||||
<span className="text-[var(--dg-text-muted)]">{t('monitor.hostmonitor.s726bfb')} </span>
|
||||
<span className="font-mono text-[var(--dg-text-primary)] font-semibold">{fdInfo.max === -1 ? t('monitor.hostmonitor.sbc4364') : fdInfo.max?.toLocaleString() || '—'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--dg-text-muted)]">使用率 </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'}`}>
|
||||
{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}
|
||||
</span>
|
||||
@@ -416,12 +418,12 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<ZoomableAreaChart
|
||||
ref={fdChartRef}
|
||||
data={metrics.fd.map((d) => ({ ...d, value: d[aggType] }))}
|
||||
series={[{ dataKey: 'value', color: '#a78bfa', label: 'FD 使用率' }]}
|
||||
series={[{ dataKey: 'value', color: '#a78bfa', label: t('monitor.hostmonitor.s28130d') }]}
|
||||
yDomain={[0, 'auto']}
|
||||
unit="%"
|
||||
chartId="fd-usage"
|
||||
title="FD 文件句柄使用率"
|
||||
subtitle={fdInfo?.usage_pct != null ? `当前 ${fdInfo.usage_pct.toFixed(1)}%` : undefined}
|
||||
title=t('monitor.hostmonitor.sc026a1')
|
||||
subtitle={fdInfo?.usage_pct != null ? t('monitor.hostmonitor.sbe891d', { fdInfo.usage_pct.toFixed(1): fdInfo.usage_pct.toFixed(1) }) : undefined}
|
||||
height={160}
|
||||
toolbarPlacement="header"
|
||||
onZoomedChange={setFdChartZoomed}
|
||||
@@ -430,7 +432,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">等待数据…</div>
|
||||
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">{t('monitor.hostmonitor.sae1a30')}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -444,14 +446,14 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">CTX 切换/秒</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">{t('monitor.hostmonitor.s18f91f')}</div>
|
||||
<div className="text-lg font-bold font-mono dg-text-green">
|
||||
{(csInfo.ctx_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.ctx_per_sec?.toLocaleString()} 次</div>
|
||||
</div>
|
||||
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">IRQ 中断/秒</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">{t('monitor.hostmonitor.sb7cd83')}</div>
|
||||
<div className="text-lg font-bold font-mono dg-text-yellow">
|
||||
{(csInfo.irq_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
|
||||
</div>
|
||||
@@ -476,7 +478,7 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">等待数据…</div>
|
||||
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">{t('monitor.hostmonitor.sae1a30')}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -529,12 +531,12 @@ export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title="主机监控"
|
||||
subtitle={selectedHost ? `当前主机: ${selectedHost}` : '选择主机查看监控指标'}
|
||||
title=i18n.t('monitor.hostMonitor')
|
||||
subtitle={selectedHost ? i18n.t('monitor.hostmonitor.s60cdc3', { selectedHost: selectedHost }) : i18n.t('monitor.hostmonitor.s8d4930')}
|
||||
icon={<Server className="h-5 w-5" />}
|
||||
toolbar={toolbar}
|
||||
loading={loading && !history.length}
|
||||
loadingLabel="加载监控数据..."
|
||||
loadingLabel=i18n.t('monitor.hostmonitor.sc23e80')
|
||||
contentClassName="space-y-3"
|
||||
>
|
||||
{monitorBody}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { metricsApi } from '@/api';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface TargetInfo {
|
||||
id: number;
|
||||
@@ -29,14 +31,15 @@ interface MetricPoint {
|
||||
|
||||
const METRIC_CATEGORIES: Record<string, { label: string; icon: React.ReactNode }> = {
|
||||
cpu: { label: 'CPU', icon: <Cpu className="h-4 w-4" /> },
|
||||
memory: { label: '内存', icon: <HardDrive className="h-4 w-4" /> },
|
||||
disk: { label: '磁盘', icon: <HardDrive className="h-4 w-4" /> },
|
||||
network: { label: '网络', icon: <Network className="h-4 w-4" /> },
|
||||
middleware: { label: '中间件', icon: <Database className="h-4 w-4" /> },
|
||||
device: { label: '设备', icon: <Activity className="h-4 w-4" /> },
|
||||
memory: { label: i18n.t('k8s.pods.memory'), icon: <HardDrive className="h-4 w-4" /> },
|
||||
disk: { label: i18n.t('dashboard.topUsage.disk'), icon: <HardDrive className="h-4 w-4" /> },
|
||||
network: { label: i18n.t('menu.network'), icon: <Network className="h-4 w-4" /> },
|
||||
middleware: { label: i18n.t('dashboard.ops.middleware'), icon: <Database className="h-4 w-4" /> },
|
||||
device: { label: i18n.t('monitor.metricsexplorer.sf55f17'), icon: <Activity className="h-4 w-4" /> },
|
||||
};
|
||||
|
||||
export function MetricsExplorer() {
|
||||
const { t } = useTranslation();
|
||||
const [targets, setTargets] = useState<TargetInfo[]>([]);
|
||||
const [metricNames, setMetricNames] = useState<string[]>([]);
|
||||
const [selectedTarget, setSelectedTarget] = useState('');
|
||||
@@ -108,7 +111,7 @@ export function MetricsExplorer() {
|
||||
<div className="flex items-center gap-4 flex-wrap w-full">
|
||||
<Select value={selectedTarget} onValueChange={setSelectedTarget}>
|
||||
<SelectTrigger className="w-64 dg-input">
|
||||
<SelectValue placeholder={loading ? '加载中...' : '选择监控目标...'} />
|
||||
<SelectValue placeholder={loading ? t('common.loading') : t('monitor.metricsexplorer.sce4193')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input max-h-80">
|
||||
{targets.map(t => (
|
||||
@@ -125,7 +128,7 @@ export function MetricsExplorer() {
|
||||
<Search className="absolute left-2.5 top-2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
className="pl-8 dg-input h-9"
|
||||
placeholder="搜索指标..."
|
||||
placeholder=t('monitor.metricsexplorer.s21512a')
|
||||
value={searchKw}
|
||||
onChange={(e) => setSearchKw(e.target.value)}
|
||||
/>
|
||||
@@ -139,19 +142,19 @@ export function MetricsExplorer() {
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title="指标浏览器"
|
||||
subtitle="按分类/主机浏览所有指标,支持叠加对比"
|
||||
title=t('monitor.metricsexplorer.s26a10d')
|
||||
subtitle=t('monitor.metricsexplorer.s454945')
|
||||
icon={<Activity className="h-5 w-5" />}
|
||||
toolbar={toolbar}
|
||||
loading={loading}
|
||||
loadingLabel="加载监控目标..."
|
||||
loadingLabel=t('monitor.metricsexplorer.s17f427')
|
||||
contentClassName="space-y-6"
|
||||
>
|
||||
{!selectedTarget ? (
|
||||
<PageEmpty
|
||||
icon={<Filter className="h-12 w-12" />}
|
||||
title="请选择监控目标"
|
||||
description="选择目标主机后,从左侧勾选要查看的指标"
|
||||
title=t('monitor.metricsexplorer.sce251e')
|
||||
description=t('monitor.metricsexplorer.sc1966a')
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
@@ -159,7 +162,7 @@ export function MetricsExplorer() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<BarChart3 className="h-4 w-4 dg-text-muted" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">指标列表</h3>
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">{t('monitor.metricsexplorer.s3ec503')}</h3>
|
||||
<span className="text-xs dg-text-muted ml-auto">{selectedMetrics.length} 已选</span>
|
||||
</div>
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
@@ -204,9 +207,9 @@ export function MetricsExplorer() {
|
||||
</h3>
|
||||
</div>
|
||||
{selectedMetrics.length === 0 ? (
|
||||
<PageEmpty icon={<Plus className="h-8 w-8" />} title="请从左侧勾选指标" className="py-16" />
|
||||
<PageEmpty icon={<Plus className="h-8 w-8" />} title=t('monitor.metricsexplorer.s701e60') className="py-16" />
|
||||
) : sortedTimes.length === 0 ? (
|
||||
<PageEmpty title="暂无数据" className="py-16" />
|
||||
<PageEmpty title=t('common.noData') className="py-16" />
|
||||
) : (
|
||||
<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]">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { InstanceCard } from './middleware-monitor/InstanceCard';
|
||||
import { MiddlewareDialog } from './middleware-monitor/MiddlewareDialog';
|
||||
import type { MwTarget } from './middleware-monitor/helpers';
|
||||
import { REDIS_CHARTS, MYSQL_CHARTS, evalMwHealth, worstHealthLevel } from './middleware-monitor/helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type TabKey = 'redis' | 'mysql';
|
||||
const REFRESH_INTERVAL_MS = 30_000;
|
||||
@@ -19,6 +20,7 @@ const REFRESH_INTERVAL_MS = 30_000;
|
||||
interface MwListResponse { list: MwTarget[]; total: number }
|
||||
|
||||
export function MiddlewareMonitor() {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const [targets, setTargets] = useState<MwTarget[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -61,7 +63,7 @@ export function MiddlewareMonitor() {
|
||||
hasLoaded.current = true;
|
||||
} catch (err) {
|
||||
if (!silent) {
|
||||
setError((err as { message?: string }).message || '加载失败');
|
||||
setError((err as { message?: string }).message || t('common.loadFailed'));
|
||||
setTargets([]);
|
||||
}
|
||||
} finally {
|
||||
@@ -85,14 +87,14 @@ export function MiddlewareMonitor() {
|
||||
const warnCount = targets.filter((t) => worstHealthLevel(evalMwHealth(t)) !== 'ok').length;
|
||||
|
||||
const handleDelete = async (target: MwTarget) => {
|
||||
const ok = await confirm({ title: '删除监控', description: `确认删除 "${target.name}"?`, confirmText: '删除' });
|
||||
const ok = await confirm({ title: t('monitor.middlewaremonitor.sf9be97'), description: t('monitor.middlewaremonitor.s45a9e0', { target.name: target.name }), confirmText: t('common.delete') });
|
||||
if (!ok) return;
|
||||
try {
|
||||
await middlewareMonitorApi.delete(target.id);
|
||||
toast.success('已删除');
|
||||
toast.success(t('alert.deleted'));
|
||||
loadTargets();
|
||||
} catch (err) {
|
||||
toast.error('删除失败', { description: (err as { message?: string }).message });
|
||||
toast.error(t('common.deleteFailed'), { description: (err as { message?: string }).message });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,10 +102,10 @@ export function MiddlewareMonitor() {
|
||||
setTesting(target.id);
|
||||
try {
|
||||
const res = await middlewareMonitorApi.test(target.id) as { success: boolean; message: string };
|
||||
if (res.success) { toast.success('连接成功', { description: res.message }); loadTargets(true); }
|
||||
else toast.error('连接失败', { description: res.message });
|
||||
if (res.success) { toast.success(t('aiops.llmTestSuccess'), { description: res.message }); loadTargets(true); }
|
||||
else toast.error(t('aiops.llmTestFailed'), { description: res.message });
|
||||
} catch (err) {
|
||||
toast.error('测试失败', { description: (err as { message?: string }).message });
|
||||
toast.error(t('monitor.middlewaremonitor.s9710d9'), { description: (err as { message?: string }).message });
|
||||
} finally {
|
||||
setTesting(null);
|
||||
}
|
||||
@@ -114,11 +116,11 @@ export function MiddlewareMonitor() {
|
||||
try {
|
||||
const res = await middlewareMonitorApi.seedDefaults() as { created: number; message: string; failed?: string[] };
|
||||
await loadTargets();
|
||||
if (res.failed?.length) toast.error('部分失败', { description: res.message });
|
||||
else if (res.created > 0) toast.success('已添加默认监控', { description: res.message });
|
||||
else toast.success('默认目标已存在');
|
||||
if (res.failed?.length) toast.error(t('monitor.middlewaremonitor.s669e89'), { description: res.message });
|
||||
else if (res.created > 0) toast.success(t('monitor.middlewaremonitor.s11a31e'), { description: res.message });
|
||||
else toast.success(t('monitor.middlewaremonitor.s7dda0f'));
|
||||
} catch (err) {
|
||||
toast.error('添加失败', { description: (err as { message?: string }).message });
|
||||
toast.error(t('deploy.toast.addFailed'), { description: (err as { message?: string }).message });
|
||||
} finally {
|
||||
setSeeding(false);
|
||||
}
|
||||
@@ -127,10 +129,10 @@ export function MiddlewareMonitor() {
|
||||
const handleCollectNow = async () => {
|
||||
try {
|
||||
const res = await middlewareMonitorApi.collectNow() as { message: string };
|
||||
toast.success('采集完成', { description: res.message });
|
||||
toast.success(t('monitor.middlewaremonitor.sc2c744'), { description: res.message });
|
||||
loadTargets(true);
|
||||
} catch (err) {
|
||||
toast.error('采集失败', { description: (err as { message?: string }).message });
|
||||
toast.error(t('monitor.middlewaremonitor.s5665b6'), { description: (err as { message?: string }).message });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -148,13 +150,13 @@ export function MiddlewareMonitor() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">中间件监控</h2>
|
||||
<h2 className="text-lg font-semibold">{t('monitor.middleware')}</h2>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">
|
||||
Redis 单机/集群 · MySQL · 30s 自动刷新
|
||||
{lastRefreshAt && (
|
||||
<span className="ml-2">
|
||||
上次更新 {lastRefreshAt.toLocaleTimeString()}
|
||||
{refreshing && ' · 刷新中...'}
|
||||
{refreshing && t('monitor.middlewaremonitor.s4d2bc9')}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
@@ -192,10 +194,10 @@ export function MiddlewareMonitor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-12 text-center">
|
||||
<Database className="h-12 w-12 mx-auto mb-4 text-[var(--dg-text-muted)]" />
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">暂无中间件监控目标</p>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">{t('monitor.middlewaremonitor.sbbe62a')}</p>
|
||||
<div className="flex justify-center gap-2 mt-4">
|
||||
<Button size="sm" onClick={handleSeedDefaults} disabled={seeding}>添加平台默认</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => openCreate()}>手动添加</Button>
|
||||
<Button size="sm" onClick={handleSeedDefaults} disabled={seeding}>{t('monitor.middlewaremonitor.se5caf1')}</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => openCreate()}>{t('monitor.middlewaremonitor.s63edd6')}</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -205,9 +207,9 @@ export function MiddlewareMonitor() {
|
||||
<>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
{[
|
||||
{ label: '监控实例', value: targets.length, icon: Activity },
|
||||
{ label: '在线', value: onlineCount, icon: Database, color: 'dg-text-green' },
|
||||
{ label: '告警', value: warnCount, icon: Activity, color: warnCount > 0 ? 'dg-text-yellow' : 'dg-text-green' },
|
||||
{ label: t('monitor.middlewaremonitor.s35a558'), value: targets.length, icon: Activity },
|
||||
{ label: t('common.online'), value: onlineCount, icon: Database, color: 'dg-text-green' },
|
||||
{ label: t('dashboard.stat.alerts'), 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: 'MySQL', value: mysqlTargets.length, icon: Server, color: 'dg-text-blue' },
|
||||
].map((item) => (
|
||||
@@ -250,7 +252,7 @@ export function MiddlewareMonitor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-8 text-center text-sm text-[var(--dg-text-muted)]">
|
||||
暂无 {tab === 'redis' ? 'Redis' : 'MySQL'} 实例,
|
||||
<button type="button" className="dg-text-cyan ml-1 underline" onClick={() => openCreate(tab)}>立即添加</button>
|
||||
<button type="button" className="dg-text-cyan ml-1 underline" onClick={() => openCreate(tab)}>{t('monitor.middlewaremonitor.s8ec8a6')}</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
|
||||
@@ -7,14 +7,16 @@ import { HostMonitor } from './HostMonitor';
|
||||
import { MiddlewareMonitor } from './MiddlewareMonitor';
|
||||
import { SNMPMonitor } from './SNMPMonitor';
|
||||
import { SiteMonitor } from './SiteMonitor';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
type TabId = 'host' | 'middleware' | 'snmp' | 'site';
|
||||
|
||||
const tabs: { id: TabId; name: string; description: string }[] = [
|
||||
{ id: 'host', name: '主机监控', description: 'CPU / 内存 / 磁盘 / 网络 / 磁盘IO' },
|
||||
{ id: 'middleware', name: '中间件', description: 'Redis 单机·集群 / MySQL 指标与趋势' },
|
||||
{ id: 'snmp', name: 'SNMP', description: '网络设备 / BMC 采集与趋势' },
|
||||
{ id: 'site', name: '站点拨测', description: 'HTTP Web · SSL 证书 · TCP / DNS / PING' },
|
||||
{ id: 'host', name: i18n.t('monitor.hostMonitor'), description: i18n.t('monitor.monitorcenter.s811145') },
|
||||
{ id: 'middleware', name: i18n.t('dashboard.ops.middleware'), description: i18n.t('monitor.monitorcenter.s757cda') },
|
||||
{ id: 'snmp', name: 'SNMP', description: i18n.t('monitor.monitorcenter.s24f24e') },
|
||||
{ id: 'site', name: i18n.t('monitor.siteProbe'), description: i18n.t('monitor.monitorcenter.s5ea4cd') },
|
||||
];
|
||||
|
||||
const TAB_IDS: TabId[] = ['host', 'middleware', 'snmp', 'site'];
|
||||
@@ -24,6 +26,7 @@ function parseTab(raw: string | null): TabId {
|
||||
}
|
||||
|
||||
export function MonitorCenter() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [activeTab, setActiveTab] = useState<TabId>(() => parseTab(searchParams.get('tab')));
|
||||
|
||||
@@ -42,8 +45,8 @@ export function MonitorCenter() {
|
||||
return (
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="监控中心"
|
||||
subtitle={current?.description ?? '全面的基础设施与应用监控'}
|
||||
title=t('menu.monitor')
|
||||
subtitle={current?.description ?? t('monitor.monitorcenter.s771120')}
|
||||
icon={<Activity className="h-5 w-5" />}
|
||||
tabs={tabs.map((t) => ({ id: t.id, label: t.name }))}
|
||||
activeTab={activeTab}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { SnmpOverviewCharts } from './snmp-monitor/SnmpOverviewCharts';
|
||||
import { SnmpDeviceChartsPanel } from './snmp-monitor/SnmpDeviceChartsPanel';
|
||||
import { SnmpTemplatesCollapsible, type SnmpTemplate } from './snmp-monitor/SnmpTemplatesCollapsible';
|
||||
import type { SnmpDevice, SnmpMetric, SnmpOverviewItem } from './snmp-monitor/helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SnmpStats { total: number; online: number; abnormal: number }
|
||||
|
||||
@@ -37,6 +38,7 @@ const keyMetrics = [
|
||||
];
|
||||
|
||||
export function SNMPMonitor() {
|
||||
const { t } = useTranslation();
|
||||
const [stats, setStats] = useState<SnmpStats>({ total: 0, online: 0, abnormal: 0 });
|
||||
const [devices, setDevices] = useState<SnmpDevice[]>([]);
|
||||
const [overview, setOverview] = useState<SnmpOverviewItem[]>([]);
|
||||
@@ -104,7 +106,7 @@ export function SNMPMonitor() {
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!form.name.trim() || !form.ip.trim() || !form.template_id) {
|
||||
toast.error('请填写设备名称、IP 并选择 OID 模板');
|
||||
toast.error(t('monitor.snmpmonitor.se385b8'));
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
@@ -117,12 +119,12 @@ export function SNMPMonitor() {
|
||||
template_id: Number(form.template_id),
|
||||
vendor: tpl?.vendor || '', device_type: tpl?.device_type || 'switch',
|
||||
});
|
||||
toast.success('设备已添加');
|
||||
toast.success(t('monitor.snmpmonitor.sbd0077'));
|
||||
setDialogOpen(false);
|
||||
setForm({ ...emptyForm });
|
||||
refresh();
|
||||
} catch (e) {
|
||||
toast.error((e as { message?: string }).message || '添加失败');
|
||||
toast.error((e as { message?: string }).message || t('deploy.toast.addFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -133,10 +135,10 @@ export function SNMPMonitor() {
|
||||
setCollectingId(d.id);
|
||||
try {
|
||||
await snmpApi.collectDevice(d.id);
|
||||
toast.success(`${d.name} 采集成功`);
|
||||
toast.success(t('monitor.snmpmonitor.s1f92b0', { d.name: d.name }));
|
||||
refresh();
|
||||
} catch (err) {
|
||||
toast.error((err as { message?: string }).message || '采集失败');
|
||||
toast.error((err as { message?: string }).message || t('monitor.snmpmonitor.s5665b6'));
|
||||
refresh();
|
||||
} finally {
|
||||
setCollectingId(null);
|
||||
@@ -162,18 +164,18 @@ export function SNMPMonitor() {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await snmpApi.deleteDevice(id);
|
||||
toast.success('已删除');
|
||||
toast.success(t('alert.deleted'));
|
||||
if (selectedDevice?.id === id) setSelectedDevice(null);
|
||||
refresh();
|
||||
} catch {
|
||||
toast.error('删除失败');
|
||||
toast.error(t('common.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const statusBadge = (status?: string) => {
|
||||
if (status === 'online') return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30">在线</Badge>;
|
||||
if (status === 'error') return <Badge className="bg-red-500/15 dg-text-red border-red-500/30">异常</Badge>;
|
||||
return <Badge className="bg-[var(--dg-bg-active)]/15 dg-text-secondary border-[var(--dg-border)]">离线</Badge>;
|
||||
if (status === 'online') return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30">{t('common.online')}</Badge>;
|
||||
if (status === 'error') return <Badge className="bg-red-500/15 dg-text-red border-red-500/30">{t('common.abnormal')}</Badge>;
|
||||
return <Badge className="bg-[var(--dg-bg-active)]/15 dg-text-secondary border-[var(--dg-border)]">{t('common.offline')}</Badge>;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -181,8 +183,8 @@ export function SNMPMonitor() {
|
||||
{/* 工具栏 */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">SNMP 监控</h2>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">5 分钟采集 · 指标保留 7 天</p>
|
||||
<h2 className="text-lg font-semibold">{t('monitor.snmp.title')}</h2>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{t('monitor.snmpmonitor.s96bd64')}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={refresh} className="h-8">
|
||||
@@ -196,28 +198,28 @@ export function SNMPMonitor() {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加 SNMP 设备</DialogTitle>
|
||||
<DialogDescription>选择厂商 OID 模板,配置管理 IP 与 Community</DialogDescription>
|
||||
<DialogTitle>{t('monitor.snmpmonitor.s29b909')}</DialogTitle>
|
||||
<DialogDescription>{t('monitor.snmpmonitor.s11ea7d')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3 py-2">
|
||||
<div className="grid gap-1.5">
|
||||
<Label>设备名称</Label>
|
||||
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="核心交换机-A" />
|
||||
<Label>{t('monitor.snmpmonitor.s9f694f')}</Label>
|
||||
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder=t('monitor.snmpmonitor.s490f68') />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label>IP 地址</Label>
|
||||
<Label>{t('agent.agentIp')}</Label>
|
||||
<Input value={form.ip} onChange={(e) => setForm({ ...form, ip: e.target.value })} placeholder="192.168.1.1" />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>端口</Label>
|
||||
<Label>{t('server.csv.port')}</Label>
|
||||
<Input type="number" value={form.port} onChange={(e) => setForm({ ...form, port: Number(e.target.value) || 161 })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>OID 模板</Label>
|
||||
<Label>{t('monitor.snmpmonitor.s48cfbb')}</Label>
|
||||
<Select value={form.template_id} onValueChange={(v) => setForm({ ...form, template_id: v })}>
|
||||
<SelectTrigger><SelectValue placeholder="选择模板" /></SelectTrigger>
|
||||
<SelectTrigger><SelectValue placeholder=t('pipeline.selectTemplate') /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.map((t) => (
|
||||
<SelectItem key={t.id} value={String(t.id)}>{t.name} ({t.vendor})</SelectItem>
|
||||
@@ -227,7 +229,7 @@ export function SNMPMonitor() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label>SNMP 版本</Label>
|
||||
<Label>{t('monitor.snmpVersion')}</Label>
|
||||
<Select value={form.snmp_version} onValueChange={(v) => setForm({ ...form, snmp_version: v as 'v2c' | 'v3' })}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -243,7 +245,7 @@ export function SNMPMonitor() {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>取消</Button>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleAdd} disabled={submitting}>
|
||||
{submitting && <Loader2 className="h-4 w-4 animate-spin mr-1" />}确认
|
||||
</Button>
|
||||
@@ -256,10 +258,10 @@ export function SNMPMonitor() {
|
||||
{/* 紧凑统计 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{[
|
||||
{ label: '设备总数', value: stats.total, color: '' },
|
||||
{ label: '在线', value: stats.online, color: 'dg-text-green' },
|
||||
{ label: '异常', value: stats.abnormal, color: 'dg-text-red' },
|
||||
{ label: 'OID 模板', value: templates.length, color: 'dg-text-cyan' },
|
||||
{ label: t('monitor.snmpmonitor.sc61566'), value: stats.total, color: '' },
|
||||
{ label: t('common.online'), value: stats.online, color: 'dg-text-green' },
|
||||
{ label: t('common.abnormal'), value: stats.abnormal, color: 'dg-text-red' },
|
||||
{ label: t('monitor.snmpmonitor.s48cfbb'), value: templates.length, color: 'dg-text-cyan' },
|
||||
].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">
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
||||
@@ -292,9 +294,9 @@ export function SNMPMonitor() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">名称</TableHead>
|
||||
<TableHead className="text-xs">{t('common.name')}</TableHead>
|
||||
<TableHead className="text-xs">IP</TableHead>
|
||||
<TableHead className="text-xs">状态</TableHead>
|
||||
<TableHead className="text-xs">{t('common.status')}</TableHead>
|
||||
<TableHead className="w-24" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -350,7 +352,7 @@ export function SNMPMonitor() {
|
||||
<DialogDescription>{metricsDevice?.ip} · {templateName(metricsDevice?.template_id || 0)}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{metricsLoading ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">加载中...</div>
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">{t('common.loading')}</div>
|
||||
) : metrics.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||
暂无指标,请先采集
|
||||
@@ -371,7 +373,7 @@ export function SNMPMonitor() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>指标</TableHead><TableHead>索引</TableHead><TableHead>值</TableHead><TableHead>OID</TableHead>
|
||||
<TableHead>{t('cloud.colMetric')}</TableHead><TableHead>{t('monitor.logIndex')}</TableHead><TableHead>{t('k8s.resource.value')}</TableHead><TableHead>OID</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { JsonData } from '@/types/common';
|
||||
import { SiteProtocolPieChart } from './site-monitor/SiteProtocolPieChart';
|
||||
import { SiteTaskDetailPage } from './site-monitor/SiteTaskDetailPage';
|
||||
import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
PROBE_TYPE_OPTIONS, type ProbeType, type SiteStats, type SiteTask,
|
||||
probeTypeMeta, probeFrequencyHint, defaultProbeFrequency, MIN_PROBE_FREQUENCY, fmtAvailResponse, fmtFrequency,
|
||||
isSslProbeTask, isSslExpiringSoon, normalizeProbeType,
|
||||
@@ -43,12 +44,13 @@ const FILTER_TYPES: Array<ProbeType | 'ALL'> = ['ALL', ...PROBE_TYPE_OPTIONS];
|
||||
|
||||
function alertStatusBadge(status?: string) {
|
||||
if (status === 'alerting') {
|
||||
return <Badge className="bg-red-500/15 dg-text-red border-red-500/30 text-[10px]">告警中</Badge>;
|
||||
return <Badge className="bg-red-500/15 dg-text-red border-red-500/30 text-[10px]">{i18n.t('monitor.sitemonitor.sb2da58')}</Badge>;
|
||||
}
|
||||
return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30 text-[10px]">正常</Badge>;
|
||||
return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30 text-[10px]">{i18n.t('common.normal')}</Badge>;
|
||||
}
|
||||
|
||||
export function SiteMonitor() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const taskIdParam = searchParams.get('task');
|
||||
const [stats, setStats] = useState<SiteStats | null>(null);
|
||||
@@ -136,7 +138,7 @@ export function SiteMonitor() {
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.name.trim() || !form.url.trim()) { toast.error('名称和地址不能为空'); return; }
|
||||
if (!form.name.trim() || !form.url.trim()) { toast.error(t('monitor.sitemonitor.s8eb6b1')); return; }
|
||||
setSaving(true);
|
||||
const payload = {
|
||||
name: form.name.trim(), url: form.url.trim(), type: form.type.toLowerCase(),
|
||||
@@ -145,34 +147,34 @@ export function SiteMonitor() {
|
||||
expected_status_code: Number(form.expected_status_code) || 200,
|
||||
};
|
||||
try {
|
||||
if (editingTaskId) { await siteApi.updateTask(editingTaskId, payload); toast.success('任务已更新'); }
|
||||
else { await siteApi.createTask(payload); toast.success('拨测任务已创建'); }
|
||||
if (editingTaskId) { await siteApi.updateTask(editingTaskId, payload); toast.success(t('monitor.sitemonitor.se01498')); }
|
||||
else { await siteApi.createTask(payload); toast.success(t('monitor.sitemonitor.s48723e')); }
|
||||
setFormOpen(false); setEditingTaskId(null);
|
||||
loadTasks(); loadStats();
|
||||
} catch (e) {
|
||||
toast.error((e as JsonData).message || (editingTaskId ? '更新失败' : '创建失败'));
|
||||
toast.error((e as JsonData).message || (editingTaskId ? t('alert.updateFailed') : t('monitor.quickSetupFailed')));
|
||||
} finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const handleDelete = async (task: SiteTask) => {
|
||||
const ok = await _confirm({ title: '删除拨测任务', description: `确定删除「${task.name}」吗?`, variant: 'destructive' });
|
||||
const ok = await _confirm({ title: t('monitor.deleteProbe'), description: t('monitor.sitemonitor.sb6c3e1', { task.name: task.name }), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try {
|
||||
await siteApi.deleteTask(task.id);
|
||||
if (taskIdParam === String(task.id)) {
|
||||
closeDetail();
|
||||
}
|
||||
toast.success('任务已删除');
|
||||
toast.success(t('monitor.sitemonitor.sb2ae04'));
|
||||
loadTasks(); loadStats();
|
||||
} catch (e) { toast.error((e as JsonData).message || '删除失败'); }
|
||||
} catch (e) { toast.error((e as JsonData).message || t('common.deleteFailed')); }
|
||||
};
|
||||
|
||||
const handleToggle = async (task: SiteTask) => {
|
||||
try {
|
||||
await siteApi.toggleTask(task.id, !task.enabled);
|
||||
toast.success(task.enabled ? '已停用' : '已启用');
|
||||
toast.success(task.enabled ? t('cloud.stopped') : t('alert.enabled'));
|
||||
loadTasks(); loadStats();
|
||||
} catch (e) { toast.error((e as JsonData).message || '操作失败'); }
|
||||
} catch (e) { toast.error((e as JsonData).message || t('common.operationFailed')); }
|
||||
};
|
||||
|
||||
const targetMeta = probeTypeMeta[form.type];
|
||||
@@ -191,8 +193,8 @@ export function SiteMonitor() {
|
||||
if (!detailTask) {
|
||||
return (
|
||||
<div className="space-y-4 text-center py-16">
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">拨测任务不存在或已删除</p>
|
||||
<Button variant="outline" size="sm" onClick={closeDetail}>返回列表</Button>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">{t('monitor.sitemonitor.s62a002')}</p>
|
||||
<Button variant="outline" size="sm" onClick={closeDetail}>{t('monitor.siteTask.backToList')}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -209,8 +211,8 @@ export function SiteMonitor() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">站点拨测</h2>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">HTTP Web 拨测 · SSL 证书检测 · TCP / DNS / PING</p>
|
||||
<h2 className="text-lg font-semibold">{t('monitor.siteProbe')}</h2>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{t('monitor.sitemonitor.s9eab2b')}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" className="h-8" onClick={() => { loadStats(); loadTasks(); }} disabled={refreshing}>
|
||||
@@ -226,11 +228,11 @@ export function SiteMonitor() {
|
||||
<div className="xl:col-span-8 space-y-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{[
|
||||
{ label: '总任务', value: stats?.total_tasks ?? '--' },
|
||||
{ label: '正常', value: stats?.up_count ?? '--', color: 'dg-text-green' },
|
||||
{ label: '异常', value: stats?.down_count ?? '--', color: 'dg-text-red' },
|
||||
{ label: '可用率(24h)', value: availStr, color: 'dg-text-blue' },
|
||||
{ label: '探测次数', value: probeCountStr },
|
||||
{ label: t('monitor.sitemonitor.s0f7991'), value: stats?.total_tasks ?? '--' },
|
||||
{ label: t('common.normal'), value: stats?.up_count ?? '--', color: 'dg-text-green' },
|
||||
{ label: t('common.abnormal'), value: stats?.down_count ?? '--', color: 'dg-text-red' },
|
||||
{ label: t('monitor.sitemonitor.s72a1a9'), value: availStr, color: 'dg-text-blue' },
|
||||
{ label: t('monitor.sitemonitor.s0fad73'), value: probeCountStr },
|
||||
].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">
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{item.label}</p>
|
||||
@@ -244,8 +246,8 @@ export function SiteMonitor() {
|
||||
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<h3 className="text-sm font-medium">拨测任务</h3>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">点击可观测进入详情页</span>
|
||||
<h3 className="text-sm font-medium">{t('monitor.sitemonitor.s30319e')}</h3>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">{t('monitor.sitemonitor.sa5e6b6')}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{FILTER_TYPES.map((type) => {
|
||||
@@ -262,7 +264,7 @@ export function SiteMonitor() {
|
||||
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] border border-transparent hover:text-[var(--dg-text-primary)]'
|
||||
}`}
|
||||
>
|
||||
{type === 'ALL' ? '全部' : type} {count > 0 && count}
|
||||
{type === 'ALL' ? t('common.all') : type} {count > 0 && count}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -271,19 +273,19 @@ export function SiteMonitor() {
|
||||
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div className="text-center py-10 text-sm text-[var(--dg-text-muted)]">
|
||||
{tasks.length === 0 ? '暂无拨测任务' : '当前筛选无任务'}
|
||||
{tasks.length === 0 ? t('monitor.sitemonitor.sdd8ebb') : t('monitor.sitemonitor.s0f961f')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-xs">名称</TableHead>
|
||||
<TableHead className="text-xs">类型</TableHead>
|
||||
<TableHead className="text-xs">频率</TableHead>
|
||||
<TableHead className="text-xs">报警状态</TableHead>
|
||||
<TableHead className="text-xs">可用率/响应时间</TableHead>
|
||||
<TableHead className="text-xs">启用</TableHead>
|
||||
<TableHead className="text-xs">{t('common.name')}</TableHead>
|
||||
<TableHead className="text-xs">{t('common.type')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.siteTask.frequency')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.sitemonitor.sdb4470')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.sitemonitor.s4fe553')}</TableHead>
|
||||
<TableHead className="text-xs">{t('common.enable')}</TableHead>
|
||||
<TableHead className="text-xs w-24" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -342,16 +344,16 @@ export function SiteMonitor() {
|
||||
<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)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingTaskId ? '编辑拨测任务' : '添加拨测任务'}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">按协议类型配置探活任务,系统将按间隔自动拨测</DialogDescription>
|
||||
<DialogTitle>{editingTaskId ? t('monitor.editProbe') : t('monitor.addProbe')}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">{t('monitor.sitemonitor.s6173cf')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-2">
|
||||
<div className="grid gap-1.5">
|
||||
<Label>任务名称</Label>
|
||||
<Input placeholder="例如:官网首页" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
<Label>{t('monitor.probeName')}</Label>
|
||||
<Input placeholder=t('monitor.sitemonitor.s024a6a') value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>协议类型</Label>
|
||||
<Label>{t('monitor.sitemonitor.se825ec')}</Label>
|
||||
<Select value={form.type} onValueChange={(v) => {
|
||||
const t = v as ProbeType;
|
||||
setForm({ ...form, type: t, frequency_sec: editingTaskId ? form.frequency_sec : defaultProbeFrequency(t) });
|
||||
@@ -369,30 +371,30 @@ export function SiteMonitor() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label>检测间隔 (秒)</Label>
|
||||
<Label>{t('monitor.sitemonitor.s4966e6')}</Label>
|
||||
<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) })} />
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{probeFrequencyHint(form.type)}</p>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label>超时 (秒)</Label>
|
||||
<Label>{t('monitor.sitemonitor.s52e9bc')}</Label>
|
||||
<Input type="number" min={1} max={300} value={form.timeout_sec}
|
||||
onChange={(e) => setForm({ ...form, timeout_sec: Number(e.target.value) || 10 })} />
|
||||
</div>
|
||||
</div>
|
||||
{form.type === 'HTTP' && (
|
||||
<div className="grid gap-1.5">
|
||||
<Label>期望状态码</Label>
|
||||
<Label>{t('monitor.sitemonitor.s88cbfb')}</Label>
|
||||
<Input type="number" min={100} max={599} value={form.expected_status_code}
|
||||
onChange={(e) => setForm({ ...form, expected_status_code: Number(e.target.value) || 200 })} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => { setFormOpen(false); setEditingTaskId(null); }} disabled={saving}>取消</Button>
|
||||
<Button variant="outline" onClick={() => { setFormOpen(false); setEditingTaskId(null); }} disabled={saving}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
{saving && <Loader2 className="h-4 w-4 animate-spin mr-1" />}
|
||||
{editingTaskId ? '保存' : '创建任务'}
|
||||
{editingTaskId ? i18n.t('common.save') : i18n.t('monitor.sitemonitor.s6ef95f')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ZoomableAreaChart, type ZoomableChartHandle } from './ZoomableAreaChart
|
||||
import { ChartToolbar } from './ChartToolbar';
|
||||
import { MetricIconBox } from './MetricIconBox';
|
||||
import type { AggType, BucketPoint } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DualLineChartProps {
|
||||
title: string;
|
||||
@@ -20,6 +21,7 @@ interface DualLineChartProps {
|
||||
export function DualLineChart({
|
||||
title, currentRx, currentTx, rxData, txData, aggType, unit,
|
||||
}: DualLineChartProps) {
|
||||
const { t } = useTranslation();
|
||||
const chartRef = useRef<ZoomableChartHandle>(null);
|
||||
const [zoomed, setZoomed] = useState(false);
|
||||
|
||||
@@ -60,14 +62,14 @@ export function DualLineChart({
|
||||
ref={chartRef}
|
||||
data={chartData}
|
||||
series={[
|
||||
{ dataKey: 'rx', color: '#3b82f6', label: '接收' },
|
||||
{ dataKey: 'tx', color: '#10b981', label: '发送' },
|
||||
{ dataKey: 'rx', color: '#3b82f6', label: t('common.receive') },
|
||||
{ dataKey: 'tx', color: '#10b981', label: t('common.send') },
|
||||
]}
|
||||
yDomain={[0, 'auto']}
|
||||
unit={unit}
|
||||
chartId={`dual-${title.replace(/\s/g, '')}`}
|
||||
title={title}
|
||||
subtitle={`接收 ${currentRx} · 发送 ${currentTx}`}
|
||||
subtitle={t('monitor.duallinechart.sbc1afb', { currentRx: currentRx, currentTx: currentTx })}
|
||||
height={208}
|
||||
toolbarPlacement="header"
|
||||
onZoomedChange={setZoomed}
|
||||
|
||||
@@ -18,12 +18,14 @@ import {
|
||||
import { DEFAULT_PROBE_FORM, type ProbeForm } from './types';
|
||||
import { useConfirm } from '@/components/ui/confirm-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface HealthProbeSectionProps {
|
||||
selectedHost: string;
|
||||
}
|
||||
|
||||
export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const qc = useQueryClient();
|
||||
const { data: probes = [], isFetching, refetch } = useHealthProbes(selectedHost);
|
||||
@@ -68,15 +70,15 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
setProbeDialogOpen(false);
|
||||
qc.invalidateQueries({ queryKey: queryKeys.hostMonitor.probes(selectedHost) });
|
||||
} catch {
|
||||
toast.error('保存失败');
|
||||
toast.error(t('common.saveFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteProbe = async (p: Record<string, unknown>) => {
|
||||
const ok = await confirm({
|
||||
title: '删除探针',
|
||||
description: `确定删除探针「${p.name}」?`,
|
||||
confirmText: '删除',
|
||||
title: t('monitor.healthprobesection.s9da33b'),
|
||||
description: t('monitor.healthprobesection.s47e81a', { p.name: p.name }),
|
||||
confirmText: t('common.delete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
@@ -88,7 +90,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
<>
|
||||
<TabsContent value="health_probes" className="mt-3 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">探针列表(Agent 在目标主机本地执行探测)</h3>
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">{t('monitor.healthprobesection.s518614')}</h3>
|
||||
<div className="flex gap-2">
|
||||
<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')} />刷新
|
||||
@@ -101,7 +103,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
{probes.length === 0 ? (
|
||||
<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)]">
|
||||
暂无健康探针,点击"添加探针"创建
|
||||
暂无健康探针,点击t('monitor.healthprobesection.s5bf2e8')创建
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -139,8 +141,8 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
<div className="truncate font-mono">{String(p.exec_command)}</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<span>状态: <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() : '未检测'}
|
||||
<span>{t('monitor.healthprobesection.s13b5c7')} <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() : t('monitor.healthprobesection.s5c6585')}
|
||||
</span></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>}
|
||||
@@ -158,23 +160,23 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">
|
||||
{editingProbe ? '编辑探针' : '添加探针'}
|
||||
{editingProbe ? t('monitor.healthprobesection.sc7c452') : t('monitor.healthprobesection.s5bf2e8')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">
|
||||
{editingProbe ? '修改健康探针配置' : '创建新的健康探针,Agent 在目标主机本地执行探测'}
|
||||
{editingProbe ? t('monitor.healthprobesection.se98536') : t('monitor.healthprobesection.s6f43d0')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针名称 <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="如:cex-agent 健康检查" />
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.healthprobesection.s1fdfad')} <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=t('monitor.healthprobesection.s2b5767') />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">目标主机</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 })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针类型</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 })}>
|
||||
<option value="http_get">HTTP GET</option>
|
||||
<option value="tcp_socket">TCP Socket</option>
|
||||
@@ -187,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>
|
||||
<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><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">期望状态码</label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.healthprobesection.s88cbfb')}</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>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">期望响应内容(可选)</label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.healthprobesection.s27f2c4')}</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>
|
||||
</div>
|
||||
</>
|
||||
@@ -197,32 +199,32 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
|
||||
{probeForm.probe_type === 'tcp_socket' && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">TCP 目标地址</label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.healthprobesection.s4ff28f')}</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.tcp_host} onChange={e => setProbeForm({ ...probeForm, tcp_host: e.target.value })} placeholder="localhost" /></div>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">TCP 端口</label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.healthprobesection.s81e863')}</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.tcp_port} onChange={e => setProbeForm({ ...probeForm, tcp_port: Number(e.target.value) })} /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{probeForm.probe_type === 'exec_command' && (
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">执行命令 <span className="dg-text-red">*</span></label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('kubernetes.pod.exec')} <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>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">超时(秒)</label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.probeTimeout')}</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>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">间隔(秒)</label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('monitor.healthprobesection.sc7f835')}</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>
|
||||
</div>
|
||||
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">描述</label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">{t('common.description')}</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>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setProbeDialogOpen(false)}>取消</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => setProbeDialogOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={handleSaveProbe} disabled={!probeForm.name || !probeForm.hostname}>
|
||||
{editingProbe ? '保存' : '创建'}
|
||||
{editingProbe ? i18n.t('common.save') : i18n.t('common.create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type TimeRangePresetId,
|
||||
} from './types';
|
||||
import { presetToRange, toDatetimeLocalValue, formatRangeSummary } from './timeRange';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
value: HostMonitorTimeRange;
|
||||
@@ -16,6 +17,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function HostTimeRangePicker({ value, onChange }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [customOpen, setCustomOpen] = useState(false);
|
||||
const now = new Date();
|
||||
const [customStart, setCustomStart] = useState(toDatetimeLocalValue(new Date(now.getTime() - 3600_000)));
|
||||
@@ -44,7 +46,7 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
|
||||
const e = start.getTime() <= end.getTime() ? end : start;
|
||||
onChange({
|
||||
preset: 'custom',
|
||||
label: '自定义',
|
||||
label: t('common.custom'),
|
||||
start: s.toISOString(),
|
||||
end: e.toISOString(),
|
||||
buckets: Math.min(120, Math.max(36, Math.ceil((e.getTime() - s.getTime()) / 60_000 / 2))),
|
||||
@@ -85,10 +87,10 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
|
||||
</Button>
|
||||
{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">
|
||||
<p className="text-xs font-medium text-[var(--dg-text-primary)] mb-2">自定义时间范围</p>
|
||||
<p className="text-xs font-medium text-[var(--dg-text-primary)] mb-2">{t('monitor.hosttimerangepicker.s0122e0')}</p>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="text-[10px] dg-text-muted">开始</label>
|
||||
<label className="text-[10px] dg-text-muted">{t('common.start')}</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={customStart}
|
||||
@@ -97,7 +99,7 @@ export function HostTimeRangePicker({ value, onChange }: Props) {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] dg-text-muted">结束</label>
|
||||
<label className="text-[10px] dg-text-muted">{t('monitor.hosttimerangepicker.s12f1d7')}</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={customEnd}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { parsePortList } from '@/utils/validate';
|
||||
import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
@@ -26,16 +27,17 @@ export function PortPolicyDialog({
|
||||
onSave,
|
||||
saving,
|
||||
}: PortPolicyDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">端口监控策略</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">配置 Agent 端口上报过滤规则</DialogDescription>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">{t('monitor.portpolicydialog.s868e3d')}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]">{t('monitor.portpolicydialog.scd27ad')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">过滤模式</label>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">{t('monitor.portpolicydialog.s7824bd')}</label>
|
||||
<div className="flex gap-2">
|
||||
<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)]'}`}
|
||||
@@ -52,15 +54,15 @@ export function PortPolicyDialog({
|
||||
</div>
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1.5 leading-relaxed">
|
||||
{portPolicyMode === 'whitelist'
|
||||
? '所有发现的端口都上报,任意端口变化都会触发告警 + 恢复通知。'
|
||||
: '仅上报选中端口的变化,其余端口变化静默忽略(不告警)。系统进程端口(sshd/rpcbind/systemd等)永远忽略。'}
|
||||
? t('monitor.portpolicydialog.sf5cd9d')
|
||||
: t('monitor.portpolicydialog.sf3bf33')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
|
||||
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
|
||||
{portPolicyMode === 'whitelist' && <span className="dg-text-blue ml-1">— 白名单模式无需配置</span>}
|
||||
端口列表 <span className="text-[var(--dg-text-muted)]">{t('monitor.portpolicydialog.sb18534')}</span>
|
||||
{portPolicyMode === 'whitelist' && <span className="dg-text-blue ml-1">{t('monitor.portpolicydialog.sa80437')}</span>}
|
||||
</label>
|
||||
<textarea
|
||||
className={`w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono resize-none ${
|
||||
@@ -72,7 +74,7 @@ export function PortPolicyDialog({
|
||||
const val = e.target.value;
|
||||
onWhitelistChange(parsePortList(val));
|
||||
}}
|
||||
placeholder={portPolicyMode === 'whitelist' ? '白名单模式下所有端口自动上报' : '80,443,3306,6379'}
|
||||
placeholder={portPolicyMode === 'whitelist' ? t('monitor.portpolicydialog.s51b845') : '80,443,3306,6379'}
|
||||
readOnly={portPolicyMode === 'whitelist'}
|
||||
/>
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)] mt-1">
|
||||
@@ -82,14 +84,14 @@ export function PortPolicyDialog({
|
||||
|
||||
<div className="bg-amber-500/5 border border-amber-500/20 rounded p-2.5">
|
||||
<p className="text-[10px] text-amber-300/80 leading-relaxed">
|
||||
⚠️ 无论选择哪种模式,以下系统进程端口<b>永远不上报</b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
|
||||
⚠️ 无论选择哪种模式,以下系统进程端口<b>{t('monitor.portpolicydialog.s2ff12d')}</b>:sshd、rpcbind、master、systemd、dbus-daemon、rsyslogd 等。Agent 采集时自动跳过。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => onOpenChange(false)}>取消</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-[11px]" onClick={() => onOpenChange(false)}>{t('common.cancel')}</Button>
|
||||
<Button size="sm" className="h-7 text-[11px] bg-blue-600 hover:bg-blue-500" onClick={onSave} disabled={saving}>
|
||||
{saving ? '保存中…' : '保存策略'}
|
||||
{saving ? i18n.t('monitor.portpolicydialog.s9b5d32') : i18n.t('monitor.portpolicydialog.s2d3f52')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { ChartToolbar } from './ChartToolbar';
|
||||
import { MonitorChartTooltip } from '../shared/monitorChartTheme';
|
||||
import { ChartRangeHint } from '../shared/ChartRangeHint';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface ChartSeries {
|
||||
dataKey: string;
|
||||
@@ -88,6 +89,7 @@ function ChartBody({
|
||||
compact?: boolean;
|
||||
referenceLines?: ChartReferenceLine[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div style={{ height }} className="select-none w-full min-h-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
@@ -191,6 +193,7 @@ export const ZoomableAreaChart = forwardRef<ZoomableChartHandle, Props>(function
|
||||
onZoomedChange,
|
||||
referenceLines,
|
||||
}, ref) {
|
||||
const { t } = useTranslation();
|
||||
const uid = useId().replace(/:/g, '');
|
||||
const gradPrefix = `grad-${chartId}-${uid}`;
|
||||
|
||||
@@ -314,11 +317,11 @@ export const ZoomableAreaChart = forwardRef<ZoomableChartHandle, Props>(function
|
||||
<DialogContent className={dialogClass}>
|
||||
<DialogHeader className="shrink-0 flex-row items-start justify-between gap-4 space-y-0 pr-10">
|
||||
<div className="min-w-0">
|
||||
<DialogTitle className="text-base truncate">{title || '指标详情'}</DialogTitle>
|
||||
<DialogTitle className="text-base truncate">{title || t('monitor.zoomableareachart.sdaf533')}</DialogTitle>
|
||||
{subtitle && (
|
||||
<p className="text-xs dg-text-muted mt-0.5 truncate">{subtitle}</p>
|
||||
)}
|
||||
<DialogDescription className="sr-only">放大查看监控指标趋势</DialogDescription>
|
||||
<DialogDescription className="sr-only">{t('monitor.zoomableareachart.s365f8f')}</DialogDescription>
|
||||
</div>
|
||||
<ChartToolbar
|
||||
size="md"
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import i18n from '@/i18n';
|
||||
// 主机监控类型定义 // Logan.Li
|
||||
|
||||
/** 预设时间范围(分钟粒度,支持 15/30 分钟) // Logan.Li */
|
||||
export const TIME_RANGE_PRESETS = [
|
||||
{ id: '15m', label: '15分钟', minutes: 15, buckets: 30 },
|
||||
{ id: '30m', label: '30分钟', minutes: 30, buckets: 36 },
|
||||
{ id: '1h', label: '1小时', minutes: 60, buckets: 60 },
|
||||
{ id: '6h', label: '6小时', minutes: 360, buckets: 72 },
|
||||
{ id: '12h', label: '12小时', minutes: 720, buckets: 72 },
|
||||
{ id: '1d', label: '1天', minutes: 1440, buckets: 96 },
|
||||
{ id: '7d', label: '7天', minutes: 10080, buckets: 84 },
|
||||
{ id: '15m', label: i18n.t('monitor.types.sf511e9'), minutes: 15, buckets: 30 },
|
||||
{ id: '30m', label: i18n.t('monitor.types.s751a79'), minutes: 30, buckets: 36 },
|
||||
{ id: '1h', label: i18n.t('monitor.types.sc658fd'), minutes: 60, buckets: 60 },
|
||||
{ id: '6h', label: i18n.t('monitor.types.s0485f2'), minutes: 360, buckets: 72 },
|
||||
{ id: '12h', label: i18n.t('monitor.types.sd04708'), minutes: 720, buckets: 72 },
|
||||
{ id: '1d', label: i18n.t('monitor.types.s7db888'), minutes: 1440, buckets: 96 },
|
||||
{ id: '7d', label: i18n.t('monitor.siteProtocol.ssl.ref7days'), minutes: 10080, buckets: 84 },
|
||||
] as const;
|
||||
|
||||
export type TimeRangePresetId = (typeof TIME_RANGE_PRESETS)[number]['id'] | 'custom';
|
||||
@@ -26,7 +27,7 @@ export interface HostMonitorTimeRange {
|
||||
|
||||
export const DEFAULT_TIME_RANGE: HostMonitorTimeRange = {
|
||||
preset: '1h',
|
||||
label: '1小时',
|
||||
label: i18n.t('monitor.types.sc658fd'),
|
||||
minutes: 60,
|
||||
buckets: 60,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 主机监控工具函数 // Logan.Li
|
||||
import type { BucketPoint, HostMonitorTimeRange } from './types';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import i18n from '@/i18n';
|
||||
/** 按时间跨度格式化 X 轴标签(避免 7 天范围只显示时分) // Logan.Li */
|
||||
export function formatAxisTime(ts: number, spanMinutes: number): string {
|
||||
const d = new Date(ts * 1000);
|
||||
@@ -175,7 +176,7 @@ export function getActualDataSpan(history: JsonData[], _range: HostMonitorTimeRa
|
||||
const min = Math.min(...ts);
|
||||
const max = Math.max(...ts);
|
||||
const spanMin = Math.max(1, Math.round((max - min) / 60));
|
||||
return `${formatAxisTime(min, spanMin)} — ${formatAxisTime(max, spanMin)} · ${history.length} 点`;
|
||||
return i18n.t('monitor.utils.s3e70c7', { formatAxisTime(min, spanMin): formatAxisTime(min, spanMin), formatAxisTime(max, spanMin): formatAxisTime(max, spanMin), history.length: history.length });
|
||||
}
|
||||
|
||||
export function getCurrentVal(latest: JsonData, netInfo: JsonData, diskIOInfo: JsonData, swapInfo: JsonData, fdInfo: JsonData,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Loader2 } from 'lucide-react';
|
||||
import { middlewareMonitorApi } from '@/api';
|
||||
import { toast } from 'sonner';
|
||||
import type { MwTarget } from './helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -19,6 +20,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
name: '', type: 'redis' as 'redis' | 'mysql', ip: '', port: 6379,
|
||||
@@ -45,7 +47,7 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name.trim() || !form.ip.trim()) {
|
||||
toast.error('请填写名称和 IP');
|
||||
toast.error(t('monitor.middlewaredialog.s0fa236'));
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
@@ -62,15 +64,15 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
||||
}
|
||||
if (editTarget) {
|
||||
await middlewareMonitorApi.update(editTarget.id, payload);
|
||||
toast.success('更新成功');
|
||||
toast.success(t('monitor.middlewaredialog.s55aa63'));
|
||||
} else {
|
||||
await middlewareMonitorApi.create(payload);
|
||||
toast.success('创建成功');
|
||||
toast.success(t('monitor.dashboard.custom.toastCreateSuccess'));
|
||||
}
|
||||
onOpenChange(false);
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
toast.error(editTarget ? '更新失败' : '创建失败', { description: (err as { message?: string }).message });
|
||||
toast.error(editTarget ? t('alert.updateFailed') : t('monitor.quickSetupFailed'), { description: (err as { message?: string }).message });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -80,12 +82,12 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent showOverlay={false} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editTarget ? '编辑中间件' : '添加中间件'}</DialogTitle>
|
||||
<DialogDescription>配置 Redis(单机/集群)或 MySQL 连接,平台自动采集指标</DialogDescription>
|
||||
<DialogTitle>{editTarget ? t('monitor.middlewaredialog.sc38dde') : t('monitor.middlewaredialog.sa24241')}</DialogTitle>
|
||||
<DialogDescription>{t('monitor.middlewaredialog.s6ee717')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
|
||||
<div className="space-y-2">
|
||||
<Label>类型</Label>
|
||||
<Label>{t('common.type')}</Label>
|
||||
<div className="flex gap-2">
|
||||
{(['redis', 'mysql'] as const).map((t) => (
|
||||
<button key={t} type="button" onClick={() => setForm((f) => ({ ...f, type: t, port: t === 'redis' ? 6379 : 3306 }))}
|
||||
@@ -97,9 +99,9 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
||||
</div>
|
||||
{form.type === 'redis' && (
|
||||
<div className="space-y-2">
|
||||
<Label>部署模式</Label>
|
||||
<Label>{t('deploy.form.deployMode')}</Label>
|
||||
<div className="flex gap-2">
|
||||
{[{ value: 'standalone' as const, label: '单机' }, { value: 'cluster' as const, label: '集群' }].map((opt) => (
|
||||
{[{ value: 'standalone' as const, label: t('monitor.middlewaredialog.s047499') }, { value: 'cluster' as const, label: t('monitor.middlewaredialog.s85fe50') }].map((opt) => (
|
||||
<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)]'}`}>
|
||||
{opt.label}
|
||||
@@ -109,7 +111,7 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label>名称 *</Label>
|
||||
<Label>{t('deploy.form.nameStar')}</Label>
|
||||
<Input value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
@@ -118,32 +120,32 @@ export function MiddlewareDialog({ open, onOpenChange, editTarget, onSuccess }:
|
||||
<Input value={form.ip} onChange={(e) => setForm((f) => ({ ...f, ip: e.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>端口</Label>
|
||||
<Label>{t('server.csv.port')}</Label>
|
||||
<Input type="number" value={form.port} onChange={(e) => setForm((f) => ({ ...f, port: Number(e.target.value) }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>密码 {editTarget && <span className="text-xs text-muted-foreground">(留空不修改)</span>}</Label>
|
||||
<Label>密码 {editTarget && <span className="text-xs text-muted-foreground">{t('monitor.middlewaredialog.s7289ea')}</span>}</Label>
|
||||
<Input type="password" value={form.password} onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} />
|
||||
</div>
|
||||
{form.type === 'mysql' && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-2">
|
||||
<Label>用户名</Label>
|
||||
<Label>{t('login.username')}</Label>
|
||||
<Input value={form.username} onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>数据库</Label>
|
||||
<Label>{t('monitor.middlewaredialog.s68051b')}</Label>
|
||||
<Input value={form.database} onChange={(e) => setForm((f) => ({ ...f, database: e.target.value }))} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSubmit} disabled={saving}>
|
||||
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
{editTarget ? '保存' : '创建'}
|
||||
{editTarget ? i18n.t('common.save') : i18n.t('common.create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { MwTarget } from './helpers';
|
||||
import { fmtHitRate, fmtVal } from './helpers';
|
||||
import { ChartExpandShell } from '../shared/ChartExpandShell';
|
||||
import { MonitorChartTooltip, MONITOR_PIE_STROKE, MonitorPieTooltip } from '../shared/monitorChartTheme';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
targets: MwTarget[];
|
||||
@@ -20,12 +21,13 @@ function shortName(name: string) {
|
||||
}
|
||||
|
||||
export function MwOverviewCharts({ targets }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const statusData = useMemo(() => {
|
||||
const online = targets.filter((t) => t.status === 'online').length;
|
||||
const offline = targets.length - online;
|
||||
return [
|
||||
{ name: '在线', value: online, fill: 'var(--dg-green)' },
|
||||
{ name: '离线', value: offline, fill: 'var(--dg-red)' },
|
||||
{ name: t('common.online'), value: online, fill: 'var(--dg-green)' },
|
||||
{ name: t('common.offline'), value: offline, fill: 'var(--dg-red)' },
|
||||
].filter((d) => d.value > 0);
|
||||
}, [targets]);
|
||||
|
||||
@@ -52,7 +54,7 @@ export function MwOverviewCharts({ targets }: Props) {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3">
|
||||
<OverviewChartCard title="实例状态" icon={<PieChartIcon className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={statusData.length === 0}>
|
||||
<OverviewChartCard title=t('monitor.mwoverviewcharts.s2eeb27') icon={<PieChartIcon className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={statusData.length === 0}>
|
||||
{(h) => (
|
||||
<ResponsiveContainer width="100%" height={h}>
|
||||
<PieChart>
|
||||
@@ -66,13 +68,13 @@ export function MwOverviewCharts({ targets }: Props) {
|
||||
)}
|
||||
</OverviewChartCard>
|
||||
|
||||
<OverviewChartCard title="Redis 命中率" icon={<Percent className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={redisHitData.length === 0}>
|
||||
<OverviewChartCard title=t('monitor.mwoverviewcharts.s29d98d') icon={<Percent className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={redisHitData.length === 0}>
|
||||
{(h) => (
|
||||
<ResponsiveContainer width="100%" height={h}>
|
||||
<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}%`} />
|
||||
<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={() => '命中率'} />} />
|
||||
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtHitRate(v / 100)} nameFormatter={() => t('monitor.mwoverviewcharts.s678735')} />} />
|
||||
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
||||
{redisHitData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
||||
</Bar>
|
||||
@@ -96,13 +98,13 @@ export function MwOverviewCharts({ targets }: Props) {
|
||||
)}
|
||||
</OverviewChartCard>
|
||||
|
||||
<OverviewChartCard title="MySQL 连接" icon={<BarChart3 className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={mysqlConnData.length === 0}>
|
||||
<OverviewChartCard title=t('monitor.mwoverviewcharts.s07d3e0') icon={<BarChart3 className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />} empty={mysqlConnData.length === 0}>
|
||||
{(h) => (
|
||||
<ResponsiveContainer width="100%" height={h}>
|
||||
<BarChart data={mysqlConnData} layout="vertical" margin={{ left: 0, right: 8 }}>
|
||||
<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} />
|
||||
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtVal(v)} nameFormatter={() => '连接'} />} />
|
||||
<Tooltip content={<MonitorChartTooltip valueFormatter={(v) => fmtVal(v)} nameFormatter={() => t('monitor.mwoverviewcharts.s30f7dd')} />} />
|
||||
<Bar dataKey="value" radius={[0, 3, 3, 0]} maxBarSize={14} isAnimationActive={false}>
|
||||
{mysqlConnData.map((e) => <Cell key={e.fullName} fill={e.fill} />)}
|
||||
</Bar>
|
||||
@@ -122,6 +124,7 @@ function OverviewChartCard({
|
||||
empty?: boolean;
|
||||
children: (height: number) => ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-3">
|
||||
|
||||
@@ -18,6 +18,7 @@ import { MwTrendChart } from './MwTrendChart';
|
||||
import { MwGaugeBar } from './MwGaugeBar';
|
||||
import { MwHealthStrip } from './MwHealthStrip';
|
||||
import { ChartExpandShell } from '../shared/ChartExpandShell';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
target: MwTarget | null;
|
||||
@@ -26,6 +27,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [timeRange, setTimeRange] = useState<MwTimeRange>('6h');
|
||||
const [historyMap, setHistoryMap] = useState<Record<string, Array<{ timestamp: number; value: number }>>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -91,15 +93,15 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => setShowAll((v) => !v)}>
|
||||
{showAll ? '核心指标' : '全部指标'}
|
||||
{showAll ? t('monitor.mwtargetchartspanel.s9e7562') : t('monitor.mwtargetchartspanel.s9dea79')}
|
||||
</Button>
|
||||
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as MwTimeRange)}>
|
||||
<SelectTrigger className="h-7 w-[92px] text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1h">近 1 小时</SelectItem>
|
||||
<SelectItem value="6h">近 6 小时</SelectItem>
|
||||
<SelectItem value="24h">近 24 小时</SelectItem>
|
||||
<SelectItem value="7d">近 7 天</SelectItem>
|
||||
<SelectItem value="1h">{t('monitor.mwtargetchartspanel.sf972b7')}</SelectItem>
|
||||
<SelectItem value="6h">{t('monitor.siteProtocol.rangeDefault')}</SelectItem>
|
||||
<SelectItem value="24h">{t('monitor.mwtargetchartspanel.s6e99b7')}</SelectItem>
|
||||
<SelectItem value="7d">{t('monitor.mwtargetchartspanel.sd765ef')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
|
||||
@@ -115,47 +117,47 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||
{accent === 'redis' && (
|
||||
<>
|
||||
<MwGaugeBar
|
||||
label="命中率"
|
||||
label=t('monitor.mwtargetchartspanel.s678735')
|
||||
value={(m.redis_hit_rate || 0) * 100}
|
||||
display={fmtHitRate(m.redis_hit_rate)}
|
||||
level={m.redis_hit_rate > 0 ? gaugeToneLow((m.redis_hit_rate || 0) * 100, 90, 80) : 'ok'}
|
||||
/>
|
||||
<MwGaugeBar
|
||||
label="内存占用"
|
||||
label=t('monitor.mwtargetchartspanel.s44cdb3')
|
||||
value={memPct ?? (m.redis_used_memory > 0 ? 50 : 0)}
|
||||
display={fmtMemory(m.redis_used_memory)}
|
||||
level={memPct != null ? gaugeTone(memPct, 80, 90) : 'ok'}
|
||||
hint={memPct != null ? `已用 ${memPct.toFixed(0)}%` : '未配置 maxmemory'}
|
||||
hint={memPct != null ? t('monitor.mwtargetchartspanel.s59c6cf', { memPct.toFixed(0): memPct.toFixed(0) }) : t('monitor.mwtargetchartspanel.s1c1ce8')}
|
||||
/>
|
||||
<MwGaugeBar
|
||||
label="连接数"
|
||||
label=t('k8s.topology.connections')
|
||||
value={Math.min(100, (m.redis_connected_clients || 0) / 2)}
|
||||
display={fmtVal(m.redis_connected_clients)}
|
||||
level={(m.redis_blocked_clients || 0) > 0 ? 'warn' : 'ok'}
|
||||
hint={m.redis_cluster_known_nodes ? `集群 ${fmtVal(m.redis_cluster_known_nodes)} 节点` : undefined}
|
||||
hint={m.redis_cluster_known_nodes ? t('monitor.mwtargetchartspanel.sa8ec79', { fmtVal(m.redis_cluster_known_nodes): fmtVal(m.redis_cluster_known_nodes) }) : undefined}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{accent === 'mysql' && (
|
||||
<>
|
||||
<MwGaugeBar
|
||||
label="缓冲池命中率"
|
||||
label=t('monitor.mwtargetchartspanel.sa2d02e')
|
||||
value={(m.mysql_buffer_hit_rate || 0) * 100}
|
||||
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'}
|
||||
/>
|
||||
<MwGaugeBar
|
||||
label="连接占用"
|
||||
label=t('monitor.mwtargetchartspanel.se84e4b')
|
||||
value={connPct ?? 0}
|
||||
display={`${fmtVal(m.mysql_connections)} / ${fmtVal(m.mysql_max_connections)}`}
|
||||
level={connPct != null ? gaugeTone(connPct, 80, 90) : 'ok'}
|
||||
/>
|
||||
<MwGaugeBar
|
||||
label="活跃线程"
|
||||
label=t('monitor.mwtargetchartspanel.se0f6ab')
|
||||
value={Math.min(100, (m.mysql_threads_running || 0) * 5)}
|
||||
display={fmtVal(m.mysql_threads_running)}
|
||||
level={(m.mysql_threads_running || 0) > 20 ? 'warn' : 'ok'}
|
||||
hint={`慢查询 ${fmtVal(m.mysql_slow_queries)}`}
|
||||
hint={t('monitor.mwtargetchartspanel.s70da18', { fmtVal(m.mysql_slow_queries): fmtVal(m.mysql_slow_queries) })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -166,16 +168,16 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||
{accent === 'redis' ? (
|
||||
<>
|
||||
<SummaryTile label="QPS" value={fmtVal(m.redis_qps, 1)} />
|
||||
<SummaryTile label="Key 数" value={fmtVal(m.redis_db_size)} />
|
||||
<SummaryTile label="逐出 Key" value={fmtVal(m.redis_evicted_keys)} warn={(m.redis_evicted_keys || 0) > 0} />
|
||||
<SummaryTile label="碎片率" value={m.redis_mem_fragmentation_ratio ? fmtVal(m.redis_mem_fragmentation_ratio, 2) : '--'} />
|
||||
<SummaryTile label=t('monitor.mwtargetchartspanel.s196eef') value={fmtVal(m.redis_db_size)} />
|
||||
<SummaryTile label=t('monitor.mwtargetchartspanel.s0a5e7a') value={fmtVal(m.redis_evicted_keys)} warn={(m.redis_evicted_keys || 0) > 0} />
|
||||
<SummaryTile label=t('monitor.mwtargetchartspanel.s3bc8dd') value={m.redis_mem_fragmentation_ratio ? fmtVal(m.redis_mem_fragmentation_ratio, 2) : '--'} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SummaryTile label="QPS" value={fmtVal(m.mysql_qps, 1)} />
|
||||
<SummaryTile label="接收流量" value={fmtMemory(m.mysql_bytes_received)} />
|
||||
<SummaryTile label="发送流量" value={fmtMemory(m.mysql_bytes_sent)} />
|
||||
<SummaryTile label="运行时长" value={m.mysql_uptime ? `${Math.floor(m.mysql_uptime / 3600)}h` : '--'} />
|
||||
<SummaryTile label=t('monitor.mwtargetchartspanel.s3e54c8') value={fmtMemory(m.mysql_bytes_received)} />
|
||||
<SummaryTile label=t('monitor.mwtargetchartspanel.s90d6af') value={fmtMemory(m.mysql_bytes_sent)} />
|
||||
<SummaryTile label=t('agent.agentUptime') value={m.mysql_uptime ? `${Math.floor(m.mysql_uptime / 3600)}h` : '--'} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -219,6 +221,7 @@ export function MwTargetChartsPanel({ target, chartDefs, accent }: Props) {
|
||||
}
|
||||
|
||||
function SummaryTile({ label, value, warn }: { label: string; value: string; warn?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={`rounded-lg p-3 text-center ${warn ? 'bg-amber-500/10' : 'bg-[var(--dg-bg-tertiary)]'}`}>
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)]">{label}</p>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18n from '@/i18n';
|
||||
// 中间件监控可视化辅助 // Logan.Li
|
||||
|
||||
export interface MwConfigPublic {
|
||||
@@ -58,20 +59,20 @@ export const MYSQL_PRIMARY_KEYS = ['mysql_qps', 'mysql_connections', 'mysql_buff
|
||||
|
||||
export const REDIS_CHARTS: MwChartDef[] = [
|
||||
{ key: 'redis_qps', label: 'QPS', color: 'var(--dg-blue)', unit: 'ops/s' },
|
||||
{ key: 'redis_used_memory', label: '内存占用', color: 'var(--dg-red)', formatter: fmtMemory },
|
||||
{ key: 'redis_hit_rate', label: '命中率', color: 'var(--dg-green)', percent: true },
|
||||
{ key: 'redis_connected_clients', label: '连接数', color: 'var(--dg-cyan)' },
|
||||
{ key: 'redis_db_size', label: 'Key 总数', color: 'var(--dg-purple)' },
|
||||
{ key: 'redis_blocked_clients', label: '阻塞客户端', color: 'var(--dg-orange)' },
|
||||
{ key: 'redis_used_memory', label: i18n.t('monitor.helpers.s44cdb3'), color: 'var(--dg-red)', formatter: fmtMemory },
|
||||
{ key: 'redis_hit_rate', label: i18n.t('monitor.helpers.s678735'), color: 'var(--dg-green)', percent: true },
|
||||
{ key: 'redis_connected_clients', label: i18n.t('k8s.topology.connections'), color: 'var(--dg-cyan)' },
|
||||
{ key: 'redis_db_size', label: i18n.t('monitor.helpers.sc5f65e'), color: 'var(--dg-purple)' },
|
||||
{ key: 'redis_blocked_clients', label: i18n.t('monitor.helpers.s2ad27e'), color: 'var(--dg-orange)' },
|
||||
];
|
||||
|
||||
export const MYSQL_CHARTS: MwChartDef[] = [
|
||||
{ key: 'mysql_qps', label: 'QPS', color: 'var(--dg-blue)', unit: 'q/s' },
|
||||
{ key: 'mysql_connections', label: '连接数', color: 'var(--dg-cyan)' },
|
||||
{ key: 'mysql_threads_running', label: '活跃线程', color: 'var(--dg-purple)' },
|
||||
{ key: 'mysql_buffer_hit_rate', label: '缓冲池命中率', color: 'var(--dg-green)', percent: true },
|
||||
{ key: 'mysql_slow_queries', label: '慢查询速率', color: 'var(--dg-orange)', unit: '次/周期', counterDelta: true },
|
||||
{ key: 'mysql_bytes_sent', label: '发送流量', color: 'var(--dg-amber)', formatter: fmtMemory },
|
||||
{ key: 'mysql_connections', label: i18n.t('k8s.topology.connections'), color: 'var(--dg-cyan)' },
|
||||
{ key: 'mysql_threads_running', label: i18n.t('monitor.helpers.se0f6ab'), color: 'var(--dg-purple)' },
|
||||
{ key: 'mysql_buffer_hit_rate', label: i18n.t('monitor.helpers.sa2d02e'), color: 'var(--dg-green)', percent: true },
|
||||
{ key: 'mysql_slow_queries', label: i18n.t('monitor.helpers.sbd73bc'), color: 'var(--dg-orange)', unit: i18n.t('monitor.helpers.s3878fe'), counterDelta: true },
|
||||
{ key: 'mysql_bytes_sent', label: i18n.t('monitor.helpers.s90d6af'), color: 'var(--dg-amber)', formatter: fmtMemory },
|
||||
];
|
||||
|
||||
export function fmtMemory(bytes: number): string {
|
||||
@@ -138,7 +139,7 @@ export function statusBg(status: string): string {
|
||||
}
|
||||
|
||||
export function redisModeLabel(mode?: string): string {
|
||||
return mode === 'cluster' ? '集群' : '单机';
|
||||
return mode === 'cluster' ? i18n.t('monitor.helpers.s85fe50') : i18n.t('monitor.helpers.s047499');
|
||||
}
|
||||
|
||||
export function redisMemoryPct(metrics: Record<string, number>): number | null {
|
||||
@@ -208,27 +209,27 @@ export function evalMwHealth(target: MwTarget): MwHealthIssue[] {
|
||||
const m = target.latest_metrics || {};
|
||||
const issues: MwHealthIssue[] = [];
|
||||
if (target.status !== 'online') {
|
||||
issues.push({ level: 'crit', message: '实例离线' });
|
||||
issues.push({ level: 'crit', message: i18n.t('monitor.helpers.s4ace65') });
|
||||
return issues;
|
||||
}
|
||||
if (target.description === 'redis') {
|
||||
if (m.redis_hit_rate > 0 && m.redis_hit_rate < 0.8) issues.push({ level: 'crit', message: '命中率 < 80%' });
|
||||
else if (m.redis_hit_rate > 0 && m.redis_hit_rate < 0.9) issues.push({ level: 'warn', message: '命中率 < 90%' });
|
||||
if (m.redis_hit_rate > 0 && m.redis_hit_rate < 0.8) issues.push({ level: 'crit', message: i18n.t('monitor.helpers.sff1d97') });
|
||||
else if (m.redis_hit_rate > 0 && m.redis_hit_rate < 0.9) issues.push({ level: 'warn', message: i18n.t('monitor.helpers.s119023') });
|
||||
const memPct = redisMemoryPct(m);
|
||||
if (memPct != null && memPct > 90) issues.push({ level: 'crit', message: '内存 > 90%' });
|
||||
else if (memPct != null && memPct > 80) issues.push({ level: 'warn', message: '内存 > 80%' });
|
||||
if (m.redis_blocked_clients > 0) issues.push({ level: 'warn', message: '阻塞客户端' });
|
||||
if (m.redis_evicted_keys > 0) issues.push({ level: 'warn', message: 'Key 逐出' });
|
||||
if (memPct != null && memPct > 90) issues.push({ level: 'crit', message: i18n.t('monitor.helpers.s07eee3') });
|
||||
else if (memPct != null && memPct > 80) issues.push({ level: 'warn', message: i18n.t('monitor.helpers.sbb098a') });
|
||||
if (m.redis_blocked_clients > 0) issues.push({ level: 'warn', message: i18n.t('monitor.helpers.s2ad27e') });
|
||||
if (m.redis_evicted_keys > 0) issues.push({ level: 'warn', message: i18n.t('monitor.helpers.sc4db73') });
|
||||
} else if (target.description === 'mysql') {
|
||||
if (m.mysql_buffer_hit_rate > 0 && m.mysql_buffer_hit_rate < 0.9) {
|
||||
issues.push({ level: 'crit', message: '缓冲池命中 < 90%' });
|
||||
issues.push({ level: 'crit', message: i18n.t('monitor.helpers.s998441') });
|
||||
} else if (m.mysql_buffer_hit_rate > 0 && m.mysql_buffer_hit_rate < 0.95) {
|
||||
issues.push({ level: 'warn', message: '缓冲池命中 < 95%' });
|
||||
issues.push({ level: 'warn', message: i18n.t('monitor.helpers.s41ec17') });
|
||||
}
|
||||
const connPct = mysqlConnPct(m);
|
||||
if (connPct != null && connPct > 90) issues.push({ level: 'crit', message: '连接 > 90% 上限' });
|
||||
else if (connPct != null && connPct > 80) issues.push({ level: 'warn', message: '连接 > 80% 上限' });
|
||||
if (m.mysql_threads_running > 20) issues.push({ level: 'warn', message: '活跃线程偏多' });
|
||||
if (connPct != null && connPct > 90) issues.push({ level: 'crit', message: i18n.t('monitor.helpers.s5b95d5') });
|
||||
else if (connPct != null && connPct > 80) issues.push({ level: 'warn', message: i18n.t('monitor.helpers.se0b952') });
|
||||
if (m.mysql_threads_running > 20) issues.push({ level: 'warn', message: i18n.t('monitor.helpers.s56174d') });
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
@@ -244,8 +245,8 @@ export function fmtLastSeen(iso?: string): string {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '--';
|
||||
const diff = Math.floor((Date.now() - d.getTime()) / 1000);
|
||||
if (diff < 60) return `${diff}s 前`;
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m 前`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h 前`;
|
||||
if (diff < 60) return i18n.t('monitor.helpers.s36683c', { diff: diff });
|
||||
if (diff < 3600) return i18n.t('monitor.helpers.s18b0c1', { Math.floor(diff / 60): Math.floor(diff / 60) });
|
||||
if (diff < 86400) return i18n.t('monitor.helpers.s47d820', { Math.floor(diff / 3600): Math.floor(diff / 3600) });
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// 图表选区提示 — 仅缩放后显示恢复入口 // Logan.Li
|
||||
import { ZoomOut } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
label: string | null;
|
||||
@@ -9,10 +10,11 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ChartRangeHint({ label, zoomed, onReset }: Props) {
|
||||
const { t } = useTranslation();
|
||||
if (!zoomed) return null;
|
||||
return (
|
||||
<div className="flex items-center justify-between text-[10px] text-[var(--dg-text-muted)] mt-1 px-0.5">
|
||||
<span>已选时段: <span className="text-[var(--dg-text-primary)] font-mono">{label}</span></span>
|
||||
<span>{t('monitor.chartrangehint.s39ce9b')} <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}>
|
||||
<ZoomOut className="h-3 w-3 mr-1" />恢复全量
|
||||
</Button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
import { PieChart as PieChartIcon } from 'lucide-react';
|
||||
import { PROBE_TYPE_OPTIONS, normalizeProbeType } from './helpers';
|
||||
import type { SiteTask } from './helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
tasks: SiteTask[];
|
||||
@@ -20,6 +21,7 @@ const PROTOCOL_COLORS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export function SiteProtocolPieChart({ tasks }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const protocolData = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const t of PROBE_TYPE_OPTIONS) counts[t] = 0;
|
||||
@@ -35,7 +37,7 @@ export function SiteProtocolPieChart({ tasks }: Props) {
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-6 text-center text-sm text-[var(--dg-text-muted)]">暂无任务</CardContent>
|
||||
<CardContent className="p-6 text-center text-sm text-[var(--dg-text-muted)]">{t('monitor.siteprotocolpiechart.s0589e5')}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +47,7 @@ export function SiteProtocolPieChart({ tasks }: Props) {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<PieChartIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">协议数量分布</h3>
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">{t('monitor.siteprotocolpiechart.s20721c')}</h3>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">共 {tasks.length} 个任务</span>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
SITE_TIME_RANGE_HOURS, SITE_TIME_RANGE_LABELS,
|
||||
} from './helpers';
|
||||
import { SiteProtocolObs } from './observability/SiteProtocolObs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface Props {
|
||||
task: SiteTask | null;
|
||||
@@ -34,7 +36,7 @@ function statusBadge(status: string) {
|
||||
<Badge className={ok
|
||||
? 'bg-emerald-500/15 dg-text-green border-emerald-500/30 text-[10px]'
|
||||
: 'bg-red-500/15 dg-text-red border-red-500/30 text-[10px]'}>
|
||||
{ok ? '成功' : timeout ? '超时' : '失败'}
|
||||
{ok ? i18n.t('common.success') : timeout ? i18n.t('common.timeout') : i18n.t('common.failed')}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +49,7 @@ function httpCodeBadge(code?: number, expected = 200) {
|
||||
}
|
||||
|
||||
export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [results, setResults] = useState<SiteResult[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [probing, setProbing] = useState(false);
|
||||
@@ -76,11 +79,11 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||
setProbing(true);
|
||||
try {
|
||||
await siteApi.probeNow(task.id);
|
||||
toast.success('拨测完成');
|
||||
toast.success(t('monitor.siteresultspanel.sb8649f'));
|
||||
loadResults();
|
||||
onProbed();
|
||||
} catch (e) {
|
||||
toast.error((e as { message?: string }).message || '拨测失败');
|
||||
toast.error((e as { message?: string }).message || t('monitor.siteresultspanel.s3f60d8'));
|
||||
} finally {
|
||||
setProbing(false);
|
||||
}
|
||||
@@ -116,41 +119,41 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||
case 'HTTP':
|
||||
return (
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-xs">时间</TableHead>
|
||||
<TableHead className="text-xs">状态</TableHead>
|
||||
<TableHead className="text-xs">状态码</TableHead>
|
||||
<TableHead className="text-xs">延迟</TableHead>
|
||||
<TableHead className="text-xs">说明</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.logTime')}</TableHead>
|
||||
<TableHead className="text-xs">{t('common.status')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.siteProtocol.http.statusCodeName')}</TableHead>
|
||||
<TableHead className="text-xs">{t('common.latency')}</TableHead>
|
||||
<TableHead className="text-xs">{t('cloud.colDescription')}</TableHead>
|
||||
</TableRow>
|
||||
);
|
||||
case 'SSL':
|
||||
return (
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-xs">时间</TableHead>
|
||||
<TableHead className="text-xs">握手</TableHead>
|
||||
<TableHead className="text-xs">到期日</TableHead>
|
||||
<TableHead className="text-xs">剩余</TableHead>
|
||||
<TableHead className="text-xs">签发者</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.logTime')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.siteresultspanel.s3f04bb')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.siteresultspanel.s499b9a')}</TableHead>
|
||||
<TableHead className="text-xs">{t('common.remaining')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.siteProtocol.ssl.issuer')}</TableHead>
|
||||
</TableRow>
|
||||
);
|
||||
case 'TCP':
|
||||
return (
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-xs">时间</TableHead>
|
||||
<TableHead className="text-xs">连通</TableHead>
|
||||
<TableHead className="text-xs">延迟</TableHead>
|
||||
<TableHead className="text-xs">远端</TableHead>
|
||||
<TableHead className="text-xs">说明</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.logTime')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.siteProtocol.tcp.connected')}</TableHead>
|
||||
<TableHead className="text-xs">{t('common.latency')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.siteresultspanel.sa7d143')}</TableHead>
|
||||
<TableHead className="text-xs">{t('cloud.colDescription')}</TableHead>
|
||||
</TableRow>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="text-xs">时间</TableHead>
|
||||
<TableHead className="text-xs">状态</TableHead>
|
||||
<TableHead className="text-xs">延迟</TableHead>
|
||||
<TableHead className="text-xs">解析/主机</TableHead>
|
||||
<TableHead className="text-xs">说明</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.logTime')}</TableHead>
|
||||
<TableHead className="text-xs">{t('common.status')}</TableHead>
|
||||
<TableHead className="text-xs">{t('common.latency')}</TableHead>
|
||||
<TableHead className="text-xs">{t('monitor.siteresultspanel.s3197ed')}</TableHead>
|
||||
<TableHead className="text-xs">{t('cloud.colDescription')}</TableHead>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -187,7 +190,7 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||
<TableCell>{statusBadge(r.status)}</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)] max-w-[160px] truncate">{r.error_message || (r.status === 'success' ? '连通' : '--')}</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>
|
||||
);
|
||||
default:
|
||||
@@ -222,10 +225,10 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||
<Select value={timeRange} onValueChange={(v) => setTimeRange(v as SiteTimeRange)}>
|
||||
<SelectTrigger className="h-7 w-[92px] text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1h">近 1 小时</SelectItem>
|
||||
<SelectItem value="6h">近 6 小时</SelectItem>
|
||||
<SelectItem value="24h">近 24 小时</SelectItem>
|
||||
<SelectItem value="7d">近 7 天</SelectItem>
|
||||
<SelectItem value="1h">{t('monitor.siteresultspanel.sf972b7')}</SelectItem>
|
||||
<SelectItem value="6h">{t('monitor.siteProtocol.rangeDefault')}</SelectItem>
|
||||
<SelectItem value="24h">{t('monitor.siteresultspanel.s6e99b7')}</SelectItem>
|
||||
<SelectItem value="7d">{t('monitor.siteresultspanel.sd765ef')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadResults} disabled={loading}>
|
||||
@@ -241,9 +244,9 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||
<div className="mb-4 flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2.5 text-xs">
|
||||
<ShieldAlert className="h-4 w-4 shrink-0 mt-0.5 dg-text-yellow" />
|
||||
<div>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">SSL 证书预警</p>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">{t('monitor.siteresultspanel.sc20e3b')}</p>
|
||||
<p className="mt-0.5 text-[var(--dg-text-muted)]">
|
||||
到期 <span className="font-mono">{task.ssl_cert_expiry?.slice(0, 10) || '--'}</span>,剩余 <span className={sslInfo.tone}>{sslInfo.text}</span>(阈值 {SSL_EXPIRY_WARN_DAYS} 天)
|
||||
到期 <span className="font-mono">{task.ssl_cert_expiry?.slice(0, 10) || '--'}</span>{t('monitor.siteresultspanel.s524e22')} <span className={sslInfo.tone}>{sslInfo.text}</span>(阈值 {SSL_EXPIRY_WARN_DAYS} 天)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,7 +261,7 @@ export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||
<SiteProtocolObs task={task} results={results} rangeLabel={rangeLabel} />
|
||||
<div className={`mt-4 overflow-x-auto ${fullPage ? 'max-h-80' : 'max-h-56'}`}>
|
||||
{results.length === 0 ? (
|
||||
<p className="text-center py-6 text-sm text-[var(--dg-text-muted)]">该时段暂无拨测记录</p>
|
||||
<p className="text-center py-6 text-sm text-[var(--dg-text-muted)]">{t('monitor.siteresultspanel.s592b56')}</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>{renderTableHead()}</TableHeader>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18n from '@/i18n';
|
||||
// 站点拨测辅助函数 // Logan.Li
|
||||
|
||||
export const PROBE_TYPE_OPTIONS = ['HTTP', 'SSL', 'TCP', 'DNS', 'PING'] as const;
|
||||
@@ -56,10 +57,10 @@ export const SITE_TIME_RANGE_HOURS: Record<SiteTimeRange, number> = {
|
||||
};
|
||||
|
||||
export const SITE_TIME_RANGE_LABELS: Record<SiteTimeRange, string> = {
|
||||
'1h': '近 1 小时',
|
||||
'6h': '近 6 小时',
|
||||
'24h': '近 24 小时',
|
||||
'7d': '近 7 天',
|
||||
'1h': i18n.t('monitor.helpers.sf972b7'),
|
||||
'6h': i18n.t('monitor.siteProtocol.rangeDefault'),
|
||||
'24h': i18n.t('monitor.helpers.s6e99b7'),
|
||||
'7d': i18n.t('monitor.helpers.sd765ef'),
|
||||
};
|
||||
|
||||
/** 各协议推荐默认拨测间隔(秒)// Logan.Li */
|
||||
@@ -85,24 +86,24 @@ export function defaultProbeFrequency(type: ProbeType): number {
|
||||
}
|
||||
|
||||
export const probeTypeMeta: Record<ProbeType, { label: string; placeholder: string; hint: string }> = {
|
||||
HTTP: { label: '目标 URL', placeholder: 'https://example.com/health', hint: 'Web 请求(支持 http/https),检测状态码与响应时间' },
|
||||
SSL: { label: 'SSL 地址', placeholder: 'example.com:443 或 https://example.com', hint: 'TLS 握手检测证书(支持 host:port / https:// 域名,不发起 HTTP 请求)' },
|
||||
TCP: { label: '主机:端口', placeholder: '192.168.1.1:8080 或 example.com:443', hint: 'TCP 端口连通性' },
|
||||
DNS: { label: '域名', placeholder: 'example.com', hint: 'DNS 解析延迟与结果' },
|
||||
PING: { label: '主机地址', placeholder: '192.168.1.1 或 example.com', hint: 'ICMP 连通性与延迟(默认 2 分钟/次)' },
|
||||
HTTP: { label: i18n.t('monitor.helpers.s277c74'), placeholder: 'https://example.com/health', hint: i18n.t('monitor.helpers.s953f0b') },
|
||||
SSL: { label: i18n.t('monitor.helpers.s8e3a1e'), placeholder: i18n.t('monitor.helpers.s8a6cb9'), hint: i18n.t('monitor.helpers.sefe596') },
|
||||
TCP: { label: i18n.t('monitor.helpers.seeda3a'), placeholder: i18n.t('monitor.helpers.sd315f5'), hint: i18n.t('monitor.helpers.s5a87d2') },
|
||||
DNS: { label: i18n.t('cloud.domainName'), placeholder: 'example.com', hint: i18n.t('monitor.helpers.sd3fd8f') },
|
||||
PING: { label: i18n.t('monitor.helpers.saeb527'), placeholder: i18n.t('monitor.helpers.sd6bbd6'), hint: i18n.t('monitor.helpers.se850c8') },
|
||||
};
|
||||
|
||||
export function probeFrequencyHint(type: ProbeType): string {
|
||||
const sec = defaultProbeFrequency(type);
|
||||
return `推荐 ${fmtFrequency(sec)},最小 ${fmtFrequency(MIN_PROBE_FREQUENCY[type])}`;
|
||||
return i18n.t('monitor.helpers.see003c', { fmtFrequency(sec): fmtFrequency(sec), fmtFrequency(MIN_PROBE_FREQUENCY[type]): fmtFrequency(MIN_PROBE_FREQUENCY[type]) });
|
||||
}
|
||||
|
||||
/** 拨测频率展示 // Logan.Li */
|
||||
export function fmtFrequency(sec?: number): string {
|
||||
if (!sec) return '--';
|
||||
if (sec < 60) return `${sec}秒`;
|
||||
if (sec < 3600) return `${Math.round(sec / 60)}分钟`;
|
||||
return `${(sec / 3600).toFixed(1)}小时`;
|
||||
if (sec < 60) return i18n.t('monitor.helpers.s9c205b', { sec: sec });
|
||||
if (sec < 3600) return i18n.t('monitor.helpers.s425b42', { Math.round(sec / 60): Math.round(sec / 60) });
|
||||
return i18n.t('monitor.helpers.s22e889', { (sec / 3600).toFixed(1): (sec / 3600).toFixed(1) });
|
||||
}
|
||||
|
||||
/** 可用率 / 响应时间组合展示 // Logan.Li */
|
||||
@@ -145,10 +146,10 @@ export function isSslExpiringSoon(days?: number, threshold = SSL_EXPIRY_WARN_DAY
|
||||
/** SSL 证书剩余天数展示 // Logan.Li */
|
||||
export function fmtSslDays(days?: number): { text: string; tone: string } {
|
||||
if (days == null) return { text: '--', tone: 'text-[var(--dg-text-muted)]' };
|
||||
if (days < 0) return { text: '已过期', tone: 'dg-text-red' };
|
||||
if (days <= 7) return { text: `${days}天`, tone: 'dg-text-red' };
|
||||
if (days <= 30) return { text: `${days}天`, tone: 'dg-text-yellow' };
|
||||
return { text: `${days}天`, tone: 'dg-text-green' };
|
||||
if (days < 0) return { text: i18n.t('monitor.sslExpired'), tone: 'dg-text-red' };
|
||||
if (days <= 7) return { text: i18n.t('monitor.helpers.s510381_1', { days: days }), tone: 'dg-text-red' };
|
||||
if (days <= 30) return { text: i18n.t('monitor.helpers.s510381_0', { days: days }), tone: 'dg-text-yellow' };
|
||||
return { text: i18n.t('monitor.helpers.s510381', { days: days }), tone: 'dg-text-green' };
|
||||
}
|
||||
|
||||
/** 归一化拨测协议类型 // Logan.Li */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 站点拨测可观测分析 — 按协议聚合指标与异常事件 // Logan.Li
|
||||
import type { SiteResult, SiteTask } from '../helpers';
|
||||
import { fmtMs, fmtTime, normalizeProbeType } from '../helpers';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export interface ProbeEvent {
|
||||
id: string;
|
||||
@@ -92,16 +93,16 @@ export function analyzeHttpResults(results: SiteResult[], task: SiteTask): HttpO
|
||||
}
|
||||
if (code >= 500) {
|
||||
err5xx++;
|
||||
pushEvent(events, r, `HTTP ${code}`, 'error', r.error_message || '服务端错误');
|
||||
pushEvent(events, r, `HTTP ${code}`, 'error', r.error_message || i18n.t('monitor.analytics.s71de8a'));
|
||||
} else if (code >= 400) {
|
||||
err4xx++;
|
||||
pushEvent(events, r, `HTTP ${code}`, 'warn', r.error_message || '客户端/鉴权错误');
|
||||
pushEvent(events, r, `HTTP ${code}`, 'warn', r.error_message || i18n.t('monitor.analytics.sdc751c'));
|
||||
} else if (r.status === 'timeout') {
|
||||
otherErr++;
|
||||
pushEvent(events, r, '请求超时', 'error', r.error_message);
|
||||
pushEvent(events, r, i18n.t('common.timeoutError'), 'error', r.error_message);
|
||||
} else {
|
||||
otherErr++;
|
||||
pushEvent(events, r, code > 0 ? `HTTP ${code}` : '请求失败', 'error', r.error_message);
|
||||
pushEvent(events, r, code > 0 ? `HTTP ${code}` : i18n.t('monitor.analytics.sf50bf4'), 'error', r.error_message);
|
||||
}
|
||||
}
|
||||
const latest = results[0];
|
||||
@@ -127,9 +128,9 @@ export function analyzeSslResults(results: SiteResult[], task: SiteTask): SslObs
|
||||
const events: ProbeEvent[] = [];
|
||||
for (const r of results) {
|
||||
if (r.status !== 'success') {
|
||||
pushEvent(events, r, 'TLS 握手失败', 'error', r.error_message);
|
||||
pushEvent(events, r, i18n.t('monitor.analytics.sddbae5'), 'error', r.error_message);
|
||||
} else if (r.ssl_days_remaining != null && r.ssl_days_remaining < 30) {
|
||||
pushEvent(events, r, `证书剩余 ${r.ssl_days_remaining} 天`, r.ssl_days_remaining <= 7 ? 'error' : 'warn', expiry);
|
||||
pushEvent(events, r, i18n.t('monitor.analytics.s819918', { r.ssl_days_remaining: r.ssl_days_remaining }), r.ssl_days_remaining <= 7 ? 'error' : 'warn', expiry);
|
||||
}
|
||||
}
|
||||
const chart = tooltipOrder(results)
|
||||
@@ -157,10 +158,10 @@ function analyzeLatencyResults(results: SiteResult[], label: string): LatencyObs
|
||||
const latency = r.response_time ?? r.total_time ?? 0;
|
||||
const failed = r.status !== 'success';
|
||||
const spike = !failed && latency > spikeThreshold;
|
||||
if (failed) pushEvent(events, r, `${label}失败`, 'error', r.error_message || r.remote_ip);
|
||||
if (failed) pushEvent(events, r, i18n.t('monitor.analytics.s6fce61', { label: label }), 'error', r.error_message || r.remote_ip);
|
||||
else if (spike) {
|
||||
spikeCount++;
|
||||
pushEvent(events, r, `${label}延迟波动`, 'warn', fmtMs(latency));
|
||||
pushEvent(events, r, i18n.t('monitor.analytics.s80bda9', { label: label }), 'warn', fmtMs(latency));
|
||||
}
|
||||
return { time: fmtTime(r.checked_at).slice(-5), latency, failed, spike };
|
||||
});
|
||||
@@ -172,7 +173,7 @@ export function analyzePingResults(results: SiteResult[]): LatencyObsSummary {
|
||||
}
|
||||
|
||||
export function analyzeDnsResults(results: SiteResult[]): LatencyObsSummary {
|
||||
return analyzeLatencyResults(results, 'DNS 解析');
|
||||
return analyzeLatencyResults(results, i18n.t('cloud.domainResolve'));
|
||||
}
|
||||
|
||||
export function analyzeTcpResults(results: SiteResult[]): ConnectivityObsSummary {
|
||||
@@ -185,7 +186,7 @@ export function analyzeTcpResults(results: SiteResult[]): ConnectivityObsSummary
|
||||
const failed = r.status !== 'success';
|
||||
if (failed) {
|
||||
lastFailAt = r.checked_at;
|
||||
pushEvent(events, r, 'TCP 不通', 'error', r.error_message || '连接失败');
|
||||
pushEvent(events, r, i18n.t('monitor.analytics.s0614a3'), 'error', r.error_message || i18n.t('aiops.llmTestFailed'));
|
||||
}
|
||||
return { time: fmtTime(r.checked_at).slice(-5), up: failed ? 0 : 100, failed };
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SNMP_TIME_RANGE_HOURS, fmtMetricValue, isSnmpCounterMetric, metricColor, pickSeriesByCategory,
|
||||
} from './helpers';
|
||||
import { SnmpMetricTrendChart } from './SnmpMetricTrendChart';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
device: SnmpDevice | null;
|
||||
@@ -29,6 +30,7 @@ interface ChartBlock {
|
||||
}
|
||||
|
||||
export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [timeRange, setTimeRange] = useState<SnmpTimeRange>('24h');
|
||||
const [series, setSeries] = useState<SnmpMetricSeries[]>([]);
|
||||
const [charts, setCharts] = useState<ChartBlock[]>([]);
|
||||
@@ -114,9 +116,9 @@ export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="6h">近 6 小时</SelectItem>
|
||||
<SelectItem value="24h">近 24 小时</SelectItem>
|
||||
<SelectItem value="7d">近 7 天</SelectItem>
|
||||
<SelectItem value="6h">{t('monitor.siteProtocol.rangeDefault')}</SelectItem>
|
||||
<SelectItem value="24h">{t('monitor.snmpdevicechartspanel.s6e99b7')}</SelectItem>
|
||||
<SelectItem value="7d">{t('monitor.snmpdevicechartspanel.sd765ef')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={loadCharts} disabled={loading}>
|
||||
@@ -128,9 +130,9 @@ export function SnmpDeviceChartsPanel({ device, templateName }: Props) {
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||
{[
|
||||
{ label: '最新 CPU', value: latestCpu ? fmtMetricValue(latestCpu.latest_value, latestCpu.unit || '%') : '--' },
|
||||
{ label: '最新内存', value: latestMem ? fmtMetricValue(latestMem.latest_value, latestMem.unit || '%') : '--' },
|
||||
{ label: '数据保留', value: '7 天' },
|
||||
{ label: t('monitor.snmpdevicechartspanel.s53a041'), value: latestCpu ? fmtMetricValue(latestCpu.latest_value, latestCpu.unit || '%') : '--' },
|
||||
{ label: t('monitor.snmpdevicechartspanel.s33962a'), value: latestMem ? fmtMetricValue(latestMem.latest_value, latestMem.unit || '%') : '--' },
|
||||
{ label: t('monitor.snmpdevicechartspanel.s26bddc'), value: t('monitor.snmpdevicechartspanel.sfdef8c') },
|
||||
].map((item) => (
|
||||
<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>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BarChart3, PieChart as PieChartIcon } from 'lucide-react';
|
||||
import type { SnmpDevice, SnmpOverviewItem } from './helpers';
|
||||
import { fmtMetricValue, statusColor } from './helpers';
|
||||
import { MonitorChartTooltip, MONITOR_PIE_STROKE, MonitorPieTooltip } from '../shared/monitorChartTheme';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
devices: SnmpDevice[];
|
||||
@@ -16,14 +17,15 @@ interface Props {
|
||||
}
|
||||
|
||||
export function SnmpOverviewCharts({ devices, overview }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const statusData = useMemo(() => {
|
||||
const online = devices.filter((d) => d.status === 'online').length;
|
||||
const error = devices.filter((d) => d.status === 'error').length;
|
||||
const offline = devices.length - online - error;
|
||||
return [
|
||||
{ name: '在线', value: online, fill: 'var(--dg-green)' },
|
||||
{ name: '异常', value: error, fill: 'var(--dg-red)' },
|
||||
{ name: '离线', value: offline, fill: 'var(--dg-text-muted)' },
|
||||
{ name: t('common.online'), value: online, fill: 'var(--dg-green)' },
|
||||
{ name: t('common.abnormal'), value: error, fill: 'var(--dg-red)' },
|
||||
{ name: t('common.offline'), value: offline, fill: 'var(--dg-text-muted)' },
|
||||
].filter((d) => d.value > 0);
|
||||
}, [devices]);
|
||||
|
||||
@@ -47,10 +49,10 @@ export function SnmpOverviewCharts({ devices, overview }: Props) {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<BarChart3 className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">设备 CPU 对比</h3>
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">{t('monitor.snmpoverviewcharts.s64f5a4')}</h3>
|
||||
</div>
|
||||
{cpuData.length === 0 ? (
|
||||
<p className="text-xs text-[var(--dg-text-muted)] py-8 text-center">暂无 CPU 指标,请先完成采集</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] py-8 text-center">{t('monitor.snmpoverviewcharts.s1f4418')}</p>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={Math.max(160, cpuData.length * 36)}>
|
||||
<BarChart data={cpuData} layout="vertical" margin={{ top: 0, right: 16, left: 0, bottom: 0 }}>
|
||||
@@ -98,7 +100,7 @@ export function SnmpOverviewCharts({ devices, overview }: Props) {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<PieChartIcon className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">设备状态</h3>
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">{t('monitor.snmpoverviewcharts.sbb0ae9')}</h3>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
<PieChart>
|
||||
|
||||
Reference in new issue
Block a user