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:
YANG QIA 2023-05-19 20:34:22 +08:00 committed by GitHub
parent c0ef071baf
commit eac034cb23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 295 additions and 3241 deletions

View File

@ -74,11 +74,22 @@ module.exports = (cli) => {
if (opts.dbSync) {
argv.push('--db-sync');
}
run('ts-node-dev', argv, {
env: {
APP_PORT: serverPort,
},
});
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);

View File

@ -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,12 +41,15 @@ module.exports = (cli) => {
return;
}
await postCheck(opts);
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']);
}
if (opts.dbSync) {
await run('node', [`./packages/${APP_PACKAGE_ROOT}/server/lib/index.js`, 'db:sync']);
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']);
}
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)]);

View File

@ -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>
);
});

View File

@ -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;
}
}

View File

@ -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': '重启',
}

View File

@ -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 () => {

View File

@ -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": [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月"
]
}
}
}

View File

@ -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({

View File

@ -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);

View File

@ -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;

View File

@ -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 {

View File

@ -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 () => {

3188
yarn.lock

File diff suppressed because it is too large Load Diff