dashboard/shell/core/plugin-routes.ts

151 lines
4.4 KiB
TypeScript

import Router, { RouteConfig } from 'vue-router';
interface RouteInfo {
parent?: string;
route: RouteConfig;
}
interface RouteInstallInfo {
plugin: string;
route: RouteConfig;
}
type RouteInstallHistory = {
[route: string]: RouteInstallInfo[]
}
export class PluginRoutes {
router: Router;
pluginRoutes: RouteConfig[] = [];
replacedRoutes: RouteInstallHistory = {};
constructor(router: Router) {
this.router = router;
}
public logRoutes(r: any, indent = 0) {
const spaces = Array(indent).join(' ');
r.forEach((s: any) => {
console.log(`${ spaces }${ s.name } -> ${ s.path }`); // eslint-disable-line no-console
this.logRoutes(s.children || [], indent + 2);
});
}
// Ensure we put back any routes that the plugin that is being uninstalled added
public uninstall(plugin: any) {
// List of routes we need to restore
const restore: RouteInfo[] = [];
Object.keys(this.replacedRoutes).forEach((routeName) => {
const info = this.replacedRoutes[routeName];
for (let index = 0; index < info.length; index++) {
const savedRoute = info[index];
if (savedRoute.plugin === plugin.id) {
// The plugin that is being uninstalled replaced an existing route that we will restore
if (index === 0) {
// Need to restore the previous route, since the plugin owned the active route
info.shift();
restore.push({ route: savedRoute.route });
break;
} else {
// Need to update the previous item so that when it is removed, it restores the correct route
const previous = info[index - 1];
previous.route = savedRoute.route;
info.splice(index, 1);
break;
}
}
}
});
// Remove routes from pluginRoutes, update matcher (to avoid dupes when re-adding plugin routes)
this.pluginRoutes = this.pluginRoutes.filter(pR => !plugin.routes.find((r: any) => pR === r.route));
this.updateMatcher([], [
...this.pluginRoutes,
...(this.router.options.routes || [])
]);
// Restore appropriate routes
if (restore.length > 0) {
this.addRoutes(null, restore);
}
}
public addRoutes(plugin: any, routes: RouteInfo[]) {
const allRoutes = [
...this.pluginRoutes,
...(this.router.options.routes || [])
];
// Need to take into account if routes are being replaced
// Despite what the docs say, routes are not replaced, so we use a workaround
// Remove all routes that are being replaced
routes.forEach((r: RouteInfo) => {
// See if the route exists
let existing: any;
if (r.parent) {
const pExisting = allRoutes.findIndex((route: any) => route.name === r.parent) as any;
const path = `${ pExisting.path }${ r.route.path }`;
// TODO: Validate
existing = allRoutes.findIndex((route: any) => route.path === path);
} else {
// no parent route
existing = allRoutes.findIndex((route: any) => route.name === r.route.name);
}
if (existing >= 0) {
const existingRoute = allRoutes[existing];
// Store the route so we can restore it on uninstall
if (plugin && existingRoute?.name) {
if (!this.replacedRoutes[existingRoute.name]) {
this.replacedRoutes[existingRoute.name] = [];
}
this.replacedRoutes[existingRoute.name].unshift({
plugin: plugin.id,
route: existingRoute
});
}
allRoutes.splice(existing, 1);
}
});
this.updateMatcher(routes, allRoutes);
}
private updateMatcher(newRoutes: RouteInfo[], allRoutes: RouteConfig[]) {
// Note - Always use a new router and replace the existing router's matching
// Using the existing router and adding routes to it will force nuxt middleware (specifically authenticated on default layout) to
// execute many times (nuxt middleware boils down to router.beforeEach). This issue was seen refreshing in a harvester cluster with a
// dynamically loaded cluster
const newRouter: Router = new Router({
mode: 'history',
routes: allRoutes
});
newRoutes.forEach((r: any) => {
if (r.parent) {
newRouter.addRoute(r.parent, r.route);
} else {
newRouter.addRoute(r.route);
}
this.pluginRoutes.push(r.route);
});
// Typing is incorrect
(this.router as any).matcher = (newRouter as any).matcher;
}
}