feat(admin): allow edit client config serverName edit in admin

pull/90/head
moonrailgun 2 years ago
parent 6c26fe1c15
commit 9c7448b7cb

@ -0,0 +1,7 @@
import React from 'react';
import { parseUrlStr } from '../utils';
export const Image: React.FC<{ src: string }> = React.memo((props) => {
return <img src={parseUrlStr(props.src)} />;
});
Image.displayName = 'Image';

@ -7,6 +7,9 @@ export const englishCustom = {
permission: 'Permission', permission: 'Permission',
confirmTitle: 'Are you sure you want to perform this operation?', confirmTitle: 'Are you sure you want to perform this operation?',
confirmContent: 'This action cannot be undone', confirmContent: 'This action cannot be undone',
errorOccurred: 'some errors occurred',
operateSuccess: 'Operate Success',
operateFailed: 'Operate Failed',
}, },
menu: { menu: {
network: 'Tailchat Network', network: 'Tailchat Network',
@ -66,6 +69,8 @@ export const englishCustom = {
config: { config: {
uploadFileLimit: 'Upload file limit (KB)', uploadFileLimit: 'Upload file limit (KB)',
emailVerification: 'Mandatory Email Verification', emailVerification: 'Mandatory Email Verification',
serverName: 'Server Name',
serverEntryImage: 'Server Entry Page Image',
}, },
}, },
}; };
@ -79,6 +84,9 @@ export const chineseCustom = {
permission: '权限', permission: '权限',
confirmTitle: '确认要进行该操作么?', confirmTitle: '确认要进行该操作么?',
confirmContent: '该操作不可撤回', confirmContent: '该操作不可撤回',
errorOccurred: '发生了一些错误',
operateSuccess: '操作成功',
operateFailed: '操作失败',
}, },
menu: { menu: {
network: 'Tailchat 网络', network: 'Tailchat 网络',
@ -136,6 +144,8 @@ export const chineseCustom = {
config: { config: {
uploadFileLimit: '上传文件限制(KB)', uploadFileLimit: '上传文件限制(KB)',
emailVerification: '邮箱强制验证', emailVerification: '邮箱强制验证',
serverName: '服务器名',
serverEntryImage: '服务器登录图',
}, },
}, },
}; };

@ -1,10 +1,11 @@
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { request } from '../../request'; import { request } from '../../request';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { CircularProgress, Box, Grid } from '@mui/material'; import { CircularProgress, Box, Grid, Input } from '@mui/material';
import { useTranslate } from 'react-admin'; import { useTranslate, useDelete, useNotify } from 'react-admin';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
import ClearIcon from '@mui/icons-material/Clear'; import ClearIcon from '@mui/icons-material/Clear';
import { useEditValue } from '../../utils/hooks';
const SystemItem: React.FC< const SystemItem: React.FC<
PropsWithChildren<{ PropsWithChildren<{
@ -29,16 +30,48 @@ SystemItem.displayName = 'SystemItem';
*/ */
export const SystemConfig: React.FC = React.memo(() => { export const SystemConfig: React.FC = React.memo(() => {
const translate = useTranslate(); const translate = useTranslate();
const { data: config, loading } = useRequest(async () => { const notify = useNotify();
const { data } = await request('/config/client'); const {
data: config,
loading,
error,
} = useRequest(async () => {
const { data } = await request.get('/config/client');
return data.config ?? {}; return data.config ?? {};
}); });
const [serverName, setServerName, saveServerName] = useEditValue(
config?.serverName,
async (val) => {
if (val === config?.serverName) {
return;
}
try {
await request.patch('/config/client', {
key: 'serverName',
value: val,
});
notify('custom.common.operateSuccess', {
type: 'info',
});
} catch (err) {
notify('custom.common.operateFailed', {
type: 'info',
});
}
}
);
if (loading) { if (loading) {
return <CircularProgress />; return <CircularProgress />;
} }
if (error) {
return <div>{translate('custom.common.errorOccurred')}</div>;
}
return ( return (
<Box <Box
sx={{ sx={{
@ -58,6 +91,15 @@ export const SystemConfig: React.FC = React.memo(() => {
<ClearIcon fontSize="small" /> <ClearIcon fontSize="small" />
)} )}
</SystemItem> </SystemItem>
<SystemItem label={translate('custom.config.serverName')}>
<Input
value={serverName}
onChange={(e) => setServerName(e.target.value)}
onBlur={() => saveServerName()}
placeholder="Tailchat"
/>
</SystemItem>
</Box> </Box>
); );
}); });

@ -0,0 +1,15 @@
import { useCallback, useLayoutEffect, useState } from 'react';
export function useEditValue<T>(value: T, onChange: (val: T) => void) {
const [inner, setInner] = useState(value);
useLayoutEffect(() => {
setInner(value);
}, [value]);
const onSave = useCallback(() => {
onChange(inner);
}, [inner, onChange]);
return [inner, setInner, onSave] as const;
}

@ -0,0 +1,8 @@
/**
* parse url, and replace some constants with variable
* @param originUrl Url
* @returns url
*/
export function parseUrlStr(originUrl: string): string {
return String(originUrl).replace('{BACKEND}', window.location.origin);
}

@ -1,5 +1,5 @@
import { App } from '~/ra/App'; import { App } from '../../ra/App';
import styles from '~/styles/app.css'; import styles from '../../styles/app.css';
export function links() { export function links() {
return [{ rel: 'stylesheet', href: styles }]; return [{ rel: 'stylesheet', href: styles }];

@ -1,5 +1,5 @@
import { App } from '~/ra/App'; import { App } from '../../ra/App';
import styles from '~/styles/app.css'; import styles from '../../styles/app.css';
export function links() { export function links() {
return [{ rel: 'stylesheet', href: styles }]; return [{ rel: 'stylesheet', href: styles }];

@ -7,8 +7,13 @@ import mongoose from 'mongoose';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import { apiRouter } from './router/api'; import { apiRouter } from './router/api';
if (!process.env.MONGO_URL) {
console.error('Require env: MONGO_URL');
process.exit(1);
}
// 链接数据库 // 链接数据库
mongoose.connect(process.env.MONGO_URL!, (error: any) => { mongoose.connect(process.env.MONGO_URL, (error: any) => {
if (!error) { if (!error) {
return console.info('Datebase connected'); return console.info('Datebase connected');
} }
@ -56,6 +61,11 @@ app.all(
}) })
); );
app.use((err, req, res, next) => {
res.status(500);
res.json({ error: err.message });
});
const port = process.env.ADMIN_PORT || 3000; const port = process.env.ADMIN_PORT || 3000;
app.listen(port, () => { app.listen(port, () => {

@ -8,12 +8,31 @@ import { auth } from '../middleware/auth';
const router = Router(); const router = Router();
router.get('/client', auth(), async (req, res) => { router.get('/client', auth(), async (req, res, next) => {
try {
const config = await broker.call('config.client'); const config = await broker.call('config.client');
res.json({ res.json({
config, config,
}); });
} catch (err) {
next(err);
}
});
router.patch('/client', auth(), async (req, res, next) => {
try {
await broker.call('config.setClientConfig', {
key: req.body.key,
value: req.body.value,
});
res.json({
success: true,
});
} catch (err) {
next(err);
}
}); });
export { router as configRouter }; export { router as configRouter };

@ -16,13 +16,8 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"importsNotUsedAsValues": "error", "importsNotUsedAsValues": "error",
"experimentalDecorators": true, "experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"jsx": "react-jsx", "jsx": "react-jsx",
"noEmit": true,
// Remix takes care of building everything in `remix build`. "baseUrl": "."
"noEmit": true
} }
} }

@ -5,6 +5,7 @@ import {
modelOptions, modelOptions,
Severity, Severity,
ReturnModelType, ReturnModelType,
index,
} from '@typegoose/typegoose'; } from '@typegoose/typegoose';
import type { Base } from '@typegoose/typegoose/lib/defaultClasses'; import type { Base } from '@typegoose/typegoose/lib/defaultClasses';
import type { Types } from 'mongoose'; import type { Types } from 'mongoose';
@ -14,6 +15,7 @@ import type { Types } from 'mongoose';
allowMixed: Severity.ALLOW, allowMixed: Severity.ALLOW,
}, },
}) })
@index({ name: 1 })
export class Config implements Base { export class Config implements Base {
_id: Types.ObjectId; _id: Types.ObjectId;
id: string; id: string;
@ -47,14 +49,17 @@ export class Config implements Base {
key: string, key: string,
value: any value: any
): Promise<void> { ): Promise<void> {
await this.updateOne( await this.findOneAndUpdate(
{ {
name: Config.globalClientConfigName, name: Config.globalClientConfigName,
}, },
{ {
$set: { $set: {
[key]: value, [`data.${key}`]: value,
}, },
},
{
upsert: true,
} }
); );
} }

@ -407,10 +407,14 @@ export abstract class TcService extends Service {
* NOTICE: 使Redisservice * NOTICE: 使Redisservice
*/ */
async cleanActionCache(actionName: string, keys: string[] = []) { async cleanActionCache(actionName: string, keys: string[] = []) {
if (keys.length === 0) {
await this.broker.cacher.clean(`${this.serviceName}.${actionName}`);
} else {
await this.broker.cacher.clean( await this.broker.cacher.clean(
`${this.serviceName}.${actionName}:${keys.join('|')}**` `${this.serviceName}.${actionName}:${keys.join('|')}**`
); );
} }
}
/** /**
* *

@ -71,7 +71,7 @@ class ConfigService extends TcService {
* NOTICE: * NOTICE:
*/ */
async client(ctx: TcPureContext) { async client(ctx: TcPureContext) {
const persistConfig = this.adapter.model.getAllClientPersistConfig(); const persistConfig = await this.adapter.model.getAllClientPersistConfig();
return { return {
uploadFileLimit: config.storage.limit, uploadFileLimit: config.storage.limit,

Loading…
Cancel
Save