From 4e88715b8d1cb2c0476cdd8aa984c2d989724a47 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 13 Jul 2020 19:32:55 +0300 Subject: [PATCH] PoC: first run Signed-off-by: Roman --- src/common/base-store.ts | 6 +- src/common/cluster-store.ts | 2 +- src/main/cluster-manager.ts | 5 +- src/main/cluster.ts | 9 +- src/main/index.ts | 8 +- src/main/window-manager.ts | 104 +++++++++++------- .../+cluster-settings/cluster-icon.scss | 6 + .../+cluster-settings/cluster-icon.tsx | 30 +++-- .../cluster-manager/clusters-menu.scss | 8 +- .../cluster-manager/clusters-menu.tsx | 7 +- src/renderer/components/menu/menu.tsx | 10 +- src/renderer/template.html | 1 - 12 files changed, 116 insertions(+), 80 deletions(-) diff --git a/src/common/base-store.ts b/src/common/base-store.ts index b7254a559f..d9f7d44da7 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -70,7 +70,7 @@ export class BaseStore extends Singleton { ...confOptions, }); const jsonModel = this.storeConfig.store; - logger.info(`[STORE]: loaded from ${this.storeConfig.path}`); + logger.info(`๐Ÿ’ฟ Store loaded from ${this.storeConfig.path}`); this.fromStore(jsonModel); this.isLoaded = true; } @@ -92,14 +92,14 @@ export class BaseStore extends Singleton { protected onConfigChange(data: T, oldValue: Partial) { if (!isEqual(this.toJSON(), data)) { - logger.debug(`[STORE]: received update from ${this.name}`, { data, oldValue }); + logger.debug(`๐Ÿ’ฟ Store received update from ${this.name}`, { data, oldValue }); this.fromStore(data); } } protected onModelChange(model: T) { if (!isEqual(this.storeModel, model)) { - logger.debug(`[STORE]: saving ${this.name} from runtime`, { + logger.debug(`๐Ÿ’ฟ Store ${this.name} is saving updates from app runtime`, { data: model, oldValue: this.storeModel }); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 0a416bda4a..0935ddc5b9 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -53,7 +53,7 @@ export class ClusterStore extends BaseStore { @observable clusters = observable.map(); @computed get activeCluster(): Cluster { - return this.clusters.get(this.activeClusterId); + return this.getById(this.activeClusterId); } @computed get clustersList(): Cluster[] { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 75e74ce454..4f12b3d4d0 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -41,9 +41,8 @@ export class ClusterManager { }); // auto-refresh status for active cluster autorun(() => { - const { activeCluster } = clusterStore; - if (activeCluster && activeCluster.initialized) { - activeCluster.refreshStatus(); + if (clusterStore.activeCluster) { + clusterStore.activeCluster.refreshStatus(); } }); // listen ipc-events diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 24623706b6..26868bbe0d 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,6 +1,6 @@ import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { FeatureStatusMap } from "./feature" -import { action, observable, toJS } from "mobx"; +import { action, observable, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; import { ContextHandler } from "./context-handler" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" @@ -23,6 +23,8 @@ export class Cluster implements ClusterModel { public contextHandler: ContextHandler; protected kubeconfigManager: KubeconfigManager; + public whenReady = when(() => this.initialized); + @observable initialized = false; @observable contextName: string; @observable workspace: string; @@ -63,14 +65,14 @@ export class Cluster implements ClusterModel { this.webContentUrl = `http://${this.id}.localhost:${port}`; this.kubeconfigManager = new KubeconfigManager(this); this.initialized = true; - logger.info(`[โœ”] Cluster(${this.id}) init success`, { + logger.info(`โœ… ๏ธCluster(${this.id}) init success`, { serverUrl: this.apiUrl, webContentUrl: this.webContentUrl, kubeProxyUrl: this.kubeProxyUrl, kubeAuthProxyUrl: this.kubeAuthProxyUrl, }); } catch (err) { - logger.error(`[X] Cluster(${this.id}) init failed: ${err}`); + logger.error(`๐Ÿ’ฃ Cluster(${this.id}) init failed: ${err}`); } } @@ -82,6 +84,7 @@ export class Cluster implements ClusterModel { @action async refreshStatus() { + await this.whenReady; const connectionStatus = await this.getConnectionStatus(); this.online = connectionStatus > ClusterStatus.Offline; this.accessible = connectionStatus == ClusterStatus.AccessGranted; diff --git a/src/main/index.ts b/src/main/index.ts index c78b450d48..752f6a3510 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -35,7 +35,7 @@ async function main() { const workingDir = path.join(app.getPath("appData"), appName); app.setName(appName); app.setPath("userData", workingDir); - logger.info(`Start app from "${workingDir}"`) + logger.info(`๐Ÿš€ Starting Lens from "${workingDir}"`) tracker.event("app", "start"); const updater = new AppUpdater() @@ -75,12 +75,13 @@ async function main() { // create window manager and open app windowManager = new WindowManager(); - // windowManager.showSplash(); + windowManager.showSplash(); } // Events app.on("ready", main); +// fixme: never happens, Cmd+W doesn't work app.on('window-all-closed', function () { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q @@ -98,9 +99,10 @@ app.on("activate", () => { logger.debug("app:activate"); }) + app.on("will-quit", async (event) => { event.preventDefault(); // To allow mixpanel sending to be executed if (clusterManager) clusterManager.stop() if (proxyServer) proxyServer.close() - app.exit(0); + app.exit(); }) diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index ef4e3e926a..4030ce149c 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -3,35 +3,52 @@ import { BrowserWindow, shell } from "electron" import windowStateKeeper from "electron-window-state" import type { ClusterId } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store"; +import logger from "./logger"; export class WindowManager { protected activeView: BrowserWindow; protected views = new Map(); + protected disposers: CallableFunction[] = []; + protected splashWindow: BrowserWindow; + protected windowState: windowStateKeeper.State; - protected disposers = [ - // auto-destroy views for removed clusters - reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { - removedClusters.forEach(cluster => { - this.destroyView(cluster.id); - }); - }) - ]; + constructor() { + this.splashWindow = new BrowserWindow({ + width: 500, + height: 300, + backgroundColor: "#1e2124", + center: true, + frame: false, + resizable: false, + show: false, + }); - protected splashWindow = new BrowserWindow({ - width: 500, - height: 300, - backgroundColor: "#1e2124", - center: true, - frame: false, - resizable: false, - show: false, - }); + // Manage main window size and position with state persistence + this.windowState = windowStateKeeper({ + defaultHeight: 900, + defaultWidth: 1440, + }); - // Manage main window size and position with state persistence - protected windowState = windowStateKeeper({ - defaultHeight: 900, - defaultWidth: 1440, - }); + // init events and show active cluster view + this.bindEvents(); + } + + protected bindEvents() { + this.disposers.push( + // auto-destroy views for removed clusters + reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { + removedClusters.forEach(cluster => { + this.destroyView(cluster.id); + }); + }), + // auto-show active cluster view + reaction(() => clusterStore.activeClusterId, clusterId => { + this.activateView(clusterId); + }, { + fireImmediately: true, + }) + ) + } async showSplash() { await this.splashWindow.loadURL("static://splash.html") @@ -49,36 +66,45 @@ export class WindowManager { async activateView(clusterId: ClusterId) { const cluster = clusterStore.getById(clusterId); if (!cluster) { - throw new Error(`Can't load lens for non-existing cluster="${clusterId}"`); + logger.error(`Can't show a view for non-existing cluster(${clusterId})`); + return; } - const activeView = this.activeView; - const isFresh = !this.getView(clusterId); - const view = this.initView(clusterId); - if (view !== activeView) { - if (isFresh) { - await view.loadURL(cluster.webContentUrl); + try { + const activeView = this.activeView; + const isFresh = !this.getView(clusterId); + const view = this.initView(clusterId); + if (view !== activeView) { + if (isFresh) { + await cluster.whenReady; + await view.loadURL(cluster.webContentUrl); + } + if (activeView) { + view.setBounds(activeView.getBounds()); // refresh position and swap windows + activeView.hide(); + } + view.show(); + this.hideSplash(); + this.activeView = view; } - if (activeView) { - view.setBounds(activeView.getBounds()); // refresh position for "invisible swap" - activeView.hide(); - } - view.show(); - this.activeView = view; + } catch (err) { + logger.error(`Activating cluster(${clusterId}) view has failed: ${err.stack}`); } } protected initView(clusterId: ClusterId) { let view = this.getView(clusterId); if (!view) { + const { width, height, x, y } = this.windowState; view = new BrowserWindow({ show: false, - x: this.windowState.x, - y: this.windowState.y, - width: this.windowState.width, - height: this.windowState.height, + x: x, y: y, + width: width, + height: height, titleBarStyle: "hidden", + backgroundColor: "#1e2124", webPreferences: { nodeIntegration: true, + enableRemoteModule: true, }, }); // open external links in default browser (target=_blank, window.open) diff --git a/src/renderer/components/+cluster-settings/cluster-icon.scss b/src/renderer/components/+cluster-settings/cluster-icon.scss index a047a85336..9c95fd8c2d 100644 --- a/src/renderer/components/+cluster-settings/cluster-icon.scss +++ b/src/renderer/components/+cluster-settings/cluster-icon.scss @@ -1,5 +1,11 @@ .ClusterIcon { position: relative; + --size: 40px; + + > img { + width: var(--size); + height: var(--size); + } .Badge { position: absolute; diff --git a/src/renderer/components/+cluster-settings/cluster-icon.tsx b/src/renderer/components/+cluster-settings/cluster-icon.tsx index 7c686e817e..68feba6444 100644 --- a/src/renderer/components/+cluster-settings/cluster-icon.tsx +++ b/src/renderer/components/+cluster-settings/cluster-icon.tsx @@ -7,15 +7,16 @@ import { Cluster } from "../../../main/cluster"; import { cssNames, IClassName } from "../../utils"; import { Badge } from "../badge"; -interface Props extends DOMAttributes, Omit { - className?: IClassName; - showBadge?: boolean; +interface Props extends DOMAttributes { cluster: Cluster; + className?: IClassName; + errorClass?: IClassName; + showErrorCount?: boolean; + options?: HashiconProps["options"] } const defaultProps: Partial = { - size: 38, - showBadge: true, + showErrorCount: true, }; @observer @@ -23,23 +24,20 @@ export class ClusterIcon extends React.Component { static defaultProps = defaultProps as object; render() { - const { className, cluster, showBadge, options, size, ...elemProps } = this.props; + const { className, cluster, showErrorCount, errorClass, options, children, ...elemProps } = this.props; const { isAdmin, eventCount, preferences } = cluster; const { clusterName, icon } = preferences; - const eventsCount = eventCount >= 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount; return (
- {icon && {clusterName}/} - {!icon && ( - } + {!icon && } + {showErrorCount && isAdmin && eventCount > 0 && ( + = 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount} /> )} - {showBadge && isAdmin && eventsCount && ( - - )} + {children}
); } diff --git a/src/renderer/components/cluster-manager/clusters-menu.scss b/src/renderer/components/cluster-manager/clusters-menu.scss index 2a2bd6212c..022dd766c6 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.scss +++ b/src/renderer/components/cluster-manager/clusters-menu.scss @@ -5,6 +5,10 @@ padding: $padding * 1.5; background: $menuBgc; + > * { + cursor: pointer; + } + .add-cluster { position: relative; @@ -27,8 +31,7 @@ } } - // todo: reuse in cluster-icon.tsx - .Badge.new-contexts { + .Badge.counter { $boxSize: 17px; $offset: -7px; @@ -45,6 +48,7 @@ font-weight: normal; border-radius: $radius; padding: 0; + pointer-events: none; } } } \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index e26d234f01..3a791d43ce 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -64,15 +64,16 @@ export class ClustersMenu extends React.Component { this.selectCluster(cluster)} onContextMenu={() => this.showContextMenu(cluster)} /> ) })} -
+

This is the quick launch menu.

@@ -85,7 +86,7 @@ export class ClustersMenu extends React.Component { )} /> {newContexts.length > 0 && ( - + )}
diff --git a/src/renderer/components/menu/menu.tsx b/src/renderer/components/menu/menu.tsx index b6453b2e89..a84259d75d 100644 --- a/src/renderer/components/menu/menu.tsx +++ b/src/renderer/components/menu/menu.tsx @@ -65,8 +65,7 @@ export class Menu extends React.Component { const parent = this.elem.parentElement; const position = window.getComputedStyle(parent).position; if (position === 'static') parent.style.position = 'relative'; - } - else if (this.isOpen) { + } else if (this.isOpen) { this.refreshPosition(); } this.opener = document.getElementById(this.props.htmlFor); // might not exist in sub-menus @@ -109,8 +108,7 @@ export class Menu extends React.Component { let nextItem = reverse ? items[activeIndex - 1] : items[activeIndex + 1]; if (!nextItem) nextItem = items[activeIndex]; nextItem.elem.focus(); - } - else { + } else { items[0].elem.focus(); } } @@ -224,7 +222,7 @@ export class Menu extends React.Component { } render() { - const { position } = this.props; + const { position, id } = this.props; let { className, usePortal } = this.props; className = cssNames('Menu', className, this.state.position || position, { portal: usePortal, @@ -246,7 +244,7 @@ export class Menu extends React.Component { const menu = ( -
    +
      {menuItems}
    diff --git a/src/renderer/template.html b/src/renderer/template.html index d9fc793a7d..82e137d046 100755 --- a/src/renderer/template.html +++ b/src/renderer/template.html @@ -3,7 +3,6 @@ Lens - The Kubernetes IDE -