feat: notice for backup (#1134)

Reviewed-on: daoyoucloud/tachybase#1134
This commit is contained in:
sealday 2024-06-06 13:47:47 +08:00
parent d5c1864092
commit 8b47ac3f7c
17 changed files with 205 additions and 43 deletions

View File

@ -42,6 +42,7 @@
"markdown-it-highlightjs": "3.3.1",
"mathjs": "^10.6.0",
"mermaid": "9.4.3",
"mitt": "^3.0.1",
"nanoid": "^3.3.6",
"rc-tree-select": "^5.15.0",
"rc-util": "^5.32.0",

View File

@ -1,6 +1,7 @@
import React, { createContext, ReactNode, useContext } from 'react';
import React, { createContext, ReactNode, useContext, useEffect } from 'react';
import { message, notification } from 'antd';
import mitt, { Emitter, EventType } from 'mitt';
import { Application } from './Application';
import { useApp } from './hooks';
@ -17,6 +18,7 @@ export const enum NoticeType {
STATUS = 'status',
TOAST = 'toast',
NOTIFICATION = 'notification',
CUSTOM = 'custom',
}
export interface NoticeManagerContextProps {
@ -36,15 +38,29 @@ export const useNoticeManager = () => {
return useContext(NoticeManagerContext);
};
export const useNoticeSub = (name: string, handler: () => {}) => {
const { manager } = useNoticeManager();
useEffect(() => {
manager.emitter.on(name, handler);
return () => {
manager.emitter.off(name, handler);
};
}, [manager.emitter, name, handler]);
};
export class NoticeManager {
private ws: WebSocketClient;
public emitter: Emitter<Record<EventType, unknown>>;
constructor(private app: Application) {
this.ws = app.ws;
this.emitter = mitt();
}
on(data: { type: NoticeType; title?: string; content: string; level: NoticeLevel }) {
on(data: { type: NoticeType; title?: string; content: string; level: NoticeLevel; eventType?: string; event?: any }) {
if (data.type === NoticeType.NOTIFICATION) {
this[data.type](data.title, data.content, data.level);
} else if (data.type === NoticeType.CUSTOM) {
this.emitter.emit(data.eventType, data.event);
} else {
this[data.type](data.content, data.level);
}

View File

@ -8,5 +8,6 @@ export * from './schema-initializer';
export * from './schema-settings';
export * from './schema-toolbar';
export * from './PluginSettingsManager';
export * from './NoticesManager';
export * from './hoc';
export { ApplicationContext } from './context';

View File

@ -12,6 +12,7 @@ import {
findByUid,
findMenuItem,
NavigateIfNotSignIn,
NoticeManagerProvider,
PinnedPluginList,
RemoteCollectionManagerProvider,
RemoteSchemaTemplateManagerPlugin,
@ -440,6 +441,7 @@ export const InternalAdminLayout = (props: any) => {
export const AdminProvider = (props) => {
return (
<NoticeManagerProvider>
<CurrentAppInfoProvider>
<NavigateIfNotSignIn>
<RemoteCollectionManagerProvider>
@ -449,6 +451,7 @@ export const AdminProvider = (props) => {
</RemoteCollectionManagerProvider>
</NavigateIfNotSignIn>
</CurrentAppInfoProvider>
</NoticeManagerProvider>
);
};

View File

@ -6,7 +6,7 @@
"types": "./lib/index.d.ts",
"dependencies": {
"@hapi/topo": "^6.0.0",
"@koa/cors": "^3.1.0",
"@koa/cors": "^5.0.0",
"@koa/multer": "^3.0.2",
"@koa/router": "^9.4.0",
"@tachybase/acl": "workspace:*",
@ -55,6 +55,8 @@
},
"devDependencies": {
"@types/koa": "^2.13.4",
"@types/koa-bodyparser": "^4.3.12",
"@types/koa__cors": "^5.0.0",
"@types/lodash": "4.17.0",
"@types/qs": "^6.9.14",
"@types/semver": "^7.3.9",
@ -63,7 +65,7 @@
"async-mutex": "^0.3.2",
"execa": "^5.1.1",
"fast-glob": "^3.3.1",
"glob": "^7.1.6",
"glob": "^10.4.1",
"koa-compose": "^4.1.0",
"qs": "^6.11.2",
"redis": "^4.6.10"

View File

@ -1,6 +1,8 @@
import { applyMixins, AsyncEmitter } from '@tachybase/utils';
import { Mutex } from 'async-mutex';
import { EventEmitter } from 'events';
import { applyMixins, AsyncEmitter } from '@tachybase/utils';
import { Mutex } from 'async-mutex';
import Application, { ApplicationOptions, MaintainingCommandStatus } from './application';
import { getErrorLevel } from './errors/handler';

View File

@ -29,7 +29,6 @@ import lodash from 'lodash';
import _ from 'lodash';
import { nanoid } from 'nanoid';
import semver from 'semver';
import winston from 'winston';
import packageJson from '../package.json';
import { createACL } from './acl';
@ -50,9 +49,12 @@ import {
import { ApplicationVersion } from './helpers/application-version';
import { Locale } from './locale';
import { MainDataSource } from './main-data-source';
import { NoticeManager } from './notice';
import { Plugin } from './plugin';
import { InstallOptions, PluginManager } from './plugin-manager';
export { Logger } from 'winston';
export type PluginType = string | typeof Plugin;
export type PluginConfiguration = PluginType | [PluginType, any];
@ -183,6 +185,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
private _maintainingCommandStatus: MaintainingCommandStatus;
private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null;
private _actionCommand: Command;
private _noticeManager: NoticeManager;
static KEY_CORE_APP_PREFIX = 'KEY_CORE_APP_';
private currentId = nanoid();
@ -193,6 +196,11 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this.init();
this._appSupervisor.addApp(this);
this._noticeManager = new NoticeManager(this);
}
get noticeManager() {
return this._noticeManager;
}
protected _loaded: boolean;

View File

@ -1,14 +1,16 @@
import cors from '@koa/cors';
import { randomUUID } from 'crypto';
import fs from 'fs';
import { resolve } from 'path';
import { createHistogram, RecordableHistogram } from 'perf_hooks';
import { requestLogger } from '@tachybase/logger';
import { Resourcer } from '@tachybase/resourcer';
import { uid } from '@tachybase/utils';
import cors from '@koa/cors';
import { Command } from 'commander';
import { randomUUID } from 'crypto';
import fs from 'fs';
import i18next from 'i18next';
import bodyParser from 'koa-bodyparser';
import { resolve } from 'path';
import { createHistogram, RecordableHistogram } from 'perf_hooks';
import Application, { ApplicationOptions } from './application';
import { parseVariables } from './middlewares';
import { dateTemplate } from './middlewares/data-template';

View File

@ -6,3 +6,4 @@ export * from './plugin';
export * from './plugin-manager';
export * from './gateway';
export * from './app-supervisor';
export * from './notice';

View File

@ -0,0 +1,59 @@
import Application from '../application';
import { Gateway } from '../gateway';
import { WSServer } from '../gateway/ws-server';
export const enum NoticeLevel {
INFO = 'info',
WARNING = 'warning',
SUCCESS = 'success',
ERROR = 'error',
}
export const enum NoticeType {
STATUS = 'status',
TOAST = 'toast',
NOTIFICATION = 'notification',
CUSTOM = 'custom',
}
export class NoticeManager {
private ws: WSServer;
constructor(private app: Application) {
const gateway = Gateway.getInstance();
this.ws = gateway['wsServer'];
}
#emit(msg: {
type: NoticeType;
title?: string;
content?: string;
level?: NoticeLevel;
eventType?: string;
event?: unknown;
}) {
this.ws.sendToConnectionsByTag('app', this.app.name, {
type: 'notice',
payload: msg,
});
}
notify(eventType: string, event: unknown) {
this.#emit({
type: NoticeType.CUSTOM,
eventType,
event,
});
}
status(content: string, level: NoticeLevel) {
this.#emit({ type: NoticeType.STATUS, content, level });
}
toast(content: string, level: NoticeLevel) {
this.#emit({ type: NoticeType.TOAST, content, level });
}
notification(title: string, content: string, level: NoticeLevel) {
this.#emit({ type: NoticeType.NOTIFICATION, title, content, level });
}
}

View File

@ -1,12 +1,14 @@
import fs from 'fs';
import { basename, resolve } from 'path';
import { Model } from '@tachybase/database';
import { LoggerOptions } from '@tachybase/logger';
import { fsExists, importModule } from '@tachybase/utils';
import fs from 'fs';
import glob from 'glob';
import type { TFuncKey, TOptions } from 'i18next';
import { basename, resolve } from 'path';
import { Application } from './application';
import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager';
import { getExposeChangelogUrl, getExposeReadmeUrl, InstallOptions } from './plugin-manager';
import { checkAndGetCompatible } from './plugin-manager/utils';
export interface PluginInterface {
@ -61,6 +63,10 @@ export abstract class Plugin<O = any> implements PluginInterface {
});
}
get noticeManager() {
return this.app.noticeManager;
}
get name() {
return this.options.name as string;
}

View File

@ -1,7 +1,7 @@
import './actions';
import path from 'path';
import Application, { InstallOptions, Plugin, type PluginOptions } from '@tachybase/server';
import Application, { InstallOptions, NoticeLevel, Plugin, type PluginOptions } from '@tachybase/server';
import { Container } from '@tachybase/utils';
import { DepartmentsPlugin } from './features/departments';

View File

@ -14,6 +14,7 @@
"@koa/multer": "^3.0.2",
"@tachybase/components": "workspace:*",
"@types/archiver": "^5.3.1",
"@types/file-saver": "^2.0.7",
"@types/lodash": "4.17.0",
"antd": "5.16.1",
"archiver": "^5.3.1",

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Checkbox, DatePicker, useAPIClient, useCompile } from '@tachybase/client';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Checkbox, DatePicker, useAPIClient, useCompile, useNoticeSub } from '@tachybase/client';
import { FormItem } from '@tachybase/components';
import { InboxOutlined, PlusOutlined, ReloadOutlined, UploadOutlined } from '@ant-design/icons';
@ -142,7 +142,7 @@ const LearnMore: any = (props: { collectionsData?: any; isBackup?: boolean }) =>
);
};
const Restore: React.FC<any> = ({ ButtonComponent = Button, title, upload = false, fileData }) => {
const Restore = ({ ButtonComponent = Button, title, upload = false, fileData }: any) => {
const { t } = useDuplicatorTranslation();
const [dataTypes, setDataTypes] = useState<any[]>(['required']);
const [isModalOpen, setIsModalOpen] = useState(false);
@ -232,7 +232,7 @@ const Restore: React.FC<any> = ({ ButtonComponent = Button, title, upload = fals
);
};
const NewBackup: React.FC<any> = ({ ButtonComponent = Button, refresh }) => {
const NewBackup = ({ ButtonComponent = Button, refresh }) => {
const { t } = useDuplicatorTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [dataTypes, setBackupData] = useState<any[]>(['required']);
@ -295,7 +295,7 @@ const NewBackup: React.FC<any> = ({ ButtonComponent = Button, refresh }) => {
);
};
const RestoreUpload: React.FC<any> = (props: any) => {
const RestoreUpload = (props: any) => {
const { t } = useDuplicatorTranslation();
const uploadProps: UploadProps = {
multiple: false,
@ -338,6 +338,12 @@ export const BackupAndRestoreList = () => {
return apiClient.resource('backupFiles');
}, [apiClient]);
const handleRefresh = useCallback(async () => {
await queryFieldList();
}, []);
useNoticeSub('backup', handleRefresh);
useEffect(() => {
queryFieldList();
}, []);
@ -361,9 +367,6 @@ export const BackupAndRestoreList = () => {
const blob = new Blob([data.data]);
saveAs(blob, fileData.name);
};
const handleRefresh = async () => {
await queryFieldList();
};
const handleDestory = (fileData) => {
modal.confirm({
title: t('Delete record', { ns: 'client' }),
@ -393,7 +396,7 @@ export const BackupAndRestoreList = () => {
/>
<NewBackup refresh={handleRefresh} />
</Space>
<Table
<Table<any>
dataSource={dataSource}
loading={loading}
columns={[

View File

@ -1,5 +1,5 @@
import React from 'react';
import { CurrentAppInfoProvider, SchemaComponentOptions } from '@tachybase/client';
import { SchemaComponentOptions } from '@tachybase/client';
export const DuplicatorProvider = function (props) {
return <SchemaComponentOptions>{props.children}</SchemaComponentOptions>;

View File

@ -237,6 +237,7 @@ export class Dumper extends AppMigrator {
}).finally(() => {
this.cleanLockFile(backupFileName);
Dumper.dumpTasks.delete(backupFileName);
this.app.noticeManager.notify('backup', { msg: 'done' });
});
Dumper.dumpTasks.set(backupFileName, promise);

View File

@ -536,6 +536,9 @@ importers:
mermaid:
specifier: 9.4.3
version: 9.4.3
mitt:
specifier: ^3.0.1
version: 3.0.1
nanoid:
specifier: ^3.3.6
version: 3.3.7
@ -1053,8 +1056,8 @@ importers:
specifier: ^6.0.0
version: 6.0.2
'@koa/cors':
specifier: ^3.1.0
version: 3.4.3
specifier: ^5.0.0
version: 5.0.0
'@koa/multer':
specifier: ^3.0.2
version: 3.0.2(multer@1.4.4)
@ -1194,6 +1197,12 @@ importers:
'@types/koa':
specifier: ^2.13.4
version: 2.13.12
'@types/koa-bodyparser':
specifier: ^4.3.12
version: 4.3.12
'@types/koa__cors':
specifier: ^5.0.0
version: 5.0.0
'@types/lodash':
specifier: 4.17.0
version: 4.17.0
@ -1219,8 +1228,8 @@ importers:
specifier: ^3.3.1
version: 3.3.2
glob:
specifier: ^7.1.6
version: 7.2.3
specifier: ^10.4.1
version: 10.4.1
koa-compose:
specifier: ^4.1.0
version: 4.1.0
@ -2125,6 +2134,9 @@ importers:
'@types/archiver':
specifier: ^5.3.1
version: 5.3.4
'@types/file-saver':
specifier: ^2.0.7
version: 2.0.7
'@types/lodash':
specifier: 4.17.0
version: 4.17.0
@ -9735,9 +9747,9 @@ packages:
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
dev: false
/@koa/cors@3.4.3:
resolution: {integrity: sha512-WPXQUaAeAMVaLTEFpoq3T2O1C+FstkjJnDQqy95Ck1UdILajsRhu6mhJ8H2f4NFPRBoCNN+qywTJfq/gGki5mw==}
engines: {node: '>= 8.0.0'}
/@koa/cors@5.0.0:
resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==}
engines: {node: '>= 14.0.0'}
dependencies:
vary: 1.1.2
dev: false
@ -12062,6 +12074,10 @@ packages:
'@types/qs': 6.9.14
'@types/serve-static': 1.15.5
/@types/file-saver@2.0.7:
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
dev: true
/@types/fs-extra@11.0.4:
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
dependencies:
@ -12164,7 +12180,6 @@ packages:
resolution: {integrity: sha512-hKMmRMVP889gPIdLZmmtou/BijaU1tHPyMNmcK7FAHAdATnRcGQQy78EqTTxLH1D4FTsrxIzklAQCso9oGoebQ==}
dependencies:
'@types/koa': 2.13.12
dev: false
/@types/koa-compose@3.2.8:
resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==}
@ -12195,6 +12210,12 @@ packages:
'@types/koa-compose': 3.2.8
'@types/node': 20.12.8
/@types/koa__cors@5.0.0:
resolution: {integrity: sha512-LCk/n25Obq5qlernGOK/2LUwa/2YJb2lxHUkkvYFDOpLXlVI6tKcdfCHRBQnOY4LwH6el5WOLs6PD/a8Uzau6g==}
dependencies:
'@types/koa': 2.13.12
dev: true
/@types/linkify-it@3.0.5:
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
dev: true
@ -19276,8 +19297,21 @@ packages:
path-scurry: 1.10.2
dev: true
/glob@10.4.1:
resolution: {integrity: sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==}
engines: {node: '>=16 || 14 >=14.18'}
hasBin: true
dependencies:
foreground-child: 3.1.1
jackspeak: 3.3.0
minimatch: 9.0.4
minipass: 7.1.2
path-scurry: 1.11.1
dev: true
/glob@7.1.6:
resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
deprecated: Glob versions prior to v9 are no longer supported
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
@ -20560,6 +20594,15 @@ packages:
'@pkgjs/parseargs': 0.11.0
dev: true
/jackspeak@3.3.0:
resolution: {integrity: sha512-glPiBfKguqA7v8JsXO3iLjJWZ9FV1vNpoI0I9hI9Mnk5yetO9uPLSpiCEmiVijAssv2f54HpvtzvAHfhPieiDQ==}
engines: {node: '>=14'}
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
dev: true
/javascript-natural-sort@0.7.1:
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
@ -22049,6 +22092,11 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
dev: true
/minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
dev: true
/minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
@ -23099,6 +23147,14 @@ packages:
minipass: 7.0.4
dev: true
/path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
dependencies:
lru-cache: 10.2.0
minipass: 7.1.2
dev: true
/path-temp@2.1.0:
resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==}
engines: {node: '>=8.15'}