From 853573afcb691c9474a04c56b492b87002fdc1a6 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 5 Apr 2022 11:24:50 -0400 Subject: [PATCH] Fix cluster connect statuses being intertwined - Broadcast on a signle channel and add clusterId as data argument - Make everything else computed based on the current props.cluster value - Fix view staying on cluster connect screen after disconnect - Initially clicking on a disconnected cluster should not immediately nativate to the Catalog if currently viewing a connected cluster - Make route-path-parameters more type correct by not using keyedSingleton Signed-off-by: Sebastian Malton --- src/common/cluster-store/cluster-store.ts | 8 +- src/common/cluster-types.ts | 2 +- src/common/cluster/cluster.ts | 2 +- .../cluster-view-route.injectable.ts | 9 +- src/main/__test__/kube-auth-proxy.test.ts | 12 +- src/main/cluster-manager.injectable.ts | 2 +- src/main/cluster-manager.ts | 26 ++-- .../get-active-cluster-entity.injectable.ts | 10 +- .../hosted-cluster.injectable.ts | 4 + .../catalog-route-parameters.injectable.ts | 2 +- src/renderer/components/+catalog/catalog.tsx | 4 +- .../+custom-resources/crd-resources.tsx | 13 +- ...m-resources-route-parameters.injectable.ts | 2 +- ...ty-settings-route-parameters.injectable.ts | 2 +- .../+entity-settings/entity-settings.tsx | 11 +- ...helm-charts-route-parameters.injectable.ts | 2 +- .../components/+helm-charts/helm-charts.tsx | 4 +- ...lm-releases-route-parameters.injectable.ts | 2 +- .../components/+helm-releases/releases.tsx | 28 ++-- ...rt-forwards-route-parameters.injectable.ts | 2 +- .../+network-port-forwards/port-forwards.tsx | 24 +-- .../cluster-status-watcher.injectable.ts | 57 +++++++ .../cluster-status.state.injectable.ts | 73 +++++++++ .../cluster-manager/cluster-status.tsx | 142 +++++++----------- ...luster-view-route-parameters.injectable.ts | 2 +- .../cluster-manager/cluster-view.tsx | 80 +++++++--- .../command-palette/command-container.tsx | 4 + .../route-path-parameters.injectable.ts | 29 ++-- 28 files changed, 361 insertions(+), 197 deletions(-) create mode 100644 src/renderer/components/cluster-manager/cluster-status-watcher.injectable.ts create mode 100644 src/renderer/components/cluster-manager/cluster-status.state.injectable.ts diff --git a/src/common/cluster-store/cluster-store.ts b/src/common/cluster-store/cluster-store.ts index 62b8cda973..a3671a047b 100644 --- a/src/common/cluster-store/cluster-store.ts +++ b/src/common/cluster-store/cluster-store.ts @@ -106,12 +106,8 @@ export class ClusterStore extends BaseStore { return this.clusters.size > 0; } - getById(id: ClusterId | undefined): Cluster | undefined { - if (id) { - return this.clusters.get(id); - } - - return undefined; + getById(id: ClusterId): Cluster | undefined { + return this.clusters.get(id); } addCluster(clusterOrModel: ClusterModel | Cluster): Cluster { diff --git a/src/common/cluster-types.ts b/src/common/cluster-types.ts index f0862e07b8..22cb461e1c 100644 --- a/src/common/cluster-types.ts +++ b/src/common/cluster-types.ts @@ -120,7 +120,7 @@ export enum ClusterStatus { } /** - * The message format for the "cluster::connection-update" channels + * The message format for the "cluster:connection-update" channel */ export interface KubeAuthUpdate { message: string; diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index 11b67bd002..f28da10c0d 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -625,7 +625,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:${this.id}:connection-update`, update); + broadcastMessage(`cluster:connection-update`, this.id, update); } protected async getAllowedNamespaces(proxyConfig: KubeConfig) { diff --git a/src/common/front-end-routing/routes/cluster-view/cluster-view-route.injectable.ts b/src/common/front-end-routing/routes/cluster-view/cluster-view-route.injectable.ts index e912ff63e0..e4b719aeb1 100644 --- a/src/common/front-end-routing/routes/cluster-view/cluster-view-route.injectable.ts +++ b/src/common/front-end-routing/routes/cluster-view/cluster-view-route.injectable.ts @@ -5,15 +5,20 @@ import { getInjectable } from "@ogre-tools/injectable"; import { computed } from "mobx"; import { frontEndRouteInjectionToken } from "../../front-end-route-injection-token"; +import type { Route } from "../../front-end-route-injection-token"; + +export interface ClusterViewRouteParams { + clusterId: string; +} const clusterViewRouteInjectable = getInjectable({ id: "cluster-view-route", instantiate: () => ({ - path: "/cluster/:clusterId", + path: "/cluster/:clusterId" as const, clusterFrame: false, isEnabled: computed(() => true), - }), + }) as Route, injectionToken: frontEndRouteInjectionToken, }); diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 1e66d0d42b..cb00c075e0 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -219,7 +219,7 @@ describe("kube auth proxy tests", () => { mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()); const cluster = createCluster({ - id: "foobar", + id: "some-cluster-id", kubeConfigPath: "minikube-config.yml", contextName: "minikube", }, { @@ -233,34 +233,34 @@ describe("kube auth proxy tests", () => { await proxy.run(); listeners.emit("error", { message: "foobarbat" }); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", isError: true }); + expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "foobarbat", isError: true }); }); it("should call spawn and broadcast exit", async () => { await proxy.run(); listeners.emit("exit", 0); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 0", isError: false }); + expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "proxy exited with code: 0", isError: false }); }); it("should call spawn and broadcast errors from stderr", async () => { await proxy.run(); listeners.emit("stderr/data", "an error"); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", isError: true }); + expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "an error", isError: true }); }); it("should call spawn and broadcast stdout serving info", async () => { await proxy.run(); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", isError: false }); + expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "Authentication proxy started", isError: false }); }); it("should call spawn and broadcast stdout other info", async () => { await proxy.run(); listeners.emit("stdout/data", "some info"); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "some info", isError: false }); + expect(mockBroadcastIpc).toBeCalledWith("cluster:connection-update", "some-cluster-id", { message: "some info", isError: false }); }); }); }); diff --git a/src/main/cluster-manager.injectable.ts b/src/main/cluster-manager.injectable.ts index 2b55f0e854..40d17ec267 100644 --- a/src/main/cluster-manager.injectable.ts +++ b/src/main/cluster-manager.injectable.ts @@ -13,7 +13,7 @@ const clusterManagerInjectable = getInjectable({ instantiate: (di) => { const clusterManager = new ClusterManager({ store: di.inject(clusterStoreInjectable), - catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable), + entityRegistry: di.inject(catalogEntityRegistryInjectable), }); clusterManager.init(); diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 787f2a1618..ed368a9c93 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -23,8 +23,8 @@ const logPrefix = "[CLUSTER-MANAGER]:"; const lensSpecificClusterStatuses: Set = new Set(Object.values(LensKubernetesClusterStatus)); interface Dependencies { - store: ClusterStore; - catalogEntityRegistry: CatalogEntityRegistry; + readonly store: ClusterStore; + readonly entityRegistry: CatalogEntityRegistry; } export class ClusterManager { @@ -32,7 +32,7 @@ export class ClusterManager { @observable visibleCluster: ClusterId | undefined = undefined; - constructor(private dependencies: Dependencies) { + constructor(protected readonly dependencies: Dependencies) { makeObservable(this); } @@ -52,12 +52,12 @@ export class ClusterManager { ); reaction( - () => this.dependencies.catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster), + () => this.dependencies.entityRegistry.filterItemsByPredicate(isKubernetesCluster), entities => this.syncClustersFromCatalog(entities), ); reaction(() => [ - this.dependencies.catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster), + this.dependencies.entityRegistry.filterItemsByPredicate(isKubernetesCluster), this.visibleCluster, ] as const, ([entities, visibleCluster]) => { for (const entity of entities) { @@ -71,7 +71,7 @@ export class ClusterManager { observe(this.deleting, change => { if (change.type === "add") { - this.updateEntityStatus(this.dependencies.catalogEntityRegistry.findById(change.newValue) as KubernetesCluster); + this.updateEntityStatus(this.dependencies.entityRegistry.findById(change.newValue) as KubernetesCluster); } }); @@ -89,13 +89,13 @@ export class ClusterManager { } protected updateEntityFromCluster(cluster: Cluster) { - const index = this.dependencies.catalogEntityRegistry.items.findIndex((entity) => entity.getId() === cluster.id); + const index = this.dependencies.entityRegistry.items.findIndex((entity) => entity.getId() === cluster.id); if (index === -1) { return; } - const entity = this.dependencies.catalogEntityRegistry.items[index] as KubernetesCluster; + const entity = this.dependencies.entityRegistry.items[index] as KubernetesCluster; this.updateEntityStatus(entity, cluster); @@ -136,7 +136,7 @@ export class ClusterManager { cluster.preferences.icon = undefined; } - this.dependencies.catalogEntityRegistry.items.splice(index, 1, entity); + this.dependencies.entityRegistry.items.splice(index, 1, entity); } @action @@ -279,7 +279,13 @@ export class ClusterManager { return cluster; } - return this.dependencies.store.getById(getClusterIdFromHost(req.headers.host)); + const clusterId = getClusterIdFromHost(req.headers.host); + + if (!clusterId) { + return undefined; + } + + return this.dependencies.store.getById(clusterId); }; } diff --git a/src/renderer/api/catalog/entity/get-active-cluster-entity.injectable.ts b/src/renderer/api/catalog/entity/get-active-cluster-entity.injectable.ts index d5a2d1f3f8..a0055f7ccb 100644 --- a/src/renderer/api/catalog/entity/get-active-cluster-entity.injectable.ts +++ b/src/renderer/api/catalog/entity/get-active-cluster-entity.injectable.ts @@ -15,7 +15,15 @@ const getActiveClusterEntityInjectable = getInjectable({ const store = di.inject(clusterStoreInjectable); const entityRegistry = di.inject(catalogEntityRegistryInjectable); - return () => store.getById(entityRegistry.activeEntity?.getId()); + return () => { + const entityId = entityRegistry.activeEntity?.getId(); + + if (!entityId) { + return undefined; + } + + return store.getById(entityId); + }; }, }); diff --git a/src/renderer/cluster-frame-context/hosted-cluster.injectable.ts b/src/renderer/cluster-frame-context/hosted-cluster.injectable.ts index a466984952..0eead72ccf 100644 --- a/src/renderer/cluster-frame-context/hosted-cluster.injectable.ts +++ b/src/renderer/cluster-frame-context/hosted-cluster.injectable.ts @@ -13,6 +13,10 @@ const hostedClusterInjectable = getInjectable({ const hostedClusterId = di.inject(hostedClusterIdInjectable); const store = di.inject(clusterStoreInjectable); + if (!hostedClusterId) { + return undefined; + } + return store.getById(hostedClusterId); }, }); diff --git a/src/renderer/components/+catalog/catalog-route-parameters.injectable.ts b/src/renderer/components/+catalog/catalog-route-parameters.injectable.ts index 0d9bd1fc4e..d5aafe263e 100644 --- a/src/renderer/components/+catalog/catalog-route-parameters.injectable.ts +++ b/src/renderer/components/+catalog/catalog-route-parameters.injectable.ts @@ -12,7 +12,7 @@ const catalogRouteParametersInjectable = getInjectable({ instantiate: (di) => { const route = di.inject(catalogRouteInjectable); - const pathParameters = di.inject(routePathParametersInjectable, route); + const pathParameters = di.inject(routePathParametersInjectable)(route); return { group: computed(() => pathParameters.get().group), diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 723c21da13..7ab008368e 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -56,8 +56,8 @@ interface Dependencies { customCategoryViews: IComputedValue>>; emitEvent: (event: AppEvent) => void; routeParameters: { - group: IComputedValue; - kind: IComputedValue; + group: IComputedValue; + kind: IComputedValue; }; navigateToCatalog: NavigateToCatalog; hotbarStore: HotbarStore; diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 7b3d87529f..96e042b451 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -27,8 +27,8 @@ enum columnId { } interface Dependencies { - group: IComputedValue; - name: IComputedValue; + group: IComputedValue; + name: IComputedValue; apiManager: ApiManager; customResourceDefinitionStore: CustomResourceDefinitionStore; } @@ -41,7 +41,14 @@ class NonInjectedCustomResources extends React.Component { } @computed get crd() { - return this.props.customResourceDefinitionStore.getByGroup(this.props.group.get(), this.props.name.get()); + const group = this.props.group.get(); + const name = this.props.name.get(); + + if (!group || !name) { + return undefined; + } + + return this.props.customResourceDefinitionStore.getByGroup(group, name); } @computed get store() { diff --git a/src/renderer/components/+custom-resources/custom-resources-route-parameters.injectable.ts b/src/renderer/components/+custom-resources/custom-resources-route-parameters.injectable.ts index 2debbe85bd..a0671fded9 100644 --- a/src/renderer/components/+custom-resources/custom-resources-route-parameters.injectable.ts +++ b/src/renderer/components/+custom-resources/custom-resources-route-parameters.injectable.ts @@ -12,7 +12,7 @@ const customResourcesRouteParametersInjectable = getInjectable({ instantiate: (di) => { const route = di.inject(customResourcesRouteInjectable); - const pathParameters = di.inject(routePathParametersInjectable, route); + const pathParameters = di.inject(routePathParametersInjectable)(route); return { group: computed(() => pathParameters.get().group), diff --git a/src/renderer/components/+entity-settings/entity-settings-route-parameters.injectable.ts b/src/renderer/components/+entity-settings/entity-settings-route-parameters.injectable.ts index 8a1d5600b8..5adf55c72b 100644 --- a/src/renderer/components/+entity-settings/entity-settings-route-parameters.injectable.ts +++ b/src/renderer/components/+entity-settings/entity-settings-route-parameters.injectable.ts @@ -12,7 +12,7 @@ const entitySettingsRouteParametersInjectable = getInjectable({ instantiate: (di) => { const route = di.inject(entitySettingsRouteInjectable); - const pathParameters = di.inject(routePathParametersInjectable, route); + const pathParameters = di.inject(routePathParametersInjectable)(route); return { entityId: computed(() => pathParameters.get().entityId), diff --git a/src/renderer/components/+entity-settings/entity-settings.tsx b/src/renderer/components/+entity-settings/entity-settings.tsx index 366f5758f6..27bf79f572 100644 --- a/src/renderer/components/+entity-settings/entity-settings.tsx +++ b/src/renderer/components/+entity-settings/entity-settings.tsx @@ -24,7 +24,7 @@ import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.i import observableHistoryInjectable from "../../navigation/observable-history.injectable"; interface Dependencies { - entityId: IComputedValue; + entityId: IComputedValue; entityRegistry: CatalogEntityRegistry; observableHistory: ObservableHistory; } @@ -54,8 +54,15 @@ class NonInjectedEntitySettings extends React.Component { return this.props.entityId.get(); } + @computed get entity() { - return this.props.entityRegistry.getById(this.entityId); + const { entityId } = this; + + if (!entityId) { + return undefined; + } + + return this.props.entityRegistry.getById(entityId); } get menuItems() { diff --git a/src/renderer/components/+helm-charts/helm-charts-route-parameters.injectable.ts b/src/renderer/components/+helm-charts/helm-charts-route-parameters.injectable.ts index cc800ff7f3..bc9b3cc31d 100644 --- a/src/renderer/components/+helm-charts/helm-charts-route-parameters.injectable.ts +++ b/src/renderer/components/+helm-charts/helm-charts-route-parameters.injectable.ts @@ -12,7 +12,7 @@ const helmChartsRouteParametersInjectable = getInjectable({ instantiate: (di) => { const route = di.inject(helmChartsRouteInjectable); - const pathParameters = di.inject(routePathParametersInjectable, route); + const pathParameters = di.inject(routePathParametersInjectable)(route); return { chartName: computed(() => pathParameters.get().chartName), diff --git a/src/renderer/components/+helm-charts/helm-charts.tsx b/src/renderer/components/+helm-charts/helm-charts.tsx index 5a603cf7aa..a2f63f6585 100644 --- a/src/renderer/components/+helm-charts/helm-charts.tsx +++ b/src/renderer/components/+helm-charts/helm-charts.tsx @@ -32,8 +32,8 @@ enum columnId { interface Dependencies { routeParameters: { - chartName: IComputedValue; - repo: IComputedValue; + chartName: IComputedValue; + repo: IComputedValue; }; navigateToHelmCharts: NavigateToHelmCharts; diff --git a/src/renderer/components/+helm-releases/helm-releases-route-parameters.injectable.ts b/src/renderer/components/+helm-releases/helm-releases-route-parameters.injectable.ts index e8f637621f..685bb3d8e9 100644 --- a/src/renderer/components/+helm-releases/helm-releases-route-parameters.injectable.ts +++ b/src/renderer/components/+helm-releases/helm-releases-route-parameters.injectable.ts @@ -12,7 +12,7 @@ const helmReleasesRouteParametersInjectable = getInjectable({ instantiate: (di) => { const route = di.inject(helmReleasesRouteInjectable); - const pathParameters = di.inject(routePathParametersInjectable, route); + const pathParameters = di.inject(routePathParametersInjectable)(route); return { namespace: computed(() => pathParameters.get().namespace), diff --git a/src/renderer/components/+helm-releases/releases.tsx b/src/renderer/components/+helm-releases/releases.tsx index 7b5fd99dac..609efa729e 100644 --- a/src/renderer/components/+helm-releases/releases.tsx +++ b/src/renderer/components/+helm-releases/releases.tsx @@ -40,7 +40,7 @@ interface Dependencies { releases: IComputedValue; releasesArePending: IComputedValue; selectNamespace: (namespace: string) => void; - namespace: IComputedValue; + namespace: IComputedValue; navigateToHelmReleases: NavigateToHelmReleases; } @@ -213,20 +213,12 @@ class NonInjectedHelmReleases extends Component { } } -export const HelmReleases = withInjectables( - NonInjectedHelmReleases, - - { - getProps: (di) => { - const routeParameters = di.inject(helmReleasesRouteParametersInjectable); - - return { - releases: di.inject(removableReleasesInjectable), - releasesArePending: di.inject(releasesInjectable).pending, - selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces, - navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable), - namespace: routeParameters.namespace, - }; - }, - }, -); +export const HelmReleases = withInjectables(NonInjectedHelmReleases, { + getProps: (di) => ({ + releases: di.inject(removableReleasesInjectable), + releasesArePending: di.inject(releasesInjectable).pending, + selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces, + navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable), + ...di.inject(helmReleasesRouteParametersInjectable), + }), +}); diff --git a/src/renderer/components/+network-port-forwards/port-forwards-route-parameters.injectable.ts b/src/renderer/components/+network-port-forwards/port-forwards-route-parameters.injectable.ts index 7b56a14ab3..b1c77bc958 100644 --- a/src/renderer/components/+network-port-forwards/port-forwards-route-parameters.injectable.ts +++ b/src/renderer/components/+network-port-forwards/port-forwards-route-parameters.injectable.ts @@ -12,7 +12,7 @@ const portForwardsRouteParametersInjectable = getInjectable({ instantiate: (di) => { const route = di.inject(portForwardsRouteInjectable); - const pathParameters = di.inject(routePathParametersInjectable, route); + const pathParameters = di.inject(routePathParametersInjectable)(route); return { forwardport: computed(() => pathParameters.get().forwardport), diff --git a/src/renderer/components/+network-port-forwards/port-forwards.tsx b/src/renderer/components/+network-port-forwards/port-forwards.tsx index fc6b4c4735..d77d5e0058 100644 --- a/src/renderer/components/+network-port-forwards/port-forwards.tsx +++ b/src/renderer/components/+network-port-forwards/port-forwards.tsx @@ -32,7 +32,7 @@ enum columnId { interface Dependencies { portForwardStore: PortForwardStore; - forwardport: IComputedValue; + forwardport: IComputedValue; navigateToPortForwards: NavigateToPortForwards; } @@ -158,19 +158,11 @@ class NonInjectedPortForwards extends React.Component { } } -export const PortForwards = withInjectables( - NonInjectedPortForwards, - - { - getProps: (di) => { - const routeParameters = di.inject(portForwardsRouteParametersInjectable); - - return { - portForwardStore: di.inject(portForwardStoreInjectable), - forwardport: routeParameters.forwardport, - navigateToPortForwards: di.inject(navigateToPortForwardsInjectable), - }; - }, - }, -); +export const PortForwards = withInjectables(NonInjectedPortForwards, { + getProps: (di) => ({ + portForwardStore: di.inject(portForwardStoreInjectable), + navigateToPortForwards: di.inject(navigateToPortForwardsInjectable), + ...di.inject(portForwardsRouteParametersInjectable), + }), +}); diff --git a/src/renderer/components/cluster-manager/cluster-status-watcher.injectable.ts b/src/renderer/components/cluster-manager/cluster-status-watcher.injectable.ts new file mode 100644 index 0000000000..4f77ceb6f1 --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-status-watcher.injectable.ts @@ -0,0 +1,57 @@ +/** + * 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 { 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"; + +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({ + id: "cluster-status-watcher", + instantiate: (di) => { + const ipcRenderer = di.inject(ipcRendererInjectable); + const clusterStore = di.inject(clusterStoreInjectable); + + 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); + + forCluster.clearReconnectingState(); + forCluster.resetAuthOutput(); + })); + }; + }, +}); + +export default initClusterStatusWatcherInjectable; diff --git a/src/renderer/components/cluster-manager/cluster-status.state.injectable.ts b/src/renderer/components/cluster-manager/cluster-status.state.injectable.ts new file mode 100644 index 0000000000..3a755a2f83 --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-status.state.injectable.ts @@ -0,0 +1,73 @@ +/** + * 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 { IComputedValue } from "mobx"; +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; + readonly hasErrorOutput: IComputedValue; + readonly isReconnecting: IComputedValue; + resetAuthOutput(): void; + setAsReconnecting(): void; + clearReconnectingState(): void; + appendAuthUpdate(update: unknown): void; +} + +export interface ClusterConnectionStatusState { + forCluster(clusterId: ClusterId): Readonly; +} + +const clusterConnectionStatusStateInjectable = getInjectable({ + id: "cluster-connection-status-state", + instantiate: (di) => { + const authOutputs = observable.map(); + const reconnecting = observable.set(); + const initWatcher = di.inject(initClusterStatusWatcherInjectable); + const logger = di.inject(loggerInjectable); + + const state: ClusterConnectionStatusState = { + forCluster: (clusterId) => { + const authOutput = computed(() => authOutputs.get(clusterId) ?? []); + + return { + authOutput, + isReconnecting: computed(() => reconnecting.has(clusterId)), + hasErrorOutput: computed(() => authOutput.get().some(output => output.isError)), + resetAuthOutput: action(() => { + authOutputs.delete(clusterId); + }), + setAsReconnecting: action(() => { + reconnecting.add(clusterId); + }), + clearReconnectingState: action(() => { + reconnecting.delete(clusterId); + }), + appendAuthUpdate: action((update) => { + if ( + isObject(update) + && hasTypedProperty(update, "message", isString) + && hasTypedProperty(update, "isError", isBoolean) + ) { + getOrInsert(authOutputs, clusterId, []).push(update); + } else { + logger.warn(`[CLUSTER]: invalid connection update`, { update, clusterId }); + } + }), + }; + }, + }; + + initWatcher(state); + + return state; + }, +}); + +export default clusterConnectionStatusStateInjectable; diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 1183b4f52b..5c016a7bab 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -5,23 +5,23 @@ import styles from "./cluster-status.module.scss"; -import { computed, observable, makeObservable } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; +import { runInAction } from "mobx"; +import { observer } from "mobx-react"; import React from "react"; -import { ipcRendererOn } from "../../../common/ipc"; import type { Cluster } from "../../../common/cluster/cluster"; import type { IClassName } from "../../utils"; -import { isBoolean, hasTypedProperty, isObject, isString, cssNames } from "../../utils"; +import { cssNames } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; -import type { KubeAuthUpdate } from "../../../common/cluster-types"; 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"; export interface ClusterStatusProps { className?: IClassName; @@ -31,89 +31,58 @@ export interface ClusterStatusProps { interface Dependencies { navigateToEntitySettings: NavigateToEntitySettings; entityRegistry: CatalogEntityRegistry; + state: ClusterConnectionStatus; } -@observer -class NonInjectedClusterStatus extends React.Component { - @observable authOutput: KubeAuthUpdate[] = []; - @observable isReconnecting = false; +const NonInjectedClusterStatus = observer((props: ClusterStatusProps & Dependencies) => { + const { + cluster, + navigateToEntitySettings, + state, + className, + entityRegistry, + } = props; + const entity = entityRegistry.getById(cluster.id); + const clusterName = entity?.getName() ?? cluster.name; - constructor(props: ClusterStatusProps & Dependencies) { - super(props); - makeObservable(this); - } - - get cluster(): Cluster { - return this.props.cluster; - } - - @computed get entity() { - return this.props.entityRegistry.getById(this.cluster.id); - } - - @computed get hasErrors(): boolean { - return this.authOutput.some(({ isError }) => isError); - } - - componentDidMount() { - disposeOnUnmount(this, [ - ipcRendererOn(`cluster:${this.cluster.id}:connection-update`, (evt, res: unknown) => { - if ( - isObject(res) - && hasTypedProperty(res, "message", isString) - && hasTypedProperty(res, "isError", isBoolean) - ) { - this.authOutput.push(res); - } else { - console.warn(`Got invalid connection update for ${this.cluster.id}`, { update: res }); - } - }), - ]); - } - - componentDidUpdate(prevProps: Readonly): void { - if (prevProps.cluster.id !== this.props.cluster.id) { - this.isReconnecting = false; - this.authOutput = []; - } - } - - reconnect = async () => { - this.authOutput = []; - this.isReconnecting = true; + const reconnect = async () => { + runInAction(() => { + state.resetAuthOutput(); + state.setAsReconnecting(); + }); try { - await requestClusterActivation(this.cluster.id, true); + await requestClusterActivation(cluster.id, true); } catch (error) { - this.authOutput.push({ + state.appendAuthUpdate({ message: String(error), isError: true, }); } finally { - this.isReconnecting = false; + state.clearReconnectingState(); } }; - manageProxySettings = () => { - this.props.navigateToEntitySettings(this.cluster.id, "proxy"); - }; + const manageProxySettings = () => navigateToEntitySettings(cluster.id, "proxy"); - renderAuthenticationOutput() { + const renderAuthenticationOutput = () => { return (
         {
-          this.authOutput.map(({ message, isError }, index) => (
-            

- {message.trim()} -

- )) + state.authOutput + .get() + .map(({ message, isError }, index) => ( +

+ {message.trim()} +

+ )) }
); - } + }; - renderStatusIcon() { - if (this.hasErrors) { + const renderStatusIcon = () => { + if (state.hasErrorOutput.get()) { return ; } @@ -122,28 +91,28 @@ class NonInjectedClusterStatus extends React.Component
           

- {this.isReconnecting ? "Reconnecting" : "Connecting"} + {state.isReconnecting.get() ? "Reconnecting" : "Connecting"} …

); - } + }; - renderReconnectionHelp() { - if (this.hasErrors && !this.isReconnecting) { + const renderReconnectionHelp = () => { + if (state.hasErrorOutput.get() && !state.isReconnecting.get()) { return ( <>