535 lines
28 KiB
TypeScript
535 lines
28 KiB
TypeScript
import type { ServerMetrics } from '../../types';
|
||
import { formatBytes, formatUptime, formatPercent, formatLatencyMs } from '../../utils/format';
|
||
import './ServerDetail.css';
|
||
|
||
interface ServerDetailProps {
|
||
metrics: ServerMetrics;
|
||
serverName: string;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export const ServerDetail = ({ metrics, serverName, onClose }: ServerDetailProps) => {
|
||
return (
|
||
<div className="modal-overlay" onClick={onClose}>
|
||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-header">
|
||
<h2>{serverName} - 详细信息</h2>
|
||
<button className="btn-close" onClick={onClose}>×</button>
|
||
</div>
|
||
|
||
<div className="modal-body">
|
||
<section className="detail-section">
|
||
<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 system-info-grid--cols-3">
|
||
<div className="system-info-item">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">主机名</span>
|
||
<span className="system-info-value">{metrics.hostname}</span>
|
||
</div>
|
||
</div>
|
||
<div className="system-info-item">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">操作系统</span>
|
||
<span className="system-info-value">{metrics.os.distro}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="system-info-item">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">架构</span>
|
||
<span className="system-info-value">{metrics.os.architecture}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="system-info-item system-info-item-full">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">内核版本</span>
|
||
<span className="system-info-value system-info-value-long">{metrics.os.kernel}</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
{/* 运行时信息 */}
|
||
<div className="system-info-group">
|
||
<h4 className="info-group-title">运行时信息</h4>
|
||
<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>
|
||
<span className="system-info-value highlight">{formatUptime(metrics.uptimeSeconds)}</span>
|
||
</div>
|
||
</div>
|
||
<div className="system-info-item">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">进程数量</span>
|
||
<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">
|
||
<span className="system-info-label">已安装软件包</span>
|
||
<span className="system-info-value">{metrics.system.packageCount} <span className="system-info-sub">({metrics.system.packageManager})</span></span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 性能指标 */}
|
||
<div className="system-info-group">
|
||
<h4 className="info-group-title">性能指标</h4>
|
||
<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>
|
||
<span className="system-info-value">{metrics.system.diskReadSpeed.toFixed(2)} MB/s</span>
|
||
</div>
|
||
</div>
|
||
<div className="system-info-item">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">磁盘写入</span>
|
||
<span className="system-info-value">{metrics.system.diskWriteSpeed.toFixed(2)} MB/s</span>
|
||
</div>
|
||
</div>
|
||
{metrics.system.temperature && metrics.system.temperature > 0 && (
|
||
<div className="system-info-item system-info-item-temperature">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">系统温度</span>
|
||
<span className="system-info-value temperature-value">{metrics.system.temperature.toFixed(1)}°C</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{(metrics.system.networkRxSpeed !== undefined || metrics.system.networkTxSpeed !== undefined) && (
|
||
<>
|
||
<div className="system-info-item">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">网络下载</span>
|
||
<span className="system-info-value">{(metrics.system.networkRxSpeed || 0).toFixed(2)} MB/s</span>
|
||
</div>
|
||
</div>
|
||
<div className="system-info-item">
|
||
<div className="system-info-content">
|
||
<span className="system-info-label">网络上传</span>
|
||
<span className="system-info-value">{(metrics.system.networkTxSpeed || 0).toFixed(2)} MB/s</span>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 延迟信息 */}
|
||
{metrics.latency && (
|
||
<div className="system-info-group">
|
||
<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 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">
|
||
{formatLatencyMs(metrics.latency.clientToServer)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{metrics.latency.external && (
|
||
<>
|
||
<div className="system-info-item">
|
||
<div className="system-info-content system-info-content--stack">
|
||
<span className="system-info-label">百度 (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 system-info-content--stack">
|
||
<span className="system-info-label">谷歌 (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 system-info-content--stack">
|
||
<span className="system-info-label">GitHub (github.com)</span>
|
||
<span className="system-info-sublabel">采集端 TCP 探测</span>
|
||
<span className="system-info-value">
|
||
{metrics.latency.external['github.com'] || '超时'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="detail-section">
|
||
<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 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 emphasize">{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>
|
||
</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>
|
||
{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>
|
||
|
||
<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 className="detail-category-heading">存储与磁盘</h3>
|
||
<ul className="storage-plain-list">
|
||
{metrics.storage.map((disk, index) => (
|
||
<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>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
|
||
{metrics.gpu && metrics.gpu.length > 0 && metrics.gpu[0].status !== 'not_available' && (
|
||
<section className="detail-section">
|
||
<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="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>
|
||
</div>
|
||
<div className="detail-item">
|
||
<span className="item-label">显存使用</span>
|
||
<span className="item-value">{gpu.memoryUsedMB} MB</span>
|
||
</div>
|
||
{gpu.temperature && gpu.temperature > 0 && (
|
||
<div className="detail-item">
|
||
<span className="item-label">温度</span>
|
||
<span className="item-value">{gpu.temperature.toFixed(1)}°C</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</section>
|
||
)}
|
||
|
||
{metrics.network && metrics.network.length > 0 && (
|
||
<section className="detail-section">
|
||
<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-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.dockerStats && metrics.system.dockerStats.available)) && (
|
||
<section className="detail-section">
|
||
<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>
|
||
</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>
|
||
)}
|
||
|
||
{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>
|
||
)}
|
||
|
||
{metrics.system.systemLogs && metrics.system.systemLogs.length > 0 && (
|
||
<section className="detail-section">
|
||
<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">
|
||
{log}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|