flaschengeist-frontend/src/boot/plugins.ts

344 lines
10 KiB
TypeScript

import { Notify } from 'quasar';
import { api } from 'boot/axios';
import { boot } from 'quasar/wrappers';
import routes from 'src/router/routes';
import { AxiosResponse } from 'axios';
import { RouteRecordRaw } from 'vue-router';
import { FG_Plugin } from '@flaschengeist/types';
/****************************************************
******** Internal area for some magic **************
****************************************************/
declare type ImportPlgn = { default: FG_Plugin.Plugin };
function validatePlugin(plugin: FG_Plugin.Plugin) {
return (
typeof plugin.name === 'string' &&
typeof plugin.id === 'string' &&
plugin.id.length > 0 &&
typeof plugin.version === 'string'
);
}
/* eslint-disable */
// This functions are used by webpack magic
// Called when import promise resolved
function success(value: ImportPlgn, path: string) {
if (validatePlugin(value.default)) PLUGINS.plugins.set(value.default.id, value.default);
else failure(path);
}
// Called when import promise rejected
function failure(path = 'unknown') {
console.error(`Plugin ${path} could not be found and not imported`);
}
/* eslint-enable */
// Here does some magic happens, WebPack will automatically replace the following comment with the import statements
const PLUGINS = {
context: <Array<() => Promise<ImportPlgn>>>[
/*INSERT_PLUGIN_LIST*/
],
plugins: new Map<string, FG_Plugin.Plugin>(),
};
interface BackendPlugin {
permissions: string[];
version: string;
}
interface BackendPlugins {
[key: string]: BackendPlugin;
}
interface Backend {
plugins: BackendPlugins;
version: string;
}
export { Backend };
// Handle Notifications
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
// Combine routes, shortcuts and widgets from plugins
/**
* Helper function, set permissions from MenuRoute to meta from RouteRecordRaw
* @param object MenuRoute to set route meta
*/
function setPermissions(object: FG_Plugin.MenuRoute) {
if (object.permissions !== undefined) {
if (object.route.meta === undefined) object.route.meta = {};
object.route.meta['permissions'] = object.permissions;
}
}
/**
* Helper function to convert MenuRoute to the parents RouteRecordRaw
* @param parent Parent RouteRecordRaw
* @param children MenuRoute to convert
*/
function convertRoutes(parent: RouteRecordRaw, children?: FG_Plugin.MenuRoute[]) {
if (children !== undefined) {
children.forEach((child) => {
setPermissions(child);
convertRoutes(child.route, child.children);
if (parent.children === undefined) parent.children = [];
parent.children.push(child.route);
});
}
}
/**
* Combines routes from plugin MenuRoute to Vue-Router RouteRecordRaw to get a clean route-tree
* @param target
* @param source
* @param mainPath
*/
function combineMenuRoutes(
target: RouteRecordRaw[],
source: FG_Plugin.MenuRoute[],
mainPath: '/' | '/in' = '/'
): RouteRecordRaw[] {
// Search parent
target.forEach((target) => {
if (target.path === mainPath) {
// Parent found = target
source.forEach((sourceMainConfig: FG_Plugin.MenuRoute) => {
// Check if source is already in target
const targetMainConfig = target.children?.find((targetMainConfig: RouteRecordRaw) => {
return sourceMainConfig.route.path === targetMainConfig.path;
});
// Already in target routes, add only children
if (targetMainConfig) {
convertRoutes(targetMainConfig, sourceMainConfig.children);
} else {
// Append to target
if (target.children === undefined) {
target.children = [];
}
convertRoutes(sourceMainConfig.route, sourceMainConfig.children);
if (
sourceMainConfig.children &&
sourceMainConfig.children.length > 0 &&
!sourceMainConfig.route.component
)
Object.assign(sourceMainConfig.route, {
component: () => import('src/components/navigation/EmptyParent.vue'),
});
target.children.push(sourceMainConfig.route);
}
});
}
});
return target;
}
function combineRoutes(
target: RouteRecordRaw[],
source: FG_Plugin.NamedRouteRecordRaw[],
mainPath: '/' | '/in'
) {
// Search parent
target.forEach((target) => {
if (target.path === mainPath) {
// Parent found = target
source.forEach((sourceRoute) => {
// Check if source is already in target
const targetRoot = target.children?.find(
(targetRoot) => sourceRoute.path === targetRoot.path
);
// Already in target routes, add only children
if (targetRoot) {
if (targetRoot.children === undefined) targetRoot.children = [];
targetRoot.children.push(...(sourceRoute.children || []));
} else {
// Append to target
if (target.children === undefined) target.children = [];
if (
sourceRoute.children &&
sourceRoute.children.length > 0 &&
sourceRoute.component === undefined
)
Object.assign(sourceRoute, {
component: () => import('src/components/navigation/EmptyParent.vue'),
});
target.children.push(sourceRoute);
}
});
}
});
}
/**
* Combine MenuRoutes into Flaschengeist MenuLinks for the main menu
* @param target Flaschengeist list of menu links
* @param source MenuRoutes to combine
*/
function combineMenuLinks(target: FG_Plugin.MenuLink[], source: FG_Plugin.MenuRoute) {
let idx = target.findIndex((link) => link.title == source.title);
// Link not found, add new one
if (idx === -1) {
idx += target.push({
title: source.title,
icon: source.icon,
link: source.route.name,
permissions: source.permissions,
});
}
if (target[idx].children === undefined) {
target[idx].children = [];
}
source.children?.forEach((sourceChild) => {
target[idx].children?.push({
title: sourceChild.title,
icon: sourceChild.icon,
link: sourceChild.route.name,
permissions: sourceChild.permissions,
});
});
}
/**
* Combine shortcuts from Plugin MenuRouts into the Flaschenbeist Shortcut list
* @param target Flaschengeist list of shortcuts
* @param source MenuRoutes to extract shortcuts from
*/
function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRoute[]) {
source.forEach((route) => {
if (route.shortcut) {
target.push(<FG_Plugin.Shortcut>{
link: route.route.name,
icon: route.icon,
permissions: route.permissions,
});
}
if (route.children) {
combineShortcuts(target, route.children);
}
});
}
/**
* Load a Flaschengeist plugin
* @param loadedPlugins Flaschgeist object
* @param plugin Plugin to load
* @param router VueRouter instance
*/
function loadPlugin(
loadedPlugins: FG_Plugin.Flaschengeist,
plugin: FG_Plugin.Plugin,
backend: Backend
) {
// Check if already loaded
if (loadedPlugins.plugins.findIndex((p) => p.name === plugin.name) !== -1) return true;
// Check backend dependencies
if (
!plugin.requiredModules.every(
(required) =>
backend.plugins[required[0]] !== undefined &&
(required.length == 1 ||
true) /* validate the version, semver440 from python is... tricky on node*/
)
) {
console.error(`Plugin ${plugin.name}: Backend modules not satisfied`);
return false;
}
// Start combining and loading routes, shortcuts etc
if (plugin.internalRoutes) {
combineRoutes(loadedPlugins.routes, plugin.internalRoutes, '/in');
}
if (plugin.innerRoutes) {
// Routes for Vue Router
combineMenuRoutes(loadedPlugins.routes, plugin.innerRoutes, '/in');
// Combine links for menu
plugin.innerRoutes.forEach((route) => combineMenuLinks(loadedPlugins.menuLinks, route));
// Combine shortcuts
combineShortcuts(loadedPlugins.shortcuts, plugin.innerRoutes);
}
if (plugin.outerRoutes) {
combineMenuRoutes(loadedPlugins.routes, plugin.outerRoutes);
combineShortcuts(loadedPlugins.outerShortcuts, plugin.outerRoutes);
}
if (plugin.widgets.length > 0) {
plugin.widgets.forEach((widget) => (widget.name = plugin.name + '_' + widget.name));
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
}
loadedPlugins.plugins.push({
name: plugin.name,
version: plugin.version,
notification: plugin.notification?.bind({}) || translateNotification,
});
return true;
}
/**
* Loading backend information
* @returns Backend object or null
*/
async function getBackend() {
try {
const { data }: AxiosResponse<Backend> = await api.get('/');
return data;
} catch (e) {
console.warn(e);
return null;
}
}
/**
* Boot file, load all required plugins, check for dependencies
*/
export default boot(async ({ router, app }) => {
const backend = await getBackend();
if (!backend || typeof backend !== 'object' || !('plugins' in backend)) {
console.log('Backend error');
router.isReady().finally(() => void router.push({ name: 'offline', params: { refresh: 1 } }));
return;
}
const loadedPlugins: FG_Plugin.Flaschengeist = {
routes,
plugins: [],
menuLinks: [],
shortcuts: [],
outerShortcuts: [],
widgets: [],
};
const BreakError = {};
try {
PLUGINS.plugins.forEach((plugin, name) => {
if (!loadPlugin(loadedPlugins, plugin, backend)) {
void router.push({ name: 'error' });
Notify.create({
type: 'negative',
message: `Fehler beim Laden: Bitte wende dich an den Admin (error: PNF-${name}!`,
timeout: 10000,
progress: true,
});
throw BreakError;
}
});
} catch (e) {
if (e !== BreakError) throw e;
}
// Sort widgets by priority
loadedPlugins.widgets.sort((a, b) => b.priority - a.priority);
// Add loaded routes to router
loadedPlugins.routes.forEach((route) => router.addRoute(route));
// save plugins in VM-variable
app.provide('flaschengeist', loadedPlugins);
});