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;