diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index f1bf896d15..b36dafe0d5 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -40,6 +40,7 @@ export interface ClusterPreferences { export class ClusterStore extends BaseStore { @observable activeCluster: ClusterId; @observable clusters = observable.map(); + @observable removedClusters = observable.map(); private constructor() { super({ @@ -86,12 +87,32 @@ export class ClusterStore extends BaseStore { @action protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { - const clustersMap = new Map(); + const currentClusters = this.clusters.toJS(); + const newClusters = new Map(); + const removedClusters = new Map(); + + // update new clusters clusters.forEach(clusterModel => { - clustersMap.set(clusterModel.id, new Cluster(clusterModel)); + let cluster = currentClusters.get(clusterModel.id); + if (cluster) { + Object.assign(cluster, clusterModel); + cluster.mergeModel(clusterModel); + } else { + cluster = new Cluster(clusterModel); + } + newClusters.set(clusterModel.id, cluster); }); - this.activeCluster = clustersMap.has(activeCluster) ? activeCluster : null; - this.clusters.replace(clustersMap); + + // update removed clusters + currentClusters.forEach(cluster => { + if (!newClusters.has(cluster.id)) { + removedClusters.set(cluster.id, cluster); + } + }); + + this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null; + this.clusters.replace(newClusters); + this.removedClusters.replace(removedClusters); } toJSON(): ClusterStoreModel { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 93bb74db19..9871201a58 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,18 +1,18 @@ -import { autorun } from "mobx"; -import { apiPrefix, appProto } from "../common/vars"; import { app } from "electron" +import { reaction } from "mobx"; import path from "path" import http from "http" import { copyFile, ensureDir } from "fs-extra" import filenamify from "filenamify" -import { validateConfig } from "./k8s"; -import { Cluster } from "./cluster" +import { apiPrefix, appProto } from "../common/vars"; import { ClusterId, ClusterModel, clusterStore } from "../common/cluster-store" -import logger from "./logger" import { onMessages } from "../common/ipc-helpers"; import { ClusterIpcMessage } from "../common/ipc-messages"; -import { FeatureInstallRequest } from "./feature"; import { tracker } from "../common/tracker"; +import { validateConfig } from "./k8s"; +import { Cluster } from "./cluster" +import { FeatureInstallRequest } from "./feature"; +import logger from "./logger" export interface ClusterIconUpload { clusterId: string; @@ -26,16 +26,19 @@ export class ClusterManager { } constructor(protected port: number) { - autorun(() => { - // fixme: detect and stop removed clusters from config file ? - clusterStore.clusters.forEach((cluster: Cluster) => { + reaction(() => clusterStore.clusters.toJS(), clusters => { + clusters.forEach(cluster => { if (!cluster.initialized) { - cluster.init(this.port); - cluster.refreshCluster(); + cluster.init(this.port).then(() => cluster.refreshCluster()); } }) }); - + reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { + if (removedClusters.size > 0) { + removedClusters.forEach(cluster => cluster.stopServer()); + clusterStore.removedClusters.clear(); + } + }); ClusterManager.ipcListen(this); } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index fa76c64ab9..3adca3a36f 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -56,6 +56,10 @@ export class Cluster implements ClusterModel { @observable features: FeatureStatusMap = {}; constructor(model: ClusterModel) { + this.mergeModel(model); + } + + mergeModel(model: ClusterModel) { Object.assign(this, model) } diff --git a/src/main/index.ts b/src/main/index.ts index 4ccde7ee32..39da0e194c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -25,12 +25,6 @@ let windowManager: WindowManager = null; let clusterManager: ClusterManager = null; let proxyServer: proxy.LensProxy = null; -const vmURL = formatUrl({ - pathname: path.join(__dirname, `${appName}.html`), - protocol: "file", - slashes: true, -}) - mangleProxyEnv() if (app.commandLine.getSwitchValue("proxy-server") !== "") { process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") @@ -82,8 +76,13 @@ async function main() { } // manage lens windows - windowManager = new WindowManager({showSplash: true}); - windowManager.showMain(vmURL) + const vmURL = formatUrl({ + pathname: path.join(__dirname, `${appName}.html`), + protocol: "file", + slashes: true, + }) + windowManager = new WindowManager(); + windowManager.loadURL(vmURL) } app.on("ready", main) @@ -96,15 +95,8 @@ app.on('window-all-closed', function () { windowManager = null if (clusterManager) clusterManager.stop() } -}) -// app.on("activate", () => { -// if (!windowManager) { -// logger.debug("activate main window") -// windowManager = new WindowManager({ showSplash: false }) -// windowManager.showMain(vmURL) -// } -// }) -app.on("will-quit", async (event) => { +}); +app.on("will-quit", async event => { event.preventDefault(); // To allow mixpanel sending to be executed if (clusterManager) clusterManager.stop() if (proxyServer) proxyServer.close() diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 996b3fbb8c..e6f4ab3c3c 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,36 +1,30 @@ -import { BrowserWindow, shell } from "electron" +import { BrowserView, BrowserWindow, shell } from "electron" +import { reaction } from "mobx"; import windowStateKeeper from "electron-window-state" +import type { ClusterId } from "../common/cluster-store"; +import { clusterStore } from "../common/cluster-store"; import { tracker } from "../common/tracker"; -export class WindowManager { - public mainWindow: BrowserWindow = null; - public splashWindow: BrowserWindow = null; - protected windowState: windowStateKeeper.State; +export interface WindowManagerParams { + showSplash?: boolean; +} - constructor({ showSplash = true } = {}) { - // Manage main window size&position with persistence +export class WindowManager { + protected mainWindow: BrowserWindow; + protected splashWindow?: BrowserWindow; + protected windowState: windowStateKeeper.State; + protected views = new Map(); + protected disposers: Function[] = []; + + constructor(protected params: WindowManagerParams = {}) { + this.params = { showSplash: true, ...params }; + + // Manage main window size and position with state persistence this.windowState = windowStateKeeper({ defaultHeight: 900, defaultWidth: 1440, }); - this.splashWindow = new BrowserWindow({ - width: 500, - height: 300, - backgroundColor: "#1e2124", - center: true, - frame: false, - resizable: false, - show: false, - webPreferences: { - nodeIntegration: true - } - }) - if (showSplash) { - this.splashWindow.loadURL("static://splash.html") - this.splashWindow.show() - } - this.mainWindow = new BrowserWindow({ show: false, x: this.windowState.x, @@ -41,45 +35,92 @@ export class WindowManager { titleBarStyle: "hidden", webPreferences: { nodeIntegration: true, - webviewTag: true }, }); + // Splash-screen window with loading indicator + this.splashWindow = new BrowserWindow({ + width: 500, + height: 300, + backgroundColor: "#1e2124", + center: true, + frame: false, + resizable: false, + show: false, + }); + this.splashWindow.loadURL("static://splash.html") + // Hook window state manager into window lifecycle this.windowState.manage(this.mainWindow); - // handle close event - this.mainWindow.on("close", () => { - this.mainWindow = null; + // Disallow closing main window + this.mainWindow.on("close", (evt) => { + evt.preventDefault(); }); - // open external links in default browser (target=_blank, window.open) + // Open external links in default browser (target=_blank, window.open) this.mainWindow.webContents.on("new-window", (event, url) => { event.preventDefault(); shell.openExternal(url); }); - // handle external links - this.mainWindow.webContents.on("will-navigate", (event, link) => { - if (link.startsWith("http://localhost")) { - return; - } - event.preventDefault(); - shell.openExternal(link); - }) - + // Track main window focus this.mainWindow.on("focus", () => { tracker.event("app", "focus") - }) + }); + + // Clean up views for removed clusters + this.disposers.push( + reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { + removedClusters.forEach(cluster => { + const lensView = this.getView(cluster.id); + if (lensView) { + lensView.destroy(); + this.views.delete(cluster.id); + } + }); + }) + ); } - public showMain(url: string) { - this.mainWindow.loadURL(url).then(() => { - this.splashWindow.hide() - this.splashWindow.loadURL("data:text/html;charset=utf-8,").then(() => { - this.splashWindow.close() - this.mainWindow.show() + setView(clusterId: ClusterId) { + const view = this.getView(clusterId) + this.mainWindow.setBrowserView(view); + } + + getView(clusterId: ClusterId): BrowserView { + let view = this.views.get(clusterId); + if (!view) { + view = new BrowserView({ + webPreferences: { + nodeIntegration: true + } }) - }) + // view.setBackgroundColor("#878686"); + // view.setAutoResize({ horizontal: true, vertical: true }); + // view.webContents.loadURL("data:text/html;charset=utf-8,TEST") + this.views.set(clusterId, view); + } + return view; + } + + async loadURL(url: string) { + if (this.params.showSplash) { + this.splashWindow.show(); + } + await this.mainWindow.loadURL(url); + this.mainWindow.show(); + this.splashWindow.hide(); + } + + destroy() { + this.disposers.forEach(dispose => dispose()); + this.disposers.length = 0; + this.views.forEach(view => view.destroy()); + this.views.clear(); + this.mainWindow.destroy(); + this.splashWindow.destroy(); + this.mainWindow = null; + this.splashWindow = null; } } diff --git a/src/renderer/components/+workspaces/workspaces.scss b/src/renderer/components/+workspaces/workspaces.scss index bf2910c3b5..1f67f4c7af 100644 --- a/src/renderer/components/+workspaces/workspaces.scss +++ b/src/renderer/components/+workspaces/workspaces.scss @@ -1,10 +1,9 @@ .Workspaces { - height: 100%; - display: grid; grid-template-areas: "draggable draggable" "menu lens-view" "bottom-bar bottom-bar"; grid-template-rows: 20px 1fr min-content; grid-template-columns: min-content 1fr; + height: 100%; > .draggable-top { @include set-draggable; diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx index efd8f478bc..5af336a872 100644 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ b/src/renderer/components/+workspaces/workspaces.tsx @@ -1,5 +1,6 @@ import "./workspaces.scss" import React from "react"; +import { observable } from "mobx"; import { observer } from "mobx-react"; import { Link } from "react-router-dom"; import { Trans } from "@lingui/macro"; @@ -8,7 +9,6 @@ import { Icon } from "../icon"; import { ClustersMenu } from "./clusters-menu"; import { Menu, MenuItem } from "../menu"; import { prevDefault } from "../../utils"; -import { observable } from "mobx"; // todo: support `workspaceId` in URL