feat: server status list display
This commit is contained in:
parent
46c83fcb01
commit
d3a5654e8e
@ -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",
|
||||||
|
@ -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'}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
@ -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) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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
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