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>
               )}

Reply via email to