diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index e8f681cd7f..b8f9f5f84b 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -3,21 +3,14 @@ import { ClusterId, clusterStore } from "./cluster-store"; import { tracker } from "./tracker"; export const clusterIpc = { - initView: createIpcChannel({ - channel: "cluster:init", - handle: async (clusterId: ClusterId, frameId: number) => { - const cluster = clusterStore.getById(clusterId); - if (cluster) { - cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates - return cluster.pushState(); - } - }, - }), - activate: createIpcChannel({ channel: "cluster:activate", - handle: (clusterId: ClusterId) => { - return clusterStore.getById(clusterId)?.activate(); + handle: (clusterId: ClusterId, frameId?: number) => { + const cluster = clusterStore.getById(clusterId); + if (cluster) { + if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates + return cluster.activate(true); + } }, }), diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index fcee67cb8f..4fb4d15aea 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -63,7 +63,7 @@ export class ClusterStore extends BaseStore { static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string { const filePath = ClusterStore.getCustomKubeConfigPath(clusterId); const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig); - saveToAppFiles(filePath, fileContents, { mode: 0o600}); + saveToAppFiles(filePath, fileContents, { mode: 0o600 }); return filePath; } @@ -209,11 +209,17 @@ export class ClusterStore extends BaseStore { export const clusterStore = ClusterStore.getInstance(); -export function getHostedClusterId(): ClusterId { - const clusterHost = location.hostname.match(/^(.*?)\.localhost/); - if (clusterHost) { - return clusterHost[1] - } +export function getClusterIdFromHost(hostname: string): ClusterId { + const subDomains = hostname.split(":")[0].split("."); + return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345" +} + +export function getClusterFrameUrl(clusterId: ClusterId) { + return `//${clusterId}.${location.host}`; +} + +export function getHostedClusterId() { + return getClusterIdFromHost(location.hostname); } export function getHostedCluster(): Cluster { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index af1e0d5657..ff5eaae32d 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,7 +1,7 @@ import "../common/cluster-ipc"; import type http from "http" import { autorun } from "mobx"; -import { ClusterId, clusterStore } from "../common/cluster-store" +import { clusterStore, getClusterIdFromHost } from "../common/cluster-store" import { Cluster } from "./cluster" import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; @@ -38,26 +38,20 @@ export class ClusterManager { }) } - protected getCluster(id: ClusterId) { - return clusterStore.getById(id); - } - getClusterForRequest(req: http.IncomingMessage): Cluster { let cluster: Cluster = null // lens-server is connecting to 127.0.0.1:/ if (req.headers.host.startsWith("127.0.0.1")) { const clusterId = req.url.split("/")[1] - if (clusterId) { - cluster = this.getCluster(clusterId) - if (cluster) { - // we need to swap path prefix so that request is proxied to kube api - req.url = req.url.replace(`/${clusterId}`, apiKubePrefix) - } + const cluster = clusterStore.getById(clusterId) + if (cluster) { + // we need to swap path prefix so that request is proxied to kube api + req.url = req.url.replace(`/${clusterId}`, apiKubePrefix) } } else { - const id = req.headers.host.split(".")[0] - cluster = this.getCluster(id) + const clusterId = getClusterIdFromHost(req.headers.host); + cluster = clusterStore.getById(clusterId) } return cluster; diff --git a/src/main/cluster.ts b/src/main/cluster.ts index d160496cdf..80bd208a40 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -2,7 +2,7 @@ import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/clus import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; import type { FeatureStatusMap } from "./feature" -import { action, computed, intercept, observable, reaction, toJS, when } from "mobx"; +import { action, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; import { broadcastIpc } from "../common/ipc"; import { ContextHandler } from "./context-handler" @@ -126,13 +126,13 @@ export class Cluster implements ClusterModel { this.eventDisposers.length = 0; } - async activate() { + async activate(init = false) { logger.info(`[CLUSTER]: activate`, this.getMeta()); await this.whenInitialized; if (!this.eventDisposers.length) { this.bindEvents(); } - if (this.disconnected || !this.accessible) { + if (this.disconnected || (!init && !this.accessible)) { await this.reconnect(); } await this.refresh(); @@ -411,6 +411,7 @@ export class Cluster implements ClusterModel { id: this.id, name: this.contextName, initialized: this.initialized, + ready: this.ready, online: this.online, accessible: this.accessible, disconnected: this.disconnected, diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 6f7ab6478a..be5a95e47a 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,9 +1,9 @@ +import type { ClusterId } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store"; import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" import windowStateKeeper from "electron-window-state" import { observable } from "mobx"; import { initMenu } from "./menu"; -import type { ClusterId } from "../common/cluster-store"; export class WindowManager { protected mainView: BrowserWindow; @@ -42,7 +42,7 @@ export class WindowManager { }); // track visible cluster from ui - ipcMain.on("cluster-view:change", (event, clusterId: ClusterId) => { + ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { this.activeClusterId = clusterId; }); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 0fcb216cd9..80c27cb418 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -4,7 +4,7 @@ import { render } from "react-dom"; import { isMac } from "../common/vars"; import { userStore } from "../common/user-store"; import { workspaceStore } from "../common/workspace-store"; -import { clusterStore, getHostedClusterId } from "../common/cluster-store"; +import { clusterStore } from "../common/cluster-store"; import { i18nStore } from "./i18n"; import { themeStore } from "./theme.store"; import { App } from "./components/app"; @@ -35,4 +35,4 @@ export async function bootstrap(App: AppComponent) { } // run -bootstrap(getHostedClusterId() ? App : LensApp); +bootstrap(process.isMainFrame ? LensApp : App); diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 23e71e2be9..a60d794168 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -43,8 +43,8 @@ export class App extends React.Component { const clusterId = getHostedClusterId(); logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`) await Terminal.preloadFonts() - await clusterIpc.initView.invokeFromRenderer(clusterId, frameId); - await getHostedCluster().whenInitialized; + await clusterIpc.activate.invokeFromRenderer(clusterId, frameId); + await getHostedCluster().whenReady; // cluster.refresh() is done at this point } get startURL() { diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 6011549e8c..db0026bf75 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -1,7 +1,7 @@ import "./cluster-manager.scss" import React from "react"; import { Redirect, Route, Switch } from "react-router"; -import { reaction } from "mobx"; +import { comparer, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { ClustersMenu } from "./clusters-menu"; import { BottomBar } from "./bottom-bar"; @@ -23,11 +23,14 @@ export class ClusterManager extends React.Component { fireImmediately: true }), reaction(() => [ + getMatchedClusterId(), // refresh when active cluster-view changed hasLoadedView(getMatchedClusterId()), // refresh when cluster's webview loaded getMatchedCluster()?.available, // refresh on disconnect active-cluster + getMatchedCluster()?.ready, // refresh when cluster ready-state change ], refreshViews, { - fireImmediately: true - }) + fireImmediately: true, + equals: comparer.shallow, + }), ]) } diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 1f5d52f814..b0fae210dd 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -38,7 +38,7 @@ export class ClusterStatus extends React.Component { error: res.error, }); }) - if (!this.cluster.initialized || this.cluster.disconnected) { + if (this.cluster.disconnected) { await this.refreshCluster(); } } @@ -63,7 +63,7 @@ export class ClusterStatus extends React.Component { if (!hasErrors || this.isReconnecting) { return ( <> - +
             

{this.isReconnecting ? "Reconnecting..." : "Connecting..."}

{authOutput.map(({ data, error }, index) => { @@ -75,7 +75,7 @@ export class ClusterStatus extends React.Component { } return ( <> - +

{cluster.preferences.clusterName}

diff --git a/src/renderer/components/cluster-manager/cluster-view.route.ts b/src/renderer/components/cluster-manager/cluster-view.route.ts index d2e39cbb0c..e1fb0d1f54 100644 --- a/src/renderer/components/cluster-manager/cluster-view.route.ts +++ b/src/renderer/components/cluster-manager/cluster-view.route.ts @@ -2,7 +2,7 @@ import { reaction } from "mobx"; import { ipcRenderer } from "electron"; import { matchPath, RouteProps } from "react-router"; import { buildURL, navigation } from "../../navigation"; -import { clusterStore, getHostedClusterId } from "../../../common/cluster-store"; +import { clusterStore } from "../../../common/cluster-store"; import { clusterSettingsRoute } from "../+cluster-settings/cluster-settings.route"; export interface IClusterViewRouteParams { @@ -34,11 +34,10 @@ export function getMatchedCluster() { } if (ipcRenderer) { - // Refresh global menu depending on active route's type (common/cluster view) - const isMainView = !getHostedClusterId(); - if (isMainView) { + if (process.isMainFrame) { + // Keep track of active cluster-id for handling IPC/menus/etc. reaction(() => getMatchedClusterId(), clusterId => { - ipcRenderer.send("cluster-view:change", clusterId); + ipcRenderer.send("cluster-view:current-id", clusterId); }, { fireImmediately: true }) diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index 9ae08c5e4e..370ed665c3 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -1,5 +1,5 @@ import { observable, when } from "mobx"; -import { ClusterId, clusterStore } from "../../../common/cluster-store"; +import { ClusterId, clusterStore, getClusterFrameUrl } from "../../../common/cluster-store"; import { getMatchedCluster } from "./cluster-view.route" import logger from "../../../main/logger"; @@ -21,29 +21,38 @@ export async function initView(clusterId: ClusterId) { } logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`) const cluster = clusterStore.getById(clusterId); - await cluster.whenReady; const parentElem = document.getElementById("lens-views"); const iframe = document.createElement("iframe"); iframe.name = cluster.contextName; - iframe.setAttribute("src", `//${clusterId}.${location.host}`) - iframe.addEventListener("load", async () => { + 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); - // auto-clean when cluster removed + await autoCleanOnRemove(clusterId, iframe); +} + +export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) { await when(() => !clusterStore.getById(clusterId)); logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`) - parentElem.removeChild(iframe) lensViews.delete(clusterId) + // Keep frame in DOM to avoid possible bugs when same cluster re-created after being removed. + // In that case for some reasons `webFrame.routingId` returns some previous frameId (usage in app.tsx) + // Issue: https://github.com/lensapp/lens/issues/811 + iframe.dataset.meta = `${iframe.name} was removed at ${new Date().toLocaleString()}`; + iframe.removeAttribute("src") + iframe.removeAttribute("name") } export function refreshViews() { const cluster = getMatchedCluster(); lensViews.forEach(({ clusterId, view, isLoaded }) => { - const isVisible = cluster && cluster.available && cluster.id === clusterId; - view.style.display = isLoaded && isVisible ? "flex" : "none" + const isCurrent = clusterId === cluster?.id; + const isReady = cluster?.available && cluster?.ready; + const isVisible = isCurrent && isLoaded && isReady; + view.style.display = isVisible ? "flex" : "none" }) }