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:
parent
0490dc1d12
commit
408f64c184
@ -17,12 +17,4 @@ export const clusterIpc = {
|
||||
return clusterStore.getById(clusterId)?.disconnect();
|
||||
},
|
||||
}),
|
||||
|
||||
reconnect: createIpcChannel({
|
||||
channel: "cluster:reconnect",
|
||||
handle: (clusterId: ClusterId = clusterStore.activeClusterId) => {
|
||||
tracker.event("cluster", "reconnect");
|
||||
return clusterStore.getById(clusterId)?.reconnect();
|
||||
},
|
||||
}),
|
||||
}
|
||||
@ -197,3 +197,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
export function getHostedCluster(): Cluster {
|
||||
const clusterId = location.hostname.split(".")[0];
|
||||
return clusterStore.getById(clusterId);
|
||||
}
|
||||
|
||||
@ -12,9 +12,8 @@ export class ClusterManager {
|
||||
autorun(() => {
|
||||
clusterStore.clusters.forEach(cluster => {
|
||||
if (!cluster.initialized) {
|
||||
logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta());
|
||||
cluster.init(port); // connect to kube-auth-proxy, context handling
|
||||
cluster.bindEvents(); // send push-updates to renderer
|
||||
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
|
||||
cluster.init(port);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -25,10 +24,7 @@ export class ClusterManager {
|
||||
if (removedClusters.length > 0) {
|
||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
||||
removedClusters.forEach(cluster => {
|
||||
cluster.disconnect();
|
||||
cluster.unbindEvents();
|
||||
});
|
||||
removedClusters.forEach(cluster => cluster.disconnect());
|
||||
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)
|
||||
clusterIpc.activate.handleInMain();
|
||||
clusterIpc.disconnect.handleInMain();
|
||||
clusterIpc.reconnect.handleInMain();
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
||||
import type { FeatureStatusMap } from "./feature"
|
||||
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 { broadcastIpc } from "../common/ipc";
|
||||
import { ContextHandler } from "./context-handler"
|
||||
@ -23,6 +23,7 @@ export interface ClusterState extends ClusterModel {
|
||||
initialized: boolean;
|
||||
apiUrl: string;
|
||||
online: boolean;
|
||||
disconnected: boolean;
|
||||
accessible: boolean;
|
||||
failureReason: string;
|
||||
nodes: number;
|
||||
@ -40,7 +41,9 @@ export class Cluster implements ClusterModel {
|
||||
public kubeCtl: Kubectl
|
||||
public contextHandler: ContextHandler;
|
||||
protected kubeconfigManager: KubeconfigManager;
|
||||
protected disposers: Function[] = [];
|
||||
protected eventDisposers: Function[] = [];
|
||||
|
||||
whenInitialized = when(() => this.initialized);
|
||||
|
||||
@observable initialized = false;
|
||||
@observable contextName: string;
|
||||
@ -67,10 +70,6 @@ export class Cluster implements ClusterModel {
|
||||
this.updateModel(model);
|
||||
}
|
||||
|
||||
@computed get isReady() {
|
||||
return this.initialized && this.accessible === true;
|
||||
}
|
||||
|
||||
@action
|
||||
updateModel(model: ClusterModel) {
|
||||
Object.assign(this, model);
|
||||
@ -103,47 +102,53 @@ export class Cluster implements ClusterModel {
|
||||
}
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
protected bindEvents() {
|
||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||
const refreshTimer = setInterval(() => this.online && this.refresh(), 30000); // every 30s
|
||||
const refreshEventsTimer = setInterval(() => this.online && this.refreshEvents(), 3000); // every 3s
|
||||
|
||||
this.disposers.push(
|
||||
this.eventDisposers.push(
|
||||
reaction(this.getState, this.pushState),
|
||||
() => clearInterval(refreshTimer),
|
||||
() => clearInterval(refreshEventsTimer),
|
||||
reaction(this.getState, this.pushState, {
|
||||
fireImmediately: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
protected unbindEvents() {
|
||||
logger.info(`[CLUSTER]: unbind events`, this.getMeta());
|
||||
this.disposers.forEach(dispose => dispose());
|
||||
this.disposers.length = 0;
|
||||
this.eventDisposers.forEach(dispose => dispose());
|
||||
this.eventDisposers.length = 0;
|
||||
}
|
||||
|
||||
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();
|
||||
return this.pushState();
|
||||
}
|
||||
|
||||
// todo: check, possibly doesn't work as expected
|
||||
async reconnect() {
|
||||
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||
this.disconnected = false;
|
||||
await this.contextHandler.stopServer();
|
||||
await this.contextHandler.ensureServer();
|
||||
this.disconnected = false;
|
||||
}
|
||||
|
||||
@action
|
||||
disconnect() {
|
||||
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
|
||||
this.unbindEvents();
|
||||
this.contextHandler.stopServer();
|
||||
this.disconnected = true;
|
||||
this.online = false;
|
||||
this.accessible = false;
|
||||
this.contextHandler.stopServer();
|
||||
this.pushState();
|
||||
}
|
||||
|
||||
@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 => {
|
||||
const state: ClusterState = {
|
||||
...this.toJSON(),
|
||||
initialized: this.initialized,
|
||||
apiUrl: this.apiUrl,
|
||||
online: this.online,
|
||||
disconnected: this.disconnected,
|
||||
accessible: this.accessible,
|
||||
failureReason: this.failureReason,
|
||||
nodes: this.nodes,
|
||||
@ -383,8 +389,9 @@ export class Cluster implements ClusterModel {
|
||||
id: this.id,
|
||||
name: this.contextName,
|
||||
initialized: this.initialized,
|
||||
accessible: this.accessible,
|
||||
online: this.online,
|
||||
accessible: this.accessible,
|
||||
disconnected: this.disconnected,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -74,7 +74,6 @@ async function main() {
|
||||
|
||||
// create window manager and open app
|
||||
windowManager = new WindowManager(proxyPort);
|
||||
windowManager.showSplash();
|
||||
initMenu(windowManager);
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ import { bundledKubectl, Kubectl } from "./kubectl"
|
||||
import logger from "./logger"
|
||||
|
||||
export interface KubeAuthProxyResponse {
|
||||
data: string;
|
||||
stream: "stderr" | "stdout";
|
||||
data: string; // stream=stdout
|
||||
error?: boolean; // stream=stderr
|
||||
}
|
||||
|
||||
export class KubeAuthProxy {
|
||||
@ -47,10 +47,7 @@ export class KubeAuthProxy {
|
||||
env: this.env
|
||||
})
|
||||
this.proxyProcess.on("exit", (code) => {
|
||||
if (code) {
|
||||
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.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 })
|
||||
this.proxyProcess = null
|
||||
})
|
||||
this.proxyProcess.stdout.on('data', (data) => {
|
||||
@ -58,11 +55,11 @@ export class KubeAuthProxy {
|
||||
if (logItem.startsWith("Starting to serve on")) {
|
||||
logItem = "Authentication proxy started\n"
|
||||
}
|
||||
this.sendIpcLogMessage({ data: logItem, stream: "stdout" })
|
||||
this.sendIpcLogMessage({ data: logItem })
|
||||
})
|
||||
this.proxyProcess.stderr.on('data', (data) => {
|
||||
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)
|
||||
@ -97,6 +94,7 @@ export class KubeAuthProxy {
|
||||
if (this.proxyProcess) {
|
||||
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta())
|
||||
this.proxyProcess.kill()
|
||||
this.proxyProcess = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ export function initMenu(windowManager: WindowManager) {
|
||||
|
||||
function navigate(url: string) {
|
||||
const activeClusterId = clusterStore.activeClusterId;
|
||||
const view = windowManager.getView(activeClusterId);
|
||||
const view = windowManager.getClusterView(activeClusterId);
|
||||
if (view) {
|
||||
broadcastIpc({
|
||||
channel: "menu:navigate",
|
||||
|
||||
@ -4,7 +4,7 @@ import { Cluster } from "../cluster"
|
||||
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
|
||||
|
||||
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 {
|
||||
'apiVersion': 'v1',
|
||||
'kind': 'Config',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { autorun, reaction, when } from "mobx";
|
||||
import { autorun, reaction } from "mobx";
|
||||
import { BrowserWindow, shell } from "electron"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import type { ClusterId } from "../common/cluster-store";
|
||||
@ -15,25 +15,23 @@ export class WindowManager {
|
||||
protected disposers: CallableFunction[] = [];
|
||||
protected windowState: windowStateKeeper.State;
|
||||
|
||||
constructor(protected proxyPort: number) {
|
||||
this.splashWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 300,
|
||||
backgroundColor: "#1e2124",
|
||||
center: true,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
show: false,
|
||||
});
|
||||
|
||||
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,
|
||||
@ -42,33 +40,37 @@ export class WindowManager {
|
||||
// auto-destroy views for removed clusters
|
||||
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
|
||||
removedClusters.forEach(cluster => {
|
||||
this.destroyView(cluster.id);
|
||||
this.destroyClusterView(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();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected async handleNoClustersView() {
|
||||
if (!this.noClustersWindow) {
|
||||
this.noClustersWindow = this.initView(undefined);
|
||||
await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`);
|
||||
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();
|
||||
}
|
||||
this.activeView = this.noClustersWindow;
|
||||
this.noClustersWindow.show();
|
||||
this.hideSplash();
|
||||
}
|
||||
|
||||
async showSplash() {
|
||||
await this.splashWindow.loadURL("static://splash.html").catch(() => null)
|
||||
if (!this.splashWindow) {
|
||||
this.splashWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 300,
|
||||
backgroundColor: "#1e2124",
|
||||
center: true,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
await this.splashWindow.loadURL("static://splash.html").catch(Function)
|
||||
this.splashWindow.show();
|
||||
}
|
||||
|
||||
@ -76,19 +78,17 @@ export class WindowManager {
|
||||
this.splashWindow.hide();
|
||||
}
|
||||
|
||||
getView(clusterId: ClusterId) {
|
||||
getClusterView(clusterId: ClusterId): BrowserWindow {
|
||||
return this.views.get(clusterId);
|
||||
}
|
||||
|
||||
activateView = async (clusterId: ClusterId): Promise<number> => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
if (!cluster) return;
|
||||
try {
|
||||
const prevActiveView = this.activeView;
|
||||
const isLoadedBefore = !!this.getView(clusterId);
|
||||
const view = this.initView(clusterId);
|
||||
const isLoadedBefore = !!this.getClusterView(clusterId);
|
||||
const view = this.initClusterView(clusterId);
|
||||
logger.info(`[WINDOW-MANAGER]: activating cluster view`, {
|
||||
id: view.id,
|
||||
clusterId: cluster.id,
|
||||
@ -97,9 +97,8 @@ export class WindowManager {
|
||||
});
|
||||
if (prevActiveView !== view) {
|
||||
this.activeView = view;
|
||||
cluster.activate(); // refresh + reconnect when required
|
||||
if (!isLoadedBefore) {
|
||||
await when(() => cluster.initialized);
|
||||
await cluster.whenInitialized; // wait for url
|
||||
await view.loadURL(cluster.webContentUrl);
|
||||
this.hideSplash();
|
||||
}
|
||||
@ -119,8 +118,8 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
protected initView(clusterId: ClusterId): BrowserWindow {
|
||||
let view = this.getView(clusterId);
|
||||
protected initClusterView(clusterId: ClusterId): BrowserWindow {
|
||||
let view = this.getClusterView(clusterId);
|
||||
if (!view) {
|
||||
const { width, height, x, y } = this.windowState;
|
||||
view = new BrowserWindow({
|
||||
@ -146,7 +145,7 @@ export class WindowManager {
|
||||
return view;
|
||||
}
|
||||
|
||||
protected destroyView(clusterId: ClusterId) {
|
||||
protected destroyClusterView(clusterId: ClusterId) {
|
||||
const view = this.views.get(clusterId);
|
||||
if (view) {
|
||||
view.destroy();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import "./app.scss";
|
||||
import React from "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 { Notifications } from "./notifications";
|
||||
import { NotFound } from "./+404";
|
||||
@ -28,45 +28,45 @@ 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, landingURL } from "./+landing-page";
|
||||
import { LandingPage, landingRoute } from "./+landing-page";
|
||||
import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
|
||||
import { Workspaces, workspacesRoute } from "./+workspaces";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
import { navigation } from "../navigation";
|
||||
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 { Preferences, preferencesRoute } from "./+preferences";
|
||||
import { ClusterStatus } from "./cluster-manager/cluster-status";
|
||||
import { CubeSpinner } from "./spinner";
|
||||
import { navigate, navigation } from "../navigation";
|
||||
|
||||
@observer
|
||||
export class App extends React.Component {
|
||||
@observable isReady = false;
|
||||
|
||||
@computed get clusterReady() {
|
||||
const clusterId = location.hostname.split(".")[0];
|
||||
return !!clusterStore.getById(clusterId)?.isReady;
|
||||
@computed get clusterReady(): boolean {
|
||||
const cluster = getHostedCluster();
|
||||
if (cluster) {
|
||||
return cluster.initialized && cluster.accessible;
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await clusterIpc.activate.invokeFromRenderer();
|
||||
await clusterIpc.activate.invokeFromRenderer(); // refresh state, reconnect, etc.
|
||||
this.isReady = true;
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.startURL, url => {
|
||||
const redirect = !this.clusterReady || !clusterStore.hasClusters();
|
||||
if (redirect) navigation.replace(url);
|
||||
}, {
|
||||
fireImmediately: true
|
||||
autorun(() => {
|
||||
if (!this.clusterReady) {
|
||||
navigate(clusterStatusURL());
|
||||
} else if (clusterStatusURL() == navigation.getPath()) {
|
||||
navigate("/"); // redirect when cluster accessible
|
||||
}
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
get startURL() {
|
||||
if (!clusterStore.hasClusters()) {
|
||||
return landingURL();
|
||||
}
|
||||
if (!this.clusterReady) {
|
||||
return clusterStatusURL();
|
||||
}
|
||||
|
||||
@ -1,65 +1,76 @@
|
||||
import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy";
|
||||
|
||||
import "./cluster-status.scss"
|
||||
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 { 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 { Button } from "../button";
|
||||
import { cssNames } from "../../utils";
|
||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||
|
||||
@observer
|
||||
export class ClusterStatus extends React.Component {
|
||||
@observable authOutput: string[] = [];
|
||||
@observable hasErrors = false;
|
||||
@observable authOutput: KubeAuthProxyResponse[] = [];
|
||||
@observable isReconnecting = false;
|
||||
|
||||
get cluster() {
|
||||
return clusterStore.activeCluster;
|
||||
@computed get hasErrors() {
|
||||
return this.authOutput.some(({ error }) => error)
|
||||
}
|
||||
|
||||
get clusterId() {
|
||||
return clusterStore.activeClusterId;
|
||||
@computed get cluster() {
|
||||
return getHostedCluster()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.authOutput = ["Connecting ...\n"];
|
||||
ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, { data, stream }: KubeAuthProxyResponse) => {
|
||||
this.authOutput.push(`[${stream}]: ${data}`);
|
||||
if (stream === "stderr") {
|
||||
this.hasErrors = true;
|
||||
}
|
||||
async componentDidMount() {
|
||||
this.authOutput = [{ data: "Connecting ...\n" }];
|
||||
ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res) => {
|
||||
this.authOutput.push(res);
|
||||
})
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ipcRenderer.removeAllListeners(`kube-auth:${this.clusterId}`);
|
||||
ipcRenderer.removeAllListeners(`kube-auth:${this.cluster.id}`);
|
||||
}
|
||||
|
||||
reconnect = () => {
|
||||
this.authOutput = ["Reconnecting ...\n"];
|
||||
clusterIpc.reconnect.invokeFromRenderer(this.clusterId);
|
||||
reconnect = async () => {
|
||||
this.authOutput = [{ data: "Reconnecting ...\n" }];
|
||||
this.isReconnecting = true;
|
||||
await clusterIpc.activate.invokeFromRenderer();
|
||||
this.isReconnecting = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { authOutput, cluster, hasErrors } = this;
|
||||
const isDisconnected = !!cluster.disconnected;
|
||||
const isInactive = hasErrors || isDisconnected;
|
||||
return (
|
||||
<div className="ClusterStatus flex column gaps">
|
||||
{!hasErrors && <Icon material="cloud_queue"/>}
|
||||
{hasErrors && <Icon material="cloud_off" className="error"/>}
|
||||
{cluster && <h2>{cluster.contextName}</h2>}
|
||||
<pre className="kube-auth-out">
|
||||
{authOutput.map((data, index) => {
|
||||
const error = data.startsWith("[stderr]");
|
||||
return <p key={index} className={cssNames({ error })}>{data}</p>
|
||||
})}
|
||||
</pre>
|
||||
{hasErrors && (
|
||||
{isInactive && (
|
||||
<Icon
|
||||
material="cloud_off"
|
||||
className={cssNames({ error: hasErrors })}
|
||||
/>
|
||||
)}
|
||||
<h2>
|
||||
{cluster.contextName}
|
||||
</h2>
|
||||
{!isDisconnected && (
|
||||
<pre className="kube-auth-out">
|
||||
{authOutput.map(({ data, error }, index) => {
|
||||
return <p key={index} className={cssNames({ error })}>{data}</p>
|
||||
})}
|
||||
</pre>
|
||||
)}
|
||||
{isInactive && (
|
||||
<Button
|
||||
primary className="box center"
|
||||
primary
|
||||
label="Reconnect"
|
||||
className="box center"
|
||||
onClick={this.reconnect}
|
||||
waiting={this.isReconnecting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -20,6 +20,7 @@ import { landingURL } from "../+landing-page";
|
||||
import { Tooltip, TooltipContent } from "../tooltip";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
||||
import { clusterStatusURL } from "./cluster-status.route";
|
||||
|
||||
// fixme: allow to rearrange clusters with drag&drop
|
||||
|
||||
@ -54,9 +55,9 @@ export class ClustersMenu extends React.Component<Props> {
|
||||
if (cluster.online) {
|
||||
menu.append(new MenuItem({
|
||||
label: _i18n._(t`Disconnect`),
|
||||
click: () => {
|
||||
navigate(landingURL());
|
||||
clusterIpc.disconnect.invokeFromRenderer();
|
||||
click: async () => {
|
||||
await clusterIpc.disconnect.invokeFromRenderer();
|
||||
navigate(clusterStatusURL());
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user