This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new c541c637c9f UI: Add data interval override option for manual DAG runs 
(#57342)
c541c637c9f is described below

commit c541c637c9fe1b833b28fedf0f8a5ac6131becd7
Author: Bastien Menissier <[email protected]>
AuthorDate: Wed Dec 17 16:41:00 2025 +0000

    UI: Add data interval override option for manual DAG runs (#57342)
    
    * Add data interval start and end to TriggerDAGForm
    
    * Update API call
    
    * Fix margins
    
    * Update "Trigger Dag" form
---
 .../ui/public/i18n/locales/en/components.json      |   5 +
 .../TriggerDag/TriggerDAGAdvancedOptions.tsx       |  84 +++++++++
 .../src/components/TriggerDag/TriggerDAGForm.tsx   | 189 +++++++++++++--------
 .../src/components/TriggerDag/TriggerDAGModal.tsx  |   1 +
 .../src/airflow/ui/src/queries/useTrigger.ts       |  15 +-
 5 files changed, 224 insertions(+), 70 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json
index efa382be0dc..13e637f981b 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json
@@ -114,6 +114,11 @@
   "toggleTableView": "Show table view",
   "triggerDag": {
     "button": "Trigger",
+    "dataInterval": "Data Interval",
+    "dataIntervalAuto": "Inferred from Logical Date and Timetable",
+    "dataIntervalManual": "Specify Manually",
+    "intervalEnd": "End",
+    "intervalStart": "Start",
     "loading": "Loading Dag information...",
     "loadingFailed": "Failed to load Dag information. Please try again.",
     "runIdHelp": "Optional - will be generated if not provided",
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx
 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx
new file mode 100644
index 00000000000..b8d3fe2b9f4
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx
@@ -0,0 +1,84 @@
+/*!
+ * 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 { Input, Field, Stack } from "@chakra-ui/react";
+import { Controller, type Control } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+
+import EditableMarkdown from "./EditableMarkdown";
+import type { DagRunTriggerParams } from "./TriggerDAGForm";
+
+type TriggerDAGAdvancedOptionsProps = {
+  readonly control: Control<DagRunTriggerParams>;
+};
+
+const TriggerDAGAdvancedOptions = ({ control }: 
TriggerDAGAdvancedOptionsProps) => {
+  const { t: translate } = useTranslation(["common", "components"]);
+  const { t: rootTranslate } = useTranslation();
+
+  return (
+    <>
+      <Controller
+        control={control}
+        name="dagRunId"
+        render={({ field }) => (
+          <Field.Root mt={6} orientation="horizontal">
+            <Stack>
+              <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
+                {translate("runId")}
+              </Field.Label>
+            </Stack>
+            <Stack css={{ flexBasis: "70%" }}>
+              <Input {...field} size="sm" />
+              
<Field.HelperText>{translate("components:triggerDag.runIdHelp")}</Field.HelperText>
+            </Stack>
+          </Field.Root>
+        )}
+      />
+
+      <Controller
+        control={control}
+        name="partitionKey"
+        render={({ field }) => (
+          <Field.Root mt={6} orientation="horizontal">
+            <Stack>
+              <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
+                {rootTranslate("dagRun.partitionKey")}
+              </Field.Label>
+            </Stack>
+            <Stack css={{ flexBasis: "70%" }}>
+              <Input {...field} size="sm" />
+            </Stack>
+          </Field.Root>
+        )}
+      />
+      <Controller
+        control={control}
+        name="note"
+        render={({ field }) => (
+          <Field.Root mt={6}>
+            <Field.Label fontSize="md">{translate("note.dagRun")}</Field.Label>
+            <EditableMarkdown field={field} 
placeholder={translate("note.placeholder")} />
+          </Field.Root>
+        )}
+      />
+    </>
+  );
+};
+
+export default TriggerDAGAdvancedOptions;
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 ce2aedc46c0..fe87cfe1f5e 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Button, Box, Spacer, HStack, Input, Field, Stack } from 
"@chakra-ui/react";
+import { Button, Box, Spacer, HStack, Field, Stack, Text, VStack } from 
"@chakra-ui/react";
 import dayjs from "dayjs";
 import { useEffect, useState } from "react";
 import { Controller, useForm } from "react-hook-form";
@@ -33,27 +33,45 @@ import ConfigForm from "../ConfigForm";
 import { DateTimeInput } from "../DateTimeInput";
 import { ErrorAlert } from "../ErrorAlert";
 import { Checkbox } from "../ui/Checkbox";
-import EditableMarkdown from "./EditableMarkdown";
+import { RadioCardItem, RadioCardRoot } from "../ui/RadioCard";
+import TriggerDAGAdvancedOptions from "./TriggerDAGAdvancedOptions";
 
 type TriggerDAGFormProps = {
   readonly dagDisplayName: string;
   readonly dagId: string;
+  readonly hasSchedule: boolean;
   readonly isPaused: boolean;
   readonly onClose: () => void;
   readonly open: boolean;
 };
 
+type DataIntervalMode = "auto" | "manual";
+
 export type DagRunTriggerParams = {
   conf: string;
   dagRunId: string;
+  dataIntervalEnd: string;
+  dataIntervalMode: DataIntervalMode;
+  dataIntervalStart: string;
   logicalDate: string;
   note: string;
   partitionKey: string | undefined;
 };
 
-const TriggerDAGForm = ({ dagDisplayName, dagId, isPaused, onClose, open }: 
TriggerDAGFormProps) => {
+const dataIntervalModeOptions: Array<{ label: string; value: DataIntervalMode 
}> = [
+  { label: "components:triggerDag.dataIntervalAuto", value: "auto" },
+  { label: "components:triggerDag.dataIntervalManual", value: "manual" },
+];
+
+const TriggerDAGForm = ({
+  dagDisplayName,
+  dagId,
+  hasSchedule,
+  isPaused,
+  onClose,
+  open,
+}: TriggerDAGFormProps) => {
   const { t: translate } = useTranslation(["common", "components"]);
-  const { t: rootTranslate } = useTranslation();
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
   const [formError, setFormError] = useState(false);
   const initialParamsDict = useDagParams(dagId, open);
@@ -63,10 +81,13 @@ const TriggerDAGForm = ({ dagDisplayName, dagId, isPaused, 
onClose, open }: Trig
 
   const { mutate: togglePause } = useTogglePause({ dagId });
 
-  const { control, handleSubmit, reset } = useForm<DagRunTriggerParams>({
+  const { control, handleSubmit, reset, watch } = 
useForm<DagRunTriggerParams>({
     defaultValues: {
       conf,
       dagRunId: "",
+      dataIntervalEnd: "",
+      dataIntervalMode: "auto",
+      dataIntervalStart: "",
       // Default logical date to now, show it in the selected timezone
       logicalDate: dayjs().format(DEFAULT_DATETIME_FORMAT),
       note: "",
@@ -88,6 +109,14 @@ const TriggerDAGForm = ({ dagDisplayName, dagId, isPaused, 
onClose, open }: Trig
     setErrors((prev) => ({ ...prev, date: undefined }));
   };
 
+  const dataIntervalMode = watch("dataIntervalMode");
+  const dataIntervalStart = watch("dataIntervalStart");
+  const dataIntervalEnd = watch("dataIntervalEnd");
+  const noDataInterval = !Boolean(dataIntervalStart) || 
!Boolean(dataIntervalEnd);
+  const dataIntervalInvalid =
+    dataIntervalMode === "manual" &&
+    (noDataInterval || 
dayjs(dataIntervalStart).isAfter(dayjs(dataIntervalEnd)));
+
   const onSubmit = (data: DagRunTriggerParams) => {
     if (unpause && isPaused) {
       togglePause({
@@ -101,14 +130,9 @@ const TriggerDAGForm = ({ dagDisplayName, dagId, isPaused, 
onClose, open }: Trig
   };
 
   return (
-    <Box mt={8}>
-      <ConfigForm
-        control={control}
-        errors={errors}
-        initialParamsDict={initialParamsDict}
-        setErrors={setErrors}
-        setFormError={setFormError}
-      >
+    <>
+      <ErrorAlert error={errors.date ?? errorTrigger} />
+      <VStack alignItems="stretch" gap={2} pt={4}>
         <Controller
           control={control}
           name="logicalDate"
@@ -125,69 +149,96 @@ const TriggerDAGForm = ({ dagDisplayName, dagId, 
isPaused, onClose, open }: Trig
             </Field.Root>
           )}
         />
-
-        <Controller
-          control={control}
-          name="dagRunId"
-          render={({ field }) => (
-            <Field.Root mt={6} orientation="horizontal">
-              <Stack>
-                <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
-                  {translate("runId")}
-                </Field.Label>
-              </Stack>
-              <Stack css={{ flexBasis: "70%" }}>
-                <Input {...field} size="sm" />
-                
<Field.HelperText>{translate("components:triggerDag.runIdHelp")}</Field.HelperText>
-              </Stack>
-            </Field.Root>
-          )}
-        />
-        <Controller
+        <Spacer />
+        {hasSchedule ? (
+          <Box>
+            <Text fontSize="md" fontWeight="semibold" mb={3}>
+              {translate("components:triggerDag.dataInterval")}
+            </Text>
+            <Controller
+              control={control}
+              name="dataIntervalMode"
+              render={({ field }) => (
+                <RadioCardRoot defaultValue={String(field.value)} 
onChange={field.onChange}>
+                  <HStack align="stretch">
+                    {dataIntervalModeOptions.map((mode) => (
+                      <RadioCardItem
+                        colorPalette="brand"
+                        indicatorPlacement="start"
+                        key={mode.value}
+                        label={translate(mode.label)}
+                        value={mode.value}
+                      />
+                    ))}
+                  </HStack>
+                </RadioCardRoot>
+              )}
+            />
+            <Spacer />
+            {dataIntervalMode === "manual" ? (
+              <HStack alignItems="flex-start" mt={3} w="full">
+                <Controller
+                  control={control}
+                  name="dataIntervalStart"
+                  render={({ field }) => (
+                    <Field.Root invalid={Boolean(errors.date) || 
dataIntervalInvalid} required>
+                      
<Field.Label>{translate("components:triggerDag.intervalStart")}</Field.Label>
+                      <DateTimeInput {...field} onBlur={resetDateError} 
size="sm" />
+                    </Field.Root>
+                  )}
+                />
+                <Controller
+                  control={control}
+                  name="dataIntervalEnd"
+                  render={({ field }) => (
+                    <Field.Root invalid={Boolean(errors.date) || 
dataIntervalInvalid} required>
+                      
<Field.Label>{translate("components:triggerDag.intervalEnd")}</Field.Label>
+                      <DateTimeInput {...field} onBlur={resetDateError} 
size="sm" />
+                    </Field.Root>
+                  )}
+                />
+              </HStack>
+            ) : // eslint-disable-next-line unicorn/no-null
+            null}
+          </Box>
+        ) : // eslint-disable-next-line unicorn/no-null
+        null}
+        <Spacer />
+        {isPaused ? (
+          <>
+            <Checkbox
+              checked={unpause}
+              colorPalette="brand"
+              onChange={() => setUnpause(!unpause)}
+              wordBreak="break-all"
+            >
+              {translate("components:triggerDag.unpause", { dagDisplayName })}
+            </Checkbox>
+            <Spacer />
+          </>
+        ) : undefined}
+        <ConfigForm
           control={control}
-          name="partitionKey"
-          render={({ field }) => (
-            <Field.Root mt={6} orientation="horizontal">
-              <Stack>
-                <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
-                  {rootTranslate("dagRun.partitionKey")}
-                </Field.Label>
-              </Stack>
-              <Stack css={{ flexBasis: "70%" }}>
-                <Input {...field} size="sm" />
-              </Stack>
-            </Field.Root>
-          )}
-        />
-        <Controller
-          control={control}
-          name="note"
-          render={({ field }) => (
-            <Field.Root mt={6}>
-              <Field.Label 
fontSize="md">{translate("note.dagRun")}</Field.Label>
-              <EditableMarkdown field={field} 
placeholder={translate("note.placeholder")} />
-            </Field.Root>
-          )}
-        />
-      </ConfigForm>
-      {isPaused ? (
-        <Checkbox
-          checked={unpause}
-          colorPalette="brand"
-          onChange={() => setUnpause(!unpause)}
-          wordBreak="break-all"
+          errors={errors}
+          initialParamsDict={initialParamsDict}
+          setErrors={setErrors}
+          setFormError={setFormError}
         >
-          {translate("components:triggerDag.unpause", { dagDisplayName })}
-        </Checkbox>
-      ) : undefined}
-      <ErrorAlert error={errors.date ?? errorTrigger} />
+          <TriggerDAGAdvancedOptions control={control} />
+        </ConfigForm>
+      </VStack>
       <Box as="footer" display="flex" justifyContent="flex-end" mt={4}>
         <HStack w="full">
           <Spacer />
           <Button
             colorPalette="brand"
             disabled={
-              Boolean(errors.conf) || Boolean(errors.date) || formError || 
isPending || Boolean(errorTrigger)
+              Boolean(errors.conf) ||
+              Boolean(errors.date) ||
+              formError ||
+              isPending ||
+              Boolean(errorTrigger) ||
+              dataIntervalInvalid
             }
             onClick={() => void handleSubmit(onSubmit)()}
           >
@@ -195,7 +246,7 @@ const TriggerDAGForm = ({ dagDisplayName, dagId, isPaused, 
onClose, open }: Trig
           </Button>
         </HStack>
       </Box>
-    </Box>
+    </>
   );
 };
 
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx
index d6d7ebe9d1d..9eb3e00a0e1 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx
@@ -125,6 +125,7 @@ const TriggerDAGModal: React.FC<TriggerDAGModalProps> = ({
                 <TriggerDAGForm
                   dagDisplayName={dagDisplayName}
                   dagId={dagId}
+                  hasSchedule={hasSchedule}
                   isPaused={isPaused}
                   onClose={onClose}
                   open={open}
diff --git a/airflow-core/src/airflow/ui/src/queries/useTrigger.ts 
b/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
index b0b0a3d41fd..de2baede248 100644
--- a/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
@@ -80,10 +80,21 @@ export const useTrigger = ({ dagId, onSuccessConfirm }: { 
dagId: string; onSucce
     const parsedConfig = JSON.parse(dagRunRequestBody.conf) as Record<string, 
unknown>;
 
     const logicalDate = dagRunRequestBody.logicalDate ? new 
Date(dagRunRequestBody.logicalDate) : undefined;
-
     // eslint-disable-next-line unicorn/no-null
     const formattedLogicalDate = logicalDate?.toISOString() ?? null;
 
+    const dataIntervalStart = dagRunRequestBody.dataIntervalStart
+      ? new Date(dagRunRequestBody.dataIntervalStart)
+      : undefined;
+    // eslint-disable-next-line unicorn/no-null
+    const formattedDataIntervalStart = dataIntervalStart?.toISOString() ?? 
null;
+
+    const dataIntervalEnd = dagRunRequestBody.dataIntervalEnd
+      ? new Date(dagRunRequestBody.dataIntervalEnd)
+      : undefined;
+    // eslint-disable-next-line unicorn/no-null
+    const formattedDataIntervalEnd = dataIntervalEnd?.toISOString() ?? null;
+
     const checkDagRunId = dagRunRequestBody.dagRunId === "" ? undefined : 
dagRunRequestBody.dagRunId;
     const checkNote = dagRunRequestBody.note === "" ? undefined : 
dagRunRequestBody.note;
 
@@ -92,6 +103,8 @@ export const useTrigger = ({ dagId, onSuccessConfirm }: { 
dagId: string; onSucce
       requestBody: {
         conf: parsedConfig,
         dag_run_id: checkDagRunId,
+        data_interval_end: formattedDataIntervalEnd,
+        data_interval_start: formattedDataIntervalStart,
         logical_date: formattedLogicalDate,
         note: checkNote,
         partition_key: dagRunRequestBody.partitionKey ?? null,

Reply via email to