mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add the ability to sync kube config files (#2567)
* Add the ability to sync kube config files - Will update when the files changes - add KUBECONFIG_SYNC label - fix rebase and change to addObservableSource - move UI to user settings - support shallow folder watching - add some unit tests for the diff-er Signed-off-by: Sebastian Malton <sebastian@malton.name> * responding to review comments Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix tests and add try/catch Signed-off-by: Sebastian Malton <sebastian@malton.name> * always sync c&p folder, remove bad rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix preferences Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix settings saving and catalog view Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix unit tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix synced clusters not connectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * change to non-complete shallow watching Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sizing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Catch readStream errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * don't clear UserStore on non-existant preference field, fix unlinking not removing items from source Signed-off-by: Sebastian Malton <sebastian@malton.name> * change label to file Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
f06d330835
commit
998f7aa934
32 files changed
+1215
-236
No files matched your search
@@ -27,7 +27,7 @@ describe("CatalogEntityRegistry", () => {
|
||||
it ("allows to add an observable source", () => {
|
||||
const source = observable.array([]);
|
||||
|
||||
registry.addSource("test", source);
|
||||
registry.addObservableSource("test", source);
|
||||
expect(registry.items.length).toEqual(0);
|
||||
|
||||
source.push(entity);
|
||||
@@ -38,7 +38,7 @@ describe("CatalogEntityRegistry", () => {
|
||||
it ("added source change triggers reaction", (done) => {
|
||||
const source = observable.array([]);
|
||||
|
||||
registry.addSource("test", source);
|
||||
registry.addObservableSource("test", source);
|
||||
reaction(() => registry.items, () => {
|
||||
done();
|
||||
});
|
||||
@@ -51,7 +51,7 @@ describe("CatalogEntityRegistry", () => {
|
||||
it ("removes source", () => {
|
||||
const source = observable.array([]);
|
||||
|
||||
registry.addSource("test", source);
|
||||
registry.addObservableSource("test", source);
|
||||
source.push(entity);
|
||||
registry.removeSource("test");
|
||||
|
||||
|
||||
@@ -72,55 +72,63 @@ describe("kube helpers", () => {
|
||||
});
|
||||
describe("with default validation options", () => {
|
||||
describe("with valid kubeconfig", () => {
|
||||
it("does not raise exceptions", () => {
|
||||
expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow();
|
||||
it("does not return an error", () => {
|
||||
expect(validateKubeConfig(kc, "valid")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe("with invalid context object", () => {
|
||||
it("it raises exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'");
|
||||
it("returns an error", () => {
|
||||
expect(String(validateKubeConfig(kc, "invalid"))).toEqual(
|
||||
expect.stringContaining("No valid context object provided in kubeconfig for context 'invalid'")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with invalid cluster object", () => {
|
||||
it("it raises exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'");
|
||||
it("returns an error", () => {
|
||||
expect(String(validateKubeConfig(kc, "invalidCluster"))).toEqual(
|
||||
expect.stringContaining("No valid cluster object provided in kubeconfig for context 'invalidCluster'")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with invalid user object", () => {
|
||||
it("it raises exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'");
|
||||
it("returns an error", () => {
|
||||
expect(String(validateKubeConfig(kc, "invalidUser"))).toEqual(
|
||||
expect.stringContaining("No valid user object provided in kubeconfig for context 'invalidUser'")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with invalid exec command", () => {
|
||||
it("it raises exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig");
|
||||
it("returns an error", () => {
|
||||
expect(String(validateKubeConfig(kc, "invalidExec"))).toEqual(
|
||||
expect.stringContaining("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with validateCluster as false", () => {
|
||||
describe("with invalid cluster object", () => {
|
||||
it("does not raise exception", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow();
|
||||
it("does not return an error", () => {
|
||||
expect(validateKubeConfig(kc, "invalidCluster", { validateCluster: false })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with validateUser as false", () => {
|
||||
describe("with invalid user object", () => {
|
||||
it("does not raise exceptions", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
|
||||
it("does not return an error", () => {
|
||||
expect(validateKubeConfig(kc, "invalidUser", { validateUser: false })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with validateExec as false", () => {
|
||||
describe("with invalid exec object", () => {
|
||||
it("does not raise exceptions", () => {
|
||||
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
|
||||
it("does not return an error", () => {
|
||||
expect(validateKubeConfig(kc, "invalidExec", { validateExec: false })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,12 +19,13 @@ import { UserStore } from "../user-store";
|
||||
import { SemVer } from "semver";
|
||||
import electron from "electron";
|
||||
import { stdout, stderr } from "process";
|
||||
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
describe("user store tests", () => {
|
||||
describe("for an empty config", () => {
|
||||
beforeEach(() => {
|
||||
beforeEachWrapped(() => {
|
||||
UserStore.resetInstance();
|
||||
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
||||
|
||||
@@ -90,7 +91,7 @@ describe("user store tests", () => {
|
||||
});
|
||||
|
||||
describe("migrations", () => {
|
||||
beforeEach(() => {
|
||||
beforeEachWrapped(() => {
|
||||
UserStore.resetInstance();
|
||||
mockFs({
|
||||
"tmp": {
|
||||
|
||||
@@ -94,7 +94,7 @@ export class KubernetesClusterCategory extends CatalogCategory {
|
||||
ctx.menuItems.push({
|
||||
icon: "text_snippet",
|
||||
title: "Add from kubeconfig",
|
||||
onClick: async () => {
|
||||
onClick: () => {
|
||||
ctx.navigate("/add-cluster");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { action, computed, observable, IObservableArray } from "mobx";
|
||||
import { action, computed, observable, IComputedValue, IObservableArray } from "mobx";
|
||||
import { CatalogEntity } from "./catalog-entity";
|
||||
import { iter } from "../utils";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
protected sources = observable.map<string, IObservableArray<CatalogEntity>>([], { deep: true });
|
||||
protected sources = observable.map<string, IComputedValue<CatalogEntity[]>>([], { deep: true });
|
||||
|
||||
@action addSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
@action addObservableSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
this.sources.set(id, computed(() => source));
|
||||
}
|
||||
|
||||
@action addComputedSource(id: string, source: IComputedValue<CatalogEntity[]>) {
|
||||
this.sources.set(id, source);
|
||||
}
|
||||
|
||||
@@ -13,7 +18,7 @@ export class CatalogEntityRegistry {
|
||||
}
|
||||
|
||||
@computed get items(): CatalogEntity[] {
|
||||
return Array.from(this.sources.values()).flat();
|
||||
return Array.from(iter.flatMap(this.sources.values(), source => source.get()));
|
||||
}
|
||||
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
|
||||
@@ -65,7 +65,7 @@ export interface CatalogEntityContextMenu {
|
||||
icon: string;
|
||||
title: string;
|
||||
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
|
||||
onClick: () => Promise<void>;
|
||||
onClick: () => void | Promise<void>;
|
||||
confirm?: {
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ export interface ClusterStoreModel {
|
||||
|
||||
export type ClusterId = string;
|
||||
|
||||
export interface UpdateClusterModel extends Omit<ClusterModel, "id"> {
|
||||
id?: ClusterId;
|
||||
}
|
||||
|
||||
export interface ClusterModel {
|
||||
/** Unique id for a cluster */
|
||||
id: ClusterId;
|
||||
@@ -94,8 +98,12 @@ export interface ClusterPrometheusPreferences {
|
||||
}
|
||||
|
||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
static get storedKubeConfigFolder(): string {
|
||||
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
|
||||
}
|
||||
|
||||
static getCustomKubeConfigPath(clusterId: ClusterId): string {
|
||||
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId);
|
||||
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
|
||||
}
|
||||
|
||||
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
|
||||
@@ -259,18 +267,18 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
addCluster(model: ClusterModel | Cluster): Cluster {
|
||||
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
||||
appEventBus.emit({ name: "cluster", action: "add" });
|
||||
let cluster = model as Cluster;
|
||||
|
||||
if (!(model instanceof Cluster)) {
|
||||
cluster = new Cluster(model);
|
||||
}
|
||||
const cluster = clusterOrModel instanceof Cluster
|
||||
? clusterOrModel
|
||||
: new Cluster(clusterOrModel);
|
||||
|
||||
if (!cluster.isManaged) {
|
||||
cluster.enabled = true;
|
||||
}
|
||||
this.clusters.set(model.id, cluster);
|
||||
|
||||
this.clusters.set(cluster.id, cluster);
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
+71
-41
@@ -6,7 +6,7 @@ import yaml from "js-yaml";
|
||||
import logger from "../main/logger";
|
||||
import commandExists from "command-exists";
|
||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
||||
import { newClusters, newContexts, newUsers } from "@kubernetes/client-node/dist/config_types";
|
||||
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
|
||||
|
||||
export type KubeConfigValidationOpts = {
|
||||
validateCluster?: boolean;
|
||||
@@ -28,11 +28,26 @@ function readResolvedPathSync(filePath: string): string {
|
||||
return fse.readFileSync(path.resolve(resolveTilde(filePath)), "utf8");
|
||||
}
|
||||
|
||||
function checkRawContext(rawContext: any): boolean {
|
||||
return rawContext.name && rawContext.context?.cluster && rawContext.context?.user;
|
||||
function checkRawCluster(rawCluster: any): boolean {
|
||||
return Boolean(rawCluster?.name && rawCluster?.cluster?.server);
|
||||
}
|
||||
|
||||
function loadToOptions(rawYaml: string): any {
|
||||
function checkRawUser(rawUser: any): boolean {
|
||||
return Boolean(rawUser?.name);
|
||||
}
|
||||
|
||||
function checkRawContext(rawContext: any): boolean {
|
||||
return Boolean(rawContext.name && rawContext.context?.cluster && rawContext.context?.user);
|
||||
}
|
||||
|
||||
export interface KubeConfigOptions {
|
||||
clusters: Cluster[];
|
||||
users: User[];
|
||||
contexts: Context[];
|
||||
currentContext: string;
|
||||
}
|
||||
|
||||
function loadToOptions(rawYaml: string): KubeConfigOptions {
|
||||
const obj = yaml.safeLoad(rawYaml);
|
||||
|
||||
if (typeof obj !== "object" || !obj) {
|
||||
@@ -40,16 +55,14 @@ function loadToOptions(rawYaml: string): any {
|
||||
}
|
||||
|
||||
const { clusters: rawClusters, users: rawUsers, contexts: rawContexts, "current-context": currentContext } = obj;
|
||||
const clusters = newClusters(rawClusters);
|
||||
const users = newUsers(rawUsers);
|
||||
const clusters = newClusters(rawClusters?.filter(checkRawCluster));
|
||||
const users = newUsers(rawUsers?.filter(checkRawUser));
|
||||
const contexts = newContexts(rawContexts?.filter(checkRawContext));
|
||||
|
||||
return { clusters, users, contexts, currentContext };
|
||||
}
|
||||
|
||||
export function loadConfig(pathOrContent?: string): KubeConfig {
|
||||
const content = fse.pathExistsSync(pathOrContent) ? readResolvedPathSync(pathOrContent) : pathOrContent;
|
||||
const options = loadToOptions(content);
|
||||
export function loadFromOptions(options: KubeConfigOptions): KubeConfig {
|
||||
const kc = new KubeConfig();
|
||||
|
||||
// need to load using the kubernetes client to generate a kubeconfig object
|
||||
@@ -58,6 +71,18 @@ export function loadConfig(pathOrContent?: string): KubeConfig {
|
||||
return kc;
|
||||
}
|
||||
|
||||
export function loadConfig(pathOrContent?: string): KubeConfig {
|
||||
return loadConfigFromString(
|
||||
fse.pathExistsSync(pathOrContent)
|
||||
? readResolvedPathSync(pathOrContent)
|
||||
: pathOrContent
|
||||
);
|
||||
}
|
||||
|
||||
export function loadConfigFromString(content: string): KubeConfig {
|
||||
return loadFromOptions(loadToOptions(content));
|
||||
}
|
||||
|
||||
/**
|
||||
* KubeConfig is valid when there's at least one of each defined:
|
||||
* - User
|
||||
@@ -181,42 +206,47 @@ export function getNodeWarningConditions(node: V1Node) {
|
||||
|
||||
/**
|
||||
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
|
||||
*
|
||||
* Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes
|
||||
*/
|
||||
export function validateKubeConfig (config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}) {
|
||||
// we only receive a single context, cluster & user object here so lets validate them as this
|
||||
// will be called when we add a new cluster to Lens
|
||||
export function validateKubeConfig(config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}): Error | undefined {
|
||||
try {
|
||||
// we only receive a single context, cluster & user object here so lets validate them as this
|
||||
// will be called when we add a new cluster to Lens
|
||||
|
||||
const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts;
|
||||
const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts;
|
||||
|
||||
const contextObject = config.getContextObject(contextName);
|
||||
const contextObject = config.getContextObject(contextName);
|
||||
|
||||
// Validate the Context Object
|
||||
if (!contextObject) {
|
||||
throw new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
// Validate the Cluster Object
|
||||
if (validateCluster && !config.getCluster(contextObject.cluster)) {
|
||||
throw new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
const user = config.getUser(contextObject.user);
|
||||
|
||||
// Validate the User Object
|
||||
if (validateUser && !user) {
|
||||
throw new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
// Validate exec command if present
|
||||
if (validateExec && user?.exec) {
|
||||
const execCommand = user.exec["command"];
|
||||
// check if the command is absolute or not
|
||||
const isAbsolute = path.isAbsolute(execCommand);
|
||||
|
||||
// validate the exec struct in the user object, start with the command field
|
||||
if (!commandExists.sync(execCommand)) {
|
||||
logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${contextName} not found`);
|
||||
throw new ExecValidationNotFoundError(execCommand, isAbsolute);
|
||||
// Validate the Context Object
|
||||
if (!contextObject) {
|
||||
return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
// Validate the Cluster Object
|
||||
if (validateCluster && !config.getCluster(contextObject.cluster)) {
|
||||
return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
const user = config.getUser(contextObject.user);
|
||||
|
||||
// Validate the User Object
|
||||
if (validateUser && !user) {
|
||||
return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
||||
}
|
||||
|
||||
// Validate exec command if present
|
||||
if (validateExec && user?.exec) {
|
||||
const execCommand = user.exec["command"];
|
||||
// check if the command is absolute or not
|
||||
const isAbsolute = path.isAbsolute(execCommand);
|
||||
|
||||
// validate the exec struct in the user object, start with the command field
|
||||
if (!commandExists.sync(execCommand)) {
|
||||
return new ExecValidationNotFoundError(execCommand, isAbsolute);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
+58
-32
@@ -11,6 +11,7 @@ import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
|
||||
import { appEventBus } from "./event-bus";
|
||||
import logger from "../main/logger";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { fileNameMigration } from "../migrations/user-store";
|
||||
import { ObservableToggleSet } from "../renderer/utils";
|
||||
|
||||
@@ -21,6 +22,12 @@ export interface UserStoreModel {
|
||||
preferences: UserPreferencesModel;
|
||||
}
|
||||
|
||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export interface KubeconfigSyncValue {}
|
||||
|
||||
export interface UserPreferencesModel {
|
||||
httpsProxy?: string;
|
||||
shell?: string;
|
||||
@@ -34,6 +41,7 @@ export interface UserPreferencesModel {
|
||||
kubectlBinariesPath?: string;
|
||||
openAtLogin?: boolean;
|
||||
hiddenTableColumns?: [string, string[]][];
|
||||
syncKubeconfigEntries?: KubeconfigSyncEntry[];
|
||||
}
|
||||
|
||||
export class UserStore extends BaseStore<UserStoreModel> {
|
||||
@@ -44,8 +52,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
configName: "lens-user-store",
|
||||
migrations,
|
||||
});
|
||||
|
||||
this.handleOnLoad();
|
||||
}
|
||||
|
||||
@observable lastSeenAppVersion = "0.0.0";
|
||||
@@ -71,14 +77,31 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
*/
|
||||
@observable downloadKubectlBinaries = true;
|
||||
@observable openAtLogin = false;
|
||||
|
||||
/**
|
||||
* The column IDs under each configurable table ID that have been configured
|
||||
* to not be shown
|
||||
*/
|
||||
hiddenTableColumns = observable.map<string, ObservableToggleSet<string>>();
|
||||
|
||||
protected async handleOnLoad() {
|
||||
await this.whenLoaded;
|
||||
/**
|
||||
* The set of file/folder paths to be synced
|
||||
*/
|
||||
syncKubeconfigEntries = observable.map<string, KubeconfigSyncValue>([
|
||||
[path.join(os.homedir(), ".kube"), {}]
|
||||
]);
|
||||
|
||||
async load(): Promise<void> {
|
||||
/**
|
||||
* This has to be here before the call to `new Config` in `super.load()`
|
||||
* as we have to make sure that file is in the expected place for that call
|
||||
*/
|
||||
await fileNameMigration();
|
||||
await super.load();
|
||||
|
||||
// refresh new contexts
|
||||
this.refreshNewContexts();
|
||||
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
|
||||
await this.refreshNewContexts();
|
||||
reaction(() => this.kubeConfigPath, () => this.refreshNewContexts());
|
||||
|
||||
if (app) {
|
||||
// track telemetry availability
|
||||
@@ -99,16 +122,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
}
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
/**
|
||||
* This has to be here before the call to `new Config` in `super.load()`
|
||||
* as we have to make sure that file is in the expected place for that call
|
||||
*/
|
||||
await fileNameMigration();
|
||||
|
||||
return super.load();
|
||||
}
|
||||
|
||||
@computed get isNewVersion() {
|
||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||
}
|
||||
@@ -171,7 +184,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
this.localeTimezone = tz;
|
||||
}
|
||||
|
||||
protected refreshNewContexts = async () => {
|
||||
protected async refreshNewContexts() {
|
||||
try {
|
||||
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
||||
|
||||
@@ -186,7 +199,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
logger.error(err);
|
||||
this.resetKubeConfigPath();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
markNewContextsAsSeen() {
|
||||
@@ -225,37 +238,50 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
this.kubectlBinariesPath = preferences.kubectlBinariesPath;
|
||||
this.openAtLogin = preferences.openAtLogin;
|
||||
|
||||
this.hiddenTableColumns.clear();
|
||||
if (preferences.hiddenTableColumns) {
|
||||
this.hiddenTableColumns.replace(
|
||||
preferences.hiddenTableColumns
|
||||
.map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)])
|
||||
);
|
||||
}
|
||||
|
||||
for (const [tableId, columnIds] of preferences.hiddenTableColumns ?? []) {
|
||||
this.hiddenTableColumns.set(tableId, new ObservableToggleSet(columnIds));
|
||||
if (preferences.syncKubeconfigEntries) {
|
||||
this.syncKubeconfigEntries.replace(
|
||||
preferences.syncKubeconfigEntries.map(({ filePath, ...rest }) => [filePath, rest])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): UserStoreModel {
|
||||
const hiddenTableColumns: [string, string[]][] = [];
|
||||
const syncKubeconfigEntries: KubeconfigSyncEntry[] = [];
|
||||
|
||||
for (const [key, values] of this.hiddenTableColumns.entries()) {
|
||||
hiddenTableColumns.push([key, Array.from(values)]);
|
||||
}
|
||||
|
||||
for (const [filePath, rest] of this.syncKubeconfigEntries) {
|
||||
syncKubeconfigEntries.push({ filePath, ...rest });
|
||||
}
|
||||
|
||||
const model: UserStoreModel = {
|
||||
kubeConfigPath: this.kubeConfigPath,
|
||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||
seenContexts: Array.from(this.seenContexts),
|
||||
preferences: {
|
||||
httpsProxy: this.httpsProxy,
|
||||
shell: this.shell,
|
||||
colorTheme: this.colorTheme,
|
||||
localeTimezone: this.localeTimezone,
|
||||
allowUntrustedCAs: this.allowUntrustedCAs,
|
||||
allowTelemetry: this.allowTelemetry,
|
||||
downloadMirror: this.downloadMirror,
|
||||
downloadKubectlBinaries: this.downloadKubectlBinaries,
|
||||
downloadBinariesPath: this.downloadBinariesPath,
|
||||
kubectlBinariesPath: this.kubectlBinariesPath,
|
||||
openAtLogin: this.openAtLogin,
|
||||
httpsProxy: toJS(this.httpsProxy),
|
||||
shell: toJS(this.shell),
|
||||
colorTheme: toJS(this.colorTheme),
|
||||
localeTimezone: toJS(this.localeTimezone),
|
||||
allowUntrustedCAs: toJS(this.allowUntrustedCAs),
|
||||
allowTelemetry: toJS(this.allowTelemetry),
|
||||
downloadMirror: toJS(this.downloadMirror),
|
||||
downloadKubectlBinaries: toJS(this.downloadKubectlBinaries),
|
||||
downloadBinariesPath: toJS(this.downloadBinariesPath),
|
||||
kubectlBinariesPath: toJS(this.kubectlBinariesPath),
|
||||
openAtLogin: toJS(this.openAtLogin),
|
||||
hiddenTableColumns,
|
||||
syncKubeconfigEntries,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx";
|
||||
|
||||
export class ExtendedMap<K, V> extends Map<K, V> {
|
||||
constructor(protected getDefault: () => V, entries?: readonly (readonly [K, V])[] | null) {
|
||||
super(entries);
|
||||
}
|
||||
|
||||
getOrInsert(key: K, val: V): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return this.set(key, val).get(key);
|
||||
}
|
||||
|
||||
getOrInsertWith(key: K, getVal: () => V): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return this.set(key, getVal()).get(key);
|
||||
}
|
||||
|
||||
getOrDefault(key: K): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return this.set(key, this.getDefault()).get(key);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtendedObservableMap<K, V> extends ObservableMap<K, V> {
|
||||
constructor(protected getDefault: () => V, initialData?: IObservableMapInitialValues<K, V>, enhancer?: IEnhancer<V>, name?: string) {
|
||||
super(initialData, enhancer, name);
|
||||
}
|
||||
|
||||
@action
|
||||
getOrInsert(key: K, val: V): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return this.set(key, val).get(key);
|
||||
}
|
||||
|
||||
@action
|
||||
getOrInsertWith(key: K, getVal: () => V): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return this.set(key, getVal()).get(key);
|
||||
}
|
||||
|
||||
@action
|
||||
getOrDefault(key: K): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return this.set(key, this.getDefault()).get(key);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export * from "./disposer";
|
||||
export * from "./disposer";
|
||||
export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./extended-map";
|
||||
export * from "./getRandId";
|
||||
export * from "./openExternal";
|
||||
export * from "./reject-promise";
|
||||
|
||||
@@ -23,3 +23,67 @@ export function* take<T>(src: Iterable<T>, n: number): Iterable<T> {
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||
* result of `fn` for each item.
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that is called for each value
|
||||
*/
|
||||
export function* map<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
|
||||
for (const from of src) {
|
||||
yield fn(from);
|
||||
}
|
||||
}
|
||||
|
||||
export function* flatMap<T, U>(src: Iterable<T>, fn: (from: T) => Iterable<U>): Iterable<U> {
|
||||
for (const from of src) {
|
||||
yield* fn(from);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||
* items that return a `truthy` value from `fn`.
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that is called for each value
|
||||
*/
|
||||
export function* filter<T>(src: Iterable<T>, fn: (from: T) => any): Iterable<T> {
|
||||
for (const from of src) {
|
||||
if (fn(from)) {
|
||||
yield from;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||
* result of `fn` when it is `truthy`
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that is called for each value
|
||||
*/
|
||||
export function* filterMap<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
|
||||
for (const from of src) {
|
||||
const res = fn(from);
|
||||
|
||||
if (res) {
|
||||
yield res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new iterator that iterates (lazily) over its input and yields the
|
||||
* result of `fn` when it is not null or undefined
|
||||
* @param src A type that can be iterated over
|
||||
* @param fn The function that is called for each value
|
||||
*/
|
||||
export function* filterMapStrict<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
|
||||
for (const from of src) {
|
||||
const res = fn(from);
|
||||
|
||||
if (res != null) {
|
||||
yield res;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user