1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix race condition in refreshViews (#4094)

This commit is contained in:
Sebastian Malton 2021-11-10 12:07:55 -05:00 committed by GitHub
parent c8fbb35967
commit de4c7e4cff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 135 additions and 88 deletions

View File

@ -20,7 +20,7 @@
*/ */
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx"; import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityKindData } from "../../common/catalog"; import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor, CatalogEntityKindData } from "../../common/catalog";
import { iter } from "../../common/utils"; import { iter } from "../../common/utils";
export class CatalogEntityRegistry { export class CatalogEntityRegistry {
@ -59,7 +59,7 @@ export class CatalogEntityRegistry {
return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[]; return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[];
} }
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData): T[] { getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData & CatalogEntityConstructor<T>): T[] {
return this.getItemsForApiKind(apiVersion, kind); return this.getItemsForApiKind(apiVersion, kind);
} }
} }

View File

@ -41,6 +41,8 @@ export class ClusterManager extends Singleton {
private store = ClusterStore.getInstance(); private store = ClusterStore.getInstance();
deleting = observable.set<ClusterId>(); deleting = observable.set<ClusterId>();
@observable visibleCluster: ClusterId | undefined = undefined;
constructor() { constructor() {
super(); super();
makeObservable(this); makeObservable(this);
@ -61,8 +63,22 @@ export class ClusterManager extends Singleton {
{ fireImmediately: false }, { fireImmediately: false },
); );
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => { reaction(
this.syncClustersFromCatalog(entities); () => catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster),
entities => this.syncClustersFromCatalog(entities),
);
reaction(() => [
catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster),
this.visibleCluster,
] as const, ([entities, visibleCluster]) => {
for (const entity of entities) {
if (entity.getId() === visibleCluster) {
entity.status.active = true;
} else {
entity.status.active = false;
}
}
}); });
observe(this.deleting, change => { observe(this.deleting, change => {

View File

@ -20,13 +20,12 @@
*/ */
import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron"; import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron";
import { KubernetesCluster } from "../../common/catalog-entities";
import { clusterFrameMap } from "../../common/cluster-frames"; import { clusterFrameMap } from "../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../common/cluster-ipc"; import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../common/cluster-ipc";
import type { ClusterId } from "../../common/cluster-types"; import type { ClusterId } from "../../common/cluster-types";
import { ClusterStore } from "../../common/cluster-store"; import { ClusterStore } from "../../common/cluster-store";
import { appEventBus } from "../../common/event-bus"; import { appEventBus } from "../../common/event-bus";
import { dialogShowOpenDialogHandler, ipcMainHandle } from "../../common/ipc"; import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../common/ipc";
import { catalogEntityRegistry } from "../catalog"; import { catalogEntityRegistry } from "../catalog";
import { pushCatalogToRenderer } from "../catalog-pusher"; import { pushCatalogToRenderer } from "../catalog-pusher";
import { ClusterManager } from "../cluster-manager"; import { ClusterManager } from "../cluster-manager";
@ -54,16 +53,8 @@ export function initIpcMainHandlers() {
} }
}); });
ipcMainHandle(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => { ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => {
const entity = catalogEntityRegistry.getById(clusterId); ClusterManager.getInstance().visibleCluster = clusterId;
for (const kubeEntity of catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster)) {
kubeEntity.status.active = false;
}
if (entity) {
entity.status.active = visible;
}
}); });
ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => { ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => {

View File

@ -25,7 +25,7 @@ import { computed, makeObservable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { ClusterStatus } from "./cluster-status"; import { ClusterStatus } from "./cluster-status";
import { hasLoadedView, initView, refreshViews } from "./lens-views"; import { ClusterFrameHandler } from "./lens-views";
import type { Cluster } from "../../../main/cluster"; import type { Cluster } from "../../../main/cluster";
import { ClusterStore } from "../../../common/cluster-store"; import { ClusterStore } from "../../../common/cluster-store";
import { requestMain } from "../../../common/ipc"; import { requestMain } from "../../../common/ipc";
@ -57,7 +57,7 @@ export class ClusterView extends React.Component<Props> {
@computed get isReady(): boolean { @computed get isReady(): boolean {
const { cluster, clusterId } = this; const { cluster, clusterId } = this;
return cluster?.ready && cluster?.available && hasLoadedView(clusterId); return cluster?.ready && cluster?.available && ClusterFrameHandler.getInstance().hasLoadedView(clusterId);
} }
componentDidMount() { componentDidMount() {
@ -65,25 +65,23 @@ export class ClusterView extends React.Component<Props> {
} }
componentWillUnmount() { componentWillUnmount() {
refreshViews(); ClusterFrameHandler.getInstance().clearVisibleCluster();
catalogEntityRegistry.activeEntity = null; catalogEntityRegistry.activeEntity = null;
} }
bindEvents() { bindEvents() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => this.clusterId, async (clusterId) => { reaction(() => this.clusterId, async (clusterId) => {
refreshViews(clusterId); // refresh visibility of active cluster ClusterFrameHandler.getInstance().setVisibleCluster(clusterId);
initView(clusterId); // init cluster-view (iframe), requires parent container #lens-views to be in DOM ClusterFrameHandler.getInstance().initView(clusterId);
requestMain(clusterActivateHandler, clusterId, false); // activate and fetch cluster's state from main requestMain(clusterActivateHandler, clusterId, false); // activate and fetch cluster's state from main
catalogEntityRegistry.activeEntity = catalogEntityRegistry.getById(clusterId); catalogEntityRegistry.activeEntity = clusterId;
}, { }, {
fireImmediately: true, fireImmediately: true,
}), }),
reaction(() => [this.cluster?.ready, this.cluster?.disconnected], (values) => { reaction(() => [this.cluster?.ready, this.cluster?.disconnected], ([, disconnected]) => {
const disconnected = values[1]; if (ClusterFrameHandler.getInstance().hasLoadedView(this.clusterId) && disconnected) {
if (hasLoadedView(this.clusterId) && disconnected) {
navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
} }
}), }),

View File

@ -19,30 +19,38 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { observable, when } from "mobx"; import { action, IReactionDisposer, makeObservable, observable, reaction, when } from "mobx";
import logger from "../../../main/logger"; import logger from "../../../main/logger";
import { requestMain } from "../../../common/ipc";
import { clusterVisibilityHandler } from "../../../common/cluster-ipc"; import { clusterVisibilityHandler } from "../../../common/cluster-ipc";
import { ClusterStore } from "../../../common/cluster-store"; import { ClusterStore } from "../../../common/cluster-store";
import type { ClusterId } from "../../../common/cluster-types"; import type { ClusterId } from "../../../common/cluster-types";
import { getClusterFrameUrl } from "../../utils"; import { getClusterFrameUrl, Singleton } from "../../utils";
import { ipcRenderer } from "electron";
export interface LensView { export interface LensView {
isLoaded?: boolean isLoaded: boolean;
clusterId: ClusterId; frame: HTMLIFrameElement;
view: HTMLIFrameElement
} }
export const lensViews = observable.map<ClusterId, LensView>(); export class ClusterFrameHandler extends Singleton {
private views = observable.map<string, LensView>();
@observable private visibleCluster: string | null = null;
export function hasLoadedView(clusterId: ClusterId): boolean { constructor() {
return !!lensViews.get(clusterId)?.isLoaded; super();
makeObservable(this);
reaction(() => this.visibleCluster, this.handleVisibleClusterChange);
} }
export async function initView(clusterId: ClusterId) { public hasLoadedView(clusterId: string): boolean {
return Boolean(this.views.get(clusterId)?.isLoaded);
}
@action
public initView(clusterId: ClusterId) {
const cluster = ClusterStore.getInstance().getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (!cluster || lensViews.has(clusterId)) { if (!cluster || this.views.has(clusterId)) {
return; return;
} }
@ -52,49 +60,76 @@ export async function initView(clusterId: ClusterId) {
iframe.id = `cluster-frame-${cluster.id}`; iframe.id = `cluster-frame-${cluster.id}`;
iframe.name = cluster.contextName; iframe.name = cluster.contextName;
iframe.style.display = "none";
iframe.setAttribute("src", getClusterFrameUrl(clusterId)); iframe.setAttribute("src", getClusterFrameUrl(clusterId));
iframe.addEventListener("load", () => { iframe.addEventListener("load", () => {
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`); logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`);
lensViews.get(clusterId).isLoaded = true; this.views.get(clusterId).isLoaded = true;
}, { once: true }); }, { once: true });
lensViews.set(clusterId, { clusterId, view: iframe }); this.views.set(clusterId, { frame: iframe, isLoaded: false });
parentElem.appendChild(iframe); parentElem.appendChild(iframe);
logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`); logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
try { const dispose = when(
await when(() => cluster.ready, { timeout: 5_000 }); // we cannot wait forever because cleanup would be blocked for broken cluster connections () => cluster.ready,
logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`); () => logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`),
} finally { );
await autoCleanOnRemove(clusterId, iframe);
}
}
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) { when(
await when(() => { // cluster.disconnect is set to `false` when the cluster starts to connect
() => !cluster.disconnected,
() => {
when(
() => {
const cluster = ClusterStore.getInstance().getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
return !cluster || (cluster.disconnected && lensViews.get(clusterId)?.isLoaded); return !cluster || (cluster.disconnected && this.views.get(clusterId)?.isLoaded);
}); },
() => {
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`); logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
lensViews.delete(clusterId); this.views.delete(clusterId);
iframe.parentNode.removeChild(iframe); iframe.parentNode.removeChild(iframe);
dispose();
},
);
},
);
} }
export function refreshViews(visibleClusterId?: string) { public setVisibleCluster(clusterId: ClusterId) {
logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${visibleClusterId}`); this.visibleCluster = clusterId;
const cluster = ClusterStore.getInstance().getById(visibleClusterId); }
lensViews.forEach(({ clusterId, view, isLoaded }) => { public clearVisibleCluster() {
const isCurrent = clusterId === cluster?.id; this.visibleCluster = null;
const isReady = cluster?.available && cluster?.ready; }
const isVisible = isCurrent && isLoaded && isReady;
private prevVisibleClusterChange?: IReactionDisposer;
view.style.display = isVisible ? "flex" : "none";
private handleVisibleClusterChange = (clusterId: ClusterId | undefined) => {
requestMain(clusterVisibilityHandler, clusterId, isVisible).catch(() => { logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${clusterId}`);
logger.error(`[LENS-VIEW]: failed to set cluster visibility, clusterId=${clusterId}`);
}); ipcRenderer.send(clusterVisibilityHandler);
});
const cluster = ClusterStore.getInstance().getById(clusterId);
for (const { frame: view } of this.views.values()) {
view.style.display = "none";
}
if (cluster) {
const lensView = this.views.get(clusterId);
this.prevVisibleClusterChange?.();
this.prevVisibleClusterChange = when(
() => cluster.available && cluster.ready && lensView.isLoaded,
() => {
logger.info(`[LENS-VIEW]: cluster id=${clusterId} should now be visible`);
lensView.frame.style.display = "flex";
ipcRenderer.send(clusterVisibilityHandler, clusterId);
},
);
}
};
} }

View File

@ -38,6 +38,7 @@ import { IpcRendererNavigationEvents } from "./navigation/events";
import { catalogEntityRegistry } from "./api/catalog-entity-registry"; import { catalogEntityRegistry } from "./api/catalog-entity-registry";
import logger from "../common/logger"; import logger from "../common/logger";
import { unmountComponentAtNode } from "react-dom"; import { unmountComponentAtNode } from "react-dom";
import { ClusterFrameHandler } from "./components/cluster-manager/lens-views";
injectSystemCAs(); injectSystemCAs();
@ -61,6 +62,12 @@ export class LensApp extends React.Component {
}; };
} }
constructor(props: {}) {
super(props);
ClusterFrameHandler.createInstance();
}
componentDidMount() { componentDidMount() {
ipcRenderer.send(IpcRendererNavigationEvents.LOADED); ipcRenderer.send(IpcRendererNavigationEvents.LOADED);
} }