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 0000000000000..3cb2d5076b6ed --- /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, when navigating renders 1`] = ` +<body> + <div> + <div + class="Notifications flex column align-flex-end" + /> + <div + class="mainLayout" + style="--sidebar-width: 200px;" + > + <div + class="sidebar" + > + <div + class="flex flex-col" + data-testid="cluster-sidebar" + > + <div + class="SidebarCluster" + > + <div + class="Avatar rounded loadingAvatar" + style="width: 40px; height: 40px;" + > + ?? + </div> + <div + class="loadingClusterName" + /> + </div> + <div + class="sidebarNav sidebar-active-status" + > + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-workloads" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-workloads" + href="/" + > + <i + class="Icon svg focusable" + > + <span + class="icon" + /> + </i> + <span + class="link-text box grow" + > + Workloads + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-config" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-config" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="list" + > + list + </span> + </i> + <span + class="link-text box grow" + > + Config + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-network" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-network" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="device_hub" + > + device_hub + </span> + </i> + <span + class="link-text box grow" + > + Network + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-storage" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-storage" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="storage" + > + storage + </span> + </i> + <span + class="link-text box grow" + > + Storage + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-helm" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-helm" + href="/" + > + <i + class="Icon svg focusable" + > + <span + class="icon" + /> + </i> + <span + class="link-text box grow" + > + Helm + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-user-management" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-user-management" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="security" + > + security + </span> + </i> + <span + class="link-text box grow" + > + Access Control + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-custom-resources" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-custom-resources" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="extension" + > + extension + </span> + </i> + <span + class="link-text box grow" + > + Custom Resources + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + </div> + </div> + <div + class="ResizingAnchor horizontal trailing" + /> + </div> + <div + class="contents" + > + <i + class="Icon KubeObjectStatusIcon error material focusable" + id="tooltip_target_156" + > + <span + class="icon" + data-icon-name="error" + > + error + </span> + <div /> + </i> + </div> + <div + class="footer" + > + <div + class="Dock" + tabindex="-1" + > + <div + class="ResizingAnchor vertical leading" + /> + <div + class="tabs-container flex align-center" + > + <div + class="dockTabs" + role="tablist" + > + <div + class="Tabs tabs" + > + <div + class="Tab flex gaps align-center DockTab TerminalTab active" + id="tab-terminal" + role="tab" + tabindex="0" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="terminal" + > + terminal + </span> + </i> + <div + class="label" + > + <div + class="flex align-center" + > + <span + class="title" + > + Terminal + </span> + <div + class="close" + > + <i + class="Icon material interactive focusable small" + id="tooltip_target_137" + tabindex="0" + > + <span + class="icon" + data-icon-name="close" + > + close + </span> + <div /> + </i> + </div> + </div> + </div> + </div> + </div> + </div> + <div + class="toolbar flex gaps align-center box grow" + > + <div + class="dock-menu box grow" + > + <i + class="Icon new-dock-tab material interactive focusable" + id="menu-actions-for-dock" + tabindex="0" + > + <span + class="icon" + data-icon-name="add" + > + add + </span> + <div /> + </i> + </div> + <i + class="Icon material interactive focusable" + id="tooltip_target_139" + tabindex="0" + > + <span + class="icon" + data-icon-name="fullscreen" + > + fullscreen + </span> + <div /> + </i> + <i + class="Icon material interactive focusable" + id="tooltip_target_140" + tabindex="0" + > + <span + class="icon" + data-icon-name="keyboard_arrow_up" + > + keyboard_arrow_up + </span> + <div /> + </i> + </div> + </div> + </div> + </div> + </div> + </div> +</body> +`; + +exports[`disable kube object statuses when cluster is not relevant given extension shouldn't be enabled for the cluster, when navigating renders 1`] = ` +<body> + <div> + <div + class="Notifications flex column align-flex-end" + /> + <div + class="mainLayout" + style="--sidebar-width: 200px;" + > + <div + class="sidebar" + > + <div + class="flex flex-col" + data-testid="cluster-sidebar" + > + <div + class="SidebarCluster" + > + <div + class="Avatar rounded loadingAvatar" + style="width: 40px; height: 40px;" + > + ?? + </div> + <div + class="loadingClusterName" + /> + </div> + <div + class="sidebarNav sidebar-active-status" + > + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-workloads" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-workloads" + href="/" + > + <i + class="Icon svg focusable" + > + <span + class="icon" + /> + </i> + <span + class="link-text box grow" + > + Workloads + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-config" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-config" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="list" + > + list + </span> + </i> + <span + class="link-text box grow" + > + Config + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-network" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-network" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="device_hub" + > + device_hub + </span> + </i> + <span + class="link-text box grow" + > + Network + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-storage" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-storage" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="storage" + > + storage + </span> + </i> + <span + class="link-text box grow" + > + Storage + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-helm" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-helm" + href="/" + > + <i + class="Icon svg focusable" + > + <span + class="icon" + /> + </i> + <span + class="link-text box grow" + > + Helm + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-user-management" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-user-management" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="security" + > + security + </span> + </i> + <span + class="link-text box grow" + > + Access Control + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-custom-resources" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-custom-resources" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="extension" + > + extension + </span> + </i> + <span + class="link-text box grow" + > + Custom Resources + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + </div> + </div> + <div + class="ResizingAnchor horizontal trailing" + /> + </div> + <div + class="contents" + /> + <div + class="footer" + > + <div + class="Dock" + tabindex="-1" + > + <div + class="ResizingAnchor vertical leading" + /> + <div + class="tabs-container flex align-center" + > + <div + class="dockTabs" + role="tablist" + > + <div + class="Tabs tabs" + > + <div + class="Tab flex gaps align-center DockTab TerminalTab active" + id="tab-terminal" + role="tab" + tabindex="0" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="terminal" + > + terminal + </span> + </i> + <div + class="label" + > + <div + class="flex align-center" + > + <span + class="title" + > + Terminal + </span> + <div + class="close" + > + <i + class="Icon material interactive focusable small" + id="tooltip_target_75" + tabindex="0" + > + <span + class="icon" + data-icon-name="close" + > + close + </span> + <div /> + </i> + </div> + </div> + </div> + </div> + </div> + </div> + <div + class="toolbar flex gaps align-center box grow" + > + <div + class="dock-menu box grow" + > + <i + class="Icon new-dock-tab material interactive focusable" + id="menu-actions-for-dock" + tabindex="0" + > + <span + class="icon" + data-icon-name="add" + > + add + </span> + <div /> + </i> + </div> + <i + class="Icon material interactive focusable" + id="tooltip_target_77" + tabindex="0" + > + <span + class="icon" + data-icon-name="fullscreen" + > + fullscreen + </span> + <div /> + </i> + <i + class="Icon material interactive focusable" + id="tooltip_target_78" + tabindex="0" + > + <span + class="icon" + data-icon-name="keyboard_arrow_up" + > + keyboard_arrow_up + </span> + <div /> + </i> + </div> + </div> + </div> + </div> + </div> + </div> +</body> +`; + +exports[`disable kube object statuses when cluster is not relevant given not yet known if extension should be enabled for the cluster, when navigating renders 1`] = ` +<body> + <div> + <div + class="Notifications flex column align-flex-end" + /> + <div + class="mainLayout" + style="--sidebar-width: 200px;" + > + <div + class="sidebar" + > + <div + class="flex flex-col" + data-testid="cluster-sidebar" + > + <div + class="SidebarCluster" + > + <div + class="Avatar rounded loadingAvatar" + style="width: 40px; height: 40px;" + > + ?? + </div> + <div + class="loadingClusterName" + /> + </div> + <div + class="sidebarNav sidebar-active-status" + > + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-workloads" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-workloads" + href="/" + > + <i + class="Icon svg focusable" + > + <span + class="icon" + /> + </i> + <span + class="link-text box grow" + > + Workloads + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-config" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-config" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="list" + > + list + </span> + </i> + <span + class="link-text box grow" + > + Config + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-network" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-network" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="device_hub" + > + device_hub + </span> + </i> + <span + class="link-text box grow" + > + Network + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-storage" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-storage" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="storage" + > + storage + </span> + </i> + <span + class="link-text box grow" + > + Storage + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-helm" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-helm" + href="/" + > + <i + class="Icon svg focusable" + > + <span + class="icon" + /> + </i> + <span + class="link-text box grow" + > + Helm + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-user-management" + > + <a + class="nav-item flex gaps align-center" + data-testid="sidebar-item-link-for-user-management" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="security" + > + security + </span> + </i> + <span + class="link-text box grow" + > + Access Control + </span> + </a> + </div> + <div + class="SidebarItem" + data-is-active-test="false" + data-testid="sidebar-item-custom-resources" + > + <a + class="nav-item flex gaps align-center expandable" + data-testid="sidebar-item-link-for-custom-resources" + href="/" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="extension" + > + extension + </span> + </i> + <span + class="link-text box grow" + > + Custom Resources + </span> + <i + class="Icon expand-icon box right material focusable" + > + <span + class="icon" + data-icon-name="keyboard_arrow_down" + > + keyboard_arrow_down + </span> + </i> + </a> + </div> + </div> + </div> + <div + class="ResizingAnchor horizontal trailing" + /> + </div> + <div + class="contents" + /> + <div + class="footer" + > + <div + class="Dock" + tabindex="-1" + > + <div + class="ResizingAnchor vertical leading" + /> + <div + class="tabs-container flex align-center" + > + <div + class="dockTabs" + role="tablist" + > + <div + class="Tabs tabs" + > + <div + class="Tab flex gaps align-center DockTab TerminalTab active" + id="tab-terminal" + role="tab" + tabindex="0" + > + <i + class="Icon material focusable" + > + <span + class="icon" + data-icon-name="terminal" + > + terminal + </span> + </i> + <div + class="label" + > + <div + class="flex align-center" + > + <span + class="title" + > + Terminal + </span> + <div + class="close" + > + <i + class="Icon material interactive focusable small" + id="tooltip_target_13" + tabindex="0" + > + <span + class="icon" + data-icon-name="close" + > + close + </span> + <div /> + </i> + </div> + </div> + </div> + </div> + </div> + </div> + <div + class="toolbar flex gaps align-center box grow" + > + <div + class="dock-menu box grow" + > + <i + class="Icon new-dock-tab material interactive focusable" + id="menu-actions-for-dock" + tabindex="0" + > + <span + class="icon" + data-icon-name="add" + > + add + </span> + <div /> + </i> + </div> + <i + class="Icon material interactive focusable" + id="tooltip_target_15" + tabindex="0" + > + <span + class="icon" + data-icon-name="fullscreen" + > + fullscreen + </span> + <div /> + </i> + <i + class="Icon material interactive focusable" + id="tooltip_target_16" + tabindex="0" + > + <span + class="icon" + data-icon-name="keyboard_arrow_up" + > + keyboard_arrow_up + </span> + <div /> + </i> + </div> + </div> + </div> + </div> + </div> + </div> +</body> +`; 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 0000000000000..2ca7f97852666 --- /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<boolean> + >; + + 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, when navigating", () => { + 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, when navigating", () => { + 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, when navigating", () => { + 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: () => ( + <KubeObjectStatusIcon + object={getKubeObjectStub("some-kind", "some-api-version")} + /> + ), + }), + + 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-status-icon/kube-object-status-icon.test.tsx b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx index eccb136222dc4..e1c3ffdcceff9 100644 --- a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx @@ -3,38 +3,29 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; -import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; import type { DiRender } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor"; -import { computed } from "mobx"; -import { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import type { KubeObjectStatus } from "../../../common/k8s-api/kube-object-status"; import { KubeObjectStatusLevel } from "../../../common/k8s-api/kube-object-status"; import { KubeObject } from "../../../common/k8s-api/kube-object"; import { KubeObjectStatusIcon } from "./kube-object-status-icon"; import React from "react"; -import type { KubeObjectStatusRegistration } from "./kube-object-status-registration"; +import { useFakeTime } from "../../../common/test-utils/use-fake-time"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import { kubeObjectStatusTextInjectionToken } from "./kube-object-status-text-injection-token"; +import { computed } from "mobx"; describe("kube-object-status-icon", () => { let render: DiRender; - let kubeObjectStatusRegistrations: KubeObjectStatusRegistration[]; + let di: DiContainer; beforeEach(() => { - // TODO: Make mocking of date in unit tests global - global.Date.now = () => new Date("2015-10-21T07:28:00Z").getTime(); + useFakeTime("2015-10-21T07:28:00Z"); - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); - - kubeObjectStatusRegistrations = []; - - const someTestExtension = new SomeTestExtension( - kubeObjectStatusRegistrations, - ); - - di.override(rendererExtensionsInjectable, () => - computed(() => [someTestExtension]), - ); }); it("given no statuses, when rendered, renders as empty", () => { @@ -48,14 +39,14 @@ describe("kube-object-status-icon", () => { it('given level "critical" status, when rendered, renders with status', () => { const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); - const statusRegistration = getStatusRegistration( + const statusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.CRITICAL, "critical", "some-kind", ["some-api-version"], ); - kubeObjectStatusRegistrations.push(statusRegistration); + di.register(statusTextInjectable); const { baseElement } = render( <KubeObjectStatusIcon object={kubeObject} />, @@ -67,14 +58,14 @@ describe("kube-object-status-icon", () => { it('given level "info" status, when rendered, renders with status', () => { const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); - const statusRegistration = getStatusRegistration( + const statusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.INFO, "info", "some-kind", ["some-api-version"], ); - kubeObjectStatusRegistrations.push(statusRegistration); + di.register(statusTextInjectable); const { baseElement } = render( <KubeObjectStatusIcon object={kubeObject} />, @@ -86,14 +77,14 @@ describe("kube-object-status-icon", () => { it('given level "warning" status, when rendered, renders with status', () => { const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); - const statusRegistration = getStatusRegistration( + const statusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.WARNING, "warning", "some-kind", ["some-api-version"], ); - kubeObjectStatusRegistrations.push(statusRegistration); + di.register(statusTextInjectable); const { baseElement } = render( <KubeObjectStatusIcon object={kubeObject} />, @@ -105,30 +96,32 @@ describe("kube-object-status-icon", () => { it("given status for all levels is present, when rendered, renders with statuses", () => { const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); - const critical = getStatusRegistration( + const criticalStatusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.CRITICAL, "critical", "some-kind", ["some-api-version"], ); - const warning = getStatusRegistration( + const warningStatusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.WARNING, "warning", "some-kind", ["some-api-version"], ); - const info = getStatusRegistration( + const infoStatusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.INFO, "info", "some-kind", ["some-api-version"], ); - kubeObjectStatusRegistrations.push(critical); - kubeObjectStatusRegistrations.push(warning); - kubeObjectStatusRegistrations.push(info); + di.register( + criticalStatusTextInjectable, + warningStatusTextInjectable, + infoStatusTextInjectable, + ); const { baseElement } = render( <KubeObjectStatusIcon object={kubeObject} />, @@ -140,22 +133,21 @@ describe("kube-object-status-icon", () => { it("given info and warning statuses are present, when rendered, renders with statuses", () => { const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); - const warning = getStatusRegistration( + const warningStatusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.WARNING, "warning", "some-kind", ["some-api-version"], ); - const info = getStatusRegistration( + const infoStatusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.INFO, "info", "some-kind", ["some-api-version"], ); - kubeObjectStatusRegistrations.push(warning); - kubeObjectStatusRegistrations.push(info); + di.register(warningStatusTextInjectable, infoStatusTextInjectable); const { baseElement } = render( <KubeObjectStatusIcon object={kubeObject} />, @@ -168,14 +160,14 @@ describe("kube-object-status-icon", () => { it("given registration for wrong api version, when rendered, renders as empty", () => { const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); - const statusRegistration = getStatusRegistration( + const statusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.CRITICAL, "irrelevant", "some-kind", ["some-other-api-version"], ); - kubeObjectStatusRegistrations.push(statusRegistration); + di.register(statusTextInjectable); const { baseElement } = render( <KubeObjectStatusIcon object={kubeObject} />, @@ -187,14 +179,14 @@ describe("kube-object-status-icon", () => { it("given registration for wrong kind, when rendered, renders as empty", () => { const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); - const statusRegistration = getStatusRegistration( + const statusTextInjectable = getStatusTextInjectable( KubeObjectStatusLevel.CRITICAL, "irrelevant", "some-other-kind", ["some-api-version"], ); - kubeObjectStatusRegistrations.push(statusRegistration); + di.register(statusTextInjectable); const { baseElement } = render( <KubeObjectStatusIcon object={kubeObject} />, @@ -206,21 +198,25 @@ describe("kube-object-status-icon", () => { it("given registration without status for exact kube object, when rendered, renders as empty", () => { const kubeObject = getKubeObjectStub("some-kind", "some-api-version"); - const statusRegistration = { - apiVersions: ["some-api-version"], - kind: "some-kind", - resolve: (): void => {}, - }; + const statusTextInjectable = getInjectable({ + id: "some-id", + instantiate: () => ({ + apiVersions: ["some-api-version"], + kind: "some-kind", + resolve: () => { return undefined as unknown as KubeObjectStatus; }, + enabled: computed(() => true), + }), + + injectionToken: kubeObjectStatusTextInjectionToken, + }); - // @ts-ignore - kubeObjectStatusRegistrations.push(statusRegistration); + di.register(statusTextInjectable); const { baseElement } = render( <KubeObjectStatusIcon object={kubeObject} />, ); expect(baseElement).toMatchSnapshot(); - }); }); @@ -236,28 +232,20 @@ const getKubeObjectStub = (kind: string, apiVersion: string) => KubeObject.creat }, }); -const getStatusRegistration = (level: KubeObjectStatusLevel, title: string, kind: string, apiVersions: string[]) => ({ - apiVersions, - kind, - resolve: (kubeObject: KubeObject) => ({ - level, - text: `Some ${title} status for ${kubeObject.getName()}`, - timestamp: "2015-10-19T07:28:00Z", - }), -}); +const getStatusTextInjectable = (level: KubeObjectStatusLevel, title: string, kind: string, apiVersions: string[]) => getInjectable({ + id: title, + instantiate: () => ({ + apiVersions, + kind, -class SomeTestExtension extends LensRendererExtension { - constructor(kubeObjectStatusTexts: KubeObjectStatusRegistration[]) { - 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", - }); + resolve: (kubeObject: KubeObject) => ({ + level, + text: `Some ${title} status for ${kubeObject.getName()}`, + timestamp: "2015-10-19T07:28:00Z", + }), + + enabled: computed(() => true), + }), - this.kubeObjectStatusTexts = kubeObjectStatusTexts; - } -} + injectionToken: kubeObjectStatusTextInjectionToken, +}); diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx index ee85ad6753949..dd5010e9d90e5 100644 --- a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.tsx @@ -9,10 +9,12 @@ import React from "react"; import { Icon } from "../icon"; import { cssNames, formatDuration, getOrInsert } from "../../utils"; import { withInjectables } from "@ogre-tools/injectable-react"; -import statusesForKubeObjectInjectable from "./statuses-for-kube-object.injectable"; +import kubeObjectStatusTextsForObjectInjectable from "./kube-object-status-texts-for-object.injectable"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import type { KubeObjectStatus } from "../../../common/k8s-api/kube-object-status"; import { KubeObjectStatusLevel } from "../../../common/k8s-api/kube-object-status"; +import type { IComputedValue } from "mobx"; +import { observer } from "mobx-react"; function statusClassName(level: KubeObjectStatusLevel): string { switch (level) { @@ -76,9 +78,10 @@ export interface KubeObjectStatusIconProps { } interface Dependencies { - statuses: KubeObjectStatus[]; + statuses: IComputedValue<KubeObjectStatus[]>; } +@observer class NonInjectedKubeObjectStatusIcon extends React.Component<KubeObjectStatusIconProps & Dependencies> { renderStatuses(statuses: KubeObjectStatus[], level: number) { const filteredStatuses = statuses.filter((item) => item.level == level); @@ -104,7 +107,7 @@ class NonInjectedKubeObjectStatusIcon extends React.Component<KubeObjectStatusIc } render() { - const statuses = this.props.statuses; + const statuses = this.props.statuses.get(); if (statuses.length === 0) { return null; @@ -135,7 +138,7 @@ export const KubeObjectStatusIcon = withInjectables<Dependencies, KubeObjectStat { getProps: (di, props) => ({ - statuses: di.inject(statusesForKubeObjectInjectable, props.object), + statuses: di.inject(kubeObjectStatusTextsForObjectInjectable, props.object), ...props, }), }, diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-text-injection-token.ts b/src/renderer/components/kube-object-status-icon/kube-object-status-text-injection-token.ts new file mode 100644 index 0000000000000..bac6e127d4ce3 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-text-injection-token.ts @@ -0,0 +1,20 @@ +/** + * 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 { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { KubeObjectStatus } from "../../../common/k8s-api/kube-object-status"; +import type { IComputedValue } from "mobx"; + +export interface KubeObjectStatusText { + kind: string; + apiVersions: string[]; + resolve: (object: KubeObject) => KubeObjectStatus; + enabled: IComputedValue<boolean>; +} + +export const kubeObjectStatusTextInjectionToken = + getInjectionToken<KubeObjectStatusText>({ + id: "kube-object-status-text-injection-token", + }); diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-text-registrator.injectable.ts b/src/renderer/components/kube-object-status-icon/kube-object-status-text-registrator.injectable.ts new file mode 100644 index 0000000000000..0592330e8caf1 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-text-registrator.injectable.ts @@ -0,0 +1,55 @@ +/** + * 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 { kubeObjectStatusTextInjectionToken } from "./kube-object-status-text-injection-token"; +import extensionShouldBeEnabledForClusterFrameInjectable from "../../extension-loader/extension-should-be-enabled-for-cluster-frame.injectable"; +import { computed } from "mobx"; + +const kubeObjectStatusTextRegistratorInjectable = getInjectable({ + id: "kube-object-status-text-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.kubeObjectStatusTexts.map((registration) => { + const id = `kube-object-status-text-registration-from-${ + extension.sanitizedExtensionId + }-${getRandomId()}`; + + return getInjectable({ + id, + + instantiate: () => ({ + ...registration, + + enabled: computed(() => + extensionShouldBeEnabledForClusterFrame.value.get(), + ), + }), + + injectionToken: kubeObjectStatusTextInjectionToken, + }); + }); + }; + }, + + injectionToken: extensionRegistratorInjectionToken, +}); + +export default kubeObjectStatusTextRegistratorInjectable; diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-texts-for-object.injectable.ts b/src/renderer/components/kube-object-status-icon/kube-object-status-texts-for-object.injectable.ts new file mode 100644 index 0000000000000..108c79b273857 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-texts-for-object.injectable.ts @@ -0,0 +1,42 @@ +/** + * 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 kubeObjectStatusTextsInjectable from "./kube-object-status-texts.injectable"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { conforms, eq, includes } from "lodash/fp"; +import type { KubeObjectStatusRegistration } from "./kube-object-status-registration"; +import { computed } from "mobx"; + +const kubeObjectStatusTextsForObjectInjectable = getInjectable({ + id: "kube-object-status-texts-for-object", + + instantiate: (di, kubeObject: KubeObject) => { + const allStatusTexts = di.inject(kubeObjectStatusTextsInjectable); + + return computed(() => + allStatusTexts + .get() + .filter(toKubeObjectRelated(kubeObject)) + .map(toStatus(kubeObject)) + .filter(Boolean), + ); + }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, kubeObject: KubeObject) => kubeObject.getId(), + }), +}); + +const toKubeObjectRelated = (kubeObject: KubeObject) => + conforms({ + kind: eq(kubeObject.kind), + apiVersions: includes(kubeObject.apiVersion), + }); + +const toStatus = + (kubeObject: KubeObject) => (item: KubeObjectStatusRegistration) => + item.resolve(kubeObject); + +export default kubeObjectStatusTextsForObjectInjectable; diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-texts.injectable.ts b/src/renderer/components/kube-object-status-icon/kube-object-status-texts.injectable.ts new file mode 100644 index 0000000000000..df5b5b65b3312 --- /dev/null +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-texts.injectable.ts @@ -0,0 +1,23 @@ +/** + * 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 { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { computed } from "mobx"; +import { kubeObjectStatusTextInjectionToken } from "./kube-object-status-text-injection-token"; + +const kubeObjectStatusTextsInjectable = getInjectable({ + id: "kube-object-status-texts", + + instantiate: (di) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + const statusTexts = computedInjectMany(kubeObjectStatusTextInjectionToken); + + return computed(() => + statusTexts.get().filter((statusText) => statusText.enabled.get()), + ); + }, +}); + +export default kubeObjectStatusTextsInjectable; diff --git a/src/renderer/components/kube-object-status-icon/status-registrations.injectable.ts b/src/renderer/components/kube-object-status-icon/status-registrations.injectable.ts deleted file mode 100644 index 2e5f302382df5..0000000000000 --- a/src/renderer/components/kube-object-status-icon/status-registrations.injectable.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * 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 { computed } from "mobx"; -import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; - -const statusRegistrationsInjectable = getInjectable({ - id: "status-registrations", - - instantiate: (di) => { - const extensions = di.inject(rendererExtensionsInjectable); - - return computed(() => - extensions.get().flatMap((extension) => extension.kubeObjectStatusTexts), - ); - }, -}); - -export default statusRegistrationsInjectable; diff --git a/src/renderer/components/kube-object-status-icon/statuses-for-kube-object.injectable.ts b/src/renderer/components/kube-object-status-icon/statuses-for-kube-object.injectable.ts deleted file mode 100644 index d277dd04a6d2c..0000000000000 --- a/src/renderer/components/kube-object-status-icon/statuses-for-kube-object.injectable.ts +++ /dev/null @@ -1,35 +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 statusRegistrationsInjectable from "./status-registrations.injectable"; -import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import { conforms, eq, includes } from "lodash/fp"; -import type { KubeObjectStatusRegistration } from "./kube-object-status-registration"; - -const statusesForKubeObjectInjectable = getInjectable({ - id: "statuses-for-kube-object", - - instantiate: (di, kubeObject: KubeObject) => - di - .inject(statusRegistrationsInjectable) - .get() - .filter(toKubeObjectRelated(kubeObject)) - .map(toStatus(kubeObject)) - .filter(Boolean), - - lifecycle: lifecycleEnum.transient, -}); - -const toKubeObjectRelated = (kubeObject: KubeObject) => - conforms({ - kind: eq(kubeObject.kind), - apiVersions: includes(kubeObject.apiVersion), - }); - -const toStatus = - (kubeObject: KubeObject) => (item: KubeObjectStatusRegistration) => - item.resolve(kubeObject); - -export default statusesForKubeObjectInjectable;