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:
parent
c8fbb35967
commit
de4c7e4cff
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 => {
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -19,82 +19,117 @@
|
|||||||
* 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) {
|
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
|
||||||
|
|
||||||
if (!cluster || lensViews.has(clusterId)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`);
|
public hasLoadedView(clusterId: string): boolean {
|
||||||
const parentElem = document.getElementById("lens-views");
|
return Boolean(this.views.get(clusterId)?.isLoaded);
|
||||||
const iframe = document.createElement("iframe");
|
|
||||||
|
|
||||||
iframe.id = `cluster-frame-${cluster.id}`;
|
|
||||||
iframe.name = cluster.contextName;
|
|
||||||
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
|
|
||||||
iframe.addEventListener("load", () => {
|
|
||||||
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`);
|
|
||||||
lensViews.get(clusterId).isLoaded = true;
|
|
||||||
}, { once: true });
|
|
||||||
lensViews.set(clusterId, { clusterId, view: iframe });
|
|
||||||
parentElem.appendChild(iframe);
|
|
||||||
|
|
||||||
logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await when(() => cluster.ready, { timeout: 5_000 }); // we cannot wait forever because cleanup would be blocked for broken cluster connections
|
|
||||||
logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`);
|
|
||||||
} finally {
|
|
||||||
await autoCleanOnRemove(clusterId, iframe);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
|
@action
|
||||||
await when(() => {
|
public initView(clusterId: ClusterId) {
|
||||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|
||||||
return !cluster || (cluster.disconnected && lensViews.get(clusterId)?.isLoaded);
|
if (!cluster || this.views.has(clusterId)) {
|
||||||
});
|
return;
|
||||||
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
}
|
||||||
lensViews.delete(clusterId);
|
|
||||||
|
|
||||||
iframe.parentNode.removeChild(iframe);
|
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`);
|
||||||
}
|
const parentElem = document.getElementById("lens-views");
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
export function refreshViews(visibleClusterId?: string) {
|
|
||||||
logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${visibleClusterId}`);
|
iframe.id = `cluster-frame-${cluster.id}`;
|
||||||
const cluster = ClusterStore.getInstance().getById(visibleClusterId);
|
iframe.name = cluster.contextName;
|
||||||
|
iframe.style.display = "none";
|
||||||
lensViews.forEach(({ clusterId, view, isLoaded }) => {
|
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
|
||||||
const isCurrent = clusterId === cluster?.id;
|
iframe.addEventListener("load", () => {
|
||||||
const isReady = cluster?.available && cluster?.ready;
|
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`);
|
||||||
const isVisible = isCurrent && isLoaded && isReady;
|
this.views.get(clusterId).isLoaded = true;
|
||||||
|
}, { once: true });
|
||||||
view.style.display = isVisible ? "flex" : "none";
|
this.views.set(clusterId, { frame: iframe, isLoaded: false });
|
||||||
|
parentElem.appendChild(iframe);
|
||||||
requestMain(clusterVisibilityHandler, clusterId, isVisible).catch(() => {
|
|
||||||
logger.error(`[LENS-VIEW]: failed to set cluster visibility, clusterId=${clusterId}`);
|
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}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
// cluster.disconnect is set to `false` when the cluster starts to connect
|
||||||
|
() => !cluster.disconnected,
|
||||||
|
() => {
|
||||||
|
when(
|
||||||
|
() => {
|
||||||
|
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||||
|
|
||||||
|
return !cluster || (cluster.disconnected && this.views.get(clusterId)?.isLoaded);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
||||||
|
this.views.delete(clusterId);
|
||||||
|
iframe.parentNode.removeChild(iframe);
|
||||||
|
dispose();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setVisibleCluster(clusterId: ClusterId) {
|
||||||
|
this.visibleCluster = clusterId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearVisibleCluster() {
|
||||||
|
this.visibleCluster = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private prevVisibleClusterChange?: IReactionDisposer;
|
||||||
|
|
||||||
|
private handleVisibleClusterChange = (clusterId: ClusterId | undefined) => {
|
||||||
|
logger.info(`[LENS-VIEW]: refreshing iframe views, visible cluster id=${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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user