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 7298091cb80 Fix dialog unmounting for Chakra upgrade (#67674)
7298091cb80 is described below
commit 7298091cb80dafdcde1f42119f4ab6ffd90a30ee
Author: Aditya Patel <[email protected]>
AuthorDate: Tue Jun 2 16:32:09 2026 -0400
Fix dialog unmounting for Chakra upgrade (#67674)
---
airflow-core/src/airflow/ui/CONTRIBUTING.md | 21 -----
.../ui/src/components/Clear/Run/ClearRunButton.tsx | 2 +-
.../ui/src/components/Clear/Run/ClearRunDialog.tsx | 26 +++++-
.../Clear/TaskInstance/ClearTaskInstanceButton.tsx | 5 +-
.../Clear/TaskInstance/ClearTaskInstanceDialog.tsx | 101 +++++++++++----------
.../src/components/MarkAs/Run/MarkRunAsButton.tsx | 2 +-
.../src/components/MarkAs/Run/MarkRunAsDialog.tsx | 25 ++++-
.../MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx | 14 ++-
.../MarkAs/TaskGroup/MarkTaskGroupAsDialog.tsx | 25 ++++-
.../TaskInstance/MarkTaskInstanceAsButton.tsx | 4 +-
.../TaskInstance/MarkTaskInstanceAsDialog.tsx | 25 ++++-
.../airflow/ui/src/pages/TaskInstance/Header.tsx | 12 +--
12 files changed, 160 insertions(+), 102 deletions(-)
diff --git a/airflow-core/src/airflow/ui/CONTRIBUTING.md
b/airflow-core/src/airflow/ui/CONTRIBUTING.md
index e904d83a724..3dc9073b5cd 100644
--- a/airflow-core/src/airflow/ui/CONTRIBUTING.md
+++ b/airflow-core/src/airflow/ui/CONTRIBUTING.md
@@ -40,27 +40,6 @@ Manually:
- Run `pnpm install && pnpm dev`
- Note: Make sure to access the UI via the Airflow localhost port (8080 or
28080) and not the vite port (5173)
-## Dependency upgrade caveats
-
-### `@chakra-ui/react` — held at `~3.34.0`
-
-Do not relax this pin or bump `@chakra-ui/react` above `3.34.x` without
-re-checking every dialog in this codebase that is mounted conditionally
-(`{open ? <Dialog ... /> : undefined}`), e.g. `ClearRunButton`,
-`MarkRunAsButton`, `ClearTaskInstanceButton`, `MarkTaskInstanceAsButton`.
-
-`@chakra-ui/[email protected]` pulls in `@ark-ui/react@>=5.36.0`, where dialog
-`pointer-events` cleanup moved from an inline style on the dialog DOM
-to the dismissable layer's close-transition completion. Conditionally
-mounted dialogs unmount before that transition runs, leaving the
-`pointer-events: none` lock stuck on `document` — the page then refuses
-every click (scroll still works) until a full refresh. See PR #67646
-for the original revert and the timeline.
-
-To bump safely, first rewrite the conditional-mount sites to always
-render the dialog (and gate any expensive dry-run queries with
-`enabled: open`) so the dialog can drive its own close transition.
-
## More
See [node environment setup
docs](/contributing-docs/15_node_environment_setup.rst)
diff --git
a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx
b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx
index 0c396b7b212..6cbc3aa08b0 100644
--- a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunButton.tsx
@@ -55,7 +55,7 @@ const ClearRunButton = ({ dagRun, isHotkeyEnabled = false }:
Props) => {
>
<CgRedo />
</IconButton>
- {open ? <ClearRunDialog dagRun={dagRun} onClose={onClose} open={open} />
: undefined}
+ <ClearRunDialog dagRun={dagRun} onClose={onClose} open={open} />
</>
);
};
diff --git
a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx
b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx
index 62710def9d4..425ce9f8236 100644
--- a/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Clear/Run/ClearRunDialog.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { Button, Flex, Heading, VStack } from "@chakra-ui/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { CgRedo } from "react-icons/cg";
@@ -43,6 +43,17 @@ const ClearRunDialog = ({ dagRun, onClose, open }: Props) =>
{
const { t: translate } = useTranslation();
const [note, setNote] = useState<string | null>(dagRun.note);
+
+ useEffect(() => {
+ if (open) {
+ setNote(dagRun.note);
+ }
+ }, [dagRun.note, open]);
+
+ const handleClose = () => {
+ setNote(dagRun.note);
+ onClose();
+ };
const [selectedOptions, setSelectedOptions] =
useState<Array<string>>(["existingTasks"]);
const onlyFailed = selectedOptions.includes("onlyFailed");
const onlyNew = selectedOptions.includes("newTasks");
@@ -61,6 +72,7 @@ const ClearRunDialog = ({ dagRun, onClose, open }: Props) => {
dagId,
dagRunId,
options: {
+ enabled: open,
refetchInterval: (query) =>
query.state.data?.task_instances.some((ti) => "state" in ti &&
isStatePending(ti.state))
? refetchInterval
@@ -76,7 +88,7 @@ const ClearRunDialog = ({ dagRun, onClose, open }: Props) => {
const { isPending, mutate } = useClearDagRun({
dagId,
dagRunId,
- onSuccessConfirm: onClose,
+ onSuccessConfirm: handleClose,
});
// Check if DAG versions differ (works for both bundle-versioned and local
bundles)
@@ -89,7 +101,15 @@ const ClearRunDialog = ({ dagRun, onClose, open }: Props)
=> {
const shouldShowBundleVersionOption = versionsDiffer && !onlyNew;
return (
- <Dialog.Root lazyMount onOpenChange={onClose} open={open}>
+ <Dialog.Root
+ lazyMount
+ onOpenChange={(details) => {
+ if (!details.open) {
+ handleClose();
+ }
+ }}
+ open={open}
+ >
<Dialog.Content backdrop>
<Dialog.Header>
<VStack align="start" gap={4}>
diff --git
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx
index 3e3992e3e6b..84fc78c1e68 100644
---
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx
+++
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceButton.tsx
@@ -87,12 +87,11 @@ const ClearTaskInstanceButton = ({
<CgRedo />
</IconButton>
- {useInternalDialog && open && isGroup ? (
+ {useInternalDialog && isGroup ? (
<ClearGroupTaskInstanceDialog onClose={onClose} open={open}
taskInstance={groupTaskInstance} />
) : undefined}
{useInternalDialog &&
- open &&
!isGroup &&
allMapped &&
dagId !== undefined &&
@@ -108,7 +107,7 @@ const ClearTaskInstanceButton = ({
/>
) : undefined}
- {useInternalDialog && open && !isGroup && !allMapped && taskInstance ? (
+ {useInternalDialog && !isGroup && !allMapped && taskInstance ? (
<ClearTaskInstanceDialog onClose={onClose} open={open}
taskInstance={taskInstance} />
) : undefined}
</>
diff --git
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx
index b152566eb7d..02f20f2a9b7 100644
---
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx
+++
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { Button, Flex, Heading, useDisclosure, VStack } from
"@chakra-ui/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { CgRedo } from "react-icons/cg";
@@ -70,18 +70,12 @@ const ClearTaskInstanceDialog = (props: Props) => {
const taskId = props.allMapped ? props.taskId : props.taskInstance.task_id;
const mapIndex: number | undefined = props.allMapped ? undefined :
props.taskInstance.map_index;
const taskInstance: TaskInstanceResponse | undefined = props.allMapped ?
undefined : props.taskInstance;
- const onCloseDialog = props.onClose;
+ const closeDialog = props.onClose;
const openDialog = props.open;
/* eslint-enable react/destructuring-assignment */
const { t: translate } = useTranslation();
const { onClose, onOpen, open } = useDisclosure();
- const { isPending, mutate } = useClearTaskInstances({
- dagId,
- dagRunId,
- onSuccessConfirm: onCloseDialog,
- });
-
const [selectedOptions, setSelectedOptions] =
useState<Array<string>>(["downstream"]);
const onlyFailed = selectedOptions.includes("onlyFailed");
@@ -93,6 +87,17 @@ const ClearTaskInstanceDialog = (props: Props) => {
const [note, setNote] = useState<string | null>(taskInstance?.note ?? null);
+ useEffect(() => {
+ if (openDialog) {
+ setNote(taskInstance?.note ?? null);
+ }
+ }, [openDialog, taskInstance?.note]);
+
+ const onCloseDialog = () => {
+ setNote(taskInstance?.note ?? null);
+ closeDialog();
+ };
+
// Get current DAG's bundle version to compare with task instance's DAG
version bundle version
const { data: dagDetails } = useDagServiceGetDagDetails({
dagId,
@@ -112,6 +117,12 @@ const ClearTaskInstanceDialog = (props: Props) => {
fallback: dagVersionsDiffer,
});
+ const { isPending, mutate } = useClearTaskInstances({
+ dagId,
+ dagRunId,
+ onSuccessConfirm: onCloseDialog,
+ });
+
const refetchInterval = useAutoRefresh({ dagId });
const { data } = useClearTaskInstancesDryRun({
@@ -230,45 +241,43 @@ const ClearTaskInstanceDialog = (props: Props) => {
</Dialog.Body>
</Dialog.Content>
</Dialog.Root>
- {open ? (
- <ClearTaskInstanceConfirmationDialog
- dagDetails={{
- dagId,
- dagRunId,
- downstream,
- future,
- mapIndex,
- onlyFailed,
- past,
- taskId,
- upstream,
- }}
- onClose={onClose}
- onConfirm={() => {
- const noteChanged = note !== (taskInstance?.note ?? null);
+ <ClearTaskInstanceConfirmationDialog
+ dagDetails={{
+ dagId,
+ dagRunId,
+ downstream,
+ future,
+ mapIndex,
+ onlyFailed,
+ past,
+ taskId,
+ upstream,
+ }}
+ onClose={onClose}
+ onConfirm={() => {
+ const noteChanged = note !== (taskInstance?.note ?? null);
- mutate({
- dagId,
- requestBody: {
- dag_run_id: dagRunId,
- dry_run: false,
- include_downstream: downstream,
- include_future: future,
- include_past: past,
- include_upstream: upstream,
- note: noteChanged ? note : undefined,
- only_failed: onlyFailed,
- run_on_latest_version: runOnLatestVersion,
- task_ids: allMapped ? [taskId] : [[taskId, mapIndex as
number]],
- ...(preventRunningTask ? { prevent_running_task: true } : {}),
- },
- });
- onCloseDialog();
- }}
- open={open}
- preventRunningTask={preventRunningTask}
- />
- ) : null}
+ mutate({
+ dagId,
+ requestBody: {
+ dag_run_id: dagRunId,
+ dry_run: false,
+ include_downstream: downstream,
+ include_future: future,
+ include_past: past,
+ include_upstream: upstream,
+ note: noteChanged ? note : undefined,
+ only_failed: onlyFailed,
+ run_on_latest_version: runOnLatestVersion,
+ task_ids: allMapped ? [taskId] : [[taskId, mapIndex as number]],
+ ...(preventRunningTask ? { prevent_running_task: true } : {}),
+ },
+ });
+ onCloseDialog();
+ }}
+ open={open}
+ preventRunningTask={preventRunningTask}
+ />
</>
);
};
diff --git
a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx
b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx
index 7268e837b71..e933ec88838 100644
--- a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx
@@ -109,7 +109,7 @@ const MarkRunAsButton = ({ dagRun, isHotkeyEnabled = false
}: Props) => {
</Menu.Content>
</Menu.Root>
- {open ? <MarkRunAsDialog dagRun={dagRun} onClose={onClose} open={open}
state={state} /> : undefined}
+ <MarkRunAsDialog dagRun={dagRun} onClose={onClose} open={open}
state={state} />
</Box>
);
};
diff --git
a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsDialog.tsx
b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsDialog.tsx
index 0c01cb01981..8a0112a2d9f 100644
--- a/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsDialog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsDialog.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { Button, Flex, Heading, VStack } from "@chakra-ui/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import type { DagRunMutableStates, DAGRunResponse } from
"openapi/requests/types.gen";
@@ -39,10 +39,29 @@ const MarkRunAsDialog = ({ dagRun, onClose, open, state }:
Props) => {
const { t: translate } = useTranslation();
const [note, setNote] = useState<string | null>(dagRun.note);
- const { isPending, mutate } = usePatchDagRun({ dagId, dagRunId, onSuccess:
onClose });
+
+ useEffect(() => {
+ if (open) {
+ setNote(dagRun.note);
+ }
+ }, [dagRun.note, open]);
+
+ const handleClose = () => {
+ setNote(dagRun.note);
+ onClose();
+ };
+ const { isPending, mutate } = usePatchDagRun({ dagId, dagRunId, onSuccess:
handleClose });
return (
- <Dialog.Root lazyMount onOpenChange={onClose} open={open}>
+ <Dialog.Root
+ lazyMount
+ onOpenChange={(details) => {
+ if (!details.open) {
+ handleClose();
+ }
+ }}
+ open={open}
+ >
<Dialog.Content backdrop>
<Dialog.Header>
<VStack align="start" gap={4}>
diff --git
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx
index 1fd80b403ac..fa5cd2322ae 100644
---
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx
+++
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsButton.tsx
@@ -108,14 +108,12 @@ const MarkTaskGroupAsButton = ({ groupTaskInstance,
isHotkeyEnabled = false }: P
</Menu.Content>
</Menu.Root>
- {open ? (
- <MarkTaskGroupAsDialog
- groupTaskInstance={groupTaskInstance}
- onClose={onClose}
- open={open}
- state={state}
- />
- ) : undefined}
+ <MarkTaskGroupAsDialog
+ groupTaskInstance={groupTaskInstance}
+ onClose={onClose}
+ open={open}
+ state={state}
+ />
</Box>
);
};
diff --git
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsDialog.tsx
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsDialog.tsx
index e3fd6505732..fbaeefcb1bb 100644
---
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsDialog.tsx
+++
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskGroup/MarkTaskGroupAsDialog.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { Button, Flex, Heading, VStack } from "@chakra-ui/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
@@ -51,11 +51,22 @@ const MarkTaskGroupAsDialog = ({ groupTaskInstance,
onClose, open, state }: Prop
const [note, setNote] = useState<string | null>(null);
+ useEffect(() => {
+ if (open) {
+ setNote(null);
+ }
+ }, [open]);
+
+ const handleClose = () => {
+ setNote(null);
+ onClose();
+ };
+
const { isPending, mutate } = usePatchTaskGroup({
dagId,
dagRunId: runId,
groupId,
- onSuccess: onClose,
+ onSuccess: handleClose,
});
const { data, isPending: isPendingDryRun } = usePatchTaskGroupDryRun({
dagId,
@@ -81,7 +92,15 @@ const MarkTaskGroupAsDialog = ({ groupTaskInstance, onClose,
open, state }: Prop
};
return (
- <Dialog.Root lazyMount onOpenChange={onClose} open={open}>
+ <Dialog.Root
+ lazyMount
+ onOpenChange={(details) => {
+ if (!details.open) {
+ handleClose();
+ }
+ }}
+ open={open}
+ >
<Dialog.Content backdrop>
<Dialog.Header>
<VStack align="start" gap={4}>
diff --git
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx
index a930938132d..20680890aa7 100644
---
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx
+++
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsButton.tsx
@@ -109,9 +109,7 @@ const MarkTaskInstanceAsButton = ({ isHotkeyEnabled =
false, taskInstance }: Pro
</Menu.Content>
</Menu.Root>
- {open ? (
- <MarkTaskInstanceAsDialog onClose={onClose} open={open} state={state}
taskInstance={taskInstance} />
- ) : undefined}
+ <MarkTaskInstanceAsDialog onClose={onClose} open={open} state={state}
taskInstance={taskInstance} />
</Box>
);
};
diff --git
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsDialog.tsx
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsDialog.tsx
index 3b0c170d5e4..06847b5a8ae 100644
---
a/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsDialog.tsx
+++
b/airflow-core/src/airflow/ui/src/components/MarkAs/TaskInstance/MarkTaskInstanceAsDialog.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { Button, Flex, Heading, VStack } from "@chakra-ui/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import type { TaskInstanceResponse, TaskInstanceState } from
"openapi/requests/types.gen";
@@ -52,11 +52,22 @@ const MarkTaskInstanceAsDialog = ({ onClose, open, state,
taskInstance }: Props)
const [note, setNote] = useState<string | null>(taskInstance.note);
+ useEffect(() => {
+ if (open) {
+ setNote(taskInstance.note);
+ }
+ }, [open, taskInstance.note]);
+
+ const handleClose = () => {
+ setNote(taskInstance.note);
+ onClose();
+ };
+
const { isPending, mutate } = usePatchTaskInstance({
dagId,
dagRunId,
mapIndex,
- onSuccess: onClose,
+ onSuccess: handleClose,
taskId,
});
const { data, isPending: isPendingDryRun } = usePatchTaskInstanceDryRun({
@@ -84,7 +95,15 @@ const MarkTaskInstanceAsDialog = ({ onClose, open, state,
taskInstance }: Props)
};
return (
- <Dialog.Root lazyMount onOpenChange={onClose} open={open}>
+ <Dialog.Root
+ lazyMount
+ onOpenChange={(details) => {
+ if (!details.open) {
+ handleClose();
+ }
+ }}
+ open={open}
+ >
<Dialog.Content backdrop>
<Dialog.Header>
<VStack align="start" gap={4}>
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
index 438054f48df..8c0ac81dc03 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
@@ -121,13 +121,11 @@ export const Header = ({ taskInstance }: { readonly
taskInstance: TaskInstanceRe
stats={stats}
title={`${taskInstance.task_display_name}${taskInstance.map_index > -1
? ` [${taskInstance.rendered_map_index ?? taskInstance.map_index}]` : ""}`}
/>
- {clearOpen ? (
- <ClearTaskInstanceDialog
- onClose={() => setClearOpen(false)}
- open={clearOpen}
- taskInstance={taskInstance}
- />
- ) : undefined}
+ <ClearTaskInstanceDialog
+ onClose={() => setClearOpen(false)}
+ open={clearOpen}
+ taskInstance={taskInstance}
+ />
</Box>
);
};