101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import type { Page } from '@shared/api/types'
|
|
import { api } from '@shared/api/client'
|
|
import { openLive } from '@shared/api/source'
|
|
|
|
/** 展示页能拿到的主机状态:只有汇总的数字,没有型号和 IP。 */
|
|
export interface Host {
|
|
cpu_percent: number | null
|
|
mem_used: number | null
|
|
mem_total: number | null
|
|
disk_used: number | null
|
|
disk_total: number | null
|
|
net_rx_total: number | null
|
|
net_tx_total: number | null
|
|
net_rx_bps: number | null
|
|
net_tx_bps: number | null
|
|
uptime_seconds: number | null
|
|
at: string
|
|
}
|
|
|
|
export interface HostItem {
|
|
agent_id: string
|
|
name: string
|
|
online: boolean
|
|
stale: boolean
|
|
last_seen_at: string | null
|
|
host: Host | null
|
|
}
|
|
|
|
/**
|
|
* Agent 主机状态的实时订阅:先用 REST 取一次全量,再用 SSE(`hosts` 主题)接收每一次更新。
|
|
* 连接断开时浏览器会自动重连;重连之后服务端会先推一份当前快照。
|
|
*/
|
|
export function useHosts(): { hosts: Map<string, HostItem>; live: boolean } {
|
|
const [hosts, setHosts] = useState<Map<string, HostItem>>(new Map())
|
|
const [live, setLive] = useState(false)
|
|
|
|
useEffect(() => {
|
|
let alive = true
|
|
api
|
|
.get<Page<HostItem>>('/api/public/hosts')
|
|
.then((page) => alive && setHosts(new Map(page.items.map((i) => [i.agent_id, i]))))
|
|
.catch(() => undefined)
|
|
|
|
const source = openLive('/api/public/live?topics=hosts,nodes')
|
|
source.onopen = () => setLive(true)
|
|
source.onerror = () => setLive(false)
|
|
source.addEventListener('host', (event) => {
|
|
const data = JSON.parse((event as MessageEvent<string>).data) as { agent_id: string; online: boolean; host: Host }
|
|
setHosts((previous) => {
|
|
const next = new Map(previous)
|
|
const old = previous.get(data.agent_id)
|
|
next.set(data.agent_id, { agent_id: data.agent_id, name: old?.name ?? data.agent_id, online: data.online, stale: false, last_seen_at: data.host.at, host: data.host })
|
|
return next
|
|
})
|
|
})
|
|
source.addEventListener('node', (event) => {
|
|
const data = JSON.parse((event as MessageEvent<string>).data) as { id: string; online: boolean }
|
|
setHosts((previous) => {
|
|
const old = previous.get(data.id)
|
|
if (!old) return previous
|
|
const next = new Map(previous)
|
|
next.set(data.id, { ...old, online: data.online, stale: data.online ? old.stale : true })
|
|
return next
|
|
})
|
|
})
|
|
return () => {
|
|
alive = false
|
|
source.close()
|
|
}
|
|
}, [])
|
|
|
|
return { hosts, live }
|
|
}
|
|
|
|
export function fmtBytes(v: number | null | undefined): string {
|
|
if (v === null || v === undefined) return '—'
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
let value = v
|
|
let i = 0
|
|
while (value >= 1024 && i < units.length - 1) {
|
|
value /= 1024
|
|
i++
|
|
}
|
|
return `${value.toFixed(value >= 100 || i === 0 ? 0 : 2)} ${units[i]}`
|
|
}
|
|
|
|
export function fmtSpeed(v: number | null | undefined): string {
|
|
return v === null || v === undefined ? '—' : `${fmtBytes(v)}/s`
|
|
}
|
|
|
|
export function fmtUptime(seconds: number | null | undefined): string {
|
|
if (seconds === null || seconds === undefined) return '—'
|
|
const d = Math.floor(seconds / 86400)
|
|
const h = Math.floor((seconds % 86400) / 3600)
|
|
const m = Math.floor((seconds % 3600) / 60)
|
|
return d > 0 ? `${d} 天 ${h} 小时 ${m} 分` : h > 0 ? `${h} 小时 ${m} 分` : `${m} 分 ${Math.floor(seconds % 60)} 秒`
|
|
}
|
|
|
|
export const pct = (used: number | null, total: number | null): number | null => (used !== null && total !== null && total > 0 ? (100 * used) / total : null)
|