This is an automated email from the ASF dual-hosted git repository. kaxilnaik pushed a commit to branch v3-0-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 848c1b31140bf7ebc728f5e58306519f994688e9 Author: Aritra Basu <[email protected]> AuthorDate: Thu May 1 20:37:25 2025 +0530 Added validation in flexibleform (#49981) * Added validation in flexibleform * Updated errors * Added more validation and disabling button * Fixed boolean error * Makes section red on error (cherry picked from commit 607ff45fa00812b86c52e62dbb3a3229f169ed5d) --- .../src/airflow/ui/src/components/ConfigForm.tsx | 3 ++ .../src/components/DagActions/RunBackfillForm.tsx | 6 ++- .../components/FlexibleForm/FieldAdvancedArray.tsx | 27 ++++------- .../src/components/FlexibleForm/FieldDateTime.tsx | 4 +- .../src/components/FlexibleForm/FieldDropdown.tsx | 3 +- .../components/FlexibleForm/FieldMultiSelect.tsx | 3 +- .../components/FlexibleForm/FieldMultilineText.tsx | 3 +- .../ui/src/components/FlexibleForm/FieldNumber.tsx | 3 +- .../ui/src/components/FlexibleForm/FieldObject.tsx | 23 ++++------ .../ui/src/components/FlexibleForm/FieldRow.tsx | 38 +++++++++++----- .../src/components/FlexibleForm/FieldSelector.tsx | 26 +++++------ .../ui/src/components/FlexibleForm/FieldString.tsx | 3 +- .../components/FlexibleForm/FieldStringArray.tsx | 3 +- .../src/components/FlexibleForm/FlexibleForm.tsx | 52 ++++++++++++++++++++-- .../airflow/ui/src/components/FlexibleForm/Row.tsx | 8 +++- .../ui/src/components/FlexibleForm/index.tsx | 1 + .../{index.tsx => isParamRequired.tsx} | 14 +++--- .../src/components/TriggerDag/TriggerDAGForm.tsx | 4 +- .../ui/src/pages/Connections/ConnectionForm.tsx | 5 ++- 19 files changed, 148 insertions(+), 81 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx index a216b22de60..c8e75edfa81 100644 --- a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx @@ -39,6 +39,7 @@ type ConfigFormProps<T extends FieldValues = FieldValues> = { date?: unknown; }> >; + readonly setFormError: (error: boolean) => void; }; const ConfigForm = <T extends FieldValues = FieldValues>({ @@ -47,6 +48,7 @@ const ConfigForm = <T extends FieldValues = FieldValues>({ errors, initialParamsDict, setErrors, + setFormError, }: ConfigFormProps<T>) => { const { conf, setConf } = useParamStore(); @@ -86,6 +88,7 @@ const ConfigForm = <T extends FieldValues = FieldValues>({ <FlexibleForm flexibleFormDefaultSection={flexibleFormDefaultSection} initialParamsDict={initialParamsDict} + setError={setFormError} /> <Accordion.Item key="advancedOptions" value="advancedOptions"> <Accordion.ItemTrigger cursor="button">Advanced Options</Accordion.ItemTrigger> diff --git a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx index 23162df0453..b86aa824c9b 100644 --- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx @@ -49,6 +49,7 @@ type BackfillFormProps = DagRunTriggerParams & Omit<BackfillPostBody, "dag_run_c const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({}); const [unpause, setUnpause] = useState(true); + const [formError, setFormError] = useState(false); const initialParamsDict = useDagParams(dag.dag_id, true); const { conf } = useParamStore(); const { control, handleSubmit, reset, watch } = useForm<BackfillFormProps>({ @@ -251,6 +252,7 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { errors={errors} initialParamsDict={initialParamsDict} setErrors={setErrors} + setFormError={setFormError} /> </VStack> <Box as="footer" display="flex" justifyContent="flex-end" mt={4}> @@ -259,7 +261,9 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { <Button onClick={() => void handleSubmit(onCancel)()}>Cancel</Button> <Button colorPalette="blue" - disabled={Boolean(errors.date) || isPendingDryRun || affectedTasks.total_entries === 0} + disabled={ + Boolean(errors.date) || isPendingDryRun || formError || affectedTasks.total_entries === 0 + } loading={isPending} onClick={() => void handleSubmit(onSubmit)()} > diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx index 80cb46dbdbc..27221204665 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx @@ -16,23 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -import { Text } from "@chakra-ui/react"; -import { useState } from "react"; - import { paramPlaceholder, useParamStore } from "src/queries/useParamStore"; import type { FlexibleFormElementProps } from "."; import { JsonEditor } from "../JsonEditor"; -export const FieldAdvancedArray = ({ name }: FlexibleFormElementProps) => { +export const FieldAdvancedArray = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; - const [error, setError] = useState<unknown>(undefined); // Determine the expected type based on schema const expectedType = param.schema.items?.type ?? "object"; const handleChange = (value: string) => { - setError(undefined); if (value === "") { if (paramsDict[name]) { // "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults. @@ -64,24 +59,18 @@ export const FieldAdvancedArray = ({ name }: FlexibleFormElementProps) => { } setParamsDict(paramsDict); + onUpdate(String(parsedValue)); } catch (_error) { - setError(expectedType === "number" ? String(_error).replace("JSON", "Array") : _error); + onUpdate(undefined, expectedType === "number" ? String(_error).replace("JSON", "Array") : _error); } } }; return ( - <> - <JsonEditor - id={`element_${name}`} - onChange={handleChange} - value={JSON.stringify(param.value ?? [], undefined, 2)} - /> - {Boolean(error) ? ( - <Text color="fg.error" fontSize="xs"> - {String(error)} - </Text> - ) : undefined} - </> + <JsonEditor + id={`element_${name}`} + onChange={handleChange} + value={JSON.stringify(param.value ?? [], undefined, 2)} + /> ); }; diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx index 07911837996..cc8d23184b3 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx @@ -23,7 +23,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore"; import type { FlexibleFormElementProps } from "."; import { DateTimeInput } from "../DateTimeInput"; -export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & InputProps) => { +export const FieldDateTime = ({ name, onUpdate, ...rest }: FlexibleFormElementProps & InputProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; const handleChange = (value: string) => { @@ -40,6 +40,7 @@ export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & Inpu } setParamsDict(paramsDict); + onUpdate(value); }; if (rest.type === "datetime-local") { @@ -59,6 +60,7 @@ export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & Inpu id={`element_${name}`} name={`element_${name}`} onChange={(event) => handleChange(event.target.value)} + required={rest.required} size="sm" type={rest.type} value={((param.value ?? "") as string).slice(0, 16)} diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx index a12d4dc366d..de15568c39c 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx @@ -33,7 +33,7 @@ const labelLookup = (key: string, valuesDisplay: Record<string, string> | undefi }; const enumTypes = ["string", "number", "integer"]; -export const FieldDropdown = ({ name }: FlexibleFormElementProps) => { +export const FieldDropdown = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; @@ -55,6 +55,7 @@ export const FieldDropdown = ({ name }: FlexibleFormElementProps) => { } setParamsDict(paramsDict); + onUpdate(value); }; return ( diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx index aa4a4e112f9..e34241037e1 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx @@ -31,7 +31,7 @@ const labelLookup = (key: string, valuesDisplay: Record<string, string> | undefi return key; }; -export const FieldMultiSelect = ({ name }: FlexibleFormElementProps) => { +export const FieldMultiSelect = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; @@ -64,6 +64,7 @@ export const FieldMultiSelect = ({ name }: FlexibleFormElementProps) => { paramsDict[name].value = newValueArray; } setParamsDict(paramsDict); + onUpdate(String(newValueArray)); }; return ( diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx index bfefcb276d8..93c7410bb82 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx @@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore"; import type { FlexibleFormElementProps } from "."; -export const FieldMultilineText = ({ name }: FlexibleFormElementProps) => { +export const FieldMultilineText = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; const handleChange = (value: string) => { @@ -33,6 +33,7 @@ export const FieldMultilineText = ({ name }: FlexibleFormElementProps) => { } setParamsDict(paramsDict); + onUpdate(value); }; return ( diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx index dcaaf37cf56..635f461335c 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx @@ -21,7 +21,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore"; import type { FlexibleFormElementProps } from "."; import { NumberInputField, NumberInputRoot } from "../ui/NumberInput"; -export const FieldNumber = ({ name }: FlexibleFormElementProps) => { +export const FieldNumber = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; const handleChange = (value: string) => { @@ -40,6 +40,7 @@ export const FieldNumber = ({ name }: FlexibleFormElementProps) => { } setParamsDict(paramsDict); + onUpdate(value); }; return ( diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx index fcd7d7d2a16..ce5bf307311 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx @@ -16,21 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { Text } from "@chakra-ui/react"; -import { useState } from "react"; - import { paramPlaceholder, useParamStore } from "src/queries/useParamStore"; import type { FlexibleFormElementProps } from "."; import { JsonEditor } from "../JsonEditor"; -export const FieldObject = ({ name }: FlexibleFormElementProps) => { +export const FieldObject = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; - const [error, setError] = useState<unknown>(undefined); const handleChange = (value: string) => { - setError(undefined); try { // "undefined" values are removed from params, so we set it to null to avoid falling back to DAG defaults. // eslint-disable-next-line unicorn/no-null @@ -41,19 +36,17 @@ export const FieldObject = ({ name }: FlexibleFormElementProps) => { } setParamsDict(paramsDict); + onUpdate(value); } catch (_error) { - setError(_error); + onUpdate("", _error); } }; return ( - <> - <JsonEditor - id={`element_${name}`} - onChange={handleChange} - value={JSON.stringify(param.value ?? [], undefined, 2)} - /> - {Boolean(error) ? <Text color="fg,.error">{String(error)}</Text> : undefined} - </> + <JsonEditor + id={`element_${name}`} + onChange={handleChange} + value={JSON.stringify(param.value ?? [], undefined, 2)} + /> ); }; diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx index e41d4643f6b..406dbaf18fe 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx @@ -17,35 +17,52 @@ * under the License. */ import { Field, Stack } from "@chakra-ui/react"; +import { useState } from "react"; import Markdown from "react-markdown"; import remarkGfm from "remark-gfm"; -import type { ParamSpec } from "src/queries/useDagParams"; import { paramPlaceholder, useParamStore } from "src/queries/useParamStore"; import type { FlexibleFormElementProps } from "."; import { FieldSelector } from "./FieldSelector"; - -const isRequired = (param: ParamSpec) => - // The field is required if the schema type is defined. - // But if the type "null" is included, then the field is not required. - // We assume that "null" is only defined if the type is an array. - Boolean(param.schema.type) && (!Array.isArray(param.schema.type) || !param.schema.type.includes("null")); +import { isRequired } from "./isParamRequired"; /** Render a normal form row with a field that is auto-selected */ -export const FieldRow = ({ name }: FlexibleFormElementProps) => { +export const FieldRow = ({ name, onUpdate: rowOnUpdate }: FlexibleFormElementProps) => { const { paramsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; + const [error, setError] = useState<unknown>( + isRequired(param) && param.value === null ? "This field is required" : undefined, + ); + const [isValid, setIsValid] = useState(!(isRequired(param) && param.value === null)); + + // console.log(param); + + const onUpdate = (value?: string, _error?: unknown) => { + if (Boolean(_error)) { + setIsValid(false); + setError(_error); + rowOnUpdate(undefined, _error); + } else if (isRequired(param) && (!Boolean(value) || value === "")) { + setIsValid(false); + setError("This field is required"); + rowOnUpdate(undefined, "This field is required"); + } else { + setIsValid(true); + setError(undefined); + rowOnUpdate(); + } + }; return ( - <Field.Root orientation="horizontal" required={isRequired(param)}> + <Field.Root invalid={!isValid} orientation="horizontal" required={isRequired(param)}> <Stack> <Field.Label fontSize="md" style={{ flexBasis: "30%" }}> {param.schema.title ?? name} <Field.RequiredIndicator /> </Field.Label> </Stack> <Stack css={{ flexBasis: "70%" }}> - <FieldSelector name={name} /> + <FieldSelector name={name} onUpdate={onUpdate} /> {param.description === null ? ( param.schema.description_md === undefined ? undefined : ( <Field.HelperText> @@ -55,6 +72,7 @@ export const FieldRow = ({ name }: FlexibleFormElementProps) => { ) : ( <Field.HelperText>{param.description}</Field.HelperText> )} + {isValid ? undefined : <Field.ErrorText>{String(error)}</Field.ErrorText>} </Stack> </Field.Root> ); diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx index 8665b41aa2b..cd0061987a6 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx @@ -87,35 +87,35 @@ const isFieldStringArray = (fieldType: string, fieldSchema: ParamSchema) => const isFieldTime = (fieldType: string, fieldSchema: ParamSchema) => fieldType === "string" && fieldSchema.format === "time"; -export const FieldSelector = ({ name }: FlexibleFormElementProps) => { +export const FieldSelector = ({ name, onUpdate }: FlexibleFormElementProps) => { // FUTURE: Add support for other types as described in AIP-68 via Plugins const { initialParamDict } = useParamStore(); const param = initialParamDict[name] ?? paramPlaceholder; const fieldType = inferType(param); if (isFieldBool(fieldType)) { - return <FieldBool name={name} />; + return <FieldBool name={name} onUpdate={onUpdate} />; } else if (isFieldDateTime(fieldType, param.schema)) { - return <FieldDateTime name={name} type="datetime-local" />; + return <FieldDateTime name={name} onUpdate={onUpdate} type="datetime-local" />; } else if (isFieldDate(fieldType, param.schema)) { - return <FieldDateTime name={name} type="date" />; + return <FieldDateTime name={name} onUpdate={onUpdate} type="date" />; } else if (isFieldTime(fieldType, param.schema)) { - return <FieldDateTime name={name} type="time" />; + return <FieldDateTime name={name} onUpdate={onUpdate} type="time" />; } else if (isFieldDropdown(fieldType, param.schema)) { - return <FieldDropdown name={name} />; + return <FieldDropdown name={name} onUpdate={onUpdate} />; } else if (isFieldMultiSelect(fieldType, param.schema)) { - return <FieldMultiSelect name={name} />; + return <FieldMultiSelect name={name} onUpdate={onUpdate} />; } else if (isFieldStringArray(fieldType, param.schema)) { - return <FieldStringArray name={name} />; + return <FieldStringArray name={name} onUpdate={onUpdate} />; } else if (isFieldAdvancedArray(fieldType, param.schema)) { - return <FieldAdvancedArray name={name} />; + return <FieldAdvancedArray name={name} onUpdate={onUpdate} />; } else if (isFieldObject(fieldType)) { - return <FieldObject name={name} />; + return <FieldObject name={name} onUpdate={onUpdate} />; } else if (isFieldNumber(fieldType)) { - return <FieldNumber name={name} />; + return <FieldNumber name={name} onUpdate={onUpdate} />; } else if (isFieldMultilineText(fieldType, param.schema)) { - return <FieldMultilineText name={name} />; + return <FieldMultilineText name={name} onUpdate={onUpdate} />; } else { - return <FieldString name={name} />; + return <FieldString name={name} onUpdate={onUpdate} />; } }; diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx index e8cd0185706..8d49e604452 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx @@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore"; import type { FlexibleFormElementProps } from "."; -export const FieldString = ({ name }: FlexibleFormElementProps) => { +export const FieldString = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; const handleChange = (value: string) => { @@ -33,6 +33,7 @@ export const FieldString = ({ name }: FlexibleFormElementProps) => { } setParamsDict(paramsDict); + onUpdate(value); }; return ( diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx index f5fc6ac69f5..2078d95276d 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx @@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from "src/queries/useParamStore"; import type { FlexibleFormElementProps } from "."; -export const FieldStringArray = ({ name }: FlexibleFormElementProps) => { +export const FieldStringArray = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict, setParamsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; @@ -34,6 +34,7 @@ export const FieldStringArray = ({ name }: FlexibleFormElementProps) => { } setParamsDict(paramsDict); + onUpdate(newValue); }; const handleBlur = () => { diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx index 165e9d6dd19..ae9e60c812f 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx @@ -17,23 +17,44 @@ * under the License. */ import { Box, Stack, StackSeparator } from "@chakra-ui/react"; -import { useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import type { ParamsSpec } from "src/queries/useDagParams"; import { useParamStore } from "src/queries/useParamStore"; import { Accordion } from "../ui"; import { Row } from "./Row"; +import { isRequired } from "./isParamRequired"; export type FlexibleFormProps = { flexibleFormDefaultSection: string; initialParamsDict: { paramsDict: ParamsSpec }; key?: string; + setError: (error: boolean) => void; }; -export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict }: FlexibleFormProps) => { +export const FlexibleForm = ({ + flexibleFormDefaultSection, + initialParamsDict, + setError, +}: FlexibleFormProps) => { const { paramsDict: params, setinitialParamDict, setParamsDict } = useParamStore(); const processedSections = new Map(); + const [sectionError, setSectionError] = useState<Map<string, boolean>>(new Map()); + + const recheckSection = useCallback(() => { + sectionError.clear(); + Object.entries(params).forEach(([, element]) => { + if ( + isRequired(element) && + (element.value === null || element.value === undefined || element.value === "") + ) { + sectionError.set(element.schema.section ?? flexibleFormDefaultSection, true); + setSectionError(sectionError); + } + }); + console.log("errors1", sectionError); + }, [flexibleFormDefaultSection, params, sectionError]); useEffect(() => { // Initialize paramsDict and initialParamDict when modal opens @@ -54,6 +75,24 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict }: [setParamsDict, setinitialParamDict], ); + useEffect( + () => () => { + recheckSection(); + }, + [params, recheckSection], + ); + + const onUpdate = (_value?: string, error?: unknown) => { + recheckSection(); + if (!Boolean(error) && sectionError.size === 0) { + setError(false); + } else { + setError(true); + } + }; + + console.log(sectionError); + return Object.entries(params).some(([, param]) => typeof param.schema.section !== "string") ? Object.entries(params).map(([, secParam]) => { const currentSection = secParam.schema.section ?? flexibleFormDefaultSection; @@ -65,7 +104,12 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict }: return ( <Accordion.Item key={currentSection} value={currentSection}> - <Accordion.ItemTrigger cursor="button">{currentSection}</Accordion.ItemTrigger> + <Accordion.ItemTrigger + color={sectionError.get(currentSection) ? "red" : undefined} + cursor="button" + > + {currentSection} + </Accordion.ItemTrigger> <Accordion.ItemContent paddingTop={0}> <Box p={5}> <Stack separator={<StackSeparator />}> @@ -76,7 +120,7 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict }: (currentSection === flexibleFormDefaultSection && !Boolean(param.schema.section)), ) .map(([name]) => ( - <Row key={name} name={name} /> + <Row key={name} name={name} onUpdate={onUpdate} /> ))} </Stack> </Box> diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx index 1f1186124a2..cc71fedb6ee 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx @@ -26,9 +26,13 @@ import { HiddenInput } from "./HiddenInput"; const isHidden = (fieldSchema: ParamSchema) => Boolean(fieldSchema.const); /** Generates a form row */ -export const Row = ({ name }: FlexibleFormElementProps) => { +export const Row = ({ name, onUpdate }: FlexibleFormElementProps) => { const { paramsDict } = useParamStore(); const param = paramsDict[name] ?? paramPlaceholder; - return isHidden(param.schema) ? <HiddenInput name={name} /> : <FieldRow name={name} />; + return isHidden(param.schema) ? ( + <HiddenInput name={name} onUpdate={onUpdate} /> + ) : ( + <FieldRow name={name} onUpdate={onUpdate} /> + ); }; diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx index f7e8ab0d8c4..704b5d956ae 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx @@ -19,6 +19,7 @@ export type FlexibleFormElementProps = { readonly name: string; + readonly onUpdate: (value?: string, error?: unknown) => void; }; export const flexibleFormDefaultSection = "Run Parameters"; diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx b/airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx similarity index 66% copy from airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx copy to airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx index f7e8ab0d8c4..7a9a0668855 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx @@ -16,12 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +import type { ParamSpec } from "src/queries/useDagParams"; -export type FlexibleFormElementProps = { - readonly name: string; -}; - -export const flexibleFormDefaultSection = "Run Parameters"; -export const flexibleFormExtraFieldSection = "Extra Fields"; - -export { FlexibleForm } from "./FlexibleForm"; +export const isRequired = (param: ParamSpec) => + // The field is required if the schema type is defined. + // But if the type "null" is included, then the field is not required. + // We assume that "null" is only defined if the type is an array. + Boolean(param.schema.type) && (!Array.isArray(param.schema.type) || !param.schema.type.includes("null")); diff --git a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx index fdeae4bdb69..e1337f530f6 100644 --- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx @@ -49,6 +49,7 @@ export type DagRunTriggerParams = { const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps) => { const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({}); + const [formError, setFormError] = useState(false); const initialParamsDict = useDagParams(dagId, open); const { error: errorTrigger, isPending, triggerDagRun } = useTrigger({ dagId, onSuccessConfirm: onClose }); const { conf } = useParamStore(); @@ -96,6 +97,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps) errors={errors} initialParamsDict={initialParamsDict} setErrors={setErrors} + setFormError={setFormError} > <Controller control={control} @@ -153,7 +155,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps) <Spacer /> <Button colorPalette="blue" - disabled={Boolean(errors.conf) || Boolean(errors.date) || isPending} + disabled={Boolean(errors.conf) || Boolean(errors.date) || formError || isPending} onClick={() => void handleSubmit(onSubmit)()} > <FiPlay /> Trigger diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx index f2a47c47b15..6a17f1794f6 100644 --- a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx @@ -68,6 +68,8 @@ const ConnectionForm = ({ const standardFields = connectionTypeMeta[selectedConnType]?.standard_fields ?? {}; const paramsDic = { paramsDict: connectionTypeMeta[selectedConnType]?.extra_fields ?? ({} as ParamsSpec) }; + const [formErrors, setFormErrors] = useState(false); + useEffect(() => { reset((prevValues) => ({ ...initialConnection, @@ -198,6 +200,7 @@ const ConnectionForm = ({ flexibleFormDefaultSection={flexibleFormExtraFieldSection} initialParamsDict={paramsDic} key={selectedConnType} + setError={setFormErrors} /> <Accordion.Item key="extraJson" value="extraJson"> <Accordion.ItemTrigger cursor="button">Extra Fields JSON</Accordion.ItemTrigger> @@ -228,7 +231,7 @@ const ConnectionForm = ({ <Spacer /> <Button colorPalette="blue" - disabled={Boolean(errors.conf) || isPending || !isValid} + disabled={Boolean(errors.conf) || formErrors || isPending || !isValid} onClick={() => void handleSubmit(onSubmit)()} > <FiSave /> Save
