chore: initial project import
This commit is contained in:
commit
3fc4e5ca53
347 files changed
+56172
No files matched your search
@@ -0,0 +1,100 @@
|
||||
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)
|
||||
Reference in new issue
Block a user