diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 53e7a52e4b..a64a5a5eaa 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -10,8 +10,10 @@ import { broadcastMessage } from "../ipc"; import { app } from "electron"; import type { CatalogEntityConstructor, CatalogEntitySpec } from "../catalog/catalog-entity"; import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; -import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc"; +import { requestClusterDisconnection } from "../../renderer/ipc"; import KubeClusterCategoryIcon from "./icons/kubernetes.svg"; +import { asLegacyGlobalFunctionForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; +import requestClusterActivationInjectable from "../../renderer/cluster/request-activation.injectable"; export interface KubernetesClusterPrometheusMetrics { address?: { @@ -63,6 +65,8 @@ export function isKubernetesCluster(item: unknown): item is KubernetesCluster { return item instanceof KubernetesCluster; } +const requestClusterActivation = asLegacyGlobalFunctionForExtensionApi(requestClusterActivationInjectable); + export class KubernetesCluster< Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata, Status extends KubernetesClusterStatus = KubernetesClusterStatus, @@ -78,7 +82,10 @@ export class KubernetesCluster< if (app) { await ClusterStore.getInstance().getById(this.getId())?.activate(); } else { - await requestClusterActivation(this.getId(), false); + await requestClusterActivation({ + clusterId: this.getId(), + force: false, + }); } } diff --git a/src/common/cluster-store/clusters.injectable.ts b/src/common/cluster-store/clusters.injectable.ts new file mode 100644 index 0000000000..50e3e3a6d0 --- /dev/null +++ b/src/common/cluster-store/clusters.injectable.ts @@ -0,0 +1,18 @@ +/** + * 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 clusterStoreInjectable from "./cluster-store.injectable"; + +const clustersInjectable = getInjectable({ + id: "clusters", + instantiate: (di) => { + const store = di.inject(clusterStoreInjectable); + + return computed(() => [...store.clusters.values()]); + }, +}); + +export default clustersInjectable; diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index f28da10c0d..37110bcb3c 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -25,6 +25,8 @@ import type { CanI } from "./authorization-review.injectable"; import type { ListNamespaces } from "./list-namespaces.injectable"; 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"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; @@ -36,6 +38,10 @@ export interface ClusterDependencies { createAuthorizationReview: (config: KubeConfig) => CanI; createListNamespaces: (config: KubeConfig) => ListNamespaces; createVersionDetector: (cluster: Cluster) => VersionDetector; + emitClusterConnectionUpdate: EmitClusterConnectionUpdate; + + // TODO: creating a Cluster should not have such wild side effects + readFileSync: ReadFileSync; } /** @@ -625,7 +631,7 @@ export class Cluster implements ClusterModel, ClusterState { const update: KubeAuthUpdate = { message, isError }; this.dependencies.logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() }); - broadcastMessage(`cluster:connection-update`, this.id, update); + this.dependencies.emitClusterConnectionUpdate({ clusterId: this.id, update }); } protected async getAllowedNamespaces(proxyConfig: KubeConfig) { diff --git a/src/common/cluster/connection-update-channel.injectable.ts b/src/common/cluster/connection-update-channel.injectable.ts new file mode 100644 index 0000000000..34dd6907e5 --- /dev/null +++ b/src/common/cluster/connection-update-channel.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { ClusterId, KubeAuthUpdate } from "../cluster-types"; +import type { MessageChannel } from "../utils/channel/message-channel-injection-token"; +import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token"; +import { getInjectable } from "@ogre-tools/injectable"; + +export interface ClusterConnectionUpdateMessage { + clusterId: ClusterId; + update: KubeAuthUpdate; +} + +export type ClusterConnectionUpdateChannel = MessageChannel; + +const clusterConnectionUpdateChannelInjectable = getInjectable({ + id: "cluster-connection-update-channel", + instantiate: (): ClusterConnectionUpdateChannel => ({ + id: "cluster:connection-update", + }), + injectionToken: messageChannelInjectionToken, +}); + +export default clusterConnectionUpdateChannelInjectable; diff --git a/src/common/cluster/request-activation.injectable.ts b/src/common/cluster/request-activation.injectable.ts new file mode 100644 index 0000000000..47f6febdb1 --- /dev/null +++ b/src/common/cluster/request-activation.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 type { ClusterId } from "../cluster-types"; +import type { RequestChannel } from "../utils/channel/request-channel-injection-token"; + +export interface ClusterActivationRequest { + clusterId: ClusterId; + force: boolean; +} +export type ClusterActivationRequestChannel = RequestChannel; + +const clusterActivationRequestChannelInjectable = getInjectable({ + id: "cluster-activation-request-channel", + instantiate: (): ClusterActivationRequestChannel => ({ + id: "cluster-request-activation", + }), +}); + +export default clusterActivationRequestChannelInjectable; diff --git a/src/common/ipc/cluster.ts b/src/common/ipc/cluster.ts index 9f69ff42d5..4b27b12431 100644 --- a/src/common/ipc/cluster.ts +++ b/src/common/ipc/cluster.ts @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export const clusterActivateHandler = "cluster:activate"; export const clusterSetFrameIdHandler = "cluster:set-frame-id"; export const clusterVisibilityHandler = "cluster:visibility"; export const clusterRefreshHandler = "cluster:refresh"; diff --git a/src/common/utils/channel/channel-injection-token.ts b/src/common/utils/channel/channel-injection-token.ts deleted file mode 100644 index 6006290f89..0000000000 --- a/src/common/utils/channel/channel-injection-token.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - - -export interface Channel { - id: string; - _messageTemplate?: MessageTemplate; - _returnTemplate?: ReturnTemplate; -} - diff --git a/src/common/utils/channel/message-channel-injection-token.ts b/src/common/utils/channel/message-channel-injection-token.ts index 3141acedf3..e7502c6e34 100644 --- a/src/common/utils/channel/message-channel-injection-token.ts +++ b/src/common/utils/channel/message-channel-injection-token.ts @@ -4,9 +4,8 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { JsonValue } from "type-fest"; -export interface MessageChannel { +export interface MessageChannel { id: string; _messageSignature?: Message; } diff --git a/src/common/utils/channel/message-to-channel-injection-token.ts b/src/common/utils/channel/message-to-channel-injection-token.ts index 8c5f03b9ee..eedc2e81ad 100644 --- a/src/common/utils/channel/message-to-channel-injection-token.ts +++ b/src/common/utils/channel/message-to-channel-injection-token.ts @@ -6,6 +6,10 @@ import { getInjectionToken } from "@ogre-tools/injectable"; import type { SetRequired } from "type-fest"; import type { MessageChannel } from "./message-channel-injection-token"; +export type EmitChannelMessage = Channel extends MessageChannel + ? (message: Message) => void + : never; + export interface MessageToChannel { , TMessage extends void>( channel: TChannel, diff --git a/src/common/utils/channel/request-channel-injection-token.ts b/src/common/utils/channel/request-channel-injection-token.ts index 67044db878..84c0b64138 100644 --- a/src/common/utils/channel/request-channel-injection-token.ts +++ b/src/common/utils/channel/request-channel-injection-token.ts @@ -4,11 +4,10 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { JsonValue } from "type-fest"; export interface RequestChannel< - Request extends JsonValue | void = void, - Response extends JsonValue | void = void, + Request = void, + Response = void, > { id: string; _requestSignature?: Request; diff --git a/src/common/utils/channel/request-from-channel-injection-token.ts b/src/common/utils/channel/request-from-channel-injection-token.ts index 5f4492543f..271f4b6aba 100644 --- a/src/common/utils/channel/request-from-channel-injection-token.ts +++ b/src/common/utils/channel/request-from-channel-injection-token.ts @@ -6,6 +6,12 @@ import { getInjectionToken } from "@ogre-tools/injectable"; import type { SetRequired } from "type-fest"; import type { RequestChannel } from "./request-channel-injection-token"; +export type RequestFromChannelImpl = Channel extends RequestChannel + ? Request extends void + ? () => Promise + : (req: Request) => Promise + : never; + export type RequestFromChannel = < TChannel extends RequestChannel, >( diff --git a/src/features/cluster/connection-status.test.tsx b/src/features/cluster/connection-status.test.tsx new file mode 100644 index 0000000000..521408e113 --- /dev/null +++ b/src/features/cluster/connection-status.test.tsx @@ -0,0 +1,84 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +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"; +import type { Cluster } from "../../common/cluster/cluster"; +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 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"; + +describe("cluster connection status", () => { + let clusterStore: ClusterStore; + let clusters: Map; + let cluster: Cluster; + let applicationBuilder: ApplicationBuilder; + + beforeEach(async () => { + applicationBuilder = getApplicationBuilder(); + + const readFileSyncMock: ReadFileSync = (filePath) => { + expect(filePath).toBe("/some/file/path"); + + return JSON.stringify({ + apiVersion: "v1", + clusters: [{ + name: "minikube", + cluster: { + server: "https://192.168.64.3:8443", + }, + }], + contexts: [{ + context: { + cluster: "minikube", + user: "minikube", + }, + name: "minikube", + }], + users: [{ + name: "minikube", + }], + kind: "Config", + preferences: {}, + }); + }; + + applicationBuilder.dis.rendererDi.override(readFileSyncInjectable, () => readFileSyncMock); + + const navigateToClusterView = applicationBuilder.dis.rendererDi.inject(navigateToClusterViewInjectable); + const createCluster = applicationBuilder.dis.rendererDi.inject(createClusterInjectable); + + cluster = createCluster({ + contextName: "minikube", + id: "some-cluster-id", + kubeConfigPath: "/some/file/path", + }, { + clusterServerUrl: "https://localhost:1234", + }); + + clusters = new Map(); + + clusters.set(cluster.id, cluster); + + clusterStore = ({ + clusters, + get clustersList() { + return [...clusters.values()]; + }, + getById: (id) => clusters.get(id), + }) as ClusterStore; + + applicationBuilder.dis.mainDi.override(clusterStoreInjectable, () => clusterStore); + applicationBuilder.dis.rendererDi.override(clusterStoreInjectable, () => clusterStore); + + await applicationBuilder.render(); + + navigateToClusterView(cluster.id); + }); +}); diff --git a/src/main/cluster/emit-connection-update.injectable.ts b/src/main/cluster/emit-connection-update.injectable.ts new file mode 100644 index 0000000000..a6d981d0ae --- /dev/null +++ b/src/main/cluster/emit-connection-update.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 type { ClusterConnectionUpdateChannel } from "../../common/cluster/connection-update-channel.injectable"; +import clusterConnectionUpdateChannelInjectable from "../../common/cluster/connection-update-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 EmitClusterConnectionUpdate = EmitChannelMessage; + +const emitClusterConnectionUpdateInjectable = getInjectable({ + id: "emit-cluster-connection-update", + instantiate: (di): EmitClusterConnectionUpdate => { + const channel = di.inject(clusterConnectionUpdateChannelInjectable); + const messageToChannel = di.inject(messageToChannelInjectable); + + return (message) => messageToChannel(channel, message); + }, +}); + +export default emitClusterConnectionUpdateInjectable; diff --git a/src/main/cluster/request-activation-handler.injectable.ts b/src/main/cluster/request-activation-handler.injectable.ts new file mode 100644 index 0000000000..0041de753d --- /dev/null +++ b/src/main/cluster/request-activation-handler.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable"; +import type { ClusterActivationRequestChannel } from "../../common/cluster/request-activation.injectable"; +import clusterActivationRequestChannelInjectable from "../../common/cluster/request-activation.injectable"; +import type { RequestChannelListener } from "../../common/utils/channel/request-channel-listener-injection-token"; +import { requestChannelListenerInjectionToken } from "../../common/utils/channel/request-channel-listener-injection-token"; + +const requestClusterActivationHandlerInjectable = getInjectable({ + id: "request-cluster-activation-handler", + instantiate: (di) => { + const channel = di.inject(clusterActivationRequestChannelInjectable); + const store = di.inject(clusterStoreInjectable); + + return { + channel, + handler: ({ clusterId, force }) => { + store.getById(clusterId)?.activate(force); + }, + } as RequestChannelListener; + }, + injectionToken: requestChannelListenerInjectionToken, +}); + +export default requestClusterActivationHandlerInjectable; diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts index 07c2ee3247..a4b1befeac 100644 --- a/src/main/create-cluster/create-cluster.injectable.ts +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -15,6 +15,8 @@ import listNamespacesInjectable from "../../common/cluster/list-namespaces.injec import loggerInjectable from "../../common/logger.injectable"; import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable"; import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable"; +import readFileSyncInjectable from "../../common/fs/read-file-sync.injectable"; +import emitClusterConnectionUpdateInjectable from "../cluster/emit-connection-update.injectable"; const createClusterInjectable = getInjectable({ id: "create-cluster", @@ -30,6 +32,8 @@ const createClusterInjectable = getInjectable({ logger: di.inject(loggerInjectable), detectorRegistry: di.inject(detectorRegistryInjectable), createVersionDetector: di.inject(createVersionDetectorInjectable), + readFileSync: di.inject(readFileSyncInjectable), + emitClusterConnectionUpdate: di.inject(emitClusterConnectionUpdateInjectable), }; return (model, configData) => new Cluster(dependencies, model, configData); 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 9342696c77..03e0cab953 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 { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../../common/ipc/cluster"; +import { clusterSetFrameIdHandler, clusterVisibilityHandler, 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"; @@ -37,12 +37,6 @@ interface Dependencies { } export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath, clusterManager, catalogEntityRegistry, clusterStore, operatingSystemTheme, askUserForFilePaths }: Dependencies) => { - ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { - return ClusterStore.getInstance() - .getById(clusterId) - ?.activate(force); - }); - ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => { const cluster = ClusterStore.getInstance().getById(clusterId); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index a333e5b1d4..5ca2feb16a 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -95,6 +95,7 @@ import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; import electronInjectable from "./utils/resolve-system-proxy/electron.injectable"; import type { HotbarStore } from "../common/hotbars/store"; import focusApplicationInjectable from "./electron-app/features/focus-application.injectable"; +import readFileSyncInjectable from "../common/fs/read-file-sync.injectable"; import type { GlobalOverride } from "../common/test-utils/get-global-override"; export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) { @@ -177,6 +178,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) readJsonFileInjectable, readFileInjectable, execFileInjectable, + readFileSyncInjectable, ]); // TODO: Remove usages of globally exported appEventBus to get rid of this diff --git a/src/main/utils/channel/message-to-channel.injectable.ts b/src/main/utils/channel/message-to-channel.injectable.ts index cbcdc2badd..6c5c8d48de 100644 --- a/src/main/utils/channel/message-to-channel.injectable.ts +++ b/src/main/utils/channel/message-to-channel.injectable.ts @@ -3,8 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import type { MessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token"; import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; -import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token"; import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json"; import getVisibleWindowsInjectable from "../../start-main-application/lens-window/get-visible-windows.injectable"; @@ -14,15 +14,13 @@ const messageToChannelInjectable = getInjectable({ instantiate: (di) => { const getVisibleWindows = di.inject(getVisibleWindowsInjectable); - // TODO: Figure out way to improve typing in internals - // Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly. - return (channel: MessageChannel, message?: unknown) => { + return ((channel, message) => { const stringifiedMessage = tentativeStringifyJson(message); getVisibleWindows().forEach((lensWindow) => lensWindow.send({ channel: channel.id, data: stringifiedMessage ? [stringifiedMessage] : [] }), ); - }; + }) as MessageToChannel; }, injectionToken: messageToChannelInjectionToken, diff --git a/src/renderer/cluster/connection-update-channel-listener.injectable.ts b/src/renderer/cluster/connection-update-channel-listener.injectable.ts new file mode 100644 index 0000000000..b1ad6c91fa --- /dev/null +++ b/src/renderer/cluster/connection-update-channel-listener.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import clusterConnectionStatusStateInjectable from "../components/cluster-manager/cluster-status.state.injectable"; +import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token"; +import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token"; +import type { ClusterConnectionUpdateChannel } from "../../common/cluster/connection-update-channel.injectable"; +import clusterConnectionUpdateChannelInjectable from "../../common/cluster/connection-update-channel.injectable"; + +const clusterConnectionChannelListenerInjectable = getInjectable({ + id: "cluster-connection-channel-listener", + instantiate: (di): MessageChannelListener => { + const clusterConnectionUpdateChannel = di.inject(clusterConnectionUpdateChannelInjectable); + const state = di.inject(clusterConnectionStatusStateInjectable); + + return { + channel: clusterConnectionUpdateChannel, + handler: ({ clusterId, update }) => { + const status = state.forCluster(clusterId); + + status.appendAuthUpdate(update); + }, + }; + }, + injectionToken: messageChannelListenerInjectionToken, +}); + +export default clusterConnectionChannelListenerInjectable; diff --git a/src/renderer/cluster/request-activation.injectable.ts b/src/renderer/cluster/request-activation.injectable.ts new file mode 100644 index 0000000000..6ac50401e2 --- /dev/null +++ b/src/renderer/cluster/request-activation.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 type { ClusterActivationRequestChannel } from "../../common/cluster/request-activation.injectable"; +import clusterActivationRequestChannelInjectable from "../../common/cluster/request-activation.injectable"; +import type { RequestFromChannelImpl } from "../../common/utils/channel/request-from-channel-injection-token"; +import requestFromChannelInjectable from "../utils/channel/request-from-channel.injectable"; + +export type RequestClusterActivation = RequestFromChannelImpl; + +const requestClusterActivationInjectable = getInjectable({ + id: "request-cluster-activation", + instantiate: (di): RequestClusterActivation => { + const channel = di.inject(clusterActivationRequestChannelInjectable); + const requestFromChannel = di.inject(requestFromChannelInjectable); + + return (clusterId) => requestFromChannel(channel, clusterId); + }, +}); + +export default requestClusterActivationInjectable; diff --git a/src/renderer/components/cluster-manager/cluster-status-watcher.injectable.ts b/src/renderer/components/cluster-manager/cluster-status-watcher.injectable.ts index 4f77ceb6f1..8804800143 100644 --- a/src/renderer/components/cluster-manager/cluster-status-watcher.injectable.ts +++ b/src/renderer/components/cluster-manager/cluster-status-watcher.injectable.ts @@ -3,55 +3,53 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { action, reaction } from "mobx"; -import type { ClusterStore } from "../../../common/cluster-store/cluster-store"; -import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable"; -import type { ClusterId, KubeAuthUpdate } from "../../../common/cluster-types"; -import ipcRendererInjectable from "../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; -import type { ClusterConnectionStatusState } from "./cluster-status.state.injectable"; +import { action, computed, reaction } from "mobx"; +import clustersInjectable from "../../../common/cluster-store/clusters.injectable"; +import type { ClusterId } from "../../../common/cluster-types"; +import setupAppPathsInjectable from "../../app-paths/setup-app-paths.injectable"; +import { beforeFrameStartsInjectionToken } from "../../before-frame-starts/before-frame-starts-injection-token"; +import clusterConnectionStatusStateInjectable from "./cluster-status.state.injectable"; -function computeRisingEdgeForClusterDisconnect(store: ClusterStore, onNewlyDisconnected: (clusterId: ClusterId) => void) { - const disconnectedStateComputer = () => store.clustersList.map(cluster => [cluster.id, cluster.disconnected] as const); - const state = new Map(disconnectedStateComputer()); - - reaction( - disconnectedStateComputer, - (disconnectedStates) => { - for (const [clusterId, isDisconnected] of disconnectedStates) { - if (state.get(clusterId) === isDisconnected) { - // do nothing - } else { - state.set(clusterId, isDisconnected); // save the new state - - if (isDisconnected) { - // If the new value is `true` then the previous value was falsy and this is the rising edge. - onNewlyDisconnected(clusterId); - } - } - } - }, - ); -} - -// This needs to be an `init` function to bypass a bug in the setup -> injectable -> setup path -const initClusterStatusWatcherInjectable = getInjectable({ +const startClusterStatusClearingInjectable = getInjectable({ id: "cluster-status-watcher", instantiate: (di) => { - const ipcRenderer = di.inject(ipcRendererInjectable); - const clusterStore = di.inject(clusterStoreInjectable); + const statusState = di.inject(clusterConnectionStatusStateInjectable); + const state = new Map(); + const onNewlyDisconnected = action((clusterId: ClusterId) => { + const status = statusState.forCluster(clusterId); - return (state: ClusterConnectionStatusState) => { - ipcRenderer.on("cluster:connection-update", (evt, clusterId: ClusterId, update: KubeAuthUpdate) => { - state.forCluster(clusterId).appendAuthUpdate(update); - }); - computeRisingEdgeForClusterDisconnect(clusterStore, action((clusterId) => { - const forCluster = state.forCluster(clusterId); + status.clearReconnectingState(); + status.resetAuthOutput(); + }); - forCluster.clearReconnectingState(); - forCluster.resetAuthOutput(); - })); + return { + run: () => { + const clusters = di.inject(clustersInjectable); // This has to be in here so that it happens after the `setupAppPaths` + const disconnectedStates = computed(() => clusters.get().map(cluster => [cluster.id, cluster.disconnected] as const)); + + reaction( + () => disconnectedStates.get(), + states => { + for (const [clusterId, isDisconnected] of states) { + if (state.get(clusterId) !== isDisconnected) { + state.set(clusterId, isDisconnected); // save the new state + + if (isDisconnected) { + // If the new value is `true` then the previous value was falsy and this is the rising edge. + onNewlyDisconnected(clusterId); + } + } + } + }, + { + fireImmediately: true, + }, + ); + }, + runAfter: di.inject(setupAppPathsInjectable), }; }, + injectionToken: beforeFrameStartsInjectionToken, }); -export default initClusterStatusWatcherInjectable; +export default startClusterStatusClearingInjectable; diff --git a/src/renderer/components/cluster-manager/cluster-status.state.injectable.ts b/src/renderer/components/cluster-manager/cluster-status.state.injectable.ts index 3a755a2f83..ea373305c9 100644 --- a/src/renderer/components/cluster-manager/cluster-status.state.injectable.ts +++ b/src/renderer/components/cluster-manager/cluster-status.state.injectable.ts @@ -8,7 +8,6 @@ import { action, computed, observable } from "mobx"; import type { ClusterId, KubeAuthUpdate } from "../../../common/cluster-types"; import loggerInjectable from "../../../common/logger.injectable"; import { getOrInsert, hasTypedProperty, isBoolean, isObject, isString } from "../../utils"; -import initClusterStatusWatcherInjectable from "./cluster-status-watcher.injectable"; export interface ClusterConnectionStatus { readonly authOutput: IComputedValue; @@ -21,18 +20,17 @@ export interface ClusterConnectionStatus { } export interface ClusterConnectionStatusState { - forCluster(clusterId: ClusterId): Readonly; + forCluster(clusterId: ClusterId): ClusterConnectionStatus; } const clusterConnectionStatusStateInjectable = getInjectable({ id: "cluster-connection-status-state", - instantiate: (di) => { + instantiate: (di): ClusterConnectionStatusState => { const authOutputs = observable.map(); const reconnecting = observable.set(); - const initWatcher = di.inject(initClusterStatusWatcherInjectable); const logger = di.inject(loggerInjectable); - const state: ClusterConnectionStatusState = { + return { forCluster: (clusterId) => { const authOutput = computed(() => authOutputs.get(clusterId) ?? []); @@ -63,10 +61,6 @@ const clusterConnectionStatusStateInjectable = getInjectable({ }; }, }; - - initWatcher(state); - - return state; }, }); diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 5c016a7bab..811a30478b 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -15,13 +15,14 @@ import { Button } from "../button"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry"; -import { requestClusterActivation } from "../../ipc"; import type { NavigateToEntitySettings } from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; import navigateToEntitySettingsInjectable from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable"; import type { ClusterConnectionStatus } from "./cluster-status.state.injectable"; import clusterConnectionStatusStateInjectable from "./cluster-status.state.injectable"; +import type { RequestClusterActivation } from "../../cluster/request-activation.injectable"; +import requestClusterActivationInjectable from "../../cluster/request-activation.injectable"; export interface ClusterStatusProps { className?: IClassName; @@ -32,6 +33,7 @@ interface Dependencies { navigateToEntitySettings: NavigateToEntitySettings; entityRegistry: CatalogEntityRegistry; state: ClusterConnectionStatus; + requestClusterActivation: RequestClusterActivation; } const NonInjectedClusterStatus = observer((props: ClusterStatusProps & Dependencies) => { @@ -41,6 +43,7 @@ const NonInjectedClusterStatus = observer((props: ClusterStatusProps & Dependenc state, className, entityRegistry, + requestClusterActivation, } = props; const entity = entityRegistry.getById(cluster.id); const clusterName = entity?.getName() ?? cluster.name; @@ -52,7 +55,10 @@ const NonInjectedClusterStatus = observer((props: ClusterStatusProps & Dependenc }); try { - await requestClusterActivation(cluster.id, true); + await requestClusterActivation({ + clusterId: cluster.id, + force: true, + }); } catch (error) { state.appendAuthUpdate({ message: String(error), @@ -141,5 +147,6 @@ export const ClusterStatus = withInjectables(N navigateToEntitySettings: di.inject(navigateToEntitySettingsInjectable), entityRegistry: di.inject(catalogEntityRegistryInjectable), state: di.inject(clusterConnectionStatusStateInjectable).forCluster(props.cluster.id), + requestClusterActivation: di.inject(requestClusterActivationInjectable), }), }); diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index 0badc8e0d5..09046af112 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -12,7 +12,6 @@ import { ClusterStatus } from "./cluster-status"; import type { ClusterFrameHandler } from "./cluster-frame-handler"; import type { Cluster } from "../../../common/cluster/cluster"; import type { ClusterStore } from "../../../common/cluster-store/cluster-store"; -import { requestClusterActivation } from "../../ipc"; import { withInjectables } from "@ogre-tools/injectable-react"; import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; @@ -25,6 +24,8 @@ import clusterConnectionStatusStateInjectable from "./cluster-status.state.injec import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable"; import type { Disposer } from "../../utils"; import { disposer } from "../../utils"; +import type { RequestClusterActivation } from "../../cluster/request-activation.injectable"; +import requestClusterActivationInjectable from "../../cluster/request-activation.injectable"; interface Dependencies { clusterId: IComputedValue; @@ -33,6 +34,7 @@ interface Dependencies { entityRegistry: CatalogEntityRegistry; clusterConnectionStatusState: ClusterConnectionStatusState; clusterStore: ClusterStore; + requestClusterActivation: RequestClusterActivation; } @observer @@ -104,7 +106,10 @@ class NonInjectedClusterView extends React.Component { this.props.clusterFrames.setVisibleCluster(clusterId); this.props.clusterFrames.initView(clusterId); - requestClusterActivation(clusterId, false); // activate and fetch cluster's state from main + this.props.requestClusterActivation({ + clusterId, + force: false, + }); this.props.entityRegistry.activeEntity = clusterId; const navigateToClusterDisposer = disposer( @@ -160,6 +165,7 @@ export const ClusterView = withInjectables(NonInjectedClusterView, entityRegistry: di.inject(catalogEntityRegistryInjectable), clusterConnectionStatusState: di.inject(clusterConnectionStatusStateInjectable), clusterStore: di.inject(clusterStoreInjectable), + requestClusterActivation: di.inject(requestClusterActivationInjectable), }), }); diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index 55e3a4817d..891a4b8d9c 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -143,8 +143,6 @@ export const getApplicationBuilder = () => { doGeneralOverrides: true, }); - mainDi.register(mainExtensionsStateInjectable); - const overrideChannelsForWindow = overrideChannels(mainDi); const beforeApplicationStartCallbacks: Callback[] = []; diff --git a/src/renderer/create-cluster/create-cluster.injectable.ts b/src/renderer/create-cluster/create-cluster.injectable.ts index 7e997477a6..fae1bf2519 100644 --- a/src/renderer/create-cluster/create-cluster.injectable.ts +++ b/src/renderer/create-cluster/create-cluster.injectable.ts @@ -8,6 +8,7 @@ import { Cluster } from "../../common/cluster/cluster"; import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import loggerInjectable from "../../common/logger.injectable"; +import readFileSyncInjectable from "../../common/fs/read-file-sync.injectable"; const createClusterInjectable = getInjectable({ id: "create-cluster", @@ -16,6 +17,7 @@ const createClusterInjectable = getInjectable({ const dependencies: ClusterDependencies = { directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), logger: di.inject(loggerInjectable), + readFileSync: di.inject(readFileSyncInjectable), // TODO: Dismantle wrong abstraction // Note: "as never" to get around strictness in unnatural scenario @@ -26,6 +28,7 @@ const createClusterInjectable = getInjectable({ createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); }, detectorRegistry: undefined as never, createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); }, + emitClusterConnectionUpdate: () => { throw new Error("Tried to access back-end feature in front-end."); }, }; return (model, configData) => new Cluster(dependencies, model, configData); diff --git a/src/renderer/ipc/index.ts b/src/renderer/ipc/index.ts index 5e204ef98b..2977834776 100644 --- a/src/renderer/ipc/index.ts +++ b/src/renderer/ipc/index.ts @@ -5,7 +5,7 @@ import type { OpenDialogOptions } from "electron"; import { ipcRenderer } from "electron"; -import { clusterActivateHandler, clusterClearDeletingHandler, clusterDeleteHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterSetDeletingHandler, clusterSetFrameIdHandler, clusterStates } from "../../common/ipc/cluster"; +import { clusterClearDeletingHandler, clusterDeleteHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterSetDeletingHandler, clusterSetFrameIdHandler, clusterStates } from "../../common/ipc/cluster"; import type { ClusterId, ClusterState } from "../../common/cluster-types"; import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel, type WindowAction } from "../../common/ipc/window"; import { openFilePickingDialogChannel } from "../../common/ipc/dialog"; @@ -43,10 +43,6 @@ export function requestSetClusterFrameId(clusterId: ClusterId): Promise { return requestMain(clusterSetFrameIdHandler, clusterId); } -export function requestClusterActivation(clusterId: ClusterId, force?: boolean): Promise { - return requestMain(clusterActivateHandler, clusterId, force); -} - export function requestClusterDisconnection(clusterId: ClusterId, force?: boolean): Promise { return requestMain(clusterDisconnectHandler, clusterId, force); } diff --git a/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts b/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts index 6d76e35340..fa9a6661f9 100644 --- a/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts +++ b/src/renderer/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts @@ -24,9 +24,11 @@ const enlistMessageChannelListenerInjectable = getInjectable({ ); }; + console.debug(`[IPC]: listening on ${channel.id}`); ipcRenderer.on(channel.id, nativeCallback); return () => { + console.debug(`[IPC]: listenering off ${channel.id}`); ipcRenderer.off(channel.id, nativeCallback); }; };