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

fix reconnect, better handling no-clusters state

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-24 14:17:14 +03:00
parent 0490dc1d12
commit 408f64c184
12 changed files with 145 additions and 138 deletions

View File

@ -17,12 +17,4 @@ export const clusterIpc = {
return clusterStore.getById(clusterId)?.disconnect(); return clusterStore.getById(clusterId)?.disconnect();
}, },
}), }),
reconnect: createIpcChannel({
channel: "cluster:reconnect",
handle: (clusterId: ClusterId = clusterStore.activeClusterId) => {
tracker.event("cluster", "reconnect");
return clusterStore.getById(clusterId)?.reconnect();
},
}),
} }

View File

@ -197,3 +197,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
export const clusterStore = ClusterStore.getInstance<ClusterStore>(); export const clusterStore = ClusterStore.getInstance<ClusterStore>();
export function getHostedCluster(): Cluster {
const clusterId = location.hostname.split(".")[0];
return clusterStore.getById(clusterId);
}

View File

@ -12,9 +12,8 @@ export class ClusterManager {
autorun(() => { autorun(() => {
clusterStore.clusters.forEach(cluster => { clusterStore.clusters.forEach(cluster => {
if (!cluster.initialized) { if (!cluster.initialized) {
logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta()); logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
cluster.init(port); // connect to kube-auth-proxy, context handling cluster.init(port);
cluster.bindEvents(); // send push-updates to renderer
} }
}); });
}); });
@ -25,10 +24,7 @@ export class ClusterManager {
if (removedClusters.length > 0) { if (removedClusters.length > 0) {
const meta = removedClusters.map(cluster => cluster.getMeta()); const meta = removedClusters.map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => { removedClusters.forEach(cluster => cluster.disconnect());
cluster.disconnect();
cluster.unbindEvents();
});
clusterStore.removedClusters.clear(); clusterStore.removedClusters.clear();
} }
}, { }, {
@ -38,7 +34,6 @@ export class ClusterManager {
// listen for ipc-events that must/can be handled *only* in main-process (nodeIntegration=true) // listen for ipc-events that must/can be handled *only* in main-process (nodeIntegration=true)
clusterIpc.activate.handleInMain(); clusterIpc.activate.handleInMain();
clusterIpc.disconnect.handleInMain(); clusterIpc.disconnect.handleInMain();
clusterIpc.reconnect.handleInMain();
} }
stop() { stop() {

View File

@ -1,7 +1,7 @@
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature" import type { FeatureStatusMap } from "./feature"
import type { WorkspaceId } from "../common/workspace-store"; import type { WorkspaceId } from "../common/workspace-store";
import { action, computed, observable, reaction, toJS } from "mobx"; import { action, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { broadcastIpc } from "../common/ipc"; import { broadcastIpc } from "../common/ipc";
import { ContextHandler } from "./context-handler" import { ContextHandler } from "./context-handler"
@ -23,6 +23,7 @@ export interface ClusterState extends ClusterModel {
initialized: boolean; initialized: boolean;
apiUrl: string; apiUrl: string;
online: boolean; online: boolean;
disconnected: boolean;
accessible: boolean; accessible: boolean;
failureReason: string; failureReason: string;
nodes: number; nodes: number;
@ -40,7 +41,9 @@ export class Cluster implements ClusterModel {
public kubeCtl: Kubectl public kubeCtl: Kubectl
public contextHandler: ContextHandler; public contextHandler: ContextHandler;
protected kubeconfigManager: KubeconfigManager; protected kubeconfigManager: KubeconfigManager;
protected disposers: Function[] = []; protected eventDisposers: Function[] = [];
whenInitialized = when(() => this.initialized);
@observable initialized = false; @observable initialized = false;
@observable contextName: string; @observable contextName: string;
@ -67,10 +70,6 @@ export class Cluster implements ClusterModel {
this.updateModel(model); this.updateModel(model);
} }
@computed get isReady() {
return this.initialized && this.accessible === true;
}
@action @action
updateModel(model: ClusterModel) { updateModel(model: ClusterModel) {
Object.assign(this, model); Object.assign(this, model);
@ -103,47 +102,53 @@ export class Cluster implements ClusterModel {
} }
} }
bindEvents() { protected bindEvents() {
logger.info(`[CLUSTER]: bind events`, this.getMeta()); logger.info(`[CLUSTER]: bind events`, this.getMeta());
const refreshTimer = setInterval(() => this.online && this.refresh(), 30000); // every 30s const refreshTimer = setInterval(() => this.online && this.refresh(), 30000); // every 30s
const refreshEventsTimer = setInterval(() => this.online && this.refreshEvents(), 3000); // every 3s const refreshEventsTimer = setInterval(() => this.online && this.refreshEvents(), 3000); // every 3s
this.disposers.push( this.eventDisposers.push(
reaction(this.getState, this.pushState),
() => clearInterval(refreshTimer), () => clearInterval(refreshTimer),
() => clearInterval(refreshEventsTimer), () => clearInterval(refreshEventsTimer),
reaction(this.getState, this.pushState, {
fireImmediately: true
})
); );
} }
unbindEvents() { protected unbindEvents() {
logger.info(`[CLUSTER]: unbind events`, this.getMeta()); logger.info(`[CLUSTER]: unbind events`, this.getMeta());
this.disposers.forEach(dispose => dispose()); this.eventDisposers.forEach(dispose => dispose());
this.disposers.length = 0; this.eventDisposers.length = 0;
} }
async activate() { async activate() {
if (this.disconnected) await this.reconnect(); logger.info(`[CLUSTER]: activate`, this.getMeta());
await this.whenInitialized;
if (!this.eventDisposers.length) {
this.bindEvents();
}
if (this.disconnected) {
await this.reconnect();
}
await this.refresh(); await this.refresh();
return this.pushState(); return this.pushState();
} }
// todo: check, possibly doesn't work as expected
async reconnect() { async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta()); logger.info(`[CLUSTER]: reconnect`, this.getMeta());
this.disconnected = false;
await this.contextHandler.stopServer(); await this.contextHandler.stopServer();
await this.contextHandler.ensureServer(); await this.contextHandler.ensureServer();
this.disconnected = false;
} }
@action @action
disconnect() { disconnect() {
logger.info(`[CLUSTER]: disconnect`, this.getMeta()); logger.info(`[CLUSTER]: disconnect`, this.getMeta());
this.unbindEvents();
this.contextHandler.stopServer();
this.disconnected = true; this.disconnected = true;
this.online = false; this.online = false;
this.accessible = false; this.accessible = false;
this.contextHandler.stopServer(); this.pushState();
} }
@action @action
@ -344,13 +349,14 @@ export class Cluster implements ClusterModel {
}) })
} }
// serializable cluster-state (mostly used for push-notifications) // serializable cluster-state used for sync btw main <-> renderer
getState = (): ClusterState => { getState = (): ClusterState => {
const state: ClusterState = { const state: ClusterState = {
...this.toJSON(), ...this.toJSON(),
initialized: this.initialized, initialized: this.initialized,
apiUrl: this.apiUrl, apiUrl: this.apiUrl,
online: this.online, online: this.online,
disconnected: this.disconnected,
accessible: this.accessible, accessible: this.accessible,
failureReason: this.failureReason, failureReason: this.failureReason,
nodes: this.nodes, nodes: this.nodes,
@ -383,8 +389,9 @@ export class Cluster implements ClusterModel {
id: this.id, id: this.id,
name: this.contextName, name: this.contextName,
initialized: this.initialized, initialized: this.initialized,
accessible: this.accessible,
online: this.online, online: this.online,
accessible: this.accessible,
disconnected: this.disconnected,
} }
} }

View File

@ -74,7 +74,6 @@ async function main() {
// create window manager and open app // create window manager and open app
windowManager = new WindowManager(proxyPort); windowManager = new WindowManager(proxyPort);
windowManager.showSplash();
initMenu(windowManager); initMenu(windowManager);
} }

View File

@ -6,8 +6,8 @@ import { bundledKubectl, Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
export interface KubeAuthProxyResponse { export interface KubeAuthProxyResponse {
data: string; data: string; // stream=stdout
stream: "stderr" | "stdout"; error?: boolean; // stream=stderr
} }
export class KubeAuthProxy { export class KubeAuthProxy {
@ -47,10 +47,7 @@ export class KubeAuthProxy {
env: this.env env: this.env
}) })
this.proxyProcess.on("exit", (code) => { this.proxyProcess.on("exit", (code) => {
if (code) { this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 })
logger.error(`[KUBE-AUTH]: proxying ${this.cluster.contextName} exited with code ${code}`, this.cluster.getMeta());
}
this.sendIpcLogMessage({ data: `proxy exited with code ${code}`, stream: "stderr" })
this.proxyProcess = null this.proxyProcess = null
}) })
this.proxyProcess.stdout.on('data', (data) => { this.proxyProcess.stdout.on('data', (data) => {
@ -58,11 +55,11 @@ export class KubeAuthProxy {
if (logItem.startsWith("Starting to serve on")) { if (logItem.startsWith("Starting to serve on")) {
logItem = "Authentication proxy started\n" logItem = "Authentication proxy started\n"
} }
this.sendIpcLogMessage({ data: logItem, stream: "stdout" }) this.sendIpcLogMessage({ data: logItem })
}) })
this.proxyProcess.stderr.on('data', (data) => { this.proxyProcess.stderr.on('data', (data) => {
this.lastError = this.parseError(data.toString()) this.lastError = this.parseError(data.toString())
this.sendIpcLogMessage({ data: data.toString(), stream: "stderr" }) this.sendIpcLogMessage({ data: data.toString(), error: true })
}) })
return waitUntilUsed(this.port, 500, 10000) return waitUntilUsed(this.port, 500, 10000)
@ -97,6 +94,7 @@ export class KubeAuthProxy {
if (this.proxyProcess) { if (this.proxyProcess) {
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()) logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta())
this.proxyProcess.kill() this.proxyProcess.kill()
this.proxyProcess = null;
} }
} }
} }

View File

@ -13,7 +13,7 @@ export function initMenu(windowManager: WindowManager) {
function navigate(url: string) { function navigate(url: string) {
const activeClusterId = clusterStore.activeClusterId; const activeClusterId = clusterStore.activeClusterId;
const view = windowManager.getView(activeClusterId); const view = windowManager.getClusterView(activeClusterId);
if (view) { if (view) {
broadcastIpc({ broadcastIpc({
channel: "menu:navigate", channel: "menu:navigate",

View File

@ -4,7 +4,7 @@ import { Cluster } from "../cluster"
import { CoreV1Api, V1Secret } from "@kubernetes/client-node" import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) { function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
const tokenData = new Buffer(secret.data["token"], "base64") const tokenData = Buffer.from(secret.data["token"], "base64")
return { return {
'apiVersion': 'v1', 'apiVersion': 'v1',
'kind': 'Config', 'kind': 'Config',

View File

@ -1,4 +1,4 @@
import { autorun, reaction, when } from "mobx"; import { autorun, 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";
@ -15,7 +15,51 @@ export class WindowManager {
protected disposers: CallableFunction[] = []; protected disposers: CallableFunction[] = [];
protected windowState: windowStateKeeper.State; protected windowState: windowStateKeeper.State;
constructor(protected proxyPort: number) { constructor(protected proxyPort: number, showSplash = true) {
// Manage main window size and position with state persistence
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
// Show while app not ready
if (showSplash) {
this.showSplash();
}
// Manage reactive state
this.disposers.push(
// show and hide "no-clusters" window when necessary
autorun(this.handleNoClustersView),
// auto-show active cluster window and subscribe for push-events
reaction(() => clusterStore.activeClusterId, this.activateView, {
fireImmediately: true,
}),
// auto-destroy views for removed clusters
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
removedClusters.forEach(cluster => {
this.destroyClusterView(cluster.id);
});
}, {
delay: 25, // fix: destroy later and allow to use view's state in next activateView()
}),
);
}
protected handleNoClustersView = async () => {
this.noClustersWindow = this.initClusterView(null);
await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`).catch(Function);
if (!clusterStore.hasClusters()) {
this.activeView = this.noClustersWindow;
this.noClustersWindow.show();
this.hideSplash();
}
}
async showSplash() {
if (!this.splashWindow) {
this.splashWindow = new BrowserWindow({ this.splashWindow = new BrowserWindow({
width: 500, width: 500,
height: 300, height: 300,
@ -25,50 +69,8 @@ export class WindowManager {
resizable: false, resizable: false,
show: false, show: false,
}); });
// Manage main window size and position with state persistence
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
// Manage reactive state
this.disposers.push(
// auto-show active cluster window and subscribe for push-events
reaction(() => clusterStore.activeClusterId, this.activateView, {
fireImmediately: true,
}),
// auto-destroy views for removed clusters
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
removedClusters.forEach(cluster => {
this.destroyView(cluster.id);
});
}, {
delay: 25, // fix: destroy later and allow to use view's state in next activateView()
}),
// handle no-clusters view
autorun(() => {
if (!clusterStore.hasClusters()) {
this.handleNoClustersView();
} }
}) await this.splashWindow.loadURL("static://splash.html").catch(Function)
);
}
protected async handleNoClustersView() {
if (!this.noClustersWindow) {
this.noClustersWindow = this.initView(undefined);
await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`);
}
this.activeView = this.noClustersWindow;
this.noClustersWindow.show();
this.hideSplash();
}
async showSplash() {
await this.splashWindow.loadURL("static://splash.html").catch(() => null)
this.splashWindow.show(); this.splashWindow.show();
} }
@ -76,19 +78,17 @@ export class WindowManager {
this.splashWindow.hide(); this.splashWindow.hide();
} }
getView(clusterId: ClusterId) { getClusterView(clusterId: ClusterId): BrowserWindow {
return this.views.get(clusterId); return this.views.get(clusterId);
} }
activateView = async (clusterId: ClusterId): Promise<number> => { activateView = async (clusterId: ClusterId): Promise<number> => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (!cluster) { if (!cluster) return;
return;
}
try { try {
const prevActiveView = this.activeView; const prevActiveView = this.activeView;
const isLoadedBefore = !!this.getView(clusterId); const isLoadedBefore = !!this.getClusterView(clusterId);
const view = this.initView(clusterId); const view = this.initClusterView(clusterId);
logger.info(`[WINDOW-MANAGER]: activating cluster view`, { logger.info(`[WINDOW-MANAGER]: activating cluster view`, {
id: view.id, id: view.id,
clusterId: cluster.id, clusterId: cluster.id,
@ -97,9 +97,8 @@ export class WindowManager {
}); });
if (prevActiveView !== view) { if (prevActiveView !== view) {
this.activeView = view; this.activeView = view;
cluster.activate(); // refresh + reconnect when required
if (!isLoadedBefore) { if (!isLoadedBefore) {
await when(() => cluster.initialized); await cluster.whenInitialized; // wait for url
await view.loadURL(cluster.webContentUrl); await view.loadURL(cluster.webContentUrl);
this.hideSplash(); this.hideSplash();
} }
@ -119,8 +118,8 @@ export class WindowManager {
} }
} }
protected initView(clusterId: ClusterId): BrowserWindow { protected initClusterView(clusterId: ClusterId): BrowserWindow {
let view = this.getView(clusterId); let view = this.getClusterView(clusterId);
if (!view) { if (!view) {
const { width, height, x, y } = this.windowState; const { width, height, x, y } = this.windowState;
view = new BrowserWindow({ view = new BrowserWindow({
@ -146,7 +145,7 @@ export class WindowManager {
return view; return view;
} }
protected destroyView(clusterId: ClusterId) { protected destroyClusterView(clusterId: ClusterId) {
const view = this.views.get(clusterId); const view = this.views.get(clusterId);
if (view) { if (view) {
view.destroy(); view.destroy();

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 { computed, observable, reaction } from "mobx"; import { autorun, computed, observable } 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,45 +28,45 @@ 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, landingURL } from "./+landing-page"; import { LandingPage, landingRoute } 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";
import { navigation } from "../navigation";
import { clusterIpc } from "../../common/cluster-ipc"; import { clusterIpc } from "../../common/cluster-ipc";
import { clusterStore } from "../../common/cluster-store"; import { getHostedCluster } from "../../common/cluster-store";
import { clusterStatusRoute, clusterStatusURL } from "./cluster-manager/cluster-status.route"; import { clusterStatusRoute, clusterStatusURL } from "./cluster-manager/cluster-status.route";
import { Preferences, preferencesRoute } from "./+preferences"; import { Preferences, preferencesRoute } from "./+preferences";
import { ClusterStatus } from "./cluster-manager/cluster-status"; import { ClusterStatus } from "./cluster-manager/cluster-status";
import { CubeSpinner } from "./spinner"; import { CubeSpinner } from "./spinner";
import { navigate, navigation } from "../navigation";
@observer @observer
export class App extends React.Component { export class App extends React.Component {
@observable isReady = false; @observable isReady = false;
@computed get clusterReady() { @computed get clusterReady(): boolean {
const clusterId = location.hostname.split(".")[0]; const cluster = getHostedCluster();
return !!clusterStore.getById(clusterId)?.isReady; if (cluster) {
return cluster.initialized && cluster.accessible;
}
} }
async componentDidMount() { async componentDidMount() {
await clusterIpc.activate.invokeFromRenderer(); await clusterIpc.activate.invokeFromRenderer(); // refresh state, reconnect, etc.
this.isReady = true; this.isReady = true;
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => this.startURL, url => { autorun(() => {
const redirect = !this.clusterReady || !clusterStore.hasClusters(); if (!this.clusterReady) {
if (redirect) navigation.replace(url); navigate(clusterStatusURL());
}, { } else if (clusterStatusURL() == navigation.getPath()) {
fireImmediately: true navigate("/"); // redirect when cluster accessible
}
}) })
]) ])
} }
get startURL() { get startURL() {
if (!clusterStore.hasClusters()) {
return landingURL();
}
if (!this.clusterReady) { if (!this.clusterReady) {
return clusterStatusURL(); return clusterStatusURL();
} }

View File

@ -1,65 +1,76 @@
import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy";
import "./cluster-status.scss" import "./cluster-status.scss"
import React from "react"; import React from "react";
import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy";
import { clusterStore } from "../../../common/cluster-store";
import { ipcRenderer } from "electron";
import { observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ipcRenderer } from "electron";
import { computed, observable } from "mobx";
import { clusterIpc } from "../../../common/cluster-ipc";
import { getHostedCluster } from "../../../common/cluster-store";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Button } from "../button"; import { Button } from "../button";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { clusterIpc } from "../../../common/cluster-ipc";
@observer @observer
export class ClusterStatus extends React.Component { export class ClusterStatus extends React.Component {
@observable authOutput: string[] = []; @observable authOutput: KubeAuthProxyResponse[] = [];
@observable hasErrors = false; @observable isReconnecting = false;
get cluster() { @computed get hasErrors() {
return clusterStore.activeCluster; return this.authOutput.some(({ error }) => error)
} }
get clusterId() { @computed get cluster() {
return clusterStore.activeClusterId; return getHostedCluster()
} }
componentDidMount() { async componentDidMount() {
this.authOutput = ["Connecting ...\n"]; this.authOutput = [{ data: "Connecting ...\n" }];
ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, { data, stream }: KubeAuthProxyResponse) => { ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res) => {
this.authOutput.push(`[${stream}]: ${data}`); this.authOutput.push(res);
if (stream === "stderr") {
this.hasErrors = true;
}
}) })
} }
componentWillUnmount() { componentWillUnmount() {
ipcRenderer.removeAllListeners(`kube-auth:${this.clusterId}`); ipcRenderer.removeAllListeners(`kube-auth:${this.cluster.id}`);
} }
reconnect = () => { reconnect = async () => {
this.authOutput = ["Reconnecting ...\n"]; this.authOutput = [{ data: "Reconnecting ...\n" }];
clusterIpc.reconnect.invokeFromRenderer(this.clusterId); this.isReconnecting = true;
await clusterIpc.activate.invokeFromRenderer();
this.isReconnecting = false;
} }
render() { render() {
const { authOutput, cluster, hasErrors } = this; const { authOutput, cluster, hasErrors } = this;
const isDisconnected = !!cluster.disconnected;
const isInactive = hasErrors || isDisconnected;
return ( return (
<div className="ClusterStatus flex column gaps"> <div className="ClusterStatus flex column gaps">
{!hasErrors && <Icon material="cloud_queue"/>} {isInactive && (
{hasErrors && <Icon material="cloud_off" className="error"/>} <Icon
{cluster && <h2>{cluster.contextName}</h2>} material="cloud_off"
className={cssNames({ error: hasErrors })}
/>
)}
<h2>
{cluster.contextName}
</h2>
{!isDisconnected && (
<pre className="kube-auth-out"> <pre className="kube-auth-out">
{authOutput.map((data, index) => { {authOutput.map(({ data, error }, index) => {
const error = data.startsWith("[stderr]");
return <p key={index} className={cssNames({ error })}>{data}</p> return <p key={index} className={cssNames({ error })}>{data}</p>
})} })}
</pre> </pre>
{hasErrors && ( )}
{isInactive && (
<Button <Button
primary className="box center" primary
label="Reconnect" label="Reconnect"
className="box center"
onClick={this.reconnect} onClick={this.reconnect}
waiting={this.isReconnecting}
/> />
)} )}
</div> </div>

View File

@ -20,6 +20,7 @@ import { landingURL } from "../+landing-page";
import { Tooltip, TooltipContent } from "../tooltip"; import { Tooltip, TooltipContent } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc"; import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterStatusURL } from "./cluster-status.route";
// fixme: allow to rearrange clusters with drag&drop // fixme: allow to rearrange clusters with drag&drop
@ -54,9 +55,9 @@ export class ClustersMenu extends React.Component<Props> {
if (cluster.online) { if (cluster.online) {
menu.append(new MenuItem({ menu.append(new MenuItem({
label: _i18n._(t`Disconnect`), label: _i18n._(t`Disconnect`),
click: () => { click: async () => {
navigate(landingURL()); await clusterIpc.disconnect.invokeFromRenderer();
clusterIpc.disconnect.invokeFromRenderer(); navigate(clusterStatusURL());
} }
})) }))
} }