2023-11-07 15:13:44 +00:00
|
|
|
import React, { useMemo, useRef, useState } from 'react';
|
2023-10-14 17:35:20 +00:00
|
|
|
import {
|
|
|
|
Badge,
|
|
|
|
Button,
|
2023-11-07 15:13:44 +00:00
|
|
|
Divider,
|
2023-10-31 16:39:37 +00:00
|
|
|
Empty,
|
2023-10-14 17:35:20 +00:00
|
|
|
Modal,
|
2023-11-07 15:13:44 +00:00
|
|
|
Popconfirm,
|
2023-10-14 17:35:20 +00:00
|
|
|
Steps,
|
2023-10-31 16:39:37 +00:00
|
|
|
Switch,
|
2023-10-14 17:35:20 +00:00
|
|
|
Table,
|
2023-11-07 14:36:07 +00:00
|
|
|
Tabs,
|
2023-10-31 16:39:37 +00:00
|
|
|
Tooltip,
|
2023-10-14 17:35:20 +00:00
|
|
|
Typography,
|
|
|
|
} from 'antd';
|
2023-09-02 07:40:38 +00:00
|
|
|
import { ColumnsType } from 'antd/es/table';
|
|
|
|
import { PlusOutlined } from '@ant-design/icons';
|
2023-10-03 12:45:00 +00:00
|
|
|
import { ServerStatusInfo } from '../../types';
|
|
|
|
import { useSocketSubscribe } from '../api/socketio';
|
|
|
|
import { filesize } from 'filesize';
|
|
|
|
import prettyMilliseconds from 'pretty-ms';
|
2023-10-03 12:57:24 +00:00
|
|
|
import { UpDownCounter } from '../components/UpDownCounter';
|
2023-10-03 13:06:58 +00:00
|
|
|
import { max } from 'lodash-es';
|
|
|
|
import dayjs from 'dayjs';
|
2023-10-14 17:35:20 +00:00
|
|
|
import { useCurrentWorkspaceId } from '../store/user';
|
|
|
|
import { useWatch } from '../hooks/useWatch';
|
|
|
|
import { Loading } from '../components/Loading';
|
|
|
|
import { without } from 'lodash-es';
|
2023-10-31 16:39:37 +00:00
|
|
|
import { useIntervalUpdate } from '../hooks/useIntervalUpdate';
|
|
|
|
import clsx from 'clsx';
|
2023-11-07 15:13:44 +00:00
|
|
|
import { isServerOnline } from '../../shared';
|
|
|
|
import { defaultErrorHandler, trpc } from '../api/trpc';
|
|
|
|
import { useRequest } from '../hooks/useRequest';
|
2023-08-31 16:11:47 +00:00
|
|
|
|
|
|
|
export const Servers: React.FC = React.memo(() => {
|
2023-09-02 07:40:38 +00:00
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
2023-10-31 16:39:37 +00:00
|
|
|
const [hideOfflineServer, setHideOfflineServer] = useState(false);
|
2023-11-07 15:13:44 +00:00
|
|
|
const workspaceId = useCurrentWorkspaceId();
|
2023-09-02 07:40:38 +00:00
|
|
|
|
|
|
|
const handleOk = () => {
|
|
|
|
setIsModalOpen(false);
|
|
|
|
};
|
|
|
|
|
2023-11-07 15:13:44 +00:00
|
|
|
const clearOfflineNodeMutation =
|
|
|
|
trpc.serverStatus.clearOfflineServerStatus.useMutation({
|
|
|
|
onError: defaultErrorHandler,
|
|
|
|
});
|
|
|
|
|
|
|
|
const [{ loading }, handleClearOfflineNode] = useRequest(async (e) => {
|
|
|
|
await clearOfflineNodeMutation.mutateAsync({
|
|
|
|
workspaceId,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-09-02 07:40:38 +00:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div className="h-24 flex items-center">
|
|
|
|
<div className="text-2xl flex-1">Servers</div>
|
2023-10-31 16:39:37 +00:00
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
<div className="flex items-center gap-1 text-gray-500">
|
|
|
|
<Switch
|
|
|
|
checked={hideOfflineServer}
|
|
|
|
onChange={setHideOfflineServer}
|
|
|
|
/>
|
|
|
|
Hide Offline
|
|
|
|
</div>
|
|
|
|
|
2023-11-07 15:13:44 +00:00
|
|
|
<div>
|
|
|
|
<Popconfirm
|
|
|
|
title="Clear Offline Node"
|
|
|
|
description="Are you sure to clear all offline node?"
|
|
|
|
disabled={loading}
|
|
|
|
onConfirm={handleClearOfflineNode}
|
|
|
|
>
|
|
|
|
<Button size="large" loading={loading}>
|
|
|
|
Clear Offline
|
|
|
|
</Button>
|
|
|
|
</Popconfirm>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Divider type="vertical" />
|
|
|
|
|
2023-09-02 07:40:38 +00:00
|
|
|
<Button
|
|
|
|
type="primary"
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
size="large"
|
|
|
|
onClick={() => setIsModalOpen(true)}
|
|
|
|
>
|
|
|
|
Add Server
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2023-10-31 16:39:37 +00:00
|
|
|
<ServerList hideOfflineServer={hideOfflineServer} />
|
2023-09-02 07:40:38 +00:00
|
|
|
|
|
|
|
<Modal
|
|
|
|
title="Add Server"
|
|
|
|
open={isModalOpen}
|
2023-10-14 17:35:20 +00:00
|
|
|
destroyOnClose={true}
|
|
|
|
okText="Done"
|
2023-09-02 07:40:38 +00:00
|
|
|
onOk={handleOk}
|
|
|
|
onCancel={() => setIsModalOpen(false)}
|
|
|
|
>
|
2023-10-14 17:35:20 +00:00
|
|
|
<div>
|
2023-11-07 14:36:07 +00:00
|
|
|
<Tabs
|
|
|
|
items={[
|
|
|
|
{
|
2023-12-20 17:46:38 +00:00
|
|
|
key: 'auto',
|
|
|
|
label: 'Auto',
|
2023-11-07 14:36:07 +00:00
|
|
|
children: <InstallScript />,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: 'manual',
|
|
|
|
label: 'Manual',
|
|
|
|
children: <AddServerStep />,
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
/>
|
2023-10-14 17:35:20 +00:00
|
|
|
</div>
|
2023-09-02 07:40:38 +00:00
|
|
|
</Modal>
|
|
|
|
</div>
|
|
|
|
);
|
2023-08-31 16:11:47 +00:00
|
|
|
});
|
|
|
|
Servers.displayName = 'Servers';
|
2023-09-02 07:40:38 +00:00
|
|
|
|
2023-10-14 17:35:20 +00:00
|
|
|
function useServerMap(): Record<string, ServerStatusInfo> {
|
2023-10-03 12:45:00 +00:00
|
|
|
const serverMap = useSocketSubscribe<Record<string, ServerStatusInfo>>(
|
|
|
|
'onServerStatusUpdate',
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
2023-10-14 17:35:20 +00:00
|
|
|
return serverMap;
|
|
|
|
}
|
|
|
|
|
2023-10-31 16:39:37 +00:00
|
|
|
export const ServerList: React.FC<{
|
|
|
|
hideOfflineServer: boolean;
|
|
|
|
}> = React.memo((props) => {
|
2023-10-14 17:35:20 +00:00
|
|
|
const serverMap = useServerMap();
|
2023-10-31 16:39:37 +00:00
|
|
|
const inc = useIntervalUpdate(2 * 1000);
|
|
|
|
const { hideOfflineServer } = props;
|
|
|
|
|
|
|
|
const dataSource = useMemo(
|
|
|
|
() =>
|
|
|
|
Object.values(serverMap)
|
|
|
|
.sort((info) => (isServerOnline(info) ? -1 : 1))
|
|
|
|
.filter((info) => {
|
|
|
|
if (hideOfflineServer) {
|
|
|
|
return isServerOnline(info);
|
|
|
|
}
|
2023-10-14 17:35:20 +00:00
|
|
|
|
2023-10-31 16:39:37 +00:00
|
|
|
return true;
|
|
|
|
}), // make online server is up and offline is down
|
|
|
|
[serverMap, inc, hideOfflineServer]
|
|
|
|
);
|
2023-10-03 13:06:58 +00:00
|
|
|
const lastUpdatedAt = max(dataSource.map((d) => d.updatedAt));
|
2023-09-02 07:40:38 +00:00
|
|
|
|
2023-10-03 12:45:00 +00:00
|
|
|
const columns = useMemo((): ColumnsType<ServerStatusInfo> => {
|
2023-09-02 07:40:38 +00:00
|
|
|
return [
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
key: 'status',
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'Status',
|
2023-10-03 12:45:00 +00:00
|
|
|
render: (val, record) => {
|
2023-10-31 16:39:37 +00:00
|
|
|
return isServerOnline(record) ? (
|
2023-10-03 16:49:54 +00:00
|
|
|
<Badge status="success" text="online" />
|
|
|
|
) : (
|
2023-10-31 16:39:37 +00:00
|
|
|
<Tooltip
|
|
|
|
title={`Last online: ${dayjs(record.updatedAt).format(
|
|
|
|
'YYYY-MM-DD HH:mm:ss'
|
|
|
|
)}`}
|
|
|
|
>
|
|
|
|
<Badge status="error" text="offline" />
|
|
|
|
</Tooltip>
|
2023-10-03 16:49:54 +00:00
|
|
|
);
|
2023-10-03 12:45:00 +00:00
|
|
|
},
|
2023-09-02 07:40:38 +00:00
|
|
|
},
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
dataIndex: 'name',
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'Node Name',
|
|
|
|
},
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
dataIndex: 'hostname',
|
|
|
|
title: 'Host Name',
|
2023-09-02 07:40:38 +00:00
|
|
|
},
|
2023-11-07 15:13:44 +00:00
|
|
|
// {
|
|
|
|
// dataIndex: ['payload', 'system'],
|
|
|
|
// title: 'System',
|
|
|
|
// },
|
2023-10-03 12:45:00 +00:00
|
|
|
{
|
|
|
|
dataIndex: ['payload', 'uptime'],
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'Uptime',
|
2023-10-03 12:45:00 +00:00
|
|
|
render: (val) => prettyMilliseconds(Number(val) * 1000),
|
2023-09-02 07:40:38 +00:00
|
|
|
},
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
dataIndex: ['payload', 'load'],
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'Load',
|
|
|
|
},
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
key: 'nework',
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'Network',
|
2023-10-03 12:45:00 +00:00
|
|
|
render: (_, record) => {
|
2023-10-03 12:57:24 +00:00
|
|
|
return (
|
|
|
|
<UpDownCounter
|
|
|
|
up={filesize(record.payload.network_out)}
|
|
|
|
down={filesize(record.payload.network_in)}
|
|
|
|
/>
|
|
|
|
);
|
2023-10-03 12:45:00 +00:00
|
|
|
},
|
2023-09-02 07:40:38 +00:00
|
|
|
},
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
key: 'traffic',
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'Traffic',
|
2023-10-03 12:45:00 +00:00
|
|
|
render: (_, record) => {
|
2023-10-03 12:57:24 +00:00
|
|
|
return (
|
|
|
|
<UpDownCounter
|
|
|
|
up={filesize(record.payload.network_tx) + '/s'}
|
|
|
|
down={filesize(record.payload.network_rx) + '/s'}
|
|
|
|
/>
|
|
|
|
);
|
2023-10-03 12:45:00 +00:00
|
|
|
},
|
2023-09-02 07:40:38 +00:00
|
|
|
},
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
dataIndex: ['payload', 'cpu'],
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'cpu',
|
2023-10-03 12:45:00 +00:00
|
|
|
render: (val) => `${val}%`,
|
2023-09-02 07:40:38 +00:00
|
|
|
},
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
key: 'ram',
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'ram',
|
2023-10-03 12:45:00 +00:00
|
|
|
render: (_, record) => {
|
|
|
|
return `${filesize(record.payload.memory_used * 1000)} / ${filesize(
|
|
|
|
record.payload.memory_total * 1000
|
|
|
|
)}`;
|
|
|
|
},
|
2023-09-02 07:40:38 +00:00
|
|
|
},
|
|
|
|
{
|
2023-10-03 12:45:00 +00:00
|
|
|
key: 'hdd',
|
2023-09-02 07:40:38 +00:00
|
|
|
title: 'hdd',
|
2023-10-03 12:45:00 +00:00
|
|
|
render: (_, record) => {
|
2023-11-04 16:55:51 +00:00
|
|
|
return `${filesize(
|
|
|
|
record.payload.hdd_used * 1000 * 1000
|
|
|
|
)} / ${filesize(record.payload.hdd_total * 1000 * 1000)}`;
|
2023-10-03 12:45:00 +00:00
|
|
|
},
|
2023-09-02 07:40:38 +00:00
|
|
|
},
|
2023-11-15 14:26:45 +00:00
|
|
|
{
|
|
|
|
key: 'updatedAt',
|
|
|
|
title: 'updatedAt',
|
|
|
|
render: (val) => {
|
|
|
|
return dayjs(val).format('MMM D HH:mm:ss');
|
|
|
|
},
|
|
|
|
},
|
2023-09-02 07:40:38 +00:00
|
|
|
];
|
|
|
|
}, []);
|
|
|
|
|
2023-10-03 12:45:00 +00:00
|
|
|
return (
|
2023-10-03 13:06:58 +00:00
|
|
|
<div>
|
|
|
|
<div className="text-right text-sm opacity-80">
|
|
|
|
Last updated at: {dayjs(lastUpdatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
|
|
|
</div>
|
|
|
|
<Table
|
|
|
|
rowKey="hostname"
|
|
|
|
columns={columns}
|
|
|
|
dataSource={dataSource}
|
|
|
|
pagination={false}
|
2023-10-31 16:39:37 +00:00
|
|
|
locale={{ emptyText: <Empty description="No server online" /> }}
|
2023-10-31 16:44:01 +00:00
|
|
|
rowClassName={(record) => clsx(!isServerOnline(record) && 'opacity-60')}
|
2023-10-03 13:06:58 +00:00
|
|
|
/>
|
|
|
|
</div>
|
2023-10-03 12:45:00 +00:00
|
|
|
);
|
2023-09-02 07:40:38 +00:00
|
|
|
});
|
|
|
|
ServerList.displayName = 'ServerList';
|
2023-10-14 17:35:20 +00:00
|
|
|
|
2023-11-07 14:36:07 +00:00
|
|
|
export const InstallScript: React.FC = React.memo(() => {
|
|
|
|
const workspaceId = useCurrentWorkspaceId();
|
|
|
|
const command = `curl -o- ${window.location.origin}/serverStatus/${workspaceId}/install.sh | bash`;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<div>Run this command in your linux machine</div>
|
|
|
|
|
|
|
|
<Typography.Paragraph
|
|
|
|
copyable={{
|
|
|
|
format: 'text/plain',
|
|
|
|
text: command,
|
|
|
|
}}
|
|
|
|
className="h-[96px] flex p-2 rounded bg-black bg-opacity-5 border border-black border-opacity-10 overflow-auto"
|
|
|
|
>
|
|
|
|
<span>{command}</span>
|
|
|
|
</Typography.Paragraph>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2023-10-14 17:35:20 +00:00
|
|
|
export const AddServerStep: React.FC = React.memo(() => {
|
|
|
|
const workspaceId = useCurrentWorkspaceId();
|
|
|
|
const [current, setCurrent] = useState(0);
|
|
|
|
const serverMap = useServerMap();
|
|
|
|
const [checking, setChecking] = useState(false);
|
|
|
|
const oldServerMapNames = useRef<string[]>([]);
|
|
|
|
const [diffServerNames, setDiffServerNames] = useState<string[]>([]);
|
|
|
|
|
|
|
|
const allServerNames = useMemo(() => Object.keys(serverMap), [serverMap]);
|
|
|
|
|
|
|
|
useWatch([checking], () => {
|
|
|
|
if (checking === true) {
|
|
|
|
oldServerMapNames.current = [...allServerNames];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
useWatch([allServerNames], () => {
|
|
|
|
if (checking === true) {
|
|
|
|
setDiffServerNames(without(allServerNames, ...oldServerMapNames.current));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-11-04 16:55:51 +00:00
|
|
|
const command = `./tianji-reporter --url ${window.location.origin} --workspace ${workspaceId}`;
|
|
|
|
|
2023-10-14 17:35:20 +00:00
|
|
|
return (
|
|
|
|
<Steps
|
|
|
|
direction="vertical"
|
|
|
|
current={current}
|
|
|
|
items={[
|
|
|
|
{
|
|
|
|
title: 'Download Client Reportor',
|
|
|
|
description: (
|
|
|
|
<div>
|
|
|
|
Download reporter from{' '}
|
|
|
|
<Typography.Link
|
|
|
|
href="https://github.com/msgbyte/tianji/releases"
|
|
|
|
target="_blank"
|
|
|
|
onClick={() => {
|
|
|
|
if (current === 0) {
|
|
|
|
setCurrent(1);
|
|
|
|
setChecking(true);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Releases Page
|
|
|
|
</Typography.Link>
|
|
|
|
</div>
|
|
|
|
),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'Run',
|
|
|
|
description: (
|
|
|
|
<div>
|
2023-11-04 16:55:51 +00:00
|
|
|
run reporter with:{' '}
|
2023-11-07 14:36:07 +00:00
|
|
|
<Typography.Text
|
|
|
|
code={true}
|
|
|
|
copyable={{ format: 'text/plain', text: command }}
|
|
|
|
>
|
2023-11-04 16:55:51 +00:00
|
|
|
{command}
|
2023-10-14 17:35:20 +00:00
|
|
|
</Typography.Text>
|
|
|
|
<Button
|
|
|
|
type="link"
|
|
|
|
size="small"
|
|
|
|
disabled={current !== 1}
|
|
|
|
onClick={() => {
|
|
|
|
if (current === 1) {
|
|
|
|
setCurrent(2);
|
|
|
|
setChecking(true);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Next step
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'Waiting for receive UDP pack',
|
|
|
|
description: (
|
|
|
|
<div>
|
|
|
|
{diffServerNames.length === 0 || checking === false ? (
|
|
|
|
<Loading />
|
|
|
|
) : (
|
|
|
|
<div>
|
|
|
|
Is this your servers?
|
|
|
|
{diffServerNames.map((n) => (
|
|
|
|
<div key={n}>- {n}</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
),
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
AddServerStep.displayName = 'AddServerStep';
|