chore: sync local updates

This commit is contained in:
2026-05-16 19:03:34 +08:00
parent 9c086c3301
commit 80cf54a201
89 changed files with 10622 additions and 2443 deletions

View File

@@ -1,6 +1,5 @@
import type { ServerMetrics } from '../../types';
import { formatBytes, formatUptime, formatPercent } from '../../utils/format';
import { CircularProgress } from '../Common/CircularProgress';
import { formatBytes, formatUptime, formatPercent, formatLatencyMs } from '../../utils/format';
import './ServerDetail.css';
interface ServerDetailProps {
@@ -20,12 +19,12 @@ export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps
<div className="modal-body">
<section className="detail-section">
<h3></h3>
<h3 className="detail-category-heading"></h3>
<div className="system-info-container">
{/* 系统基本信息 */}
<div className="system-info-group">
<h4 className="info-group-title"></h4>
<div className="system-info-grid">
<div className="system-info-grid system-info-grid--cols-3">
<div className="system-info-item">
<div className="system-info-content">
<span className="system-info-label"></span>
@@ -59,7 +58,7 @@ export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps
{/* 运行时信息 */}
<div className="system-info-group">
<h4 className="info-group-title"></h4>
<div className="system-info-grid">
<div className="system-info-grid system-info-grid--cols-3">
<div className="system-info-item system-info-item-highlight">
<div className="system-info-content">
<span className="system-info-label"></span>
@@ -72,6 +71,22 @@ export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps
<span className="system-info-value">{metrics.system.processCount}</span>
</div>
</div>
{metrics.system.loggedInUsers !== undefined && (
<div className="system-info-item">
<div className="system-info-content">
<span className="system-info-label"></span>
<span className="system-info-value">{metrics.system.loggedInUsers}</span>
</div>
</div>
)}
{metrics.system.tcpConnections !== undefined && (
<div className="system-info-item">
<div className="system-info-content">
<span className="system-info-label">TCP </span>
<span className="system-info-value">{metrics.system.tcpConnections}</span>
</div>
</div>
)}
{metrics.system.packageCount > 0 && (
<div className="system-info-item">
<div className="system-info-content">
@@ -86,7 +101,7 @@ export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps
{/* 性能指标 */}
<div className="system-info-group">
<h4 className="info-group-title"></h4>
<div className="system-info-grid">
<div className="system-info-grid system-info-grid--cols-3">
<div className="system-info-item">
<div className="system-info-content">
<span className="system-info-label"></span>
@@ -129,34 +144,48 @@ export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps
{/* 延迟信息 */}
{metrics.latency && (
<div className="system-info-group">
<h4 className="info-group-title"></h4>
<div className="system-info-grid">
<h4 className="info-group-title"></h4>
<p className="latency-section-hint">
gRPC TCP
TCP:80 / :443 ICMP ping
</p>
<div className="system-info-grid system-info-grid--cols-4">
<div className="system-info-item system-info-item-highlight">
<div className="system-info-content">
<span className="system-info-label"></span>
<div className="system-info-content system-info-content--stack">
<span className="system-info-label"></span>
<span className="system-info-sublabel"> CENTRAL_GRPC TCP </span>
<span className="system-info-value highlight">
{metrics.latency.clientToServer >= 0 ? `${metrics.latency.clientToServer} ms` : '超时'}
{formatLatencyMs(metrics.latency.clientToServer)}
</span>
</div>
</div>
{metrics.latency.external && (
<>
<div className="system-info-item">
<div className="system-info-content">
<div className="system-info-content system-info-content--stack">
<span className="system-info-label"> (baidu.com)</span>
<span className="system-info-value">{metrics.latency.external['baidu.com'] || '超时'}</span>
<span className="system-info-sublabel"> TCP </span>
<span className="system-info-value">
{metrics.latency.external['baidu.com'] || '超时'}
</span>
</div>
</div>
<div className="system-info-item">
<div className="system-info-content">
<div className="system-info-content system-info-content--stack">
<span className="system-info-label"> (google.com)</span>
<span className="system-info-value">{metrics.latency.external['google.com'] || '超时'}</span>
<span className="system-info-sublabel"> TCP </span>
<span className="system-info-value">
{metrics.latency.external['google.com'] || '超时'}
</span>
</div>
</div>
<div className="system-info-item">
<div className="system-info-content">
<div className="system-info-content system-info-content--stack">
<span className="system-info-label">GitHub (github.com)</span>
<span className="system-info-value">{metrics.latency.external['github.com'] || '超时'}</span>
<span className="system-info-sublabel"> TCP </span>
<span className="system-info-value">
{metrics.latency.external['github.com'] || '超时'}
</span>
</div>
</div>
</>
@@ -168,152 +197,152 @@ export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps
</section>
<section className="detail-section">
<h3>CPU</h3>
<div className="cpu-section-content">
<div className="cpu-circular-group">
<div className="cpu-circular">
<CircularProgress
value={metrics.cpu.usagePercent}
label=""
size={140}
color={metrics.cpu.usagePercent > 80 ? '#f87171' : '#86efac'}
subLabel={formatPercent(metrics.cpu.usagePercent)}
/>
</div>
{metrics.cpu.loadAverages && metrics.cpu.loadAverages.length > 0 && metrics.cpu.cores > 0 && (
<div className="cpu-load-circular">
<CircularProgress
value={Math.min((metrics.cpu.loadAverages[0] / metrics.cpu.cores) * 100, 100)}
label="负载"
size={140}
color={metrics.cpu.loadAverages[0] / metrics.cpu.cores > 0.8 ? '#fde047' : '#a7f3d0'}
subLabel={metrics.cpu.loadAverages[0].toFixed(2)}
/>
<h3 className="detail-category-heading"></h3>
<div className="detail-subsection">
<h4 className="detail-subheading"></h4>
<div className="detail-plain">
<div className="detail-grid detail-grid--cpu">
<div className="detail-item detail-item--span-full">
<span className="item-label"></span>
<span className="item-value">{metrics.cpu.model}</span>
</div>
)}
</div>
<div className="detail-grid">
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.cpu.model}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.cpu.cores}</span>
</div>
<div className="detail-item">
<span className="item-label">使</span>
<span className="item-value highlight">{formatPercent(metrics.cpu.usagePercent)}</span>
</div>
{metrics.cpu.temperature && metrics.cpu.temperature > 0 && (
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.cpu.temperature.toFixed(1)}°C</span>
<span className="item-label"></span>
<span className="item-value">{metrics.cpu.cores}</span>
</div>
)}
{metrics.cpu.loadAverages && metrics.cpu.loadAverages.length > 0 && (
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.cpu.loadAverages.join(' / ')}</span>
<span className="item-label">使</span>
<span className="item-value emphasize">{formatPercent(metrics.cpu.usagePercent)}</span>
</div>
)}
</div>
</div>
{metrics.cpu.perCoreUsage && metrics.cpu.perCoreUsage.length > 0 && (
<div className="per-core-section">
<h4>使</h4>
<div className="core-grid-circular">
{metrics.cpu.perCoreUsage.map((core) => (
<div key={core.core} className="core-item-circular">
<CircularProgress
value={core.percent}
label={`Core ${core.core}`}
size={140}
color={core.percent > 80 ? '#f87171' : '#86efac'}
subLabel={formatPercent(core.percent)}
/>
{metrics.cpu.temperature && metrics.cpu.temperature > 0 && (
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.cpu.temperature.toFixed(1)}°C</span>
</div>
))}
)}
{metrics.cpu.loadAverages && metrics.cpu.loadAverages.length > 0 && (
<>
<div className="detail-item">
<span className="item-label"> (1 / 5 / 15 min)</span>
<span className="item-value">{metrics.cpu.loadAverages.map((l) => l.toFixed(2)).join(' / ')}</span>
</div>
{metrics.cpu.cores > 0 && (
<div className="detail-item">
<span className="item-label">1 min </span>
<span className="item-value">
{formatPercent(Math.min((metrics.cpu.loadAverages[0] / metrics.cpu.cores) * 100, 100))}{' '}
<span className="item-sub">
{metrics.cpu.loadAverages[0].toFixed(2)} / {metrics.cpu.cores}
</span>
</span>
</div>
)}
</>
)}
</div>
</div>
)}
</section>
{metrics.cpu.perCoreUsage && metrics.cpu.perCoreUsage.length > 0 && (
<div className="per-core-plain">
<h4 className="detail-subheading detail-subheading--inline">使</h4>
<div className="per-core-grid">
{metrics.cpu.perCoreUsage.map((core) => (
<div key={core.core} className="per-core-chip">
<span className="per-core-name"> {core.core}</span>
<span className="per-core-pct">{formatPercent(core.percent)}</span>
</div>
))}
</div>
</div>
)}
</div>
<section className="detail-section">
<h3></h3>
<div className="memory-section-content">
<div className="memory-circular">
<CircularProgress
value={metrics.memory.usedPercent}
label=""
size={140}
color={metrics.memory.usedPercent > 80 ? '#fde047' : '#6ee7b7'}
subLabel={formatPercent(metrics.memory.usedPercent)}
/>
</div>
<div className="detail-grid">
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{formatBytes(metrics.memory.totalBytes)}</span>
</div>
<div className="detail-item">
<span className="item-label">使</span>
<span className="item-value">{formatBytes(metrics.memory.usedBytes)}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{formatBytes(metrics.memory.freeBytes)}</span>
</div>
<div className="detail-item">
<span className="item-label">使</span>
<span className="item-value highlight">{formatPercent(metrics.memory.usedPercent)}</span>
<div className="detail-subsection">
<h4 className="detail-subheading"></h4>
<div className="detail-plain">
<div className="detail-grid">
<div className="detail-item">
<span className="item-label">使</span>
<span className="item-value emphasize">{formatPercent(metrics.memory.usedPercent)}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{formatBytes(metrics.memory.totalBytes)}</span>
</div>
<div className="detail-item">
<span className="item-label">使</span>
<span className="item-value">{formatBytes(metrics.memory.usedBytes)}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{formatBytes(metrics.memory.freeBytes)}</span>
</div>
{(metrics.memory.swapTotalBytes !== undefined ||
metrics.memory.swapType !== undefined) && (
<>
<div className="detail-item">
<span className="item-label">Swap </span>
<span className="item-value">
{formatBytes(metrics.memory.swapTotalBytes ?? 0)}
</span>
</div>
<div className="detail-item">
<span className="item-label">Swap </span>
<span className="item-value">
{formatBytes(metrics.memory.swapUsedBytes ?? 0)}
{metrics.memory.swapUsedPercent !== undefined && (
<span className="item-sub"> ({formatPercent(metrics.memory.swapUsedPercent)})</span>
)}
</span>
</div>
<div className="detail-item">
<span className="item-label">Swap </span>
<span className="item-value">
{formatBytes(metrics.memory.swapFreeBytes ?? 0)}
</span>
</div>
<div className="detail-item">
<span className="item-label">Swap </span>
<span className="item-value">{metrics.memory.swapType ?? '—'}</span>
</div>
</>
)}
</div>
</div>
</div>
</section>
<section className="detail-section">
<h3></h3>
<div className="storage-grid-circular">
<h3 className="detail-category-heading"></h3>
<ul className="storage-plain-list">
{metrics.storage.map((disk, index) => (
<div key={index} className="storage-item-circular">
<CircularProgress
value={disk.usedPercent}
label={disk.mount}
size={160}
color={disk.usedPercent > 90 ? '#f87171' : '#a7f3d0'}
subLabel={formatPercent(disk.usedPercent)}
/>
<div className="storage-details-mini">
<div>: {formatBytes(disk.totalBytes)}</div>
<div>: {formatBytes(disk.usedBytes)}</div>
</div>
</div>
<li key={index} className="storage-plain-row">
<div className="storage-plain-head">
<span className="storage-mount">{disk.mount}</span>
<span className="storage-pct">{formatPercent(disk.usedPercent)}</span>
</div>
<div className="storage-meta">
{formatBytes(disk.usedBytes)} / {formatBytes(disk.totalBytes)} · {' '}
{formatBytes(disk.freeBytes)}
</div>
</li>
))}
</div>
</ul>
</section>
{metrics.gpu && metrics.gpu.length > 0 && metrics.gpu[0].status !== 'not_available' && (
<section className="detail-section">
<h3>GPU</h3>
<h3 className="detail-category-heading"></h3>
{metrics.gpu.map((gpu, index) => (
<div key={index} className="gpu-item">
<div className="gpu-header">
<strong>{gpu.name}</strong>
<span className={`gpu-status ${gpu.status}`}>{gpu.status}</span>
</div>
<div className="gpu-bar-container">
<div className="gpu-bar-label"></div>
<div className="gpu-bar-bg">
<div
className="gpu-bar"
style={{ width: `${gpu.utilizationPercent}%` }}
/>
</div>
<div className="gpu-bar-value">{formatPercent(gpu.utilizationPercent)}</div>
</div>
<div className="detail-grid">
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value emphasize">{formatPercent(gpu.utilizationPercent)}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{gpu.memoryTotalMB} MB</span>
@@ -336,167 +365,150 @@ export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps
{metrics.network && metrics.network.length > 0 && (
<section className="detail-section">
<h3></h3>
<div className="network-list">
<h3 className="detail-category-heading"></h3>
<div className="network-compact" role="table" aria-label="网络接口">
<div className="network-compact__head" role="row">
<span role="columnheader"></span>
<span role="columnheader">IP</span>
<span role="columnheader">MAC</span>
<span role="columnheader"></span>
<span role="columnheader"></span>
</div>
{metrics.network.map((iface, index) => (
<div key={index} className="network-item">
<div className="network-header">
<div className="network-name-group">
<div className="network-name-info">
<strong className="network-name">{iface.name}</strong>
{iface.ipAddress && (
<span className="network-ip has-ip">
{iface.ipAddress}
</span>
)}
</div>
</div>
</div>
<div className="network-details">
{(iface.macAddress || iface.ipAddress) && (
<div className="network-detail-row">
{iface.macAddress && (
<div className="network-detail-item">
<span className="network-detail-label">MAC </span>
<span className="network-detail-value">{iface.macAddress}</span>
</div>
)}
{iface.ipAddress && (
<div className="network-detail-item">
<span className="network-detail-label">IP </span>
<span className="network-detail-value">{iface.ipAddress}</span>
</div>
)}
</div>
)}
<div className="network-traffic">
<div className="traffic-item">
<div className="traffic-header">
<span className="traffic-label"></span>
<span className="traffic-value">{formatBytes(iface.rxBytes)}</span>
</div>
</div>
<div className="traffic-item">
<div className="traffic-header">
<span className="traffic-label"></span>
<span className="traffic-value">{formatBytes(iface.txBytes)}</span>
</div>
</div>
</div>
</div>
<div key={index} className="network-compact__row" role="row">
<strong className="network-compact__iface" role="cell">
{iface.name}
</strong>
<span
className={
iface.ipAddress && iface.ipAddress !== 'N/A'
? 'network-compact__ip network-compact__ip--ok'
: 'network-compact__placeholder'
}
role="cell"
>
{iface.ipAddress && iface.ipAddress !== 'N/A' ? iface.ipAddress : '—'}
</span>
<span
className="network-compact__mac"
role="cell"
title={iface.macAddress || undefined}
>
{iface.macAddress || '—'}
</span>
<span className="network-compact__num" role="cell" title="接收流量">
{formatBytes(iface.rxBytes)}
</span>
<span className="network-compact__num" role="cell" title="发送流量">
{formatBytes(iface.txBytes)}
</span>
</div>
))}
</div>
</section>
)}
{metrics.system.topProcesses && metrics.system.topProcesses.length > 0 && (
{((metrics.system.topProcesses && metrics.system.topProcesses.length > 0) ||
(metrics.system.dockerStats && metrics.system.dockerStats.available)) && (
<section className="detail-section">
<h3>Top 5 </h3>
<div className="process-list">
{metrics.system.topProcesses.map((proc, index) => (
<div key={index} className="process-item">
<div className="process-rank">{index + 1}</div>
<div className="process-content">
<div className="process-header">
<div className="process-name-group">
<strong className="process-name">{proc.name}</strong>
<span className="process-pid">PID: {proc.pid}</span>
</div>
<h3 className="detail-category-heading"></h3>
{metrics.system.topProcesses && metrics.system.topProcesses.length > 0 && (
<div className="detail-subsection">
<h4 className="detail-subheading"> 10</h4>
<div className="process-compact" role="table" aria-label="进程占用">
<div className="process-compact__head" role="row">
<span role="columnheader">#</span>
<span role="columnheader"></span>
<span role="columnheader">PID</span>
<span role="columnheader">CPU</span>
<span role="columnheader"></span>
</div>
{metrics.system.topProcesses.map((proc, index) => (
<div key={index} className="process-compact__row" role="row">
<span className="process-compact__rank" role="cell">
{index + 1}
</span>
<span className="process-compact__name" role="cell" title={proc.name}>
{proc.name}
</span>
<span className="process-compact__pid" role="cell">
{proc.pid}
</span>
<span className="process-compact__num" role="cell">
{proc.cpu}%
</span>
<span className="process-compact__num process-compact__mem" role="cell">
{proc.memory}%
<span className="process-compact__mem-abs"> {proc.memoryMB.toFixed(1)} MB</span>
</span>
</div>
<div className="process-metrics">
<div className="process-metric-item">
<div className="metric-header">
<span className="metric-label">CPU</span>
<span className="metric-value">{proc.cpu}%</span>
</div>
<div className="metric-bar-container">
<div
className="metric-bar cpu-bar"
style={{ width: `${Math.min(proc.cpu, 100)}%` }}
/>
</div>
</div>
<div className="process-metric-item">
<div className="metric-header">
<span className="metric-label"></span>
<span className="metric-value">{proc.memory}% ({proc.memoryMB.toFixed(1)} MB)</span>
</div>
<div className="metric-bar-container">
<div
className="metric-bar memory-bar"
style={{ width: `${Math.min(proc.memory, 100)}%` }}
/>
</div>
</div>
))}
</div>
</div>
)}
{metrics.system.dockerStats && metrics.system.dockerStats.available && (
<div className="detail-subsection">
<h4 className="detail-subheading">Docker</h4>
{metrics.system.dockerStats.version && (
<div className="detail-grid">
<div className="detail-item">
<span className="item-label">Docker </span>
<span className="item-value">{metrics.system.dockerStats.version}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.system.dockerStats.running}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.system.dockerStats.stopped}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.system.dockerStats.imageCount}</span>
</div>
</div>
</div>
))}
</div>
</section>
)}
)}
{metrics.system.dockerStats && metrics.system.dockerStats.available && (
<section className="detail-section">
<h3>Docker </h3>
{metrics.system.dockerStats.version && (
<div className="detail-grid">
<div className="detail-item">
<span className="item-label">Docker </span>
<span className="item-value">{metrics.system.dockerStats.version}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.system.dockerStats.running}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.system.dockerStats.stopped}</span>
</div>
<div className="detail-item">
<span className="item-label"></span>
<span className="item-value">{metrics.system.dockerStats.imageCount}</span>
</div>
</div>
)}
{metrics.system.dockerStats.runningNames && metrics.system.dockerStats.runningNames.length > 0 && (
<div className="docker-containers-section">
<h4></h4>
<div className="docker-names-list">
{metrics.system.dockerStats.runningNames.map((name, index) => (
<span key={index} className="docker-name-tag running">
{name}
</span>
))}
</div>
</div>
)}
{metrics.system.dockerStats.stoppedNames && metrics.system.dockerStats.stoppedNames.length > 0 && (
<div className="docker-containers-section">
<h4></h4>
<div className="docker-names-list">
{metrics.system.dockerStats.stoppedNames.map((name, index) => (
<span key={index} className="docker-name-tag stopped">
{name}
</span>
))}
</div>
</div>
)}
{metrics.system.dockerStats.imageNames && metrics.system.dockerStats.imageNames.length > 0 && (
<div className="docker-containers-section">
<h4></h4>
<div className="docker-names-list">
{metrics.system.dockerStats.imageNames.map((name, index) => (
<span key={index} className="docker-name-tag image">
{name}
</span>
))}
</div>
{metrics.system.dockerStats.runningNames && metrics.system.dockerStats.runningNames.length > 0 && (
<div className="docker-containers-section">
<h4 className="detail-subheading detail-subheading--minor"></h4>
<div className="docker-names-list">
{metrics.system.dockerStats.runningNames.map((name, index) => (
<span key={index} className="docker-name-tag running">
{name}
</span>
))}
</div>
</div>
)}
{metrics.system.dockerStats.stoppedNames && metrics.system.dockerStats.stoppedNames.length > 0 && (
<div className="docker-containers-section">
<h4 className="detail-subheading detail-subheading--minor"></h4>
<div className="docker-names-list">
{metrics.system.dockerStats.stoppedNames.map((name, index) => (
<span key={index} className="docker-name-tag stopped">
{name}
</span>
))}
</div>
</div>
)}
{metrics.system.dockerStats.imageNames && metrics.system.dockerStats.imageNames.length > 0 && (
<div className="docker-containers-section">
<h4 className="detail-subheading detail-subheading--minor"></h4>
<div className="docker-names-list">
{metrics.system.dockerStats.imageNames.map((name, index) => (
<span key={index} className="docker-name-tag image">
{name}
</span>
))}
</div>
</div>
)}
</div>
)}
</section>
@@ -504,7 +516,8 @@ export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps
{metrics.system.systemLogs && metrics.system.systemLogs.length > 0 && (
<section className="detail-section">
<h3></h3>
<h3 className="detail-category-heading"></h3>
<h4 className="detail-subheading"></h4>
<div className="logs-container">
{metrics.system.systemLogs.map((log, index) => (
<div key={index} className="log-line">