From 240dfad1672b2ea80ada87bebf5b17247ac87ee0 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 9 Jun 2022 13:39:37 +0300 Subject: [PATCH 01/64] Enable csp on lens proxy (#5581) * enable csp on lens proxy Signed-off-by: Jari Kolehmainen * move csp default value to package.json Signed-off-by: Jari Kolehmainen --- package.json | 3 ++- src/common/vars.ts | 1 + src/main/lens-proxy/lens-proxy.ts | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2f652eb980..c34198e158 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "k8sProxyVersion": "0.2.1", "bundledKubectlVersion": "1.23.3", "bundledHelmVersion": "3.7.2", - "sentryDsn": "" + "sentryDsn": "", + "contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src *" }, "engines": { "node": ">=16 <17" diff --git a/src/common/vars.ts b/src/common/vars.ts index e11e49a7a2..32eda45db6 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -143,3 +143,4 @@ export const appSemVer = new SemVer(packageInfo.version); export const docsUrl = "https://docs.k8slens.dev/main/" as string; export const sentryDsn = packageInfo.config?.sentryDsn ?? ""; +export const contentSecurityPolicy = packageInfo.config?.contentSecurityPolicy ?? ""; diff --git a/src/main/lens-proxy/lens-proxy.ts b/src/main/lens-proxy/lens-proxy.ts index 571cbea7b4..5e58104d60 100644 --- a/src/main/lens-proxy/lens-proxy.ts +++ b/src/main/lens-proxy/lens-proxy.ts @@ -7,7 +7,7 @@ import net from "net"; import type http from "http"; import spdy from "spdy"; import type httpProxy from "http-proxy"; -import { apiPrefix, apiKubePrefix } from "../../common/vars"; +import { apiPrefix, apiKubePrefix, contentSecurityPolicy } from "../../common/vars"; import type { Router } from "../router/router"; import type { ClusterContextHandler } from "../context-handler/context-handler"; import logger from "../logger"; @@ -239,6 +239,10 @@ export class LensProxy { } } + if (contentSecurityPolicy) { + res.setHeader("Content-Security-Policy", contentSecurityPolicy); + } + this.dependencies.router.route(cluster, req, res); } } From ef6f4a5bfa916bab6ec086d5222a9f7f2e119e2d Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Thu, 9 Jun 2022 15:12:57 +0300 Subject: [PATCH 02/64] Bump injectable for faster unit tests (#5587) Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- package.json | 8 ++++---- yarn.lock | 42 +++++++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index c34198e158..1b1414d2a5 100644 --- a/package.json +++ b/package.json @@ -208,10 +208,10 @@ "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.16.3", "@material-ui/styles": "^4.11.5", - "@ogre-tools/injectable": "7.0.0", - "@ogre-tools/injectable-react": "7.0.0", - "@ogre-tools/fp": "7.0.0", - "@ogre-tools/injectable-extension-for-auto-registration": "7.0.0", + "@ogre-tools/injectable": "7.1.0", + "@ogre-tools/injectable-react": "7.1.0", + "@ogre-tools/fp": "7.1.0", + "@ogre-tools/injectable-extension-for-auto-registration": "7.1.0", "@sentry/electron": "^3.0.7", "@sentry/integrations": "^6.19.3", "@types/circular-dependency-plugin": "5.0.5", diff --git a/yarn.lock b/yarn.lock index 120613934c..8c519e766f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1102,37 +1102,37 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@ogre-tools/fp@7.0.0", "@ogre-tools/fp@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-7.0.0.tgz#55cd32cc2fcf0505fa0d3ebfd45eb0a9bbb9554c" - integrity sha512-vmd+Ctr9pTSulWWiYaJT4Ca2vkq1MVQvwZ42hJq+LK/tgC4vVMf6G13EMHwhRlhPyrro1/5NeN2kf6SlhgrVOg== +"@ogre-tools/fp@7.1.0", "@ogre-tools/fp@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-7.1.0.tgz#63bdd23e82d4d0f3cfffbf575017e4dac893f590" + integrity sha512-lhXreCXr1mlyvNf+YXvqBcIvL2ZKswYJXK1hx2ieeQLmgyvDs6xGio6h+6bHf1kJhvM55PfBATAJ3DyyiUfwiA== dependencies: lodash "^4.17.21" -"@ogre-tools/injectable-extension-for-auto-registration@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-auto-registration/-/injectable-extension-for-auto-registration-7.0.0.tgz#2144417dd3b3c10afe232661e11d7d0e292a3e6b" - integrity sha512-11RVfzMIBIS/29EUnrhRl/WhwWTYi2cJCAEyh2NnPBV7m51wS40kE/ZjDNn9s37Mfsxc8wNX0aUKwMphXFosVw== +"@ogre-tools/injectable-extension-for-auto-registration@7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-auto-registration/-/injectable-extension-for-auto-registration-7.1.0.tgz#eb4aabee04fff1c4e353e4b5e805639e5e8923d5" + integrity sha512-xEnXF2iAxYOCj46HymDIjO85zsunONmMD8CThks46pyUD7kTOaf7c9tClEUFdXQTmCAfU3UFxcAxDjyJynoSTg== dependencies: - "@ogre-tools/fp" "^7.0.0" - "@ogre-tools/injectable" "^7.0.0" + "@ogre-tools/fp" "^7.1.0" + "@ogre-tools/injectable" "^7.1.0" lodash "^4.17.21" -"@ogre-tools/injectable-react@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-7.0.0.tgz#09c45fbf9a904673a6a0b450cb9a6a7c1110ab14" - integrity sha512-L/NlM4nzIg0FOfH/5T3wYBsPUxALkpt+HaZSOcgzCM9fJnaSgYv/FwdAqshg+nA3KngPDYIMkAUdS+j3uq1yEA== +"@ogre-tools/injectable-react@7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-7.1.0.tgz#afb60951e7e22b59921eb5ce5d04e3be93fe119e" + integrity sha512-JDTZR+1IhFUAPjlzP7IwMRysFBVjpQke4Y/7Ddy7MhOF+WtfFx8pOnuWFWKQRlU2FS4PsMpCG09Jgy09j+Qa8Q== dependencies: - "@ogre-tools/fp" "^7.0.0" - "@ogre-tools/injectable" "^7.0.0" + "@ogre-tools/fp" "^7.1.0" + "@ogre-tools/injectable" "^7.1.0" lodash "^4.17.21" -"@ogre-tools/injectable@7.0.0", "@ogre-tools/injectable@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-7.0.0.tgz#25e6168ba3781f6b562bfbb06d1d54c976e9c94d" - integrity sha512-yfdNUU/q7Oy+bXnQN1lRD2wzt81xqGpf37C4iWfarZ+5GF4sjGi9w4Wuxe1s24rl2rpHZtcQ0j9bEEX51ifQHA== +"@ogre-tools/injectable@7.1.0", "@ogre-tools/injectable@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-7.1.0.tgz#4137d630720245e9bc8635a0f62888878f711da5" + integrity sha512-EN82trTh80TuSE0Gr1Gk4oC/Mj3Od//kWH2r+0Z3Np554e4zOgW6lyEOQlxaGIm77taGSvHq9B9cO7lI9hJonQ== dependencies: - "@ogre-tools/fp" "^7.0.0" + "@ogre-tools/fp" "^7.1.0" lodash "^4.17.21" "@panva/asn1.js@^1.0.0": From c54d59f2058f2097abee415a0f8a85dfee5001f9 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 9 Jun 2022 16:55:01 -0400 Subject: [PATCH 03/64] Fix cluster.k8s.io/v1alpha1/clusters CRD not being marked as Namespaced (#5593) --- src/common/k8s-api/endpoints/cluster.api.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/common/k8s-api/endpoints/cluster.api.ts b/src/common/k8s-api/endpoints/cluster.api.ts index 53e12800b0..082f0adb09 100644 --- a/src/common/k8s-api/endpoints/cluster.api.ts +++ b/src/common/k8s-api/endpoints/cluster.api.ts @@ -10,7 +10,14 @@ import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; export class ClusterApi extends KubeApi { + /** + * @deprecated This field is legacy and never used. + */ static kind = "Cluster"; + + /** + * @deprecated This field is legacy and never used. + */ static namespaced = true; constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) { @@ -104,6 +111,7 @@ export interface Cluster { export class Cluster extends KubeObject { static kind = "Cluster"; static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"; + static namespaced = true; getStatus() { if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING; From 3d64425df20df13e12fddb3c5a1a6f2581801e27 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 10 Jun 2022 13:23:25 +0300 Subject: [PATCH 04/64] allow to load images with data:base64 urls (#5599) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b1414d2a5..b30076ec10 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "bundledKubectlVersion": "1.23.3", "bundledHelmVersion": "3.7.2", "sentryDsn": "", - "contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src *" + "contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:" }, "engines": { "node": ">=16 <17" From 5420780ae0af5da14c0dbcd5bba073e7d38e26e5 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 10 Jun 2022 06:41:29 -0400 Subject: [PATCH 05/64] Fix changes to InputValidator to resolve accidental breaking changes (#5403) * Fix changes to InputValidator to resolve accidental breaking changes Signed-off-by: Sebastian Malton * Show changes working by removing no longer required empty props objects Signed-off-by: Sebastian Malton * fix type errors Signed-off-by: Sebastian Malton * Fix async validation errors not being displayed - And fix validation errors sometimes being displayed multiple times Signed-off-by: Sebastian Malton * Simplify builders for validators - Replace the boolean type params on `function inputValidator` with a new builder `function inputValidatorWithRequiredProps` to make the code easier to read Signed-off-by: Sebastian Malton * Introduce unionizing functions for input validators Signed-off-by: Sebastian Malton * fix tests Signed-off-by: Sebastian Malton * Remove RequiredProps type param Signed-off-by: Sebastian Malton --- src/common/utils/type-narrowing.ts | 8 ++ .../install-from-input/install-from-input.tsx | 17 +-- .../components/+extensions/install.tsx | 21 ++- .../input/__tests__/input_validators.test.ts | 83 ++++++++++- src/renderer/components/input/input.tsx | 83 +++++------ .../components/input/input_validators.ts | 130 +++++++++++++----- 6 files changed, 244 insertions(+), 98 deletions(-) diff --git a/src/common/utils/type-narrowing.ts b/src/common/utils/type-narrowing.ts index a138b2a0ec..eb5b6996cf 100644 --- a/src/common/utils/type-narrowing.ts +++ b/src/common/utils/type-narrowing.ts @@ -137,6 +137,14 @@ export function hasDefiniteField(field: Field): (val: return (val): val is T & { [f in Field]-?: NonNullable } => val[field] != null; } +export function isPromiseSettledRejected(result: PromiseSettledResult): result is PromiseRejectedResult { + return result.status === "rejected"; +} + +export function isPromiseSettledFulfilled(result: PromiseSettledResult): result is PromiseFulfilledResult { + return result.status === "fulfilled"; +} + export function isErrnoException(error: unknown): error is NodeJS.ErrnoException { return isObject(error) && hasOptionalTypedProperty(error, "code", isString) diff --git a/src/renderer/components/+extensions/install-from-input/install-from-input.tsx b/src/renderer/components/+extensions/install-from-input/install-from-input.tsx index ab85221421..eb5d6febe4 100644 --- a/src/renderer/components/+extensions/install-from-input/install-from-input.tsx +++ b/src/renderer/components/+extensions/install-from-input/install-from-input.tsx @@ -14,7 +14,6 @@ import { readFileNotify } from "../read-file-notify/read-file-notify"; import type { InstallRequest } from "../attempt-install/install-request"; import type { ExtensionInfo } from "../attempt-install-by-info.injectable"; import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; -import { AsyncInputValidationError } from "../../input/input_validators"; export type InstallFromInput = (input: string) => Promise; @@ -34,7 +33,7 @@ export const installFromInput = ({ try { // fixme: improve error messages for non-tar-file URLs - if (InputValidators.isUrl.validate(input, {})) { + if (InputValidators.isUrl.validate(input)) { // install via url disposer = extensionInstallationStateStore.startPreInstall(); const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 }); @@ -44,23 +43,19 @@ export const installFromInput = ({ } try { - await InputValidators.isPath.validate(input, {}); + await InputValidators.isPath.validate(input); // install from system path const fileName = path.basename(input); return await attemptInstall({ fileName, dataP: readFileNotify(input) }); } catch (error) { - if (error instanceof AsyncInputValidationError) { - const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input); + const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input); - if (extNameCaptures) { - const { name, version } = extNameCaptures; + if (extNameCaptures) { + const { name, version } = extNameCaptures; - return await attemptInstallByInfo({ name, version }); - } - } else { - throw error; + return await attemptInstallByInfo({ name, version }); } } diff --git a/src/renderer/components/+extensions/install.tsx b/src/renderer/components/+extensions/install.tsx index 52c0bd8906..7fa192e3a2 100644 --- a/src/renderer/components/+extensions/install.tsx +++ b/src/renderer/components/+extensions/install.tsx @@ -13,10 +13,9 @@ import { Input, InputValidators } from "../input"; import { SubTitle } from "../layout/sub-title"; import { TooltipPosition } from "../tooltip"; import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; -import extensionInstallationStateStoreInjectable - from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; -import { inputValidator } from "../input/input_validators"; +import { unionInputValidatorsAsync } from "../input/input_validators"; export interface InstallProps { installPath: string; @@ -30,18 +29,14 @@ interface Dependencies { extensionInstallationStateStore: ExtensionInstallationStateStore; } -const installInputValidators = [ +const installInputValidator = unionInputValidatorsAsync( + { + message: "Invalid URL, absolute path, or extension name", + }, InputValidators.isUrl, - InputValidators.isPath, InputValidators.isExtensionNameInstall, -]; - -const installInputValidator = inputValidator({ - message: "Invalid URL, absolute path, or extension name", - validate: (value: string, props) => ( - installInputValidators.some(({ validate }) => validate(value, props)) - ), -}); + InputValidators.isPath, +); const NonInjectedInstall: React.FC = ({ installPath, diff --git a/src/renderer/components/input/__tests__/input_validators.test.ts b/src/renderer/components/input/__tests__/input_validators.test.ts index ae45b998dd..cb6a0b3db9 100644 --- a/src/renderer/components/input/__tests__/input_validators.test.ts +++ b/src/renderer/components/input/__tests__/input_validators.test.ts @@ -3,11 +3,86 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { isEmail, isUrl, systemName } from "../input_validators"; +import { isEmail, isUrl, systemName, unionInputValidators, unionInputValidatorsAsync } from "../input_validators"; type TextValidationCase = [string, boolean]; describe("input validation tests", () => { + describe("unionInputValidators()", () => { + const emailOrUrl = unionInputValidators( + { + message: "Not an email or URL", + }, + isEmail, + isUrl, + ); + + it.each([ + "abc@news.com", + "abc@news.co.uk", + ])("Given '%s' is a valid email, emailOrUrl matches", (input) => { + expect(emailOrUrl.validate(input)).toBe(true); + }); + + it.each([ + "https://github-production-registry-package-file-4f11e5.s3.amazonaws.com/307985088/68bbbf00-309f-11eb-8457-a15e4efe9e77?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201127%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20201127T123754Z&X-Amz-Expires=300&X-Amz-Signature=9b8167f00685a20d980224d397892195abc187cdb2934cefb79edcd7ec600f78&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=0&response-content-disposition=filename%3Dstarboard-lens-extension-0.0.1-alpha.1-npm.tgz&response-content-type=application%2Foctet-stream", + "http://www.google.com", + ])("Given '%s' is a valid url, emailOrUrl matches", (input) => { + expect(emailOrUrl.validate(input)).toBe(true); + }); + + it.each([ + "hello", + "57", + ])("Given '%s' is neither a valid email nor URL, emailOrUrl does not match", (input) => { + expect(emailOrUrl.validate(input)).toBe(false); + }); + }); + + describe("unionInputValidatorsAsync()", () => { + const emailOrUrl = unionInputValidatorsAsync( + { + message: "Not an email or URL", + }, + isEmail, + isUrl, + ); + + it.each([ + "abc@news.com", + "abc@news.co.uk", + ])("Given '%s' is a valid email, emailOrUrl matches", async (input) => { + try { + await emailOrUrl.validate(input); + } catch { + fail("Should not throw on valid input"); + } + }); + + it.each([ + "https://github-production-registry-package-file-4f11e5.s3.amazonaws.com/307985088/68bbbf00-309f-11eb-8457-a15e4efe9e77?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201127%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20201127T123754Z&X-Amz-Expires=300&X-Amz-Signature=9b8167f00685a20d980224d397892195abc187cdb2934cefb79edcd7ec600f78&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=0&response-content-disposition=filename%3Dstarboard-lens-extension-0.0.1-alpha.1-npm.tgz&response-content-type=application%2Foctet-stream", + "http://www.google.com", + ])("Given '%s' is a valid url, emailOrUrl matches", async (input) => { + try { + await emailOrUrl.validate(input); + } catch { + fail("Should not throw on valid input"); + } + }); + + it.each([ + "hello", + "57", + ])("Given '%s' is neither a valid email nor URL, emailOrUrl does not match", async (input) => { + try { + await emailOrUrl.validate(input); + fail("Should throw on invalid input"); + } catch { + // We want this to happen + } + }); + }); + describe("isEmail tests", () => { const tests: TextValidationCase[] = [ ["abc@news.com", true], @@ -21,7 +96,7 @@ describe("input validation tests", () => { ]; it.each(tests)("validate %s", (input, output) => { - expect(isEmail.validate(input, {})).toBe(output); + expect(isEmail.validate(input)).toBe(output); }); }); @@ -38,7 +113,7 @@ describe("input validation tests", () => { ]; it.each(cases)("validate %s", (input, output) => { - expect(isUrl.validate(input, {})).toBe(output); + expect(isUrl.validate(input)).toBe(output); }); }); @@ -68,7 +143,7 @@ describe("input validation tests", () => { ]; it.each(tests)("validate %s", (input, output) => { - expect(systemName.validate(input, {})).toBe(output); + expect(systemName.validate(input)).toBe(output); }); }); }); diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx index 9514e796e3..f45c8377a0 100644 --- a/src/renderer/components/input/input.tsx +++ b/src/renderer/components/input/input.tsx @@ -7,20 +7,37 @@ import "./input.scss"; import type { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react"; import React from "react"; -import { autoBind, cssNames, debouncePromise, getRandId } from "../../utils"; +import { autoBind, cssNames, debouncePromise, getRandId, isPromiseSettledFulfilled } from "../../utils"; import { Icon } from "../icon"; import type { TooltipProps } from "../tooltip"; import { Tooltip } from "../tooltip"; import * as Validators from "./input_validators"; -import type { InputValidator } from "./input_validators"; -import isFunction from "lodash/isFunction"; +import type { InputValidator, InputValidation, InputValidationResult, SyncValidationMessage } from "./input_validators"; import uniqueId from "lodash/uniqueId"; import { debounce } from "lodash"; -const { conditionalValidators, ...InputValidators } = Validators; +const { + conditionalValidators, + asyncInputValidator, + inputValidator, + isAsyncValidator, + unionInputValidatorsAsync, + ...InputValidators +} = Validators; -export { InputValidators }; -export type { InputValidator }; +export { + InputValidators, + asyncInputValidator, + inputValidator, + isAsyncValidator, + unionInputValidatorsAsync, +}; +export type { + InputValidator, + InputValidation, + InputValidationResult, + SyncValidationMessage, +}; type InputElement = HTMLInputElement | HTMLTextAreaElement; type InputElementProps = @@ -78,15 +95,11 @@ const defaultProps: Partial = { blurOnEnter: true, }; -function isAsyncValidator(validator: InputValidator): validator is InputValidator { - return typeof validator.debounce === "number"; -} - export class Input extends React.Component { static defaultProps = defaultProps as object; public input: InputElement | null = null; - public validators: InputValidator[] = []; + public validators: InputValidator[] = []; public state: State = { focused: false, @@ -155,7 +168,7 @@ export class Input extends React.Component { async validate() { const value = this.getValue(); let validationId = (this.validationId = ""); // reset every time for async validators - const asyncValidators: Promise[] = []; + const asyncValidators: Promise[] = []; const errors: React.ReactNode[] = []; // run validators @@ -169,28 +182,15 @@ export class Input extends React.Component { if (!validationId) { this.validationId = validationId = uniqueId("validation_id_"); } - asyncValidators.push( - validator.validate(value, this.props).then( - () => null, // don't consider any valid result from promise since we interested in errors only - error => this.getValidatorError(value, validator) || error, - ), - ); - } - - const isValid = validator.validate(value, this.props); - - if (isValid === false) { + asyncValidators.push((async () => { + try { + await validator.validate(value, this.props); + } catch (error) { + return this.getValidatorError(value, validator) || (error instanceof Error ? error.message : String(error)); + } + })()); + } else if (!validator.validate(value, this.props)) { errors.push(this.getValidatorError(value, validator)); - } else if (isValid instanceof Promise) { - if (!validationId) { - this.validationId = validationId = uniqueId("validation_id_"); - } - asyncValidators.push( - isValid.then( - () => null, // don't consider any valid result from promise since we interested in errors only - error => this.getValidatorError(value, validator) || error, - ), - ); } } @@ -200,10 +200,15 @@ export class Input extends React.Component { // handle async validators result if (asyncValidators.length > 0) { this.setState({ validating: true, valid: false }); - const asyncErrors = await Promise.all(asyncValidators); + const asyncErrors = await Promise.allSettled(asyncValidators); if (this.validationId === validationId) { - this.setValidation(errors.concat(...asyncErrors.filter(err => err))); + errors.push(...asyncErrors + .filter(isPromiseSettledFulfilled) + .map(res => res.value) + .filter(Boolean)); + + this.setValidation(errors); } } @@ -218,10 +223,10 @@ export class Input extends React.Component { }); } - private getValidatorError(value: string, { message }: InputValidator) { - if (isFunction(message)) return message(value, this.props); - - return message || ""; + private getValidatorError(value: string, { message }: InputValidator) { + return typeof message === "function" + ? message(value, this.props) + : message; } private setupValidators() { diff --git a/src/renderer/components/input/input_validators.ts b/src/renderer/components/input/input_validators.ts index a9844bfcd8..59e12a977a 100644 --- a/src/renderer/components/input/input_validators.ts +++ b/src/renderer/components/input/input_validators.ts @@ -4,41 +4,113 @@ */ import type { InputProps } from "./input"; -import type { ReactNode } from "react"; +import type React from "react"; import fse from "fs-extra"; import { TypedRegEx } from "typed-regex"; +import type { SetRequired } from "type-fest"; -export class AsyncInputValidationError extends Error { -} +export type InputValidationResult = + IsAsync extends true + ? Promise + : boolean; -export type InputValidator = { +export type InputValidation = (value: string, props?: InputProps) => InputValidationResult; + +export type SyncValidationMessage = React.ReactNode | ((value: string, props?: InputProps) => React.ReactNode); + +export type InputValidator = { /** * Filters itself based on the input props */ condition?: (props: InputProps) => any; } & ( - IsAsync extends false + IsAsync extends true ? { - validate: (value: string, props: InputProps) => boolean; - message: ReactNode | ((value: string, props: InputProps) => ReactNode | string); - debounce?: undefined; + /** + * The validation message maybe either specified from the `message` field (higher priority) + * or if that is not provided then the message will retrived from the rejected with value + */ + validate: InputValidation; + message?: SyncValidationMessage; + debounce: number; } : { - /** - * If asyncronous then the rejection message is the error message - * - * This function MUST reject with an instance of {@link AsyncInputValidationError} - */ - validate: (value: string, props: InputProps) => Promise; - message?: undefined; - debounce: number; + validate: InputValidation; + message: SyncValidationMessage; + debounce?: undefined; } ); -export function inputValidator(validator: InputValidator): InputValidator { +export function isAsyncValidator(validator: InputValidator): validator is InputValidator { + return typeof validator.debounce === "number"; +} + +export function asyncInputValidator(validator: InputValidator): InputValidator { return validator; } +export function inputValidator(validator: InputValidator): InputValidator { + return validator; +} + +/** + * Create a new input validator from a list of syncronous input validators. Will match as valid if + * one of the input validators matches the input + */ +export function unionInputValidators( + baseValidator: Pick, "condition" | "message">, + ...validators: InputValidator[] +): InputValidator { + return inputValidator({ + ...baseValidator, + validate: (value, props) => validators.some(validator => validator.validate(value, props)), + }); +} + +/** + * Create a new input validator from a list of syncronous or async input validators. Will match as + * valid if one of the input validators matches the input + */ +export function unionInputValidatorsAsync( + baseValidator: SetRequired, "condition" | "message">, "message">, + ...validators: InputValidator[] +): InputValidator { + const longestDebounce = Math.max( + ...validators + .filter(isAsyncValidator) + .map(validator => validator.debounce), + 0, + ); + + return asyncInputValidator({ + debounce: longestDebounce, + validate: async (value, props) => { + for (const validator of validators) { + if (isAsyncValidator(validator)) { + try { + await validator.validate(value, props); + + return; + } catch { + // Do nothing + } + } else { + if (validator.validate(value, props)) { + return; + } + } + } + + /** + * If no validator returns `true` then mark as invalid by throwing. The message will be + * obtained from the `message` field. + */ + throw new Error(); + }, + ...baseValidator, + }); +} + export const isRequired = inputValidator({ condition: ({ required }) => required, message: () => `This field is required`, @@ -53,7 +125,7 @@ export const isEmail = inputValidator({ export const isNumber = inputValidator({ condition: ({ type }) => type === "number", - message(value, { min, max }) { + message(value, { min, max } = {}) { const minMax: string = [ typeof min === "number" ? `min: ${min}` : undefined, typeof max === "number" ? `max: ${max}` : undefined, @@ -61,7 +133,7 @@ export const isNumber = inputValidator({ return `Invalid number${minMax ? ` (${minMax})` : ""}`; }, - validate: (value, { min, max }) => { + validate: (value, { min, max } = {}) => { const numVal = +value; return !( @@ -100,30 +172,26 @@ export const isExtensionNameInstall = inputValidator({ validate: value => isExtensionNameInstallRegex.isMatch(value), }); -export const isPath = inputValidator({ +export const isPath = asyncInputValidator({ debounce: 100, condition: ({ type }) => type === "text", validate: async value => { - try { - await fse.pathExists(value); - } catch { - throw new AsyncInputValidationError(`${value} is not a valid file path`); + if (!await fse.pathExists(value)) { + throw new Error(`"${value}" is not a valid file path`); } }, }); export const minLength = inputValidator({ condition: ({ minLength }) => !!minLength, - message: (value, { minLength }) => `Minimum length is ${minLength}`, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - validate: (value, { minLength }) => value.length >= minLength!, + message: (value, { minLength = 0 } = {}) => `Minimum length is ${minLength}`, + validate: (value, { minLength = 0 } = {}) => value.length >= minLength, }); export const maxLength = inputValidator({ condition: ({ maxLength }) => !!maxLength, - message: (value, { maxLength }) => `Maximum length is ${maxLength}`, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - validate: (value, { maxLength }) => value.length <= maxLength!, + message: (value, { maxLength = 0 } = {}) => `Maximum length is ${maxLength}`, + validate: (value, { maxLength = 0 } = {}) => value.length <= maxLength, }); const systemNameMatcher = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/; @@ -135,7 +203,7 @@ export const systemName = inputValidator({ export const accountId = inputValidator({ message: () => `Invalid account ID`, - validate: (value, props) => (isEmail.validate(value, props) || systemName.validate(value, props)), + validate: (value) => (isEmail.validate(value) || systemName.validate(value)), }); export const conditionalValidators = [ From 1393cc303d6c04268904cac476e33b4ebe4ba5a7 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Mon, 13 Jun 2022 11:42:53 +0300 Subject: [PATCH 06/64] Stop using HelmCli from Renderer (#4861) * Introduce way for execute file Signed-off-by: Janne Savolainen * Make typing of HelmRepo shared Signed-off-by: Janne Savolainen * Introduce way to get Helm environment values Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce function to read YAML file Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce competition for listing active helm repositories in preferences Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make sense in name of injectable Signed-off-by: Janne Savolainen * Introduce helper for opening and selecting values of select Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce competition for activating, deactivating public helm repositories in preferences Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce competition for deactivating helm repository from list of active repositories Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add missing global overrides Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make some tests more deterministic by mocking tooltips Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce competition for activating custom helm repository in preferences Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Update snapshots Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove old implementation made redundant with competition for preferences of helm repositories Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add success notification for activating custom helm repository Signed-off-by: Janne Savolainen * Introduce way to get single active helm repository Signed-off-by: Janne Savolainen * Extract responsibilities from god-class and switch to getting helm repositories using competition instead of another god class Signed-off-by: Janne Savolainen * Remove dead code Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add TODO Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak position of spinner Signed-off-by: Janne Savolainen * Start handling errors when accessing helm repositories Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Handle error about no helm repositories when updating repositories Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove unwarranted function configuration Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add missing global overrides Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove duplication how to acquire binary path for helm Signed-off-by: Janne Savolainen * Remove redundant comment Signed-off-by: Janne Savolainen * Consolidate naming to match Helm's internal Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Relocate file closer to it's relatives Signed-off-by: Janne Savolainen --- ...elm-repository-in-preferences.test.ts.snap | 6813 +++++++++++++++++ ...tory-from-list-in-preferences.test.ts.snap | 4926 ++++++++++++ ...m-repositories-in-preferences.test.ts.snap | 4184 ++++++++++ ...ive-repository-in-preferences.test.ts.snap | 1887 +++++ ...tom-helm-repository-in-preferences.test.ts | 387 + ...epository-from-list-in-preferences.test.ts | 278 + ...e-helm-repositories-in-preferences.test.ts | 458 ++ ...f-active-repository-in-preferences.test.ts | 126 + ...ion-to-kubernetes-preferences.test.ts.snap | 185 +- ...vigation-to-kubernetes-preferences.test.ts | 11 + src/common/fs/exec-file.injectable.ts | 25 + src/common/fs/read-yaml-file.injectable.ts | 25 + .../add-helm-repository-channel.injectable.ts | 23 + ...ve-helm-repositories-channel.injectable.ts | 23 + src/common/helm/helm-repo.ts | 16 + ...move-helm-repository-channel.injectable.ts | 22 + src/common/utils/async-result.ts | 7 + src/common/utils/get-error-message.ts | 15 + .../base-bundled-binaries-dir.injectable.ts | 24 +- .../vars/bundled-resources-dir.injectable.ts | 9 +- ...lized-platform-architecture.injectable.ts} | 6 +- .../vars/normalized-platform.injectable.ts | 11 +- src/main/getDiForUnitTesting.ts | 58 +- src/main/helm/__mocks__/helm-chart-manager.ts | 2 +- src/main/helm/__tests__/helm-service.test.ts | 56 +- .../helm/exec-helm/exec-helm.injectable.ts | 31 + .../get-helm-env/get-helm-env.injectable.ts | 43 + src/main/helm/helm-binary-path.injectable.ts | 25 + src/main/helm/helm-chart-manager.ts | 2 +- src/main/helm/helm-repo-manager.injectable.ts | 172 - src/main/helm/helm-repo-manager.ts | 33 - src/main/helm/helm-service.ts | 134 - .../delete-helm-release.injectable.ts | 28 + .../get-helm-chart-values.injectable.ts | 29 + .../helm-service/get-helm-chart.injectable.ts | 34 + .../get-helm-release-history.injectable.ts | 28 + .../get-helm-release-values.injectable.ts | 41 + .../get-helm-release.injectable.ts | 30 + .../install-helm-chart.injectable.ts | 30 + .../list-helm-charts.injectable.ts | 41 + .../list-helm-releases.injectable.ts | 28 + .../rollback-helm-release.injectable.ts | 32 + .../update-helm-release.injectable.ts | 45 + ...-repository-channel-listener.injectable.ts | 26 + .../add-helm-repository.injectable.ts | 62 + ...epositories-channel-listener.injectable.ts | 26 + ...get-active-helm-repositories.injectable.ts | 131 + .../get-active-helm-repository.injectable.ts | 27 + ...-repository-channel-listener.injectable.ts | 26 + .../remove-helm-repository.injectable.ts | 29 + .../create-kube-auth-proxy.injectable.ts | 4 +- .../kubectl/bundled-binary-path.injectable.ts | 4 +- src/main/kubectl/create-kubectl.injectable.ts | 4 +- .../helm/charts/get-chart-route.injectable.ts | 32 +- .../get-chart-values-route.injectable.ts | 26 +- .../charts/list-charts-route.injectable.ts | 18 +- .../delete-release-route.injectable.ts | 22 +- .../get-release-history-route.injectable.ts | 26 +- .../releases/get-release-route.injectable.ts | 26 +- .../get-release-values-route.injectable.ts | 28 +- .../install-chart-route.injectable.ts | 24 +- .../list-releases-route.injectable.ts | 18 +- .../rollback-release-route.injectable.ts | 20 +- .../update-release-route.injectable.ts | 32 +- src/renderer/bootstrap.tsx | 3 - .../+preferences/add-helm-repo-dialog.tsx | 220 - .../components/+preferences/helm-charts.tsx | 202 - .../components/+preferences/kubernetes.tsx | 3 +- .../active-helm-repositories.injectable.ts | 43 + .../add-helm-repo-dialog.scss | 0 ...-custom-helm-repository-dialog-content.tsx | 152 + ...dding-of-custom-helm-repository-dialog.tsx | 47 + ...-of-custom-helm-repository-open-button.tsx | 32 + .../custom-helm-repo.injectable.ts | 25 + ...repository-dialog-is-visible.injectable.ts | 13 + ...dding-custom-helm-repository.injectable.ts | 21 + ...dding-custom-helm-repository.injectable.ts | 21 + .../get-file-paths.injectable.ts | 23 + .../helm-file-input/helm-file-input.tsx | 74 + ...-helm-repo-options-are-shown.injectable.ts | 13 + ...ubmit-custom-helm-repository.injectable.ts | 25 + .../adding-of-public-helm-repository.tsx | 80 + ...for-public-helm-repositories.injectable.ts | 29 + .../public-helm-repositories.injectable.ts | 21 + .../add-helm-repository.injectable.ts | 42 + .../select-helm-repository.injectable.ts | 33 + .../helm-charts}/helm-charts.module.scss | 0 .../kubernetes/helm-charts/helm-charts.tsx | 61 + ...elm-repositories-error-state.injectable.ts | 19 + .../helm-charts/helm-repositories.tsx | 68 + .../remove-helm-repository.injectable.ts | 27 + .../+preferences/removable-item.tsx | 4 +- .../input/validators/is-path.injectable.ts | 32 + .../show-error-notification.injectable.ts | 26 + .../show-success-notification.injectable.ts | 26 + src/renderer/components/select/select.tsx | 4 +- .../test-utils/get-application-builder.tsx | 34 +- src/renderer/components/wizard/wizard.tsx | 6 +- src/renderer/getDiForUnitTesting.tsx | 25 +- 99 files changed, 21409 insertions(+), 1039 deletions(-) create mode 100644 src/behaviours/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap create mode 100644 src/behaviours/helm-charts/__snapshots__/add-helm-repository-from-list-in-preferences.test.ts.snap create mode 100644 src/behaviours/helm-charts/__snapshots__/listing-active-helm-repositories-in-preferences.test.ts.snap create mode 100644 src/behaviours/helm-charts/__snapshots__/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts.snap create mode 100644 src/behaviours/helm-charts/add-custom-helm-repository-in-preferences.test.ts create mode 100644 src/behaviours/helm-charts/add-helm-repository-from-list-in-preferences.test.ts create mode 100644 src/behaviours/helm-charts/listing-active-helm-repositories-in-preferences.test.ts create mode 100644 src/behaviours/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts create mode 100644 src/common/fs/exec-file.injectable.ts create mode 100644 src/common/fs/read-yaml-file.injectable.ts create mode 100644 src/common/helm/add-helm-repository-channel.injectable.ts create mode 100644 src/common/helm/get-active-helm-repositories-channel.injectable.ts create mode 100644 src/common/helm/helm-repo.ts create mode 100644 src/common/helm/remove-helm-repository-channel.injectable.ts create mode 100644 src/common/utils/async-result.ts create mode 100644 src/common/utils/get-error-message.ts rename src/common/vars/{bundled-binaries-normalized-arch.injectable.ts => normalized-platform-architecture.injectable.ts} (77%) create mode 100644 src/main/helm/exec-helm/exec-helm.injectable.ts create mode 100644 src/main/helm/get-helm-env/get-helm-env.injectable.ts create mode 100644 src/main/helm/helm-binary-path.injectable.ts delete mode 100644 src/main/helm/helm-repo-manager.injectable.ts delete mode 100644 src/main/helm/helm-repo-manager.ts delete mode 100644 src/main/helm/helm-service.ts create mode 100644 src/main/helm/helm-service/delete-helm-release.injectable.ts create mode 100644 src/main/helm/helm-service/get-helm-chart-values.injectable.ts create mode 100644 src/main/helm/helm-service/get-helm-chart.injectable.ts create mode 100644 src/main/helm/helm-service/get-helm-release-history.injectable.ts create mode 100644 src/main/helm/helm-service/get-helm-release-values.injectable.ts create mode 100644 src/main/helm/helm-service/get-helm-release.injectable.ts create mode 100644 src/main/helm/helm-service/install-helm-chart.injectable.ts create mode 100644 src/main/helm/helm-service/list-helm-charts.injectable.ts create mode 100644 src/main/helm/helm-service/list-helm-releases.injectable.ts create mode 100644 src/main/helm/helm-service/rollback-helm-release.injectable.ts create mode 100644 src/main/helm/helm-service/update-helm-release.injectable.ts create mode 100644 src/main/helm/repositories/add-helm-repository/add-helm-repository-channel-listener.injectable.ts create mode 100644 src/main/helm/repositories/add-helm-repository/add-helm-repository.injectable.ts create mode 100644 src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories-channel-listener.injectable.ts create mode 100644 src/main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable.ts create mode 100644 src/main/helm/repositories/get-active-helm-repository.injectable.ts create mode 100644 src/main/helm/repositories/remove-helm-repository/remove-helm-repository-channel-listener.injectable.ts create mode 100644 src/main/helm/repositories/remove-helm-repository/remove-helm-repository.injectable.ts delete mode 100644 src/renderer/components/+preferences/add-helm-repo-dialog.tsx delete mode 100644 src/renderer/components/+preferences/helm-charts.tsx create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/active-helm-repositories.injectable.ts rename src/renderer/components/+preferences/{ => kubernetes/helm-charts/adding-of-custom-helm-repository}/add-helm-repo-dialog.scss (100%) create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog-content.tsx create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog.tsx create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/adding-of-custom-helm-repository-open-button.tsx create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/custom-helm-repo.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/dialog-visibility/adding-of-custom-helm-repository-dialog-is-visible.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/dialog-visibility/show-dialog-for-adding-custom-helm-repository.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/helm-file-input/get-file-paths.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/helm-file-input/helm-file-input.tsx create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/maximal-custom-helm-repo-options-are-shown.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/submit-custom-helm-repository.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/adding-of-public-helm-repository.tsx create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/public-helm-repositories.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/select-helm-repository/select-helm-repository.injectable.ts rename src/renderer/components/+preferences/{ => kubernetes/helm-charts}/helm-charts.module.scss (100%) create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/helm-charts.tsx create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/helm-repositories-error-state.injectable.ts create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/helm-repositories.tsx create mode 100644 src/renderer/components/+preferences/kubernetes/helm-charts/remove-helm-repository.injectable.ts create mode 100644 src/renderer/components/input/validators/is-path.injectable.ts create mode 100644 src/renderer/components/notifications/show-error-notification.injectable.ts create mode 100644 src/renderer/components/notifications/show-success-notification.injectable.ts diff --git a/src/behaviours/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap b/src/behaviours/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap new file mode 100644 index 0000000000..35db1088d9 --- /dev/null +++ b/src/behaviours/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap @@ -0,0 +1,6813 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`add custom helm repository in preferences when navigating to preferences containing helm repositories renders 1`] = ` + +
+
+ +
+
+
+
+

+ Kubernetes +

+
+
+ Kubectl binary download + +
+ +
+
+
+ Download mirror + +
+
+ + +
+
+
+ Download mirror for kubectl +
+
+ +
+
+
+ + +
+
+
+
+
+
+ Directory for binaries + +
+
+ +
+
+
+ The directory to download binaries into. +
+
+
+
+ Path to kubectl binary + +
+
+ +
+
+
+
+
+
+

+ Kubeconfig Syncs +

+
+ +
+
+ Synced Items + +
+
+
+ No files and folders have been synced yet +
+
+
+
+
+

+ Helm Charts +

+
+
+
+
+ + +
+
+
+ Repositories +
+
+ +
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+ +`; + +exports[`add custom helm repository in preferences when navigating to preferences containing helm repositories when active repositories resolve renders 1`] = ` + +
+
+ +
+
+
+
+

+ Kubernetes +

+
+
+ Kubectl binary download + +
+ +
+
+
+ Download mirror + +
+
+ + +
+
+
+ Download mirror for kubectl +
+
+ +
+
+
+ + +
+
+
+
+
+
+ Directory for binaries + +
+
+ +
+
+
+ The directory to download binaries into. +
+
+
+
+ Path to kubectl binary + +
+
+ +
+
+
+
+
+
+

+ Kubeconfig Syncs +

+
+ +
+
+ Synced Items + +
+
+
+ No files and folders have been synced yet +
+
+
+
+
+

+ Helm Charts +

+
+
+
+
+ + +
+
+
+ Repositories +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+
+ Some active repository +
+
+ some-url +
+ + + delete + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+ +`; + +exports[`add custom helm repository in preferences when navigating to preferences containing helm repositories when active repositories resolve when selecting to add custom repository renders 1`] = ` + +
+
+ +
+
+
+
+

+ Kubernetes +

+
+
+ Kubectl binary download + +
+ +
+
+
+ Download mirror + +
+
+ + +
+
+
+ Download mirror for kubectl +
+
+ +
+
+
+ + +
+
+
+
+
+
+ Directory for binaries + +
+
+ +
+
+
+ The directory to download binaries into. +
+
+
+
+ Path to kubectl binary + +
+
+ +
+
+
+
+
+
+

+ Kubeconfig Syncs +

+
+ +
+
+ Synced Items + +
+
+
+ No files and folders have been synced yet +
+
+
+
+
+

+ Helm Charts +

+
+
+
+
+ + +
+
+
+ Repositories +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+
+ Some active repository +
+
+ some-url +
+ + + delete + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+