ChengLei Shao 797f566d70
feat(gateway): response cli result when run nocobase command (#2563)
* chore(gateway): refresh message in websocket

* chore(gateway): throw error when cli error

* chore(gateway): await ipc server response

* chore: notification message

* fix: build

* chore: notification type

* feat: notification

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2023-09-01 16:11:27 +08:00

228 lines
7.0 KiB
TypeScript

import { define, observable } from '@formily/reactive';
import { APIClientOptions } from '@nocobase/sdk';
import { i18n as i18next } from 'i18next';
import get from 'lodash/get';
import merge from 'lodash/merge';
import set from 'lodash/set';
import React, { ComponentType, FC, ReactElement } from 'react';
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
import { Link, NavLink, Navigate } from 'react-router-dom';
import { APIClient, APIClientProvider } from '../api-client';
import { i18n } from '../i18n';
import type { Plugin } from './Plugin';
import { PluginManager, PluginType } from './PluginManager';
import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager';
import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
import { AppComponent, BlankComponent, defaultAppComponents } from './components';
import { compose, normalizeContainer } from './utils';
import { defineGlobalDeps } from './utils/globalDeps';
import type { RequireJS } from './utils/requirejs';
import { getRequireJs } from './utils/requirejs';
declare global {
interface Window {
define: RequireJS['define'];
}
}
export type DevDynamicImport = (packageName: string) => Promise<{ default: typeof Plugin }>;
export type ComponentAndProps<T = any> = [ComponentType, T];
export interface ApplicationOptions {
apiClient?: APIClientOptions;
ws?: WebSocketClientOptions | boolean;
i18n?: i18next;
providers?: (ComponentType | ComponentAndProps)[];
plugins?: PluginType[];
components?: Record<string, ComponentType>;
scopes?: Record<string, any>;
router?: RouterOptions;
devDynamicImport?: DevDynamicImport;
}
export class Application {
public providers: ComponentAndProps[] = [];
public router: RouterManager;
public scopes: Record<string, any> = {};
public i18n: i18next;
public ws: WebSocketClient;
public apiClient: APIClient;
public components: Record<string, ComponentType> = { ...defaultAppComponents };
public pm: PluginManager;
public devDynamicImport: DevDynamicImport;
public requirejs: RequireJS;
public notification;
loading = true;
maintained = false;
maintaining = false;
error = null;
constructor(protected options: ApplicationOptions = {}) {
this.initRequireJs();
define(this, {
maintained: observable.ref,
loading: observable.ref,
maintaining: observable.ref,
error: observable.ref,
});
this.devDynamicImport = options.devDynamicImport;
this.scopes = merge(this.scopes, options.scopes);
this.components = merge(this.components, options.components);
this.apiClient = new APIClient(options.apiClient);
this.apiClient.app = this;
this.i18n = options.i18n || i18n;
this.router = new RouterManager({
...options.router,
renderComponent: this.renderComponent.bind(this),
});
this.pm = new PluginManager(options.plugins, this);
this.addDefaultProviders();
this.addReactRouterComponents();
this.addProviders(options.providers || []);
this.ws = new WebSocketClient(options.ws);
}
private initRequireJs() {
this.requirejs = getRequireJs();
defineGlobalDeps(this.requirejs);
window.define = this.requirejs.define;
}
private addDefaultProviders() {
this.use(APIClientProvider, { apiClient: this.apiClient });
this.use(I18nextProvider, { i18n: this.i18n });
}
private addReactRouterComponents() {
this.addComponents({
Link,
Navigate: Navigate as ComponentType,
NavLink,
});
}
getComposeProviders() {
const Providers = compose(...this.providers)(BlankComponent);
Providers.displayName = 'Providers';
return Providers;
}
use<T = any>(component: ComponentType, props?: T) {
return this.addProvider(component, props);
}
addProvider<T = any>(component: ComponentType, props?: T) {
return this.providers.push([component, props]);
}
addProviders(providers: (ComponentType | [ComponentType, any])[]) {
providers.forEach((provider) => {
if (Array.isArray(provider)) {
this.addProvider(provider[0], provider[1]);
} else {
this.addProvider(provider);
}
});
}
async load() {
this.ws.on('message', (event) => {
const data = JSON.parse(event.data);
console.log(data.payload);
if (data?.payload?.refresh) {
window.location.reload();
return;
}
if (data.type === 'notification') {
this.notification[data.payload?.type || 'info']({ message: data.payload?.message });
return;
}
const maintaining = data.type === 'maintaining' && data.payload.code !== 'APP_RUNNING';
if (maintaining) {
this.maintaining = true;
this.error = data.payload;
} else {
this.maintaining = false;
this.maintained = true;
this.error = null;
}
});
this.ws.on('serverDown', () => {
this.maintaining = true;
});
this.ws.connect();
try {
this.loading = true;
await this.pm.load();
} catch (error) {
this.error = {
code: 'LOAD_ERROR',
message: error.message,
};
console.error(error);
}
this.loading = false;
}
getComponent<T = any>(Component: ComponentTypeAndString<T>, isShowError = true): ComponentType<T> | undefined {
const showError = (msg: string) => isShowError && console.error(msg);
if (!Component) {
showError(`getComponent called with ${Component}`);
return;
}
// ClassComponent or FunctionComponent
if (typeof Component === 'function') return Component;
// Component is a string, try to get it from this.components
if (typeof Component === 'string') {
const res = get(this.components, Component) as ComponentType<T>;
if (!res) {
showError(`Component ${Component} not found`);
return;
}
return res;
}
showError(`Component ${Component} should be a string or a React component`);
return;
}
renderComponent<T extends {}>(Component: ComponentTypeAndString, props?: T): ReactElement {
return React.createElement(this.getComponent(Component), props);
}
addComponent(component: ComponentType, name?: string) {
const componentName = name || component.displayName || component.name;
if (!componentName) {
console.error('Component must have a displayName or pass name as second argument');
return;
}
set(this.components, componentName, component);
}
addComponents(components: Record<string, ComponentType>) {
Object.keys(components).forEach((name) => {
this.addComponent(components[name], name);
});
}
addScopes(scopes: Record<string, any>) {
this.scopes = merge(this.scopes, scopes);
}
getRootComponent() {
const Root: FC = () => <AppComponent app={this} />;
return Root;
}
mount(containerOrSelector: Element | ShadowRoot | string) {
const container = normalizeContainer(containerOrSelector);
if (!container) return;
const App = this.getRootComponent();
const root = createRoot(container);
root.render(<App />);
return root;
}
}