feat: server status list display
This commit is contained in:
parent
46c83fcb01
commit
d3a5654e8e
@ -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",
|
||||
|
@ -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'}
|
||||
|
@ -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<T extends keyof SubscribeEventMap>(
|
||||
name: T,
|
||||
onData: (data: SubscribeEventData<T>) => void
|
||||
) {
|
||||
export function useSocketSubscribe<T>(
|
||||
name: keyof SubscribeEventMap,
|
||||
defaultData: T
|
||||
): T {
|
||||
const { subscribe } = useSocket();
|
||||
const cb = useEvent(onData);
|
||||
const [data, setData] = useState<T>(defaultData);
|
||||
|
||||
const cb = useEvent((_data) => {
|
||||
setData(_data);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribe(name, cb);
|
||||
@ -103,4 +106,6 @@ export function useSocketSubscribe<T extends keyof SubscribeEventMap>(
|
||||
unsubscribe();
|
||||
};
|
||||
}, [name]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
@ -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<Record<string, ServerStatusInfo>>(
|
||||
'onServerStatusUpdate',
|
||||
{}
|
||||
);
|
||||
|
||||
const columns = useMemo((): ColumnsType<ServerInfoRecordType> => {
|
||||
const dataSource = Object.values(serverMap);
|
||||
|
||||
console.log('dataSource', dataSource);
|
||||
|
||||
const columns = useMemo((): ColumnsType<ServerStatusInfo> => {
|
||||
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 <Table columns={columns} dataSource={dataSource} pagination={false} />;
|
||||
return (
|
||||
<Table
|
||||
rowKey="hostname"
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
pagination={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
ServerList.displayName = 'ServerList';
|
||||
|
@ -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) {}
|
||||
});
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { EventEmitter } from 'eventemitter-strict';
|
||||
import { Socket } from 'socket.io';
|
||||
import { ServerStatusInfo } from '../../types';
|
||||
|
||||
type SubscribeEventFn<T> = (workspaceId: string, eventData: T) => void;
|
||||
|
||||
export interface SubscribeEventMap {
|
||||
__test: SubscribeEventFn<number>;
|
||||
onServerStatusUpdate: SubscribeEventFn<Record<string, ServerStatusInfo>>;
|
||||
}
|
||||
|
||||
type SocketEventFn<T, U = unknown> = (
|
||||
@ -25,6 +26,19 @@ export interface SocketEventMap {
|
||||
export const socketEventBus = new EventEmitter<SocketEventMap>();
|
||||
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;
|
||||
const subscribeFnMap: Record<string, SubscribeEventFn<any>> = {};
|
||||
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<T extends keyof SubscribeEventMap>(
|
||||
name: T,
|
||||
initializer: SubscribeInitializerFn
|
||||
) {
|
||||
subscribeInitializerList.push([name, initializer]);
|
||||
}
|
||||
|
1
src/types/index.ts
Normal file
1
src/types/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './server';
|
24
src/types/server.ts
Normal file
24
src/types/server.ts
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user