mirror of https://github.com/msgbyte/tailchat
feat(admin-next): add custom routes
- SystemConfig - TailchatNetwork - SocketIOAdminpull/90/head
parent
d68108b21a
commit
ef88363bbf
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { styled, Tag } from 'tushan';
|
||||
|
||||
const Root = styled.div`
|
||||
* {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TagItems: React.FC<{
|
||||
items: string[];
|
||||
}> = React.memo((props) => {
|
||||
return (
|
||||
<Root>
|
||||
{props.items.map((item, i) => (
|
||||
<Tag key={i}>{item}</Tag>
|
||||
))}
|
||||
</Root>
|
||||
);
|
||||
});
|
||||
TagItems.displayName = 'TagItems';
|
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { parseUrlStr } from '../utils';
|
||||
import { Image as OriginImage, ImageProps, styled } from 'tushan';
|
||||
|
||||
const Image = styled(OriginImage)`
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TailchatImage: React.FC<ImageProps> = React.memo((props) => {
|
||||
return <Image {...props} src={parseUrlStr(props.src ?? '')} />;
|
||||
});
|
||||
TailchatImage.displayName = 'TailchatImage';
|
@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { request } from '../../request';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import { TagItems } from '../../components/TagItems';
|
||||
import { Card, Spin, Table, Typography, useAsync } from 'tushan';
|
||||
|
||||
/**
|
||||
* Tailchat 网络状态
|
||||
*/
|
||||
export const TailchatNetwork: React.FC = React.memo(() => {
|
||||
const { value: data, loading } = useAsync(async () => {
|
||||
const { data } = await request('/network/all');
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Typography.Title heading={6}>
|
||||
{'custom.network.nodeList'}
|
||||
</Typography.Title>
|
||||
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
dataIndex: 'id',
|
||||
title: 'ID',
|
||||
render: (id, item: any) => (
|
||||
<div>
|
||||
{id}
|
||||
{item.local && <span> (*)</span>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'hostname',
|
||||
title: 'Host',
|
||||
},
|
||||
{
|
||||
dataIndex: 'cpu',
|
||||
title: 'CPU',
|
||||
render: (usage) => usage + '%',
|
||||
},
|
||||
{
|
||||
dataIndex: 'ipList',
|
||||
title: 'IP',
|
||||
render: (ips) => <TagItems items={ips ?? []} />,
|
||||
},
|
||||
{
|
||||
dataIndex: 'client.version',
|
||||
title: 'Client Version',
|
||||
},
|
||||
]}
|
||||
data={data.nodes ?? []}
|
||||
/>
|
||||
|
||||
<Typography.Title heading={6}>
|
||||
{'custom.network.serviceList'}
|
||||
</Typography.Title>
|
||||
|
||||
<div>
|
||||
<TagItems items={_uniq<string>(data.services ?? [])} />
|
||||
</div>
|
||||
|
||||
<Typography.Title heading={6}>
|
||||
{'custom.network.actionList'}
|
||||
</Typography.Title>
|
||||
<div>
|
||||
<TagItems items={_uniq<string>(data.actions ?? [])} />
|
||||
</div>
|
||||
|
||||
<Typography.Title heading={6}>
|
||||
{'custom.network.eventList'}
|
||||
</Typography.Title>
|
||||
|
||||
<div>
|
||||
<TagItems items={_uniq<string>(data.events ?? [])} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
TailchatNetwork.displayName = 'TailchatNetwork';
|
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Button, Card, Typography } from 'tushan';
|
||||
|
||||
/**
|
||||
* SocketIO 管理
|
||||
*/
|
||||
export const SocketIOAdmin: React.FC = React.memo(() => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div>
|
||||
<Typography.Paragraph>
|
||||
{'custom.socketio.tip1'}{' '}
|
||||
<strong>
|
||||
{protocol}://{window.location.host}
|
||||
</strong>
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph>{'custom.socketio.tip2'}</Typography.Paragraph>
|
||||
<Typography.Paragraph>{'custom.socketio.tip3'}</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
window.open('https://admin.socket.io/');
|
||||
}}
|
||||
>
|
||||
{'custom.socketio.btn'}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
SocketIOAdmin.displayName = 'SocketIOAdmin';
|
@ -0,0 +1,147 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { request } from '../../request';
|
||||
import {
|
||||
useAsyncRequest,
|
||||
useEditValue,
|
||||
Button,
|
||||
Input,
|
||||
Spin,
|
||||
Message,
|
||||
Form,
|
||||
Upload,
|
||||
} from 'tushan';
|
||||
import { IconCheck, IconClose, IconDelete } from 'tushan/icon';
|
||||
import { TailchatImage } from '../../components/TailchatImage';
|
||||
|
||||
/**
|
||||
* Tailchat 系统设置
|
||||
*/
|
||||
export const SystemConfig: React.FC = React.memo(() => {
|
||||
const [{ value: config = {}, loading }, fetchConfig] = useAsyncRequest(
|
||||
async () => {
|
||||
const { data } = await request.get('/config/client');
|
||||
|
||||
return data.config ?? {};
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
const [serverName, setServerName, saveServerName] = useEditValue(
|
||||
config?.serverName,
|
||||
async (val) => {
|
||||
if (val === config?.serverName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await request.patch('/config/client', {
|
||||
key: 'serverName',
|
||||
value: val,
|
||||
});
|
||||
fetchConfig();
|
||||
Message.success('Success');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
Message.error(String(err));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const [{}, handleChangeServerEntryImage] = useAsyncRequest(
|
||||
async (file: File | null) => {
|
||||
if (file) {
|
||||
const formdata = new FormData();
|
||||
formdata.append('file', file);
|
||||
|
||||
const { data } = await request.put('/file/upload', formdata, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
|
||||
const fileInfo = data.files[0];
|
||||
|
||||
if (!fileInfo) {
|
||||
throw new Error('not get file');
|
||||
}
|
||||
|
||||
const url = fileInfo.url;
|
||||
await request.patch('/config/client', {
|
||||
key: 'serverEntryImage',
|
||||
value: url,
|
||||
});
|
||||
fetchConfig();
|
||||
} else {
|
||||
// delete
|
||||
await request.patch('/config/client', {
|
||||
key: 'serverEntryImage',
|
||||
value: '',
|
||||
});
|
||||
fetchConfig();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Form.Item label={'custom.config.uploadFileLimit'}>
|
||||
{config.uploadFileLimit}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={'custom.config.emailVerification'}>
|
||||
{config.emailVerification ? <IconCheck /> : <IconClose />}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={'custom.config.serverName'}>
|
||||
<Input
|
||||
value={serverName}
|
||||
onChange={(val) => setServerName(val)}
|
||||
onBlur={() => saveServerName()}
|
||||
placeholder="Tailchat"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={'custom.config.serverEntryImage'}>
|
||||
<div>
|
||||
{config?.serverEntryImage ? (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div>
|
||||
<TailchatImage
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: 360,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
src={config?.serverEntryImage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconDelete />}
|
||||
onClick={() => handleChangeServerEntryImage(null)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Upload
|
||||
onChange={(_, file) => {
|
||||
handleChangeServerEntryImage(file.originFile);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
SystemConfig.displayName = 'SystemConfig';
|
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* parse url, and replace some constants with variable
|
||||
* @param originUrl 原始Url
|
||||
* @returns 解析后的url
|
||||
*/
|
||||
export function parseUrlStr(originUrl: string): string {
|
||||
return String(originUrl).replace(
|
||||
'{BACKEND}',
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:11000'
|
||||
: window.location.origin
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue