diff --git a/package.json b/package.json index 2ea9e4ed06..d453512caa 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ "@types/node": "^12.12.45", "@types/proper-lockfile": "^4.1.1", "@types/tar": "^4.0.3", + "chalk": "^4.1.0", "conf": "^7.0.1", "crypto-js": "^4.0.0", "electron-promise-ipc": "^2.1.0", diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 0935ddc5b9..1e71cdb398 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -52,7 +52,7 @@ export class ClusterStore extends BaseStore { @observable removedClusters = observable.map(); @observable clusters = observable.map(); - @computed get activeCluster(): Cluster { + @computed get activeCluster(): Cluster | null { return this.getById(this.activeClusterId); } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 26868bbe0d..ebce77e52b 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -31,7 +31,6 @@ export class Cluster implements ClusterModel { @observable kubeConfigPath: string; @observable apiUrl: string; // cluster server url @observable kubeProxyUrl: string; // lens-proxy to kube-api url - @observable kubeAuthProxyUrl: string; // auth-proxy to temp kube-config @observable webContentUrl: string; // page content url for loading in renderer @observable online: boolean; @observable accessible: boolean; @@ -59,20 +58,21 @@ export class Cluster implements ClusterModel { async init(port: number) { try { this.contextHandler = new ContextHandler(this); - const contextPort = await this.contextHandler.ensurePort(); - this.kubeAuthProxyUrl = `http://127.0.0.1:${contextPort}`; + this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler); this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`; this.webContentUrl = `http://${this.id}.localhost:${port}`; - this.kubeconfigManager = new KubeconfigManager(this); this.initialized = true; - logger.info(`✅ ️Cluster(${this.id}) init success`, { + logger.info(`✅ ️Cluster init success`, { + id: this.id, serverUrl: this.apiUrl, webContentUrl: this.webContentUrl, kubeProxyUrl: this.kubeProxyUrl, - kubeAuthProxyUrl: this.kubeAuthProxyUrl, }); } catch (err) { - logger.error(`💣 Cluster(${this.id}) init failed: ${err}`); + logger.error(`💣 Cluster init failed: ${err}`, { + id: this.id, + error: err, + }); } } diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index d6310fe5f4..228721887c 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -68,6 +68,11 @@ export class ContextHandler { return this.prometheusPath; } + public async resolveAuthProxyUrl() { + const proxyPort = await this.ensurePort(); + return `http://127.0.0.1:${proxyPort}`; + } + public async getApiTarget(isWatchRequest = false): Promise { if (this.apiTarget && !isWatchRequest) { return this.apiTarget @@ -81,19 +86,14 @@ export class ContextHandler { } protected async newApiTarget(timeout: number): Promise { - await this.ensurePort(); + const proxyUrl = await this.resolveAuthProxyUrl(); return { + target: proxyUrl + this.clusterUrl.path, changeOrigin: true, timeout: timeout, headers: { "Host": this.clusterUrl.hostname, }, - target: { - protocol: "http://", - host: "127.0.0.1", - port: this.proxyPort, - path: this.clusterUrl.path - } } } diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index f888c1dd6f..9d297ed223 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -1,5 +1,6 @@ import type { KubeConfig } from "@kubernetes/client-node"; import type { Cluster } from "./cluster" +import type { ContextHandler } from "./context-handler"; import { app } from "electron" import path from "path" import fs from "fs-extra" @@ -10,18 +11,13 @@ export class KubeconfigManager { protected configDir = app.getPath("temp") protected tempFile: string; - constructor(protected cluster: Cluster) { - if(!cluster.kubeAuthProxyUrl) { - throw new Error(`Cluster's auth proxy url must be initialized`) - } - if (!cluster.contextHandler.proxyPort) { - throw new Error("Context-handler proxy port must be resolved") - } + constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) { this.init(); } protected async init() { try { + await this.contextHandler.ensurePort(); await this.createProxyKubeconfig(); } catch (err) { logger.error(`Failed to created temp config for auth-proxy`, { err }) @@ -37,37 +33,38 @@ export class KubeconfigManager { * This way any user of the config does not need to know anything about the auth etc. details. */ protected async createProxyKubeconfig(): Promise { - const { configDir, cluster } = this; + const { configDir, cluster, contextHandler } = this; const { contextName, kubeConfigPath, id } = cluster; const tempFile = path.join(configDir, `kubeconfig-${id}`); const kubeConfig = loadConfig(kubeConfigPath); - const proxyUser = "proxy"; const proxyConfig: Partial = { currentContext: contextName, clusters: [ { name: contextName, - server: cluster.kubeAuthProxyUrl, + server: await contextHandler.resolveAuthProxyUrl(), skipTLSVerify: undefined, } ], users: [ - { name: proxyUser }, + { name: "proxy" }, ], contexts: [ { - user: proxyUser, + user: "proxy", name: contextName, cluster: contextName, namespace: kubeConfig.getContextObject(contextName).namespace, } ] }; + + // write const configYaml = dumpConfigYaml(proxyConfig); fs.ensureDir(path.dirname(tempFile)); fs.writeFileSync(tempFile, configYaml); - logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`); this.tempFile = tempFile; + logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`); return tempFile; } diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 4030ce149c..099c0090f1 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -42,10 +42,13 @@ export class WindowManager { }); }), // auto-show active cluster view - reaction(() => clusterStore.activeClusterId, clusterId => { - this.activateView(clusterId); + reaction(() => clusterStore.activeCluster, activeCluster => { + if (activeCluster) { + this.activateView(activeCluster.id); + } }, { fireImmediately: true, + delay: 250, }) ) } @@ -65,10 +68,7 @@ export class WindowManager { async activateView(clusterId: ClusterId) { const cluster = clusterStore.getById(clusterId); - if (!cluster) { - logger.error(`Can't show a view for non-existing cluster(${clusterId})`); - return; - } + if (!cluster) return; try { const activeView = this.activeView; const isFresh = !this.getView(clusterId); diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts index 81b759ec6d..8aaf1f3e1b 100644 --- a/src/migrations/cluster-store/3.6.0-beta.1.ts +++ b/src/migrations/cluster-store/3.6.0-beta.1.ts @@ -22,6 +22,7 @@ export default migration({ try { // take the embedded kubeconfig and dump it into a file cluster.kubeConfigPath = writeEmbeddedKubeConfig(cluster.id, cluster.kubeConfig) + delete cluster.kubeConfig; migratedClusters.push(cluster) } catch (error) { log(`Failed to migrate Kubeconfig for cluster "${cluster.id}"`, error) diff --git a/src/renderer/components/+cluster-settings/cluster-icon.scss b/src/renderer/components/+cluster-settings/cluster-icon.scss index 9c95fd8c2d..b58d31983a 100644 --- a/src/renderer/components/+cluster-settings/cluster-icon.scss +++ b/src/renderer/components/+cluster-settings/cluster-icon.scss @@ -1,7 +1,19 @@ .ClusterIcon { - position: relative; --size: 40px; + position: relative; + opacity: .75; + border-radius: $radius; + cursor: pointer; + + &.interactive { + &:hover { + opacity: 1; + background-color: #fff; + box-shadow: 0 0 0 $radius #fff; + } + } + > img { width: var(--size); height: var(--size); diff --git a/src/renderer/components/+cluster-settings/cluster-icon.tsx b/src/renderer/components/+cluster-settings/cluster-icon.tsx index 68feba6444..981414370b 100644 --- a/src/renderer/components/+cluster-settings/cluster-icon.tsx +++ b/src/renderer/components/+cluster-settings/cluster-icon.tsx @@ -2,7 +2,8 @@ import "./cluster-icon.scss" import React, { DOMAttributes } from "react"; import { observer } from "mobx-react"; -import { Hashicon, HashiconProps } from "@emeraldpay/hashicon-react"; +import { Params as HashiconParams } from "@emeraldpay/hashicon"; +import { Hashicon } from "@emeraldpay/hashicon-react"; import { Cluster } from "../../../main/cluster"; import { cssNames, IClassName } from "../../utils"; import { Badge } from "../badge"; @@ -11,12 +12,13 @@ interface Props extends DOMAttributes { cluster: Cluster; className?: IClassName; errorClass?: IClassName; - showErrorCount?: boolean; - options?: HashiconProps["options"] + showErrors?: boolean; + interactive?: boolean; + options?: HashiconParams; } const defaultProps: Partial = { - showErrorCount: true, + showErrors: true, }; @observer @@ -24,14 +26,17 @@ export class ClusterIcon extends React.Component { static defaultProps = defaultProps as object; render() { - const { className, cluster, showErrorCount, errorClass, options, children, ...elemProps } = this.props; + const { className: cName, cluster, showErrors, errorClass, options, interactive, children, ...elemProps } = this.props; const { isAdmin, eventCount, preferences } = cluster; const { clusterName, icon } = preferences; + const className = cssNames("ClusterIcon flex inline", cName, { + interactive: interactive || !!this.props.onClick, + }); return ( -
+
{icon && {clusterName}/} {!icon && } - {showErrorCount && isAdmin && eventCount > 0 && ( + {showErrors && isAdmin && eventCount > 0 && ( = 1000 ? Math.ceil(eventCount / 1000) * 1000 + "+" : eventCount} diff --git a/src/renderer/components/cluster-manager/clusters-menu.scss b/src/renderer/components/cluster-manager/clusters-menu.scss index 022dd766c6..241af09bbd 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.scss +++ b/src/renderer/components/cluster-manager/clusters-menu.scss @@ -1,13 +1,11 @@ .ClustersMenu { @include hidden-scrollbar; - $menuBgc: #252729; + + --flex-gap: #{$padding * 2}; + --menu-bgc: #252729; padding: $padding * 1.5; - background: $menuBgc; - - > * { - cursor: pointer; - } + background: var(--menu-bgc); .add-cluster { position: relative; @@ -15,7 +13,7 @@ .Icon.add { border-radius: $radius; padding: $padding / 3; - color: $menuBgc !important; + color: var(--menu-bgc) !important; background: white !important; font-weight: bold; cursor: pointer; diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index 3a791d43ce..68577e3faf 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -63,8 +63,8 @@ export class ClustersMenu extends React.Component { return ( this.selectCluster(cluster)} onContextMenu={() => this.showContextMenu(cluster)} diff --git a/yarn.lock b/yarn.lock index b5448bc5d9..5d94051a79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3442,6 +3442,14 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"