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

Reply via email to