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:
parent
0489ee25b5
commit
dd9b2efdc7
@ -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);
|
||||||
|
|||||||
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user