diff --git a/src/behaviours/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap b/src/behaviours/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap
new file mode 100644
index 0000000000..98d203ca6a
--- /dev/null
+++ b/src/behaviours/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap
@@ -0,0 +1,1418 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`disable-cluster-pages-when-cluster-is-not-relevant given extension should be enabled for the cluster, when navigating renders 1`] = `
+
+
+
+`;
+
+exports[`disable-cluster-pages-when-cluster-is-not-relevant given extension shouldn't be enabled for the cluster, when navigating renders 1`] = `
+
+
+
+`;
+
+exports[`disable-cluster-pages-when-cluster-is-not-relevant given not yet known if extension should be enabled for the cluster, when navigating renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/cluster/extension-api/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx b/src/behaviours/cluster/extension-api/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx
new file mode 100644
index 0000000000..d49d40d2bc
--- /dev/null
+++ b/src/behaviours/cluster/extension-api/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { RenderResult } from "@testing-library/react";
+import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import type { TestExtensionRenderer } from "../../../renderer/components/test-utils/get-extension-fake";
+import { getExtensionFakeFor } from "../../../renderer/components/test-utils/get-extension-fake";
+import type { KubernetesCluster } from "../../../common/catalog-entities";
+import React from "react";
+import extensionShouldBeEnabledForClusterFrameInjectable from "../../../renderer/extension-loader/extension-should-be-enabled-for-cluster-frame.injectable";
+
+describe("disable-cluster-pages-when-cluster-is-not-relevant", () => {
+ let builder: ApplicationBuilder;
+ let rendered: RenderResult;
+ let rendererTestExtension: TestExtensionRenderer;
+ let isEnabledForClusterMock: AsyncFnMock<(cluster: KubernetesCluster) => boolean>;
+
+ beforeEach(async () => {
+ builder = getApplicationBuilder();
+
+ builder.setEnvironmentToClusterFrame();
+
+ builder.dis.rendererDi.unoverride(extensionShouldBeEnabledForClusterFrameInjectable);
+
+ const getExtensionFake = getExtensionFakeFor(builder);
+
+ isEnabledForClusterMock = asyncFn();
+
+ const testExtension = getExtensionFake({
+ id: "test-extension-id",
+ name: "test-extension",
+
+ rendererOptions: {
+ isEnabledForCluster: isEnabledForClusterMock,
+
+ clusterPages: [{
+ components: {
+ Page: () => Some page
,
+ },
+ }],
+ },
+ });
+
+ rendererTestExtension = testExtension.renderer;
+
+ rendered = await builder.render();
+
+ builder.extensions.enable(testExtension);
+ });
+
+ describe("given not yet known if extension should be enabled for the cluster, when navigating", () => {
+ beforeEach(() => {
+ rendererTestExtension.navigate();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show the page", () => {
+ const actual = rendered.queryByTestId("some-test-page");
+
+ expect(actual).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given extension shouldn't be enabled for the cluster, when navigating", () => {
+ beforeEach(async () => {
+ await isEnabledForClusterMock.resolve(false);
+
+ rendererTestExtension.navigate();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show the page", () => {
+ const actual = rendered.queryByTestId("some-test-page");
+
+ expect(actual).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given extension should be enabled for the cluster, when navigating", () => {
+ beforeEach(async () => {
+ await isEnabledForClusterMock.resolve(true);
+
+ rendererTestExtension.navigate();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows the page", () => {
+ const actual = rendered.getByTestId("some-test-page");
+
+ expect(actual).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/extensions/extension-loader/extension-is-enabled-for-cluster.injectable.ts b/src/extensions/extension-loader/extension-is-enabled-for-cluster.injectable.ts
new file mode 100644
index 0000000000..23e1c87561
--- /dev/null
+++ b/src/extensions/extension-loader/extension-is-enabled-for-cluster.injectable.ts
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
+import type { KubernetesCluster } from "../../common/catalog-entities";
+import type { LensRendererExtension } from "../lens-renderer-extension";
+
+interface ExtensionIsEnabledForCluster {
+ extension: LensRendererExtension;
+ cluster: KubernetesCluster;
+}
+
+const extensionIsEnabledForClusterInjectable = getInjectable({
+ id: "extension-is-enabled-for-cluster",
+
+ instantiate: async (
+ di,
+ { extension, cluster }: ExtensionIsEnabledForCluster,
+ ) => (await extension.isEnabledForCluster(cluster)) as boolean,
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (
+ di,
+ { extension, cluster }: ExtensionIsEnabledForCluster,
+ ) => `${extension.sanitizedExtensionId}-${cluster.getId()}`,
+ }),
+});
+
+export default extensionIsEnabledForClusterInjectable;
diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts
index 7fe1cd5421..c0cff52785 100644
--- a/src/extensions/extension-loader/extension-loader.injectable.ts
+++ b/src/extensions/extension-loader/extension-loader.injectable.ts
@@ -9,6 +9,9 @@ import { createExtensionInstanceInjectionToken } from "./create-extension-instan
import extensionInstancesInjectable from "./extension-instances.injectable";
import type { LensExtension } from "../lens-extension";
import extensionInjectable from "./extension/extension.injectable";
+import type { LensRendererExtension } from "../lens-renderer-extension";
+import extensionIsEnabledForClusterInjectable from "./extension-is-enabled-for-cluster.injectable";
+import type { KubernetesCluster } from "../../common/catalog-entities";
const extensionLoaderInjectable = getInjectable({
id: "extension-loader",
@@ -18,6 +21,11 @@ const extensionLoaderInjectable = getInjectable({
createExtensionInstance: di.inject(createExtensionInstanceInjectionToken),
extensionInstances: di.inject(extensionInstancesInjectable),
getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance),
+
+ getExtensionIsEnabledForCluster: (extension: LensRendererExtension, cluster: KubernetesCluster) => di.inject(extensionIsEnabledForClusterInjectable, {
+ extension,
+ cluster,
+ }),
}),
});
diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts
index 0b0db85819..2071400564 100644
--- a/src/extensions/extension-loader/extension-loader.ts
+++ b/src/extensions/extension-loader/extension-loader.ts
@@ -32,6 +32,7 @@ interface Dependencies {
createExtensionInstance: CreateExtensionInstance;
readonly extensionInstances: ObservableMap;
getExtension: (instance: LensExtension) => Extension;
+ getExtensionIsEnabledForCluster: (extension: LensRendererExtension, cluster: KubernetesCluster) => Promise;
}
export interface ExtensionLoading {
@@ -282,7 +283,7 @@ export class ExtensionLoader {
const extension = ext as LensRendererExtension;
// getCluster must be a callback, as the entity might be available only after an extension has been loaded
- if ((await extension.isEnabledForCluster(entity)) === false) {
+ if (await this.dependencies.getExtensionIsEnabledForCluster(extension, entity)) {
return [];
}
diff --git a/src/renderer/cluster-frame-context/active-kubernetes-cluster.injectable.ts b/src/renderer/cluster-frame-context/active-kubernetes-cluster.injectable.ts
new file mode 100644
index 0000000000..db6e109767
--- /dev/null
+++ b/src/renderer/cluster-frame-context/active-kubernetes-cluster.injectable.ts
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import catalogEntityRegistryInjectable from "../api/catalog/entity/registry.injectable";
+import { computed } from "mobx";
+import { isKubernetesCluster } from "../../common/catalog-entities";
+
+const activeKubernetesClusterInjectable = getInjectable({
+ id: "active-kubernetes-cluster",
+
+ instantiate: (di) => {
+ const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
+
+ return computed(() => {
+ const activeEntity = catalogEntityRegistry.activeEntity;
+
+ if (!isKubernetesCluster(activeEntity)) {
+ return null;
+ }
+
+ return activeEntity;
+ });
+ },
+});
+
+export default activeKubernetesClusterInjectable;
diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx
index 174686bbdc..a83fd0d45d 100644
--- a/src/renderer/components/test-utils/get-application-builder.tsx
+++ b/src/renderer/components/test-utils/get-application-builder.tsx
@@ -58,6 +58,8 @@ import { renderFor } from "./renderFor";
import { RootFrame } from "../../frames/root-frame/root-frame";
import { ClusterFrame } from "../../frames/cluster-frame/cluster-frame";
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
+import activeKubernetesClusterInjectable from "../../cluster-frame-context/active-kubernetes-cluster.injectable";
+import { catalogEntityFromCluster } from "../../../main/cluster-manager";
type Callback = (dis: DiContainers) => void | Promise;
@@ -382,6 +384,10 @@ export const getApplicationBuilder = () => {
accessibleNamespaces: [],
} as unknown as Cluster;
+ rendererDi.override(activeKubernetesClusterInjectable, () =>
+ computed(() => catalogEntityFromCluster(clusterStub)),
+ );
+
const namespaceStoreStub = {
contextNamespaces: [],
items: [],
diff --git a/src/renderer/extension-loader/extension-should-be-enabled-for-cluster-frame.injectable.ts b/src/renderer/extension-loader/extension-should-be-enabled-for-cluster-frame.injectable.ts
new file mode 100644
index 0000000000..575ae3482e
--- /dev/null
+++ b/src/renderer/extension-loader/extension-should-be-enabled-for-cluster-frame.injectable.ts
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
+import { asyncComputed } from "@ogre-tools/injectable-react";
+import type { LensRendererExtension } from "../../extensions/lens-renderer-extension";
+import type { KubernetesCluster } from "../../common/catalog-entities";
+import extensionIsEnabledForClusterInjectable from "../../extensions/extension-loader/extension-is-enabled-for-cluster.injectable";
+import activeKubernetesClusterInjectable from "../cluster-frame-context/active-kubernetes-cluster.injectable";
+
+const extensionShouldBeEnabledForClusterFrameInjectable = getInjectable({
+ id: "extension-should-be-enabled-for-cluster-frame",
+
+ instantiate: (di, extension: LensRendererExtension) => {
+ const activeKubernetesCluster = di.inject(activeKubernetesClusterInjectable);
+
+ const getExtensionIsEnabledForCluster = (
+ extension: LensRendererExtension,
+ cluster: KubernetesCluster,
+ ) =>
+ di.inject(extensionIsEnabledForClusterInjectable, { extension, cluster });
+
+ return asyncComputed(
+ async () => {
+ const cluster = activeKubernetesCluster.get();
+
+ if (!cluster) {
+ return false;
+ }
+
+ return getExtensionIsEnabledForCluster(
+ extension,
+ cluster,
+ );
+ },
+
+ false,
+ );
+ },
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, extension: LensRendererExtension) =>
+ extension.sanitizedExtensionId,
+ }),
+});
+
+export default extensionShouldBeEnabledForClusterFrameInjectable;
diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx
index f70f8938cb..8b950f9d1d 100644
--- a/src/renderer/getDiForUnitTesting.tsx
+++ b/src/renderer/getDiForUnitTesting.tsx
@@ -69,6 +69,8 @@ import kubeObjectDetailsClusterFrameChildComponentInjectable from "./components/
import kubeconfigDialogClusterFrameChildComponentInjectable from "./components/kubeconfig-dialog/kubeconfig-dialog-cluster-frame-child-component.injectable";
import portForwardDialogClusterFrameChildComponentInjectable from "./port-forward/port-forward-dialog-cluster-frame-child-component.injectable";
import setupSystemCaInjectable from "./frames/root-frame/setup-system-ca.injectable";
+import extensionShouldBeEnabledForClusterFrameInjectable from "./extension-loader/extension-should-be-enabled-for-cluster-frame.injectable";
+import { asyncComputed } from "@ogre-tools/injectable-react";
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
const {
@@ -119,6 +121,11 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
shouldRender: computed(() => false),
}));
+ // TODO: Remove after "LensRendererExtension.isEnabledForCluster" is removed
+ di.override(extensionShouldBeEnabledForClusterFrameInjectable, () =>
+ asyncComputed(async () => true, true),
+ );
+
// TODO: Remove side-effects and shared global state
const clusterFrameChildComponentInjectables: Injectable[] = [
commandContainerClusterFrameChildComponentInjectable,
diff --git a/src/renderer/routes/extension-route-registrator.injectable.tsx b/src/renderer/routes/extension-route-registrator.injectable.tsx
index 54eecb3fe1..241e6950a5 100644
--- a/src/renderer/routes/extension-route-registrator.injectable.tsx
+++ b/src/renderer/routes/extension-route-registrator.injectable.tsx
@@ -14,9 +14,11 @@ import { extensionRegistratorInjectionToken } from "../../extensions/extension-l
import { SiblingsInTabLayout } from "../components/layout/siblings-in-tab-layout";
import extensionPageParametersInjectable from "./extension-page-parameters.injectable";
import { routeSpecificComponentInjectionToken } from "./route-specific-component-injection-token";
+import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../common/front-end-routing/front-end-route-injection-token";
import { getExtensionRoutePath } from "./for-extension";
+import extensionShouldBeEnabledForClusterFrameInjectable from "../extension-loader/extension-should-be-enabled-for-cluster-frame.injectable";
const extensionRouteRegistratorInjectable = getInjectable({
id: "extension-route-registrator",
@@ -24,14 +26,27 @@ const extensionRouteRegistratorInjectable = getInjectable({
instantiate: (di) => {
return (ext) => {
const extension = ext as LensRendererExtension;
- const toRouteInjectable = toRouteInjectableFor(
- di,
+ const toRouteInjectable = toRouteInjectableFor(di, extension);
+
+ const extensionShouldBeEnabledForClusterFrame = di.inject(
+ extensionShouldBeEnabledForClusterFrameInjectable,
extension,
);
return [
- ...extension.globalPages.map(toRouteInjectable(false)),
- ...extension.clusterPages.map(toRouteInjectable(true)),
+ ...extension.globalPages.map(
+ toRouteInjectable(
+ false,
+ computed(() => true),
+ ),
+ ),
+
+ ...extension.clusterPages.map(
+ toRouteInjectable(
+ true,
+ computed(() => extensionShouldBeEnabledForClusterFrame.value.get()),
+ ),
+ ),
].flat();
};
},
@@ -46,7 +61,7 @@ const toRouteInjectableFor =
di: DiContainerForInjection,
extension: LensRendererExtension,
) =>
- (clusterFrame: boolean) =>
+ (clusterFrame: boolean, isEnabled: IComputedValue) =>
(registration: PageRegistration) => {
const routeInjectable = getInjectable({
id: `route-${registration.id}-for-extension-${extension.sanitizedExtensionId}`,
@@ -54,7 +69,7 @@ const toRouteInjectableFor =
instantiate: () => ({
path: getExtensionRoutePath(extension, registration.id),
clusterFrame,
- isEnabled: computed(() => true),
+ isEnabled,
extension,
}),