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 93960d8d194ded926b212c90dcc9bdd7c5c9bded Author: Guan Ming(Wesley) Chiu <[email protected]> AuthorDate: Thu May 1 01:49:30 2025 +0800 Add `dag_run_conf` to `RunBackfillForm` (#49763) * feat: add `dag_run_conf` to `RunBackfillForm` Co-authored-by: Brent Bovenzi <[email protected]> * fix: add missing conf preset Co-authored-by: Brent Bovenzi <[email protected]> --------- Co-authored-by: Brent Bovenzi <[email protected]> (cherry picked from commit 6ca99d898e0a65a41b47e0188255ad1cb4068ca9) --- .../src/airflow/ui/src/components/ConfigForm.tsx | 118 ++++++++++++++ .../src/components/DagActions/RunBackfillForm.tsx | 37 ++++- .../src/components/TriggerDag/TriggerDAGForm.tsx | 174 +++++++-------------- 3 files changed, 206 insertions(+), 123 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx new file mode 100644 index 00000000000..a216b22de60 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx @@ -0,0 +1,118 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Accordion, Box, Field } from "@chakra-ui/react"; +import { type Control, type FieldValues, type Path, Controller } from "react-hook-form"; + +import type { ParamsSpec } from "src/queries/useDagParams"; +import { useParamStore } from "src/queries/useParamStore"; + +import { FlexibleForm, flexibleFormDefaultSection } from "./FlexibleForm"; +import { JsonEditor } from "./JsonEditor"; + +type ConfigFormProps<T extends FieldValues = FieldValues> = { + readonly children?: React.ReactNode; + readonly control: Control<T>; + readonly errors: { + conf?: string; + date?: unknown; + }; + readonly initialParamsDict: { paramsDict: ParamsSpec }; + readonly setErrors: React.Dispatch< + React.SetStateAction<{ + conf?: string; + date?: unknown; + }> + >; +}; + +const ConfigForm = <T extends FieldValues = FieldValues>({ + children, + control, + errors, + initialParamsDict, + setErrors, +}: ConfigFormProps<T>) => { + const { conf, setConf } = useParamStore(); + + const validateAndPrettifyJson = (value: string) => { + try { + const parsedJson = JSON.parse(value) as JSON; + + setErrors((prev) => ({ ...prev, conf: undefined })); + + const formattedJson = JSON.stringify(parsedJson, undefined, 2); + + if (formattedJson !== conf) { + setConf(formattedJson); // Update only if the value is different + } + + return formattedJson; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred."; + + setErrors((prev) => ({ + ...prev, + conf: `Invalid JSON format: ${errorMessage}`, + })); + + return value; + } + }; + + return ( + <Accordion.Root + collapsible + defaultValue={[flexibleFormDefaultSection]} + mb={4} + size="lg" + variant="enclosed" + > + <FlexibleForm + flexibleFormDefaultSection={flexibleFormDefaultSection} + initialParamsDict={initialParamsDict} + /> + <Accordion.Item key="advancedOptions" value="advancedOptions"> + <Accordion.ItemTrigger cursor="button">Advanced Options</Accordion.ItemTrigger> + <Accordion.ItemContent> + <Box p={4}> + {children} + <Controller + control={control} + name={"conf" as Path<T>} + render={({ field }) => ( + <Field.Root invalid={Boolean(errors.conf)} mt={6}> + <Field.Label fontSize="md">Configuration JSON</Field.Label> + <JsonEditor + {...field} + onBlur={() => { + field.onChange(validateAndPrettifyJson(field.value as string)); + }} + /> + {Boolean(errors.conf) ? <Field.ErrorText>{errors.conf}</Field.ErrorText> : undefined} + </Field.Root> + )} + /> + </Box> + </Accordion.ItemContent> + </Accordion.Item> + </Accordion.Root> + ); +}; + +export default ConfigForm; 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 4ab35b2f466..2079de6c604 100644 --- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx @@ -25,11 +25,15 @@ import { Button } from "src/components/ui"; import { reprocessBehaviors } from "src/constants/reprocessBehaviourParams"; import { useCreateBackfill } from "src/queries/useCreateBackfill"; import { useCreateBackfillDryRun } from "src/queries/useCreateBackfillDryRun"; +import { useDagParams } from "src/queries/useDagParams"; +import { useParamStore } from "src/queries/useParamStore"; import { useTogglePause } from "src/queries/useTogglePause"; import { pluralize } from "src/utils"; +import ConfigForm from "../ConfigForm"; import { DateTimeInput } from "../DateTimeInput"; import { ErrorAlert } from "../ErrorAlert"; +import type { DagRunTriggerParams } from "../TriggerDag/TriggerDAGForm"; import { Checkbox } from "../ui/Checkbox"; import { RadioCardItem, RadioCardLabel, RadioCardRoot } from "../ui/RadioCard"; @@ -39,14 +43,17 @@ type RunBackfillFormProps = { }; const today = new Date().toISOString().slice(0, 16); +type BackfillFormProps = DagRunTriggerParams & Omit<BackfillPostBody, "dag_run_conf">; + const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({}); const [unpause, setUnpause] = useState(true); - - const { control, handleSubmit, reset, watch } = useForm<BackfillPostBody>({ + const initialParamsDict = useDagParams(dag.dag_id, true); + const { conf } = useParamStore(); + const { control, handleSubmit, reset, watch } = useForm<BackfillFormProps>({ defaultValues: { + conf, dag_id: dag.dag_id, - dag_run_conf: {}, from_date: "", max_active_runs: 1, reprocess_behavior: "none", @@ -55,7 +62,7 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { }, mode: "onBlur", }); - const values = useWatch<BackfillPostBody>({ + const values = useWatch<BackfillFormProps>({ control, }); @@ -85,10 +92,16 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { } }, [dateValidationError]); + useEffect(() => { + if (conf) { + reset({ conf }); + } + }, [conf, reset]); + const dataIntervalStart = watch("from_date"); const dataIntervalEnd = watch("to_date"); - const onSubmit = (fdata: BackfillPostBody) => { + const onSubmit = (fdata: BackfillFormProps) => { if (unpause && dag.is_paused) { togglePause({ dagId: dag.dag_id, @@ -98,11 +111,14 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { }); } createBackfill({ - requestBody: fdata, + requestBody: { + ...fdata, + dag_run_conf: JSON.parse(fdata.conf) as Record<string, unknown>, + }, }); }; - const onCancel = (fdata: BackfillPostBody) => { + const onCancel = (fdata: BackfillFormProps) => { reset(fdata); onClose(); }; @@ -237,6 +253,13 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { <Spacer /> </> ) : undefined} + + <ConfigForm + control={control} + errors={errors} + initialParamsDict={initialParamsDict} + setErrors={setErrors} + /> </VStack> <Box as="footer" display="flex" justifyContent="flex-end" mt={4}> <HStack w="full"> 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 b3d470ba417..fdeae4bdb69 100644 --- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { Input, Button, Box, Spacer, HStack, Field, Stack } from "@chakra-ui/react"; +import { Button, Box, Spacer, HStack, Input, Field, Stack } from "@chakra-ui/react"; import dayjs from "dayjs"; import { useEffect, useState } from "react"; -import { useForm, Controller } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { FiPlay } from "react-icons/fi"; import { useDagParams } from "src/queries/useDagParams"; @@ -27,11 +27,9 @@ import { useParamStore } from "src/queries/useParamStore"; import { useTogglePause } from "src/queries/useTogglePause"; import { useTrigger } from "src/queries/useTrigger"; +import ConfigForm from "../ConfigForm"; import { DateTimeInput } from "../DateTimeInput"; import { ErrorAlert } from "../ErrorAlert"; -import { FlexibleForm, flexibleFormDefaultSection } from "../FlexibleForm"; -import { JsonEditor } from "../JsonEditor"; -import { Accordion } from "../ui"; import { Checkbox } from "../ui/Checkbox"; import EditableMarkdown from "./EditableMarkdown"; @@ -53,7 +51,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps) const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({}); const initialParamsDict = useDagParams(dagId, open); const { error: errorTrigger, isPending, triggerDagRun } = useTrigger({ dagId, onSuccessConfirm: onClose }); - const { conf, setConf } = useParamStore(); + const { conf } = useParamStore(); const [unpause, setUnpause] = useState(true); const { mutate: togglePause } = useTogglePause({ dagId }); @@ -75,6 +73,10 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps) } }, [conf, reset]); + const resetDateError = () => { + setErrors((prev) => ({ ...prev, date: undefined })); + }; + const onSubmit = (data: DagRunTriggerParams) => { if (unpause && isPaused) { togglePause({ @@ -87,119 +89,59 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps) triggerDagRun(data); }; - const validateAndPrettifyJson = (value: string) => { - try { - const parsedJson = JSON.parse(value) as JSON; - - setErrors((prev) => ({ ...prev, conf: undefined })); - - const formattedJson = JSON.stringify(parsedJson, undefined, 2); - - if (formattedJson !== conf) { - setConf(formattedJson); // Update only if the value is different - } - - return formattedJson; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error occurred."; - - setErrors((prev) => ({ - ...prev, - conf: `Invalid JSON format: ${errorMessage}`, - })); - - return value; - } - }; - - const resetDateError = () => { - setErrors((prev) => ({ ...prev, date: undefined })); - }; - return ( - <> - <Accordion.Root - collapsible - defaultValue={[flexibleFormDefaultSection]} - mb={4} - mt={8} - size="lg" - variant="enclosed" + <Box mt={8}> + <ConfigForm + control={control} + errors={errors} + initialParamsDict={initialParamsDict} + setErrors={setErrors} > - <FlexibleForm - flexibleFormDefaultSection={flexibleFormDefaultSection} - initialParamsDict={initialParamsDict} + <Controller + control={control} + name="logicalDate" + render={({ field }) => ( + <Field.Root invalid={Boolean(errors.date)} orientation="horizontal"> + <Stack> + <Field.Label fontSize="md" style={{ flexBasis: "30%" }}> + Logical Date + </Field.Label> + </Stack> + <Stack css={{ flexBasis: "70%" }}> + <DateTimeInput {...field} onBlur={resetDateError} size="sm" /> + </Stack> + </Field.Root> + )} /> - <Accordion.Item key="advancedOptions" value="advancedOptions"> - <Accordion.ItemTrigger cursor="button">Advanced Options</Accordion.ItemTrigger> - <Accordion.ItemContent> - <Box p={4}> - <Controller - control={control} - name="logicalDate" - render={({ field }) => ( - <Field.Root invalid={Boolean(errors.date)} orientation="horizontal"> - <Stack> - <Field.Label fontSize="md" style={{ flexBasis: "30%" }}> - Logical Date - </Field.Label> - </Stack> - <Stack css={{ flexBasis: "70%" }}> - <DateTimeInput {...field} onBlur={resetDateError} size="sm" /> - </Stack> - </Field.Root> - )} - /> - - <Controller - control={control} - name="dagRunId" - render={({ field }) => ( - <Field.Root mt={6} orientation="horizontal"> - <Stack> - <Field.Label fontSize="md" style={{ flexBasis: "30%" }}> - Run ID - </Field.Label> - </Stack> - <Stack css={{ flexBasis: "70%" }}> - <Input {...field} size="sm" /> - <Field.HelperText>Optional - will be generated if not provided</Field.HelperText> - </Stack> - </Field.Root> - )} - /> - - <Controller - control={control} - name="conf" - render={({ field }) => ( - <Field.Root invalid={Boolean(errors.conf)} mt={6}> - <Field.Label fontSize="md">Configuration JSON</Field.Label> - <JsonEditor - {...field} - onBlur={() => { - field.onChange(validateAndPrettifyJson(field.value)); - }} - /> - {Boolean(errors.conf) ? <Field.ErrorText>{errors.conf}</Field.ErrorText> : undefined} - </Field.Root> - )} - /> - <Controller - control={control} - name="note" - render={({ field }) => ( - <Field.Root mt={6}> - <Field.Label fontSize="md">Dag Run Notes</Field.Label> - <EditableMarkdown field={field} placeholder="Click to add note" /> - </Field.Root> - )} - /> - </Box> - </Accordion.ItemContent> - </Accordion.Item> - </Accordion.Root> + <Controller + control={control} + name="dagRunId" + render={({ field }) => ( + <Field.Root mt={6} orientation="horizontal"> + <Stack> + <Field.Label fontSize="md" style={{ flexBasis: "30%" }}> + Run ID + </Field.Label> + </Stack> + <Stack css={{ flexBasis: "70%" }}> + <Input {...field} size="sm" /> + <Field.HelperText>Optional - will be generated if not provided</Field.HelperText> + </Stack> + </Field.Root> + )} + /> + <Controller + control={control} + name="note" + render={({ field }) => ( + <Field.Root mt={6}> + <Field.Label fontSize="md">Dag Run Notes</Field.Label> + <EditableMarkdown field={field} placeholder="Click to add note" /> + </Field.Root> + )} + /> + </ConfigForm> {isPaused ? ( <Checkbox checked={unpause} colorPalette="blue" onChange={() => setUnpause(!unpause)}> Unpause {dagId} on trigger @@ -218,7 +160,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: TriggerDAGFormProps) </Button> </HStack> </Box> - </> + </Box> ); };
