this.navigate()}>
+
this.navigate("/support")}>
)
diff --git a/src/extensions/__tests__/lens-extension.test.ts b/src/extensions/__tests__/lens-extension.test.ts
new file mode 100644
index 0000000000..c7e9ed5513
--- /dev/null
+++ b/src/extensions/__tests__/lens-extension.test.ts
@@ -0,0 +1,23 @@
+import { LensExtension } from "../lens-extension"
+
+let ext: LensExtension = null
+
+describe("lens extension", () => {
+ beforeEach(async () => {
+ ext = new LensExtension({
+ manifest: {
+ name: "foo-bar",
+ version: "0.1.1"
+ },
+ manifestPath: "/this/is/fake/package.json",
+ isBundled: false,
+ isEnabled: true
+ })
+ })
+
+ describe("name", () => {
+ it("returns name", () => {
+ expect(ext.name).toBe("foo-bar")
+ })
+ })
+})
diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts
index b75fd5147d..969cff33b1 100644
--- a/src/extensions/extension-loader.ts
+++ b/src/extensions/extension-loader.ts
@@ -57,29 +57,29 @@ export class ExtensionLoader {
loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoInitExtensions((ext: LensMainExtension) => [
- registries.menuRegistry.add(ext.appMenus, { key: ext })
+ registries.menuRegistry.add(ext.appMenus)
]);
}
loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoInitExtensions((ext: LensRendererExtension) => [
- registries.globalPageRegistry.add(ext.globalPages, { key: ext }),
- registries.globalPageMenuRegistry.add(ext.globalPageMenus, { key: ext }),
- registries.appPreferenceRegistry.add(ext.appPreferences, { key: ext }),
- registries.clusterFeatureRegistry.add(ext.clusterFeatures, { key: ext }),
- registries.statusBarRegistry.add(ext.statusBarItems, { key: ext }),
+ registries.globalPageRegistry.add(ext.globalPages, ext),
+ registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
+ registries.appPreferenceRegistry.add(ext.appPreferences),
+ registries.clusterFeatureRegistry.add(ext.clusterFeatures),
+ registries.statusBarRegistry.add(ext.statusBarItems),
]);
}
loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoInitExtensions((ext: LensRendererExtension) => [
- registries.clusterPageRegistry.add(ext.clusterPages, { key: ext }),
- registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, { key: ext }),
- registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems, { key: ext }),
- registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems, { key: ext }),
- registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts, { key: ext })
+ registries.clusterPageRegistry.add(ext.clusterPages, ext),
+ registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
+ registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
+ registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
+ registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
])
}
diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts
index ca135a0b39..f1ffb9184b 100644
--- a/src/extensions/lens-extension.ts
+++ b/src/extensions/lens-extension.ts
@@ -1,6 +1,5 @@
import type { InstalledExtension } from "./extension-manager";
import { action, observable, reaction } from "mobx";
-import { compile } from "path-to-regexp"
import logger from "../main/logger";
export type LensExtensionId = string; // path to manifest (package.json)
@@ -15,7 +14,6 @@ export interface LensExtensionManifest {
}
export class LensExtension {
- readonly routePrefix = "/extension/:name"
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean;
@@ -44,14 +42,6 @@ export class LensExtension {
return this.manifest.description
}
- getPageUrl(baseUrl = "") {
- return compile(this.routePrefix)({ name: this.name }) + baseUrl;
- }
-
- getPageRoute(baseRoute = "") {
- return this.routePrefix + baseRoute;
- }
-
@action
async enable() {
if (this.isEnabled) return;
diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts
index 0055344a66..de9aae40ec 100644
--- a/src/extensions/lens-main-extension.ts
+++ b/src/extensions/lens-main-extension.ts
@@ -2,13 +2,14 @@ import type { MenuRegistration } from "./registries/menu-registry";
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
import { WindowManager } from "../main/window-manager";
+import { getPageUrl } from "./registries/page-registry"
export class LensMainExtension extends LensExtension {
@observable.shallow appMenus: MenuRegistration[] = []
async navigate(location?: string, frameId?: number) {
const windowManager = WindowManager.getInstance
();
- const url = this.getPageUrl(location); // get full path to extension's page
+ const url = getPageUrl(this, location); // get full path to extension's page
await windowManager.navigate(url, frameId);
}
}
diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts
index 87ca214805..5968124cbc 100644
--- a/src/extensions/lens-renderer-extension.ts
+++ b/src/extensions/lens-renderer-extension.ts
@@ -1,6 +1,7 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
+import { getPageUrl } from "./registries/page-registry"
export class LensRendererExtension extends LensExtension {
@observable.shallow globalPages: PageRegistration[] = []
@@ -16,6 +17,6 @@ export class LensRendererExtension extends LensExtension {
async navigate(location?: string) {
const { navigate } = await import("../renderer/navigation");
- navigate(this.getPageUrl(location));
+ navigate(getPageUrl(this, location));
}
}
diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts
new file mode 100644
index 0000000000..b293b598dc
--- /dev/null
+++ b/src/extensions/registries/__tests__/page-registry.test.ts
@@ -0,0 +1,31 @@
+import { getPageUrl } from "../page-registry"
+import { LensExtension } from "../../lens-extension"
+
+let ext: LensExtension = null
+
+describe("getPageUrl", () => {
+ beforeEach(async () => {
+ ext = new LensExtension({
+ manifest: {
+ name: "foo-bar",
+ version: "0.1.1"
+ },
+ manifestPath: "/this/is/fake/package.json",
+ isBundled: false,
+ isEnabled: true
+ })
+ })
+
+ it("returns a page url for extension", () => {
+ expect(getPageUrl(ext)).toBe("/extension/foo-bar")
+ })
+
+ it("allows to pass base url as parameter", () => {
+ expect(getPageUrl(ext, "/test")).toBe("/extension/foo-bar/test")
+ })
+
+ it("removes @", () => {
+ ext.manifest.name = "@foo/bar"
+ expect(getPageUrl(ext)).toBe("/extension/foo-bar")
+ })
+})
diff --git a/src/extensions/registries/app-preference-registry.ts b/src/extensions/registries/app-preference-registry.ts
index dc15ec3e20..6c54911f82 100644
--- a/src/extensions/registries/app-preference-registry.ts
+++ b/src/extensions/registries/app-preference-registry.ts
@@ -1,12 +1,12 @@
import type React from "react"
-import { BaseRegistry, BaseRegistryItem } from "./base-registry";
+import { BaseRegistry } from "./base-registry";
export interface AppPreferenceComponents {
Hint: React.ComponentType;
Input: React.ComponentType;
}
-export interface AppPreferenceRegistration extends BaseRegistryItem {
+export interface AppPreferenceRegistration {
title: string;
components: AppPreferenceComponents;
}
diff --git a/src/extensions/registries/base-registry.ts b/src/extensions/registries/base-registry.ts
index 76678791f9..bc23c29f5d 100644
--- a/src/extensions/registries/base-registry.ts
+++ b/src/extensions/registries/base-registry.ts
@@ -1,65 +1,24 @@
// Base class for extensions-api registries
import { action, observable } from "mobx";
-import { LensExtension } from "../lens-extension";
-import { getRandId } from "../../common/utils";
-export type BaseRegistryKey = LensExtension | null;
-export type BaseRegistryItemId = string | symbol;
+export class BaseRegistry {
+ private items = observable([], { deep: false });
-export interface BaseRegistryItem {
- id?: BaseRegistryItemId; // uniq id, generated automatically when not provided
-}
-
-export interface BaseRegistryAddMeta {
- key?: BaseRegistryKey;
- merge?: boolean
-}
-
-export class BaseRegistry {
- private items = observable.map([], { deep: false });
-
- getItems(): (T & { extension?: LensExtension | null })[] {
- return Array.from(this.items).map(([ext, items]) => {
- return items.map(item => ({
- ...item,
- extension: ext,
- }))
- }).flat()
- }
-
- getById(itemId: BaseRegistryItemId, key?: BaseRegistryKey): T {
- const byId = (item: BaseRegistryItem) => item.id === itemId;
- if (key) {
- return this.items.get(key)?.find(byId)
- }
- return this.getItems().find(byId);
+ getItems(): T[] {
+ return this.items.toJS();
}
@action
- add(items: T | T[], { key = null, merge = true }: BaseRegistryAddMeta = {}) {
- const normalizedItems = (Array.isArray(items) ? items : [items]).map((item: T) => {
- item.id = item.id || getRandId();
- return item;
- });
- if (merge && this.items.has(key)) {
- const newItems = new Set(this.items.get(key));
- normalizedItems.forEach(item => newItems.add(item))
- this.items.set(key, [...newItems]);
- } else {
- this.items.set(key, normalizedItems);
- }
- return () => this.remove(normalizedItems, key)
+ add(items: T | T[]) {
+ const normalizedItems = (Array.isArray(items) ? items : [items])
+ this.items.push(...normalizedItems);
+ return () => this.remove(...normalizedItems);
}
@action
- remove(items: T[], key: BaseRegistryKey = null) {
- const storedItems = this.items.get(key);
- if (!storedItems) return;
- const newItems = storedItems.filter(item => !items.includes(item)); // works because of {deep: false};
- if (newItems.length > 0) {
- this.items.set(key, newItems)
- } else {
- this.items.delete(key);
- }
+ remove(...items: T[]) {
+ items.forEach(item => {
+ this.items.remove(item); // works because of {deep: false};
+ })
}
}
diff --git a/src/extensions/registries/cluster-feature-registry.ts b/src/extensions/registries/cluster-feature-registry.ts
index 17a4ef1eef..0e3363d0f0 100644
--- a/src/extensions/registries/cluster-feature-registry.ts
+++ b/src/extensions/registries/cluster-feature-registry.ts
@@ -1,12 +1,12 @@
import type React from "react"
-import { BaseRegistry, BaseRegistryItem } from "./base-registry";
+import { BaseRegistry } from "./base-registry";
import { ClusterFeature } from "../cluster-feature";
export interface ClusterFeatureComponents {
Description: React.ComponentType;
}
-export interface ClusterFeatureRegistration extends BaseRegistryItem {
+export interface ClusterFeatureRegistration {
title: string;
components: ClusterFeatureComponents
feature: ClusterFeature
diff --git a/src/extensions/registries/kube-object-detail-registry.ts b/src/extensions/registries/kube-object-detail-registry.ts
index 63d9cfa30d..32fea66b81 100644
--- a/src/extensions/registries/kube-object-detail-registry.ts
+++ b/src/extensions/registries/kube-object-detail-registry.ts
@@ -1,11 +1,11 @@
import React from "react"
-import { BaseRegistry, BaseRegistryItem } from "./base-registry";
+import { BaseRegistry } from "./base-registry";
export interface KubeObjectDetailComponents {
Details: React.ComponentType;
}
-export interface KubeObjectDetailRegistration extends BaseRegistryItem {
+export interface KubeObjectDetailRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectDetailComponents;
diff --git a/src/extensions/registries/kube-object-menu-registry.ts b/src/extensions/registries/kube-object-menu-registry.ts
index 3e0cfb0251..8f527d6a3d 100644
--- a/src/extensions/registries/kube-object-menu-registry.ts
+++ b/src/extensions/registries/kube-object-menu-registry.ts
@@ -1,11 +1,11 @@
import React from "react"
-import { BaseRegistry, BaseRegistryItem } from "./base-registry";
+import { BaseRegistry } from "./base-registry";
export interface KubeObjectMenuComponents {
MenuItem: React.ComponentType;
}
-export interface KubeObjectMenuRegistration extends BaseRegistryItem {
+export interface KubeObjectMenuRegistration {
kind: string;
apiVersions: string[];
components: KubeObjectMenuComponents;
diff --git a/src/extensions/registries/kube-object-status-registry.ts b/src/extensions/registries/kube-object-status-registry.ts
index 3a516a82e1..74fd8145d2 100644
--- a/src/extensions/registries/kube-object-status-registry.ts
+++ b/src/extensions/registries/kube-object-status-registry.ts
@@ -1,7 +1,7 @@
import { KubeObject, KubeObjectStatus } from "../renderer-api/k8s-api";
-import { BaseRegistry, BaseRegistryItem } from "./base-registry";
+import { BaseRegistry } from "./base-registry";
-export interface KubeObjectStatusRegistration extends BaseRegistryItem {
+export interface KubeObjectStatusRegistration {
kind: string;
apiVersions: string[];
resolve: (object: KubeObject) => KubeObjectStatus;
diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts
index 6bd161ccb2..dc1ef6669c 100644
--- a/src/extensions/registries/page-menu-registry.ts
+++ b/src/extensions/registries/page-menu-registry.ts
@@ -1,15 +1,21 @@
// Extensions-api -> Register page menu items
import type React from "react";
+import { action } from "mobx";
import type { IconProps } from "../../renderer/components/icon";
-import { BaseRegistry, BaseRegistryItem, BaseRegistryItemId } from "./base-registry";
+import { BaseRegistry } from "./base-registry";
+import { LensExtension } from "../lens-extension";
-export interface PageMenuRegistration extends BaseRegistryItem {
- id: BaseRegistryItemId; // required id from page-registry item to match with
- url?: string; // when not provided initial extension's path used, e.g. "/extension/lens-extension-name"
+export interface PageMenuTarget {
+ pageId: string;
+ extensionId?: string;
+ params?: object;
+}
+
+export interface PageMenuRegistration {
+ target?: PageMenuTarget;
title: React.ReactNode;
components: PageMenuComponents;
- subMenus?: PageSubMenuRegistration[];
}
export interface PageSubMenuRegistration {
@@ -22,13 +28,18 @@ export interface PageMenuComponents {
}
export class PageMenuRegistry extends BaseRegistry {
- getItems() {
- return super.getItems().map(item => {
- item.url = item.extension.getPageUrl(item.url)
- return item
- });
+
+ @action
+ add(items: T[], ext?: LensExtension) {
+ const normalizedItems = items.map((menu) => {
+ if (menu.target && !menu.target.extensionId) {
+ menu.target.extensionId = ext.name
+ }
+ return menu
+ })
+ return super.add(normalizedItems);
}
}
-export const globalPageMenuRegistry = new PageMenuRegistry>();
-export const clusterPageMenuRegistry = new PageMenuRegistry();
+export const globalPageMenuRegistry = new PageMenuRegistry();
+export const clusterPageMenuRegistry = new PageMenuRegistry();
diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts
index 886ab8a584..7c34a7b9c4 100644
--- a/src/extensions/registries/page-registry.ts
+++ b/src/extensions/registries/page-registry.ts
@@ -1,13 +1,17 @@
// Extensions-api -> Custom page registration
import React from "react";
-import { BaseRegistry, BaseRegistryItem } from "./base-registry";
+import { action } from "mobx";
+import { compile } from "path-to-regexp";
+import { BaseRegistry } from "./base-registry";
+import { LensExtension } from "../lens-extension"
+import type { PageMenuTarget } from "./page-menu-registry";
-export interface PageRegistration extends BaseRegistryItem {
+export interface PageRegistration {
+ id: string; // will be automatically prefixed with extension name
routePath?: string; // additional (suffix) route path to base extension's route: "/extension/:name"
exact?: boolean; // route matching flag, see: https://reactrouter.com/web/api/NavLink/exact-bool
components: PageComponents;
- subPages?: SubPageRegistration[];
}
export interface SubPageRegistration {
@@ -20,14 +24,31 @@ export interface PageComponents {
Page: React.ComponentType;
}
+const routePrefix = "/extension/:name"
+
+export function getPageUrl(ext: LensExtension, baseUrl = "") {
+ const validUrlName = ext.name.replace("@", "").replace("/", "-");
+ return compile(routePrefix)({ name: validUrlName }) + baseUrl;
+}
+
export class PageRegistry extends BaseRegistry {
- getItems() {
- return super.getItems().map(item => {
- item.routePath = item.extension.getPageRoute(item.routePath)
- return item
- });
+
+ @action
+ add(items: T[], ext?: LensExtension) {
+ const normalizedItems = items.map((i) => {
+ i.routePath = getPageUrl(ext, i.routePath)
+ return i
+ })
+ return super.add(normalizedItems);
+ }
+
+ getByPageMenuTarget(target: PageMenuTarget) {
+ if (!target) {
+ return null
+ }
+ return this.getItems().find((page) => page.routePath.startsWith(`/extension/${target.extensionId}/`) && page.id === target.pageId)
}
}
-export const globalPageRegistry = new PageRegistry>();
-export const clusterPageRegistry = new PageRegistry();
+export const globalPageRegistry = new PageRegistry();
+export const clusterPageRegistry = new PageRegistry();
diff --git a/src/extensions/registries/status-bar-registry.ts b/src/extensions/registries/status-bar-registry.ts
index c1029e6d68..88c4132d30 100644
--- a/src/extensions/registries/status-bar-registry.ts
+++ b/src/extensions/registries/status-bar-registry.ts
@@ -1,9 +1,9 @@
// Extensions API -> Status bar customizations
import React from "react";
-import { BaseRegistry, BaseRegistryItem } from "./base-registry";
+import { BaseRegistry } from "./base-registry";
-export interface StatusBarRegistration extends BaseRegistryItem {
+export interface StatusBarRegistration {
item?: React.ReactNode;
}
diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx
index 45b7d565f2..cb8d02172b 100755
--- a/src/renderer/components/app.tsx
+++ b/src/renderer/components/app.tsx
@@ -74,27 +74,8 @@ export class App extends React.Component {
}
renderExtensionRoutes() {
- return clusterPageRegistry.getItems().map(({ id: pageId, components: { Page }, exact, routePath, subPages }) => {
+ return clusterPageRegistry.getItems().map(({ components: { Page }, exact, routePath }) => {
const Component = () => {
- if (subPages) {
- const tabs: TabLayoutRoute[] = subPages.map(({ exact, routePath, components: { Page } }) => {
- const menuItem = clusterPageMenuRegistry.getById(pageId);
- if (!menuItem) return;
- return {
- routePath, exact,
- component: Page,
- url: menuItem.url,
- title: menuItem.title,
- }
- }).filter(Boolean);
- if (tabs.length > 0) {
- return (
-
-
-
- )
- }
- }
return
};
return
diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx
index 7844399227..302e4e76d2 100644
--- a/src/renderer/components/cluster-manager/clusters-menu.tsx
+++ b/src/renderer/components/cluster-manager/clusters-menu.tsx
@@ -5,7 +5,6 @@ import { remote } from "electron"
import type { Cluster } from "../../../main/cluster";
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { observer } from "mobx-react";
-import { matchPath } from "react-router";
import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro";
import { userStore } from "../../../common/user-store";
@@ -15,7 +14,7 @@ import { ClusterIcon } from "../cluster-icon";
import { Icon } from "../icon";
import { autobind, cssNames, IClassName } from "../../utils";
import { Badge } from "../badge";
-import { navigate, navigation } from "../../navigation";
+import { isActiveRoute, navigate } from "../../navigation";
import { addClusterURL } from "../+add-cluster";
import { clusterSettingsURL } from "../+cluster-settings";
import { landingURL } from "../+landing-page";
@@ -24,6 +23,7 @@ import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL } from "./cluster-view.route";
import { globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries";
+import { compile } from "path-to-regexp";
interface Props {
className?: IClassName;
@@ -149,17 +149,16 @@ export class ClustersMenu extends React.Component {
)}
- {globalPageMenuRegistry.getItems().map(({ id: menuItemId, title, url, components: { Icon } }) => {
- const registeredPage = globalPageRegistry.getById(menuItemId);
+ {globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
+ const registeredPage = globalPageRegistry.getByPageMenuTarget(target);
if (!registeredPage) return;
const { routePath, exact } = registeredPage;
- const isActive = !!matchPath(navigation.location.pathname, { path: routePath, exact });
return (
navigate(url)}
+ active={isActiveRoute({ path: routePath, exact })}
+ onClick={() => navigate(compile(routePath)(target.params))}
/>
)
})}
diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx
index b70d7a3e62..8485f1e134 100644
--- a/src/renderer/components/layout/sidebar.tsx
+++ b/src/renderer/components/layout/sidebar.tsx
@@ -30,6 +30,7 @@ import { isActiveRoute } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"
import { Spinner } from "../spinner";
import { clusterPageMenuRegistry, clusterPageRegistry } from "../../../extensions/registries";
+import { compile } from "path-to-regexp";
const SidebarContext = React.createContext({ pinned: false });
type SidebarContextValue = {
@@ -191,10 +192,11 @@ export class Sidebar extends React.Component {
>
{this.renderCustomResources()}
- {clusterPageMenuRegistry.getItems().map(({ id: menuItemId, title, url, components: { Icon } }) => {
- const registeredPage = clusterPageRegistry.getById(menuItemId);
+ {clusterPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
+ const registeredPage = clusterPageRegistry.getByPageMenuTarget(target);
if (!registeredPage) return;
const { routePath, exact } = registeredPage;
+ const url = compile(routePath)(target.params)
return (