diff --git a/src/common/base-store.ts b/src/common/base-store.ts index f476965736..351b5827c5 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -12,6 +12,7 @@ import isEqual from "lodash/isEqual"; export interface BaseStoreParams extends ConfOptions { autoLoad?: boolean; syncEnabled?: boolean; + syncDelayMs?: number; } export class BaseStore extends Singleton { @@ -27,6 +28,7 @@ export class BaseStore extends Singleton { this.params = { autoLoad: false, syncEnabled: true, + syncDelayMs: 100, ...params, } this.init(); @@ -73,7 +75,9 @@ export class BaseStore extends Singleton { enableSync() { this.syncDisposers.push( - reaction(() => this.toJSON(), model => this.onModelChange(model)), + reaction(() => this.toJSON(), model => this.onModelChange(model), { + delay: this.params.syncDelayMs, + }), ); if (ipcMain) { const callback = (event: IpcMainEvent, model: T) => { diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index a382006cdc..715431185f 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -1,5 +1,5 @@ import path from "path"; -import { app, ipcRenderer, remote, webFrame, webContents } from "electron"; +import { app, ipcRenderer, remote, webFrame } from "electron"; import { unlink } from "fs-extra"; import { action, computed, observable, toJS } from "mobx"; import { BaseStore } from "./base-store"; @@ -108,7 +108,7 @@ export class ClusterStore extends BaseStore { @action setActive(id: ClusterId) { - this.activeClusterId = id; + this.activeClusterId = this.clusters.has(id) ? id : null; } @action @@ -155,7 +155,7 @@ export class ClusterStore extends BaseStore { if (cluster) { this.clusters.delete(clusterId); if (this.activeClusterId === clusterId) { - this.activeClusterId = null; + this.setActive(null); } // remove only custom kubeconfigs (pasted as text) if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) { diff --git a/src/main/tray.ts b/src/main/tray.ts index a042e28eaf..1e635a2fdb 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,7 +1,7 @@ import path from "path" import sharp from "sharp"; import packageInfo from "../../package.json" -import { app, dialog, Menu, nativeImage, Tray } from "electron" +import { dialog, Menu, nativeImage, Tray } from "electron" import { isDevelopment, isMac } from "../common/vars"; import { autorun } from "mobx"; import { showAbout } from "./menu"; @@ -21,17 +21,39 @@ export const trayIcon = isDevelopment : path.resolve(__static, "logo.svg") // electron-builder's extraResources export function initTray(windowManager: WindowManager) { - return autorun(() => buildTrayMenu(windowManager), { - delay: 100 + return autorun(() => { + const menu = createTrayMenu(windowManager); + buildTray(menu); }) } -export async function buildTrayMenu(windowManager: WindowManager) { - // note: browserWindow not available within menuItem.click() as argument[1] when app is not focused / hidden - const trayMenu = Menu.buildFromTemplate([ +export async function buildTray(menu: Menu) { + logger.info("[TRAY]: build start"); + const iconSize = isMac ? 16 : 32; // todo: verify on windows/linux + const pngIcon = await sharp(trayIcon).png().toBuffer(); + const icon = nativeImage.createFromBuffer(pngIcon).resize({ + width: iconSize, + height: iconSize + }); + + if (tray) { + tray.destroy(); // remove old tray on update + } + + tray = new Tray(icon) + tray.setToolTip(packageInfo.description) + tray.setIgnoreDoubleClickEvents(true); + tray.setContextMenu(menu); + + return tray; +} + +export function createTrayMenu(windowManager: WindowManager): Menu { + return Menu.buildFromTemplate([ { label: "About Lens", click() { + // note: argument[1] (browserWindow) not available when app is not focused / hidden windowManager.bringToTop(); showAbout(windowManager.mainView); }, @@ -45,22 +67,25 @@ export async function buildTrayMenu(windowManager: WindowManager) { }, { label: "Clusters", - submenu: workspaceStore.workspacesList.map(workspace => { - const clusters = clusterStore.getByWorkspaceId(workspace.id); - return { - label: workspace.name, - toolTip: workspace.description, - submenu: clusters.map(({ id: clusterId, contextName: label }) => { - return { - label, - click() { - windowManager.bringToTop(); - windowManager.navigate(clusterViewURL({ params: { clusterId } })); + submenu: workspaceStore.workspacesList + .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces + .map(workspace => { + const clusters = clusterStore.getByWorkspaceId(workspace.id); + return { + label: workspace.name, + toolTip: workspace.description, + submenu: clusters.map(({ id: clusterId, preferences: { clusterName: label }, online }) => { + return { + label: `${label}${online ? " (online)" : ""}`, + toolTip: clusterId, + click() { + windowManager.bringToTop(); + windowManager.navigate(clusterViewURL({ params: { clusterId } })); + } } - } - }) - } - }), + }) + } + }), }, { label: "Check for updates", @@ -76,26 +101,4 @@ export async function buildTrayMenu(windowManager: WindowManager) { }, }, ]); - - // note: all "await"-s must be defined *AFTER* getting observables for proper mobx reactions - await app.whenReady(); - logger.info('[TRAY]: building tray icon and menu'); - - const iconSize = isMac ? 16 : 32; // todo: verify on windows/linux - const pngIcon = await sharp(trayIcon).png().toBuffer(); - const icon = nativeImage.createFromBuffer(pngIcon).resize({ - width: iconSize, - height: iconSize - }); - - if (tray) { - tray.destroy(); // remove old tray on update - } - - tray = new Tray(icon) - tray.setToolTip(packageInfo.description) - tray.setIgnoreDoubleClickEvents(true); - tray.setContextMenu(trayMenu); - - return tray; } diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index a0414541d8..2e483a718d 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -2,7 +2,7 @@ import "./add-cluster.scss" import os from "os"; import React, { Fragment } from "react"; import { observer } from "mobx-react"; -import { action, observable, runInAction } from "mobx"; +import { action, observable } from "mobx"; import { remote } from "electron"; import { KubeConfig } from "@kubernetes/client-node"; import { _i18n } from "../../i18n"; @@ -45,6 +45,7 @@ export class AddCluster extends React.Component { @observable dropAreaActive = false; componentDidMount() { + clusterStore.setActive(null); this.setKubeConfig(userStore.kubeConfigPath); } @@ -117,6 +118,7 @@ export class AddCluster extends React.Component { } } + @action addClusters = () => { try { if (!this.selectedContexts.length) { @@ -125,6 +127,7 @@ export class AddCluster extends React.Component { } this.error = "" this.isWaiting = true + const newClusters: ClusterModel[] = this.selectedContexts.map(context => { const clusterId = uuid(); const kubeConfig = this.kubeContexts.get(context); @@ -142,19 +145,17 @@ export class AddCluster extends React.Component { }, } }); - runInAction(() => { - clusterStore.addCluster(...newClusters); - if (newClusters.length === 1) { - const clusterId = newClusters[0].id; - clusterStore.setActive(clusterId); - navigate(clusterViewURL({ params: { clusterId } })); - } else { - Notifications.ok( - Successfully imported {newClusters.length} cluster(s) - ); - } - }) - this.refreshContexts(); + + clusterStore.addCluster(...newClusters); + + if (newClusters.length === 1) { + const clusterId = newClusters[0].id; + navigate(clusterViewURL({ params: { clusterId } })); + } else { + Notifications.ok( + Successfully imported {newClusters.length} cluster(s) + ); + } } catch (err) { this.error = String(err); Notifications.error(Error while adding cluster(s): {this.error}); @@ -206,7 +207,7 @@ export class AddCluster extends React.Component { Select kubeconfig file} - active={this.sourceTab == KubeConfigSourceTab.FILE} /> + active={this.sourceTab == KubeConfigSourceTab.FILE}/> Paste as text} @@ -320,8 +321,8 @@ export class AddCluster extends React.Component { return (
{context} - {isNew && } - {isSelected && } + {isNew && } + {isSelected && }
) }; diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index b284ed9c88..939cc61879 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -1,7 +1,9 @@ import "./cluster-settings.scss"; import React from "react"; -import { observer, disposeOnUnmount } from "mobx-react"; +import { reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; +import { RouteComponentProps } from "react-router"; import { Features } from "./features"; import { Removal } from "./removal"; import { Status } from "./status"; @@ -13,30 +15,35 @@ import { Icon } from "../icon"; import { navigate } from "../../navigation"; import { IClusterSettingsRouteParams } from "./cluster-settings.route"; import { clusterStore } from "../../../common/cluster-store"; -import { RouteComponentProps } from "react-router"; import { clusterIpc } from "../../../common/cluster-ipc"; -import { autorun } from "mobx"; interface Props extends RouteComponentProps { } @observer export class ClusterSettings extends React.Component { + get clusterId() { + return this.props.match.params.clusterId + } + get cluster(): Cluster { - return clusterStore.getById(this.props.match.params.clusterId); + return clusterStore.getById(this.clusterId); } async componentDidMount() { - window.addEventListener('keydown', this.onEscapeKey); - disposeOnUnmount(this, - autorun(() => { - this.refreshCluster(); + window.addEventListener("keydown", this.onEscapeKey); + disposeOnUnmount(this, [ + reaction(() => this.cluster, this.refreshCluster, { + fireImmediately: true, + }), + reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { + fireImmediately: true, }) - ) + ]) } componentWillUnmount() { - window.removeEventListener('keydown', this.onEscapeKey); + window.removeEventListener("keydown", this.onEscapeKey); } onEscapeKey = (evt: KeyboardEvent) => { @@ -46,10 +53,9 @@ export class ClusterSettings extends React.Component { } } - refreshCluster = () => { - if(this.cluster) { - clusterIpc.refresh.invokeFromRenderer(this.cluster.id); - } + refreshCluster = (cluster: Cluster) => { + if (!cluster) return; + clusterIpc.refresh.invokeFromRenderer(cluster.id); } close() { diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index f537352c22..51610c9e26 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -1,14 +1,37 @@ import "./cluster-view.scss" import React from "react"; -import { observer } from "mobx-react"; -import { getMatchedCluster } from "./cluster-view.route"; +import { reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; +import { IClusterViewRouteParams } from "./cluster-view.route"; import { ClusterStatus } from "./cluster-status"; import { hasLoadedView } from "./lens-views"; +import { Cluster } from "../../../main/cluster"; +import { clusterStore } from "../../../common/cluster-store"; +import { RouteComponentProps } from "react-router"; + +interface Props extends RouteComponentProps { +} @observer -export class ClusterView extends React.Component { +export class ClusterView extends React.Component { + get clusterId() { + return this.props.match.params.clusterId; + } + + get cluster(): Cluster { + return clusterStore.getById(this.clusterId); + } + + async componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { + fireImmediately: true, + }) + ]) + } + render() { - const cluster = getMatchedCluster(); + const { cluster } = this; const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id)) return (
diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 323c28dd18..11a7bc40f6 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -1,6 +1,9 @@ import "./clusters-menu.scss" + +import type { Cluster } from "../../../main/cluster"; import { remote } from "electron" import React from "react"; +import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { observer } from "mobx-react"; import { _i18n } from "../../i18n"; import { t, Trans } from "@lingui/macro"; @@ -9,7 +12,7 @@ import { ClusterId, clusterStore } from "../../../common/cluster-store"; import { workspaceStore } from "../../../common/workspace-store"; import { ClusterIcon } from "../cluster-icon"; import { Icon } from "../icon"; -import { cssNames, IClassName, autobind } from "../../utils"; +import { autobind, cssNames, IClassName } from "../../utils"; import { Badge } from "../badge"; import { navigate } from "../../navigation"; import { addClusterURL } from "../+add-cluster"; @@ -19,8 +22,6 @@ import { Tooltip } from "../tooltip"; import { ConfirmDialog } from "../confirm-dialog"; import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterViewURL } from "./cluster-view.route"; -import { DragDropContext, Droppable, Draggable, DropResult, DroppableProvided, DraggableProvided } from "react-beautiful-dnd"; -import type { Cluster } from "../../../main/cluster"; interface Props { className?: IClassName; @@ -29,13 +30,11 @@ interface Props { @observer export class ClustersMenu extends React.Component { showCluster = (clusterId: ClusterId) => { - clusterStore.setActive(clusterId); navigate(clusterViewURL({ params: { clusterId } })); } addCluster = () => { navigate(addClusterURL()); - clusterStore.setActive(null); } showContextMenu = (cluster: Cluster) => { @@ -45,7 +44,6 @@ export class ClustersMenu extends React.Component { menu.append(new MenuItem({ label: _i18n._(t`Settings`), click: () => { - clusterStore.setActive(cluster.id); navigate(clusterSettingsURL({ params: { clusterId: cluster.id @@ -110,35 +108,29 @@ export class ClustersMenu extends React.Component {
- {(provided: DroppableProvided) => ( -
+ {({ innerRef, droppableProps, placeholder }: DroppableProvided) => ( +
{clusters.map((cluster, index) => { - const isActive = cluster.id === clusterStore.activeClusterId; - return ( - - {(provided: DraggableProvided) => ( -
- this.showCluster(cluster.id)} - onContextMenu={() => this.showContextMenu(cluster)} - /> -
- )} -
- )} + const isActive = cluster.id === clusterStore.activeClusterId; + return ( + + {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( +
+ this.showCluster(cluster.id)} + onContextMenu={() => this.showContextMenu(cluster)} + /> +
+ )} +
+ ) + } )} - {provided.placeholder} + {placeholder}
)} @@ -148,9 +140,9 @@ export class ClustersMenu extends React.Component { Add Cluster - + {newContexts.size > 0 && ( - new} /> + new}/> )}