This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 c81ca3c4c9e UI: Warn when the logical date is outside the Dag's dates
(#75223)
c81ca3c4c9e is described below
commit c81ca3c4c9e5e6da15b3971f042be4267a9d53e6
Author: Yuseok Jo <[email protected]>
AuthorDate: Thu Oct 22 05:20:04 2026 +0900
UI: Warn when the logical date is outside the Dag's dates (#75223)
* UI: Warn in the trigger form when the logical date is outside the Dag's
dates
* Show the logical date warning as field helper text instead of an alert
* Show the Dag start and end dates under the logical date to keep the form
from shifting
---
.../ui/public/i18n/locales/en/components.json | 6 +++
.../components/TriggerDag/TriggerDAGForm.test.tsx | 46 ++++++++++++++++++++++
.../src/components/TriggerDag/TriggerDAGForm.tsx | 43 +++++++++++++++++++-
3 files changed, 94 insertions(+), 1 deletion(-)
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 ab5b658685f..2a90a9cd84a 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
@@ -162,6 +162,12 @@
"toggleTableView": "Show table view",
"triggerDag": {
"button": "Trigger",
+ "dagStartEndDate": {
+ "both": "Dag start date: {{startDate}}, end date: {{endDate}}",
+ "end": "Dag end date: {{endDate}}",
+ "noTaskWillRun": "No task will run.",
+ "start": "Dag start date: {{startDate}}"
+ },
"dataInterval": "Data Interval",
"dataIntervalAuto": "Inferred from Logical Date and Timetable",
"dataIntervalManual": "Specify Manually",
diff --git
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.test.tsx
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.test.tsx
index 2d3cffef5bd..d4a4e96bdb4 100644
---
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.test.tsx
+++
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.test.tsx
@@ -18,10 +18,13 @@
*/
import "@testing-library/jest-dom/vitest";
import { fireEvent, render, screen, waitFor, within } from
"@testing-library/react";
+import { http, HttpResponse } from "msw";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type * as OpenapiQueries from "openapi/queries";
+import { MOCK_DAG } from "src/mocks/handlers/dag";
+import { server } from "src/mocks/server";
import { Wrapper } from "src/utils/Wrapper";
import TriggerDAGForm from "./TriggerDAGForm";
@@ -173,6 +176,49 @@ describe("TriggerDAGForm", () => {
expect(screen.queryByText("pausedDag.drain")).not.toBeInTheDocument();
});
+ it.each([
+ {
+ dates: { end_date: null, start_date: "2000-01-01T00:00:00Z" },
+ scenario: "within the Dag's dates",
+ text: "components:triggerDag.dagStartEndDate.start",
+ },
+ {
+ dates: { end_date: null, start_date: "2999-01-01T00:00:00Z" },
+ scenario: "before the Dag's start date",
+ text: "components:triggerDag.dagStartEndDate.noTaskWillRun
components:triggerDag.dagStartEndDate.start",
+ },
+ {
+ dates: { end_date: "2000-01-01T00:00:00Z", start_date:
"1999-01-01T00:00:00Z" },
+ scenario: "after the Dag's end date",
+ text: "components:triggerDag.dagStartEndDate.noTaskWillRun
components:triggerDag.dagStartEndDate.both",
+ },
+ ])(
+ "shows the Dag start and end dates when the default logical date is
$scenario",
+ async ({ dates, text }) => {
+ server.use(
+ http.get("/api/v2/dags/dated_dag/details", () =>
+ HttpResponse.json({ ...MOCK_DAG, ...dates, dag_id: "dated_dag" }),
+ ),
+ );
+
+ render(
+ <TriggerDAGForm
+ dagId="dated_dag"
+ error={undefined}
+ hasSchedule={false}
+ isPartitioned={false}
+ isPaused={false}
+ isPending={false}
+ onSubmitTrigger={vi.fn()}
+ open
+ />,
+ { wrapper: Wrapper },
+ );
+
+ expect(await screen.findByText(text)).toBeInTheDocument();
+ },
+ );
+
it("propagates the selected run's conf to the JSON editor when the Dag has
no declared params", async () => {
useDagParamsMock.mockReturnValue({ paramsDict: {} });
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 5f40f57f485..52e9150a9a1 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -24,12 +24,15 @@ import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { FiPlay } from "react-icons/fi";
+import { useDagServiceGetDagDetails } from "openapi/queries";
+
import { RadioCardItem, RadioCardRoot } from "src/system-components";
+import { useTimezone } from "src/context/timezone";
import { useDagParams } from "src/queries/useDagParams";
import { useParamStore } from "src/queries/useParamStore";
import { useTogglePause } from "src/queries/useTogglePause";
-import { DEFAULT_DATETIME_FORMAT } from "src/utils/datetimeUtils";
+import { DATE_FORMAT, DEFAULT_DATETIME_FORMAT, formatDate } from
"src/utils/datetimeUtils";
import ConfigForm from "../ConfigForm";
import { DateTimeInput } from "../DateTimeInput";
@@ -64,6 +67,8 @@ type TriggerDAGFormProps = {
| undefined;
};
+const DAG_DATE_FORMAT = `${DATE_FORMAT} HH:mm z`;
+
const TriggerDAGForm = ({
dagId,
disabled = false,
@@ -81,6 +86,8 @@ const TriggerDAGForm = ({
const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
const [formError, setFormError] = useState(false);
const initialParamsDict = useDagParams(dagId, open);
+ const { data: dagDetails } = useDagServiceGetDagDetails({ dagId },
undefined, { enabled: open });
+ const { selectedTimezone } = useTimezone();
const { conf, initialParamDict, setConf, setInitialParamDict } =
useParamStore();
const [pausedDagAction, setPausedDagAction] =
useState<PausedDagAction>("unpause");
const [hasAppliedPrefill, setHasAppliedPrefill] = useState(false);
@@ -165,6 +172,33 @@ const TriggerDAGForm = ({
const resetDateError = () => setErrors((prev) => ({ ...prev, date: undefined
}));
+ const logicalDate = watch("logicalDate");
+ const startDate = dagDetails?.start_date ?? undefined;
+ const endDate = dagDetails?.end_date ?? undefined;
+ const formatDagDate = (date: string) => formatDate(date, selectedTimezone,
DAG_DATE_FORMAT);
+ let dagStartEndDate: string | undefined;
+
+ if (startDate !== undefined && endDate !== undefined) {
+ dagStartEndDate = translate("components:triggerDag.dagStartEndDate.both", {
+ endDate: formatDagDate(endDate),
+ startDate: formatDagDate(startDate),
+ });
+ } else if (startDate !== undefined) {
+ dagStartEndDate = translate("components:triggerDag.dagStartEndDate.start",
{
+ startDate: formatDagDate(startDate),
+ });
+ } else if (endDate !== undefined) {
+ dagStartEndDate = translate("components:triggerDag.dagStartEndDate.end", {
+ endDate: formatDagDate(endDate),
+ });
+ }
+ // Every task's start and end dates are bounded by the Dag's, so a run
outside them creates no
+ // task instances and is marked successful without running anything.
+ const isOutsideDagStartEndDate =
+ Boolean(logicalDate) &&
+ ((startDate !== undefined && dayjs(logicalDate).isBefore(startDate)) ||
+ (endDate !== undefined && dayjs(logicalDate).isAfter(endDate)));
+
const dataIntervalMode = watch("dataIntervalMode");
const dataIntervalStart = watch("dataIntervalStart");
const dataIntervalEnd = watch("dataIntervalEnd");
@@ -200,6 +234,13 @@ const TriggerDAGForm = ({
</Stack>
<Stack css={{ flexBasis: "70%" }}>
<DateTimeInput {...field} onBlur={resetDateError}
size="sm" />
+ {dagStartEndDate === undefined ? undefined : (
+ <Field.HelperText color={isOutsideDagStartEndDate ?
"fg.warning" : undefined}>
+ {isOutsideDagStartEndDate
+ ?
`${translate("components:triggerDag.dagStartEndDate.noTaskWillRun")}
${dagStartEndDate}`
+ : dagStartEndDate}
+ </Field.HelperText>
+ )}
</Stack>
</Field.Root>
)}