1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

auto-refresh menu, no-clusters page + navigation fixes

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-24 16:33:20 +03:00
parent 408f64c184
commit fe06643a8e
13 changed files with 109 additions and 90 deletions

View File

@ -77,10 +77,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@observable removedClusters = observable.map<ClusterId, Cluster>(); @observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>(); @observable clusters = observable.map<ClusterId, Cluster>();
@computed get activeCluster(): Cluster | null {
return this.getById(this.activeClusterId);
}
@computed get clustersList(): Cluster[] { @computed get clustersList(): Cluster[] {
return Array.from(this.clusters.values()); return Array.from(this.clusters.values());
} }

View File

@ -6,7 +6,7 @@ import { bundledKubectl, Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
export interface KubeAuthProxyResponse { export interface KubeAuthProxyResponse {
data: string; // stream=stdout data: string;
error?: boolean; // stream=stderr error?: boolean; // stream=stderr
} }

View File

@ -1,5 +1,6 @@
import type { WindowManager } from "./window-manager"; import type { WindowManager } from "./window-manager";
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron"
import { autorun } from "mobx";
import { broadcastIpc } from "../common/ipc"; import { broadcastIpc } from "../common/ipc";
import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars"; import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars";
import { clusterStore } from "../common/cluster-store"; 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 { preferencesURL } from "../renderer/components/+preferences/preferences.route";
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route"; import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route"; import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
import logger from "./logger";
export function initMenu(windowManager: WindowManager) { 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) { function navigate(url: string) {
const activeClusterId = clusterStore.activeClusterId; broadcastIpc({
const view = windowManager.getClusterView(activeClusterId); channel: "menu:navigate",
if (view) { webContentId: clusterView ? clusterView.id : undefined /*no-clusters*/,
broadcastIpc({ args: [url],
channel: "menu:navigate", });
webContentId: view.id,
args: [url],
});
}
} }
function macOnly(menuItems: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] { function macOnly(menuItems: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] {
@ -28,8 +35,7 @@ export function initMenu(windowManager: WindowManager) {
return menuItems; return menuItems;
} }
// "File" submenu const fileMenu: MenuItemConstructorOptions = {
menuItems.push({
label: isMac ? app.getName() : "File", label: isMac ? app.getName() : "File",
submenu: [ submenu: [
{ {
@ -38,12 +44,12 @@ export function initMenu(windowManager: WindowManager) {
navigate(addClusterURL()) navigate(addClusterURL())
} }
}, },
{ ...(hasClusters ? [{
label: 'Cluster Settings', label: 'Cluster Settings',
click() { click() {
navigate(clusterSettingsURL()) navigate(clusterSettingsURL())
} }
}, }] : []),
{ type: 'separator' }, { type: 'separator' },
{ {
label: 'Preferences', label: 'Preferences',
@ -62,10 +68,9 @@ export function initMenu(windowManager: WindowManager) {
{ type: 'separator' }, { type: 'separator' },
{ role: 'quit' } { role: 'quit' }
] ]
}); };
// "Edit" submenu const editMenu: MenuItemConstructorOptions = {
menuItems.push({
label: 'Edit', label: 'Edit',
submenu: [ submenu: [
{ role: 'undo' }, { role: 'undo' },
@ -78,10 +83,9 @@ export function initMenu(windowManager: WindowManager) {
{ type: 'separator' }, { type: 'separator' },
{ role: 'selectAll' }, { role: 'selectAll' },
] ]
}); };
// "View" submenu const viewMenu: MenuItemConstructorOptions = {
menuItems.push({
label: 'View', label: 'View',
submenu: [ submenu: [
{ {
@ -113,10 +117,9 @@ export function initMenu(windowManager: WindowManager) {
{ type: 'separator' }, { type: 'separator' },
{ role: 'togglefullscreen' } { role: 'togglefullscreen' }
] ]
}) };
// "Help" submenu const helpMenu: MenuItemConstructorOptions = {
menuItems.push({
role: 'help', role: 'help',
submenu: [ submenu: [
{ {
@ -162,8 +165,9 @@ export function initMenu(windowManager: WindowManager) {
} }
} }
] ]
}); };
const menu = Menu.buildFromTemplate(menuItems); Menu.setApplicationMenu(Menu.buildFromTemplate([
Menu.setApplicationMenu(menu); fileMenu, editMenu, viewMenu, helpMenu
]));
} }

View File

@ -1,4 +1,4 @@
import { autorun, reaction } from "mobx"; import { reaction } from "mobx";
import { BrowserWindow, shell } from "electron" import { BrowserWindow, shell } from "electron"
import windowStateKeeper from "electron-window-state" import windowStateKeeper from "electron-window-state"
import type { ClusterId } from "../common/cluster-store"; import type { ClusterId } from "../common/cluster-store";
@ -29,8 +29,12 @@ export class WindowManager {
// Manage reactive state // Manage reactive state
this.disposers.push( this.disposers.push(
// show and hide "no-clusters" window when necessary // auto-show/hide "no-clusters" window when necessary
autorun(this.handleNoClustersView), reaction(() => clusterStore.hasClusters(), hasClusters => {
this.handleNoClustersView({ activate: !hasClusters });
}, {
fireImmediately: true
}),
// auto-show active cluster window and subscribe for push-events // auto-show active cluster window and subscribe for push-events
reaction(() => clusterStore.activeClusterId, this.activateView, { reaction(() => clusterStore.activeClusterId, this.activateView, {
@ -48,10 +52,12 @@ export class WindowManager {
); );
} }
protected handleNoClustersView = async () => { protected handleNoClustersView = async ({ activate = false } = {}) => {
this.noClustersWindow = this.initClusterView(null); if (!this.noClustersWindow) {
await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`).catch(Function); this.noClustersWindow = this.initClusterView(null);
if (!clusterStore.hasClusters()) { await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`);
}
if (activate) {
this.activeView = this.noClustersWindow; this.activeView = this.noClustersWindow;
this.noClustersWindow.show(); this.noClustersWindow.show();
this.hideSplash(); this.hideSplash();
@ -69,8 +75,8 @@ export class WindowManager {
resizable: false, resizable: false,
show: false, show: false,
}); });
await this.splashWindow.loadURL("static://splash.html");
} }
await this.splashWindow.loadURL("static://splash.html").catch(Function)
this.splashWindow.show(); this.splashWindow.show();
} }

View File

@ -8,7 +8,7 @@ import type { KubeObjectStore } from "../kube-object.store";
import { KubeApi } from "./kube-api"; import { KubeApi } from "./kube-api";
import { apiManager } from "./api-manager"; import { apiManager } from "./api-manager";
import { apiPrefix, isDevelopment } from "../../common/vars"; import { apiPrefix, isDevelopment } from "../../common/vars";
import { clusterStore } from "../../common/cluster-store"; import { getHostedCluster } from "../../common/cluster-store";
export interface IKubeWatchEvent<T = any> { export interface IKubeWatchEvent<T = any> {
type: "ADDED" | "MODIFIED" | "DELETED"; type: "ADDED" | "MODIFIED" | "DELETED";
@ -61,7 +61,7 @@ export class KubeWatchApi {
} }
protected getQuery(): Partial<IKubeWatchRouteQuery> { protected getQuery(): Partial<IKubeWatchRouteQuery> {
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster; const { isAdmin, allowedNamespaces } = getHostedCluster();
return { return {
api: this.activeApis.map(api => { api: this.activeApis.map(api => {
if (isAdmin) return api.getWatchUrl(); if (isAdmin) return api.getWatchUrl();

View File

@ -1,4 +1,4 @@
import { clusterStore } from "../../common/cluster-store"; import { getHostedCluster } from "../../common/cluster-store";
// todo: refactor / move to cluster-store.ts? // todo: refactor / move to cluster-store.ts?
@ -6,7 +6,7 @@ export function isAllowedResource(resources: string | string[]) {
if (!Array.isArray(resources)) { if (!Array.isArray(resources)) {
resources = [resources]; resources = [resources];
} }
const allowedResources = clusterStore.activeCluster?.allowedResources || []; const { allowedResources } = getHostedCluster();
for (const resource of resources) { for (const resource of resources) {
if (!allowedResources.includes(resource)) { if (!allowedResources.includes(resource)) {
return false; return false;

View File

@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl
import { ItemStore } from "../../item.store"; import { ItemStore } from "../../item.store";
import { Secret } from "../../api/endpoints"; import { Secret } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store"; import { secretsStore } from "../+config-secrets/secrets.store";
import { clusterStore } from "../../../common/cluster-store"; import { getHostedCluster } from "../../../common/cluster-store";
@autobind() @autobind()
export class ReleaseStore extends ItemStore<HelmRelease> { export class ReleaseStore extends ItemStore<HelmRelease> {
@ -58,7 +58,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
this.isLoading = true; this.isLoading = true;
let items; let items;
try { try {
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster; const { isAdmin, allowedNamespaces } = getHostedCluster();
items = await this.loadItems(!isAdmin ? allowedNamespaces : null); items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
} finally { } finally {
if (items) { if (items) {

View File

@ -4,6 +4,7 @@
position: relative; position: relative;
opacity: .75; opacity: .75;
border-radius: $radius; border-radius: $radius;
user-select: none;
cursor: pointer; cursor: pointer;
&.active, &.interactive:hover { &.active, &.interactive:hover {

View File

@ -1,7 +1,7 @@
import "./app.scss"; import "./app.scss";
import React from "react"; import React from "react";
import { disposeOnUnmount, observer } from "mobx-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 { Redirect, Route, Switch } from "react-router";
import { Notifications } from "./notifications"; import { Notifications } from "./notifications";
import { NotFound } from "./+404"; import { NotFound } from "./+404";
@ -28,7 +28,7 @@ import { CustomResources } from "./+custom-resources/custom-resources";
import { crdRoute } from "./+custom-resources"; import { crdRoute } from "./+custom-resources";
import { isAllowedResource } from "../api/rbac"; import { isAllowedResource } from "../api/rbac";
import { AddCluster, addClusterRoute } from "./+add-cluster"; 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 { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
import { Workspaces, workspacesRoute } from "./+workspaces"; import { Workspaces, workspacesRoute } from "./+workspaces";
import { ErrorBoundary } from "./error-boundary"; import { ErrorBoundary } from "./error-boundary";
@ -44,36 +44,41 @@ import { navigate, navigation } from "../navigation";
export class App extends React.Component { export class App extends React.Component {
@observable isReady = false; @observable isReady = false;
@computed get clusterReady(): boolean { get cluster() {
const cluster = getHostedCluster(); return getHostedCluster()
if (cluster) {
return cluster.initialized && cluster.accessible;
}
} }
async componentDidMount() { async componentDidMount() {
await clusterIpc.activate.invokeFromRenderer(); // refresh state, reconnect, etc. if (this.cluster) {
this.isReady = true; await clusterIpc.activate.invokeFromRenderer(); // refresh state, reconnect, etc.
}
disposeOnUnmount(this, [ disposeOnUnmount(this, [
autorun(() => { reaction(() => this.startURL, this.onStartUrlChange, {
if (!this.clusterReady) { fireImmediately: true
navigate(clusterStatusURL());
} else if (clusterStatusURL() == navigation.getPath()) {
navigate("/"); // redirect when cluster accessible
}
}) })
]) ])
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() { get startURL() {
if (!this.clusterReady) { if (this.cluster) {
return clusterStatusURL(); if (!this.cluster.accessible) {
return clusterStatusURL();
}
if (isAllowedResource(["events", "nodes", "pods"])) {
return clusterURL();
}
return workloadsURL();
} }
if (isAllowedResource(["events", "nodes", "pods"])) { return landingURL();
return clusterURL();
}
return workloadsURL();
} }
render() { render() {
@ -87,19 +92,23 @@ export class App extends React.Component {
<Route component={Preferences} {...preferencesRoute}/> <Route component={Preferences} {...preferencesRoute}/>
<Route component={Workspaces} {...workspacesRoute}/> <Route component={Workspaces} {...workspacesRoute}/>
<Route component={AddCluster} {...addClusterRoute}/> <Route component={AddCluster} {...addClusterRoute}/>
<Route component={Cluster} {...clusterRoute}/> {this.cluster && (
<Route component={ClusterStatus} {...clusterStatusRoute}/> <>
<Route component={ClusterSettings} {...clusterSettingsRoute}/> <Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/> <Route component={ClusterStatus} {...clusterStatusRoute}/>
<Route component={Workloads} {...workloadsRoute}/> <Route component={ClusterSettings} {...clusterSettingsRoute}/>
<Route component={Config} {...configRoute}/> <Route component={Nodes} {...nodesRoute}/>
<Route component={Network} {...networkRoute}/> <Route component={Workloads} {...workloadsRoute}/>
<Route component={Storage} {...storageRoute}/> <Route component={Config} {...configRoute}/>
<Route component={Namespaces} {...namespacesRoute}/> <Route component={Network} {...networkRoute}/>
<Route component={Events} {...eventRoute}/> <Route component={Storage} {...storageRoute}/>
<Route component={CustomResources} {...crdRoute}/> <Route component={Namespaces} {...namespacesRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/> <Route component={Events} {...eventRoute}/>
<Route component={Apps} {...appsRoute}/> <Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
</>
)}
<Redirect exact from="/" to={this.startURL}/> <Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/> <Route component={NotFound}/>
</Switch> </Switch>

View File

@ -21,13 +21,16 @@ export class ClusterStatus extends React.Component {
} }
@computed get cluster() { @computed get cluster() {
return getHostedCluster() return getHostedCluster();
} }
async componentDidMount() { async componentDidMount() {
this.authOutput = [{ data: "Connecting ...\n" }]; this.authOutput = [{ data: "Connecting..." }];
ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res) => { ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyResponse) => {
this.authOutput.push(res); this.authOutput.push({
data: res.data.trimRight(),
error: res.error,
});
}) })
} }
@ -36,7 +39,7 @@ export class ClusterStatus extends React.Component {
} }
reconnect = async () => { reconnect = async () => {
this.authOutput = [{ data: "Reconnecting ...\n" }]; this.authOutput = [{ data: "Reconnecting..." }];
this.isReconnecting = true; this.isReconnecting = true;
await clusterIpc.activate.invokeFromRenderer(); await clusterIpc.activate.invokeFromRenderer();
this.isReconnecting = false; this.isReconnecting = false;

View File

@ -56,8 +56,8 @@ export class ClustersMenu extends React.Component<Props> {
menu.append(new MenuItem({ menu.append(new MenuItem({
label: _i18n._(t`Disconnect`), label: _i18n._(t`Disconnect`),
click: async () => { click: async () => {
await clusterIpc.disconnect.invokeFromRenderer();
navigate(clusterStatusURL()); navigate(clusterStatusURL());
await clusterIpc.disconnect.invokeFromRenderer();
} }
})) }))
} }

View File

@ -11,7 +11,7 @@ import { ErrorBoundary } from "../error-boundary";
import { Dock } from "../dock"; import { Dock } from "../dock";
import { navigate, navigation } from "../../navigation"; import { navigate, navigation } from "../../navigation";
import { themeStore } from "../../theme.store"; import { themeStore } from "../../theme.store";
import { clusterStore } from "../../../common/cluster-store"; import { getHostedCluster } from "../../../common/cluster-store";
export interface TabRoute extends RouteProps { export interface TabRoute extends RouteProps {
title: React.ReactNode; title: React.ReactNode;
@ -47,7 +47,7 @@ export class MainLayout extends React.Component<Props> {
render() { render() {
const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props; const { className, contentClass, headerClass, tabs, footer, footerClass, children } = this.props;
const clusterName = clusterStore.activeCluster?.contextName; const { contextName: clusterName } = getHostedCluster();
const routePath = navigation.location.pathname; const routePath = navigation.location.pathname;
return ( return (
<div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}> <div className={cssNames("MainLayout", className, themeStore.activeTheme.type)}>

View File

@ -6,7 +6,7 @@ import { ItemStore } from "./item.store";
import { apiManager } from "./api/api-manager"; import { apiManager } from "./api/api-manager";
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api"; import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
import { KubeJsonApiData } from "./api/kube-json-api"; import { KubeJsonApiData } from "./api/kube-json-api";
import { clusterStore } from "../common/cluster-store"; import { getHostedCluster } from "../common/cluster-store";
@autobind() @autobind()
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> { export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
@ -76,7 +76,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
this.isLoading = true; this.isLoading = true;
let items: T[]; let items: T[];
try { try {
const { isAdmin, allowedNamespaces } = clusterStore.activeCluster; const { isAdmin, allowedNamespaces } = getHostedCluster();
items = await this.loadItems(!isAdmin ? allowedNamespaces : null); items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
items = this.filterItemsOnLoad(items); items = this.filterItemsOnLoad(items);
} finally { } finally {