From 1e2069466a8751f6ab8784d96ca0b8f352c8140c Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 26 Feb 2021 16:14:34 -0500 Subject: [PATCH] make Pages and PageMenus observable and reactive Signed-off-by: Sebastian Malton --- src/extensions/core-api/index.ts | 4 +- src/extensions/extension-loader.ts | 23 ++- src/extensions/lens-main-extension.ts | 2 +- src/extensions/lens-renderer-extension.ts | 54 +++++- .../__tests__/page-registry.test.ts | 156 ++++++++------- src/extensions/registries/base-registry.ts | 3 +- src/extensions/registries/index.ts | 13 ++ .../registries/page-menu-registry.ts | 84 ++++---- src/extensions/registries/page-registry.ts | 183 +++++++++++++----- src/extensions/registries/sources.ts | 7 + src/renderer/components/app.tsx | 43 ++-- .../cluster-manager/cluster-manager.tsx | 4 +- .../cluster-manager/clusters-menu.tsx | 6 +- src/renderer/components/layout/sidebar.tsx | 51 ++--- 14 files changed, 385 insertions(+), 248 deletions(-) create mode 100644 src/extensions/registries/sources.ts diff --git a/src/extensions/core-api/index.ts b/src/extensions/core-api/index.ts index 19ede51817..96fa4dea36 100644 --- a/src/extensions/core-api/index.ts +++ b/src/extensions/core-api/index.ts @@ -1,6 +1,6 @@ // Lens-extensions api developer's kit -export * from "../lens-main-extension"; -export * from "../lens-renderer-extension"; +export { LensMainExtension } from "../lens-main-extension"; +export { LensRendererExtension } from "../lens-renderer-extension"; // APIs import * as App from "./app"; diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 3af9ad874e..f2eac0741d 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -14,7 +14,6 @@ import type { LensRendererExtension } from "./lens-renderer-extension"; import * as registries from "./registries"; import fs from "fs"; - export function extensionPackagesRoot() { return path.join((app || remote.app).getPath("userData")); } @@ -66,10 +65,22 @@ export class ExtensionLoader { return extensions; } + @computed get allEnabledInstances(): LensExtension[] { + const res: LensExtension[] = []; + + for (const [extId, ext] of this.instances) { + if (this.extensions.get(extId).isEnabled) { + res.push(ext); + } + } + + return res; + } + getExtensionByName(name: string): LensExtension | null { - for (const [, val] of this.instances) { - if (val.name === name) { - return val; + for (const [extId, ext] of this.instances) { + if (ext.name === name && this.extensions.get(extId).isEnabled) { + return ext; } } @@ -210,8 +221,6 @@ export class ExtensionLoader { logger.debug(`${logModule}: load on main renderer (cluster manager)`); this.autoInitExtensions(async (extension: LensRendererExtension) => { const removeItems = [ - registries.globalPageRegistry.add(extension.globalPages, extension), - registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension), registries.appPreferenceRegistry.add(extension.appPreferences), registries.clusterFeatureRegistry.add(extension.clusterFeatures), registries.statusBarRegistry.add(extension.statusBarItems), @@ -240,8 +249,6 @@ export class ExtensionLoader { } const removeItems = [ - registries.clusterPageRegistry.add(extension.clusterPages, extension), - registries.clusterPageMenuRegistry.add(extension.clusterPageMenus, extension), registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems), registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems), registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts), diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index f0e943540d..baa8eb52e1 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -9,7 +9,7 @@ export class LensMainExtension extends LensExtension { async navigate

(pageId?: string, params?: P, frameId?: number) { const windowManager = WindowManager.getInstance(); const pageUrl = getExtensionPageUrl({ - extensionId: this.name, + extensionName: this.name, pageId, params: params ?? {}, // compile to url with params }); diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 982830d8af..85dc31133d 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -1,14 +1,56 @@ -import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"; +import { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, getRegisteredPage, Registrable, StatusBarRegistration, recitfyRegisterable, getRegisteredPageMenu, } from "./registries"; import type { Cluster } from "../main/cluster"; import { LensExtension } from "./lens-extension"; import { getExtensionPageUrl } from "./registries/page-registry"; import { CommandRegistration } from "./registries/command-registry"; +import { computed, observable } from "mobx"; +import { getHostedCluster } from "../common/cluster-store"; + +export const registeredClusterPages = Symbol("registeredClusterPages"); +export const registeredGlobalPages = Symbol("registeredGlobalPages"); +export const registeredGlobalPageMenus = Symbol("registeredGlobalPageMenus"); +export const registeredClusterPageMenus = Symbol("registeredClusterPageMenus"); export class LensRendererExtension extends LensExtension { - globalPages: PageRegistration[] = []; - clusterPages: PageRegistration[] = []; - globalPageMenus: PageMenuRegistration[] = []; - clusterPageMenus: ClusterPageMenuRegistration[] = []; + #privateGetters = { + [registeredGlobalPages]: computed(() => ( + recitfyRegisterable(this.globalPages) + .map(page => getRegisteredPage(page, this.name)) + )), + [registeredClusterPages]: computed(() => ( + recitfyRegisterable(this.clusterPages, getHostedCluster) + .map(page => getRegisteredPage(page, this.name)) + )), + [registeredGlobalPageMenus]: computed(() => ( + recitfyRegisterable(this.globalPageMenus) + .map(pageMenu => getRegisteredPageMenu(pageMenu, this.name)) + )), + [registeredClusterPageMenus]: computed(() => ( + recitfyRegisterable(this.clusterPageMenus, getHostedCluster) + .map(pageMenu => getRegisteredPageMenu(pageMenu, this.name)) + )), + }; + + @observable globalPages: Registrable = []; + get [registeredGlobalPages]() { + return this.#privateGetters[registeredGlobalPages].get(); + } + + @observable clusterPages: Registrable = []; + get [registeredClusterPages]() { + return this.#privateGetters[registeredClusterPages].get(); + } + + @observable globalPageMenus: Registrable = []; + get [registeredGlobalPageMenus]() { + return this.#privateGetters[registeredGlobalPageMenus].get(); + } + + @observable clusterPageMenus: Registrable = []; + get [registeredClusterPageMenus]() { + return this.#privateGetters[registeredClusterPageMenus].get(); + } + kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; appPreferences: AppPreferenceRegistration[] = []; clusterFeatures: ClusterFeatureRegistration[] = []; @@ -20,7 +62,7 @@ export class LensRendererExtension extends LensExtension { async navigate

(pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); const pageUrl = getExtensionPageUrl({ - extensionId: this.name, + extensionName: this.name, pageId, params: params ?? {}, // compile to url with params }); diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 55ba3d6d64..802ed99069 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -1,126 +1,134 @@ -import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry"; -import { LensExtension } from "../../lens-extension"; +import { getExtensionPageUrl, PageParams } from "../page-registry"; import React from "react"; +import { LensRendererExtension } from "../../core-api"; +import { findRegisteredPage, PageRegistration } from ".."; +import { extensionLoader } from "../../extension-loader"; -let ext: LensExtension = null; +jest.mock("../../extension-loader"); describe("getPageUrl", () => { + const extensionName = "foo-bar"; + beforeEach(async () => { - ext = new LensExtension({ - manifest: { - name: "foo-bar", - version: "0.1.1" - }, - id: "/this/is/fake/package.json", - absolutePath: "/absolute/fake/", - manifestPath: "/this/is/fake/package.json", - isBundled: false, - isEnabled: true - }); - globalPageRegistry.add({ - id: "page-with-params", - components: { - Page: () => React.createElement("Page with params") - }, - params: { - test1: "test1-default", - test2: "" // no default value, just declaration - }, - }, ext); + jest.spyOn(extensionLoader, "getExtensionByName") + .mockImplementation(name => { + if (name !== extensionName) { + return undefined; + } + + const ext = new LensRendererExtension({ + manifest: { + name: extensionName, + version: "0.1.1" + }, + id: "/this/is/fake/package.json", + absolutePath: "/absolute/fake/", + manifestPath: "/this/is/fake/package.json", + isBundled: false, + isEnabled: true + }); + + (ext.globalPages as PageRegistration[]).push({ + id: "page-with-params", + components: { + Page: () => React.createElement("Page with params") + }, + params: { + test1: "test1-default", + test2: "" // no default value, just declaration + }, + }); + + return ext; + }); }); it("returns a page url for extension", () => { - expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar"); + expect(getExtensionPageUrl({ extensionName })).toBe("/extension/foo-bar"); }); it("allows to pass base url as parameter", () => { - expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test"); + expect(getExtensionPageUrl({ extensionName, pageId: "/test" })).toBe("/extension/foo-bar/test"); }); it("removes @ and replace `/` to `--`", () => { - expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar"); + expect(getExtensionPageUrl({ extensionName: "@foo/bar" })).toBe("/extension/foo--bar"); }); it("adds / prefix", () => { - expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test"); + expect(getExtensionPageUrl({ extensionName, pageId: "test" })).toBe("/extension/foo-bar/test"); }); it("normalize possible multi-slashes in page.id", () => { - expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test"); + expect(getExtensionPageUrl({ extensionName, pageId: "//test/" })).toBe("/extension/foo-bar/test"); }); it("gets page url with custom params", () => { const params: PageParams = { test1: "one", test2: "2" }; const searchParams = new URLSearchParams(params); - const pageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", params }); + const pageUrl = getExtensionPageUrl({ extensionName, pageId: "page-with-params", params }); expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`); }); it("gets page url with default custom params", () => { - const defaultPageUrl = getExtensionPageUrl({ extensionId: ext.name, pageId: "page-with-params", }); + const defaultPageUrl = getExtensionPageUrl({ extensionName, pageId: "page-with-params", }); expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`); }); }); describe("globalPageRegistry", () => { - beforeEach(async () => { - ext = new LensExtension({ - manifest: { - name: "@acme/foo-bar", - version: "0.1.1" - }, - id: "/this/is/fake/package.json", - absolutePath: "/absolute/fake/", - manifestPath: "/this/is/fake/package.json", - isBundled: false, - isEnabled: true - }); - globalPageRegistry.add([ - { - id: "test-page", - components: { - Page: () => React.createElement("Text") - } - }, - { - id: "another-page", - components: { - Page: () => React.createElement("Text") - }, - }, - { - components: { - Page: () => React.createElement("Default") - } - }, - ], ext); + const extensionName = "@acme/foo-bar"; + const ext = new LensRendererExtension({ + manifest: { + name: extensionName, + version: "0.1.1" + }, + id: "/this/is/fake/package.json", + absolutePath: "/absolute/fake/", + manifestPath: "/this/is/fake/package.json", + isBundled: false, + isEnabled: true }); - describe("getByPageTarget", () => { + (ext.globalPages as PageRegistration[]).push( + { + id: "test-page", + components: { + Page: () => React.createElement("Text") + } + }, + { + id: "another-page", + components: { + Page: () => React.createElement("Text") + }, + }, + { + components: { + Page: () => React.createElement("Default") + } + }, + ); + + describe("findRegisteredPage", () => { it("matching to first registered page without id", () => { - const page = globalPageRegistry.getByPageTarget({ extensionId: ext.name }); + const page = findRegisteredPage(ext); expect(page.id).toEqual(undefined); - expect(page.extensionId).toEqual(ext.name); - expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name })); + expect(page.extensionName).toEqual(ext.name); + expect(page.url).toEqual(getExtensionPageUrl({ extensionName })); }); it("returns matching page", () => { - const page = globalPageRegistry.getByPageTarget({ - pageId: "test-page", - extensionId: ext.name - }); + const page = findRegisteredPage(ext, "test-page"); expect(page.id).toEqual("test-page"); }); it("returns null if target not found", () => { - const page = globalPageRegistry.getByPageTarget({ - pageId: "wrong-page", - extensionId: ext.name - }); + const page = findRegisteredPage(ext, "wrong-page"); expect(page).toBeNull(); }); diff --git a/src/extensions/registries/base-registry.ts b/src/extensions/registries/base-registry.ts index 052a483cbd..bddc77de61 100644 --- a/src/extensions/registries/base-registry.ts +++ b/src/extensions/registries/base-registry.ts @@ -20,8 +20,9 @@ export class BaseRegistry { return () => this.remove(...itemArray); } - // eslint-disable-next-line unused-imports/no-unused-vars-ts protected getRegisteredItem(item: T, extension?: LensExtension): I { + void extension; + return item as any; } diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index b6838f1bab..8fdd67fd50 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -1,5 +1,7 @@ // All registries managed by extensions api +import { Cluster } from "../../main/cluster"; + export * from "./page-registry"; export * from "./page-menu-registry"; export * from "./menu-registry"; @@ -10,3 +12,14 @@ export * from "./kube-object-menu-registry"; export * from "./cluster-feature-registry"; export * from "./kube-object-status-registry"; export * from "./command-registry"; +export * from "./sources"; + +export type Registrable = (T[]) | ((cluster?: Cluster | null) => T[]); + +export function recitfyRegisterable(src: Registrable, getCluster?: () => Cluster | null | undefined): T[] { + if (typeof src === "function") { + return src(getCluster()); + } + + return src; +} diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts index 8fe5b68b3b..a692f8c389 100644 --- a/src/extensions/registries/page-menu-registry.ts +++ b/src/extensions/registries/page-menu-registry.ts @@ -2,9 +2,14 @@ import type { IconProps } from "../../renderer/components/icon"; import type React from "react"; import type { PageTarget, RegisteredPage } from "./page-registry"; -import { action } from "mobx"; -import { BaseRegistry } from "./base-registry"; -import { LensExtension } from "../lens-extension"; +import { RegisteredPageTarget } from "."; +import { LensRendererExtension } from "../core-api"; +import { extensionLoader } from "../extension-loader"; +import { registeredClusterPageMenus, registeredGlobalPageMenus } from "../lens-renderer-extension"; + +export interface PageMenuComponents { + Icon: React.ComponentType; +} export interface PageMenuRegistration { target?: PageTarget; @@ -12,50 +17,57 @@ export interface PageMenuRegistration { components: PageMenuComponents; } +export interface RegisteredPageMenuTarget { + target: RegisteredPageTarget; +} + +export type RegisteredPageMenu = PageMenuRegistration & RegisteredPageMenuTarget; + export interface ClusterPageMenuRegistration extends PageMenuRegistration { id?: string; parentId?: string; } -export interface PageMenuComponents { - Icon: React.ComponentType; +export type RegisteredClusterPageMenu = ClusterPageMenuRegistration & RegisteredPageMenuTarget; + +export function getRegisteredPageMenu({ target: { pageId, params } = {}, ...rest }: T, extensionName: string): T & RegisteredPageMenuTarget { + const target: RegisteredPageTarget = { + params, + pageId, + extensionName, + }; + + return { ...rest, target } as T & RegisteredPageMenuTarget; } -export class PageMenuRegistry extends BaseRegistry { - @action - add(items: T[], ext: LensExtension) { - const normalizedItems = items.map(menuItem => { - menuItem.target = { - extensionId: ext.name, - ...(menuItem.target || {}), - }; +export function getGlobalPageMenus(): RegisteredPageMenu[] { + const extensions = extensionLoader.allEnabledInstances as LensRendererExtension[]; - return menuItem; - }); - - return super.add(normalizedItems); - } + return extensions.flatMap(ext => ext[registeredGlobalPageMenus]); } -export class ClusterPageMenuRegistry extends PageMenuRegistry { - getRootItems() { - return this.getItems().filter((item) => !item.parentId); - } +function getClusterPageMenus(): RegisteredClusterPageMenu[] { + const extensions = extensionLoader.allEnabledInstances as LensRendererExtension[]; - getSubItems(parent: ClusterPageMenuRegistration) { - return this.getItems().filter((item) => ( - item.parentId === parent.id && - item.target.extensionId === parent.target.extensionId + return extensions.flatMap(ext => ext[registeredClusterPageMenus]); +} + +export function getRootClusterPageMenus(): RegisteredClusterPageMenu[] { + return getClusterPageMenus().filter(pageMenu => !pageMenu.parentId); +} + +export function getChildClusterPageMenus(parentMenu: RegisteredClusterPageMenu): RegisteredClusterPageMenu[] { + return getClusterPageMenus() + .filter(pageMenu => ( + pageMenu.parentId === parentMenu.id + && pageMenu.target.extensionName === parentMenu.target.extensionName )); - } - - getByPage({ id: pageId, extensionId }: RegisteredPage) { - return this.getItems().find((item) => ( - item.target.pageId == pageId && - item.target.extensionId === extensionId - )); - } } -export const globalPageMenuRegistry = new PageMenuRegistry(); -export const clusterPageMenuRegistry = new ClusterPageMenuRegistry(); +export function getClusterPageMenuByPage({ id: pageId, extensionName }: RegisteredPage): RegisteredClusterPageMenu { + return getClusterPageMenus() + .find(pageMenu => ( + pageMenu.target.pageId == pageId + && pageMenu.target.extensionName === extensionName + )); +} diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts index 0ec6f27da0..a6fd3b440c 100644 --- a/src/extensions/registries/page-registry.ts +++ b/src/extensions/registries/page-registry.ts @@ -2,10 +2,15 @@ import React from "react"; import { observer } from "mobx-react"; -import { BaseRegistry } from "./base-registry"; -import { LensExtension, sanitizeExtensionName } from "../lens-extension"; +import { sanitizeExtensionName } from "../lens-extension"; import { PageParam, PageParamInit } from "../../renderer/navigation/page-param"; import { createPageParam } from "../../renderer/navigation/helpers"; +import { extensionLoader } from "../extension-loader"; +import { LensRendererExtension } from "../core-api"; +import { registeredClusterPages, registeredGlobalPages } from "../lens-renderer-extension"; +import { TabLayoutRoute } from "../renderer-api/components"; +import { RegistrationScope } from "./sources"; +import { getChildClusterPageMenus, RegisteredClusterPageMenu } from "./page-menu-registry"; export interface PageRegistration { /** @@ -24,12 +29,16 @@ export interface PageComponents { Page: React.ComponentType; } -export interface PageTarget

{ - extensionId?: string; +export interface PageTarget

{ + extensionName?: string; pageId?: string; params?: P; } +export interface RegisteredPageTarget

extends PageTarget

{ + extensionName: string +} + export interface PageParams { [paramName: string]: V; } @@ -42,81 +51,153 @@ export interface PageComponentProps

{ export interface RegisteredPage { id: string; - extensionId: string; + extensionName: string; url: string; // registered extension's page URL (without page params) params: PageParams; // normalized params components: PageComponents; // normalized components } -export function getExtensionPageUrl(target: PageTarget): string { - const { extensionId, pageId = "", params: targetParams = {} } = target; +/** + * Finds the first registered page on `extension` matching `pageId` in all of `sources`' scopes + * @param extension The extension to query for a matching `RegisteredPage` + * @param pageId The `PageId` to search for + * @param sources Whether to search for global pages or cluster pages or both + */ +export function findRegisteredPage(extension: LensRendererExtension | undefined, pageId?: string, sources = new Set([RegistrationScope.GLOBAL, RegistrationScope.CLUSTER])): RegisteredPage | null { + if (sources.has(RegistrationScope.GLOBAL)) { + const page = extension?.[registeredGlobalPages].find(page => page.id === pageId); - const pagePath = ["/extension", sanitizeExtensionName(extensionId), pageId] + if (page) { + return page; + } + } + + if (sources.has(RegistrationScope.CLUSTER)) { + const page = extension?.[registeredClusterPages].find(page => page.id === pageId); + + if (page) { + return page; + } + } + + return null; +} + +export function getExtensionPageUrl(target: PageTarget): string { + const { extensionName, pageId = "", params: targetParams = {} } = target; + + const pagePath = ["/extension", sanitizeExtensionName(extensionName), pageId] .filter(Boolean) .join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id) const pageUrl = new URL(pagePath, `http://localhost`); // stringify params to matched target page - const registeredPage = globalPageRegistry.getByPageTarget(target) || clusterPageRegistry.getByPageTarget(target); + const extension = extensionLoader.getExtensionByName(extensionName); - if (registeredPage?.params) { - Object.entries(registeredPage.params).forEach(([name, param]) => { - const paramValue = param.stringify(targetParams[name]); + if (extension instanceof LensRendererExtension) { + const registeredPage = findRegisteredPage(extension, target.pageId); - if (param.init.skipEmpty && param.isEmpty(paramValue)) { - pageUrl.searchParams.delete(name); - } else { - pageUrl.searchParams.set(name, paramValue); - } - }); + if (registeredPage?.params) { + Object.entries(registeredPage.params).forEach(([name, param]) => { + const paramValue = param.stringify(targetParams[name]); + + if (param.init.skipEmpty && param.isEmpty(paramValue)) { + pageUrl.searchParams.delete(name); + } else { + pageUrl.searchParams.set(name, paramValue); + } + }); + } } + return pageUrl.href.replace(pageUrl.origin, ""); } -export class PageRegistry extends BaseRegistry { - protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage { - const { id: pageId } = page; - const extensionId = ext.name; - const params = this.normalizeParams(page.params); - const components = this.normalizeComponents(page.components, params); - const url = getExtensionPageUrl({ extensionId, pageId }); +function normalizeComponents(components: PageComponents, params?: PageParams): PageComponents { + if (params) { + const { Page } = components; - return { - id: pageId, extensionId, params, components, url, - }; + components.Page = observer((props: object) => React.createElement(Page, { params, ...props })); } - protected normalizeComponents(components: PageComponents, params?: PageParams): PageComponents { - if (params) { - const { Page } = components; + return components; +} - components.Page = observer((props: object) => React.createElement(Page, { params, ...props })); - } - - return components; +function normalizeParams(params?: PageParams): PageParams { + if (!params) { + return; } - protected normalizeParams(params?: PageParams): PageParams { - if (!params) { - return; - } - Object.entries(params).forEach(([name, value]) => { - const paramInit: PageParamInit = typeof value === "object" - ? { name, ...value } - : { name, defaultValue: value }; + Object.entries(params).forEach(([name, value]) => { + const paramInit: PageParamInit = typeof value === "object" + ? { name, ...value } + : { name, defaultValue: value }; - params[paramInit.name] = createPageParam(paramInit); - }); + params[paramInit.name] = createPageParam(paramInit); + }); - return params as PageParams; - } + return params as PageParams; +} - getByPageTarget(target: PageTarget): RegisteredPage | null { - return this.getItems().find(page => page.extensionId === target.extensionId && page.id === target.pageId) || null; +export function getRegisteredPage({ id, ...page}: PageRegistration, extensionName: string): RegisteredPage { + const params = normalizeParams(page.params); + const components = normalizeComponents(page.components); + + const pagePath = ["/extension", sanitizeExtensionName(extensionName), id] + .filter(Boolean) + .join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id) + + const pageUrl = new URL(pagePath, `http://localhost`); + const url = pageUrl.href.replace(pageUrl.origin, ""); + + return { id, params, components, extensionName, url }; +} + +/** + * Find the `RegisteredPage` of an extension looking through all `sources` + * @param target The `extensionName` and `pageId` for the desired page + * @param sources Whether to search for global pages or cluster pages or both + */ +export function getByPageTarget(target?: PageTarget, sources = new Set([RegistrationScope.GLOBAL, RegistrationScope.CLUSTER])): RegisteredPage | null { + return findRegisteredPage( + extensionLoader.getExtensionByName(target?.extensionName) as LensRendererExtension, + target?.pageId, + sources, + ); +} + +/** + * Gets all the registered pages from all extensions + * @param source Whether to get all the global or cluster pages + */ +export function getAllRegisteredPages(source: RegistrationScope): RegisteredPage[] { + const extensions = extensionLoader.allEnabledInstances as LensRendererExtension[]; + + switch (source) { + case RegistrationScope.GLOBAL: + return extensions.flatMap(ext => ext[registeredGlobalPages]); + case RegistrationScope.CLUSTER: + return extensions.flatMap(ext => ext[registeredClusterPages]); } } -export const globalPageRegistry = new PageRegistry(); -export const clusterPageRegistry = new PageRegistry(); +export function getTabLayoutRoutes(parentMenu: RegisteredClusterPageMenu): TabLayoutRoute[] { + if (!parentMenu.id) { + return []; + } + + return getChildClusterPageMenus(parentMenu) + .map(subMenu => [ + getByPageTarget(subMenu.target), + subMenu, + ] as const) + .filter(([subPage]) => subPage) + .map(([{ components, extensionName, id: pageId, url }, { title, target: { params } }]) => ({ + routePath: url, + url: getExtensionPageUrl({ extensionName, pageId, params }), + title, + component: components.Page, + })); +} diff --git a/src/extensions/registries/sources.ts b/src/extensions/registries/sources.ts new file mode 100644 index 0000000000..38feb9b490 --- /dev/null +++ b/src/extensions/registries/sources.ts @@ -0,0 +1,7 @@ +/** + * This represents which sources the RegisteredPages should be searched from + */ +export enum RegistrationScope { + GLOBAL = "global", + CLUSTER = "cluster", +} diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index f5ad684595..f2440ec6e2 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -33,14 +33,13 @@ import { Terminal } from "./dock/terminal"; import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store"; import logger from "../../main/logger"; import { webFrame } from "electron"; -import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry"; import { extensionLoader } from "../../extensions/extension-loader"; import { appEventBus } from "../../common/event-bus"; import { broadcastMessage, requestMain } from "../../common/ipc"; import whatInput from "what-input"; import { clusterSetFrameIdHandler } from "../../common/cluster-ipc"; -import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries"; -import { TabLayout, TabLayoutRoute } from "./layout/tab-layout"; +import { getAllRegisteredPages, getByPageTarget, getChildClusterPageMenus, getClusterPageMenuByPage, getExtensionPageUrl, getRootClusterPageMenus, getTabLayoutRoutes, RegisteredClusterPageMenu, RegistrationScope } from "../../extensions/registries"; +import { TabLayout } from "./layout/tab-layout"; import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog"; import { eventStore } from "./+events/event.store"; import { nodesStore } from "./+nodes/nodes.store"; @@ -100,38 +99,32 @@ export class App extends React.Component { return nodesStore.getWarningsCount() + eventStore.getWarningsCount(); } - getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) { - const routes: TabLayoutRoute[] = []; - + getTabLayoutRoutes(menuItem: RegisteredClusterPageMenu) { if (!menuItem.id) { - return routes; + return []; } - clusterPageMenuRegistry.getSubItems(menuItem).forEach((subMenu) => { - const page = clusterPageRegistry.getByPageTarget(subMenu.target); - if (page) { - routes.push({ - routePath: page.url, - url: getExtensionPageUrl(subMenu.target), - title: subMenu.title, - component: page.components.Page, - }); - } - }); - - return routes; + return getChildClusterPageMenus(menuItem) + .map(subMenu => [getByPageTarget(subMenu.target), subMenu] as const) + .filter(([page]) => page) + .map(([page, subMenu]) => ({ + routePath: page.url, + url: getExtensionPageUrl(subMenu.target), + title: subMenu.title, + component: page.components.Page, + })); } renderExtensionTabLayoutRoutes() { - return clusterPageMenuRegistry.getRootItems().map((menu, index) => { - const tabRoutes = this.getTabLayoutRoutes(menu); + return getRootClusterPageMenus().map((menu, index) => { + const tabRoutes = getTabLayoutRoutes(menu); if (tabRoutes.length > 0) { const pageComponent = () => ; return tab.routePath)}/>; } else { - const page = clusterPageRegistry.getByPageTarget(menu.target); + const page = getByPageTarget(menu.target, new Set([RegistrationScope.CLUSTER])); if (page) { return ; @@ -141,8 +134,8 @@ export class App extends React.Component { } renderExtensionRoutes() { - return clusterPageRegistry.getItems().map((page, index) => { - const menu = clusterPageMenuRegistry.getByPage(page); + return getAllRegisteredPages(RegistrationScope.CLUSTER).map((page, index) => { + const menu = getClusterPageMenuByPage(page); if (!menu) { return ; diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 1c3306b65f..1ce530a159 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -14,9 +14,9 @@ import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings"; import { clusterViewRoute, clusterViewURL } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; -import { globalPageRegistry } from "../../../extensions/registries/page-registry"; import { Extensions, extensionsRoute } from "../+extensions"; import { getMatchedClusterId } from "../../navigation"; +import { getAllRegisteredPages, RegistrationScope } from "../../../extensions/registries"; @observer export class ClusterManager extends React.Component { @@ -69,7 +69,7 @@ export class ClusterManager extends React.Component { - {globalPageRegistry.getItems().map(({ url, components: { Page } }) => { + {getAllRegisteredPages(RegistrationScope.GLOBAL).map(({ url, components: { Page } }) => { return ; })} diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 03711e41fc..28e5244a79 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -15,7 +15,7 @@ import { addClusterURL } from "../+add-cluster"; import { landingURL } from "../+landing-page"; import { clusterViewURL } from "./cluster-view.route"; import { ClusterActions } from "./cluster-actions"; -import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; +import { getByPageTarget, getExtensionPageUrl, getGlobalPageMenus, RegistrationScope } from "../../../extensions/registries"; import { commandRegistry } from "../../../extensions/registries/command-registry"; import { CommandOverlay } from "../command-palette/command-container"; import { computed, observable } from "mobx"; @@ -135,8 +135,8 @@ export class ClustersMenu extends React.Component {

- {globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => { - const registeredPage = globalPageRegistry.getByPageTarget(target); + {getGlobalPageMenus().map(({ title, target, components: { Icon } }) => { + const registeredPage = getByPageTarget(target, new Set([RegistrationScope.GLOBAL])); if (!registeredPage){ return; diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 7f0ddd818f..cad35c2d1d 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -27,8 +27,8 @@ import { CustomResources } from "../+custom-resources/custom-resources"; import { isActiveRoute } from "../../navigation"; import { isAllowedResource } from "../../../common/rbac"; import { Spinner } from "../spinner"; -import { ClusterPageMenuRegistration, clusterPageMenuRegistry, clusterPageRegistry, getExtensionPageUrl } from "../../../extensions/registries"; import { SidebarItem } from "./sidebar-item"; +import { getByPageTarget, getExtensionPageUrl, getRootClusterPageMenus, getTabLayoutRoutes, RegistrationScope } from "../../../extensions/registries"; interface Props { className?: string; @@ -50,14 +50,12 @@ export class Sidebar extends React.Component { } return Object.entries(crdStore.groups).map(([group, crds]) => { - const submenus: TabLayoutRoute[] = crds.map((crd) => { - return { - title: crd.getResourceKind(), - component: CrdList, - url: crd.getResourceUrl(), - routePath: String(crdResourcesRoute.path), - }; - }); + const submenus: TabLayoutRoute[] = crds.map(crd => ({ + title: crd.getResourceKind(), + component: CrdList, + url: crd.getResourceUrl(), + routePath: String(crdResourcesRoute.path), + })); return ( { }); } - getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] { - const routes: TabLayoutRoute[] = []; - - if (!menu.id) { - return routes; - } - - clusterPageMenuRegistry.getSubItems(menu).forEach((subMenu) => { - const subPage = clusterPageRegistry.getByPageTarget(subMenu.target); - - if (subPage) { - const { extensionId, id: pageId } = subPage; - - routes.push({ - routePath: subPage.url, - url: getExtensionPageUrl({ extensionId, pageId, params: subMenu.target.params }), - title: subMenu.title, - component: subPage.components.Page, - }); - } - }); - - return routes; - } - renderRegisteredMenus() { - return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => { - const registeredPage = clusterPageRegistry.getByPageTarget(menuItem.target); - const tabRoutes = this.getTabLayoutRoutes(menuItem); + return getRootClusterPageMenus().map((menuItem, index) => { + const registeredPage = getByPageTarget(menuItem.target, new Set([RegistrationScope.CLUSTER])); + const tabRoutes = getTabLayoutRoutes(menuItem); const id = `registered-item-${index}`; let pageUrl: string; let isActive = false; if (registeredPage) { - const { extensionId, id: pageId } = registeredPage; + const { extensionName, id: pageId } = registeredPage; - pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params }); + pageUrl = getExtensionPageUrl({ extensionName, pageId, params: menuItem.target.params }); isActive = isActiveRoute(registeredPage.url); } else if (tabRoutes.length > 0) { pageUrl = tabRoutes[0].url;