feat: server status list display

This commit is contained in:
moonrailgun 2023-10-03 20:45:00 +08:00
parent 46c83fcb01
commit d3a5654e8e
8 changed files with 190 additions and 37 deletions

View File

@ -36,6 +36,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
"filesize": "^10.0.12",
"is-localhost-ip": "^2.0.0", "is-localhost-ip": "^2.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -47,6 +48,7 @@
"openbadge": "^1.0.4", "openbadge": "^1.0.4",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"pretty-ms": "7.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router": "^6.15.0", "react-router": "^6.15.0",

View File

@ -70,6 +70,9 @@ dependencies:
express-validator: express-validator:
specifier: ^7.0.1 specifier: ^7.0.1
version: 7.0.1 version: 7.0.1
filesize:
specifier: ^10.0.12
version: 10.0.12
is-localhost-ip: is-localhost-ip:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
@ -103,6 +106,9 @@ dependencies:
passport-jwt: passport-jwt:
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
pretty-ms:
specifier: 7.0.1
version: 7.0.1
react: react:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0 version: 18.2.0
@ -3258,6 +3264,11 @@ packages:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
dev: false dev: false
/filesize@10.0.12:
resolution: {integrity: sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==}
engines: {node: '>= 10.4.0'}
dev: false
/fill-range@7.0.1: /fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -4299,6 +4310,11 @@ packages:
tiny-inflate: 1.0.3 tiny-inflate: 1.0.3
dev: false dev: false
/parse-ms@2.1.0:
resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
engines: {node: '>=6'}
dev: false
/parseurl@1.3.3: /parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -4463,6 +4479,13 @@ packages:
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
dev: false 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: /prisma@5.2.0:
resolution: {integrity: sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==} resolution: {integrity: sha512-FfFlpjVCkZwrqxDnP4smlNYSH1so+CbfjgdpioFzGGqlQAEm6VHAYSzV7jJgC3ebtY9dNOhDMS2+4/1DDSM7bQ==}
engines: {node: '>=16.13'} engines: {node: '>=16.13'}

View File

@ -3,7 +3,7 @@ import { getJWT } from './auth';
import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared'; import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared';
import { create } from 'zustand'; import { create } from 'zustand';
import { useEvent } from '../hooks/useEvent'; import { useEvent } from '../hooks/useEvent';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
const useSocketStore = create<{ const useSocketStore = create<{
socket: Socket | null; socket: Socket | null;
@ -78,7 +78,6 @@ export function useSocket() {
}; };
Promise.resolve(p).then((cursor) => { Promise.resolve(p).then((cursor) => {
console.log('aaa');
socket.on(`${name}#${cursor}` as string, receiveDataUpdate); socket.on(`${name}#${cursor}` as string, receiveDataUpdate);
}); });
@ -89,12 +88,16 @@ export function useSocket() {
return { emit, subscribe }; return { emit, subscribe };
} }
export function useSocketSubscribe<T extends keyof SubscribeEventMap>( export function useSocketSubscribe<T>(
name: T, name: keyof SubscribeEventMap,
onData: (data: SubscribeEventData<T>) => void defaultData: T
) { ): T {
const { subscribe } = useSocket(); const { subscribe } = useSocket();
const cb = useEvent(onData); const [data, setData] = useState<T>(defaultData);
const cb = useEvent((_data) => {
setData(_data);
});
useEffect(() => { useEffect(() => {
const unsubscribe = subscribe(name, cb); const unsubscribe = subscribe(name, cb);
@ -103,4 +106,6 @@ export function useSocketSubscribe<T extends keyof SubscribeEventMap>(
unsubscribe(); unsubscribe();
}; };
}, [name]); }, [name]);
return data;
} }

View File

@ -2,6 +2,10 @@ import React, { useMemo, useState } from 'react';
import { Button, Form, Input, Modal, Table } from 'antd'; import { Button, Form, Input, Modal, Table } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import { PlusOutlined } from '@ant-design/icons'; 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(() => { export const Servers: React.FC = React.memo(() => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@ -60,67 +64,98 @@ interface ServerInfoRecordType {
} }
export const ServerList: React.FC = React.memo(() => { export const ServerList: React.FC = React.memo(() => {
const dataSource: ServerInfoRecordType[] = [ const serverMap = useSocketSubscribe<Record<string, ServerStatusInfo>>(
{ 'onServerStatusUpdate',
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 columns = useMemo((): ColumnsType<ServerInfoRecordType> => { const dataSource = Object.values(serverMap);
console.log('dataSource', dataSource);
const columns = useMemo((): ColumnsType<ServerStatusInfo> => {
return [ return [
{ {
dataIndex: 'status', key: 'status',
title: 'Status', title: 'Status',
render: (val, record) => {
return Date.now() - (record.updatedAt + record.timeout) < 0
? 'online'
: 'offline';
},
}, },
{ {
dataIndex: 'nodeName', dataIndex: 'name',
title: 'Node Name', title: 'Node Name',
}, },
{ {
dataIndex: 'type', dataIndex: 'hostname',
title: 'Type', title: 'Host Name',
}, },
{ {
dataIndex: 'uptime', dataIndex: ['payload', 'system'],
title: 'System',
},
{
dataIndex: ['payload', 'uptime'],
title: 'Uptime', title: 'Uptime',
render: (val) => prettyMilliseconds(Number(val) * 1000),
}, },
{ {
dataIndex: 'load', dataIndex: ['payload', 'load'],
title: 'Load', title: 'Load',
}, },
{ {
dataIndex: 'network', key: 'nework',
title: 'Network', title: 'Network',
render: (_, record) => {
return `${filesize(record.payload.network_in)} | ${filesize(
record.payload.network_out
)}`;
},
}, },
{ {
dataIndex: 'traffic', key: 'traffic',
title: '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', title: 'cpu',
render: (val) => `${val}%`,
}, },
{ {
dataIndex: 'ram', key: 'ram',
title: 'ram', title: 'ram',
render: (_, record) => {
return `${filesize(record.payload.memory_used * 1000)} / ${filesize(
record.payload.memory_total * 1000
)}`;
},
}, },
{ {
dataIndex: 'hdd', key: 'hdd',
title: 'hdd', title: 'hdd',
render: (_, record) => {
return `${filesize(record.payload.hdd_used * 1000)} / ${filesize(
record.payload.hdd_total * 1000
)}`;
},
}, },
]; ];
}, []); }, []);
return <Table columns={columns} dataSource={dataSource} pagination={false} />; return (
<Table
rowKey="hostname"
columns={columns}
dataSource={dataSource}
pagination={false}
/>
);
}); });
ServerList.displayName = 'ServerList'; ServerList.displayName = 'ServerList';

View File

@ -1,4 +1,18 @@
import dgram from 'dgram'; 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) { export function initUdpServer(port: number) {
const server = dgram.createSocket('udp4'); const server = dgram.createSocket('udp4');
@ -12,7 +26,7 @@ export function initUdpServer(port: number) {
try { try {
const raw = String(msg); const raw = String(msg);
const json = JSON.parse(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) { if (!workspaceId || !name || !hostname) {
console.warn( console.warn(
@ -22,6 +36,25 @@ export function initUdpServer(port: number) {
} }
console.log('recevice tianji report:', raw, 'info', rinfo); 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) {} } catch (err) {}
}); });

View File

@ -1,10 +1,11 @@
import { EventEmitter } from 'eventemitter-strict'; import { EventEmitter } from 'eventemitter-strict';
import { Socket } from 'socket.io'; import { Socket } from 'socket.io';
import { ServerStatusInfo } from '../../types';
type SubscribeEventFn<T> = (workspaceId: string, eventData: T) => void; type SubscribeEventFn<T> = (workspaceId: string, eventData: T) => void;
export interface SubscribeEventMap { export interface SubscribeEventMap {
__test: SubscribeEventFn<number>; onServerStatusUpdate: SubscribeEventFn<Record<string, ServerStatusInfo>>;
} }
type SocketEventFn<T, U = unknown> = ( type SocketEventFn<T, U = unknown> = (
@ -25,6 +26,19 @@ export interface SocketEventMap {
export const socketEventBus = new EventEmitter<SocketEventMap>(); export const socketEventBus = new EventEmitter<SocketEventMap>();
export const subscribeEventBus = new EventEmitter<SubscribeEventMap>(); export const subscribeEventBus = new EventEmitter<SubscribeEventMap>();
type SubscribeInitializerFn<
T extends keyof SubscribeEventMap = keyof SubscribeEventMap
> = (
workspaceId: string,
socket: Socket
) =>
| Parameters<SubscribeEventMap[T]>[1]
| Promise<Parameters<SubscribeEventMap[T]>[1]>;
const subscribeInitializerList: [
keyof SubscribeEventMap,
SubscribeInitializerFn
][] = [];
let i = 0; let i = 0;
const subscribeFnMap: Record<string, SubscribeEventFn<any>> = {}; const subscribeFnMap: Record<string, SubscribeEventFn<any>> = {};
socketEventBus.on('$subscribe', (eventData, socket, callback) => { socketEventBus.on('$subscribe', (eventData, socket, callback) => {
@ -42,6 +56,12 @@ socketEventBus.on('$subscribe', (eventData, socket, callback) => {
subscribeFnMap[`${name}#${cursor}`] = fn; subscribeFnMap[`${name}#${cursor}`] = fn;
subscribeInitializerList.forEach(async ([_name, initializer]) => {
if (_name === name) {
socket.emit(`${name}#${cursor}`, await initializer(_workspaceId, socket));
}
});
callback(cursor); callback(cursor);
}); });
socketEventBus.on('$unsubscribe', (eventData, socket, callback) => { socketEventBus.on('$unsubscribe', (eventData, socket, callback) => {
@ -53,3 +73,13 @@ socketEventBus.on('$unsubscribe', (eventData, socket, callback) => {
subscribeEventBus.off(name, fn); subscribeEventBus.off(name, fn);
} }
}); });
/**
* Listen for subscribed requests and return results immediately
*/
export function createSubscribeInitializer<T extends keyof SubscribeEventMap>(
name: T,
initializer: SubscribeInitializerFn
) {
subscribeInitializerList.push([name, initializer]);
}

1
src/types/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './server';

24
src/types/server.ts Normal file
View File

@ -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;
}