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

amoghrajesh 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 a57fcc1c19d UI: Add custom expiration datetime picker for task store 
modal (#68394)
a57fcc1c19d is described below

commit a57fcc1c19dffb73e813c12de030d2e9f7c51a84
Author: Amogh Desai <[email protected]>
AuthorDate: Fri Jun 12 09:38:24 2026 +0530

    UI: Add custom expiration datetime picker for task store modal (#68394)
---
 .../ui/src/pages/TaskStore/TaskStoreModal.tsx      | 117 ++++++++++++++-------
 1 file changed, 79 insertions(+), 38 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/TaskStore/TaskStoreModal.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskStore/TaskStoreModal.tsx
index 6f0dd6e65f7..f0475f1d2bf 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskStore/TaskStoreModal.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskStore/TaskStoreModal.tsx
@@ -16,8 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Button, Heading, Input, RadioCard, Text, VStack } from 
"@chakra-ui/react";
-import { useEffect, useState } from "react";
+import { Box, Button, Flex, Heading, Input, RadioCard, Text, VStack } from 
"@chakra-ui/react";
+import dayjs from "dayjs";
+import tz from "dayjs/plugin/timezone";
+import { useEffect, useMemo, useState } from "react";
 import { useTranslation } from "react-i18next";
 
 import {
@@ -26,10 +28,14 @@ import {
   useTaskStoreServiceListTaskStoreKey,
   useTaskStoreServiceSetTaskStore,
 } from "openapi/queries";
+import { DateTimeInput } from "src/components/DateTimeInput";
 import { JsonEditor } from "src/components/JsonEditor";
 import { Dialog, ProgressBar } from "src/components/ui";
+import { useTimezone } from "src/context/timezone";
 import { useStoreMutation } from "src/queries/useStoreMutation";
 
+dayjs.extend(tz);
+
 type Props = {
   readonly dagId: string;
   readonly isOpen: boolean;
@@ -66,11 +72,17 @@ export const TaskStoreModal = ({
   taskId,
 }: Props) => {
   const { t: translate } = useTranslation(["dag", "common"]);
+  const { selectedTimezone } = useTimezone();
   const [key, setKey] = useState("");
   const [value, setValue] = useState("");
-  const [expiresAt, setExpiresAt] = useState<"default" | "never">("default");
+  const [expiresAt, setExpiresAt] = useState<"custom" | "default" | 
"never">("default");
+  const [customExpiresAt, setCustomExpiresAt] = useState("");
   const isEditMode = mode === "edit";
   const isValueValid = isJsonValid(value);
+  const minDateTime = useMemo(
+    () => dayjs().tz(selectedTimezone).format("YYYY-MM-DDTHH:mm"),
+    [selectedTimezone],
+  );
 
   const { data: existingState, isLoading: isFetchingExisting } = 
useTaskStoreServiceGetTaskStore(
     { dagId, dagRunId: runId, key: storeKey ?? "", mapIndex, taskId },
@@ -81,9 +93,17 @@ export const TaskStoreModal = ({
   useEffect(() => {
     if (isEditMode && existingState !== undefined) {
       setValue(JSON.stringify(existingState.value, null, 2));
-      setExpiresAt(existingState.expires_at === null ? "never" : "default");
+      if (existingState.expires_at === null) {
+        setExpiresAt("never");
+      } else {
+        // The API always returns an absolute datetime — there is no way to 
distinguish
+        // the default value from value saved as custom datetime. Show it as
+        // Custom pre-filled with the resolved date so the user can adjust it.
+        setExpiresAt("custom");
+        
setCustomExpiresAt(dayjs(existingState.expires_at).tz(selectedTimezone).format("YYYY-MM-DDTHH:mm"));
+      }
     }
-  }, [existingState, isEditMode]);
+  }, [existingState, isEditMode, selectedTimezone]);
 
   const { isPending, mutate: setTaskStore } = useTaskStoreServiceSetTaskStore(
     useStoreMutation({
@@ -100,7 +120,10 @@ export const TaskStoreModal = ({
       dagRunId: runId,
       key: isEditMode ? (storeKey ?? "") : key,
       mapIndex,
-      requestBody: { expires_at: expiresAt === "never" ? null : "default", 
value: JSON.parse(value) },
+      requestBody: {
+        expires_at: expiresAt === "never" ? null : expiresAt === "custom" ? 
customExpiresAt : "default",
+        value: JSON.parse(value),
+      },
       taskId,
     });
   };
@@ -140,40 +163,53 @@ export const TaskStoreModal = ({
                   {translate("dag:taskStore.expiresAt.label")}
                 </Text>
                 <RadioCard.Root
-                  onValueChange={(ev) => setExpiresAt(ev.value as "default" | 
"never")}
+                  onValueChange={(ev) => setExpiresAt(ev.value as "custom" | 
"default" | "never")}
                   value={expiresAt}
                 >
-                  <RadioCard.Item value="default">
-                    <RadioCard.ItemHiddenInput />
-                    <RadioCard.ItemControl>
-                      <RadioCard.ItemContent>
-                        <RadioCard.ItemText>
-                          {translate("dag:taskStore.expiresAt.default", { 
interval: "30 days" })}
-                        </RadioCard.ItemText>
-                      </RadioCard.ItemContent>
-                      <RadioCard.ItemIndicator />
-                    </RadioCard.ItemControl>
-                  </RadioCard.Item>
-                  <RadioCard.Item value="never">
-                    <RadioCard.ItemHiddenInput />
-                    <RadioCard.ItemControl>
-                      <RadioCard.ItemContent>
-                        
<RadioCard.ItemText>{translate("dag:taskStore.expiresAt.never")}</RadioCard.ItemText>
-                      </RadioCard.ItemContent>
-                      <RadioCard.ItemIndicator />
-                    </RadioCard.ItemControl>
-                  </RadioCard.Item>
-                  {/* TODO: Add a datetime picker for custom expiry once a 
picker component is available */}
-                  <RadioCard.Item disabled value="custom">
-                    <RadioCard.ItemHiddenInput />
-                    <RadioCard.ItemControl>
-                      <RadioCard.ItemContent>
-                        
<RadioCard.ItemText>{translate("dag:taskStore.expiresAt.custom")}</RadioCard.ItemText>
-                      </RadioCard.ItemContent>
-                      <RadioCard.ItemIndicator />
-                    </RadioCard.ItemControl>
-                  </RadioCard.Item>
+                  <Flex gap={2}>
+                    <RadioCard.Item flex={1} value="default">
+                      <RadioCard.ItemHiddenInput />
+                      <RadioCard.ItemControl>
+                        <RadioCard.ItemContent>
+                          <RadioCard.ItemText>
+                            {translate("dag:taskStore.expiresAt.default", { 
interval: "30 days" })}
+                          </RadioCard.ItemText>
+                        </RadioCard.ItemContent>
+                        <RadioCard.ItemIndicator />
+                      </RadioCard.ItemControl>
+                    </RadioCard.Item>
+                    <RadioCard.Item flex={1} value="never">
+                      <RadioCard.ItemHiddenInput />
+                      <RadioCard.ItemControl>
+                        <RadioCard.ItemContent>
+                          <RadioCard.ItemText>
+                            {translate("dag:taskStore.expiresAt.never")}
+                          </RadioCard.ItemText>
+                        </RadioCard.ItemContent>
+                        <RadioCard.ItemIndicator />
+                      </RadioCard.ItemControl>
+                    </RadioCard.Item>
+                    <RadioCard.Item flex={1} value="custom">
+                      <RadioCard.ItemHiddenInput />
+                      <RadioCard.ItemControl>
+                        <RadioCard.ItemContent>
+                          <RadioCard.ItemText>
+                            {translate("dag:taskStore.expiresAt.custom")}
+                          </RadioCard.ItemText>
+                        </RadioCard.ItemContent>
+                        <RadioCard.ItemIndicator />
+                      </RadioCard.ItemControl>
+                    </RadioCard.Item>
+                  </Flex>
                 </RadioCard.Root>
+                {expiresAt === "custom" && (
+                  <DateTimeInput
+                    min={minDateTime}
+                    mt={2}
+                    onChange={(ev) => setCustomExpiresAt(ev.target.value)}
+                    value={customExpiresAt}
+                  />
+                )}
               </Box>
             </VStack>
           )}
@@ -183,7 +219,12 @@ export const TaskStoreModal = ({
             {translate("common:modal.cancel")}
           </Button>
           <Button
-            disabled={isFetchingExisting || !isValueValid || (!isEditMode && 
key === "")}
+            disabled={
+              isFetchingExisting ||
+              !isValueValid ||
+              (!isEditMode && key === "") ||
+              (expiresAt === "custom" && !customExpiresAt)
+            }
             loading={isPending}
             onClick={onSave}
           >

Reply via email to