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,