dashboard/shell/core/plugin.ts

276 lines
7.4 KiB
TypeScript

import { RouteConfig } from 'vue-router';
import { DSL as STORE_DSL } from '@shell/store/type-map';
import {
CoreStoreInit,
Action,
Tab,
Card,
Panel,
TableColumn,
IPlugin,
LocationConfig,
ExtensionPoint,
} from './types';
import coreStore, { coreStoreModule, coreStoreState } from '@shell/plugins/dashboard-store';
import {
PluginRouteConfig, RegisterStore, UnregisterStore, CoreStoreSpecifics, CoreStoreConfig, OnNavToPackage, OnNavAwayFromPackage, OnLogOut
} from '@shell/core/types';
export class Plugin implements IPlugin {
public id: string;
public name: string;
public types: any = {};
public l10n: { [key: string]: Function[] } = {};
public locales: { locale: string, label: string}[] = [];
public products: Function[] = [];
public productNames: string[] = [];
public routes: { parent?: string, route: RouteConfig }[] = [];
public stores: { storeName: string, register: RegisterStore, unregister: UnregisterStore }[] = [];
public onEnter: OnNavToPackage = () => Promise.resolve();
public onLeave: OnNavAwayFromPackage = () => Promise.resolve();
public _onLogOut: OnLogOut = () => Promise.resolve();
public uiConfig: { [key: string]: any } = {};
// Plugin metadata (plugin package.json)
public _metadata: any = {};
public _validators: {[key:string]: Function } = {}
// Is this a built-in plugin (bundled with the application)
public builtin = false;
// Uninstall hooks
public uninstallHooks: Function[] = [];
constructor(id: string) {
this.id = id;
this.name = id;
// Initialize uiConfig for all of the possible enum values
Object.values(ExtensionPoint).forEach((v) => {
this.uiConfig[v] = {};
});
}
get metadata() {
return this._metadata;
}
set metadata(value) {
this._metadata = value;
this.name = this._metadata.name || this.id;
}
get version() {
return this._metadata.version;
}
get validators() {
return this._validators;
}
set validators(vals: {[key:string]: Function }) {
this._validators = vals;
}
// Track which products the plugin creates
DSL(store: any, productName: string) {
const storeDSL = STORE_DSL(store, productName);
this.productNames.push(productName);
return storeDSL;
}
addProduct(product: Function): void {
this.products.push(product);
}
addLocale(locale: string, label: string): void {
this.locales.push({ locale, label });
}
addL10n(locale: string, fn: Function) {
this.register('l10n', locale, fn);
}
addRoutes(routes: PluginRouteConfig[] | RouteConfig[]) {
routes.forEach((r: PluginRouteConfig | RouteConfig) => {
if (Object.keys(r).includes('parent')) {
const pConfig = r as PluginRouteConfig;
if (pConfig.parent) {
this.addRoute(pConfig.parent, pConfig.route);
} else {
this.addRoute(pConfig.route);
}
} else {
this.addRoute(r as RouteConfig);
}
});
}
addRoute(parentOrRoute: RouteConfig | string, optionalRoute?: RouteConfig): void {
// Always add the pkg name to the route metadata
const hasParent = typeof (parentOrRoute) === 'string';
const parent: string | undefined = hasParent ? parentOrRoute as string : undefined;
const route: RouteConfig = hasParent ? optionalRoute as RouteConfig : parentOrRoute as RouteConfig;
route.meta = {
...route?.meta,
pkg: this.name,
};
this.routes.push({ parent, route });
}
private _addUIConfig(type: string, where: string, when: LocationConfig | string, config: any) {
// For convenience 'when' can be a string to indicate a resource, so convert it to the LocationConfig format
const locationConfig = (typeof when === 'string') ? { resource: when } : when;
this.uiConfig[type][where] = this.uiConfig[type][where] || [];
this.uiConfig[type][where].push({ ...config, locationConfig });
}
/**
* Adds an action/button to the UI
*/
addAction(where: string, when: LocationConfig | string, action: Action): void {
this._addUIConfig(ExtensionPoint.ACTION, where, when, action);
}
/**
* Adds a tab to the UI
*/
addTab(where: string, when: LocationConfig | string, tab: Tab): void {
this._addUIConfig(ExtensionPoint.TAB, where, when, tab);
}
/**
* Adds a panel/component to the UI
*/
addPanel(where: string, when: LocationConfig | string, panel: Panel): void {
this._addUIConfig(ExtensionPoint.PANEL, where, when, panel);
}
/**
* Adds a card to the to the UI
*/
addCard( where: string, when: LocationConfig | string, card: Card): void {
this._addUIConfig(ExtensionPoint.CARD, where, when, card);
}
/**
* Adds a new column to a table on the UI
*/
addTableColumn(where: string, when: LocationConfig | string, column: TableColumn): void {
this._addUIConfig(ExtensionPoint.TABLE_COL, where, when, column);
}
setHomePage(component: any) {
this.addRoute({
name: 'home',
path: '/home',
component
});
}
addUninstallHook(hook: Function) {
this.uninstallHooks.push(hook);
}
addStore(storeName: string, register: RegisterStore, unregister: UnregisterStore) {
this.stores.push({
storeName, register, unregister
});
}
addDashboardStore(storeName: string, storeSpecifics: CoreStoreSpecifics, config: CoreStoreConfig, init?: CoreStoreInit) {
this.stores.push({
storeName,
register: () => {
return coreStore(
this.storeFactory(storeSpecifics, config),
config,
init,
);
},
unregister: (store: any) => {
store.unregisterModule(storeName);
}
});
}
private storeFactory(storeSpecifics: CoreStoreSpecifics, config: CoreStoreConfig) {
return {
...coreStoreModule,
state() {
return {
...coreStoreState(config.namespace, config.baseUrl, config.isClusterStore),
...storeSpecifics.state()
};
},
getters: {
...coreStoreModule.getters,
...storeSpecifics.getters
},
mutations: {
...coreStoreModule.mutations,
...storeSpecifics.mutations
},
actions: {
...coreStoreModule.actions,
...storeSpecifics.actions
},
};
}
public addNavHooks(
onEnter: OnNavToPackage = () => Promise.resolve(),
onLeave: OnNavAwayFromPackage = () => Promise.resolve(),
onLogOut: OnLogOut = () => Promise.resolve(),
): void {
this.onEnter = onEnter;
this.onLeave = onLeave;
this._onLogOut = onLogOut;
}
public async onLogOut(store: any) {
await Promise.all(this.stores.map((s: any) => store.dispatch(`${ s.storeName }/onLogout`)));
await this._onLogOut(store);
}
public register(type: string, name: string, fn: Function) {
const nparts = name.split('/');
// Support components in a sub-folder - component_name/index.vue (and ignore other componnets in that folder)
// Allow store-scoped models via sub-folder - pkgname/models/storename/type will be registered as storename/type to avoid overwriting shell/models/type
if (nparts.length === 2 && type !== 'models') {
if (nparts[1] !== 'index') {
return;
}
name = nparts[0];
}
// Accumulate l10n resources rather than replace
if (type === 'l10n') {
if (!this.l10n[name]) {
this.l10n[name] = [];
}
this.l10n[name].push(fn);
} else {
if (!this.types[type]) {
this.types[type] = {};
}
this.types[type][name] = fn;
}
}
}