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 551a2326874c6504ac9b8538d0d7aaa051a659d5
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Apr 29 08:50:13 2025 -0400

    Create a timezone-aware datetime input component (#49911)
    
    (cherry picked from commit 935c49d780ff3da349ba55289f949ecdecbbe21e)
---
 .../src/components/DagActions/RunBackfillForm.tsx  |  7 ++-
 .../airflow/ui/src/components/DateTimeInput.tsx    | 56 ++++++++++++++++++++++
 .../src/components/FlexibleForm/FieldDateTime.tsx  | 13 +++++
 .../src/components/TriggerDag/TriggerDAGForm.tsx   | 17 ++-----
 4 files changed, 76 insertions(+), 17 deletions(-)

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 544876efa2f..4ab35b2f466 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -28,6 +28,7 @@ import { useCreateBackfillDryRun } from 
"src/queries/useCreateBackfillDryRun";
 import { useTogglePause } from "src/queries/useTogglePause";
 import { pluralize } from "src/utils";
 
+import { DateTimeInput } from "../DateTimeInput";
 import { ErrorAlert } from "../ErrorAlert";
 import { Checkbox } from "../ui/Checkbox";
 import { RadioCardItem, RadioCardLabel, RadioCardRoot } from "../ui/RadioCard";
@@ -143,12 +144,11 @@ const RunBackfillForm = ({ dag, onClose }: 
RunBackfillFormProps) => {
               render={({ field }) => (
                 <Field.Root invalid={Boolean(errors.date)}>
                   <Field.Label>From</Field.Label>
-                  <Input
+                  <DateTimeInput
                     {...field}
                     max={dataIntervalEnd || today}
                     onBlur={resetDateError}
                     size="sm"
-                    type="datetime-local"
                   />
                 </Field.Root>
               )}
@@ -159,13 +159,12 @@ const RunBackfillForm = ({ dag, onClose }: 
RunBackfillFormProps) => {
               render={({ field }) => (
                 <Field.Root invalid={Boolean(errors.date)}>
                   <Field.Label>To</Field.Label>
-                  <Input
+                  <DateTimeInput
                     {...field}
                     max={today}
                     min={dataIntervalStart || undefined}
                     onBlur={resetDateError}
                     size="sm"
-                    type="datetime-local"
                   />
                 </Field.Root>
               )}
diff --git a/airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx 
b/airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx
new file mode 100644
index 00000000000..c4fa9010fc2
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/DateTimeInput.tsx
@@ -0,0 +1,56 @@
+/*!
+ * 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, type InputProps } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import tz from "dayjs/plugin/timezone";
+import { forwardRef } from "react";
+
+import { useTimezone } from "src/context/timezone";
+
+dayjs.extend(tz);
+
+type Props = {
+  readonly value: string;
+} & InputProps;
+
+export const DateTimeInput = forwardRef<HTMLInputElement, Props>(({ onChange, 
value, ...rest }, ref) => {
+  const { selectedTimezone } = useTimezone();
+
+  // Make the value timezone-aware
+  const date = 
dayjs(value).tz(selectedTimezone).format("YYYY-MM-DDTHH:mm:ss.SSS");
+
+  return (
+    <Input
+      onChange={(event) =>
+        onChange?.({
+          ...event,
+          target: {
+            ...event.target,
+            // Return a timezone-aware ISO string
+            value: dayjs(event.target.value).tz(selectedTimezone, 
true).toISOString(),
+          },
+        })
+      }
+      ref={ref}
+      type="datetime-local"
+      value={date}
+      {...rest}
+    />
+  );
+});
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
index a5deae64b9a..07911837996 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
@@ -21,6 +21,7 @@ import { Input, type InputProps } from "@chakra-ui/react";
 import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
+import { DateTimeInput } from "../DateTimeInput";
 
 export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & 
InputProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
@@ -41,6 +42,18 @@ export const FieldDateTime = ({ name, ...rest }: 
FlexibleFormElementProps & Inpu
     setParamsDict(paramsDict);
   };
 
+  if (rest.type === "datetime-local") {
+    return (
+      <DateTimeInput
+        id={`element_${name}`}
+        name={`element_${name}`}
+        onChange={(event) => handleChange(event.target.value)}
+        size="sm"
+        value={((param.value ?? "") as string).slice(0, 16)}
+      />
+    );
+  }
+
   return (
     <Input
       id={`element_${name}`}
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 f73b8d5398d..b3d470ba417 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -18,18 +18,16 @@
  */
 import { Input, Button, Box, Spacer, HStack, Field, Stack } from 
"@chakra-ui/react";
 import dayjs from "dayjs";
-import tz from "dayjs/plugin/timezone";
-import utc from "dayjs/plugin/utc";
 import { useEffect, useState } from "react";
 import { useForm, Controller } from "react-hook-form";
 import { FiPlay } from "react-icons/fi";
 
-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 { useTrigger } from "src/queries/useTrigger";
 
+import { DateTimeInput } from "../DateTimeInput";
 import { ErrorAlert } from "../ErrorAlert";
 import { FlexibleForm, flexibleFormDefaultSection } from "../FlexibleForm";
 import { JsonEditor } from "../JsonEditor";
@@ -37,9 +35,6 @@ import { Accordion } from "../ui";
 import { Checkbox } from "../ui/Checkbox";
 import EditableMarkdown from "./EditableMarkdown";
 
-dayjs.extend(utc);
-dayjs.extend(tz);
-
 type TriggerDAGFormProps = {
   readonly dagId: string;
   readonly isPaused: boolean;
@@ -57,7 +52,6 @@ export type DagRunTriggerParams = {
 const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: 
TriggerDAGFormProps) => {
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
   const initialParamsDict = useDagParams(dagId, open);
-  const { selectedTimezone } = useTimezone();
   const { error: errorTrigger, isPending, triggerDagRun } = useTrigger({ 
dagId, onSuccessConfirm: onClose });
   const { conf, setConf } = useParamStore();
   const [unpause, setUnpause] = useState(true);
@@ -69,7 +63,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: 
TriggerDAGFormProps)
       conf,
       dagRunId: "",
       // Default logical date to now, show it in the selected timezone
-      logicalDate: 
dayjs().tz(selectedTimezone).format("YYYY-MM-DDTHH:mm:ss.SSS"),
+      logicalDate: dayjs().format("YYYY-MM-DDTHH:mm:ss.SSS"),
       note: "",
     },
   });
@@ -90,10 +84,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: 
TriggerDAGFormProps)
         },
       });
     }
-    triggerDagRun({
-      ...data,
-      logicalDate: dayjs(data.logicalDate).tz(selectedTimezone, 
true).toISOString(),
-    });
+    triggerDagRun(data);
   };
 
   const validateAndPrettifyJson = (value: string) => {
@@ -154,7 +145,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: 
TriggerDAGFormProps)
                       </Field.Label>
                     </Stack>
                     <Stack css={{ flexBasis: "70%" }}>
-                      <Input {...field} onBlur={resetDateError} size="sm" 
type="datetime-local" />
+                      <DateTimeInput {...field} onBlur={resetDateError} 
size="sm" />
                     </Stack>
                   </Field.Root>
                 )}

Reply via email to