Files
mengyamonitor/mengyamonitor-frontend/src/components/ServerDetail/ServerDetail.tsx
2026-05-16 19:03:34 +08:00

535 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};