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();
|
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 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(() => {
|
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() {
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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,25 +15,23 @@ 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) {
|
||||||
this.splashWindow = new BrowserWindow({
|
|
||||||
width: 500,
|
|
||||||
height: 300,
|
|
||||||
backgroundColor: "#1e2124",
|
|
||||||
center: true,
|
|
||||||
frame: false,
|
|
||||||
resizable: false,
|
|
||||||
show: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manage main window size and position with state persistence
|
// Manage main window size and position with state persistence
|
||||||
this.windowState = windowStateKeeper({
|
this.windowState = windowStateKeeper({
|
||||||
defaultHeight: 900,
|
defaultHeight: 900,
|
||||||
defaultWidth: 1440,
|
defaultWidth: 1440,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Show while app not ready
|
||||||
|
if (showSplash) {
|
||||||
|
this.showSplash();
|
||||||
|
}
|
||||||
|
|
||||||
// Manage reactive state
|
// Manage reactive state
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
|
// show and hide "no-clusters" window when necessary
|
||||||
|
autorun(this.handleNoClustersView),
|
||||||
|
|
||||||
// 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, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
@ -42,33 +40,37 @@ export class WindowManager {
|
|||||||
// auto-destroy views for removed clusters
|
// auto-destroy views for removed clusters
|
||||||
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
|
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
|
||||||
removedClusters.forEach(cluster => {
|
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()
|
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() {
|
protected handleNoClustersView = async () => {
|
||||||
if (!this.noClustersWindow) {
|
this.noClustersWindow = this.initClusterView(null);
|
||||||
this.noClustersWindow = this.initView(undefined);
|
await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`).catch(Function);
|
||||||
await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`);
|
if (!clusterStore.hasClusters()) {
|
||||||
|
this.activeView = this.noClustersWindow;
|
||||||
|
this.noClustersWindow.show();
|
||||||
|
this.hideSplash();
|
||||||
}
|
}
|
||||||
this.activeView = this.noClustersWindow;
|
|
||||||
this.noClustersWindow.show();
|
|
||||||
this.hideSplash();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async showSplash() {
|
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();
|
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();
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
<pre className="kube-auth-out">
|
className={cssNames({ error: hasErrors })}
|
||||||
{authOutput.map((data, index) => {
|
/>
|
||||||
const error = data.startsWith("[stderr]");
|
)}
|
||||||
return <p key={index} className={cssNames({ error })}>{data}</p>
|
<h2>
|
||||||
})}
|
{cluster.contextName}
|
||||||
</pre>
|
</h2>
|
||||||
{hasErrors && (
|
{!isDisconnected && (
|
||||||
|
<pre className="kube-auth-out">
|
||||||
|
{authOutput.map(({ data, error }, index) => {
|
||||||
|
return <p key={index} className={cssNames({ error })}>{data}</p>
|
||||||
|
})}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user