diff --git a/cyclops-ui/src/components/pages/NewModule/NewModule.tsx b/cyclops-ui/src/components/pages/NewModule/NewModule.tsx index ae34a628..cbf04628 100644 --- a/cyclops-ui/src/components/pages/NewModule/NewModule.tsx +++ b/cyclops-ui/src/components/pages/NewModule/NewModule.tsx @@ -3,7 +3,6 @@ import { Alert, Button, Col, - Collapse, Divider, Form, Input, @@ -15,11 +14,11 @@ import { notification, } from "antd"; import axios from "axios"; -import { findMaps, flattenObjectKeys, mapsToArray } from "../../../utils/form"; +import { deepMerge, findMaps, flattenObjectKeys, mapsToArray } from "../../../utils/form"; import "./custom.css"; import defaultTemplate from "../../../static/img/default-template-icon.png"; -import YAML from "yaml"; +import YAML, { YAMLError } from "yaml"; import AceEditor from "react-ace"; @@ -79,12 +78,7 @@ const NewModule = () => { const [loadingTemplateInitialValues, setLoadingTemplateInitialValues] = useState(false); - var initLoadedFrom: string[]; - initLoadedFrom = []; - const [newFile, setNewFile] = useState(""); - const [loadedFrom, setLoadedFrom] = useState(initLoadedFrom); const [loadedValues, setLoadedValues] = useState(""); - const [loadingValuesFile, setLoadingValuesFile] = useState(false); const [loadingValuesModal, setLoadingValuesModal] = useState(false); const [templateStore, setTemplateStore] = useState([]); @@ -290,25 +284,6 @@ const NewModule = () => { loadTemplate(ts.ref.repo, ts.ref.path, ts.ref.version, ts.ref.sourceType); }; - const onLoadFromFile = () => { - setLoadingValuesFile(true); - setLoadedValues(""); - - if (newFile.trim() === "") { - setError({ - message: "Invalid values file", - description: "Values file can't be empty", - }); - setLoadingValuesFile(false); - return; - } - - setLoadingValuesModal(true); - - loadValues(newFile); - setLoadingValuesFile(false); - }; - function renderFormFields() { if (!loadingTemplate && !loadingTemplateInitialValues) { return ( @@ -333,56 +308,34 @@ const NewModule = () => { }; const handleImportValues = () => { - form.setFieldsValue( - mapsToArray(config.root.properties, YAML.parse(loadedValues)), - ); - setLoadedValues(""); - setLoadingValuesModal(false); - }; + let yamlValues = null; + try { + yamlValues = YAML.parse(loadedValues) + } catch(err: any) { + if (err instanceof YAMLError) { + setError({ + message: err.name, + description: err.message, + }); + return; + } - const renderLoadedFromFiles = () => { - if (loadedFrom.length === 0) { + setError({ + message: "Failed injecting YAML to values", + description: "check if YAML is correctly indented", + }); return; } - const files: {} | any = []; + const currentValues = findMaps(config.root.properties, form.getFieldsValue(), null); + const values = deepMerge(currentValues, yamlValues) - loadedFrom.forEach((value: string) => { - files.push(

{value}

); - }); - - return ( - + form.setFieldsValue( + mapsToArray(config.root.properties, values), ); - }; - - const loadValues = (fileName: string) => { - axios - .get(fileName) - .then((res) => { - setLoadedValues(res.data); - setError({ - message: "", - description: "", - }); - let tmp = loadedFrom; - tmp.push(newFile); - setLoadedFrom(tmp); - }) - .catch(function (error) { - // setLoadingTemplate(false); - // setSuccessLoad(false); - setError(mapResponseError(error)); - }); + setLoadedValues(""); + setLoadingValuesModal(false); + setError({message: "", description: ""}); }; const onFinishFailed = (errors: any) => { @@ -609,7 +562,7 @@ const NewModule = () => { !config.root.properties } > - Load values from file + Import values as YAML {" "} +
+ You can paste your values in YAML format here, and after submitting them, you can see them in the form and edit them further. + If you set a value in YAML that does not exist in the UI, it will not be applied to your Module. +
{ const testCases = [ @@ -38,3 +38,158 @@ describe("fileExtension", () => { } }); }); + +describe("deepMerge", () => { + const testCases = [ + { + description: "both source and target empty", + target: {}, + source: {}, + out: {}, + }, + { + description: "null target", + target: null, + source: {}, + out: {}, + }, + { + description: "null source", + target: {}, + source: null, + out: {}, + }, + { + description: "both source and target null", + target: null, + source: null, + out: {}, + }, + { + description: "target undefined", + target: undefined, + source: {}, + out: {}, + }, + { + description: "source undefined", + target: {}, + source: undefined, + out: {}, + }, + { + description: "both source and target undefined", + target: undefined, + source: undefined, + out: {}, + }, + { + description: "target has fields", + target: {name: "my-app"}, + source: {}, + out: {name: "my-app"}, + }, + { + description: "field overlap", + target: {name: "my-app"}, + source: {name: "another-app"}, + out: {name: "another-app"}, + }, + { + description: "no field overlap", + target: {name: "my-app"}, + source: {someField: "value"}, + out: {name: "my-app", someField: "value"}, + }, + { + description: "nested fields, no overlap", + target: {general: {image: "nginx", version: 3}}, + source: {someField: "value"}, + out: {general: {image: "nginx", version: 3}, someField: "value"}, + }, + { + description: "both have nested fields, no overlap", + target: {general: {image: "nginx", version: 3}}, + source: { someField: "value", networking: {expose: true, host: "example.com", serviceType: ""}}, + out: {general: {image: "nginx", version: 3}, someField: "value", networking: {expose: true, host: "example.com", serviceType: ""}}, + }, + { + description: "both have nested fields, no overlap, null value", + target: {general: {image: "nginx", version: 3}}, + source: { someField: "value", networking: {expose: true, host: "example.com", serviceType: null}}, + out: {general: {image: "nginx", version: 3}, someField: "value", networking: {expose: true, host: "example.com", serviceType: null}}, + }, + { + description: "both have nested fields, no overlap, undefined value", + target: {general: {image: "nginx", version: 3}}, + source: { someField: "value", networking: {expose: true, host: "example.com", serviceType: undefined}}, + out: {general: {image: "nginx", version: 3}, someField: "value", networking: {expose: true, host: "example.com", serviceType: undefined}}, + }, + { + description: "both have nested fields, overlap", + target: {general: {image: "nginx", version: 3}}, + source: { someField: "value", general: {image: "redis", version: 5}, networking: {expose: true, host: "example.com", serviceType: undefined}}, + out: {general: {image: "redis", version: 5}, someField: "value", networking: {expose: true, host: "example.com", serviceType: undefined}}, + }, + { + description: "both have same nested fields", + target: { someField: "value", general: {image: "redis", version: 5}, networking: {expose: true, host: "example.com", serviceType: undefined}}, + source: { someField: "value", general: {image: "redis", version: 5}, networking: {expose: true, host: "example.com", serviceType: undefined}}, + out: { someField: "value", general: {image: "redis", version: 5}, networking: {expose: true, host: "example.com", serviceType: undefined}}, + }, + { + description: "target has arrays", + target: {myList: [1, 2, 3]}, + source: {}, + out: {myList: [1, 2, 3]}, + }, + { + description: "source has arrays", + target: {}, + source: {myList: [1, 2, 3]}, + out: {myList: [1, 2, 3]}, + }, + { + description: "both have arrays", + target: {myList: [4, 5, 6]}, + source: {myList: [1, 2, 3]}, + out: {myList: [1, 2, 3]}, + }, + { + description: "target has empty array", + target: {myList: []}, + source: {}, + out: {myList: []}, + }, + { + description: "source has empty array", + target: {}, + source: {myList: []}, + out: {myList: []}, + }, + { + description: "both have empyt arrays", + target: {myList: []}, + source: {myList: []}, + out: {myList: []}, + }, + { + description: "both have nested fields, target has arrays", + target: {general: {image: "nginx", version: 3}, myList: ["here", "I", "am"]}, + source: { someField: "value", networking: {expose: true, host: "example.com", serviceType: undefined}}, + out: {general: {image: "nginx", version: 3}, someField: "value", networking: {expose: true, host: "example.com", serviceType: undefined}, myList: ["here", "I", "am"]}, + }, + { + description: "both have nested fields, source has arrays", + target: {general: {image: "nginx", version: 3}}, + source: { someField: "value", networking: {expose: true, host: "example.com", serviceType: undefined}, myList: ["am", "I", "here"]}, + out: {general: {image: "nginx", version: 3}, someField: "value", networking: {expose: true, host: "example.com", serviceType: undefined}, myList: ["am", "I", "here"]}, + }, + ]; + + testCases.forEach((testCase) => { + it(testCase.description, () => { + expect(deepMerge(testCase.target, testCase.source)).toStrictEqual(testCase.out); + }); + }) +});