From fe06643a8ee08e904075f5e1d1b0d154e54dc0d5 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 24 Jul 2020 16:33:20 +0300 Subject: [PATCH] auto-refresh menu, no-clusters page + navigation fixes Signed-off-by: Roman --- src/common/cluster-store.ts | 4 - src/main/kube-auth-proxy.ts | 2 +- src/main/menu.ts | 56 +++++++------ src/main/window-manager.ts | 22 ++++-- src/renderer/api/kube-watch-api.ts | 4 +- src/renderer/api/rbac.ts | 4 +- .../+apps-releases/release.store.ts | 4 +- .../+cluster-settings/cluster-icon.scss | 1 + src/renderer/components/app.tsx | 79 +++++++++++-------- .../cluster-manager/cluster-status.tsx | 13 +-- .../cluster-manager/clusters-menu.tsx | 2 +- .../components/layout/main-layout.tsx | 4 +- src/renderer/kube-object.store.ts | 4 +- 13 files changed, 109 insertions(+), 90 deletions(-) diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 723cc5bfb9..ad6b48d5b3 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -77,10 +77,6 @@ export class ClusterStore extends BaseStore { @observable removedClusters = observable.map(); @observable clusters = observable.map(); - @computed get activeCluster(): Cluster | null { - return this.getById(this.activeClusterId); - } - @computed get clustersList(): Cluster[] { return Array.from(this.clusters.values()); } diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index e86bda5ffe..d756123870 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -6,7 +6,7 @@ import { bundledKubectl, Kubectl } from "./kubectl" import logger from "./logger" export interface KubeAuthProxyResponse { - data: string; // stream=stdout + data: string; error?: boolean; // stream=stderr } diff --git a/src/main/menu.ts b/src/main/menu.ts index 2e3f393fd9..dea1810326 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -1,5 +1,6 @@ import type { WindowManager } from "./window-manager"; import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" +import { autorun } from "mobx"; import { broadcastIpc } from "../common/ipc"; import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars"; import { clusterStore } from "../common/cluster-store"; @@ -7,20 +8,26 @@ import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.r import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route"; import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route"; +import logger from "./logger"; export function initMenu(windowManager: WindowManager) { - const menuItems: MenuItemConstructorOptions[] = []; + autorun(() => { + logger.debug(`[MENU]: refreshing menu, cluster=${clusterStore.activeClusterId}`); + buildMenu(windowManager); + }); +} + +function buildMenu(windowManager: WindowManager) { + const hasClusters = clusterStore.hasClusters(); + const activeClusterId = clusterStore.activeClusterId; + const clusterView = windowManager.getClusterView(activeClusterId); function navigate(url: string) { - const activeClusterId = clusterStore.activeClusterId; - const view = windowManager.getClusterView(activeClusterId); - if (view) { - broadcastIpc({ - channel: "menu:navigate", - webContentId: view.id, - args: [url], - }); - } + broadcastIpc({ + channel: "menu:navigate", + webContentId: clusterView ? clusterView.id : undefined /*no-clusters*/, + args: [url], + }); } function macOnly(menuItems: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] { @@ -28,8 +35,7 @@ export function initMenu(windowManager: WindowManager) { return menuItems; } - // "File" submenu - menuItems.push({ + const fileMenu: MenuItemConstructorOptions = { label: isMac ? app.getName() : "File", submenu: [ { @@ -38,12 +44,12 @@ export function initMenu(windowManager: WindowManager) { navigate(addClusterURL()) } }, - { + ...(hasClusters ? [{ label: 'Cluster Settings', click() { navigate(clusterSettingsURL()) } - }, + }] : []), { type: 'separator' }, { label: 'Preferences', @@ -62,10 +68,9 @@ export function initMenu(windowManager: WindowManager) { { type: 'separator' }, { role: 'quit' } ] - }); + }; - // "Edit" submenu - menuItems.push({ + const editMenu: MenuItemConstructorOptions = { label: 'Edit', submenu: [ { role: 'undo' }, @@ -78,10 +83,9 @@ export function initMenu(windowManager: WindowManager) { { type: 'separator' }, { role: 'selectAll' }, ] - }); + }; - // "View" submenu - menuItems.push({ + const viewMenu: MenuItemConstructorOptions = { label: 'View', submenu: [ { @@ -113,10 +117,9 @@ export function initMenu(windowManager: WindowManager) { { type: 'separator' }, { role: 'togglefullscreen' } ] - }) + }; - // "Help" submenu - menuItems.push({ + const helpMenu: MenuItemConstructorOptions = { role: 'help', submenu: [ { @@ -162,8 +165,9 @@ export function initMenu(windowManager: WindowManager) { } } ] - }); + }; - const menu = Menu.buildFromTemplate(menuItems); - Menu.setApplicationMenu(menu); + Menu.setApplicationMenu(Menu.buildFromTemplate([ + fileMenu, editMenu, viewMenu, helpMenu + ])); } diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index c8492a55cc..184c73ff31 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,4 +1,4 @@ -import { autorun, reaction } from "mobx"; +import { reaction } from "mobx"; import { BrowserWindow, shell } from "electron" import windowStateKeeper from "electron-window-state" import type { ClusterId } from "../common/cluster-store"; @@ -29,8 +29,12 @@ export class WindowManager { // Manage reactive state this.disposers.push( - // show and hide "no-clusters" window when necessary - autorun(this.handleNoClustersView), + // auto-show/hide "no-clusters" window when necessary + reaction(() => clusterStore.hasClusters(), hasClusters => { + this.handleNoClustersView({ activate: !hasClusters }); + }, { + fireImmediately: true + }), // auto-show active cluster window and subscribe for push-events reaction(() => clusterStore.activeClusterId, this.activateView, { @@ -48,10 +52,12 @@ export class WindowManager { ); } - protected handleNoClustersView = async () => { - this.noClustersWindow = this.initClusterView(null); - await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`).catch(Function); - if (!clusterStore.hasClusters()) { + protected handleNoClustersView = async ({ activate = false } = {}) => { + if (!this.noClustersWindow) { + this.noClustersWindow = this.initClusterView(null); + await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`); + } + if (activate) { this.activeView = this.noClustersWindow; this.noClustersWindow.show(); this.hideSplash(); @@ -69,8 +75,8 @@ export class WindowManager { resizable: false, show: false, }); + await this.splashWindow.loadURL("static://splash.html"); } - await this.splashWindow.loadURL("static://splash.html").catch(Function) this.splashWindow.show(); } diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index 3a36ef80a2..f4416aeabd 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -8,7 +8,7 @@ import type { KubeObjectStore } from "../kube-object.store"; import { KubeApi } from "./kube-api"; import { apiManager } from "./api-manager"; import { apiPrefix, isDevelopment } from "../../common/vars"; -import { clusterStore } from "../../common/cluster-store"; +import { getHostedCluster } from "../../common/cluster-store"; export interface IKubeWatchEvent { type: "ADDED" | "MODIFIED" | "DELETED"; @@ -61,7 +61,7 @@ export class KubeWatchApi { } protected getQuery(): Partial { - const { isAdmin, allowedNamespaces } = clusterStore.activeCluster; + const { isAdmin, allowedNamespaces } = getHostedCluster(); return { api: this.activeApis.map(api => { if (isAdmin) return api.getWatchUrl(); diff --git a/src/renderer/api/rbac.ts b/src/renderer/api/rbac.ts index 742b0ff17a..269132bd6d 100644 --- a/src/renderer/api/rbac.ts +++ b/src/renderer/api/rbac.ts @@ -1,4 +1,4 @@ -import { clusterStore } from "../../common/cluster-store"; +import { getHostedCluster } from "../../common/cluster-store"; // todo: refactor / move to cluster-store.ts? @@ -6,7 +6,7 @@ export function isAllowedResource(resources: string | string[]) { if (!Array.isArray(resources)) { resources = [resources]; } - const allowedResources = clusterStore.activeCluster?.allowedResources || []; + const { allowedResources } = getHostedCluster(); for (const resource of resources) { if (!allowedResources.includes(resource)) { return false; diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index b02cbf7e20..b32843be8a 100644 --- a/src/renderer/components/+apps-releases/release.store.ts +++ b/src/renderer/components/+apps-releases/release.store.ts @@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl import { ItemStore } from "../../item.store"; import { Secret } from "../../api/endpoints"; import { secretsStore } from "../+config-secrets/secrets.store"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; @autobind() export class ReleaseStore extends ItemStore { @@ -58,7 +58,7 @@ export class ReleaseStore extends ItemStore { this.isLoading = true; let items; try { - const { isAdmin, allowedNamespaces } = clusterStore.activeCluster; + const { isAdmin, allowedNamespaces } = getHostedCluster(); items = await this.loadItems(!isAdmin ? allowedNamespaces : null); } finally { if (items) { diff --git a/src/renderer/components/+cluster-settings/cluster-icon.scss b/src/renderer/components/+cluster-settings/cluster-icon.scss index 13efbffd29..4da483d54d 100644 --- a/src/renderer/components/+cluster-settings/cluster-icon.scss +++ b/src/renderer/components/+cluster-settings/cluster-icon.scss @@ -4,6 +4,7 @@ position: relative; opacity: .75; border-radius: $radius; + user-select: none; cursor: pointer; &.active, &.interactive:hover { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 5bd49fbe4e..8a291237d0 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1,7 +1,7 @@ import "./app.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; -import { autorun, computed, observable } from "mobx"; +import { observable, reaction } from "mobx"; import { Redirect, Route, Switch } from "react-router"; import { Notifications } from "./notifications"; import { NotFound } from "./+404"; @@ -28,7 +28,7 @@ import { CustomResources } from "./+custom-resources/custom-resources"; import { crdRoute } from "./+custom-resources"; import { isAllowedResource } from "../api/rbac"; import { AddCluster, addClusterRoute } from "./+add-cluster"; -import { LandingPage, landingRoute } from "./+landing-page"; +import { LandingPage, landingRoute, landingURL } from "./+landing-page"; import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings"; import { Workspaces, workspacesRoute } from "./+workspaces"; import { ErrorBoundary } from "./error-boundary"; @@ -44,36 +44,41 @@ import { navigate, navigation } from "../navigation"; export class App extends React.Component { @observable isReady = false; - @computed get clusterReady(): boolean { - const cluster = getHostedCluster(); - if (cluster) { - return cluster.initialized && cluster.accessible; - } + get cluster() { + return getHostedCluster() } async componentDidMount() { - await clusterIpc.activate.invokeFromRenderer(); // refresh state, reconnect, etc. - this.isReady = true; - + if (this.cluster) { + await clusterIpc.activate.invokeFromRenderer(); // refresh state, reconnect, etc. + } disposeOnUnmount(this, [ - autorun(() => { - if (!this.clusterReady) { - navigate(clusterStatusURL()); - } else if (clusterStatusURL() == navigation.getPath()) { - navigate("/"); // redirect when cluster accessible - } + reaction(() => this.startURL, this.onStartUrlChange, { + fireImmediately: true }) ]) + this.isReady = true; + } + + protected onStartUrlChange = (startURL: string) => { + const path = navigation.getPath(); + const redirectRequired = ["/", clusterStatusURL()].includes(path); + if (redirectRequired || !this.cluster?.accessible) { + navigate(startURL); + } } get startURL() { - if (!this.clusterReady) { - return clusterStatusURL(); + if (this.cluster) { + if (!this.cluster.accessible) { + return clusterStatusURL(); + } + if (isAllowedResource(["events", "nodes", "pods"])) { + return clusterURL(); + } + return workloadsURL(); } - if (isAllowedResource(["events", "nodes", "pods"])) { - return clusterURL(); - } - return workloadsURL(); + return landingURL(); } render() { @@ -87,19 +92,23 @@ export class App extends React.Component { - - - - - - - - - - - - - + {this.cluster && ( + <> + + + + + + + + + + + + + + + )} diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index a89cc8b871..7be7bd7836 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -21,13 +21,16 @@ export class ClusterStatus extends React.Component { } @computed get cluster() { - return getHostedCluster() + return getHostedCluster(); } async componentDidMount() { - this.authOutput = [{ data: "Connecting ...\n" }]; - ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res) => { - this.authOutput.push(res); + this.authOutput = [{ data: "Connecting..." }]; + ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyResponse) => { + this.authOutput.push({ + data: res.data.trimRight(), + error: res.error, + }); }) } @@ -36,7 +39,7 @@ export class ClusterStatus extends React.Component { } reconnect = async () => { - this.authOutput = [{ data: "Reconnecting ...\n" }]; + this.authOutput = [{ data: "Reconnecting..." }]; this.isReconnecting = true; await clusterIpc.activate.invokeFromRenderer(); this.isReconnecting = false; diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 99cbd8d59a..ec04c8f462 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -56,8 +56,8 @@ export class ClustersMenu extends React.Component { menu.append(new MenuItem({ label: _i18n._(t`Disconnect`), click: async () => { - await clusterIpc.disconnect.invokeFromRenderer(); navigate(clusterStatusURL()); + await clusterIpc.disconnect.invokeFromRenderer(); } })) } diff --git a/src/renderer/components/layout/main-layout.tsx b/src/renderer/components/layout/main-layout.tsx index e09049e8ec..e910a7e307 100755 --- a/src/renderer/components/layout/main-layout.tsx +++ b/src/renderer/components/layout/main-layout.tsx @@ -11,7 +11,7 @@ import { ErrorBoundary } from "../error-boundary"; import { Dock } from "../dock"; import { navigate, navigation } from "../../navigation"; import { themeStore } from "../../theme.store"; -import { clusterStore } from "../../../common/cluster-store"; +import { getHostedCluster } from "../../../common/cluster-store"; export interface TabRoute extends RouteProps { title: React.ReactNode; @@ -47,7 +47,7 @@ export class MainLayout extends React.Component { render() { const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props; - const clusterName = clusterStore.activeCluster?.contextName; + const { contextName: clusterName } = getHostedCluster(); const routePath = navigation.location.pathname; return (
diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 423d6db78f..0f647ced6d 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -6,7 +6,7 @@ import { ItemStore } from "./item.store"; import { apiManager } from "./api/api-manager"; import { IKubeApiQueryParams, KubeApi } from "./api/kube-api"; import { KubeJsonApiData } from "./api/kube-json-api"; -import { clusterStore } from "../common/cluster-store"; +import { getHostedCluster } from "../common/cluster-store"; @autobind() export abstract class KubeObjectStore extends ItemStore { @@ -76,7 +76,7 @@ export abstract class KubeObjectStore extends ItemSt this.isLoading = true; let items: T[]; try { - const { isAdmin, allowedNamespaces } = clusterStore.activeCluster; + const { isAdmin, allowedNamespaces } = getHostedCluster(); items = await this.loadItems(!isAdmin ? allowedNamespaces : null); items = this.filterItemsOnLoad(items); } finally {