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