feat: support reboot application manually (#1889)
* feat(reload-btn): reload application manually * feat(app): support reboot * feat: collections load after upgrade * feat(reboot): support reboot manually * chore(reboot): some typo * fix(reboot): fix storage path * fix(reboot): change restart file path * fix: menu divider --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
c0ef071baf
commit
eac034cb23
@ -74,11 +74,22 @@ module.exports = (cli) => {
|
||||
if (opts.dbSync) {
|
||||
argv.push('--db-sync');
|
||||
}
|
||||
const runDevServer = () => {
|
||||
run('ts-node-dev', argv, {
|
||||
env: {
|
||||
APP_PORT: serverPort,
|
||||
},
|
||||
}).catch((err) => {
|
||||
if (err.exitCode == 100) {
|
||||
console.log('Restarting server...');
|
||||
runDevServer();
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
runDevServer();
|
||||
}
|
||||
if (client || !server) {
|
||||
console.log('starting client', 1 * clientPort);
|
||||
|
@ -1,6 +1,6 @@
|
||||
const { Command } = require('commander');
|
||||
const { isDev, run, postCheck, runInstall, promptForTs } = require('../util');
|
||||
const { existsSync } = require('fs');
|
||||
const { existsSync, unlink } = require('fs');
|
||||
const { resolve } = require('path');
|
||||
const chalk = require('chalk');
|
||||
|
||||
@ -41,6 +41,8 @@ module.exports = (cli) => {
|
||||
return;
|
||||
}
|
||||
await postCheck(opts);
|
||||
const restartMark = resolve(process.cwd(), 'storage', 'restart');
|
||||
if (!existsSync(restartMark)) {
|
||||
if (opts.quickstart) {
|
||||
await run('node', [`./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, 'install', '--ignore-installed']);
|
||||
await run('node', [`./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, 'upgrade']);
|
||||
@ -48,6 +50,7 @@ module.exports = (cli) => {
|
||||
if (opts.dbSync) {
|
||||
await run('node', [`./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, 'db:sync']);
|
||||
}
|
||||
}
|
||||
if (opts.daemon) {
|
||||
run('pm2', ['start', `packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, '--', ...process.argv.slice(2)]);
|
||||
} else {
|
||||
|
@ -17,18 +17,17 @@ const SnippetCheckboxGroup = connect((props) => {
|
||||
}}
|
||||
value={props.value}
|
||||
onChange={(values) => {
|
||||
const snippets = ['ui.*', 'pm', 'pm.*', 'app'];
|
||||
const disallowSnippets = snippets.map((key) => `!${key}`);
|
||||
const value = uniq([...(props.value || []), ...values])
|
||||
.filter((key) => key && !['!ui.*', '!pm', '!pm.*'].includes(key))
|
||||
.filter((key) => key && !disallowSnippets.includes(key))
|
||||
.map((key) => {
|
||||
if (!['ui.*', 'pm', 'pm.*'].includes(key)) {
|
||||
return key;
|
||||
}
|
||||
if (values?.includes(key)) {
|
||||
if (!snippets.includes(key) || values?.includes(key)) {
|
||||
return key;
|
||||
}
|
||||
return `!${key}`;
|
||||
});
|
||||
for (const key of ['ui.*', 'pm', 'pm.*']) {
|
||||
for (const key of snippets) {
|
||||
if (!value.includes(key) && !value.includes(`!${key}`)) {
|
||||
value.push(`!${key}`);
|
||||
}
|
||||
@ -45,6 +44,9 @@ const SnippetCheckboxGroup = connect((props) => {
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Checkbox value="pm.*">{t('Allows to configure plugins')}</Checkbox>
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Checkbox value="app">{t('Allows to reboot application')}</Checkbox>
|
||||
</div>
|
||||
</Checkbox.Group>
|
||||
);
|
||||
});
|
||||
|
@ -16,6 +16,7 @@ const handleErrorMessage = (error) => {
|
||||
};
|
||||
export class APIClient extends APIClientSDK {
|
||||
services: Record<string, Result<any, any>> = {};
|
||||
silence = false;
|
||||
|
||||
service(uid: string) {
|
||||
return this.services[uid];
|
||||
@ -38,6 +39,9 @@ export class APIClient extends APIClientSDK {
|
||||
this.axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (this.silence) {
|
||||
throw error;
|
||||
}
|
||||
const redirectTo = error?.response?.data?.redirectTo;
|
||||
if (redirectTo) {
|
||||
return (window.location.href = redirectTo);
|
||||
@ -55,4 +59,9 @@ export class APIClient extends APIClientSDK {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
silent() {
|
||||
this.silence = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -777,4 +777,12 @@ export default {
|
||||
'Add template': '添加模板',
|
||||
'Display data template selector': '显示数据模板选择框',
|
||||
'Form data templates': '表单数据模板',
|
||||
|
||||
'Reload Application': '重载应用',
|
||||
'The application is reloading, please do not close the page.': '应用正在重新加载,请勿关闭页面。',
|
||||
'Application reloading': '应用重新加载中',
|
||||
'Reboot Application': '重启应用',
|
||||
"Allows to reboot application": "允许重启应用",
|
||||
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?': '重启将会中断当前服务,这个过程可能需要一点时间,确定要继续吗?',
|
||||
'Reboot': '重启',
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Dropdown, Menu } from 'antd';
|
||||
import { Dropdown, Menu, Modal } from 'antd';
|
||||
import React, { createContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useAPIClient, useCurrentUserContext } from '..';
|
||||
import { useACLRoleContext, useAPIClient, useCurrentUserContext } from '..';
|
||||
import { useCurrentAppInfo } from '../appInfo/CurrentAppInfoProvider';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import { EditProfile } from './EditProfile';
|
||||
@ -28,6 +28,30 @@ export const CurrentUser = () => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { data } = useCurrentUserContext();
|
||||
const { allowAll, snippets } = useACLRoleContext();
|
||||
const allowReboot = allowAll || snippets?.includes('app');
|
||||
const silenceApi = useAPIClient();
|
||||
const check = async () => {
|
||||
return await new Promise((resolve) => {
|
||||
const heartbeat = setInterval(() => {
|
||||
silenceApi
|
||||
.silent()
|
||||
.resource('app')
|
||||
.getInfo()
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
if (res?.status === 200) {
|
||||
resolve('ok');
|
||||
clearInterval(heartbeat);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore
|
||||
});
|
||||
}, 3000);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
|
||||
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
|
||||
@ -42,10 +66,36 @@ export const CurrentUser = () => {
|
||||
<Menu.Divider />
|
||||
<EditProfile />
|
||||
<ChangePassword />
|
||||
<Menu.Divider />
|
||||
<SwitchRole />
|
||||
<LanguageSettings />
|
||||
<ThemeSettings />
|
||||
<Menu.Divider />
|
||||
{allowReboot && (
|
||||
<Menu.Item
|
||||
key="reload"
|
||||
onClick={async () => {
|
||||
Modal.confirm({
|
||||
title: t('Reboot Application'),
|
||||
content: t(
|
||||
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?',
|
||||
),
|
||||
okText: t('Reboot'),
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: async () => {
|
||||
await api.resource('app').reboot();
|
||||
await check();
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Reboot Application')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
key="signout"
|
||||
onClick={async () => {
|
||||
|
@ -506,6 +506,7 @@
|
||||
"Allows to configure interface": "允许配置界面",
|
||||
"Allows to install, activate, disable plugins": "允许安装、激活、禁用插件",
|
||||
"Allows to configure plugins": "允许配置插件",
|
||||
"Allows to reload application": "允许重新加载应用",
|
||||
"Action display name": "操作名称",
|
||||
"Allow": "允许",
|
||||
"Data scope": "数据范围",
|
||||
@ -993,11 +994,26 @@
|
||||
"quarterPlaceholder": "请选择季度",
|
||||
"monthPlaceholder": "请选择月份",
|
||||
"weekPlaceholder": "请选择周",
|
||||
"rangePlaceholder": ["开始日期", "结束日期"],
|
||||
"rangeYearPlaceholder": ["开始年份", "结束年份"],
|
||||
"rangeMonthPlaceholder": ["开始月份", "结束月份"],
|
||||
"rangeQuarterPlaceholder": ["开始季度", "结束季度"],
|
||||
"rangeWeekPlaceholder": ["开始周", "结束周"],
|
||||
"rangePlaceholder": [
|
||||
"开始日期",
|
||||
"结束日期"
|
||||
],
|
||||
"rangeYearPlaceholder": [
|
||||
"开始年份",
|
||||
"结束年份"
|
||||
],
|
||||
"rangeMonthPlaceholder": [
|
||||
"开始月份",
|
||||
"结束月份"
|
||||
],
|
||||
"rangeQuarterPlaceholder": [
|
||||
"开始季度",
|
||||
"结束季度"
|
||||
],
|
||||
"rangeWeekPlaceholder": [
|
||||
"开始周",
|
||||
"结束周"
|
||||
],
|
||||
"locale": "zh_CN",
|
||||
"today": "今天",
|
||||
"now": "此刻",
|
||||
@ -1025,9 +1041,21 @@
|
||||
"previousCentury": "上一世纪",
|
||||
"nextCentury": "下一世纪"
|
||||
},
|
||||
"timePickerLocale": { "placeholder": "请选择时间", "rangePlaceholder": ["开始时间", "结束时间"] }
|
||||
"timePickerLocale": {
|
||||
"placeholder": "请选择时间",
|
||||
"rangePlaceholder": [
|
||||
"开始时间",
|
||||
"结束时间"
|
||||
]
|
||||
}
|
||||
},
|
||||
"TimePicker": {
|
||||
"placeholder": "请选择时间",
|
||||
"rangePlaceholder": [
|
||||
"开始时间",
|
||||
"结束时间"
|
||||
]
|
||||
},
|
||||
"TimePicker": { "placeholder": "请选择时间", "rangePlaceholder": ["开始时间", "结束时间"] },
|
||||
"Calendar": {
|
||||
"lang": {
|
||||
"placeholder": "请选择日期",
|
||||
@ -1035,11 +1063,26 @@
|
||||
"quarterPlaceholder": "请选择季度",
|
||||
"monthPlaceholder": "请选择月份",
|
||||
"weekPlaceholder": "请选择周",
|
||||
"rangePlaceholder": ["开始日期", "结束日期"],
|
||||
"rangeYearPlaceholder": ["开始年份", "结束年份"],
|
||||
"rangeMonthPlaceholder": ["开始月份", "结束月份"],
|
||||
"rangeQuarterPlaceholder": ["开始季度", "结束季度"],
|
||||
"rangeWeekPlaceholder": ["开始周", "结束周"],
|
||||
"rangePlaceholder": [
|
||||
"开始日期",
|
||||
"结束日期"
|
||||
],
|
||||
"rangeYearPlaceholder": [
|
||||
"开始年份",
|
||||
"结束年份"
|
||||
],
|
||||
"rangeMonthPlaceholder": [
|
||||
"开始月份",
|
||||
"结束月份"
|
||||
],
|
||||
"rangeQuarterPlaceholder": [
|
||||
"开始季度",
|
||||
"结束季度"
|
||||
],
|
||||
"rangeWeekPlaceholder": [
|
||||
"开始周",
|
||||
"结束周"
|
||||
],
|
||||
"locale": "zh_CN",
|
||||
"today": "今天",
|
||||
"now": "此刻",
|
||||
@ -1067,9 +1110,17 @@
|
||||
"previousCentury": "上一世纪",
|
||||
"nextCentury": "下一世纪"
|
||||
},
|
||||
"timePickerLocale": { "placeholder": "请选择时间", "rangePlaceholder": ["开始时间", "结束时间"] }
|
||||
"timePickerLocale": {
|
||||
"placeholder": "请选择时间",
|
||||
"rangePlaceholder": [
|
||||
"开始时间",
|
||||
"结束时间"
|
||||
]
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"placeholder": "请选择"
|
||||
},
|
||||
"global": { "placeholder": "请选择" },
|
||||
"Table": {
|
||||
"filterTitle": "筛选",
|
||||
"filterConfirm": "确定",
|
||||
@ -1088,8 +1139,15 @@
|
||||
"triggerAsc": "点击升序",
|
||||
"cancelSort": "取消排序"
|
||||
},
|
||||
"Modal": { "okText": "确定", "cancelText": "取消", "justOkText": "知道了" },
|
||||
"Popconfirm": { "cancelText": "取消", "okText": "确定" },
|
||||
"Modal": {
|
||||
"okText": "确定",
|
||||
"cancelText": "取消",
|
||||
"justOkText": "知道了"
|
||||
},
|
||||
"Popconfirm": {
|
||||
"cancelText": "取消",
|
||||
"okText": "确定"
|
||||
},
|
||||
"Transfer": {
|
||||
"searchPlaceholder": "请输入搜索内容",
|
||||
"itemUnit": "项",
|
||||
@ -1108,10 +1166,21 @@
|
||||
"previewFile": "预览文件",
|
||||
"downloadFile": "下载文件"
|
||||
},
|
||||
"Empty": { "description": "暂无数据" },
|
||||
"Icon": { "icon": "图标" },
|
||||
"Text": { "edit": "编辑", "copy": "复制", "copied": "复制成功", "expand": "展开" },
|
||||
"PageHeader": { "back": "返回" },
|
||||
"Empty": {
|
||||
"description": "暂无数据"
|
||||
},
|
||||
"Icon": {
|
||||
"icon": "图标"
|
||||
},
|
||||
"Text": {
|
||||
"edit": "编辑",
|
||||
"copy": "复制",
|
||||
"copied": "复制成功",
|
||||
"expand": "展开"
|
||||
},
|
||||
"PageHeader": {
|
||||
"back": "返回"
|
||||
},
|
||||
"Form": {
|
||||
"optional": "(可选)",
|
||||
"defaultValidateMessages": {
|
||||
@ -1157,10 +1226,14 @@
|
||||
"max": "最多${max}个${label}",
|
||||
"range": "${label}数量须在${min}-${max}之间"
|
||||
},
|
||||
"pattern": { "mismatch": "${label}与模式不匹配${pattern}" }
|
||||
"pattern": {
|
||||
"mismatch": "${label}与模式不匹配${pattern}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Image": { "preview": "预览" }
|
||||
"Image": {
|
||||
"preview": "预览"
|
||||
}
|
||||
},
|
||||
"cronstrue": {
|
||||
"atX0SecondsPastTheMinuteGt20": null,
|
||||
@ -1213,7 +1286,15 @@
|
||||
"commaOnDayX0OfTheMonth": ", 限每月%s",
|
||||
"commaEveryX0Years": ", 每隔 %s 年",
|
||||
"commaStartingX0": ", %s开始",
|
||||
"daysOfTheWeek": ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"],
|
||||
"daysOfTheWeek": [
|
||||
"星期日",
|
||||
"星期一",
|
||||
"星期二",
|
||||
"星期三",
|
||||
"星期四",
|
||||
"星期五",
|
||||
"星期六"
|
||||
],
|
||||
"monthsOfTheYear": [
|
||||
"一月",
|
||||
"二月",
|
||||
@ -1257,9 +1338,51 @@
|
||||
"suffixMinutesForHourPeriod": "分钟",
|
||||
"errorInvalidCron": "不符合 cron 规则的表达式",
|
||||
"clearButtonText": "清空",
|
||||
"weekDays": ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
|
||||
"months": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
|
||||
"altWeekDays": ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
|
||||
"altMonths": ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
|
||||
"weekDays": [
|
||||
"周日",
|
||||
"周一",
|
||||
"周二",
|
||||
"周三",
|
||||
"周四",
|
||||
"周五",
|
||||
"周六"
|
||||
],
|
||||
"months": [
|
||||
"一月",
|
||||
"二月",
|
||||
"三月",
|
||||
"四月",
|
||||
"五月",
|
||||
"六月",
|
||||
"七月",
|
||||
"八月",
|
||||
"九月",
|
||||
"十月",
|
||||
"十一月",
|
||||
"十二月"
|
||||
],
|
||||
"altWeekDays": [
|
||||
"周日",
|
||||
"周一",
|
||||
"周二",
|
||||
"周三",
|
||||
"周四",
|
||||
"周五",
|
||||
"周六"
|
||||
],
|
||||
"altMonths": [
|
||||
"一月",
|
||||
"二月",
|
||||
"三月",
|
||||
"四月",
|
||||
"五月",
|
||||
"六月",
|
||||
"七月",
|
||||
"八月",
|
||||
"九月",
|
||||
"十月",
|
||||
"十一月",
|
||||
"十二月"
|
||||
]
|
||||
}
|
||||
}
|
@ -94,8 +94,18 @@ export class ClientPlugin extends Plugin {
|
||||
this.app.acl.allow('app', 'getInfo');
|
||||
this.app.acl.allow('app', 'getPlugins');
|
||||
this.app.acl.allow('plugins', '*', 'public');
|
||||
this.app.acl.registerSnippet({
|
||||
name: 'app',
|
||||
actions: ['app:reload', 'app:reboot'],
|
||||
});
|
||||
const dialect = this.app.db.sequelize.getDialect();
|
||||
const locales = require('./locale').default;
|
||||
const restartMark = resolve(process.cwd(), 'storage', 'restart');
|
||||
this.app.on('beforeStart', async () => {
|
||||
if (fs.existsSync(restartMark)) {
|
||||
fs.unlinkSync(restartMark);
|
||||
}
|
||||
});
|
||||
this.app.resource({
|
||||
name: 'app',
|
||||
actions: {
|
||||
@ -161,6 +171,24 @@ export class ClientPlugin extends Plugin {
|
||||
.map((item) => item.name);
|
||||
await next();
|
||||
},
|
||||
async reload(ctx, next) {
|
||||
await ctx.app.reload();
|
||||
await next();
|
||||
},
|
||||
reboot(ctx) {
|
||||
const RESTART_CODE = 100;
|
||||
process.on('exit', (code) => {
|
||||
if (code === RESTART_CODE && process.env.APP_ENV === 'production') {
|
||||
fs.writeFileSync(restartMark, '1');
|
||||
console.log('Restart mark created.');
|
||||
}
|
||||
});
|
||||
ctx.app.on('afterStop', () => {
|
||||
// Exit with code 100 will restart the process
|
||||
process.exit(RESTART_CODE);
|
||||
});
|
||||
ctx.app.stop();
|
||||
},
|
||||
},
|
||||
});
|
||||
this.app.resource({
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Database, { Collection, MagicAttributeModel } from '@nocobase/database';
|
||||
import lodash from 'lodash';
|
||||
import { SyncOptions, Transactionable } from 'sequelize';
|
||||
import { FieldModel } from './field';
|
||||
import lodash from 'lodash';
|
||||
|
||||
interface LoadOptions extends Transactionable {
|
||||
// TODO
|
||||
@ -50,8 +50,12 @@ export class CollectionModel extends MagicAttributeModel {
|
||||
}
|
||||
|
||||
async loadFields(options: Transactionable = {}) {
|
||||
let fields = this.get('fields') || [];
|
||||
if (!fields.length) {
|
||||
fields = await this.getFields(options);
|
||||
}
|
||||
// @ts-ignore
|
||||
const instances: FieldModel[] = await this.getFields(options);
|
||||
const instances: FieldModel[] = fields;
|
||||
|
||||
for (const instance of instances) {
|
||||
await instance.load(options);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Repository } from '@nocobase/database';
|
||||
import { CollectionModel } from '../models/collection';
|
||||
import { CollectionsGraph } from '@nocobase/utils';
|
||||
import { CollectionModel } from '../models/collection';
|
||||
|
||||
interface LoadOptions {
|
||||
filter?: any;
|
||||
@ -10,7 +10,7 @@ interface LoadOptions {
|
||||
export class CollectionRepository extends Repository {
|
||||
async load(options: LoadOptions = {}) {
|
||||
const { filter, skipExist } = options;
|
||||
const instances = (await this.find({ filter })) as CollectionModel[];
|
||||
const instances = (await this.find({ filter, appends: ['fields'] })) as CollectionModel[];
|
||||
|
||||
const graphlib = CollectionsGraph.graphlib();
|
||||
|
||||
@ -33,7 +33,7 @@ export class CollectionRepository extends Repository {
|
||||
nameMap[collectionName] = instance;
|
||||
|
||||
// @ts-ignore
|
||||
const fields = await instance.getFields();
|
||||
const fields = instance.get('fields') || [];
|
||||
for (const field of fields) {
|
||||
if (field['type'] === 'belongsToMany') {
|
||||
const throughName = field.options.through;
|
||||
|
@ -216,6 +216,9 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
if (options?.method === 'install') {
|
||||
return;
|
||||
}
|
||||
if (options?.method === 'upgrade') {
|
||||
return;
|
||||
}
|
||||
const exists = await this.app.db.collectionExistsInDb('collections');
|
||||
if (exists) {
|
||||
try {
|
||||
|
@ -102,6 +102,7 @@ export class PresetNocoBase extends Plugin {
|
||||
);
|
||||
await this.app.reload({ method: 'upgrade' });
|
||||
await this.app.db.sync();
|
||||
await this.app.db.getRepository<any>('collections').load();
|
||||
});
|
||||
|
||||
this.app.on('beforeInstall', async () => {
|
||||
|
Loading…
Reference in New Issue
Block a user