diff --git a/src/behaviours/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap b/src/behaviours/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap
new file mode 100644
index 000000000000..c350b67280e2
--- /dev/null
+++ b/src/behaviours/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap
@@ -0,0 +1,1563 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`disable sidebar items when cluster is not relevant given extension should be enabled for the cluster renders 1`] = `
+
+
+
+`;
+
+exports[`disable sidebar items when cluster is not relevant given extension shouldn't be enabled for the cluster renders 1`] = `
+
+
+
+`;
+
+exports[`disable sidebar items when cluster is not relevant given not yet known if extension should be enabled for the cluster renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/cluster/extension-api/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx b/src/behaviours/cluster/extension-api/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx
new file mode 100644
index 000000000000..28007355b17d
--- /dev/null
+++ b/src/behaviours/cluster/extension-api/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx
@@ -0,0 +1,111 @@
+/**
+ * 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 { 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 sidebar items when cluster is not relevant", () => {
+ let builder: ApplicationBuilder;
+ let rendered: RenderResult;
+ let isEnabledForClusterMock: AsyncFnMock<(cluster: KubernetesCluster) => Promise>;
+
+ 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
,
+ },
+ }],
+
+ clusterPageMenus: [
+ {
+ id: "some-sidebar-item",
+ title: "Some sidebar item",
+
+ components: {
+ Icon: () => Some icon
,
+ },
+ },
+ ],
+ },
+ });
+
+ rendered = await builder.render();
+
+ builder.extensions.enable(testExtension);
+ });
+
+ describe("given not yet known if extension should be enabled for the cluster", () => {
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show the sidebar item", () => {
+ const actual = rendered.queryByTestId(
+ "sidebar-item-some-extension-name-some-sidebar-item",
+ );
+
+ expect(actual).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given extension shouldn't be enabled for the cluster", () => {
+ beforeEach(async () => {
+ await isEnabledForClusterMock.resolve(false);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show the sidebar item", () => {
+ const actual = rendered.queryByTestId(
+ "sidebar-item-some-extension-name-some-sidebar-item",
+ );
+
+ expect(actual).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given extension should be enabled for the cluster", () => {
+ beforeEach(async () => {
+ await isEnabledForClusterMock.resolve(true);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows the sidebar item", () => {
+ const actual = rendered.getByTestId(
+ "sidebar-item-test-extension-some-sidebar-item",
+ );
+
+ expect(actual).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/behaviours/cluster/kube-object-menu/extension-api/__snapshots__/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx.snap b/src/behaviours/cluster/kube-object-menu/extension-api/__snapshots__/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx.snap
new file mode 100644
index 000000000000..9d116616fd19
--- /dev/null
+++ b/src/behaviours/cluster/kube-object-menu/extension-api/__snapshots__/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx.snap
@@ -0,0 +1,1189 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`disable kube object menu items when cluster is not relevant given extension should be enabled for the cluster renders 1`] = `
+
+
+
+`;
+
+exports[`disable kube object menu items when cluster is not relevant given extension shouldn't be enabled for the cluster renders 1`] = `
+
+
+
+`;
+
+exports[`disable kube object menu items when cluster is not relevant given not yet known if extension should be enabled for the cluster renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/cluster/kube-object-menu/extension-api/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx b/src/behaviours/cluster/kube-object-menu/extension-api/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx
new file mode 100644
index 000000000000..98ec08aa8d1b
--- /dev/null
+++ b/src/behaviours/cluster/kube-object-menu/extension-api/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx
@@ -0,0 +1,165 @@
+/**
+ * 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 type { KubernetesCluster } from "../../../../common/catalog-entities";
+import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
+import { getExtensionFakeFor } from "../../../../renderer/components/test-utils/get-extension-fake";
+import { getInjectable } from "@ogre-tools/injectable";
+import { frontEndRouteInjectionToken } from "../../../../common/front-end-routing/front-end-route-injection-token";
+import { computed } from "mobx";
+import React from "react";
+import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
+import { routeSpecificComponentInjectionToken } from "../../../../renderer/routes/route-specific-component-injection-token";
+import { KubeObject } from "../../../../common/k8s-api/kube-object";
+import extensionShouldBeEnabledForClusterFrameInjectable from "../../../../renderer/extension-loader/extension-should-be-enabled-for-cluster-frame.injectable";
+import { KubeObjectMenu } from "../../../../renderer/components/kube-object-menu";
+import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
+
+describe("disable kube object menu items when cluster is not relevant", () => {
+ let builder: ApplicationBuilder;
+ let rendered: RenderResult;
+ let isEnabledForClusterMock: AsyncFnMock<
+ (cluster: KubernetesCluster) => Promise
+ >;
+
+ beforeEach(async () => {
+ builder = getApplicationBuilder();
+
+ builder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.override(apiManagerInjectable, () => ({}));
+ });
+
+ const rendererDi = builder.dis.rendererDi;
+
+ rendererDi.unoverride(extensionShouldBeEnabledForClusterFrameInjectable);
+
+ rendererDi.register(testRouteInjectable, testRouteComponentInjectable);
+
+ builder.setEnvironmentToClusterFrame();
+
+ const getExtensionFake = getExtensionFakeFor(builder);
+
+ isEnabledForClusterMock = asyncFn();
+
+ const testExtension = getExtensionFake({
+ id: "test-extension-id",
+ name: "test-extension",
+
+ rendererOptions: {
+ isEnabledForCluster: isEnabledForClusterMock,
+
+ kubeObjectMenuItems: [
+ {
+ kind: "some-kind",
+ apiVersions: ["some-api-version"],
+ components: {
+ MenuItem: () => (
+ Some menu item
+ ),
+ },
+ },
+ ],
+ },
+ });
+
+ rendered = await builder.render();
+
+ const navigateToRoute = rendererDi.inject(navigateToRouteInjectionToken);
+ const testRoute = rendererDi.inject(testRouteInjectable);
+
+ navigateToRoute(testRoute);
+
+ builder.extensions.enable(testExtension);
+ });
+
+ describe("given not yet known if extension should be enabled for the cluster", () => {
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show the kube object menu item", () => {
+ const actual = rendered.queryByTestId("some-test-id");
+
+ expect(actual).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given extension shouldn't be enabled for the cluster", () => {
+ beforeEach(async () => {
+ await isEnabledForClusterMock.resolve(false);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show the kube object menu item", () => {
+ const actual = rendered.queryByTestId("some-test-id");
+
+ expect(actual).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given extension should be enabled for the cluster", () => {
+ beforeEach(async () => {
+ await isEnabledForClusterMock.resolve(true);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows the kube object menu item", () => {
+ const actual = rendered.getByTestId("some-test-id");
+
+ expect(actual).toBeInTheDocument();
+ });
+ });
+});
+
+const testRouteInjectable = getInjectable({
+ id: "test-route",
+
+ instantiate: () => ({
+ path: "/test-route",
+ clusterFrame: true,
+ isEnabled: computed(() => true),
+ }),
+
+ injectionToken: frontEndRouteInjectionToken,
+});
+
+const testRouteComponentInjectable = getInjectable({
+ id: "test-route-component",
+
+ instantiate: (di) => ({
+ route: di.inject(testRouteInjectable),
+
+ Component: () => (
+
+ ),
+ }),
+
+ injectionToken: routeSpecificComponentInjectionToken,
+});
+
+const getKubeObjectStub = (kind: string, apiVersion: string) =>
+ KubeObject.create({
+ apiVersion,
+ kind,
+ metadata: {
+ uid: "some-uid",
+ name: "some-name",
+ resourceVersion: "some-resource-version",
+ namespace: "some-namespace",
+ selfLink: "",
+ },
+ });
diff --git a/src/behaviours/cluster/kube-object-status-icon/extension-api/__snapshots__/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx.snap b/src/behaviours/cluster/kube-object-status-icon/extension-api/__snapshots__/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx.snap
new file mode 100644
index 000000000000..2fb124eb282c
--- /dev/null
+++ b/src/behaviours/cluster/kube-object-status-icon/extension-api/__snapshots__/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx.snap
@@ -0,0 +1,1181 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`disable kube object statuses when cluster is not relevant given extension should be enabled for the cluster renders 1`] = `
+
+
+
+`;
+
+exports[`disable kube object statuses when cluster is not relevant given extension shouldn't be enabled for the cluster renders 1`] = `
+
+
+
+`;
+
+exports[`disable kube object statuses when cluster is not relevant given not yet known if extension should be enabled for the cluster renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/cluster/kube-object-status-icon/extension-api/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx b/src/behaviours/cluster/kube-object-status-icon/extension-api/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx
new file mode 100644
index 000000000000..da3ac641a9be
--- /dev/null
+++ b/src/behaviours/cluster/kube-object-status-icon/extension-api/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx
@@ -0,0 +1,160 @@
+/**
+ * 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 type { KubernetesCluster } from "../../../../common/catalog-entities";
+import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
+import { getExtensionFakeFor } from "../../../../renderer/components/test-utils/get-extension-fake";
+import { getInjectable } from "@ogre-tools/injectable";
+import { frontEndRouteInjectionToken } from "../../../../common/front-end-routing/front-end-route-injection-token";
+import { computed } from "mobx";
+import React from "react";
+import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
+import { routeSpecificComponentInjectionToken } from "../../../../renderer/routes/route-specific-component-injection-token";
+import { KubeObjectStatusIcon } from "../../../../renderer/components/kube-object-status-icon/kube-object-status-icon";
+import { KubeObject } from "../../../../common/k8s-api/kube-object";
+import { KubeObjectStatusLevel } from "../../../../common/k8s-api/kube-object-status";
+import extensionShouldBeEnabledForClusterFrameInjectable from "../../../../renderer/extension-loader/extension-should-be-enabled-for-cluster-frame.injectable";
+
+describe("disable kube object statuses when cluster is not relevant", () => {
+ let builder: ApplicationBuilder;
+ let rendered: RenderResult;
+ let isEnabledForClusterMock: AsyncFnMock<
+ (cluster: KubernetesCluster) => Promise
+ >;
+
+ beforeEach(async () => {
+ builder = getApplicationBuilder();
+
+ const rendererDi = builder.dis.rendererDi;
+
+ rendererDi.unoverride(extensionShouldBeEnabledForClusterFrameInjectable);
+
+ rendererDi.register(testRouteInjectable, testRouteComponentInjectable);
+
+ builder.setEnvironmentToClusterFrame();
+
+ const getExtensionFake = getExtensionFakeFor(builder);
+
+ isEnabledForClusterMock = asyncFn();
+
+ const testExtension = getExtensionFake({
+ id: "test-extension-id",
+ name: "test-extension",
+
+ rendererOptions: {
+ isEnabledForCluster: isEnabledForClusterMock,
+
+ kubeObjectStatusTexts: [
+ {
+ kind: "some-kind",
+ apiVersions: ["some-api-version"],
+
+ resolve: () => ({
+ level: KubeObjectStatusLevel.CRITICAL,
+ text: "some-kube-object-status-text",
+ }),
+ },
+ ],
+ },
+ });
+
+ rendered = await builder.render();
+
+ const navigateToRoute = rendererDi.inject(navigateToRouteInjectionToken);
+ const testRoute = rendererDi.inject(testRouteInjectable);
+
+ navigateToRoute(testRoute);
+
+ builder.extensions.enable(testExtension);
+ });
+
+ describe("given not yet known if extension should be enabled for the cluster", () => {
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show the status", () => {
+ const actual = rendered.baseElement.querySelectorAll(".KubeObjectStatusIcon");
+
+ expect(actual).toHaveLength(0);
+ });
+ });
+
+ describe("given extension shouldn't be enabled for the cluster", () => {
+ beforeEach(async () => {
+ await isEnabledForClusterMock.resolve(false);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show the status", () => {
+ const actual = rendered.baseElement.querySelectorAll(".KubeObjectStatusIcon");
+
+ expect(actual).toHaveLength(0);
+ });
+ });
+
+ describe("given extension should be enabled for the cluster", () => {
+ beforeEach(async () => {
+ await isEnabledForClusterMock.resolve(true);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows the status", () => {
+ const actual = rendered.baseElement.querySelectorAll(".KubeObjectStatusIcon");
+
+ expect(actual).toHaveLength(1);
+ });
+ });
+});
+
+const testRouteInjectable = getInjectable({
+ id: "test-route",
+
+ instantiate: () => ({
+ path: "/test-route",
+ clusterFrame: true,
+ isEnabled: computed(() => true),
+ }),
+
+ injectionToken: frontEndRouteInjectionToken,
+});
+
+const testRouteComponentInjectable = getInjectable({
+ id: "test-route-component",
+
+ instantiate: (di) => ({
+ route: di.inject(testRouteInjectable),
+
+ Component: () => (
+
+ ),
+ }),
+
+ injectionToken: routeSpecificComponentInjectionToken,
+});
+
+const getKubeObjectStub = (kind: string, apiVersion: string) =>
+ KubeObject.create({
+ apiVersion,
+ kind,
+ metadata: {
+ uid: "some-uid",
+ name: "some-name",
+ resourceVersion: "some-resource-version",
+ namespace: "some-namespace",
+ selfLink: "/foo",
+ },
+ });
diff --git a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/get-kube-object-menu-items.ts b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/get-kube-object-menu-items.ts
deleted file mode 100644
index 7539edd041e5..000000000000
--- a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/get-kube-object-menu-items.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { conforms, includes, eq } from "lodash/fp";
-import type { KubeObject } from "../../../../../common/k8s-api/kube-object";
-import type { LensRendererExtension } from "../../../../../extensions/lens-renderer-extension";
-import { staticKubeObjectMenuItems as staticMenuItems } from "../static-kube-object-menu-items";
-
-interface Dependencies {
- extensions: LensRendererExtension[];
- kubeObject: KubeObject | null | undefined;
-}
-
-export const getKubeObjectMenuItems = ({
- extensions,
- kubeObject,
-}: Dependencies) => {
- return [
- ...staticMenuItems,
- ...extensions.flatMap((extension) => extension.kubeObjectMenuItems),
- ]
- .filter(
- conforms({
- kind: eq(kubeObject?.kind),
- apiVersions: includes(kubeObject?.apiVersion),
- }),
- )
- .map((item) => item.components.MenuItem);
-};
-
diff --git a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-items.injectable.ts b/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-items.injectable.ts
deleted file mode 100644
index 1f9f8586e356..000000000000
--- a/src/renderer/components/kube-object-menu/dependencies/kube-object-menu-items/kube-object-menu-items.injectable.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * 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 { getKubeObjectMenuItems } from "./get-kube-object-menu-items";
-import type { KubeObject } from "../../../../../common/k8s-api/kube-object";
-import rendererExtensionsInjectable from "../../../../../extensions/renderer-extensions.injectable";
-
-const kubeObjectMenuItemsInjectable = getInjectable({
- id: "kube-object-menu-items",
- instantiate: (di, { kubeObject }: { kubeObject: KubeObject }) =>
- getKubeObjectMenuItems({
- extensions: di.inject(rendererExtensionsInjectable).get(),
- kubeObject,
- }),
-
- lifecycle: lifecycleEnum.transient,
-});
-
-export default kubeObjectMenuItemsInjectable;
diff --git a/src/renderer/components/kube-object-menu/dependencies/static-kube-object-menu-items.ts b/src/renderer/components/kube-object-menu/dependencies/static-kube-object-menu-items.ts
deleted file mode 100644
index b087a6551a48..000000000000
--- a/src/renderer/components/kube-object-menu/dependencies/static-kube-object-menu-items.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { ServiceAccountMenu } from "../../+user-management/+service-accounts/service-account-menu";
-import { CronJobMenu } from "../../+workloads-cronjobs/cron-job-menu";
-import { DeploymentMenu } from "../../+workloads-deployments/deployment-menu";
-import { ReplicaSetMenu } from "../../+workloads-replicasets/replica-set-menu";
-
-export const staticKubeObjectMenuItems = [
- {
- kind: "ServiceAccount",
- apiVersions: ["v1"],
- components: {
- MenuItem: ServiceAccountMenu,
- },
- },
- {
- kind: "CronJob",
- apiVersions: ["batch/v1beta1"],
- components: {
- MenuItem: CronJobMenu,
- },
- },
- {
- kind: "Deployment",
- apiVersions: ["apps/v1"],
- components: {
- MenuItem: DeploymentMenu,
- },
- },
- {
- kind: "ReplicaSet",
- apiVersions: ["apps/v1"],
- components: {
- MenuItem: ReplicaSetMenu,
- },
- },
-];
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu-item-injection-token.ts b/src/renderer/components/kube-object-menu/kube-object-menu-item-injection-token.ts
new file mode 100644
index 000000000000..a3ec5dba4b84
--- /dev/null
+++ b/src/renderer/components/kube-object-menu/kube-object-menu-item-injection-token.ts
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectionToken } from "@ogre-tools/injectable";
+import type { IComputedValue } from "mobx";
+import type { KubeObjectMenuProps } from "./kube-object-menu";
+import type { KubeObject } from "../../../common/k8s-api/kube-object";
+import type React from "react";
+
+export type KubeObjectMenuItemComponent = React.ElementType<
+ KubeObjectMenuProps
+>;
+
+export interface KubeObjectMenuItem {
+ kind: string;
+ apiVersions: string[];
+ enabled: IComputedValue;
+ Component: KubeObjectMenuItemComponent;
+ orderNumber: number;
+}
+
+export const kubeObjectMenuItemInjectionToken = getInjectionToken({
+ id: "kube-object-menu-item-injection-token",
+});
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu-item-registrator.injectable.ts b/src/renderer/components/kube-object-menu/kube-object-menu-item-registrator.injectable.ts
new file mode 100644
index 000000000000..4ee77ff4d0b2
--- /dev/null
+++ b/src/renderer/components/kube-object-menu/kube-object-menu-item-registrator.injectable.ts
@@ -0,0 +1,59 @@
+/**
+ * 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 { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
+import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
+import getRandomIdInjectable from "../../../common/utils/get-random-id.injectable";
+import extensionShouldBeEnabledForClusterFrameInjectable from "../../extension-loader/extension-should-be-enabled-for-cluster-frame.injectable";
+import { kubeObjectMenuItemInjectionToken } from "./kube-object-menu-item-injection-token";
+import { computed } from "mobx";
+
+const kubeObjectMenuItemRegistratorInjectable = getInjectable({
+ id: "kube-object-menu-item-registrator",
+
+ instantiate: (di) => {
+ const getRandomId = di.inject(getRandomIdInjectable);
+
+ const getExtensionShouldBeEnabledForClusterFrame = (
+ extension: LensRendererExtension,
+ ) =>
+ di.inject(extensionShouldBeEnabledForClusterFrameInjectable, extension);
+
+ return (ext) => {
+ const extension = ext as LensRendererExtension;
+
+ const extensionShouldBeEnabledForClusterFrame =
+ getExtensionShouldBeEnabledForClusterFrame(extension);
+
+ return extension.kubeObjectMenuItems.map((registration) => {
+ const id = `kube-object-menu-item-registration-from-${
+ extension.sanitizedExtensionId
+ }-${getRandomId()}`;
+
+ return getInjectable({
+ id,
+
+ instantiate: () => ({
+ kind: registration.kind,
+ apiVersions: registration.apiVersions,
+ Component: registration.components.MenuItem,
+
+ enabled: computed(() =>
+ extensionShouldBeEnabledForClusterFrame.value.get(),
+ ),
+
+ orderNumber: 100,
+ }),
+
+ injectionToken: kubeObjectMenuItemInjectionToken,
+ });
+ });
+ };
+ },
+
+ injectionToken: extensionRegistratorInjectionToken,
+});
+
+export default kubeObjectMenuItemRegistratorInjectable;
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu-items.injectable.ts b/src/renderer/components/kube-object-menu/kube-object-menu-items.injectable.ts
new file mode 100644
index 000000000000..796a805dfdda
--- /dev/null
+++ b/src/renderer/components/kube-object-menu/kube-object-menu-items.injectable.ts
@@ -0,0 +1,43 @@
+/**
+ * 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 { KubeObject } from "../../../common/k8s-api/kube-object";
+import { computed } from "mobx";
+import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
+import { kubeObjectMenuItemInjectionToken } from "./kube-object-menu-item-injection-token";
+import { filter, map, sortBy } from "lodash/fp";
+import { pipeline } from "@ogre-tools/fp";
+
+const kubeObjectMenuItemsInjectable = getInjectable({
+ id: "kube-object-menu-items",
+
+ instantiate: (di, kubeObject: KubeObject) => {
+ const computedInjectMany = di.inject(computedInjectManyInjectable);
+ const menuItems = computedInjectMany(kubeObjectMenuItemInjectionToken);
+
+ return computed(() =>
+ pipeline(
+ menuItems.get(),
+
+ filter(
+ (item) =>
+ item.kind === kubeObject?.kind &&
+ item.apiVersions.includes(kubeObject?.apiVersion) &&
+ item.enabled.get(),
+ ),
+
+ sortBy((item) => item.orderNumber),
+ map((item) => item.Component),
+ ),
+ );
+ },
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, kubeObject: KubeObject) => kubeObject?.getId(),
+ }),
+});
+
+export default kubeObjectMenuItemsInjectable;
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu-items/cron-job-menu.injectable.ts b/src/renderer/components/kube-object-menu/kube-object-menu-items/cron-job-menu.injectable.ts
new file mode 100644
index 000000000000..256e49736f62
--- /dev/null
+++ b/src/renderer/components/kube-object-menu/kube-object-menu-items/cron-job-menu.injectable.ts
@@ -0,0 +1,25 @@
+/**
+ * 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 type { KubeObjectMenuItemComponent } from "../kube-object-menu-item-injection-token";
+import { kubeObjectMenuItemInjectionToken } from "../kube-object-menu-item-injection-token";
+import { computed } from "mobx";
+import { CronJobMenu } from "../../+workloads-cronjobs/cron-job-menu";
+
+const cronJobMenuInjectable = getInjectable({
+ id: "cron-job-menu-kube-object-menu",
+
+ instantiate: () => ( {
+ kind: "CronJob",
+ apiVersions: ["batch/v1beta1"],
+ Component: CronJobMenu as KubeObjectMenuItemComponent,
+ enabled: computed(() => true),
+ orderNumber: 20,
+ }),
+
+ injectionToken: kubeObjectMenuItemInjectionToken,
+});
+
+export default cronJobMenuInjectable;
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu-items/deployment-menu.injectable.ts b/src/renderer/components/kube-object-menu/kube-object-menu-items/deployment-menu.injectable.ts
new file mode 100644
index 000000000000..9791b2f20a5e
--- /dev/null
+++ b/src/renderer/components/kube-object-menu/kube-object-menu-items/deployment-menu.injectable.ts
@@ -0,0 +1,25 @@
+/**
+ * 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 type { KubeObjectMenuItemComponent } from "../kube-object-menu-item-injection-token";
+import { kubeObjectMenuItemInjectionToken } from "../kube-object-menu-item-injection-token";
+import { computed } from "mobx";
+import { DeploymentMenu } from "../../+workloads-deployments/deployment-menu";
+
+const deploymentMenuInjectable = getInjectable({
+ id: "deployment-menu-kube-object-menu",
+
+ instantiate: () => ({
+ kind: "Deployment",
+ apiVersions: ["apps/v1"],
+ Component: DeploymentMenu as KubeObjectMenuItemComponent,
+ enabled: computed(() => true),
+ orderNumber: 30,
+ }),
+
+ injectionToken: kubeObjectMenuItemInjectionToken,
+});
+
+export default deploymentMenuInjectable;
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu-items/replica-set-menu.injectable.ts b/src/renderer/components/kube-object-menu/kube-object-menu-items/replica-set-menu.injectable.ts
new file mode 100644
index 000000000000..bb80bcd8fe25
--- /dev/null
+++ b/src/renderer/components/kube-object-menu/kube-object-menu-items/replica-set-menu.injectable.ts
@@ -0,0 +1,25 @@
+/**
+ * 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 type { KubeObjectMenuItemComponent } from "../kube-object-menu-item-injection-token";
+import { kubeObjectMenuItemInjectionToken } from "../kube-object-menu-item-injection-token";
+import { computed } from "mobx";
+import { ReplicaSetMenu } from "../../+workloads-replicasets/replica-set-menu";
+
+const replicaSetMenuInjectable = getInjectable({
+ id: "replica-set-menu-kube-object-menu",
+
+ instantiate: () => ({
+ kind: "ReplicaSet",
+ apiVersions: ["apps/v1"],
+ Component: ReplicaSetMenu as KubeObjectMenuItemComponent,
+ enabled: computed(() => true),
+ orderNumber: 40,
+ }),
+
+ injectionToken: kubeObjectMenuItemInjectionToken,
+});
+
+export default replicaSetMenuInjectable;
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu-items/service-account-menu.injectable.ts b/src/renderer/components/kube-object-menu/kube-object-menu-items/service-account-menu.injectable.ts
new file mode 100644
index 000000000000..37399ad86342
--- /dev/null
+++ b/src/renderer/components/kube-object-menu/kube-object-menu-items/service-account-menu.injectable.ts
@@ -0,0 +1,25 @@
+/**
+ * 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 type { KubeObjectMenuItemComponent } from "../kube-object-menu-item-injection-token";
+import { kubeObjectMenuItemInjectionToken } from "../kube-object-menu-item-injection-token";
+import { ServiceAccountMenu } from "../../+user-management/+service-accounts/service-account-menu";
+import { computed } from "mobx";
+
+const serviceAccountMenuInjectable = getInjectable({
+ id: "service-account-menu-kube-object-menu",
+
+ instantiate: () => ({
+ kind: "ServiceAccount",
+ apiVersions: ["v1"],
+ Component: ServiceAccountMenu as KubeObjectMenuItemComponent,
+ enabled: computed(() => true),
+ orderNumber: 10,
+ }),
+
+ injectionToken: kubeObjectMenuItemInjectionToken,
+});
+
+export default serviceAccountMenuInjectable;
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx
index 9dabdc9da42a..0f0b338d8f17 100644
--- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx
+++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx
@@ -8,12 +8,13 @@ import { screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import userEvent from "@testing-library/user-event";
+import { getInjectable } from "@ogre-tools/injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import { ConfirmDialog } from "../confirm-dialog";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
-
+import { computed } from "mobx";
import clusterInjectable from "./dependencies/cluster.injectable";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
@@ -21,12 +22,9 @@ import type { Cluster } from "../../../common/cluster/cluster";
import type { ApiManager } from "../../../common/k8s-api/api-manager";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import { KubeObjectMenu } from "./index";
-import type { KubeObjectMenuRegistration } from "./kube-object-menu-registration";
-import { computed } from "mobx";
-import { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
-import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable";
import hideDetailsInjectable from "../kube-detail-params/hide-details.injectable";
+import { kubeObjectMenuItemInjectionToken } from "./kube-object-menu-item-injection-token";
// TODO: Make tooltips free of side effects by making it deterministic
jest.mock("../tooltip/tooltip");
@@ -35,59 +33,21 @@ jest.mock("../tooltip/withTooltip", () => ({
}));
// TODO: make `animated={false}` not required to make tests deterministic
-
-class SomeTestExtension extends LensRendererExtension {
- constructor(
- kubeObjectMenuItems: KubeObjectMenuRegistration[],
- ) {
- super({
- id: "some-id",
- absolutePath: "irrelevant",
- isBundled: false,
- isCompatible: false,
- isEnabled: false,
- manifest: { name: "some-id", version: "some-version", engines: { lens: "^5.5.0" }},
- manifestPath: "irrelevant",
- });
-
- this.kubeObjectMenuItems = kubeObjectMenuItems;
- }
-}
-
describe("kube-object-menu", () => {
let di: DiContainer;
let render: DiRender;
beforeEach(() => {
- const MenuItemComponent = () => Some menu item;
- const someTestExtension = new SomeTestExtension([
- {
- apiVersions: ["some-api-version"],
- kind: "some-kind",
- components: { MenuItem: MenuItemComponent },
- },
-
- {
- apiVersions: ["some-unrelated-api-version"],
- kind: "some-kind",
- components: { MenuItem: MenuItemComponent },
- },
-
- {
- apiVersions: ["some-api-version"],
- kind: "some-unrelated-kind",
- components: { MenuItem: MenuItemComponent },
- },
- ]);
-
di = getDiForUnitTesting({ doGeneralOverrides: true });
- render = renderFor(di);
-
- di.override(rendererExtensionsInjectable, () =>
- computed(() => [someTestExtension]),
+ di.register(
+ someMenuItemInjectable,
+ someOtherMenuItemInjectable,
+ someAnotherMenuItemInjectable,
);
+ render = renderFor(di);
+
di.override(
clusterInjectable,
() =>
@@ -318,3 +278,47 @@ describe("kube-object-menu", () => {
});
});
});
+
+const MenuItemComponent = () => Some menu item;
+
+const someMenuItemInjectable = getInjectable({
+ id: "some-menu-item",
+
+ instantiate: () => ({
+ apiVersions: ["some-api-version"],
+ kind: "some-kind",
+ Component: MenuItemComponent,
+ enabled: computed(() => true),
+ orderNumber: 1,
+ }),
+
+ injectionToken: kubeObjectMenuItemInjectionToken,
+});
+
+const someOtherMenuItemInjectable = getInjectable({
+ id: "some-other-menu-item",
+
+ instantiate: () => ({
+ apiVersions: ["some-unrelated-api-version"],
+ kind: "some-kind",
+ Component: MenuItemComponent,
+ enabled: computed(() => true),
+ orderNumber: 1,
+ }),
+
+ injectionToken: kubeObjectMenuItemInjectionToken,
+});
+
+const someAnotherMenuItemInjectable = getInjectable({
+ id: "some-another-menu-item",
+
+ instantiate: () => ({
+ apiVersions: ["some-api-version"],
+ kind: "some-unrelated-kind",
+ Component: MenuItemComponent,
+ enabled: computed(() => true),
+ orderNumber: 1,
+ }),
+
+ injectionToken: kubeObjectMenuItemInjectionToken,
+});
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.tsx
index e78500d5edba..eb0dc025b9e6 100644
--- a/src/renderer/components/kube-object-menu/kube-object-menu.tsx
+++ b/src/renderer/components/kube-object-menu/kube-object-menu.tsx
@@ -13,13 +13,14 @@ import type { ApiManager } from "../../../common/k8s-api/api-manager";
import { withInjectables } from "@ogre-tools/injectable-react";
import clusterNameInjectable from "./dependencies/cluster-name.injectable";
import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable";
-import kubeObjectMenuItemsInjectable from "./dependencies/kube-object-menu-items/kube-object-menu-items.injectable";
+import kubeObjectMenuItemsInjectable from "./kube-object-menu-items.injectable";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import type { HideDetails } from "../kube-detail-params/hide-details.injectable";
import hideDetailsInjectable from "../kube-detail-params/hide-details.injectable";
import type { OnKubeObjectContextMenuOpen } from "./on-context-menu-open.injectable";
import onKubeObjectContextMenuOpenInjectable from "./on-context-menu-open.injectable";
import type { KubeObjectContextMenuItem } from "../../kube-object/handler";
+import type { IComputedValue } from "mobx";
import { observable, runInAction } from "mobx";
import type { WithConfirmation } from "../confirm-dialog/with-confirm.injectable";
import type { Navigate } from "../../navigation/navigate.injectable";
@@ -36,7 +37,7 @@ export interface KubeObjectMenuProps extends Men
interface Dependencies {
apiManager: ApiManager;
- kubeObjectMenuItems: React.ElementType[];
+ kubeObjectMenuItems: IComputedValue;
clusterName: string | undefined;
hideDetails: HideDetails;
createEditResourceTab: (kubeObject: KubeObject) => void;
@@ -73,7 +74,7 @@ class NonInjectedKubeObjectMenu extends React.Component
private renderMenuItems() {
const { object, toolbar } = this.props;
- return this.props.kubeObjectMenuItems.map((MenuItem, index) => (
+ return this.props.kubeObjectMenuItems.get().map((MenuItem, index) => (