feat(admin-next): add custom routes

- SystemConfig
- TailchatNetwork
- SocketIOAdmin
pull/90/head
moonrailgun 2 years ago
parent d68108b21a
commit ef88363bbf

@ -1,14 +1,26 @@
import { import {
createTextField, createTextField,
CustomRoute,
jsonServerProvider, jsonServerProvider,
ListTable, ListTable,
Resource, Resource,
Tushan, Tushan,
} from 'tushan'; } from 'tushan';
import { IconFile, IconMessage, IconUser, IconUserGroup } from 'tushan/icon'; import {
IconDashboard,
IconFile,
IconMessage,
IconSettings,
IconUser,
IconUserGroup,
IconWifi,
} from 'tushan/icon';
import { authProvider } from './auth'; import { authProvider } from './auth';
import { fileFields, groupFields, messageFields, userFields } from './fields'; import { fileFields, groupFields, messageFields, userFields } from './fields';
import { httpClient } from './request'; import { httpClient } from './request';
import { TailchatNetwork } from './routes/network';
import { SocketIOAdmin } from './routes/socketio';
import { SystemConfig } from './routes/system';
const dataProvider = jsonServerProvider('/admin/api', httpClient); const dataProvider = jsonServerProvider('/admin/api', httpClient);
@ -86,6 +98,18 @@ function App() {
/> />
} }
/> />
<CustomRoute name="system" icon={<IconSettings />}>
<SystemConfig />
</CustomRoute>
<CustomRoute name="network" icon={<IconWifi />}>
<TailchatNetwork />
</CustomRoute>
<CustomRoute name="socketio" icon={<IconDashboard />}>
<SocketIOAdmin />
</CustomRoute>
</Tushan> </Tushan>
); );
} }

@ -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…
Cancel
Save