From dd9b2efdc7ea4c8756c3e59f3e5196f4f3634919 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 25 Jul 2020 14:09:52 +0300 Subject: [PATCH] fix: detect new contexts on startup and show in add-cluster page Signed-off-by: Roman --- src/common/cluster-store.ts | 2 + src/common/kube-helpers.ts | 12 +++- src/common/user-store.ts | 43 ++++++++++---- .../components/+add-cluster/add-cluster.tsx | 59 +++++++++++-------- .../components/+preferences/preferences.tsx | 2 +- .../cluster-manager/clusters-menu.tsx | 4 +- 6 files changed, 83 insertions(+), 39 deletions(-) diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index d059b553aa..d046e98b8b 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -103,6 +103,7 @@ export class ClusterStore extends BaseStore { @action async addCluster(model: ClusterModel, activate = true): Promise { + tracker.event("cluster", "add"); const cluster = new Cluster(model); this.clusters.set(model.id, cluster); if (activate) this.activeClusterId = model.id; @@ -111,6 +112,7 @@ export class ClusterStore extends BaseStore { @action async removeById(clusterId: ClusterId) { + tracker.event("cluster", "remove"); const cluster = this.getById(clusterId); if (cluster) { this.clusters.delete(clusterId); diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index d99ac1cb6a..67bedf70c0 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -1,6 +1,6 @@ import { app, remote } from "electron"; import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node" -import { ensureDirSync, writeFileSync } from "fs-extra"; +import { ensureDirSync, readFile, writeFileSync } from "fs-extra"; import path from "path" import os from "os" import yaml from "js-yaml" @@ -150,3 +150,13 @@ export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig | writeFileSync(kubeConfigFile, kubeConfigContents); return kubeConfigFile; } + +export async function getKubeConfigLocal(): Promise { + try { + const configFile = path.join(process.env.HOME, '.kube', 'config'); + return readFile(configFile, "utf8"); + } catch (err) { + logger.debug(`Cannot read local kube-config: ${err}`) + return ""; + } +} diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 35c62d3b09..d6f7a7e73c 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -4,10 +4,9 @@ import { action, observable, reaction, toJS } from "mobx"; import { BaseStore } from "./base-store"; import migrations from "../migrations/user-store" import { getAppVersion } from "./utils/app-version"; +import { getKubeConfigLocal, loadConfig } from "./kube-helpers"; import { tracker } from "./tracker"; -// fixme: detect new contexts from .kube/config since last open - export interface UserStoreModel { lastSeenAppVersion: string; seenContexts: string[]; @@ -27,7 +26,7 @@ export class UserStore extends BaseStore { private constructor() { super({ - // configName: "lens-user-store", // todo: migrate from default filename + // configName: "lens-user-store", // todo: migrate from default "config.json" migrations: migrations, }); @@ -35,18 +34,21 @@ export class UserStore extends BaseStore { reaction(() => this.preferences.allowTelemetry, allowed => { tracker.event("telemetry", allowed ? "enabled" : "disabled"); }); + + // refresh new contexts + this.whenLoaded.then(this.refreshNewContexts); + reaction(() => this.seenContexts.size, this.refreshNewContexts); } @observable lastSeenAppVersion = "0.0.0" - @observable seenContexts: string[] = []; - @observable newContexts: string[] = []; + @observable seenContexts = observable.set(); + @observable newContexts = observable.set(); @observable preferences: UserPreferences = { allowTelemetry: true, allowUntrustedCAs: false, colorTheme: UserStore.defaultTheme, downloadMirror: "default", - httpsProxy: "", }; get isNewVersion() { @@ -64,22 +66,43 @@ export class UserStore extends BaseStore { this.lastSeenAppVersion = getAppVersion(); } + protected refreshNewContexts = async () => { + const kubeConfig = await getKubeConfigLocal(); + if (kubeConfig) { + this.newContexts.clear(); + const localContexts = loadConfig(kubeConfig).getContexts(); + localContexts.forEach(({ name }) => { + if (!this.seenContexts.has(name)) { + this.newContexts.add(name); + } + }) + } + } + + @action + markNewContextsAsSeen() { + const { seenContexts, newContexts } = this; + this.seenContexts.replace([...seenContexts, ...newContexts]); + this.newContexts.clear(); + } + @action protected fromStore(data: Partial = {}) { const { lastSeenAppVersion, seenContexts = [], preferences } = data if (lastSeenAppVersion) { this.lastSeenAppVersion = lastSeenAppVersion; } - this.seenContexts = seenContexts; + this.seenContexts.replace(seenContexts); Object.assign(this.preferences, preferences); } - toJSON() { - return toJS({ + toJSON(): UserStoreModel { + const model: UserStoreModel = { lastSeenAppVersion: this.lastSeenAppVersion, seenContexts: Array.from(this.seenContexts), preferences: this.preferences, - }, { + } + return toJS(model, { recurseEverything: true, }) } diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 821fff2971..9f2a27d6e3 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -1,23 +1,22 @@ import "./add-cluster.scss" -import path from "path"; -import fs from "fs-extra"; import React, { Fragment } from "react"; import { observer } from "mobx-react"; import { computed, observable } from "mobx"; -import { Select, SelectOption } from "../select"; +import { KubeConfig } from "@kubernetes/client-node"; import { t, Trans } from "@lingui/macro"; -import { Input } from "../input"; import { _i18n } from "../../i18n"; +import { Select, SelectOption } from "../select"; +import { Input } from "../input"; import { AceEditor } from "../ace-editor"; import { Button } from "../button"; -import { KubeConfig } from "@kubernetes/client-node"; -import { loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers"; -import { tracker } from "../../../common/tracker"; +import { Icon } from "../icon"; +import { WizardLayout } from "../layout/wizard-layout"; +import { getKubeConfigLocal, loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers"; import { clusterStore } from "../../../common/cluster-store"; import { workspaceStore } from "../../../common/workspace-store"; import { v4 as uuid } from "uuid" import { navigation } from "../../navigation"; -import { WizardLayout } from "../layout/wizard-layout"; +import { userStore } from "../../../common/user-store"; @observer export class AddCluster extends React.Component { @@ -32,16 +31,15 @@ export class AddCluster extends React.Component { @observable customConfig = "" async componentDidMount() { - const kubeConfig = await this.readLocalKubeConfig(); + const kubeConfig: string = await getKubeConfigLocal() if (kubeConfig) { this.kubeConfig = loadConfig(kubeConfig) this.customConfig = kubeConfig } } - async readLocalKubeConfig(): Promise { - const localPath = path.join(process.env.HOME, '.kube', 'config'); - return fs.readFile(localPath, "utf8").catch(() => null) + componentWillUnmount() { + userStore.markNewContextsAsSeen(); } @computed get isCustom() { @@ -51,18 +49,15 @@ export class AddCluster extends React.Component { @computed get clusterOptions() { const options: SelectOption[] = []; if (this.kubeConfig) { - const contexts = splitConfig(this.kubeConfig) - .filter(kc => !clusterStore.hasContext(kc.currentContext)); - - contexts.forEach(kubeConfig => { - const isNew = false; // fixme: detect new context since last visit - options.push({ - value: kubeConfig, - label: <> - {kubeConfig.currentContext} - {isNew && (new)} - , - }) + splitConfig(this.kubeConfig).forEach(kubeConfig => { + const context = kubeConfig.currentContext; + const hasContext = clusterStore.hasContext(context); + if (!hasContext) { + options.push({ + value: kubeConfig, + label: context, + }); + } }) } options.push({ @@ -72,8 +67,21 @@ export class AddCluster extends React.Component { return options; } + protected formatClusterContextLabel = ({ value, label }: SelectOption) => { + if (value instanceof KubeConfig) { + const context = value.currentContext; + const isNew = userStore.newContexts.has(context); + return ( +
+ {context} + {isNew && } +
+ ) + } + return label; + }; + addCluster = async () => { - tracker.event("cluster", "add"); const { clusterConfig, customConfig, proxyServer } = this; const clusterId = uuid(); this.isWaiting = true @@ -165,6 +173,7 @@ export class AddCluster extends React.Component { value={this.clusterConfig} options={this.clusterOptions} onChange={({ value }: SelectOption) => this.clusterConfig = value} + formatOptionLabel={this.formatClusterContextLabel} />
this.showSettings = !this.showSettings}> diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index ef7ecd372c..02e66b7342 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -172,7 +172,7 @@ export class Preferences extends React.Component {

HTTP Proxy

preferences.httpsProxy = v} /> diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index ec04c8f462..008adf3ffa 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -124,8 +124,8 @@ export class ClustersMenu extends React.Component { - {newContexts.length > 0 && ( - + {newContexts.size > 0 && ( + new}/> )}