diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index 37110bcb3c..517393a6bb 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -27,6 +27,7 @@ import assert from "assert"; import type { Logger } from "../logger"; import type { ReadFileSync } from "../fs/read-file-sync.injectable"; import type { EmitClusterConnectionUpdate } from "../../main/cluster/emit-connection-update.injectable"; +import type { CreateKubectl } from "../../main/kubectl/create-kubectl.injectable"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; @@ -34,7 +35,7 @@ export interface ClusterDependencies { readonly detectorRegistry: DetectorRegistry; createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; createContextHandler: (cluster: Cluster) => ClusterContextHandler; - createKubectl: (clusterVersion: string) => Kubectl; + createKubectl: CreateKubectl; createAuthorizationReview: (config: KubeConfig) => CanI; createListNamespaces: (config: KubeConfig) => ListNamespaces; createVersionDetector: (cluster: Cluster) => VersionDetector; diff --git a/src/common/cluster/get-by-id.injectable.ts b/src/common/cluster/get-by-id.injectable.ts new file mode 100644 index 0000000000..72140b4886 --- /dev/null +++ b/src/common/cluster/get-by-id.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { ClusterId } from "../cluster-types"; +import type { Cluster } from "./cluster"; +import { getInjectable } from "@ogre-tools/injectable"; +import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; + +export type GetClusterById = (id: ClusterId) => Cluster | undefined; + +const getClusterByIdInjectable = getInjectable({ + id: "get-cluster-by-id", + instantiate: (di): GetClusterById => { + const store = di.inject(clusterStoreInjectable); + + return (id) => store.getById(id); + }, +}); + +export default getClusterByIdInjectable; diff --git a/src/common/cluster/set-visible-channel.injectable.ts b/src/common/cluster/set-visible-channel.injectable.ts new file mode 100644 index 0000000000..a89659701d --- /dev/null +++ b/src/common/cluster/set-visible-channel.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 { ClusterId } from "../cluster-types"; +import type { MessageChannel } from "../utils/channel/message-channel-injection-token"; + +export type SetVisibleClusterMessage = { + action: "set"; + clusterId: ClusterId; +} | { + action: "clear"; +}; + +export type SetVisibleClusterChannel = MessageChannel; + +const setVisibleClusterChannelInjectable = getInjectable({ + id: "set-visible-cluster-channel", + instantiate: (): SetVisibleClusterChannel => ({ + id: "set-visible-cluster-channel", + }), +}); + +export default setVisibleClusterChannelInjectable; diff --git a/src/common/ipc/cluster.ts b/src/common/ipc/cluster.ts index 4b27b12431..70c63c4c3f 100644 --- a/src/common/ipc/cluster.ts +++ b/src/common/ipc/cluster.ts @@ -4,7 +4,6 @@ */ export const clusterSetFrameIdHandler = "cluster:set-frame-id"; -export const clusterVisibilityHandler = "cluster:visibility"; export const clusterRefreshHandler = "cluster:refresh"; export const clusterDisconnectHandler = "cluster:disconnect"; export const clusterDeleteHandler = "cluster:delete"; diff --git a/src/features/cluster/__snapshots__/connection-status.test.ts.snap b/src/features/cluster/__snapshots__/connection-status.test.ts.snap index e51080682b..52edb3d179 100644 --- a/src/features/cluster/__snapshots__/connection-status.test.ts.snap +++ b/src/features/cluster/__snapshots__/connection-status.test.ts.snap @@ -67,3 +67,51 @@ exports[`cluster connection status when navigating to cluster connection renders `; + +exports[`cluster connection status when navigating to cluster connection when a connection update has been broadcast for first cluster when navigating to a different cluster renders 1`] = ` + +
+
+
+
+
+
+
+
+

+ minikube-2 +

+
+
+            

+ Connecting + … +

+
+
+        
+
+
+
+
+ +`; diff --git a/src/features/cluster/connection-status.test.ts b/src/features/cluster/connection-status.test.ts index 9b6ae357d4..d917976891 100644 --- a/src/features/cluster/connection-status.test.ts +++ b/src/features/cluster/connection-status.test.ts @@ -4,6 +4,8 @@ */ import type { RenderResult } from "@testing-library/react"; +import type { ObservableMap } from "mobx"; +import { observable } from "mobx"; import type { ClusterStore } from "../../common/cluster-store/cluster-store"; import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable"; import type { ClusterId } from "../../common/cluster-types"; @@ -12,13 +14,20 @@ import type { NavigateToClusterView } from "../../common/front-end-routing/route import navigateToClusterViewInjectable from "../../common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable"; import type { ReadFileSync } from "../../common/fs/read-file-sync.injectable"; import readFileSyncInjectable from "../../common/fs/read-file-sync.injectable"; +import clusterManagerInjectable from "../../main/cluster-manager.injectable"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import createClusterInjectable from "../../renderer/create-cluster/create-cluster.injectable"; +import createClusterInjectable from "../../main/create-cluster/create-cluster.injectable"; +import createContextHandlerInjectable from "../../main/context-handler/create-context-handler.injectable"; +import createKubeconfigManagerInjectable from "../../main/kubeconfig-manager/create-kubeconfig-manager.injectable"; +import createKubectlInjectable from "../../main/kubectl/create-kubectl.injectable"; +import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager"; +import type { Kubectl } from "../../main/kubectl/kubectl"; +import type { ContextHandler } from "../../main/context-handler/context-handler"; describe("cluster connection status", () => { let clusterStore: ClusterStore; - let clusters: Map; + let clusters: ObservableMap; let cluster: Cluster; let cluster2: Cluster; let applicationBuilder: ApplicationBuilder; @@ -64,8 +73,14 @@ describe("cluster connection status", () => { }; applicationBuilder.dis.rendererDi.override(readFileSyncInjectable, () => readFileSyncMock); + applicationBuilder.dis.mainDi.override(readFileSyncInjectable, () => readFileSyncMock); + applicationBuilder.dis.mainDi.override(clusterManagerInjectable, () => ({})); + applicationBuilder.dis.mainDi.override(createKubeconfigManagerInjectable, () => () => ({} as KubeconfigManager)); + applicationBuilder.dis.mainDi.override(createKubectlInjectable, () => () => ({} as Kubectl)); + applicationBuilder.dis.mainDi.override(createContextHandlerInjectable, () => () => ({} as ContextHandler)); - applicationBuilder.beforeRender(() => { + applicationBuilder.beforeApplicationStart(() => { + clusters = observable.map(); clusterStore = ({ clusters, get clustersList() { @@ -76,10 +91,12 @@ describe("cluster connection status", () => { applicationBuilder.dis.mainDi.override(clusterStoreInjectable, () => clusterStore); applicationBuilder.dis.rendererDi.override(clusterStoreInjectable, () => clusterStore); + }); + applicationBuilder.beforeRender(() => { navigateToClusterView = applicationBuilder.dis.rendererDi.inject(navigateToClusterViewInjectable); - const createCluster = applicationBuilder.dis.rendererDi.inject(createClusterInjectable); + const createCluster = applicationBuilder.dis.mainDi.inject(createClusterInjectable); cluster = createCluster({ contextName: "minikube", @@ -92,14 +109,14 @@ describe("cluster connection status", () => { cluster2 = createCluster({ contextName: "minikube-2", - id: "some-cluster-id", + id: "some-cluster-id-2", kubeConfigPath: "/some/file/path", }, { clusterServerUrl: "https://localhost:1234", }); cluster2.activate = jest.fn(); // override for test - clusters = new Map([ + clusters.replace([ [cluster.id, cluster], [cluster2.id, cluster2], ]); diff --git a/src/main/cluster/set-visible-listener.injectable.ts b/src/main/cluster/set-visible-listener.injectable.ts new file mode 100644 index 0000000000..bf6bae8984 --- /dev/null +++ b/src/main/cluster/set-visible-listener.injectable.ts @@ -0,0 +1,36 @@ +/** + * 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 { SetVisibleClusterChannel } from "../../common/cluster/set-visible-channel.injectable"; +import setVisibleClusterChannelInjectable from "../../common/cluster/set-visible-channel.injectable"; +import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token"; +import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; +import clusterManagerInjectable from "../cluster-manager.injectable"; + +const setVisibileClusterListenerInjectable = getInjectable({ + id: "set-visible-cluster-listener", + instantiate: (di) => { + const channel = di.inject(setVisibleClusterChannelInjectable); + const clusterManager = di.inject(clusterManagerInjectable); + + return { + channel, + handler: (message) => { + switch (message.action) { + case "clear": + clusterManager.visibleCluster = undefined; + break; + case "set": + clusterManager.visibleCluster = message.clusterId; + break; + } + }, + } as MessageChannelListener; + }, + injectionToken: messageChannelListenerInjectionToken, +}); + +export default setVisibileClusterListenerInjectable; + diff --git a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts index 03e0cab953..6c08a2cb11 100644 --- a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts +++ b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts @@ -5,7 +5,7 @@ import type { IpcMainInvokeEvent } from "electron"; import { BrowserWindow, Menu } from "electron"; import { clusterFrameMap } from "../../../../common/cluster-frames"; -import { clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../../common/ipc/cluster"; +import { clusterSetFrameIdHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../../common/ipc/cluster"; import type { ClusterId } from "../../../../common/cluster-types"; import { ClusterStore } from "../../../../common/cluster-store/cluster-store"; import { appEventBus } from "../../../../common/app-event-bus/event-bus"; @@ -48,10 +48,6 @@ export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoc } }); - ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => { - clusterManager.visibleCluster = clusterId; - }); - ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => { return ClusterStore.getInstance() .getById(clusterId) diff --git a/src/main/kubectl/create-kubectl.injectable.ts b/src/main/kubectl/create-kubectl.injectable.ts index 1e13b7d02c..35afd8e3cb 100644 --- a/src/main/kubectl/create-kubectl.injectable.ts +++ b/src/main/kubectl/create-kubectl.injectable.ts @@ -13,10 +13,12 @@ import kubectlBinaryNameInjectable from "./binary-name.injectable"; import bundledKubectlBinaryPathInjectable from "./bundled-binary-path.injectable"; import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; +export type CreateKubectl = (clusterVersion: string) => Kubectl; + const createKubectlInjectable = getInjectable({ id: "create-kubectl", - instantiate: (di) => { + instantiate: (di): CreateKubectl => { const dependencies: KubectlDependencies = { userStore: di.inject(userStoreInjectable), directoryForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable), @@ -27,7 +29,7 @@ const createKubectlInjectable = getInjectable({ baseBundeledBinariesDirectory: di.inject(baseBundledBinariesDirectoryInjectable), }; - return (clusterVersion: string) => new Kubectl(dependencies, clusterVersion); + return (clusterVersion) => new Kubectl(dependencies, clusterVersion); }, }); diff --git a/src/renderer/cluster/send-set-visible.injectable.ts b/src/renderer/cluster/send-set-visible.injectable.ts new file mode 100644 index 0000000000..a2dc43f621 --- /dev/null +++ b/src/renderer/cluster/send-set-visible.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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 { SetVisibleClusterChannel } from "../../common/cluster/set-visible-channel.injectable"; +import setVisibleClusterChannelInjectable from "../../common/cluster/set-visible-channel.injectable"; +import type { EmitChannelMessage } from "../../common/utils/channel/message-to-channel-injection-token"; +import messageToChannelInjectable from "../utils/channel/message-to-channel.injectable"; + +export type SendSetVisibleCluster = EmitChannelMessage; + +const sendSetVisibleClusterInjectable = getInjectable({ + id: "send-set-visible-cluster", + instantiate: (di): SendSetVisibleCluster => { + const channel = di.inject(setVisibleClusterChannelInjectable); + const messageToChannel = di.inject(messageToChannelInjectable); + + return (message) => messageToChannel(channel, message); + }, +}); + +export default sendSetVisibleClusterInjectable; diff --git a/src/renderer/components/cluster-manager/cluster-frame-handler.injectable.ts b/src/renderer/components/cluster-manager/cluster-frame-handler.injectable.ts index 19611f3117..4354b78722 100644 --- a/src/renderer/components/cluster-manager/cluster-frame-handler.injectable.ts +++ b/src/renderer/components/cluster-manager/cluster-frame-handler.injectable.ts @@ -3,11 +3,20 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import getClusterByIdInjectable from "../../../common/cluster/get-by-id.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import sendSetVisibleClusterInjectable from "../../cluster/send-set-visible.injectable"; import { ClusterFrameHandler } from "./cluster-frame-handler"; +import clusterFrameParentElementInjectable from "./parent-element.injectable"; const clusterFrameHandlerInjectable = getInjectable({ id: "cluster-frame-handler", - instantiate: () => new ClusterFrameHandler(), + instantiate: (di) => new ClusterFrameHandler({ + getClusterById: di.inject(getClusterByIdInjectable), + sendSetVisibleCluster: di.inject(sendSetVisibleClusterInjectable), + parentElem: di.inject(clusterFrameParentElementInjectable), + logger: di.inject(loggerInjectable), + }), }); export default clusterFrameHandlerInjectable; diff --git a/src/renderer/components/cluster-manager/cluster-frame-handler.ts b/src/renderer/components/cluster-manager/cluster-frame-handler.ts index 7791837e0d..060747168f 100644 --- a/src/renderer/components/cluster-manager/cluster-frame-handler.ts +++ b/src/renderer/components/cluster-manager/cluster-frame-handler.ts @@ -4,24 +4,30 @@ */ import { action, makeObservable, observable, when } from "mobx"; -import logger from "../../../main/logger"; -import { clusterVisibilityHandler } from "../../../common/ipc/cluster"; -import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import type { ClusterId } from "../../../common/cluster-types"; import type { Disposer } from "../../utils"; import { getClusterFrameUrl, onceDefined } from "../../utils"; -import { ipcRenderer } from "electron"; import assert from "assert"; +import type { GetClusterById } from "../../../common/cluster/get-by-id.injectable"; +import type { SendSetVisibleCluster } from "../../cluster/send-set-visible.injectable"; +import type { Logger } from "../../../common/logger"; export interface LensView { isLoaded: boolean; frame: HTMLIFrameElement; } +export interface ClusterFrameHandlerDependencies { + getClusterById: GetClusterById; + sendSetVisibleCluster: SendSetVisibleCluster; + readonly parentElem: HTMLElement; + readonly logger: Logger; +} + export class ClusterFrameHandler { private readonly views = observable.map(); - constructor() { + constructor(protected readonly dependencies: ClusterFrameHandlerDependencies) { makeObservable(this); } @@ -31,36 +37,33 @@ export class ClusterFrameHandler { @action public initView(clusterId: ClusterId) { - const cluster = ClusterStore.getInstance().getById(clusterId); - const parentElem = document.getElementById("lens-views"); - - assert(parentElem, "DOM with #lens-views must be present"); + const cluster = this.dependencies.getClusterById(clusterId); if (!cluster || this.views.has(clusterId)) { return; } - logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`); + this.dependencies.logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`); const iframe = document.createElement("iframe"); iframe.id = `cluster-frame-${cluster.id}`; iframe.name = cluster.contextName; iframe.setAttribute("src", getClusterFrameUrl(clusterId)); iframe.addEventListener("load", action(() => { - logger.info(`[LENS-VIEW]: frame for clusterId=${clusterId} has loaded`); + this.dependencies.logger.info(`[LENS-VIEW]: frame for clusterId=${clusterId} has loaded`); const view = this.views.get(clusterId); assert(view, `view for ${clusterId} MUST still exist here`); view.isLoaded = true; }), { once: true }); this.views.set(clusterId, { frame: iframe, isLoaded: false }); - parentElem.appendChild(iframe); + this.dependencies.parentElem.appendChild(iframe); - logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`); + this.dependencies.logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`); const dispose = when( () => cluster.ready, - () => logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`), + () => this.dependencies.logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`), ); when( @@ -69,14 +72,14 @@ export class ClusterFrameHandler { () => { when( () => { - const cluster = ClusterStore.getInstance().getById(clusterId); + const cluster = this.dependencies.getClusterById(clusterId); return Boolean(!cluster || (cluster.disconnected && this.views.get(clusterId)?.isLoaded)); }, () => { - logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`); + this.dependencies.logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`); this.views.delete(clusterId); - parentElem.removeChild(iframe); + this.dependencies.parentElem.removeChild(iframe); dispose(); }, ); @@ -90,15 +93,15 @@ export class ClusterFrameHandler { // Clear the previous when ASAP this.prevVisibleClusterChange?.(); - logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${clusterId}`); - ipcRenderer.send(clusterVisibilityHandler); + this.dependencies.logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${clusterId}`); + this.dependencies.sendSetVisibleCluster({ action: "clear" }); for (const { frame: view } of this.views.values()) { view.classList.add("hidden"); } const cluster = clusterId - ? ClusterStore.getInstance().getById(clusterId) + ? this.dependencies.getClusterById(clusterId) : undefined; if (cluster && clusterId) { @@ -113,10 +116,10 @@ export class ClusterFrameHandler { return undefined; }, (view: LensView) => { - logger.info(`[LENS-VIEW]: cluster id=${clusterId} should now be visible`); + this.dependencies.logger.info(`[LENS-VIEW]: cluster id=${clusterId} should now be visible`); view.frame.classList.remove("hidden"); view.frame.focus(); - ipcRenderer.send(clusterVisibilityHandler, clusterId); + this.dependencies.sendSetVisibleCluster({ action: "set", clusterId }); }, ); } diff --git a/src/renderer/components/cluster-manager/parent-element.injectable.ts b/src/renderer/components/cluster-manager/parent-element.injectable.ts new file mode 100644 index 0000000000..7f9eafb7a6 --- /dev/null +++ b/src/renderer/components/cluster-manager/parent-element.injectable.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 { getInjectable } from "@ogre-tools/injectable"; +import assert from "assert"; + +const clusterFrameParentElementInjectable = getInjectable({ + id: "cluster-frame-parent-element", + instantiate: () => { + const elem = document.getElementById("#lens-view"); + + assert(elem, "DOM with #lens-views must be present"); + + return elem; + }, + causesSideEffects: true, +}); + +export default clusterFrameParentElementInjectable; diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index b139dbb9a9..9dcf88550a 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -65,6 +65,7 @@ import setupSystemCaInjectable from "./frames/root-frame/setup-system-ca.injecta import extensionShouldBeEnabledForClusterFrameInjectable from "./extension-loader/extension-should-be-enabled-for-cluster-frame.injectable"; import { asyncComputed } from "@ogre-tools/injectable-react"; import forceUpdateModalRootFrameComponentInjectable from "./application-update/force-update-modal/force-update-modal-root-frame-component.injectable"; +import clusterFrameParentElementInjectable from "./components/cluster-manager/parent-element.injectable"; import legacyOnChannelListenInjectable from "./ipc/legacy-channel-listen.injectable"; import getEntitySettingCommandsInjectable from "./components/command-palette/registered-commands/get-entity-setting-commands.injectable"; import storageSaveDelayInjectable from "./utils/create-storage/storage-save-delay.injectable"; @@ -108,6 +109,7 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) di.override(terminalSpawningPoolInjectable, () => document.createElement("div")); di.override(hostedClusterIdInjectable, () => undefined); + di.override(clusterFrameParentElementInjectable, () => document.createElement("div")); di.override(getAbsolutePathInjectable, () => getAbsolutePathFake); di.override(joinPathsInjectable, () => joinPathsFake); diff --git a/src/renderer/routes/route-path-parameters.injectable.ts b/src/renderer/routes/route-path-parameters.injectable.ts index 238002096f..e105476314 100644 --- a/src/renderer/routes/route-path-parameters.injectable.ts +++ b/src/renderer/routes/route-path-parameters.injectable.ts @@ -19,7 +19,6 @@ const routePathParametersInjectable = getInjectable({ return (route: Route): IComputedValue> => ( getOrInsertWith(pathParametersCache, route, () => computed(() => ( - console.log(currentPath.get()), matchPath(currentPath.get(), { path: route.path, exact: true,