From c2b71e8a012dc510d0d48c5caa14ea6438bb3ff2 Mon Sep 17 00:00:00 2001 From: Antonio Gamez Diaz Date: Thu, 29 Sep 2022 16:29:20 +0200 Subject: [PATCH 01/26] Remove unused deps Signed-off-by: Antonio Gamez Diaz --- dashboard/package.json | 7 ++--- dashboard/yarn.lock | 62 +++++------------------------------------- 2 files changed, 9 insertions(+), 60 deletions(-) diff --git a/dashboard/package.json b/dashboard/package.json index 7b4c8f23515..dc606132daa 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -50,9 +50,7 @@ "protobufjs": "^7.1.2", "qs": "^6.11.0", "react": "^17.0.2", - "react-compound-slider": "^3.4.0", "react-copy-to-clipboard": "^5.1.0", - "react-dom": "^17.0.2", "react-helmet": "^6.1.0", "react-intl": "^6.1.1", "react-markdown": "^8.0.3", @@ -61,9 +59,6 @@ "react-redux": "^7.2.9", "react-router-dom": "^5.3.0", "react-router-hash-link": "^2.4.3", - "react-switch": "^7.0.0", - "react-tabs": "^5.1.0", - "react-test-renderer": "^17.0.2", "react-tooltip": "^4.2.21", "react-transition-group": "^4.4.5", "redux": "^4.2.0", @@ -113,7 +108,9 @@ "postcss": "^8.4.16", "postcss-scss": "^4.0.5", "prettier": "^2.7.1", + "react-dom": "^17.0.2", "react-scripts": "^5.0.1", + "react-test-renderer": "^17.0.2", "redux-mock-store": "^1.5.4", "sass": "^1.55.0", "shx": "^0.3.4", diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock index c41194dbf06..1c37b95b08a 100644 --- a/dashboard/yarn.lock +++ b/dashboard/yarn.lock @@ -1032,9 +1032,9 @@ regenerator-runtime "^0.13.4" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" - integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" + integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== dependencies: regenerator-runtime "^0.13.4" @@ -4047,9 +4047,9 @@ class-utils@^0.3.5: static-extend "^0.1.1" classnames@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== clean-css@^5.2.2: version "5.3.1" @@ -4067,11 +4067,6 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clsx@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -4649,13 +4644,6 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== -d3-array@^2.8.0: - version "2.12.1" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" - integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== - dependencies: - internmap "^1.0.0" - damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -6896,11 +6884,6 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -internmap@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" - integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== - interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -10729,7 +10712,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -10959,15 +10942,6 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" -react-compound-slider@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/react-compound-slider/-/react-compound-slider-3.4.0.tgz#0367befe1367bb7968b38d0cbf07db6192b3c57e" - integrity sha512-KSje/rB0xSvvcb7YV0+82hkiXTV5ljSS7axKrNiXLf9AEO+rrr1Xq4MJWA+6v030YNNo/RoSoEB6D6fnoy+8ng== - dependencies: - "@babel/runtime" "^7.12.5" - d3-array "^2.8.0" - warning "^4.0.3" - react-copy-to-clipboard@5.0.4: version "5.0.4" resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz#42ec519b03eb9413b118af92d1780c403a5f19bf" @@ -11257,13 +11231,6 @@ react-side-effect@^2.1.0: resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== -react-switch@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/react-switch/-/react-switch-7.0.0.tgz#400990bb9822864938e343ed24f13276a617bdc0" - integrity sha512-KkDeW+cozZXI6knDPyUt3KBN1rmhoVYgAdCJqAh7st7tk8YE6N0iR89zjCWO8T8dUTeJGTR0KU+5CHCRMRffiA== - dependencies: - prop-types "^15.7.2" - react-syntax-highlighter@^15.4.5: version "15.5.0" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" @@ -11275,14 +11242,6 @@ react-syntax-highlighter@^15.4.5: prismjs "^1.27.0" refractor "^3.6.0" -react-tabs@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-5.1.0.tgz#5ef8fad015c71c23b0fff65bd9b3bd419219c27b" - integrity sha512-jsPVEPuhG7JljTo8Q4ujz4UKRpG90nHlDClAdvV5KrLxCHU+MT/kg7dmhq8fDv8+frciDtaYeFFlTVRLm4N5AQ== - dependencies: - clsx "^1.1.0" - prop-types "^15.5.0" - react-test-renderer@^17.0.0, react-test-renderer@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" @@ -13707,13 +13666,6 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.12" -warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" From 41b2ab9308d9b928d2cee3d9da407ce75bcc1f0e Mon Sep 17 00:00:00 2001 From: Antonio Gamez Diaz Date: Thu, 29 Sep 2022 16:36:07 +0200 Subject: [PATCH 02/26] Refactor schema/yaml util functions Signed-off-by: Antonio Gamez Diaz --- dashboard/src/actions/installedpackages.ts | 6 +- dashboard/src/components/AppView/AppView.tsx | 1 + dashboard/src/shared/schema.test.ts | 495 +++++++++---------- dashboard/src/shared/schema.ts | 226 ++++----- dashboard/src/shared/types.ts | 49 +- dashboard/src/shared/utils.ts | 1 + dashboard/src/shared/yamlUtils.test.ts | 223 +++++++++ dashboard/src/shared/yamlUtils.ts | 128 +++++ 8 files changed, 708 insertions(+), 421 deletions(-) create mode 100644 dashboard/src/shared/yamlUtils.test.ts create mode 100644 dashboard/src/shared/yamlUtils.ts diff --git a/dashboard/src/actions/installedpackages.ts b/dashboard/src/actions/installedpackages.ts index 3ad8503ae90..6e2737492f0 100644 --- a/dashboard/src/actions/installedpackages.ts +++ b/dashboard/src/actions/installedpackages.ts @@ -27,7 +27,7 @@ import { import { getPluginsSupportingRollback } from "shared/utils"; import { ActionType, deprecated } from "typesafe-actions"; import { InstalledPackage } from "../shared/InstalledPackage"; -import { validate } from "../shared/schema"; +import { validateValuesSchema } from "../shared/schema"; import { handleErrorAction } from "./auth"; const { createAction } = deprecated; @@ -234,7 +234,7 @@ export function installPackage( dispatch(requestInstallPackage()); try { if (values && schema) { - const validation = validate(values, schema); + const validation = validateValuesSchema(values, schema); if (!validation.valid) { const errorText = validation.errors && @@ -283,7 +283,7 @@ export function updateInstalledPackage( dispatch(requestUpdateInstalledPackage()); try { if (values && schema) { - const validation = validate(values, schema); + const validation = validateValuesSchema(values, schema); if (!validation.valid) { const errorText = validation.errors && diff --git a/dashboard/src/components/AppView/AppView.tsx b/dashboard/src/components/AppView/AppView.tsx index 9e1c4f9787e..36933ae6e1d 100644 --- a/dashboard/src/components/AppView/AppView.tsx +++ b/dashboard/src/components/AppView/AppView.tsx @@ -404,6 +404,7 @@ export default function AppView() {
{ [ @@ -14,9 +14,15 @@ describe("retrieveBasicFormParams", () => { } as any, result: [ { - path: "user", - value: "andres", - } as IBasicFormParam, + type: "string", + form: true, + title: "user", + key: "user", + schema: { type: "string", form: true }, + hasProperties: false, + deployedValue: "", + currentValue: "andres", + }, ], }, { @@ -27,8 +33,14 @@ describe("retrieveBasicFormParams", () => { } as any, result: [ { - path: "user", - } as IBasicFormParam, + type: "string", + form: true, + title: "user", + key: "user", + schema: { type: "string", form: true }, + hasProperties: false, + deployedValue: "", + }, ], }, { @@ -39,9 +51,17 @@ describe("retrieveBasicFormParams", () => { } as any, result: [ { - path: "user", - value: "michael", - } as IBasicFormParam, + type: "string", + form: true, + default: "michael", + title: "user", + key: "user", + schema: { type: "string", form: true, default: "michael" }, + hasProperties: false, + deployedValue: "", + defaultValue: "michael", + currentValue: "michael", + }, ], }, { @@ -52,9 +72,17 @@ describe("retrieveBasicFormParams", () => { } as any, result: [ { - path: "user", - value: "foo", - } as IBasicFormParam, + type: "string", + form: true, + default: "bar", + title: "user", + key: "user", + schema: { type: "string", form: true, default: "bar" }, + hasProperties: false, + deployedValue: "", + defaultValue: "bar", + currentValue: "foo", + }, ], }, { @@ -65,9 +93,17 @@ describe("retrieveBasicFormParams", () => { } as any, result: [ { - path: "user", - value: "andres", - } as IBasicFormParam, + type: "string", + form: true, + default: "andres", + title: "user", + key: "user", + schema: { type: "string", form: true, default: "andres" }, + hasProperties: false, + deployedValue: "", + defaultValue: "andres", + currentValue: "andres", + }, ], }, { @@ -83,9 +119,28 @@ describe("retrieveBasicFormParams", () => { } as any, result: [ { - path: "credentials/user", - value: "andres", - } as IBasicFormParam, + type: "object", + properties: { user: { type: "string", form: true } }, + title: "credentials", + key: "credentials", + schema: { type: "object", properties: { user: { type: "string", form: true } } }, + hasProperties: true, + params: [ + { + type: "string", + form: true, + title: "user", + key: "credentials/user", + schema: { type: "string", form: true }, + hasProperties: false, + deployedValue: "", + currentValue: "andres", + }, + ], + deployedValue: "", + defaultValue: "", + currentValue: "", + }, ], }, { @@ -123,17 +178,98 @@ service: ClusterIP } as any, result: [ { - path: "credentials/admin/user", - value: "andres", - } as IBasicFormParam, + type: "object", + properties: { + admin: { + type: "object", + properties: { + user: { type: "string", form: true }, + pass: { type: "string", form: true }, + }, + }, + }, + title: "credentials", + key: "credentials", + schema: { + type: "object", + properties: { + admin: { + type: "object", + properties: { + user: { type: "string", form: true }, + pass: { type: "string", form: true }, + }, + }, + }, + }, + hasProperties: true, + params: [ + { + type: "object", + properties: { + user: { type: "string", form: true }, + pass: { type: "string", form: true }, + }, + title: "admin", + key: "credentials/admin", + schema: { + type: "object", + properties: { + user: { type: "string", form: true }, + pass: { type: "string", form: true }, + }, + }, + hasProperties: true, + params: [ + { + type: "string", + form: true, + title: "user", + key: "credentials/admin/user", + schema: { type: "string", form: true }, + hasProperties: false, + deployedValue: "", + currentValue: "andres", + }, + { + type: "string", + form: true, + title: "pass", + key: "credentials/admin/pass", + schema: { type: "string", form: true }, + hasProperties: false, + deployedValue: "", + currentValue: "myPassword", + }, + ], + deployedValue: "", + defaultValue: "", + currentValue: "", + }, + ], + deployedValue: "", + defaultValue: "", + currentValue: "", + }, { - path: "credentials/admin/pass", - value: "myPassword", - } as IBasicFormParam, + type: "number", + form: true, + title: "replicas", + key: "replicas", + schema: { type: "number", form: true }, + hasProperties: false, + deployedValue: "", + currentValue: 1, + }, { - path: "replicas", - value: 1, - } as IBasicFormParam, + type: "string", + title: "service", + key: "service", + schema: { type: "string" }, + hasProperties: false, + deployedValue: "", + currentValue: "ClusterIP", + }, ], }, { @@ -151,12 +287,21 @@ service: ClusterIP } as any, result: [ { - path: "blogName", type: "string", - value: "myBlog", + form: true, title: "Blog Name", description: "Title of the blog", - } as IBasicFormParam, + key: "blogName", + schema: { + type: "string", + form: true, + title: "Blog Name", + description: "Title of the blog", + }, + hasProperties: false, + deployedValue: "", + currentValue: "myBlog", + }, ], }, { @@ -180,19 +325,49 @@ externalDatabase: } as any, result: [ { - path: "externalDatabase", type: "object", - children: [ + form: true, + properties: { + name: { type: "string", form: true }, + port: { type: "integer", form: true }, + }, + title: "externalDatabase", + key: "externalDatabase", + schema: { + type: "object", + form: true, + properties: { + name: { type: "string", form: true }, + port: { type: "integer", form: true }, + }, + }, + hasProperties: true, + params: [ { - path: "externalDatabase/name", type: "string", + form: true, + title: "name", + key: "externalDatabase/name", + schema: { type: "string", form: true }, + hasProperties: false, + deployedValue: "", + currentValue: "foo", }, { - path: "externalDatabase/port", type: "integer", + form: true, + title: "port", + key: "externalDatabase/port", + schema: { type: "integer", form: true }, + hasProperties: false, + deployedValue: "", + currentValue: 3306, }, ], - } as IBasicFormParam, + deployedValue: "", + defaultValue: "", + currentValue: "", + }, ], }, { @@ -203,7 +378,18 @@ externalDatabase: foo: { type: "boolean", form: true }, }, } as any, - result: [{ path: "foo", type: "boolean", value: false } as IBasicFormParam], + result: [ + { + type: "boolean", + form: true, + title: "foo", + key: "foo", + schema: { type: "boolean", form: true }, + hasProperties: false, + deployedValue: "", + currentValue: false, + }, + ], }, { description: "should retrieve a param with enum values", @@ -219,233 +405,32 @@ externalDatabase: } as any, result: [ { - path: "databaseType", type: "string", - value: "postgresql", + form: true, enum: ["mariadb", "postgresql"], - } as IBasicFormParam, + title: "databaseType", + key: "databaseType", + schema: { type: "string", form: true, enum: ["mariadb", "postgresql"] }, + hasProperties: false, + deployedValue: "", + currentValue: "postgresql", + }, ], }, ].forEach(t => { it(t.description, () => { - expect(retrieveBasicFormParams(t.values, t.schema)).toMatchObject(t.result); - }); - }); -}); - -describe("getValue", () => { - [ - { - description: "should return a value", - values: "foo: bar", - path: "foo", - result: "bar", - }, - { - description: "should return a nested value", - values: "foo:\n bar: foobar", - path: "foo/bar", - result: "foobar", - }, - { - description: "should return a deeply nested value", - values: "foo:\n bar:\n foobar: barfoo", - path: "foo/bar/foobar", - result: "barfoo", - }, - { - description: "should ignore an invalid path", - values: "foo:\n bar:\n foobar: barfoo", - path: "nope", - result: undefined, - }, - { - description: "should ignore an invalid path (nested)", - values: "foo:\n bar:\n foobar: barfoo", - path: "not/exists", - result: undefined, - }, - { - description: "should return the default value if the path is not valid", - values: "foo: bar", - path: "foobar", - default: '"BAR"', - result: '"BAR"', - }, - { - description: "should return a value with slashes in the key", - values: "foo/bar: value", - path: "foo~1bar", - result: "value", - }, - { - description: "should return a value with slashes and dots in the key", - values: "kubernetes.io/ingress.class: nginx", - path: "kubernetes.io~1ingress.class", - result: "nginx", - }, - ].forEach(t => { - it(t.description, () => { - expect(getValue(t.values, t.path, t.default)).toEqual(t.result); - }); - }); -}); - -describe("setValue", () => { - [ - { - description: "should set a value", - values: 'foo: "bar"', - path: "foo", - newValue: "BAR", - result: 'foo: "BAR"\n', - }, - { - description: "should set a value preserving the existing scalar quotation (simple)", - values: "foo: 'bar'", - path: "foo", - newValue: "BAR", - result: "foo: 'BAR'\n", - }, - { - description: "should set a value preserving the existing scalar quotation (double)", - values: 'foo: "bar"', - path: "foo", - newValue: "BAR", - result: 'foo: "BAR"\n', - }, - { - description: "should set a value preserving the existing scalar quotation (none)", - values: "foo: bar", - path: "foo", - newValue: "BAR", - result: "foo: BAR\n", - }, - { - description: "should set a nested value", - values: 'foo:\n bar: "foobar"', - path: "foo/bar", - newValue: "FOOBAR", - result: 'foo:\n bar: "FOOBAR"\n', - }, - { - description: "should set a deeply nested value", - values: 'foo:\n bar:\n foobar: "barfoo"', - path: "foo/bar/foobar", - newValue: "BARFOO", - result: 'foo:\n bar:\n foobar: "BARFOO"\n', - }, - { - description: "should add a new value", - values: "foo: bar", - path: "new", - newValue: "value", - result: 'foo: bar\nnew: "value"\n', - }, - { - description: "should add a new nested value", - values: "foo: bar", - path: "this/new", - newValue: 1, - result: "foo: bar\nthis:\n new: 1\n", - error: false, - }, - { - description: "should add a new deeply nested value", - values: "foo: bar", - path: "this/new/value", - newValue: 1, - result: "foo: bar\nthis:\n new:\n value: 1\n", - error: false, - }, - { - description: "Adding a value for a path partially defined (null)", - values: "foo: bar\nthis:\n", - path: "this/new/value", - newValue: 1, - result: "foo: bar\nthis:\n new:\n value: 1\n", - error: false, - }, - { - description: "Adding a value for a path partially defined (object)", - values: "foo: bar\nthis: {}\n", - path: "this/new/value", - newValue: 1, - result: "foo: bar\nthis: { new: { value: 1 } }\n", - error: false, - }, - { - description: "Adding a value in an empty doc", - values: "", - path: "foo", - newValue: "bar", - result: 'foo: "bar"\n', - error: false, - }, - { - description: "should add a value with slashes in the key", - values: 'foo/bar: "test"', - path: "foo~1bar", - newValue: "value", - result: 'foo/bar: "value"\n', - }, - { - description: "should add a value with slashes and dots in the key", - values: 'kubernetes.io/ingress.class: "default"', - path: "kubernetes.io~1ingress.class", - newValue: "nginx", - result: 'kubernetes.io/ingress.class: "nginx"\n', - }, - ].forEach(t => { - it(t.description, () => { - if (t.error) { - expect(() => setValue(t.values, t.path, t.newValue)).toThrow(); - } else { - expect(setValue(t.values, t.path, t.newValue)).toEqual(t.result); - } - }); - }); -}); - -describe("deleteValue", () => { - [ - { - description: "should delete a value", - values: "foo: bar\nbar: foo\n", - path: "bar", - result: "foo: bar\n", - }, - { - description: "should delete a value from an array", - values: `foo: - - bar - - foobar -`, - path: "foo/0", - result: `foo: - - foobar -`, - }, - { - description: "should leave the document empty", - values: "foo: bar", - path: "foo", - result: "\n", - }, - { - description: "noop when trying to delete a missing property", - values: "foo: bar\nbar: foo\n", - path: "var", - result: "foo: bar\nbar: foo\n", - }, - ].forEach(t => { - it(t.description, () => { - expect(deleteValue(t.values, t.path)).toEqual(t.result); + const result = retrieveBasicFormParams( + parseToYamlNode(t.values), + parseToYamlNode(""), + t.schema, + "install", + ); + expect(result).toMatchObject(t.result); }); }); }); -describe("validate", () => { +describe("validateValuesSchema", () => { [ { description: "Should validate a valid object", @@ -475,7 +460,7 @@ describe("validate", () => { }, ].forEach(t => { it(t.description, () => { - const res = validate(t.values, t.schema); + const res = validateValuesSchema(t.values, t.schema); expect(res.valid).toBe(t.valid); expect(res.errors).toEqual(t.errors); }); diff --git a/dashboard/src/shared/schema.ts b/dashboard/src/shared/schema.ts index 87f7a33c4e0..1e51d73225d 100644 --- a/dashboard/src/shared/schema.ts +++ b/dashboard/src/shared/schema.ts @@ -2,166 +2,128 @@ // SPDX-License-Identifier: Apache-2.0 import Ajv, { ErrorObject, JSONSchemaType } from "ajv"; -import * as jsonpatch from "fast-json-patch"; +// TODO(agamez): check if we can replace this package by js-yaml or vice-versa import * as yaml from "js-yaml"; -import { isEmpty, set } from "lodash"; +import { findIndex, isEmpty, set } from "lodash"; +import { DeploymentEvent, IAjvValidateResult, IBasicFormParam } from "shared/types"; // TODO(agamez): check if we can replace this package by js-yaml or vice-versa -import YAML, { Scalar, ToStringOptions } from "yaml"; -import { IBasicFormParam } from "./types"; +import YAML from "yaml"; +import { getPathValueInYamlNode, getPathValueInYamlNodeWithDefault } from "./yamlUtils"; const ajv = new Ajv({ strict: false }); -const toStringOptions: ToStringOptions = { - defaultKeyType: "PLAIN", - defaultStringType: Scalar.QUOTE_DOUBLE, // Preserving double quotes in scalars (see https://github.com/vmware-tanzu/kubeapps/issues/3621) - nullStr: "", // Avoid to explicitly add "null" when an element is not defined -}; +const IS_CUSTOM_COMPONENT_PROP_NAME = "x-is-custom-component"; -// retrieveBasicFormParams iterates over a JSON Schema properties looking for `form` keys -// It uses the raw yaml to setup default values. -// It returns a key:value map for easier handling. export function retrieveBasicFormParams( - defaultValues: string, - schema?: JSONSchemaType, + currentValues: YAML.Document.Parsed, + packageValues: YAML.Document.Parsed, + schema: JSONSchemaType, + deploymentEvent: DeploymentEvent, + deployedValues?: YAML.Document.Parsed, parentPath?: string, ): IBasicFormParam[] { let params: IBasicFormParam[] = []; - if (schema?.properties && !isEmpty(schema.properties)) { const properties = schema.properties; Object.keys(properties).forEach(propertyKey => { + const schemaProperty = properties[propertyKey] as JSONSchemaType; // The param path is its parent path + the object key const itemPath = `${parentPath || ""}${propertyKey}`; - const { type, form } = properties[propertyKey]; - // If the property has the key "form", it's a basic parameter - if (form) { - // Use the default value either from the JSON schema or the default values - const value = getValue(defaultValues, itemPath, properties[propertyKey].default); - const param: IBasicFormParam = { - ...properties[propertyKey], - path: itemPath, - type, - value, - enum: properties[propertyKey].enum?.map( - (item: { toString: () => any }) => item?.toString() ?? "", + const isUpgrading = deploymentEvent === "upgrade" && deployedValues; + const isLeaf = !schemaProperty?.properties; + + const param: IBasicFormParam = { + ...schemaProperty, + title: schemaProperty.title || propertyKey, + key: itemPath, + schema: schemaProperty, + hasProperties: Boolean(schemaProperty?.properties), + params: schemaProperty?.properties + ? retrieveBasicFormParams( + currentValues, + packageValues, + schemaProperty, + deploymentEvent, + deployedValues, + `${itemPath}/`, + ) + : undefined, + enum: schemaProperty?.enum?.map((item: { toString: () => any }) => item?.toString() ?? ""), + // If exists, the value that is currently deployed + deployedValue: isLeaf + ? isUpgrading + ? getPathValueInYamlNode(deployedValues, itemPath) + : "" + : "", + // The default is the value comming from the package values or the one defined in the schema, + // or vice-verse, which one shoulf take precedence? + defaultValue: isLeaf + ? getPathValueInYamlNodeWithDefault(packageValues, itemPath, schemaProperty.default) + : "", + // same as default value, but this one will be later overwritten by the user input + currentValue: isLeaf + ? getPathValueInYamlNodeWithDefault(currentValues, itemPath, schemaProperty.default) + : "", + isCustomComponent: + schemaProperty?.customComponent || schemaProperty?.[IS_CUSTOM_COMPONENT_PROP_NAME], + }; + params = params.concat(param); + + if (!schemaProperty?.properties) { + params = params.concat( + retrieveBasicFormParams( + currentValues, + packageValues, + schemaProperty, + deploymentEvent, + deployedValues, + `${itemPath}/`, ), - children: - properties[propertyKey].type === "object" - ? retrieveBasicFormParams(defaultValues, properties[propertyKey], `${itemPath}/`) - : undefined, - }; - params = params.concat(param); - } else { - // If the property is an object, iterate recursively - if (schema.properties![propertyKey].type === "object") { - params = params.concat( - retrieveBasicFormParams(defaultValues, properties[propertyKey], `${itemPath}/`), - ); - } + ); } }); } return params; } -function getDefinedPath(allElementsButTheLast: string[], doc: YAML.Document) { - let currentPath: string[] = []; - let foundUndefined = false; - allElementsButTheLast.forEach(p => { - // Iterate over the path until finding an element that is not defined - if (!foundUndefined) { - const pathToEvaluate = currentPath.concat(p); - const elem = (doc as any).getIn(pathToEvaluate); - if (elem === undefined || elem === null) { - foundUndefined = true; - } else { - currentPath = pathToEvaluate; - } - } - }); - return currentPath; -} - -function splitPath(path: string): string[] { - return ( - (path ?? "") - // ignore the first slash, if exists - .replace(/^\//, "") - // split by slashes - .split("/") - ); -} - -function unescapePath(path: string[]): string[] { - // jsonpath escapes slashes to not mistake then with objects so we need to revert that - return path.map(p => jsonpatch.unescapePathComponent(p)); -} - -function parsePath(path: string): string[] { - return unescapePath(splitPath(path)); -} - -function parsePathAndValue(doc: YAML.Document, path: string, value?: any) { - if (isEmpty(doc.contents)) { - // If the doc is empty we have an special case - return { value: set({}, path.replace(/^\//, ""), value), splittedPath: [] }; +export function updateCurrentConfigByKey( + paramsList: IBasicFormParam[], + key: string, + value: any, + depth = 1, +): any { + if (!paramsList) { + return []; } - let splittedPath = splitPath(path); - // If the path is not defined (the parent nodes are undefined) - // We need to change the path and the value to set to avoid accessing - // the undefined node. For example, if a.b is undefined: - // path: a.b.c, value: 1 ==> path: a.b, value: {c: 1} - // TODO(andresmgot): In the future, this may be implemented in the YAML library itself - // https://github.com/eemeli/yaml/issues/131 - const allElementsButTheLast = splittedPath.slice(0, splittedPath.length - 1); - const parentNode = (doc as any).getIn(allElementsButTheLast); - if (parentNode === undefined) { - const definedPath = getDefinedPath(allElementsButTheLast, doc); - const remainingPath = splittedPath.slice(definedPath.length + 1); - value = set({}, remainingPath.join("."), value); - splittedPath = splittedPath.slice(0, definedPath.length + 1); - } - return { splittedPath: unescapePath(splittedPath), value }; -} -// setValue modifies the current values (text) based on a path -export function setValue(values: string, path: string, newValue: any) { - const doc = YAML.parseDocument(values, { toStringDefaults: toStringOptions }); - const { splittedPath, value } = parsePathAndValue(doc, path, newValue); - (doc as any).setIn(splittedPath, value); - return doc.toString(toStringOptions); -} - -// parseValues returns a processed version of the values without modifying anything -export function parseValues(values: string) { - return YAML.parseDocument(values, { - toStringDefaults: toStringOptions, - }).toString(toStringOptions); -} - -export function deleteValue(values: string, path: string) { - const doc = YAML.parseDocument(values, { toStringDefaults: toStringOptions }); - const { splittedPath } = parsePathAndValue(doc, path); - (doc as any).deleteIn(splittedPath); - // If the document is empty after the deletion instead of returning {} - // we return an empty line "\n" - return doc.contents && !isEmpty((doc.contents as any).items) - ? doc.toString(toStringOptions) - : "\n"; -} - -// getValue returns the current value of an object based on YAML text and its path -export function getValue(values: string, path: string, defaultValue?: any) { - const doc = YAML.parseDocument(values, { toStringDefaults: toStringOptions }); - const splittedPath = parsePath(path); - const value = (doc as any).getIn(splittedPath); - return value === undefined || value === null ? defaultValue : value; + // Find item index using findIndex + const indexLeaf = findIndex(paramsList, { key: key }); + // is it a leaf node? + if (!paramsList?.[indexLeaf]) { + const a = key.split("/").slice(0, depth).join("/"); + const index = findIndex(paramsList, { key: a }); + if (paramsList?.[index]?.params) { + set( + paramsList[index], + "currentValue", + updateCurrentConfigByKey(paramsList?.[index]?.params || [], key, value, depth + 1), + ); + return paramsList; + } + } + // Replace item at index using native splice + paramsList?.splice(indexLeaf, 1, { + ...paramsList[indexLeaf], + currentValue: value, + }); + return paramsList; } -export function validate( +// TODO(agamez): stop loading the yaml values with the yaml.load function. +export function validateValuesSchema( values: string, schema: JSONSchemaType | any, ): { valid: boolean; errors: ErrorObject[] | null | undefined } { const valid = ajv.validate(schema, yaml.load(values)); - return { valid: !!valid, errors: ajv.errors }; + return { valid: !!valid, errors: ajv.errors } as IAjvValidateResult; } diff --git a/dashboard/src/shared/types.ts b/dashboard/src/shared/types.ts index 1b0eec27e24..3fa528cb528 100644 --- a/dashboard/src/shared/types.ts +++ b/dashboard/src/shared/types.ts @@ -1,7 +1,7 @@ // Copyright 2018-2022 the Kubeapps contributors. // SPDX-License-Identifier: Apache-2.0 -import { JSONSchemaType } from "ajv"; +import { JSONSchemaType, ErrorObject } from "ajv"; import { RouterState } from "connected-react-router"; import { AvailablePackageDetail, @@ -398,37 +398,24 @@ export interface IKubeState { kindsError?: Error; } -export interface IBasicFormParam { - path: string; - type?: "string" | "number" | "integer" | "boolean" | "object" | "array" | "null" | "any"; - value?: any; - title?: string; - minimum?: number; - maximum?: number; - render?: string; - description?: string; - customComponent?: object; +// We extend the JSONSchema properties to include the default/deployed values as well as +// other useful information for rendering each param in the UI +export type IBasicFormParam = JSONSchemaType & { + key: string; + title: string; + hasProperties: boolean; + params?: IBasicFormParam[]; enum?: string[]; - hidden?: - | { - event: DeploymentEvent; - path: string; - value: string; - conditions: Array<{ - event: DeploymentEvent; - path: string; - value: string; - }>; - operator: string; - } - | string; - children?: IBasicFormParam[]; -} -export interface IBasicFormSliderParam extends IBasicFormParam { - sliderMin?: number; - sliderMax?: number; - sliderStep?: number; - sliderUnit?: string; + defaultValue: any; + deployedValue: any; + currentValue: any; + schema: JSONSchemaType; + isCustomComponent?: boolean; +}; + +export interface IAjvValidateResult { + valid: boolean; + errors: ErrorObject[] | null | undefined; } export interface CustomInstalledPackageDetail extends InstalledPackageDetail { diff --git a/dashboard/src/shared/utils.ts b/dashboard/src/shared/utils.ts index e7a3fcdb71e..718e832edd9 100644 --- a/dashboard/src/shared/utils.ts +++ b/dashboard/src/shared/utils.ts @@ -33,6 +33,7 @@ import { } from "./types"; export const k8sObjectNameRegex = "[a-z0-9]([-a-z0-9]*[a-z0-9])?(.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"; +export const basicFormsDebounceTime = 500; export function escapeRegExp(str: string) { return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); diff --git a/dashboard/src/shared/yamlUtils.test.ts b/dashboard/src/shared/yamlUtils.test.ts new file mode 100644 index 00000000000..9ee42b15083 --- /dev/null +++ b/dashboard/src/shared/yamlUtils.test.ts @@ -0,0 +1,223 @@ +// Copyright 2019-2022 the Kubeapps contributors. +// SPDX-License-Identifier: Apache-2.0 + +import { + deleteValue, + getPathValueInYamlNodeWithDefault, + parseToYamlNode, + setValue, +} from "./yamlUtils"; + +describe("getPathValueInYamlNodeWithDefault", () => { + [ + { + description: "should return a value", + values: "foo: bar", + path: "foo", + result: "bar", + }, + { + description: "should return a nested value", + values: "foo:\n bar: foobar", + path: "foo/bar", + result: "foobar", + }, + { + description: "should return a deeply nested value", + values: "foo:\n bar:\n foobar: barfoo", + path: "foo/bar/foobar", + result: "barfoo", + }, + { + description: "should ignore an invalid path", + values: "foo:\n bar:\n foobar: barfoo", + path: "nope", + result: undefined, + }, + { + description: "should ignore an invalid path (nested)", + values: "foo:\n bar:\n foobar: barfoo", + path: "not/exists", + result: undefined, + }, + { + description: "should return the default value if the path is not valid", + values: "foo: bar", + path: "foobar", + default: '"BAR"', + result: '"BAR"', + }, + { + description: "should return a value with slashes in the key", + values: "foo/bar: value", + path: "foo~1bar", + result: "value", + }, + { + description: "should return a value with slashes and dots in the key", + values: "kubernetes.io/ingress.class: nginx", + path: "kubernetes.io~1ingress.class", + result: "nginx", + }, + ].forEach(t => { + it(t.description, () => { + expect( + getPathValueInYamlNodeWithDefault(parseToYamlNode(t.values), t.path, t.default), + ).toEqual(t.result); + }); + }); +}); + +describe("setValue", () => { + [ + { + description: "should set a value", + values: 'foo: "bar"', + path: "foo", + newValue: "BAR", + result: 'foo: "BAR"\n', + }, + { + description: "should set a value preserving the existing scalar quotation (simple)", + values: "foo: 'bar'", + path: "foo", + newValue: "BAR", + result: "foo: 'BAR'\n", + }, + { + description: "should set a value preserving the existing scalar quotation (double)", + values: 'foo: "bar"', + path: "foo", + newValue: "BAR", + result: 'foo: "BAR"\n', + }, + { + description: "should set a value preserving the existing scalar quotation (none)", + values: "foo: bar", + path: "foo", + newValue: "BAR", + result: "foo: BAR\n", + }, + { + description: "should set a nested value", + values: 'foo:\n bar: "foobar"', + path: "foo/bar", + newValue: "FOOBAR", + result: 'foo:\n bar: "FOOBAR"\n', + }, + { + description: "should set a deeply nested value", + values: 'foo:\n bar:\n foobar: "barfoo"', + path: "foo/bar/foobar", + newValue: "BARFOO", + result: 'foo:\n bar:\n foobar: "BARFOO"\n', + }, + { + description: "should add a new value", + values: "foo: bar", + path: "new", + newValue: "value", + result: 'foo: bar\nnew: "value"\n', + }, + { + description: "should add a new nested value", + values: "foo: bar", + path: "this/new", + newValue: 1, + result: "foo: bar\nthis:\n new: 1\n", + error: false, + }, + { + description: "should add a new deeply nested value", + values: "foo: bar", + path: "this/new/value", + newValue: 1, + result: "foo: bar\nthis:\n new:\n value: 1\n", + error: false, + }, + { + description: "Adding a value for a path partially defined (null)", + values: "foo: bar\nthis:\n", + path: "this/new/value", + newValue: 1, + result: "foo: bar\nthis:\n new:\n value: 1\n", + error: false, + }, + { + description: "Adding a value for a path partially defined (object)", + values: "foo: bar\nthis: {}\n", + path: "this/new/value", + newValue: 1, + result: "foo: bar\nthis: { new: { value: 1 } }\n", + error: false, + }, + { + description: "Adding a value in an empty doc", + values: "", + path: "foo", + newValue: "bar", + result: 'foo: "bar"\n', + error: false, + }, + { + description: "should add a value with slashes in the key", + values: 'foo/bar: "test"', + path: "foo~1bar", + newValue: "value", + result: 'foo/bar: "value"\n', + }, + { + description: "should add a value with slashes and dots in the key", + values: 'kubernetes.io/ingress.class: "default"', + path: "kubernetes.io~1ingress.class", + newValue: "nginx", + result: 'kubernetes.io/ingress.class: "nginx"\n', + }, + ].forEach(t => { + it(t.description, () => { + if (t.error) { + expect(() => setValue(t.values, t.path, t.newValue)).toThrow(); + } else { + expect(setValue(t.values, t.path, t.newValue)).toEqual(t.result); + } + }); + }); +}); + +describe("deleteValue", () => { + [ + { + description: "should delete a value", + values: "foo: bar\nbar: foo\n", + path: "bar", + result: "foo: bar\n", + }, + { + description: "should delete a value from an array", + values: `foo: + - bar + - foobar +`, + path: "foo/0", + result: `foo: + - foobar +`, + }, + { + description: "should leave the document empty", + values: "foo: bar", + path: "foo", + result: "\n", + }, + { + description: "noop when trying to delete a missing property", + values: "foo: bar\nbar: foo\n", + path: "var", + result: "foo: bar\nbar: foo\n", + }, + ].forEach(t => { + it(t.description, () => { + expect(deleteValue(t.values, t.path)).toEqual(t.result); + }); + }); +}); diff --git a/dashboard/src/shared/yamlUtils.ts b/dashboard/src/shared/yamlUtils.ts new file mode 100644 index 00000000000..7c4ba5a76af --- /dev/null +++ b/dashboard/src/shared/yamlUtils.ts @@ -0,0 +1,128 @@ +// Copyright 2019-2022 the Kubeapps contributors. +// SPDX-License-Identifier: Apache-2.0 + +import { unescapePathComponent } from "fast-json-patch"; +import { isEmpty, set } from "lodash"; +import YAML, { Scalar, ToStringOptions } from "yaml"; + +const toStringOptions: ToStringOptions = { + defaultKeyType: "PLAIN", + defaultStringType: Scalar.QUOTE_DOUBLE, // Preserving double quotes in scalars (see https://github.com/vmware-tanzu/kubeapps/issues/3621) + nullStr: "", // Avoid to explicitly add "null" when an element is not defined +}; + +export function parseToYamlNode(string: string) { + return YAML.parseDocument(string, { toStringDefaults: toStringOptions }); +} + +export function toStringYamlNode(valuesNode: YAML.Document.Parsed) { + return valuesNode.toString(toStringOptions); +} + +export function setPathValueInYamlNode( + valuesNode: YAML.Document.Parsed, + path: string, + newValue: any, +) { + const { splittedPath, value } = parsePathAndValue(valuesNode, path, newValue); + valuesNode.setIn(splittedPath, value); + return valuesNode; +} + +function parsePathAndValue(doc: YAML.Document, path: string, value?: any) { + if (isEmpty(doc.contents)) { + // If the doc is empty we have an special case + return { value: set({}, path.replace(/^\//, ""), value), splittedPath: [] }; + } + let splittedPath = splitPath(path); + // If the path is not defined (the parent nodes are undefined) + // We need to change the path and the value to set to avoid accessing + // the undefined node. For example, if a.b is undefined: + // path: a.b.c, value: 1 ==> path: a.b, value: {c: 1} + // TODO(andresmgot): In the future, this may be implemented in the YAML library itself + // https://github.com/eemeli/yaml/issues/131 + const allElementsButTheLast = splittedPath.slice(0, splittedPath.length - 1); + const parentNode = (doc as any).getIn(allElementsButTheLast); + if (parentNode === undefined) { + const definedPath = getDefinedPath(allElementsButTheLast, doc); + const remainingPath = splittedPath.slice(definedPath.length + 1); + value = set({}, remainingPath.join("."), value); + splittedPath = splittedPath.slice(0, definedPath.length + 1); + } + return { splittedPath: unescapePath(splittedPath), value }; +} + +function getDefinedPath(allElementsButTheLast: string[], doc: YAML.Document) { + let currentPath: string[] = []; + let foundUndefined = false; + allElementsButTheLast.forEach(p => { + // Iterate over the path until finding an element that is not defined + if (!foundUndefined) { + const pathToEvaluate = currentPath.concat(p); + const elem = (doc as any).getIn(pathToEvaluate); + if (elem === undefined || elem === null) { + foundUndefined = true; + } else { + currentPath = pathToEvaluate; + } + } + }); + return currentPath; +} + +export function getPathValueInYamlNodeWithDefault( + values: YAML.Document.Parsed, + path: string, + defaultValue?: any, +) { + const value = getPathValueInYamlNode(values, path); + + return value === undefined || value === null ? defaultValue : value; +} + +export function getPathValueInYamlNode( + values: YAML.Document.Parsed, + path: string, +) { + const splittedPath = parsePath(path); + const value = values?.getIn(splittedPath); + return value; +} + +function parsePath(path: string): string[] { + return unescapePath(splitPath(path)); +} + +function unescapePath(path: string[]): string[] { + // jsonpath escapes slashes to not mistake then with objects so we need to revert that + return path.map(p => unescapePathComponent(p)); +} + +function splitPath(path: string): string[] { + return ( + (path ?? "") + // ignore the first slash, if exists + .replace(/^\//, "") + // split by slashes + .split("/") + ); +} + +export function deleteValue(values: string, path: string) { + const doc = YAML.parseDocument(values, { toStringDefaults: toStringOptions }); + const { splittedPath } = parsePathAndValue(doc, path); + (doc as any).deleteIn(splittedPath); + // If the document is empty after the deletion instead of returning {} + // we return an empty line "\n" + return doc.contents && !isEmpty((doc.contents as any).items) + ? doc.toString(toStringOptions) + : "\n"; +} + +// setValue modifies the current values (text) based on a path +export function setValue(values: string, path: string, newValue: any) { + const doc = YAML.parseDocument(values, { toStringDefaults: toStringOptions }); + const { splittedPath, value } = parsePathAndValue(doc, path, newValue); + (doc as any).setIn(splittedPath, value); + return doc.toString(toStringOptions); +} From bf0fd453a8f06bb26f492cdbf01d26eb694a2c20 Mon Sep 17 00:00:00 2001 From: Antonio Gamez Diaz Date: Thu, 29 Sep 2022 16:36:45 +0200 Subject: [PATCH 03/26] Delete no longer used components Signed-off-by: Antonio Gamez Diaz --- .../AdvancedDeploymentForm.tsx | 44 - .../BasicDeploymentForm.scss | 73 - .../BasicDeploymentForm.test.tsx | 485 --- .../BasicDeploymentForm.tsx | 34 - .../BasicDeploymentForm/BooleanParam.test.tsx | 41 - .../BasicDeploymentForm/BooleanParam.tsx | 46 - .../CustomFormParam.test.tsx | 79 - .../BasicDeploymentForm/Param.tsx | 193 -- .../BasicDeploymentForm/SliderParam.test.tsx | 304 -- .../BasicDeploymentForm/SliderParam.tsx | 112 - .../BasicDeploymentForm/Subsection.test.tsx | 46 - .../BasicDeploymentForm/Subsection.tsx | 67 - .../BasicDeploymentForm/TextParam.test.tsx | 220 -- .../BasicDeploymentForm/TextParam.tsx | 80 - .../BasicDeploymentForm.test.tsx.snap | 2703 ----------------- .../__snapshots__/BooleanParam.test.tsx.snap | 140 - .../__snapshots__/SliderParam.test.tsx.snap | 157 - .../__snapshots__/Subsection.test.tsx.snap | 117 - .../__snapshots__/TextParam.test.tsx.snap | 64 - .../DeploymentFormBody/DeploymentFormBody.tsx | 200 -- .../DeploymentFormBody/Differential.scss | 14 - .../DeploymentFormBody/Differential.test.tsx | 88 - .../DeploymentFormBody/Differential.tsx | 44 - .../DifferentialSelector.test.tsx | 36 - .../DifferentialSelector.tsx | 47 - .../DifferentialTab.test.tsx | 93 - .../DeploymentFormBody/DifferentialTab.tsx | 51 - dashboard/src/components/Slider/Slider.tsx | 81 - .../src/components/Slider/components.tsx | 67 - 29 files changed, 5726 deletions(-) delete mode 100644 dashboard/src/components/DeploymentFormBody/AdvancedDeploymentForm.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.scss delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BooleanParam.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BooleanParam.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/CustomFormParam.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Param.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/SliderParam.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/SliderParam.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Subsection.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Subsection.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/TextParam.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/TextParam.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/__snapshots__/BasicDeploymentForm.test.tsx.snap delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/__snapshots__/BooleanParam.test.tsx.snap delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/__snapshots__/SliderParam.test.tsx.snap delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/__snapshots__/Subsection.test.tsx.snap delete mode 100644 dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/__snapshots__/TextParam.test.tsx.snap delete mode 100644 dashboard/src/components/DeploymentFormBody/DeploymentFormBody.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/Differential.scss delete mode 100644 dashboard/src/components/DeploymentFormBody/Differential.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/Differential.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/DifferentialSelector.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/DifferentialSelector.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/DifferentialTab.test.tsx delete mode 100644 dashboard/src/components/DeploymentFormBody/DifferentialTab.tsx delete mode 100644 dashboard/src/components/Slider/Slider.tsx delete mode 100644 dashboard/src/components/Slider/components.tsx diff --git a/dashboard/src/components/DeploymentFormBody/AdvancedDeploymentForm.tsx b/dashboard/src/components/DeploymentFormBody/AdvancedDeploymentForm.tsx deleted file mode 100644 index 82d8ea865f1..00000000000 --- a/dashboard/src/components/DeploymentFormBody/AdvancedDeploymentForm.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import MonacoEditor from "react-monaco-editor"; -import { useSelector } from "react-redux"; -import { SupportedThemes } from "shared/Config"; -import { IStoreState } from "shared/types"; - -export interface IAdvancedDeploymentForm { - appValues?: string; - handleValuesChange: (value: string) => void; - children?: JSX.Element; -} - -function AdvancedDeploymentForm(props: IAdvancedDeploymentForm) { - let timeout: NodeJS.Timeout; - const onChange = (value: string) => { - // Gather changes before submitting - clearTimeout(timeout); - timeout = setTimeout(() => props.handleValuesChange(value), 500); - }; - const { - config: { theme }, - } = useSelector((state: IStoreState) => state); - - return ( -
- - {props.children} -
- ); -} - -export default AdvancedDeploymentForm; diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.scss b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.scss deleted file mode 100644 index 8cde88ea6c9..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.scss +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -.description { - display: block; - color: var(--cds-global-typography-color-300, #454545); - font-size: 0.9em; -} - -.subsection { - padding: 10px; - border: 2px solid var(--cds-alias-object-border-color, #f1f1f1); - border-radius: 5px; -} - -.react-switch { - margin: 0 10px 0 0; - vertical-align: middle; -} - -.block::before { - display: inline-block; - height: 100%; - content: ""; - vertical-align: middle; -} - -.centered { - display: inline-block; -} - -.basic-deployment-form-param { - padding: 0.6rem 0; -} - -.deployment-form { - margin-top: 1rem; - margin-bottom: 1rem; -} - -.deployment-form-label { - margin-bottom: 0.2rem; - font-weight: 600; - - &-text-param { - display: block; - } -} - -.deployment-form-text-input { - min-width: 30vw; -} - -.param-separator { - border: 1px solid var(--cds-alias-object-border-color, #f1f1f1); -} - -.slider-block { - display: flex; - - .slider-input-and-unit { - margin-left: 0.6rem; - } - - .slider-content { - width: 30vw; - margin-left: 10px; - } - - .slider-input { - width: 50%; - } -} diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.test.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.test.tsx deleted file mode 100644 index 8a1cd327bdc..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.test.tsx +++ /dev/null @@ -1,485 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { mount } from "enzyme"; -import { Slider } from "react-compound-slider"; -import { DeploymentEvent, IBasicFormParam, IBasicFormSliderParam } from "shared/types"; -import BasicDeploymentForm from "./BasicDeploymentForm"; -import Subsection from "./Subsection"; - -jest.useFakeTimers(); - -const defaultProps = { - deploymentEvent: "install" as DeploymentEvent, - params: [], - handleBasicFormParamChange: jest.fn(() => jest.fn()), - appValues: "", - handleValuesChange: jest.fn(), -}; - -[ - { - description: "renders a basic deployment with a username", - params: [{ path: "wordpressUsername", value: "user" } as IBasicFormParam], - }, - { - description: "renders a basic deployment with a password", - params: [{ path: "wordpressPassword", value: "sserpdrow" } as IBasicFormParam], - }, - { - description: "renders a basic deployment with a email", - params: [{ path: "wordpressEmail", value: "user@example.com" } as IBasicFormParam], - }, - { - description: "renders a basic deployment with a generic string", - params: [{ path: "blogName", value: "my-blog", type: "string" } as IBasicFormParam], - }, - { - description: "renders a basic deployment with custom configuration", - params: [ - { - path: "configuration", - value: "First line\nSecond line", - render: "textArea", - type: "string", - } as IBasicFormParam, - ], - }, - { - description: "renders a basic deployment with a disk size", - params: [ - { - path: "size", - value: "10Gi", - type: "string", - render: "slider", - } as IBasicFormParam, - ], - }, - { - description: "renders a basic deployment with a integer disk size", - params: [ - { - path: "size", - value: 10, - type: "integer", - render: "slider", - } as IBasicFormParam, - ], - }, - { - description: "renders a basic deployment with a number disk size", - params: [ - { - path: "size", - value: 10.0, - type: "number", - render: "slider", - } as IBasicFormParam, - ], - }, - { - description: "renders a basic deployment with slider parameters", - params: [ - { - path: "size", - value: "10Gi", - type: "string", - render: "slider", - sliderMin: 1, - sliderMax: 100, - sliderStep: 1, - sliderUnit: "Gi", - } as IBasicFormSliderParam, - ], - }, - { - description: "renders a basic deployment with username, password, email and a generic string", - params: [ - { path: "wordpressUsername", value: "user" } as IBasicFormParam, - { path: "wordpressPassword", value: "sserpdrow" } as IBasicFormParam, - { path: "wordpressEmail", value: "user@example.com" } as IBasicFormParam, - { path: "blogName", value: "my-blog", type: "string" } as IBasicFormParam, - ], - }, - { - description: "renders a basic deployment with a generic boolean", - params: [{ path: "enableMetrics", value: true, type: "boolean" } as IBasicFormParam], - }, - { - description: "renders a basic deployment with a generic number", - params: [{ path: "replicas", value: 1, type: "integer" } as IBasicFormParam], - }, -].forEach(t => { - it(t.description, () => { - const onChange = jest.fn(); - const handleBasicFormParamChange = jest.fn(() => onChange); - const wrapper = mount( - , - ); - expect(wrapper).toMatchSnapshot(); - - t.params.forEach((param, i) => { - let input = wrapper.find(`input#${param.path}-${i}`); - switch (param.type) { - case "number": - case "integer": - if (param.render === "slider") { - expect(wrapper.find(Slider)).toExist(); - break; - } - expect(input.prop("type")).toBe("number"); - break; - case "string": - if (param.render === "slider") { - expect(wrapper.find(Slider)).toExist(); - break; - } - if (param.render === "textArea") { - input = wrapper.find(`textarea#${param.path}-${i}`); - expect(input).toExist(); - break; - } - if (param.path.match("Password")) { - expect(input.prop("type")).toBe("password"); - break; - } - expect(input.prop("type")).toBe("string"); - break; - default: - // Ignore the rest of cases - } - input.simulate("change"); - const mockCalls = handleBasicFormParamChange.mock.calls; - expect(mockCalls[i]).toEqual([param]); - jest.runAllTimers(); - expect(onChange.mock.calls.length).toBe(i + 1); - }); - }); -}); - -it("should render an external database section", () => { - const params = [ - { - path: "edbs", - value: {}, - type: "object", - children: [{ path: "mariadb.enabled", value: {}, type: "boolean" }], - } as IBasicFormParam, - ]; - const wrapper = mount(); - - const dbsec = wrapper.find(Subsection); - expect(dbsec).toExist(); -}); - -it("should hide an element if it depends on a param (string)", () => { - const params = [ - { - path: "foo", - type: "string", - hidden: "bar", - }, - { - path: "bar", - type: "boolean", - }, - ] as IBasicFormParam[]; - const appValues = "foo: 1\nbar: true"; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); -}); - -it("should hide an element if it depends on a single param (object)", () => { - const params = [ - { - path: "foo", - type: "string", - hidden: { - value: "enabled", - path: "bar", - }, - }, - { - path: "bar", - type: "string", - }, - ] as IBasicFormParam[]; - const appValues = "foo: 1\nbar: enabled"; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); -}); - -it("should hide an element using hidden path and values even if it is not present in values.yaml (simple)", () => { - const params = [ - { - default: "a", - enum: ["a", "b"], - path: "dropdown", - type: "string", - value: "a", - }, - { - hidden: { path: "dropdown", value: "b" }, - path: "a", - type: "string", - }, - { - hidden: { path: "dropdown", value: "a" }, - path: "b", - type: "string", - }, - ] as IBasicFormParam[]; - const appValues = ""; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); - expect(hiddenParam.text()).toBe("b"); -}); - -it("should hide an element using hidden path and values even if it is not present in values.yaml (different depth levels)", () => { - const params = [ - { - default: "a", - enum: ["a", "b"], - path: "dropdown", - type: "string", - value: "a", - }, - { - hidden: { path: "secondLevelProperties/2dropdown", value: "2b" }, - path: "a", - type: "string", - }, - { - hidden: { path: "secondLevelProperties/2dropdown", value: "2a" }, - path: "b", - type: "string", - }, - { - default: "2a", - enum: ["2a", "2b"], - path: "secondLevelProperties/2dropdown", - type: "string", - value: "2a", - }, - { - hidden: { path: "dropdown", value: "b" }, - path: "secondLevelProperties/2a", - type: "string", - }, - { - hidden: { path: "dropdown", value: "a" }, - path: "secondLevelProperties/2b", - type: "string", - }, - ] as IBasicFormParam[]; - const appValues = ""; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); - expect(hiddenParam.filterWhere(p => p.text().includes("b"))).toExist(); - expect(hiddenParam.filterWhere(p => p.text().includes("2b"))).toExist(); -}); - -it("should hide an element if it depends on multiple params (AND) (object)", () => { - const params = [ - { - path: "foo", - type: "string", - hidden: { - conditions: [ - { - value: "enabled", - path: "bar", - }, - { - value: "disabled", - path: "baz", - }, - ], - operator: "and", - }, - }, - { - path: "bar", - type: "string", - }, - ] as IBasicFormParam[]; - const appValues = "foo: 1\nbar: enabled\nbaz: disabled"; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); -}); - -it("should hide an element if it depends on multiple params (OR) (object)", () => { - const params = [ - { - path: "foo", - type: "string", - hidden: { - conditions: [ - { - value: "enabled", - path: "bar", - }, - { - value: "disabled", - path: "baz", - }, - ], - operator: "or", - }, - }, - { - path: "bar", - type: "string", - }, - ] as IBasicFormParam[]; - const appValues = "foo: 1\nbar: enabled\nbaz: enabled"; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); -}); - -it("should hide an element if it depends on multiple params (NOR) (object)", () => { - const params = [ - { - path: "foo", - type: "string", - hidden: { - conditions: [ - { - value: "enabled", - path: "bar", - }, - { - value: "disabled", - path: "baz", - }, - ], - operator: "nor", - }, - }, - { - path: "bar", - type: "string", - }, - ] as IBasicFormParam[]; - const appValues = "foo: 1\nbar: disabled\nbaz: enabled"; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); -}); - -it("should hide an element if it depends on the deploymentEvent (install | upgrade) (object)", () => { - const params = [ - { - path: "foo", - type: "string", - hidden: { - event: "upgrade", - }, - }, - ] as IBasicFormParam[]; - const appValues = "foo: 1\nbar: disabled\nbaz: enabled"; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); -}); - -it("should NOT hide an element if it depends on the deploymentEvent (install | upgrade) (object)", () => { - const params = [ - { - path: "foo", - type: "string", - hidden: { - event: "upgrade", - }, - }, - ] as IBasicFormParam[]; - const appValues = "foo: 1\nbar: disabled\nbaz: enabled"; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).not.toExist(); -}); - -it("should hide an element if it depends on deploymentEvent (install | upgrade) combined with multiple params (object)", () => { - const params = [ - { - path: "foo", - type: "string", - hidden: { - conditions: [ - { - event: "upgrade", - }, - { - value: "enabled", - path: "bar", - }, - ], - operator: "or", - }, - }, - { - path: "bar", - type: "string", - }, - ] as IBasicFormParam[]; - const appValues = "foo: 1\nbar: disabled"; - const wrapper = mount( - , - ); - - const hiddenParam = wrapper.find("div").filterWhere(p => p.prop("hidden") === true); - expect(hiddenParam).toExist(); -}); diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.tsx deleted file mode 100644 index 4d72c4decbb..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BasicDeploymentForm.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { DeploymentEvent, IBasicFormParam } from "shared/types"; -import "./BasicDeploymentForm.css"; -import Param from "./Param"; - -export interface IBasicDeploymentFormProps { - deploymentEvent: DeploymentEvent; - params: IBasicFormParam[]; - handleBasicFormParamChange: ( - p: IBasicFormParam, - ) => (e: React.FormEvent) => void; - handleValuesChange: (value: string) => void; - appValues: string; -} - -function BasicDeploymentForm(props: IBasicDeploymentFormProps) { - return ( -
- {props.params.map((param, i) => { - const id = `${param.path}-${i}`; - return ( -
- -
-
- ); - })} -
- ); -} - -export default BasicDeploymentForm; diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BooleanParam.test.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BooleanParam.test.tsx deleted file mode 100644 index 2845252161c..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BooleanParam.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { mount } from "enzyme"; -import { IBasicFormParam } from "shared/types"; -import BooleanParam from "./BooleanParam"; - -const param = { path: "enableMetrics", value: true, type: "boolean" } as IBasicFormParam; -const defaultProps = { - id: "foo", - label: "Enable Metrics", - param, - handleBasicFormParamChange: jest.fn(), -}; - -it("should render a boolean param with title and description", () => { - const wrapper = mount(); - const s = wrapper.find(".react-switch").first(); - expect(s.prop("checked")).toBe(defaultProps.param.value); - expect(wrapper).toMatchSnapshot(); -}); - -it("should send a checkbox event to handleBasicFormParamChange", () => { - const handler = jest.fn(); - const handleBasicFormParamChange = jest.fn().mockReturnValue(handler); - const wrapper = mount( - , - ); - const s = wrapper.find(".react-switch").first(); - - (s.prop("onChange") as any)(false); - - expect(handleBasicFormParamChange.mock.calls[0][0]).toEqual({ - path: "enableMetrics", - type: "boolean", - value: true, - }); - expect(handler.mock.calls[0][0]).toMatchObject({ - currentTarget: { value: "false", type: "checkbox", checked: false }, - }); -}); diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BooleanParam.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BooleanParam.tsx deleted file mode 100644 index b8ebe704656..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/BooleanParam.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import Switch from "react-switch"; -import { IBasicFormParam } from "shared/types"; - -export interface IStringParamProps { - id: string; - label: string; - param: IBasicFormParam; - handleBasicFormParamChange: ( - p: IBasicFormParam, - ) => (e: React.FormEvent) => void; -} - -function BooleanParam({ id, param, label, handleBasicFormParamChange }: IStringParamProps) { - // handleChange transform the event received by the Switch component to a checkbox event - const handleChange = (checked: boolean) => { - const event = { - currentTarget: { value: String(checked), type: "checkbox", checked }, - } as React.FormEvent; - handleBasicFormParamChange(param)(event); - }; - - return ( - - ); -} - -export default BooleanParam; diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/CustomFormParam.test.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/CustomFormParam.test.tsx deleted file mode 100644 index cfaa63bf111..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/CustomFormParam.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { getStore, mountWrapper } from "shared/specs/mountWrapper"; -import { IBasicFormParam, IStoreState } from "shared/types"; -import { CustomComponent } from "../../../RemoteComponent"; -import CustomFormComponentLoader from "./CustomFormParam"; - -const param = { - path: "enableMetrics", - value: true, - type: "boolean", - customComponent: { - className: "test", - }, -} as IBasicFormParam; - -const defaultProps = { - param, - handleBasicFormParamChange: jest.fn(), -}; - -const defaultState = { - config: { remoteComponentsUrl: "" }, -}; - -// Ensure remote-component doesn't trigger external requests during this test. -const mockOpen = jest.fn(); -const xhrMock: Partial = { - open: mockOpen, - send: jest.fn(), - setRequestHeader: jest.fn(), - readyState: 4, - status: 200, - response: "Hello World!", -}; - -beforeAll((): void => { - jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => xhrMock as XMLHttpRequest); -}); -afterEach((): void => { - mockOpen.mockReset(); -}); - -it("should render a custom form component", () => { - const wrapper = mountWrapper( - getStore(defaultState), - , - ); - expect(wrapper.find(CustomFormComponentLoader)).toExist(); -}); - -it("should render the remote component", () => { - const wrapper = mountWrapper( - getStore(defaultState), - , - ); - expect(wrapper.find(CustomComponent)).toExist(); -}); - -it("should render the remote component with the default URL", () => { - const wrapper = mountWrapper( - getStore(defaultState), - , - ); - expect(wrapper.find(CustomComponent)).toExist(); - expect(wrapper.find(CustomComponent).prop("url")).toContain("custom_components.js"); -}); - -it("should render the remote component with the URL if set in the config", () => { - const wrapper = mountWrapper( - getStore({ - config: { remoteComponentsUrl: "www.thiswebsite.com" }, - } as Partial), - , - ); - expect(wrapper.find(CustomComponent).prop("url")).toBe("www.thiswebsite.com"); - expect(xhrMock.open).toHaveBeenCalledWith("GET", "www.thiswebsite.com", true); -}); diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Param.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Param.tsx deleted file mode 100644 index a323f886ffc..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Param.tsx +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { isArray } from "lodash"; -import React from "react"; -import { getValue } from "shared/schema"; -import { DeploymentEvent, IBasicFormParam, IBasicFormSliderParam } from "shared/types"; -import BooleanParam from "./BooleanParam"; -import CustomFormComponentLoader from "./CustomFormParam"; -import SliderParam from "./SliderParam"; -import Subsection from "./Subsection"; -import TextParam from "./TextParam"; - -interface IParamProps { - appValues: string; - param: IBasicFormParam; - allParams: IBasicFormParam[]; - id: string; - handleBasicFormParamChange: ( - p: IBasicFormParam, - ) => (e: React.FormEvent) => void; - handleValuesChange: (value: string) => void; - deploymentEvent: DeploymentEvent; -} - -export default function Param({ - appValues, - param, - allParams, - id, - handleBasicFormParamChange, - handleValuesChange, - deploymentEvent, -}: IParamProps) { - let paramComponent: JSX.Element = <>; - - const isHidden = () => { - const hidden = param.hidden; - switch (typeof hidden) { - case "string": - // If hidden is a string, it points to the value that should be true - return evalCondition(hidden); - case "object": - // Two type of supported objects - // A single condition: {value: string, path: any} - // An array of conditions: {conditions: Array<{value: string, path: any}, operator: string} - if (hidden.conditions?.length > 0) { - // If hidden is an object, a different logic should be applied - // based on the operator - switch (hidden.operator) { - case "and": - // Every value matches the referenced - // value (via jsonpath) in all the conditions - return hidden.conditions.every(c => evalCondition(c.path, c.value, c.event)); - case "or": - // It is enough if the value matches the referenced - // value (via jsonpath) in any of the conditions - return hidden.conditions.some(c => evalCondition(c.path, c.value, c.event)); - case "nor": - // Every value mismatches the referenced - // value (via jsonpath) in any of the conditions - return hidden.conditions.every(c => !evalCondition(c.path, c.value, c.event)); - default: - // we consider 'and' as the default operator - return hidden.conditions.every(c => evalCondition(c.path, c.value, c.event)); - } - } else { - return evalCondition(hidden.path, hidden.value, hidden.event); - } - case "undefined": - return false; - } - }; - - const getParamMatchingPath = (params: IBasicFormParam[], path: string): any => { - let targetParam; - for (const p of params) { - if (p.path === path) { - targetParam = p; - break; - } else if (p.children && p.children?.length > 0) { - targetParam = getParamMatchingPath(p.children, path); - } - } - return targetParam; - }; - - const evalCondition = ( - path: string, - expectedValue?: any, - paramDeploymentEvent?: DeploymentEvent, - ): boolean => { - if (paramDeploymentEvent == null) { - let val = getValue(appValues, path); - // retrieve the value that the property pointed by path should have to be hidden. - // https://github.com/vmware-tanzu/kubeapps/issues/1913 - if (val === undefined) { - const target = getParamMatchingPath(allParams, path); - val = target?.value; - } - return val === (expectedValue ?? true); - } else { - return paramDeploymentEvent === deploymentEvent; - } - }; - - // Return early for custom components - if (param.customComponent) { - return ( - - ); - } - - // If the type of the param is an array, represent it as its first type - const type = isArray(param.type) ? param.type[0] : param.type; - if (type === "boolean") { - paramComponent = ( - - ); - } else if (type === "object") { - paramComponent = ( - - ); - } else if (param.render === "slider") { - const p = param as IBasicFormSliderParam; - paramComponent = ( - - ); - } else if (param.render === "textArea") { - paramComponent = ( - - ); - } else { - const label = param.title || param.path; - let inputType = "string"; - if (type === "integer") { - inputType = "number"; - } - if ( - type === "string" && - (param.render === "password" || label.toLowerCase().includes("password")) - ) { - inputType = "password"; - } - paramComponent = ( - - ); - } - - return ( - - ); -} diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/SliderParam.test.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/SliderParam.test.tsx deleted file mode 100644 index dd4bb8972fc..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/SliderParam.test.tsx +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { shallow } from "enzyme"; -import React from "react"; -import { IBasicFormParam } from "shared/types"; -import Slider from "../../Slider"; -import SliderParam from "./SliderParam"; - -const defaultProps = { - id: "disk", - label: "Disk Size", - handleBasicFormParamChange: jest.fn(() => jest.fn()), - min: 1, - max: 100, - step: 1, - unit: "Gi", -}; - -const params: IBasicFormParam[] = [ - { - value: "10Gi", - type: "string", - path: "disk", - }, - { - value: 10, - type: "integer", - path: "disk", - }, - { - value: 10.0, - type: "number", - path: "disk", - }, -]; - -it("renders a disk size param with a default value", () => { - params.forEach(param => { - const wrapper = shallow(); - expect(wrapper.find(Slider).prop("values")).toBe(10); - expect(wrapper).toMatchSnapshot(); - }); -}); - -describe("when changing the slide", () => { - it("changes the value of the string param", () => { - params.forEach(param => { - const cloneParam = { ...param } as IBasicFormParam; - const expected = param.type === "string" ? "20Gi" : 20; - - const handleBasicFormParamChange = jest.fn(() => { - cloneParam.value = expected; - return jest.fn(); - }); - - const wrapper = shallow( - , - ); - - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const slider = wrapper.find(Slider); - (slider.prop("onChange") as (values: number[]) => void)([20]); - - expect(cloneParam.value).toBe(expected); - expect(handleBasicFormParamChange.mock.calls[0]).toEqual([ - { value: expected, type: param.type, path: param.path }, - ]); - }); - }); - - it("changes the value of the string param without unit", () => { - params.forEach(param => { - const cloneParam = { ...param } as IBasicFormParam; - const expected = param.type === "string" ? "20" : 20; - - const handleBasicFormParamChange = jest.fn(() => { - cloneParam.value = expected; - return jest.fn(); - }); - - const wrapper = shallow( - , - ); - - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const slider = wrapper.find(Slider); - (slider.prop("onChange") as (values: number[]) => void)([20]); - - expect(cloneParam.value).toBe(expected); - expect(handleBasicFormParamChange.mock.calls[0]).toEqual([ - { value: expected, type: param.type, path: param.path }, - ]); - }); - }); - - it("changes the value of the string param with the step defined", () => { - params.forEach(param => { - const cloneProps = { ...defaultProps, step: 10 }; - const cloneParam = { ...param } as IBasicFormParam; - const expected = param.type === "string" ? "20Gi" : 20; - - const handleBasicFormParamChange = jest.fn(() => { - cloneParam.value = expected; - return jest.fn(); - }); - - const wrapper = shallow( - , - ); - - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const slider = wrapper.find(Slider); - (slider.prop("onChange") as (values: number[]) => void)([2]); - - expect(cloneParam.value).toBe(expected); - expect(handleBasicFormParamChange.mock.calls[0]).toEqual([ - { value: expected, type: param.type, path: param.path }, - ]); - }); - }); -}); - -it("updates state but does not change param value during slider update (only when dropped in a point)", () => { - params.forEach(param => { - const handleBasicFormParamChange = jest.fn(); - const wrapper = shallow( - , - ); - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const slider = wrapper.find(Slider); - (slider.prop("onUpdate") as (values: number[]) => void)([20]); - - expect(wrapper.find(Slider).prop("values")).toBe(20); - expect(handleBasicFormParamChange).not.toHaveBeenCalled(); - }); -}); - -describe("when changing the value in the input", () => { - it("parses a number and forwards it", () => { - params.forEach(param => { - const valueChange = jest.fn(); - const handleBasicFormParamChange = jest.fn(() => valueChange); - const wrapper = shallow( - , - ); - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const input = wrapper.find("input#disk"); - const event = { currentTarget: { value: "20" } } as React.FormEvent; - (input.prop("onChange") as (e: React.FormEvent) => void)(event); - - expect(wrapper.find(Slider).prop("values")).toBe(20); - - const expected = param.type === "string" ? "20Gi" : 20; - expect(valueChange.mock.calls[0]).toEqual([{ currentTarget: { value: expected } }]); - }); - }); - - it("parses a number and forwards it without unit", () => { - params.forEach(param => { - const valueChange = jest.fn(); - const handleBasicFormParamChange = jest.fn(() => valueChange); - const wrapper = shallow( - , - ); - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const input = wrapper.find("input#disk"); - const event = { currentTarget: { value: "20" } } as React.FormEvent; - (input.prop("onChange") as (e: React.FormEvent) => void)(event); - - expect(wrapper.find(Slider).prop("values")).toBe(20); - - const expected = param.type === "string" ? "20" : 20; - expect(valueChange.mock.calls[0]).toEqual([{ currentTarget: { value: expected } }]); - }); - }); - - it("ignores values in the input that are not digits", () => { - params.forEach(param => { - const valueChange = jest.fn(); - const handleBasicFormParamChange = jest.fn(() => valueChange); - const wrapper = shallow( - , - ); - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const input = wrapper.find("input#disk"); - const event = { currentTarget: { value: "foo20*#@$" } } as React.FormEvent; - (input.prop("onChange") as (e: React.FormEvent) => void)(event); - - expect(wrapper.find(Slider).prop("values")).toBe(20); - - const expected = param.type === "string" ? "20Gi" : 20; - expect(valueChange.mock.calls[0]).toEqual([{ currentTarget: { value: expected } }]); - }); - }); - - it("accept decimal values", () => { - params.forEach(param => { - const valueChange = jest.fn(); - const handleBasicFormParamChange = jest.fn(() => valueChange); - const wrapper = shallow( - , - ); - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const input = wrapper.find("input#disk"); - const event = { currentTarget: { value: "20.5" } } as React.FormEvent; - (input.prop("onChange") as (e: React.FormEvent) => void)(event); - - expect(wrapper.find(Slider).prop("values")).toBe(20.5); - - const expected = param.type === "string" ? "20.5Gi" : 20.5; - expect(valueChange.mock.calls[0]).toEqual([{ currentTarget: { value: expected } }]); - }); - }); - - it("modifies the max value of the slider if the input is greater than 100", () => { - params.forEach(param => { - const valueChange = jest.fn(); - const handleBasicFormParamChange = jest.fn(() => valueChange); - const wrapper = shallow( - , - ); - expect(wrapper.find(Slider).prop("values")).toBe(10); - - const input = wrapper.find("input#disk"); - const event = { currentTarget: { value: "200" } } as React.FormEvent; - (input.prop("onChange") as (e: React.FormEvent) => void)(event); - - expect(wrapper.find(Slider).prop("values")).toBe(200); - const slider = wrapper.find(Slider); - expect(slider.prop("max")).toBe(200); - }); - }); -}); - -it("uses the param minimum and maximum if defined", () => { - params.forEach(param => { - const clonedParam = { ...param } as IBasicFormParam; - clonedParam.minimum = 5; - clonedParam.maximum = 50; - - const wrapper = shallow(); - - const slider = wrapper.find(Slider); - expect(slider.prop("min")).toBe(5); - expect(slider.prop("max")).toBe(50); - }); -}); - -it("defaults to the min if the value is undefined", () => { - params.forEach(param => { - const cloneParam = { ...param } as IBasicFormParam; - cloneParam.value = undefined; - - const wrapper = shallow(); - expect(wrapper.find(Slider).prop("values")).toBe(5); - }); -}); diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/SliderParam.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/SliderParam.tsx deleted file mode 100644 index a8b95b6d531..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/SliderParam.tsx +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { useEffect, useState } from "react"; -import { IBasicFormParam } from "shared/types"; -import Slider from "../../Slider"; - -export interface ISliderParamProps { - id: string; - label: string; - param: IBasicFormParam; - unit: string; - min: number; - max: number; - step: number; - handleBasicFormParamChange: ( - p: IBasicFormParam, - ) => (e: React.FormEvent) => void; -} - -export interface ISliderParamState { - value: number; -} - -function toNumber(value: string | number) { - // Force to return a Number from a string removing any character that is not a digit - return typeof value === "number" ? value : Number(value.replace(/[^\d.]/g, "")); -} - -function getDefaultValue(min: number, value?: string) { - return (value && toNumber(value)) || min; -} - -function SliderParam({ - id, - label, - param, - unit, - min, - max, - step, - handleBasicFormParamChange, -}: ISliderParamProps) { - const [value, setValue] = useState(getDefaultValue(min, param.value)); - - useEffect(() => { - setValue(getDefaultValue(min, param.value)); - }, [param, min]); - - const handleParamChange = (newValue: number) => { - handleBasicFormParamChange(param)({ - currentTarget: { - value: param.type === "string" ? `${newValue}${unit}` : newValue, - }, - } as React.FormEvent); - }; - - // onChangeSlider is run when the slider is dropped at one point - // at that point we update the parameter - const onChangeSlider = (values: readonly number[]) => { - handleParamChange(values[0]); - }; - - // onUpdateSlider is run when dragging the slider - // we just update the state here for a faster response - const onUpdateSlider = (values: readonly number[]) => { - setValue(values[0]); - }; - - const onChangeInput = (e: React.FormEvent) => { - const numberValue = toNumber(e.currentTarget.value); - setValue(numberValue); - handleParamChange(numberValue); - }; - - return ( -
- -
- ); -} - -export default SliderParam; diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Subsection.test.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Subsection.test.tsx deleted file mode 100644 index 5babd4a7faa..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Subsection.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { shallow } from "enzyme"; -import { IBasicFormParam } from "shared/types"; -import Subsection, { ISubsectionProps } from "./Subsection"; - -const defaultProps = { - label: "Enable an external database", - param: { - children: [ - { - path: "externalDatabase.database", - type: "string", - value: "bitnami_wordpress", - }, - { path: "externalDatabase.host", type: "string", value: "localhost" }, - { path: "externalDatabase.password", type: "string" }, - { path: "externalDatabase.port", type: "integer", value: 3306 }, - { - path: "externalDatabase.user", - type: "string", - value: "bn_wordpress", - }, - { - path: "mariadb.enabled", - title: "Enable External Database", - type: "boolean", - value: true, - } as IBasicFormParam, - ], - path: "externalDatabase", - title: "External Database Details", - description: "description of the param", - type: "object", - } as IBasicFormParam, - allParams: [], - appValues: "externalDatabase: {}", - deploymentEvent: "install", - handleValuesChange: jest.fn(), -} as ISubsectionProps; - -it("should render a external database section", () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Subsection.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Subsection.tsx deleted file mode 100644 index 2f7159a0a9f..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/Subsection.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { setValue } from "shared/schema"; -import { DeploymentEvent, IBasicFormParam } from "shared/types"; -import { getValueFromEvent } from "shared/utils"; -import Param from "./Param"; - -export interface ISubsectionProps { - label: string; - param: IBasicFormParam; - allParams: IBasicFormParam[]; - appValues: string; - deploymentEvent: DeploymentEvent; - handleValuesChange: (value: string) => void; -} - -function Subsection({ - label, - param, - allParams, - appValues, - deploymentEvent, - handleValuesChange, -}: ISubsectionProps) { - const handleChildrenParamChange = (childrenParam: IBasicFormParam) => { - return (e: React.FormEvent) => { - const value = getValueFromEvent(e); - param.children = param.children!.map(p => - p.path === childrenParam.path ? { ...childrenParam, value } : p, - ); - handleValuesChange(setValue(appValues, childrenParam.path, value)); - }; - }; - - return ( -
-
- - {param.description && ( - <> -
- {param.description} - - )} -
- {param.children && - param.children.map((childrenParam, i) => { - const id = `${childrenParam.path}-${i}`; - return ( - - ); - })} -
- ); -} - -export default Subsection; diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/TextParam.test.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/TextParam.test.tsx deleted file mode 100644 index 641b1299f01..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/TextParam.test.tsx +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { mount } from "enzyme"; -import React from "react"; -import { act } from "react-dom/test-utils"; -import { IBasicFormParam } from "shared/types"; -import TextParam from "./TextParam"; - -jest.useFakeTimers(); - -const stringParam = { path: "username", value: "user", type: "string" } as IBasicFormParam; -const stringProps = { - id: "foo", - label: "Username", - param: stringParam, - handleBasicFormParamChange: jest.fn().mockReturnValue(jest.fn()), -}; - -it("should render a string parameter with title and description", () => { - const wrapper = mount(); - const input = wrapper.find("input"); - expect(input.prop("value")).toBe(stringProps.param.value); - expect(wrapper).toMatchSnapshot(); -}); - -it("should set the input type as number", () => { - const wrapper = mount(); - const input = wrapper.find("input"); - expect(input.prop("type")).toBe("number"); -}); - -it("should forward the proper value when using a string parameter", () => { - const handler = jest.fn(); - const handleBasicFormParamChange = jest.fn().mockReturnValue(handler); - const wrapper = mount( - , - ); - const input = wrapper.find("input"); - - const event = { currentTarget: { value: "" } } as React.FormEvent; - act(() => { - (input.prop("onChange") as any)(event); - }); - wrapper.update(); - jest.runAllTimers(); - - expect(handleBasicFormParamChange).toHaveBeenCalledWith({ - path: "username", - type: "string", - value: "user", - }); - expect(handler).toHaveBeenCalledWith(event); -}); - -it("should set the input value as empty if a string parameter value is not defined", () => { - const tparam = { path: "username", type: "string" } as IBasicFormParam; - const tprops = { - id: "foo", - name: "username", - label: "Username", - param: tparam, - handleBasicFormParamChange: jest.fn().mockReturnValue(jest.fn()), - }; - const wrapper = mount(); - const input = wrapper.find("input"); - expect(input.prop("value")).toBe(""); -}); - -const textAreaParam = { - path: "configuration", - value: "First line\nSecond line", - type: "string", -} as IBasicFormParam; -const textAreaProps = { - id: "bar", - label: "Configuration", - param: textAreaParam, - handleBasicFormParamChange: jest.fn().mockReturnValue(jest.fn()), - inputType: "textarea", -}; - -it("should render a textArea parameter with title and description", () => { - const wrapper = mount(); - const input = wrapper.find("textarea"); - expect(input.prop("value")).toBe(textAreaProps.param.value); - expect(wrapper).toMatchSnapshot(); -}); - -it("should forward the proper value when using a textArea parameter", () => { - const handler = jest.fn(); - const handleBasicFormParamChange = jest.fn().mockReturnValue(handler); - const wrapper = mount( - , - ); - const input = wrapper.find("textarea"); - - const event = { currentTarget: { value: "" } } as React.FormEvent; - act(() => { - (input.prop("onChange") as any)(event); - }); - wrapper.update(); - jest.runAllTimers(); - - expect(handleBasicFormParamChange).toHaveBeenCalledWith({ - path: "configuration", - type: "string", - value: "First line\nSecond line", - }); - expect(handler).toHaveBeenCalledWith(event); -}); - -it("should set the input value as empty if a textArea param value is not defined", () => { - const tparam = { path: "configuration", type: "string" } as IBasicFormParam; - const tprops = { - id: "foo", - name: "configuration", - label: "Configuration", - param: tparam, - handleBasicFormParamChange: jest.fn().mockReturnValue(jest.fn()), - inputType: "textarea", - }; - const wrapper = mount(); - const input = wrapper.find("textarea"); - expect(input.prop("value")).toBe(""); -}); - -it("should render a string parameter as select with option tags", () => { - const tparam = { - path: "databaseType", - value: "postgresql", - type: "string", - enum: ["mariadb", "postgresql"], - } as IBasicFormParam; - const tprops = { - id: "foo", - name: "databaseType", - label: "databaseType", - param: tparam, - handleBasicFormParamChange: jest.fn().mockReturnValue(jest.fn()), - }; - const wrapper = mount(); - const input = wrapper.find("select"); - - expect(wrapper.find("select").prop("value")).toBe(tparam.value); - if (tparam.enum != null) { - const options = input.find("option"); - expect(options.length).toBe(tparam.enum.length); - - for (let i = 0; i < tparam.enum.length; i++) { - const option = options.at(i); - expect(option.text()).toBe(tparam.enum[i]); - } - } -}); - -it("should forward the proper value when using a select", () => { - const tparam = { - path: "databaseType", - value: "postgresql", - type: "string", - enum: ["mariadb", "postgresql"], - } as IBasicFormParam; - const tprops = { - id: "foo", - name: "databaseType", - label: "databaseType", - param: tparam, - }; - const handler = jest.fn(); - const handleBasicFormParamChange = jest.fn().mockReturnValue(handler); - const wrapper = mount( - , - ); - const input = wrapper.find("select"); - - const event = { currentTarget: {} } as React.FormEvent; - act(() => { - (input.prop("onChange") as any)(event); - }); - - expect(handleBasicFormParamChange.mock.calls[0][0]).toEqual({ - path: "databaseType", - type: "string", - value: "postgresql", - enum: ["mariadb", "postgresql"], - }); - expect(handler.mock.calls[0][0]).toMatchObject(event); -}); - -it("a change in the param property should update the current value", () => { - const wrapper = mount(); - const input = wrapper.find("input"); - expect(input.prop("value")).toBe(""); - - wrapper.setProps({ - param: { - ...stringParam, - value: "foo", - }, - }); - wrapper.update(); - expect(wrapper.find("input").prop("value")).toBe("foo"); -}); - -it("a change in a number param property should update the current value", () => { - const numberParam = { path: "replicas", value: 0, type: "number" } as IBasicFormParam; - const wrapper = mount(); - const input = wrapper.find("input"); - expect(input.prop("value")).toBe(0); - - wrapper.setProps({ - param: { - ...numberParam, - value: 1, - }, - }); - wrapper.update(); - expect(wrapper.find("input").prop("value")).toBe(1); -}); diff --git a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/TextParam.tsx b/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/TextParam.tsx deleted file mode 100644 index 148d7fe63ca..00000000000 --- a/dashboard/src/components/DeploymentFormBody/BasicDeploymentForm/TextParam.tsx +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2019-2022 the Kubeapps contributors. -// SPDX-License-Identifier: Apache-2.0 - -import { isEmpty, isNumber } from "lodash"; -import { useEffect, useState } from "react"; -import { IBasicFormParam } from "shared/types"; - -export interface IStringParamProps { - id: string; - label: string; - inputType?: string; - param: IBasicFormParam; - handleBasicFormParamChange: ( - param: IBasicFormParam, - ) => (e: React.FormEvent) => void; -} - -function TextParam({ id, param, label, inputType, handleBasicFormParamChange }: IStringParamProps) { - const [value, setValue] = useState((param.value || "") as any); - const [valueModified, setValueModified] = useState(false); - const [timeout, setThisTimeout] = useState({} as NodeJS.Timeout); - const onChange = ( - e: React.FormEvent, - ) => { - setValue(e.currentTarget.value); - setValueModified(true); - // Gather changes before submitting - clearTimeout(timeout); - const func = handleBasicFormParamChange(param); - // The reference to target get lost, so we need to keep a copy - const targetCopy = { - currentTarget: { - value: e.currentTarget.value, - type: e.currentTarget.type, - }, - } as React.FormEvent; - setThisTimeout(setTimeout(() => func(targetCopy), 500)); - }; - - useEffect(() => { - if ((isNumber(param.value) || !isEmpty(param.value)) && !valueModified) { - setValue(param.value); - } - }, [valueModified, param.value]); - - let input = ( - - ); - if (inputType === "textarea") { - input =