From 87e606a81291fa8b6cd930845570ef66a0fa24c0 Mon Sep 17 00:00:00 2001 From: Antonio Gamez Diaz Date: Thu, 20 Oct 2022 19:11:53 +0200 Subject: [PATCH] Add new param rederers Signed-off-by: Antonio Gamez Diaz --- .../Params/ArrayParam.tsx | 244 +++++++++++++----- .../Params/BooleanParam.tsx | 5 +- .../Params/TextParam.tsx | 195 ++++++++------ .../TabularSchemaEditorTableRenderer.tsx | 57 ++-- dashboard/src/shared/utils.ts | 6 +- 5 files changed, 319 insertions(+), 188 deletions(-) diff --git a/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/ArrayParam.tsx b/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/ArrayParam.tsx index 832433b2805..58e301c8bee 100644 --- a/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/ArrayParam.tsx +++ b/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/ArrayParam.tsx @@ -2,15 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 import { CdsButton } from "@cds/react/button"; +import { CdsControlMessage } from "@cds/react/forms"; import { CdsIcon } from "@cds/react/icon"; import { CdsInput } from "@cds/react/input"; import { CdsRange } from "@cds/react/range"; +import { CdsSelect } from "@cds/react/select"; import { CdsToggle } from "@cds/react/toggle"; import Column from "components/js/Column"; import Row from "components/js/Row"; +import { isEmpty } from "lodash"; import { useState } from "react"; -import { IBasicFormParam } from "shared/types"; -import { basicFormsDebounceTime } from "shared/utils"; +import { validateValuesSchema } from "shared/schema"; +import { IAjvValidateResult, IBasicFormParam } from "shared/types"; +import { basicFormsDebounceTime, getStringValue, getValueFromString } from "shared/utils"; export interface IArrayParamProps { id: string; @@ -23,12 +27,41 @@ export interface IArrayParamProps { ) => (e: React.FormEvent) => void; } +const getDefaultDataFromType = (type: string) => { + switch (type) { + case "number": + case "integer": + return 0; + case "boolean": + return false; + case "object": + return {}; + case "array": + return []; + case "string": + default: + return ""; + } +}; + export default function ArrayParam(props: IArrayParamProps) { const { id, label, type, param, step, handleBasicFormParamChange } = props; - const [currentArrayItems, setCurrentArrayItems] = useState<(string | number | boolean)[]>( - param.currentValue ? JSON.parse(param.currentValue) : [], - ); + const initCurrentValue = () => { + const currentValueInit = []; + if (param.minItems) { + for (let index = 0; index < param.minItems; index++) { + currentValueInit[index] = getDefaultDataFromType(type); + } + } + return currentValueInit; + }; + + const [currentArrayItems, setCurrentArrayItems] = useState< + (string | number | boolean | object | Array)[] + >(param.currentValue ? param.currentValue : initCurrentValue()); + const [validated, setValidated] = useState(); + const [timeout, setThisTimeout] = useState({} as NodeJS.Timeout); const setArrayChangesInParam = () => { @@ -37,90 +70,164 @@ export default function ArrayParam(props: IArrayParamProps) { // The reference to target get lost, so we need to keep a copy const targetCopy = { currentTarget: { - value: JSON.stringify(currentArrayItems), - type: "change", + value: getStringValue(currentArrayItems), + type: "array", }, } as React.FormEvent; setThisTimeout(setTimeout(() => func(targetCopy), basicFormsDebounceTime)); }; - const onChangeArrayItem = (index: number, value: string | number | boolean) => { + const onChangeArrayItem = ( + e: React.FormEvent, + index: number, + value: string | number | boolean | object | Array, + ) => { currentArrayItems[index] = value; setCurrentArrayItems([...currentArrayItems]); setArrayChangesInParam(); + + // twofold validation: using the json schema (with ajv) and the html5 validation + setValidated(validateValuesSchema(getStringValue(currentArrayItems), param.schema)); + e.currentTarget.reportValidity(); }; + const renderControlMsg = () => + !validated?.valid && + !isEmpty(validated?.errors) && ( + <> + + {validated?.errors?.map((e: any) => e?.message).join(", ")} + +
+ + ); + const renderInput = (type: string, index: number) => { - switch (type) { - case "number": - case "integer": - return ( - <> - + if (!isEmpty(param?.items?.enum)) { + return ( + <> + + + {renderControlMsg()} + + + ); + } else { + switch (type) { + case "number": + case "integer": + return ( + <> + + onChangeArrayItem(e, index, Number(e.currentTarget.value))} + value={Number(currentArrayItems[index])} + step={step} + /> + + + onChangeArrayItem(e, index, Number(e.currentTarget.value))} + value={Number(currentArrayItems[index])} + step={step} + /> + + + ); + case "boolean": + return ( + + onChangeArrayItem(e, index, e.currentTarget.checked)} + checked={!!currentArrayItems[index]} + /> + + ); + case "object": + return ( + onChangeArrayItem(index, Number(e.currentTarget.value))} - value={Number(currentArrayItems[index])} - step={param.schema?.type === "integer" ? 1 : 0.1} + value={getStringValue(currentArrayItems[index])} + onChange={e => + onChangeArrayItem(e, index, getValueFromString(e.currentTarget.value, "object")) + } /> - + ); + case "array": + return ( + onChangeArrayItem(index, Number(e.currentTarget.value))} - value={Number(currentArrayItems[index])} - step={param.schema?.type === "integer" ? 1 : 0.1} + value={getStringValue(currentArrayItems[index])} + onChange={e => + onChangeArrayItem(e, index, getValueFromString(e.currentTarget.value, "array")) + } /> - - - ); - case "boolean": - return ( - - onChangeArrayItem(index, e.currentTarget.checked)} - checked={!!currentArrayItems[index]} - /> - - ); - - // TODO(agamez): handle enums and objects in arrays - default: - return ( - - onChangeArrayItem(index, e.currentTarget.value)} - /> - - ); + + ); + case "string": + default: + return ( + + onChangeArrayItem(e, index, e.currentTarget.value)} + /> + + ); + } } }; - const onAddArrayItem = () => { - switch (type) { - case "number": - case "integer": - currentArrayItems.push(0); - break; - case "boolean": - currentArrayItems.push(false); - break; - default: - currentArrayItems.push(""); - break; - } + const onAddArrayItem = (type: string) => { + currentArrayItems.push(getDefaultDataFromType(type)); setCurrentArrayItems([...currentArrayItems]); setArrayChangesInParam(); }; @@ -135,7 +242,7 @@ export default function ArrayParam(props: IArrayParamProps) { onAddArrayItem(type)} action="flat" status="primary" size="sm" @@ -144,6 +251,7 @@ export default function ArrayParam(props: IArrayParamProps) { Add + {renderControlMsg()} {currentArrayItems?.map((_, index) => ( {renderInput(type, index)} diff --git a/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/BooleanParam.tsx b/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/BooleanParam.tsx index 827ee6b840f..4316433a663 100644 --- a/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/BooleanParam.tsx +++ b/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/BooleanParam.tsx @@ -7,6 +7,7 @@ import Column from "components/js/Column"; import Row from "components/js/Row"; import { useState } from "react"; import { IBasicFormParam } from "shared/types"; +import { getStringValue } from "shared/utils"; export interface IBooleanParamProps { id: string; @@ -19,7 +20,7 @@ export interface IBooleanParamProps { export default function BooleanParam(props: IBooleanParamProps) { const { id, label, param, handleBasicFormParamChange } = props; - const [currentValue, setCurrentValue] = useState(param.currentValue); + const [currentValue, setCurrentValue] = useState(param.currentValue || false); const [isValueModified, setIsValueModified] = useState(false); const onChange = (e: React.FormEvent) => { @@ -27,7 +28,7 @@ export default function BooleanParam(props: IBooleanParamProps) { const event = { currentTarget: { //convert the boolean "checked" prop to a normal "value" string one - value: e.currentTarget?.checked?.toString(), + value: getStringValue(e.currentTarget?.checked), type: "checkbox", }, } as React.FormEvent; diff --git a/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/TextParam.tsx b/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/TextParam.tsx index 75d5c316884..b2640444f16 100644 --- a/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/TextParam.tsx +++ b/dashboard/src/components/DeploymentForm/DeploymentFormBody/BasicDeploymentForm/TabularSchemaEditorTable/Params/TextParam.tsx @@ -11,63 +11,47 @@ import { isEmpty } from "lodash"; import { useState } from "react"; import { validateValuesSchema } from "shared/schema"; import { IAjvValidateResult, IBasicFormParam } from "shared/types"; -import { basicFormsDebounceTime } from "shared/utils"; +import { basicFormsDebounceTime, getStringValue, getValueFromString } from "shared/utils"; export interface ITextParamProps { id: string; label: string; - inputType?: "text" | "textarea" | string; + inputType?: "text" | "textarea" | "password" | string; param: IBasicFormParam; handleBasicFormParamChange: ( param: IBasicFormParam, ) => (e: React.FormEvent) => void; } -function getStringValue(param: IBasicFormParam, value?: any) { - if (["array", "object"].includes(param?.type)) { - return JSON.stringify(value || param?.currentValue); - } else { - return value?.toString() || param?.currentValue?.toString(); - } -} -function getValueFromString(param: IBasicFormParam, value: any) { - if (["array", "object"].includes(param?.type)) { - try { - return JSON.parse(value); - } catch (e) { - return value?.toString(); - } - } else { - return value?.toString(); - } -} - -function toStringValue(value: any) { - return JSON.stringify(value?.toString() || ""); -} - export default function TextParam(props: ITextParamProps) { const { id, label, inputType, param, handleBasicFormParamChange } = props; const [validated, setValidated] = useState(); - const [currentValue, setCurrentValue] = useState(getStringValue(param)); + const [currentValue, setCurrentValue] = useState(getStringValue(param.currentValue)); const [isValueModified, setIsValueModified] = useState(false); const [timeout, setThisTimeout] = useState({} as NodeJS.Timeout); const onChange = ( e: React.FormEvent, ) => { - setValidated(validateValuesSchema(e.currentTarget.value, param.schema)); + // update the current value setCurrentValue(e.currentTarget.value); - setIsValueModified(toStringValue(e.currentTarget.value) !== toStringValue(param.currentValue)); + setIsValueModified( + getStringValue(e.currentTarget.value) !== getStringValue(param.currentValue), + ); + + // twofold validation: using the json schema (with ajv) and the html5 validation + setValidated(validateValuesSchema(e.currentTarget.value, param.schema)); + e.currentTarget.reportValidity(); + // 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: getValueFromString(param, e.currentTarget?.value), - type: e.currentTarget?.type, + value: getStringValue(e.currentTarget?.value), + type: param.type === "object" ? param.type : e.currentTarget?.type, }, } as React.FormEvent; setThisTimeout(setTimeout(() => func(targetCopy), basicFormsDebounceTime)); @@ -75,9 +59,11 @@ export default function TextParam(props: ITextParamProps) { const unsavedMessage = isValueModified ? "Unsaved" : ""; const isDiffCurrentVsDefault = - toStringValue(param.currentValue) !== toStringValue(param.defaultValue); + getStringValue(param.currentValue, param.type) !== + getStringValue(param.defaultValue, param.type); const isDiffCurrentVsDeployed = - toStringValue(param.currentValue) !== toStringValue(param.defaultValue); + getStringValue(param.currentValue, param.type) !== + getStringValue(param.defaultValue, param.type); const isModified = isValueModified || (isDiffCurrentVsDefault && (!param.deployedValue || isDiffCurrentVsDeployed)); @@ -96,58 +82,105 @@ export default function TextParam(props: ITextParamProps) { {unsavedMessage} ); - let input = ( - <> - - - {renderControlMsg()} - - - ); - if (inputType === "textarea") { - input = ( - -