From d3a5654e8ea5aea63eaccf26fcbcb0daff97bf96 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Tue, 3 Oct 2023 20:45:00 +0800 Subject: [PATCH] feat: server status list display --- package.json | 2 + pnpm-lock.yaml | 23 +++++++++ src/client/api/socketio.ts | 19 +++++--- src/client/pages/Servers.tsx | 91 +++++++++++++++++++++++++----------- src/server/udp/server.ts | 35 +++++++++++++- src/server/ws/shared.ts | 32 ++++++++++++- src/types/index.ts | 1 + src/types/server.ts | 24 ++++++++++ 8 files changed, 190 insertions(+), 37 deletions(-) create mode 100644 src/types/index.ts create mode 100644 src/types/server.ts diff --git a/package.json b/package.json index e4a8ac4..6f6cbfc 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "express": "^4.18.2", "express-async-errors": "^3.1.1", "express-validator": "^7.0.1", + "filesize": "^10.0.12", "is-localhost-ip": "^2.0.0", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", @@ -47,6 +48,7 @@ "openbadge": "^1.0.4", "passport": "^0.6.0", "passport-jwt": "^4.0.1", + "pretty-ms": "7.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router": "^6.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54797c6..a91bd23 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ dependencies: express-validator: specifier: ^7.0.1 version: 7.0.1 + filesize: + specifier: ^10.0.12 + version: 10.0.12 is-localhost-ip: specifier: ^2.0.0 version: 2.0.0 @@ -103,6 +106,9 @@ dependencies: passport-jwt: specifier: ^4.0.1 version: 4.0.1 + pretty-ms: + specifier: 7.0.1 + version: 7.0.1 react: specifier: ^18.2.0 version: 18.2.0 @@ -3258,6 +3264,11 @@ packages: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} dev: false + /filesize@10.0.12: + resolution: {integrity: sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==} + engines: {node: '>= 10.4.0'} + dev: false + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -4299,6 +4310,11 @@ packages: tiny-inflate: 1.0.3 dev: false + /parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + dev: false + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4463,6 +4479,13 @@ packages: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} dev: false + /pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + dependencies: + parse-ms: 2.1.0 + dev: false + /prisma@5.2.0: resolution: {integrity: sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==} engines: {node: '>=16.13'} diff --git a/src/client/api/socketio.ts b/src/client/api/socketio.ts index 2abef5a..ac7f81a 100644 --- a/src/client/api/socketio.ts +++ b/src/client/api/socketio.ts @@ -3,7 +3,7 @@ import { getJWT } from './auth'; import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared'; import { create } from 'zustand'; import { useEvent } from '../hooks/useEvent'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; const useSocketStore = create<{ socket: Socket | null; @@ -78,7 +78,6 @@ export function useSocket() { }; Promise.resolve(p).then((cursor) => { - console.log('aaa'); socket.on(`${name}#${cursor}` as string, receiveDataUpdate); }); @@ -89,12 +88,16 @@ export function useSocket() { return { emit, subscribe }; } -export function useSocketSubscribe( - name: T, - onData: (data: SubscribeEventData) => void -) { +export function useSocketSubscribe( + name: keyof SubscribeEventMap, + defaultData: T +): T { const { subscribe } = useSocket(); - const cb = useEvent(onData); + const [data, setData] = useState(defaultData); + + const cb = useEvent((_data) => { + setData(_data); + }); useEffect(() => { const unsubscribe = subscribe(name, cb); @@ -103,4 +106,6 @@ export function useSocketSubscribe( unsubscribe(); }; }, [name]); + + return data; } diff --git a/src/client/pages/Servers.tsx b/src/client/pages/Servers.tsx index 5dda47e..720bc1a 100644 --- a/src/client/pages/Servers.tsx +++ b/src/client/pages/Servers.tsx @@ -2,6 +2,10 @@ import React, { useMemo, useState } from 'react'; import { Button, Form, Input, Modal, Table } from 'antd'; import { ColumnsType } from 'antd/es/table'; import { PlusOutlined } from '@ant-design/icons'; +import { ServerStatusInfo } from '../../types'; +import { useSocketSubscribe } from '../api/socketio'; +import { filesize } from 'filesize'; +import prettyMilliseconds from 'pretty-ms'; export const Servers: React.FC = React.memo(() => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -60,67 +64,98 @@ interface ServerInfoRecordType { } export const ServerList: React.FC = React.memo(() => { - const dataSource: ServerInfoRecordType[] = [ - { - status: 'online', - nodeName: 'node1', - type: 'KVM', - location: 'Chicago', - uptime: 82989, - load: 0.2, - network: '82.9K | 89.3K', - traffic: '21.6G | 18.3G', - cpu: '5%', - ram: '1G/2G', - hdd: '25G/30G', - }, - ]; + const serverMap = useSocketSubscribe>( + 'onServerStatusUpdate', + {} + ); - const columns = useMemo((): ColumnsType => { + const dataSource = Object.values(serverMap); + + console.log('dataSource', dataSource); + + const columns = useMemo((): ColumnsType => { return [ { - dataIndex: 'status', + key: 'status', title: 'Status', + render: (val, record) => { + return Date.now() - (record.updatedAt + record.timeout) < 0 + ? 'online' + : 'offline'; + }, }, { - dataIndex: 'nodeName', + dataIndex: 'name', title: 'Node Name', }, { - dataIndex: 'type', - title: 'Type', + dataIndex: 'hostname', + title: 'Host Name', }, { - dataIndex: 'uptime', + dataIndex: ['payload', 'system'], + title: 'System', + }, + { + dataIndex: ['payload', 'uptime'], title: 'Uptime', + render: (val) => prettyMilliseconds(Number(val) * 1000), }, { - dataIndex: 'load', + dataIndex: ['payload', 'load'], title: 'Load', }, { - dataIndex: 'network', + key: 'nework', title: 'Network', + render: (_, record) => { + return `${filesize(record.payload.network_in)} | ${filesize( + record.payload.network_out + )}`; + }, }, { - dataIndex: 'traffic', + key: 'traffic', title: 'Traffic', + render: (_, record) => { + return `${filesize(record.payload.network_rx)}/s | ${filesize( + record.payload.network_tx + )}/s`; + }, }, { - dataIndex: 'cpu', + dataIndex: ['payload', 'cpu'], title: 'cpu', + render: (val) => `${val}%`, }, { - dataIndex: 'ram', + key: 'ram', title: 'ram', + render: (_, record) => { + return `${filesize(record.payload.memory_used * 1000)} / ${filesize( + record.payload.memory_total * 1000 + )}`; + }, }, { - dataIndex: 'hdd', + key: 'hdd', title: 'hdd', + render: (_, record) => { + return `${filesize(record.payload.hdd_used * 1000)} / ${filesize( + record.payload.hdd_total * 1000 + )}`; + }, }, ]; }, []); - return ; + return ( +
+ ); }); ServerList.displayName = 'ServerList'; diff --git a/src/server/udp/server.ts b/src/server/udp/server.ts index 904dc3e..4b0f5be 100644 --- a/src/server/udp/server.ts +++ b/src/server/udp/server.ts @@ -1,4 +1,18 @@ import dgram from 'dgram'; +import type { ServerStatusInfo } from '../../types'; +import { createSubscribeInitializer, subscribeEventBus } from '../ws/shared'; + +const serverMap: Record< + string, // workspaceId + Record< + string, // nodeName or hostname + ServerStatusInfo + > +> = {}; + +createSubscribeInitializer('onServerStatusUpdate', (workspaceId) => { + return serverMap[workspaceId]; +}); export function initUdpServer(port: number) { const server = dgram.createSocket('udp4'); @@ -12,7 +26,7 @@ export function initUdpServer(port: number) { try { const raw = String(msg); const json = JSON.parse(String(msg)); - const { workspaceId, name, hostname, payload } = json; + const { workspaceId, name, hostname, timeout, payload } = json; if (!workspaceId || !name || !hostname) { console.warn( @@ -22,6 +36,25 @@ export function initUdpServer(port: number) { } console.log('recevice tianji report:', raw, 'info', rinfo); + + if (!serverMap[workspaceId]) { + serverMap[workspaceId] = {}; + } + + serverMap[workspaceId][name || hostname] = { + workspaceId, + name, + hostname, + timeout, + updatedAt: Date.now(), + payload, + }; + + subscribeEventBus.emit( + 'onServerStatusUpdate', + workspaceId, + serverMap[workspaceId] + ); } catch (err) {} }); diff --git a/src/server/ws/shared.ts b/src/server/ws/shared.ts index fd70a0a..3e4e632 100644 --- a/src/server/ws/shared.ts +++ b/src/server/ws/shared.ts @@ -1,10 +1,11 @@ import { EventEmitter } from 'eventemitter-strict'; import { Socket } from 'socket.io'; +import { ServerStatusInfo } from '../../types'; type SubscribeEventFn = (workspaceId: string, eventData: T) => void; export interface SubscribeEventMap { - __test: SubscribeEventFn; + onServerStatusUpdate: SubscribeEventFn>; } type SocketEventFn = ( @@ -25,6 +26,19 @@ export interface SocketEventMap { export const socketEventBus = new EventEmitter(); export const subscribeEventBus = new EventEmitter(); +type SubscribeInitializerFn< + T extends keyof SubscribeEventMap = keyof SubscribeEventMap +> = ( + workspaceId: string, + socket: Socket +) => + | Parameters[1] + | Promise[1]>; +const subscribeInitializerList: [ + keyof SubscribeEventMap, + SubscribeInitializerFn +][] = []; + let i = 0; const subscribeFnMap: Record> = {}; socketEventBus.on('$subscribe', (eventData, socket, callback) => { @@ -42,6 +56,12 @@ socketEventBus.on('$subscribe', (eventData, socket, callback) => { subscribeFnMap[`${name}#${cursor}`] = fn; + subscribeInitializerList.forEach(async ([_name, initializer]) => { + if (_name === name) { + socket.emit(`${name}#${cursor}`, await initializer(_workspaceId, socket)); + } + }); + callback(cursor); }); socketEventBus.on('$unsubscribe', (eventData, socket, callback) => { @@ -53,3 +73,13 @@ socketEventBus.on('$unsubscribe', (eventData, socket, callback) => { subscribeEventBus.off(name, fn); } }); + +/** + * Listen for subscribed requests and return results immediately + */ +export function createSubscribeInitializer( + name: T, + initializer: SubscribeInitializerFn +) { + subscribeInitializerList.push([name, initializer]); +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..0ce5251 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1 @@ +export * from './server'; diff --git a/src/types/server.ts b/src/types/server.ts new file mode 100644 index 0000000..9ac4bbb --- /dev/null +++ b/src/types/server.ts @@ -0,0 +1,24 @@ +export interface ServerStatusInfo { + workspaceId: string; + name: string; + hostname: string; + timeout: number; + updatedAt: number; + payload: ServerStatusInfoPayload; +} + +export interface ServerStatusInfoPayload { + uptime: number; + load: number; + memory_total: number; + memory_used: number; + swap_total: number; + swap_used: number; + hdd_total: number; + hdd_used: number; + cpu: number; + network_tx: number; + network_rx: number; + network_in: number; + network_out: number; +}