1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

fix: detect new contexts on startup and show in add-cluster page

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-25 14:09:52 +03:00 committed by Sebastian Malton
parent b357b43e9f
commit 59969719d6
6 changed files with 83 additions and 39 deletions

View File

@ -103,6 +103,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
async addCluster(model: ClusterModel, activate = true): Promise<Cluster> { async addCluster(model: ClusterModel, activate = true): Promise<Cluster> {
tracker.event("cluster", "add");
const cluster = new Cluster(model); const cluster = new Cluster(model);
this.clusters.set(model.id, cluster); this.clusters.set(model.id, cluster);
if (activate) this.activeClusterId = model.id; if (activate) this.activeClusterId = model.id;
@ -111,6 +112,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
async removeById(clusterId: ClusterId) { async removeById(clusterId: ClusterId) {
tracker.event("cluster", "remove");
const cluster = this.getById(clusterId); const cluster = this.getById(clusterId);
if (cluster) { if (cluster) {
this.clusters.delete(clusterId); this.clusters.delete(clusterId);

View File

@ -1,6 +1,6 @@
import { app, remote } from "electron"; import { app, remote } from "electron";
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node" 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 path from "path"
import os from "os" import os from "os"
import yaml from "js-yaml" import yaml from "js-yaml"
@ -150,3 +150,13 @@ export function saveConfigToAppFiles(clusterId: string, kubeConfig: KubeConfig |
writeFileSync(kubeConfigFile, kubeConfigContents); writeFileSync(kubeConfigFile, kubeConfigContents);
return kubeConfigFile; return kubeConfigFile;
} }
export async function getKubeConfigLocal(): Promise<string> {
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 "";
}
}

View File

@ -4,10 +4,9 @@ import { action, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store" import migrations from "../migrations/user-store"
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
import { getKubeConfigLocal, loadConfig } from "./kube-helpers";
import { tracker } from "./tracker"; import { tracker } from "./tracker";
// fixme: detect new contexts from .kube/config since last open
export interface UserStoreModel { export interface UserStoreModel {
lastSeenAppVersion: string; lastSeenAppVersion: string;
seenContexts: string[]; seenContexts: string[];
@ -27,7 +26,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
private constructor() { private constructor() {
super({ super({
// configName: "lens-user-store", // todo: migrate from default filename // configName: "lens-user-store", // todo: migrate from default "config.json"
migrations: migrations, migrations: migrations,
}); });
@ -35,18 +34,21 @@ export class UserStore extends BaseStore<UserStoreModel> {
reaction(() => this.preferences.allowTelemetry, allowed => { reaction(() => this.preferences.allowTelemetry, allowed => {
tracker.event("telemetry", allowed ? "enabled" : "disabled"); 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 lastSeenAppVersion = "0.0.0"
@observable seenContexts: string[] = []; @observable seenContexts = observable.set<string>();
@observable newContexts: string[] = []; @observable newContexts = observable.set<string>();
@observable preferences: UserPreferences = { @observable preferences: UserPreferences = {
allowTelemetry: true, allowTelemetry: true,
allowUntrustedCAs: false, allowUntrustedCAs: false,
colorTheme: UserStore.defaultTheme, colorTheme: UserStore.defaultTheme,
downloadMirror: "default", downloadMirror: "default",
httpsProxy: "",
}; };
get isNewVersion() { get isNewVersion() {
@ -64,22 +66,43 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.lastSeenAppVersion = getAppVersion(); 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 @action
protected fromStore(data: Partial<UserStoreModel> = {}) { protected fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences } = data const { lastSeenAppVersion, seenContexts = [], preferences } = data
if (lastSeenAppVersion) { if (lastSeenAppVersion) {
this.lastSeenAppVersion = lastSeenAppVersion; this.lastSeenAppVersion = lastSeenAppVersion;
} }
this.seenContexts = seenContexts; this.seenContexts.replace(seenContexts);
Object.assign(this.preferences, preferences); Object.assign(this.preferences, preferences);
} }
toJSON() { toJSON(): UserStoreModel {
return toJS({ const model: UserStoreModel = {
lastSeenAppVersion: this.lastSeenAppVersion, lastSeenAppVersion: this.lastSeenAppVersion,
seenContexts: Array.from(this.seenContexts), seenContexts: Array.from(this.seenContexts),
preferences: this.preferences, preferences: this.preferences,
}, { }
return toJS(model, {
recurseEverything: true, recurseEverything: true,
}) })
} }

View File

@ -1,23 +1,22 @@
import "./add-cluster.scss" import "./add-cluster.scss"
import path from "path";
import fs from "fs-extra";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { computed, observable } from "mobx"; import { computed, observable } from "mobx";
import { Select, SelectOption } from "../select"; import { KubeConfig } from "@kubernetes/client-node";
import { t, Trans } from "@lingui/macro"; import { t, Trans } from "@lingui/macro";
import { Input } from "../input";
import { _i18n } from "../../i18n"; import { _i18n } from "../../i18n";
import { Select, SelectOption } from "../select";
import { Input } from "../input";
import { AceEditor } from "../ace-editor"; import { AceEditor } from "../ace-editor";
import { Button } from "../button"; import { Button } from "../button";
import { KubeConfig } from "@kubernetes/client-node"; import { Icon } from "../icon";
import { loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers"; import { WizardLayout } from "../layout/wizard-layout";
import { tracker } from "../../../common/tracker"; import { getKubeConfigLocal, loadConfig, saveConfigToAppFiles, splitConfig, validateConfig } from "../../../common/kube-helpers";
import { clusterStore } from "../../../common/cluster-store"; import { clusterStore } from "../../../common/cluster-store";
import { workspaceStore } from "../../../common/workspace-store"; import { workspaceStore } from "../../../common/workspace-store";
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { WizardLayout } from "../layout/wizard-layout"; import { userStore } from "../../../common/user-store";
@observer @observer
export class AddCluster extends React.Component { export class AddCluster extends React.Component {
@ -32,16 +31,15 @@ export class AddCluster extends React.Component {
@observable customConfig = "" @observable customConfig = ""
async componentDidMount() { async componentDidMount() {
const kubeConfig = await this.readLocalKubeConfig(); const kubeConfig: string = await getKubeConfigLocal()
if (kubeConfig) { if (kubeConfig) {
this.kubeConfig = loadConfig(kubeConfig) this.kubeConfig = loadConfig(kubeConfig)
this.customConfig = kubeConfig this.customConfig = kubeConfig
} }
} }
async readLocalKubeConfig(): Promise<string> { componentWillUnmount() {
const localPath = path.join(process.env.HOME, '.kube', 'config'); userStore.markNewContextsAsSeen();
return fs.readFile(localPath, "utf8").catch(() => null)
} }
@computed get isCustom() { @computed get isCustom() {
@ -51,18 +49,15 @@ export class AddCluster extends React.Component {
@computed get clusterOptions() { @computed get clusterOptions() {
const options: SelectOption<KubeConfig>[] = []; const options: SelectOption<KubeConfig>[] = [];
if (this.kubeConfig) { if (this.kubeConfig) {
const contexts = splitConfig(this.kubeConfig) splitConfig(this.kubeConfig).forEach(kubeConfig => {
.filter(kc => !clusterStore.hasContext(kc.currentContext)); const context = kubeConfig.currentContext;
const hasContext = clusterStore.hasContext(context);
contexts.forEach(kubeConfig => { if (!hasContext) {
const isNew = false; // fixme: detect new context since last visit options.push({
options.push({ value: kubeConfig,
value: kubeConfig, label: context,
label: <> });
{kubeConfig.currentContext} }
{isNew && <span className="new"> <Trans>(new)</Trans></span>}
</>,
})
}) })
} }
options.push({ options.push({
@ -72,8 +67,21 @@ export class AddCluster extends React.Component {
return options; return options;
} }
protected formatClusterContextLabel = ({ value, label }: SelectOption<KubeConfig>) => {
if (value instanceof KubeConfig) {
const context = value.currentContext;
const isNew = userStore.newContexts.has(context);
return (
<div className="kube-context flex gaps align-center">
<span>{context}</span>
{isNew && <Icon material="fiber_new"/>}
</div>
)
}
return label;
};
addCluster = async () => { addCluster = async () => {
tracker.event("cluster", "add");
const { clusterConfig, customConfig, proxyServer } = this; const { clusterConfig, customConfig, proxyServer } = this;
const clusterId = uuid(); const clusterId = uuid();
this.isWaiting = true this.isWaiting = true
@ -165,6 +173,7 @@ export class AddCluster extends React.Component {
value={this.clusterConfig} value={this.clusterConfig}
options={this.clusterOptions} options={this.clusterOptions}
onChange={({ value }: SelectOption) => this.clusterConfig = value} onChange={({ value }: SelectOption) => this.clusterConfig = value}
formatOptionLabel={this.formatClusterContextLabel}
/> />
<div className="cluster-settings"> <div className="cluster-settings">
<a href="#" onClick={() => this.showSettings = !this.showSettings}> <a href="#" onClick={() => this.showSettings = !this.showSettings}>

View File

@ -172,7 +172,7 @@ export class Preferences extends React.Component {
<h2><Trans>HTTP Proxy</Trans></h2> <h2><Trans>HTTP Proxy</Trans></h2>
<Input <Input
placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)} placeholder={_i18n._(t`Type HTTP proxy url (example: http://proxy.acme.org:8080)`)}
value={preferences.httpsProxy} value={preferences.httpsProxy || ""}
onChange={v => preferences.httpsProxy = v} onChange={v => preferences.httpsProxy = v}
/> />
<small className="hint"> <small className="hint">

View File

@ -124,8 +124,8 @@ export class ClustersMenu extends React.Component<Props> {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<Icon big material="add" id="add-cluster-icon"/> <Icon big material="add" id="add-cluster-icon"/>
{newContexts.length > 0 && ( {newContexts.size > 0 && (
<Badge className="counter" label={newContexts.length}/> <Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
)} )}
</div> </div>
</div> </div>