151 lines
5.4 KiB
JavaScript
151 lines
5.4 KiB
JavaScript
import { useEffect, useState, useCallback } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { NavBar, Card, Empty, DotLoading } from 'antd-mobile'
|
|
import { api } from '../api/client.js'
|
|
import SwipeTabs from '../components/SwipeTabs.jsx'
|
|
|
|
// 监控页: 两个横向分类
|
|
// - 主机监控: 实时数据来自 komari (服务端实时代理, 不落库), 点击节点看 24h 时序
|
|
// - 服务监控: 暂留空占位
|
|
function fmtTime(s) {
|
|
if (!s) return ''
|
|
const d = new Date(s)
|
|
if (isNaN(d.getTime())) return s
|
|
const pad = (n) => String(n).padStart(2, '0')
|
|
return pad(d.getHours()) + ':' + pad(d.getMinutes())
|
|
}
|
|
|
|
function fmtBytes(b) {
|
|
if (!b) return '0 B'
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
let i = 0
|
|
let v = Number(b)
|
|
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
|
|
return v.toFixed(v >= 100 ? 0 : 1) + ' ' + units[i]
|
|
}
|
|
|
|
function fmtUptime(sec) {
|
|
const s = Number(sec) || 0
|
|
const d = Math.floor(s / 86400)
|
|
const h = Math.floor((s % 86400) / 3600)
|
|
return d > 0 ? d + '天' + h + '小时' : h + '小时'
|
|
}
|
|
|
|
function pct(used, total) {
|
|
return total > 0 ? Math.min(100, (used / total) * 100) : 0
|
|
}
|
|
|
|
export default function MonitorPage() {
|
|
const [active, setActive] = useState('servers')
|
|
const tabs = [
|
|
{ key: 'servers', label: '主机监控', render: () => <ServerView /> },
|
|
{ key: 'services', label: '服务监控', render: () => <ServiceView /> },
|
|
]
|
|
return (
|
|
<div className="page-with-navbar">
|
|
<NavBar back={null}>监控</NavBar>
|
|
<div className="monitor-content">
|
|
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 主机监控列表模块级缓存: 切走再进入时先展示上次数据, 拉取成功后再静默更新
|
|
let cachedServers = null
|
|
|
|
// 主机监控: komari 实时节点列表 (30s 轮询, 静默刷新)
|
|
function ServerView() {
|
|
const [items, setItems] = useState(cachedServers) // 有缓存直接先展示, 无缓存为 null 进入加载态
|
|
const [err, setErr] = useState('')
|
|
const navigate = useNavigate()
|
|
|
|
const load = useCallback((silent) => {
|
|
if (!silent && cachedServers === null) setItems(null) // 仅无缓存时显示加载中
|
|
if (!silent) setErr('')
|
|
api('/servers')
|
|
.then((data) => { cachedServers = data.items || []; setErr(''); setItems(data.items || []) })
|
|
.catch((e) => { if (!silent) { setErr(e.message); if (cachedServers === null) setItems([]) } })
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
load(false)
|
|
const timer = setInterval(() => load(true), 30000)
|
|
return () => clearInterval(timer)
|
|
}, [load])
|
|
|
|
return (
|
|
<div className="server-list">
|
|
{items === null && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
|
{err && (
|
|
<div className="error-banner">
|
|
{err}
|
|
<span className="retry-link" onClick={() => load(false)}>重试</span>
|
|
</div>
|
|
)}
|
|
{items !== null && !err && items.length === 0 && <Empty description="暂无主机" />}
|
|
{items !== null && items.length > 0 && (
|
|
<>
|
|
<div className="monitor-tip">实时数据 · 每 30 秒自动刷新</div>
|
|
<div className="section-list">
|
|
{items.map((n) => (
|
|
<Card key={n.uuid} className="server-card" onClick={() => navigate('/monitor/server/' + n.uuid)}>
|
|
<div className="server-head">
|
|
<span className="server-name">
|
|
<span className={'status-dot ' + (n.status === 'online' ? 'on' : 'off')} />
|
|
{n.name}
|
|
</span>
|
|
<span className="server-update">更新于 {fmtTime(n.recordedAt)}</span>
|
|
</div>
|
|
<div className="server-metrics">
|
|
<div className="metric">
|
|
<div className="metric-label">CPU</div>
|
|
<Meter percent={n.cpuUsage} />
|
|
<div className="metric-value">{n.cpuUsage.toFixed ? n.cpuUsage.toFixed(1) : n.cpuUsage}%</div>
|
|
</div>
|
|
<div className="metric">
|
|
<div className="metric-label">内存</div>
|
|
<Meter percent={pct(n.ramUsed, n.memTotal)} />
|
|
<div className="metric-value">{fmtBytes(n.ramUsed)} / {fmtBytes(n.memTotal)}</div>
|
|
</div>
|
|
<div className="metric">
|
|
<div className="metric-label">磁盘</div>
|
|
<Meter percent={pct(n.diskUsed, n.diskTotal)} />
|
|
<div className="metric-value">{fmtBytes(n.diskUsed)} / {fmtBytes(n.diskTotal)}</div>
|
|
</div>
|
|
</div>
|
|
<div className="server-foot">
|
|
<span>负载 {n.load1} / {n.load5} / {n.load15}</span>
|
|
<span>{n.cpuCores} 核</span>
|
|
<span>进程 {n.process}</span>
|
|
<span>连接 {n.connections}</span>
|
|
<span>运行 {fmtUptime(n.uptime)}</span>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 进度条 (超 80% 变红)
|
|
function Meter({ percent }) {
|
|
const p = Math.max(0, Math.min(100, Number(percent) || 0))
|
|
return (
|
|
<div className="meter">
|
|
<div className={'meter-fill' + (p >= 80 ? ' warn' : '')} style={{ width: p + '%' }} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 服务监控: 暂留空
|
|
function ServiceView() {
|
|
return (
|
|
<div className="monitor-empty">
|
|
<Empty description="服务监控待接入" />
|
|
</div>
|
|
)
|
|
}
|