cluster-status -- part 2

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman committed 2020-07-20 19:00:07 +03:00
1 parent ef3962f8b5
commit 323a4c141e
18 files changed
+319 -379

No files matched your search

+2 -2
View File
@@ -6,7 +6,7 @@ import { action, observable, reaction, runInAction, toJS, when } from "mobx";
import Singleton from "./utils/singleton";
import { getAppVersion } from "./utils/app-version";
import logger from "../main/logger";
import { sendMessage } from "./ipc";
import { broadcastIpc } from "./ipc";
import isEqual from "lodash/isEqual";
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
@@ -118,7 +118,7 @@ export class BaseStore<T = any> extends Singleton {
protected async onModelChange(model: T) {
if (ipcMain) {
this.save(model); // save config file
sendMessage({ channel: this.syncChannel, args: [model] }); // broadcast to renderer views
broadcastIpc({ channel: this.syncChannel, args: [model] }); // broadcast to renderer views
}
// send "update-request" to main-process
if (ipcRenderer) {
+8 -14
View File
@@ -19,11 +19,11 @@ export interface IpcMessageOpts<A extends any[] = any> {
channel: IpcChannel
webContentId?: number; // sends to single webContents view
filter?: (webContent: WebContents) => boolean
timeout?: number; // fixme: support
timeout?: number; // fixme: add support
args?: A;
}
export function sendMessage({ channel, webContentId, filter, args = [] }: IpcMessageOpts) {
export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcMessageOpts) {
const singleView = webContentId ? webContents.fromId(webContentId) : null;
let views = singleView ? [singleView] : webContents.getAllWebContents();
if (filter) {
@@ -37,16 +37,16 @@ export function sendMessage({ channel, webContentId, filter, args = [] }: IpcMes
}
// todo: support timeout + merge with sendMessage?
export async function invokeMessage<T extends any[], R = any>(channel: IpcChannel, ...args: T): Promise<R> {
logger.debug(`[IPC]: invoke channel "${channel}"`, { args });
export async function invokeIpc<R = any>(channel: IpcChannel, ...args: any[]): Promise<R> {
logger.info(`[IPC]: invoke channel "${channel}"`, { args });
return ipcRenderer.invoke(channel, ...args);
}
// todo: make isomorphic api
export function handleMessage<T extends any[]>(channel: IpcChannel, handler: IpcMessageHandler<T>, options: IpcHandleOpts = {}) {
export function handleIpc<T extends any[]>(channel: IpcChannel, handler: IpcMessageHandler<T>, options: IpcHandleOpts = {}) {
const { timeout = 0 } = options;
ipcMain.handle(channel, async (event, ...args: T) => {
logger.debug(`[IPC]: handle "${channel}"`, { args });
logger.info(`[IPC]: handle "${channel}"`, { args });
return new Promise(async (resolve, reject) => {
let timerId;
if (timeout) {
@@ -57,17 +57,11 @@ export function handleMessage<T extends any[]>(channel: IpcChannel, handler: Ipc
}
try {
const result = await handler(...args); // todo: maybe exec in separate thread/worker
resolve(result);
clearTimeout(timerId);
return result;
} catch (err) {
logger.debug(`[IPC]: handling "${channel}" error`, { err });
reject(err);
}
})
})
}
export function handleMessages(messages: Record<string, IpcMessageHandler>, options?: IpcHandleOpts) {
Object.entries(messages).forEach(([channel, handler]) => {
handleMessage(channel, handler, options);
})
}
+58 -27
View File
@@ -1,61 +1,92 @@
import type http from "http"
import { autorun } from "mobx";
import { autorun, reaction } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { ClusterId, clusterStore } from "../common/cluster-store"
import { handleMessage } from "../common/ipc";
import { tracker } from "../common/tracker";
import { Cluster, ClusterIpcEvent } from "./cluster"
import { handleIpc } from "../common/ipc";
import { Cluster, ClusterIpcChannel } from "./cluster"
import logger from "./logger";
import { tracker } from "../common/tracker";
export class ClusterManager {
protected activeClusterId: ClusterId;
constructor(public readonly port: number) {
this.activeClusterId = clusterStore.activeClusterId;
// auto-init clusters
autorun(() => {
clusterStore.clusters.forEach(cluster => {
if (cluster.initialized) return;
cluster.init(port);
logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta());
if (!cluster.initialized) {
logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta());
cluster.init(port);
}
});
});
// auto-bind events for active cluster
reaction(() => clusterStore.activeCluster, activeCluster => {
const prevCluster = clusterStore.getById(this.activeClusterId);
if (prevCluster) {
prevCluster.unbindEvents();
}
if (activeCluster) {
this.activeClusterId = activeCluster.id;
activeCluster.bindEvents();
activeCluster.refreshStatus();
}
}, {
fireImmediately: true
});
// auto-stop removed clusters
autorun(() => {
const { removedClusters } = clusterStore;
const meta = Array.from(removedClusters.values()).map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => cluster.destroy());
removedClusters.clear();
if (removedClusters.size > 0) {
const meta = Array.from(removedClusters.values()).map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => cluster.disconnect());
removedClusters.clear();
}
}, {
delay: 250
});
// listen for ipc-events that must be handled *only* in main-process (nodeIntegration=true)
handleMessage(ClusterIpcEvent.STOP, this.stopCluster.bind(this));
// listen for ipc-events that must/can be handled *only* in main-process (nodeIntegration=true)
handleIpc(ClusterIpcChannel.INIT, this.onClusterInit);
handleIpc(ClusterIpcChannel.DISCONNECT, this.onClusterDisconnect);
handleIpc(ClusterIpcChannel.RECONNECT, this.onClusterReconnect);
}
stop() {
clusterStore.clusters.forEach((cluster: Cluster) => {
cluster.stop();
cluster.disconnect();
})
}
protected onClusterInit = async (id = clusterStore.activeClusterId) => {
const cluster = this.getCluster(id);
if (cluster) {
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
tracker.event("cluster", "activate");
await cluster.refreshStatus();
cluster.pushState();
}
}
protected onClusterDisconnect = (id: ClusterId) => {
tracker.event("cluster", "stop");
this.getCluster(id)?.disconnect();
}
protected onClusterReconnect = (id: ClusterId) => {
tracker.event("cluster", "reconnect");
this.getCluster(id)?.reconnect();
}
protected getCluster(id: ClusterId) {
return clusterStore.getById(id);
}
protected stopCluster(clusterId: ClusterId) {
tracker.event("cluster", "stop");
this.getCluster(clusterId)?.destroy();
}
// todo
protected reconnectCluster(clusterId: ClusterId) {
tracker.event("cluster", "reconnect");
logger.info(`[CLUSTER-MANAGER]: reconnect cluster`, {
meta: this.getCluster(clusterId)?.getMeta()
});
}
getClusterForRequest(req: http.IncomingMessage): Cluster {
let cluster: Cluster = null
+33 -31
View File
@@ -1,9 +1,9 @@
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature"
import type { WorkspaceId } from "../common/workspace-store";
import { action, observable, reaction, toJS, when } from "mobx";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { sendMessage } from "../common/ipc";
import { broadcastIpc } from "../common/ipc";
import { ContextHandler } from "./context-handler"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
import { Kubectl } from "./kubectl";
@@ -13,8 +13,9 @@ import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from ".
import request, { RequestPromiseOptions } from "request-promise-native"
import logger from "./logger"
export enum ClusterIpcEvent {
STOP = "cluster:stop",
export enum ClusterIpcChannel {
INIT = "cluster:init",
DISCONNECT = "cluster:disconnect",
RECONNECT = "cluster:reconnect",
}
@@ -43,8 +44,6 @@ export class Cluster implements ClusterModel {
public kubeCtl: Kubectl
public contextHandler: ContextHandler;
protected kubeconfigManager: KubeconfigManager;
public whenReady = when(() => this.initialized);
protected disposers: Function[] = [];
@observable initialized = false;
@@ -69,6 +68,10 @@ 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);
@@ -98,8 +101,7 @@ export class Cluster implements ClusterModel {
}
}
bindEvents(viewId: number) {
if (!this.initialized) return;
bindEvents() {
logger.info(`[CLUSTER]: bind events`, this.getMeta());
const refreshStatusTimer = setInterval(() => this.refreshStatus(), 30000); // every 30s
const refreshEventsTimer = setInterval(() => this.refreshEvents(), 3000); // every 3s
@@ -107,43 +109,35 @@ export class Cluster implements ClusterModel {
this.disposers.push(
() => clearInterval(refreshStatusTimer),
() => clearInterval(refreshEventsTimer),
reaction(() => this.getState(), clusterState => {
sendMessage({
channel: "cluster:state",
webContentId: viewId,
args: [clusterState],
})
}, {
reaction(() => this.getState(), this.pushState, {
fireImmediately: true
})
);
}
unbindEvents() {
if (!this.initialized) return;
logger.info(`[CLUSTER]: unbind events`, this.getMeta());
this.disposers.forEach(dispose => dispose());
this.disposers.length = 0;
}
stop() {
this.contextHandler.stopServer();
// fixme: possibly doesn't work as expected
async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
await this.contextHandler.stopServer();
await this.contextHandler.ensureServer();
}
destroy() {
try {
this.stop();
this.unbindEvents();
this.kubeconfigManager.unlink();
} catch (err) {
logger.error(`[CLUSTER]: destroy() throws: ${err}`, this.getMeta());
}
disconnect() {
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
this.contextHandler.stopServer();
this.unbindEvents();
}
@action
async refreshStatus() {
await this.whenReady;
await when(() => this.initialized);
logger.info(`[CLUSTER]: refreshing status`, this.getMeta());
const connectionStatus = await this.getConnectionStatus();
this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
@@ -191,9 +185,8 @@ export class Cluster implements ClusterModel {
return this.preferences.prometheus?.prefix || ""
}
protected k8sRequest(path: string, options: RequestPromiseOptions = {}) {
protected async k8sRequest(path: string, options: RequestPromiseOptions = {}) {
const apiUrl = this.kubeProxyUrl + path;
logger.debug(`[CLUSTER]: getting request to: ${apiUrl}`);
return request(apiUrl, {
json: true,
timeout: 10000,
@@ -211,7 +204,7 @@ export class Cluster implements ClusterModel {
this.failureReason = null
return ClusterStatus.AccessGranted;
} catch (error) {
logger.error(`Failed to connect cluster "${this.contextName}": ${error.stack}`)
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`)
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials";
@@ -347,6 +340,15 @@ export class Cluster implements ClusterModel {
})
}
pushState = (clusterState = this.getState()) => {
logger.info(`[CLUSTER]: push-state`, clusterState);
broadcastIpc({
// webContentId: viewId, // todo: send to cluster-view only
channel: "cluster:state",
args: [clusterState],
})
}
// get cluster system meta, e.g. use in "logger"
getMeta() {
return {
+16 -16
View File
@@ -12,7 +12,7 @@ import { KubeAuthProxy } from "./kube-auth-proxy"
export class ContextHandler {
public proxyPort: number;
public clusterUrl: UrlWithStringQuery;
protected proxyServer: KubeAuthProxy
protected kubeAuthProxy: KubeAuthProxy
protected apiTarget: httpProxy.ServerOptions
protected prometheusProvider: string
protected prometheusPath: string
@@ -36,7 +36,7 @@ export class ContextHandler {
return `${namespace}/services/${service}:${port}`
}
public async getPrometheusProvider() {
async getPrometheusProvider() {
if (!this.prometheusProvider) {
const service = await this.getPrometheusService()
logger.info(`using ${service.id} as prometheus provider`)
@@ -45,7 +45,7 @@ export class ContextHandler {
return prometheusProviders.find(p => p.id === this.prometheusProvider)
}
public async getPrometheusService(): Promise<PrometheusService> {
async getPrometheusService(): Promise<PrometheusService> {
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api)
@@ -61,19 +61,19 @@ export class ContextHandler {
}
}
public async getPrometheusPath(): Promise<string> {
async getPrometheusPath(): Promise<string> {
if (!this.prometheusPath) {
this.prometheusPath = await this.resolvePrometheusPath()
}
return this.prometheusPath;
}
public async resolveAuthProxyUrl() {
async resolveAuthProxyUrl() {
const proxyPort = await this.ensurePort();
return `http://127.0.0.1:${proxyPort}`;
}
public async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
if (this.apiTarget && !isWatchRequest) {
return this.apiTarget
}
@@ -104,26 +104,26 @@ export class ContextHandler {
return this.proxyPort
}
public async ensureServer() {
if (!this.proxyServer) {
async ensureServer() {
if (!this.kubeAuthProxy) {
await this.ensurePort();
const proxyEnv = Object.assign({}, process.env)
if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
}
this.proxyServer = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv)
await this.proxyServer.run()
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv)
await this.kubeAuthProxy.run()
}
}
public stopServer() {
if (this.proxyServer) {
this.proxyServer.exit()
this.proxyServer = null
stopServer() {
if (this.kubeAuthProxy) {
this.kubeAuthProxy.exit()
this.kubeAuthProxy = null
}
}
public proxyServerError(): string {
return this.proxyServer?.lastError || ""
get proxyLastError(): string {
return this.kubeAuthProxy?.lastError || ""
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import { ChildProcess, spawn } from "child_process"
import { waitUntilUsed } from "tcp-port-used";
import { sendMessage } from "../common/ipc";
import { broadcastIpc } from "../common/ipc";
import type { Cluster } from "./cluster"
import { bundledKubectl, Kubectl } from "./kubectl"
import logger from "./logger"
@@ -85,8 +85,8 @@ export class KubeAuthProxy {
protected async sendIpcLogMessage(res: KubeAuthProxyResponse) {
const channel = `kube-auth:${this.cluster.id}`
logger.debug(`[KUBE-AUTH]: output for ${channel}`, { ...res, meta: this.cluster.getMeta() });
sendMessage({
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
broadcastIpc({
// webContentId: null, // todo: send a message only to single cluster's window
channel: channel,
args: [res],
@@ -95,7 +95,7 @@ export class KubeAuthProxy {
public exit() {
if (this.proxyProcess) {
logger.debug(`Stopping local proxy: ${this.cluster.contextName}`)
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta())
this.proxyProcess.kill()
}
}
+7 -14
View File
@@ -52,21 +52,17 @@ export class LensProxy {
protected createProxy(): httpProxy {
const proxy = httpProxy.createProxyServer();
proxy.on("proxyRes", (proxyRes, req, res) => {
if (req.method !== "GET") {
return;
}
if (proxyRes.statusCode === 502) {
const cluster = this.clusterManager.getClusterForRequest(req)
if (cluster && cluster.contextHandler.proxyServerError()) {
res.writeHead(proxyRes.statusCode, {
"Content-Type": "text/plain"
})
res.end(cluster.contextHandler.proxyServerError())
return
const proxyError = cluster?.contextHandler.proxyLastError;
if (proxyError) {
return res.writeHead(502).end(proxyError);
}
}
if (req.method !== "GET") {
return
}
const reqId = this.getRequestId(req);
if (this.retryCounters.has(reqId)) {
logger.debug(`Resetting proxy retry cache for url: ${reqId}`);
@@ -92,10 +88,7 @@ export class LensProxy {
}
}
}
res.writeHead(500, {
'Content-Type': 'text/plain'
})
res.end('Oops, something went wrong.')
res.writeHead(500).end("Oops, something went wrong.")
})
return proxy;
+3 -18
View File
@@ -1,4 +1,4 @@
import { autorun, reaction } from "mobx";
import { autorun, reaction, when } from "mobx";
import { BrowserWindow, shell } from "electron"
import windowStateKeeper from "electron-window-state"
import type { ClusterId } from "../common/cluster-store";
@@ -9,7 +9,6 @@ import logger from "./logger";
// fixme: remove switching view delay on first load
export class WindowManager {
protected activeClusterId: ClusterId;
protected activeView: BrowserWindow;
protected views = new Map<ClusterId, BrowserWindow>();
protected disposers: CallableFunction[] = [];
@@ -36,21 +35,7 @@ export class WindowManager {
// Manage reactive state
this.disposers.push(
// auto-show active cluster window and subscribe for push-events
reaction(() => clusterStore.activeCluster, async activeCluster => {
if (this.activeClusterId) {
const prevCluster = clusterStore.getById(this.activeClusterId);
if (prevCluster) prevCluster.unbindEvents();
this.activeClusterId = null;
}
if (activeCluster) {
this.activeClusterId = activeCluster.id;
const viewId = await this.activateView(activeCluster.id);
if (viewId) {
await activeCluster.refreshStatus();
activeCluster.bindEvents(viewId);
}
}
}, {
reaction(() => clusterStore.activeClusterId, clusterId => this.activateView(clusterId), {
fireImmediately: true,
}),
@@ -108,7 +93,7 @@ export class WindowManager {
if (activeView !== view) {
this.activeView = view;
if (!isLoadedBefore) {
await cluster.whenReady;
await when(() => cluster.initialized);
await view.loadURL(cluster.webContentUrl);
this.hideSplash();
}
@@ -1,159 +0,0 @@
<template>
<div class="content">
<div class="h-100">
<div class="wrapper" v-if="status === 'LOADING'">
<cube-spinner text="" />
<div class="auth-output">
<!-- eslint-disable-next-line vue/no-v-html -->
<pre class="auth-output" v-html="authOutput" />
</div>
</div>
<div class="wrapper" v-if="status === 'ERROR'">
<div class="error">
<i class="material-icons">{{ error_icon }}</i>
<div class="text-center">
<h2>{{ cluster.preferences.clusterName }}</h2>
<!-- eslint-disable-next-line vue/no-v-html -->
<pre v-html="authOutput" />
<b-button variant="link" @click="tryAgain">
Reconnect
</b-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import CubeSpinner from "@/_vue/components/CubeSpinner";
import { tracker } from "../../../common/tracker"
export default {
name: "ClusterPage",
components: {
CubeSpinner
},
data(){
return {
authOutput: ""
}
},
computed: {
cluster: function() {
return this.$store.getters.clusterById(this.$route.params.id);
},
online: function() {
if (!this.cluster) { return false }
return this.cluster.online;
},
accessible: function() {
if (!this.cluster) { return false }
return this.cluster.accessible;
},
lens: function() {
return this.$store.getters.lensById(this.cluster.id);
},
status: function() {
if (this.cluster) {
if (this.cluster.accessible && this.lens.loaded === true) {
return "SUCCESS";
} else if (this.cluster.accessible === false) {
return "ERROR";
}
return "LOADING";
}
return "ERROR";
},
error_icon: function() {
if (!this.cluster.online) {
return "cloud_off"
} else {
return "https"
}
}
},
methods: {
tryAgain: function() {
this.authOutput = ""
this.cluster.accessible = null
setTimeout(() => {
this.loadLens()
}, 1000)
},
loadLens: function() {
this.authOutput = "Connecting ...\n";
this.$promiseIpc.on(`kube-auth:${this.cluster.id}`, (output) => {
this.authOutput += output.data;
})
this.toggleLens();
return this.$store.dispatch("refineCluster", this.$route.params.id);
},
lensLoaded: function() {
console.log("lens loaded")
this.lens.loaded = true;
this.$store.commit("updateLens", this.lens);
},
// Called only when online state changes
toggleLens: function() {
if (!this.lens) { return }
if (this.accessible) {
setTimeout(this.activateLens, 0); // see: https://github.com/electron/electron/issues/10016
} else {
this.hideLens();
}
},
activateLens: async function() {
console.log("activate lens")
if (!this.lens.webview) {
console.log("creating an iframe")
const webview = document.createElement('iframe');
webview.addEventListener('load', this.lensLoaded);
webview.src = this.cluster.url;
this.lens.webview = webview;
}
this.$store.dispatch("attachWebview", this.lens);
tracker.event("cluster", "open");
},
hideLens: function() {
this.$store.dispatch("hideWebviews");
}
},
created() {
this.loadLens();
},
destroyed() {
this.hideLens();
},
watch: {
"$route": "loadLens",
"online": "toggleLens",
"cluster": "toggleLens",
"accessible": function(newStatus, oldStatus) {
console.log("accessible watch, vals:", newStatus, oldStatus);
if(newStatus === false) { // accessble == false
tracker.event("cluster", "open-failed");
}
},
}
};
</script>
<style scoped lang="scss">
div.auth-output {
padding-top: 250px;
width: 70%;
pre {
height: 100px;
text-align: center;
}
}
.error {
width: 90%;
}
pre {
font-size: 80%;
overflow: auto;
max-height: 150px;
}
</style>
+21 -24
View File
@@ -1,5 +1,5 @@
import "./app.scss";
import React, { Fragment } from "react";
import React from "react";
import { observer } from "mobx-react";
import { i18nStore } from "../i18n";
import { configStore } from "../config.store";
@@ -34,11 +34,10 @@ import { LandingPage, landingRoute, landingURL } from "./+landing-page";
import { clusterStore } from "../../common/cluster-store";
import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings";
import { Workspaces, workspacesRoute } from "./+workspaces";
import { ErrorBoundary } from "./error-boundary";
@observer
export class App extends React.Component {
static rootElem = document.getElementById('app');
static async init() {
await i18nStore.init();
await configStore.init();
@@ -57,27 +56,25 @@ export class App extends React.Component {
render() {
return (
<Fragment>
<ErrorBoundary>
<Switch>
<Switch>
<Route component={LandingPage} {...landingRoute}/>
<Route component={AddCluster} {...addClusterRoute}/>
<Route component={Workspaces} {...workspacesRoute}/>
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
<Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/>
<Route component={Config} {...configRoute}/>
<Route component={Network} {...networkRoute}/>
<Route component={Storage} {...storageRoute}/>
<Route component={Namespaces} {...namespacesRoute}/>
<Route component={Events} {...eventRoute}/>
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch>
<Route component={LandingPage} {...landingRoute}/>
<Route component={AddCluster} {...addClusterRoute}/>
<Route component={Workspaces} {...workspacesRoute}/>
<Route component={ClusterSettings} {...clusterSettingsRoute}/>
<Route component={Cluster} {...clusterRoute}/>
<Route component={Nodes} {...nodesRoute}/>
<Route component={Workloads} {...workloadsRoute}/>
<Route component={Config} {...configRoute}/>
<Route component={Network} {...networkRoute}/>
<Route component={Storage} {...storageRoute}/>
<Route component={Namespaces} {...namespacesRoute}/>
<Route component={Events} {...eventRoute}/>
<Route component={CustomResources} {...crdRoute}/>
<Route component={UserManagement} {...usersManagementRoute}/>
<Route component={Apps} {...appsRoute}/>
<Redirect exact from="/" to={this.startURL}/>
<Route component={NotFound}/>
</Switch>
<KubeObjectDetails/>
<Notifications/>
@@ -86,7 +83,7 @@ export class App extends React.Component {
<AddRoleBindingDialog/>
<PodLogsDialog/>
<DeploymentScaleDialog/>
</Fragment>
</ErrorBoundary>
)
}
}
@@ -1,16 +1,40 @@
import "./cluster-manager.scss"
import React from "react";
import { observer } from "mobx-react";
import { computed } from "mobx";
import { App } from "../app";
import { ClusterStatus } from "./cluster-status";
import { ClustersMenu } from "./clusters-menu";
import { BottomBar } from "./bottom-bar";
import { App } from "../app";
import { cssNames, IClassName } from "../../utils";
import { invokeIpc } from "../../../common/ipc";
import { ClusterIpcChannel } from "../../../main/cluster";
import { clusterStore } from "../../../common/cluster-store";
interface Props {
className?: IClassName;
contentClass?: IClassName;
}
@observer
export class ClusterManager extends React.Component<Props> {
@computed get isReady() {
return clusterStore.activeCluster?.isReady
}
async componentDidMount() {
await invokeIpc(ClusterIpcChannel.INIT)
await App.init();
}
export class ClusterManager extends React.Component {
render() {
const { className, contentClass } = this.props;
return (
<div className="ClusterManager">
<div className={cssNames("ClusterManager", className)}>
<div id="draggable-top"/>
<div id="lens-view">
<App/>
<div id="lens-view" className={cssNames("flex", contentClass)}>
{this.isReady && <App/>}
{!this.isReady && <ClusterStatus/>}
</div>
<ClustersMenu/>
<BottomBar/>
@@ -1,3 +1,19 @@
.ClusterStatus {
--flex-gap: #{$padding * 2};
min-width: 350px;
margin: auto;
text-align: center;
pre {
@include custom-scrollbar;
max-width: 70vw;
max-height: 40vh;
//text-align: left;
}
.Icon {
--size: 70px;
margin: auto;
}
}
@@ -1,29 +1,32 @@
import "./cluster-manager.scss"
import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy";
import { Cluster, ClusterIpcEvent } from "../../../main/cluster";
import "./cluster-status.scss"
import React from "react";
import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy";
import { ClusterIpcChannel } from "../../../main/cluster";
import { invokeIpc } from "../../../common/ipc";
import { clusterStore } from "../../../common/cluster-store";
import { ipcRenderer } from "electron";
import { computed, observable } from "mobx";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { Icon } from "../icon";
import { Button } from "../button";
import { Trans } from "@lingui/macro";
interface Props {
cluster: Cluster;
}
import { cssNames } from "../../utils";
@observer
export class ClusterStatus extends React.Component<Props> {
@observable authProxyOutput = "Connecting ...\n"
export class ClusterStatus extends React.Component {
@observable authOutput: string[] = [];
@computed get clusterId() {
return this.props.cluster.id;
get cluster() {
return clusterStore.activeCluster;
}
get clusterId() {
return clusterStore.activeClusterId;
}
componentDidMount() {
ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, authResponse: KubeAuthProxyResponse) => {
this.authProxyOutput += authResponse.data;
this.authOutput = ["Connecting ...\n"];
ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, { data, stream }: KubeAuthProxyResponse) => {
this.authOutput.push(`[${stream}]: ${data}`);
})
}
@@ -31,22 +34,32 @@ export class ClusterStatus extends React.Component<Props> {
ipcRenderer.removeAllListeners(`kube-auth:${this.clusterId}`);
}
reconnectCluster = () => {
ipcRenderer.send(ClusterIpcEvent.RECONNECT, this.clusterId);
reconnect = () => {
this.authOutput = ["Reconnecting ...\n"];
invokeIpc(ClusterIpcChannel.RECONNECT, this.clusterId);
}
render() {
const { authProxyOutput } = this;
const { contextName, online } = this.props.cluster;
const { authOutput, cluster } = this;
const isError = cluster?.accessible === false;
return (
<div className="ClusterStatus flex column">
<Icon sticker className="status-icon" material={online ? "https" : "cloud_off"}/>
<h2>{contextName}</h2>
<pre className="kube-auth-stdout">{authProxyOutput}</pre>
<Button
primary label={<Trans>Reconnect</Trans>}
onClick={this.reconnectCluster}
/>
<div className="ClusterStatus flex column gaps">
{!isError && <Icon material="cloud_queue"/>}
{isError && <Icon material="cloud_off" className="error"/>}
<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>
{isError && (
<Button
primary className="box center"
label="Reconnect"
onClick={this.reconnect}
/>
)}
</div>
)
}
@@ -51,7 +51,7 @@ export class ClustersMenu extends React.Component<Props> {
label: _i18n._(t`Settings`),
click: () => navigate(clusterSettingsURL())
}));
if (cluster.initialized) {
if (cluster.online) {
menu.append(new MenuItem({
label: _i18n._(t`Disconnect`),
click: () => {
+3 -4
View File
@@ -10,7 +10,6 @@ import { I18nProvider } from "@lingui/react";
import { browserHistory } from "./navigation";
import { isMac } from "../common/vars";
import { _i18n } from "./i18n";
import { App } from "./components/app";
import { ClusterManager } from "./components/cluster-manager";
import { ErrorBoundary } from "./components/error-boundary";
import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
@@ -19,14 +18,14 @@ import { Preferences, preferencesRoute } from "./components/+preferences";
@observer
class LensApp extends React.Component {
static async init() {
App.rootElem.classList.toggle("is-mac", isMac);
await Promise.all([
userStore.load(),
workspaceStore.load(),
clusterStore.load(),
]);
await App.init();
render(<LensApp/>, App.rootElem);
const elem = document.getElementById("app");
elem.classList.toggle("is-mac", isMac);
render(<LensApp/>, elem);
}
render() {