From a49656e4a7e5477ee092c1e9c77403b69ceacb88 Mon Sep 17 00:00:00 2001 From: Indra Date: Mon, 2 Dec 2024 15:06:55 +0530 Subject: [PATCH] DBZ-8474: Add basic validation in UI forms (#33) * DBZ-8474: Add basic validation in UI forms * DBZ-8474: Add basic validation to source, destination and pipeline. --- .DS_Store | Bin 8196 -> 8196 bytes src/appLayout/AppLayout.css | 14 ++-- src/components/SourceSinkForm.tsx | 4 + src/pages/Destination/CreateDestination.tsx | 40 +++++++-- src/pages/Destination/EditDestination.tsx | 23 +++++- src/pages/Pipeline/ConfigurePipeline.tsx | 85 ++++++++++++-------- src/pages/Pipeline/EditPipeline.tsx | 2 +- src/pages/Source/CreateSource.tsx | 31 +++++-- src/pages/Source/EditSource.tsx | 27 ++++++- 9 files changed, 168 insertions(+), 58 deletions(-) diff --git a/.DS_Store b/.DS_Store index 5f1603b6a26cc7fa9795b3c23cb5ae29f62f3582..16eeb35a8f2e936ff1d9c30c7daf862cd34b817b 100644 GIT binary patch delta 723 zcmb`BzfK!L5XNU5z&8K9=4=8%QKVcG2_>SSp@_IR1z1sJD37J(ch`Fwu%@!b5Ah4}`tJlEfCE7=jvw~|X$;2rv@typs-aD67) zRYMQV20Nk+GIMYA18r`{tzNjkc^HI4xqCxvic&E&EVWvin(7^{HAqJLN^)M7ni(cV zt*&?Ee$mjqqG-wRWS7zXQgHL7D^%%FAM8}OxZCq6FNuOQM()*IrCiabg~KZIrHHa; zhQ480R`kZxUi}TjXFKn1;Y9=l><9o-xm$`AZHGsg-V{<$+bIvrI;0^5i|_{CLj`u> z2u|S)zQ6^1hfDYc*O)|t&v6l#@ingEIu`LG`q)He-i(5VEgYA*YOX`3Z`TL delta 70 zcmV-M0J;B!K!iY$PXQ9KP`eKS5|a!NIS~i|V*qaeb^v7nasXifX8>iB@DLx9MG%m) c4-vTnldBc|lf)c5v3d~$vj-UX1hIMr1IthqoB#j- diff --git a/src/appLayout/AppLayout.css b/src/appLayout/AppLayout.css index ee5747f3..e06e5ff0 100644 --- a/src/appLayout/AppLayout.css +++ b/src/appLayout/AppLayout.css @@ -1,6 +1,6 @@ -.custom-switch{ +/* .custom-switch{ margin-left: auto; -} +} */ @media (min-width: 75rem){ .custom-app-page { @@ -8,19 +8,17 @@ margin-left: auto; } .pf-v6-c-page__main-container { margin-left: 0 !important; - /* overflow-y: clip !important; */ } } -.pf-v6-c-page__main-container { - /* overflow-y: clip !important; */ +/* .pf-v6-c-page__main-container { width: 100%; -} +} */ .pf-v6-c-drawer__body{ display: flex; max-height: 100%; } -.custom-app-page__sidebar-body { +/* .custom-app-page__sidebar-body { padding-inline-start: 1rem; padding-inline-end: 1rem; -} \ No newline at end of file +} */ \ No newline at end of file diff --git a/src/components/SourceSinkForm.tsx b/src/components/SourceSinkForm.tsx index 59aa0846..8e94b1dd 100644 --- a/src/components/SourceSinkForm.tsx +++ b/src/components/SourceSinkForm.tsx @@ -22,6 +22,7 @@ import ConnectorImage from "./ComponentImage"; interface SourceSinkFormProps { ConnectorId: string; dataType?: string; + errorWarning: string[]; connectorType: "source" | "destination"; properties: Map; setValue: (key: string, value: string) => void; @@ -44,6 +45,7 @@ const SourceSinkForm = ({ setValue, getValue, setError, + errorWarning, errors, handleAddProperty, handleDeleteProperty, @@ -137,6 +139,7 @@ const SourceSinkForm = ({ isRequired type="text" placeholder="Key" + validated={errorWarning.includes(key) ? "error" : "default"} id={`${connectorType}-config-props-key-${key}`} name={`${connectorType}-config-props-key-${key}`} value={properties.get(key)?.key || ""} @@ -155,6 +158,7 @@ const SourceSinkForm = ({ type="text" id={`${connectorType}-config-props-value-${key}`} placeholder="Value" + validated={errorWarning.includes(key) ? "error" : "default"} name={`${connectorType}-config-props-value-${key}`} value={properties.get(key)?.value || ""} onChange={(_e, value) => diff --git a/src/pages/Destination/CreateDestination.tsx b/src/pages/Destination/CreateDestination.tsx index dad3a8d2..15c0d15f 100644 --- a/src/pages/Destination/CreateDestination.tsx +++ b/src/pages/Destination/CreateDestination.tsx @@ -17,13 +17,14 @@ import destinationCatalog from "../../__mocks__/data/DestinationCatalog.json"; import { useNavigate, useParams } from "react-router-dom"; import "./CreateDestination.css"; import { CodeEditor, Language } from "@patternfly/react-code-editor"; -import _ from "lodash"; +import { find } from "lodash"; import { createPost, Destination } from "../../apis/apis"; import { API_URL } from "../../utils/constants"; import { convertMapToObject } from "../../utils/helpers"; import { useNotification } from "../../appLayout/AppNotificationContext"; import PageHeader from "@components/PageHeader"; import SourceSinkForm from "@components/SourceSinkForm"; +import { useState } from "react"; interface CreateDestinationProps { modelLoaded?: boolean; @@ -32,6 +33,8 @@ interface CreateDestinationProps { onSelection?: (selection: Destination) => void; } +type Properties = { key: string; value: string }; + const CreateDestination: React.FunctionComponent = ({ modelLoaded, selectedId, @@ -51,13 +54,15 @@ const CreateDestination: React.FunctionComponent = ({ const { addNotification } = useNotification(); + const [errorWarning, setErrorWarning] = useState([]); + const [editorSelected, setEditorSelected] = React.useState("form-editor"); const [isLoading, setIsLoading] = React.useState(false); - const [properties, setProperties] = React.useState< - Map - >(new Map([["key0", { key: "", value: "" }]])); + const [properties, setProperties] = useState>( + new Map([["key0", { key: "", value: "" }]]) + ); const [keyCount, setKeyCount] = React.useState(1); const handleAddProperty = () => { @@ -97,7 +102,7 @@ const CreateDestination: React.FunctionComponent = ({ const createNewDestination = async (values: Record) => { const payload = { description: values["details"], - type: _.find(destinationCatalog, { id: destinationId })?.type || "", + type: find(destinationCatalog, { id: destinationId })?.type || "", schema: "schema321", vaults: [], config: convertMapToObject(properties), @@ -110,20 +115,40 @@ const CreateDestination: React.FunctionComponent = ({ addNotification( "danger", `Destination creation failed`, - `Failed to create ${(response.data as Destination).name}: ${response.error}` + `Failed to create ${(response.data as Destination).name}: ${ + response.error + }` ); } else { modelLoaded && onSelection && onSelection(response.data as Destination); addNotification( "success", `Create successful`, - `Destination "${(response.data as Destination).name}" created successfully.` + `Destination "${ + (response.data as Destination).name + }" created successfully.` ); } }; const handleCreateDestination = async (values: Record) => { setIsLoading(true); + const errorWarning = [] as string[]; + properties.forEach((value: Properties, key: string) => { + if (value.key === "" || value.value === "") { + errorWarning.push(key); + } + }); + setErrorWarning(errorWarning); + if (errorWarning.length > 0) { + addNotification( + "danger", + `Destination creation failed`, + `Please fill both Key and Value fields for all the properties.` + ); + setIsLoading(false); + return; + } await createNewDestination(values); setIsLoading(false); !modelLoaded && navigateTo("/destination"); @@ -203,6 +228,7 @@ const CreateDestination: React.FunctionComponent = ({ getValue={getValue} setError={setError} errors={errors} + errorWarning={errorWarning} handleAddProperty={handleAddProperty} handleDeleteProperty={handleDeleteProperty} handlePropertyChange={handlePropertyChange} diff --git a/src/pages/Destination/EditDestination.tsx b/src/pages/Destination/EditDestination.tsx index 7b8ebd81..77a7bf3a 100644 --- a/src/pages/Destination/EditDestination.tsx +++ b/src/pages/Destination/EditDestination.tsx @@ -30,6 +30,8 @@ import { useNotification } from "../../appLayout/AppNotificationContext"; import SourceSinkForm from "@components/SourceSinkForm"; import PageHeader from "@components/PageHeader"; +type Properties = { key: string; value: string }; + const EditDestination: React.FunctionComponent = () => { const navigate = useNavigate(); const { destinationId } = useParams<{ destinationId: string }>(); @@ -44,6 +46,8 @@ const EditDestination: React.FunctionComponent = () => { const [editorSelected, setEditorSelected] = React.useState("form-editor"); + const [errorWarning, setErrorWarning] = useState([]); + const [destination, setDestination] = useState(); const [isFetchLoading, setIsFetchLoading] = useState(true); const [error, setError] = useState(null); @@ -51,7 +55,7 @@ const EditDestination: React.FunctionComponent = () => { const [isLoading, setIsLoading] = useState(false); const [properties, setProperties] = useState< - Map + Map >(new Map([["key0", { key: "", value: "" }]])); const [keyCount, setKeyCount] = useState(1); @@ -148,6 +152,22 @@ const EditDestination: React.FunctionComponent = () => { const handleEditDestination = async (values: Record) => { setIsLoading(true); + const errorWarning = [] as string[]; + properties.forEach((value: Properties, key: string) => { + if (value.key === "" || value.value === "") { + errorWarning.push(key); + } + }); + setErrorWarning(errorWarning); + if (errorWarning.length > 0) { + addNotification( + "danger", + `Destination edit failed`, + `Please fill both Key and Value fields for all the properties.` + ); + setIsLoading(false); + return; + } await editDestination(values); setIsLoading(false); navigateTo("/destination"); @@ -237,6 +257,7 @@ const EditDestination: React.FunctionComponent = () => { getValue={getValue} setError={setError} errors={errors} + errorWarning={errorWarning} handleAddProperty={handleAddProperty} handleDeleteProperty={handleDeleteProperty} handlePropertyChange={handlePropertyChange} diff --git a/src/pages/Pipeline/ConfigurePipeline.tsx b/src/pages/Pipeline/ConfigurePipeline.tsx index 408367ba..96b75cea 100644 --- a/src/pages/Pipeline/ConfigurePipeline.tsx +++ b/src/pages/Pipeline/ConfigurePipeline.tsx @@ -47,6 +47,7 @@ import { API_URL } from "../../utils/constants"; import PageHeader from "@components/PageHeader"; import { useAtom } from "jotai"; import { selectedTransformAtom } from "./PipelineDesigner"; +import { useNotification } from "@appContext/AppNotificationContext"; const ConfigurePipeline: React.FunctionComponent = () => { const navigate = useNavigate(); @@ -57,7 +58,7 @@ const ConfigurePipeline: React.FunctionComponent = () => { const [selectedTransform] = useAtom(selectedTransformAtom); - console.log("selectedTransform", selectedTransform); + const { addNotification } = useNotification(); const navigateTo = (url: string) => { navigate(url); @@ -117,10 +118,10 @@ const ConfigurePipeline: React.FunctionComponent = () => { fetchDestination(); }, [destinationId]); - const createNewPipline = async (values: Record) => { + const createNewPipeline = async (values: Record) => { const payload = { description: values["description"], - logLevel: logLevel, + logLevel: values["log-level"], source: { name: source?.name, id: source?.id, @@ -135,31 +136,41 @@ const ConfigurePipeline: React.FunctionComponent = () => { const response = await createPost(`${API_URL}/api/pipelines`, payload); - if (response.error) { - console.error("Failed to create source:", response.error); - } else { - console.log("Source created successfully:", response.data); - } + return response; }; - const handleCreatePipline = async (values: Record) => { + const handleCreatePipeline = async (values: Record) => { + console.log("values", values); setIsLoading(true); - await createNewPipline(values); + if(!values["log-level"]) { + setLogLevelError(true); + setIsLoading(false); + return; + } + const response = await createNewPipeline(values); + if (response.error) { + addNotification( + "danger", + `Pipeline creation failed`, + `${response.error}` + ); + setIsLoading(false); + return; + } + addNotification( + "success", + `Pipeline creation successful.`, + `Pipeline "${values["pipeline-name"]}" created successfully.` + ); setIsLoading(false); + navigateTo("/pipeline"); }; - const [logLevel, setLogLevel] = React.useState(""); - - const onChange = ( - _event: React.FormEvent, - value: string - ) => { - setLogLevel(value); - }; + const [logLevelError, setLogLevelError] = React.useState(false); const options = [ - { value: "", label: "Select log level", disabled: false }, + { value: "", label: "Select log level", disabled: true }, { value: "OFF", label: "OFF", disabled: false }, { value: "FATAL", label: "FATAL", disabled: false }, { value: "ERROR", label: "ERROR", disabled: false }, @@ -342,22 +353,30 @@ const ConfigurePipeline: React.FunctionComponent = () => { { + setValue("log-level", value); + setLogLevelError(false); + }} + aria-label="FormSelect Input" + ouiaId="BasicFormSelect" + validated={ + logLevelError ? "error" : "default" + } > - {options.map((option, index) => ( - - ))} + {options.map((option, index) => ( + + ))} @@ -389,7 +408,7 @@ const ConfigurePipeline: React.FunctionComponent = () => { if (!values["pipeline-name"]) { setError("pipeline-name", "Pipeline name is required."); } else { - handleCreatePipline(values); + handleCreatePipeline(values); } }} > diff --git a/src/pages/Pipeline/EditPipeline.tsx b/src/pages/Pipeline/EditPipeline.tsx index 672f99af..308774ae 100644 --- a/src/pages/Pipeline/EditPipeline.tsx +++ b/src/pages/Pipeline/EditPipeline.tsx @@ -175,7 +175,7 @@ const EditPipeline: React.FunctionComponent = () => { }; const options = [ - { value: "", label: "Select log level", disabled: false }, + { value: "", label: "Select log level", disabled: true }, { value: "OFF", label: "OFF", disabled: false }, { value: "FATAL", label: "FATAL", disabled: false }, { value: "ERROR", label: "ERROR", disabled: false }, diff --git a/src/pages/Source/CreateSource.tsx b/src/pages/Source/CreateSource.tsx index a8282777..fbb1e4ec 100644 --- a/src/pages/Source/CreateSource.tsx +++ b/src/pages/Source/CreateSource.tsx @@ -21,7 +21,7 @@ import { createPost, Source } from "../../apis/apis"; import { API_URL } from "../../utils/constants"; import { convertMapToObject } from "../../utils/helpers"; import sourceCatalog from "../../__mocks__/data/SourceCatalog.json"; -import _ from "lodash"; +import { find } from "lodash"; import { useNotification } from "../../appLayout/AppNotificationContext"; import SourceSinkForm from "@components/SourceSinkForm"; import PageHeader from "@components/PageHeader"; @@ -33,6 +33,8 @@ interface CreateSourceProps { onSelection?: (selection: Source) => void; } +type Properties = { key: string; value: string }; + const CreateSource: React.FunctionComponent = ({ modelLoaded, selectedId, @@ -45,6 +47,8 @@ const CreateSource: React.FunctionComponent = ({ const sourceIdModel = selectedId; const sourceId = modelLoaded ? sourceIdModel : sourceIdParam.sourceId; + const [errorWarning, setErrorWarning] = useState([]); + const navigateTo = (url: string) => { navigate(url); }; @@ -55,9 +59,9 @@ const CreateSource: React.FunctionComponent = ({ const [isLoading, setIsLoading] = useState(false); - const [properties, setProperties] = useState< - Map - >(new Map([["key0", { key: "", value: "" }]])); + const [properties, setProperties] = useState>( + new Map([["key0", { key: "", value: "" }]]) + ); const [keyCount, setKeyCount] = useState(1); const handleAddProperty = () => { @@ -97,7 +101,7 @@ const CreateSource: React.FunctionComponent = ({ const createNewSource = async (values: Record) => { const payload = { description: values["details"], - type: _.find(sourceCatalog, { id: sourceId })?.type || "", + type: find(sourceCatalog, { id: sourceId })?.type || "", schema: "schema321", vaults: [], config: convertMapToObject(properties), @@ -124,6 +128,22 @@ const CreateSource: React.FunctionComponent = ({ const handleCreateSource = async (values: Record) => { setIsLoading(true); + const errorWarning = [] as string[]; + properties.forEach((value: Properties, key: string) => { + if (value.key === "" || value.value === "") { + errorWarning.push(key); + } + }); + setErrorWarning(errorWarning); + if (errorWarning.length > 0) { + addNotification( + "danger", + `Source creation failed`, + `Please fill both Key and Value fields for all the properties.` + ); + setIsLoading(false); + return; + } await createNewSource(values); setIsLoading(false); !modelLoaded && navigateTo("/source"); @@ -203,6 +223,7 @@ const CreateSource: React.FunctionComponent = ({ getValue={getValue} setError={setError} errors={errors} + errorWarning={errorWarning} handleAddProperty={handleAddProperty} handleDeleteProperty={handleDeleteProperty} handlePropertyChange={handlePropertyChange} diff --git a/src/pages/Source/EditSource.tsx b/src/pages/Source/EditSource.tsx index 600d0c44..34ef8b20 100644 --- a/src/pages/Source/EditSource.tsx +++ b/src/pages/Source/EditSource.tsx @@ -30,6 +30,8 @@ import { useNotification } from "../../appLayout/AppNotificationContext"; import SourceSinkForm from "@components/SourceSinkForm"; import PageHeader from "@components/PageHeader"; +type Properties = { key: string; value: string }; + const EditSource: React.FunctionComponent = () => { const navigate = useNavigate(); const { sourceId } = useParams<{ sourceId: string }>(); @@ -44,15 +46,17 @@ const EditSource: React.FunctionComponent = () => { const [editorSelected, setEditorSelected] = React.useState("form-editor"); + const [errorWarning, setErrorWarning] = useState([]); + const [source, setSource] = useState(); const [isFetchLoading, setIsFetchLoading] = useState(true); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - const [properties, setProperties] = useState< - Map - >(new Map([["key0", { key: "", value: "" }]])); + const [properties, setProperties] = useState>( + new Map([["key0", { key: "", value: "" }]]) + ); const [keyCount, setKeyCount] = useState(1); const setConfigProperties = (configProp: SourceConfig) => { @@ -149,6 +153,22 @@ const EditSource: React.FunctionComponent = () => { const handleEditSource = async (values: Record) => { setIsLoading(true); + const errorWarning = [] as string[]; + properties.forEach((value: Properties, key: string) => { + if (value.key === "" || value.value === "") { + errorWarning.push(key); + } + }); + setErrorWarning(errorWarning); + if (errorWarning.length > 0) { + addNotification( + "danger", + `Source edit failed`, + `Please fill both Key and Value fields for all the properties.` + ); + setIsLoading(false); + return; + } await editSource(values); setIsLoading(false); navigateTo("/source"); @@ -237,6 +257,7 @@ const EditSource: React.FunctionComponent = () => { getValue={getValue} setError={setError} errors={errors} + errorWarning={errorWarning} handleAddProperty={handleAddProperty} handleDeleteProperty={handleDeleteProperty} handlePropertyChange={handlePropertyChange}