This is an automated email from the ASF dual-hosted git repository.
ashb pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new b43d79c0244 [v3-4-test] UI: Fix clearing a task instance doing nothing
after Confirm (#74725) (#74738)
b43d79c0244 is described below
commit b43d79c024496bf74d4bd18cd320dc9a62525a39
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Oct 14 15:56:53 2026 +0100
[v3-4-test] UI: Fix clearing a task instance doing nothing after Confirm
(#74725) (#74738)
Confirming a task instance clear opens a second dialog that re-runs the
dry run and then sends the clear. Since the zag dialog upgrade that came
with Chakra 3.37, closing the clear dialog underneath dismisses that
second dialog twice over: the layer stack dismisses layers above a
removed one, and the clear dialog hands focus back to its trigger. The
dry run finished but the clear request was never sent, and the
queued-task warning never appeared.
The second dialog now ignores both of those dismissals. Outside clicks
and Escape still close it.
(cherry picked from commit ea805e3bfb095cfb615f957e09366acefa9b1161)
closes: #74712
Co-authored-by: Kaxil Naik <[email protected]>
---
.../ClearTaskInstanceConfirmationDialog.tsx | 12 +++++-
.../TaskInstance/ClearTaskInstanceDialog.test.tsx | 47 +++++++++++++++++++++-
.../airflow/ui/src/pages/Run/Execution.test.tsx | 22 ----------
3 files changed, 57 insertions(+), 24 deletions(-)
diff --git
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceConfirmationDialog.tsx
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceConfirmationDialog.tsx
index 3d04624e1f6..bfab29da32b 100644
---
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceConfirmationDialog.tsx
+++
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceConfirmationDialog.tsx
@@ -80,8 +80,18 @@ const ClearTaskInstanceConfirmationDialog = ({
}
}, [isFetching, data, open, taskCurrentState, preventRunningTask, onConfirm,
onClose]);
+ // This dialog opens as the clear dialog closes beneath it, and that close
would dismiss it
+ // twice over: removing a dialog layer dismisses the layers stacked above
it, and the clear
+ // dialog hands focus back to its trigger outside this one. Either drops the
clear before
+ // the dry run settles. Outside clicks and Escape still close it.
return (
- <Dialog.Root lazyMount onOpenChange={onClose} open={open}>
+ <Dialog.Root
+ lazyMount
+ onFocusOutside={(event) => event.preventDefault()}
+ onOpenChange={onClose}
+ onRequestDismiss={(event) => event.preventDefault()}
+ open={open}
+ >
<Dialog.Content backdrop>
{isFetching ? (
<VStack align="center" gap={3} justify="center" py={8}>
diff --git
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.test.tsx
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.test.tsx
index 520e3fe0872..ccf766bb46a 100644
---
a/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.test.tsx
+++
b/airflow-core/src/airflow/ui/src/components/Clear/TaskInstance/ClearTaskInstanceDialog.test.tsx
@@ -28,6 +28,7 @@ import BulkClearTaskInstancesButton from
"src/pages/TaskInstances/BulkClearTaskI
import {
CLEAR_KEEP_TASK_STATE_KEY,
+ CLEAR_PREVENT_RUNNING_TASK_KEY,
CLEAR_TASK_INSTANCE_DEFAULT_OPTIONS_KEY,
} from "src/constants/localStorage";
import { handlers } from "src/mocks/handlers";
@@ -95,6 +96,7 @@ const loopMember: TaskInstanceResponse = {
const mutateMock = vi.fn();
const dryRunsMock = vi.fn();
+const confirmationDryRun = { isFetching: false };
vi.mock("src/queries/useClearTaskInstances", () => ({
useClearTaskInstances: () => ({ isPending: false, mutate: mutateMock }),
@@ -104,7 +106,11 @@ vi.mock("src/queries/useClearTaskInstances", () => ({
// list in the main dialog and the confirmation dialog's running-task gate; a
// same-tick response for both keeps the gate from blocking on a real fetch.
vi.mock("src/queries/useClearTaskInstancesDryRun", () => ({
- useClearTaskInstancesDryRun: () => ({ data: affectedTasks, isFetching:
false, isPending: false }),
+ useClearTaskInstancesDryRun: () => ({
+ data: affectedTasks,
+ isFetching: confirmationDryRun.isFetching,
+ isPending: false,
+ }),
useClearTaskInstancesDryRuns: (args: unknown) => {
dryRunsMock(args);
@@ -124,6 +130,7 @@ beforeAll(() => {
});
afterEach(() => {
affectedTasks.task_instances = [taskInstance];
+ confirmationDryRun.isFetching = false;
mutateMock.mockClear();
dryRunsMock.mockClear();
server.resetHandlers();
@@ -154,6 +161,44 @@ describe("ClearTaskInstanceDialog", () => {
expect(requestBody.keep_task_state).toBe(true);
});
+ it("sends the clear once the running-task check resolves", async () => {
+ const onClose = vi.fn();
+
+ confirmationDryRun.isFetching = true;
+ const { rerender } = render(
+ <ClearTaskInstanceDialog onClose={onClose} open
taskInstance={taskInstance} />,
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ fireEvent.click(await screen.findByRole("button", { name:
/modal\.confirm/iu }));
+ // Let the clear dialog finish closing while the running-task check is
still in flight.
+ await new Promise((resolve) => {
+ setTimeout(resolve, 0);
+ });
+ confirmationDryRun.isFetching = false;
+ rerender(<ClearTaskInstanceDialog onClose={onClose} open
taskInstance={taskInstance} />);
+
+ await waitFor(() => expect(mutateMock).toHaveBeenCalledTimes(1));
+ const [{ requestBody }] = mutateMock.mock.calls[0] as [{ requestBody: {
dry_run?: boolean } }];
+
+ expect(requestBody.dry_run).toBe(false);
+ });
+
+ it("warns instead of clearing a queued task when running tasks are
protected", async () => {
+ localStorage.setItem(CLEAR_PREVENT_RUNNING_TASK_KEY, JSON.stringify(true));
+ affectedTasks.task_instances = [{ ...taskInstance, state: "queued" }];
+ render(<ClearTaskInstanceDialog onClose={vi.fn()} open
taskInstance={taskInstance} />, {
+ wrapper: Wrapper,
+ });
+
+ fireEvent.click(await screen.findByRole("button", { name:
/modal\.confirm/iu }));
+
+ expect(await
screen.findByText(/confirmationDialog\.title/iu)).toBeVisible();
+ expect(mutateMock).not.toHaveBeenCalled();
+ });
+
it("clears the kept loop passes by execution id when another pass is
unticked", async () => {
affectedTasks.task_instances = [loopPass("pass-0", 0), loopPass("pass-2",
2)];
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Execution.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Run/Execution.test.tsx
index 17fdd4c5728..08c512e59b3 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Execution.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Execution.test.tsx
@@ -42,28 +42,6 @@ import { Execution } from "./Execution";
vi.mock("src/router", () => ({ taskInstanceRoutes: [] }));
-// The running-task gate opens a second dialog that jsdom dismisses before its
dry run settles.
-vi.mock("src/components/Clear/TaskInstance/ClearTaskInstanceConfirmationDialog",
async () => {
- const { useEffect } = await import("react");
-
- const ConfirmImmediately = ({
- onClose,
- onConfirm,
- }: {
- readonly onClose: () => void;
- readonly onConfirm?: () => void;
- }) => {
- useEffect(() => {
- onConfirm?.();
- onClose();
- }, [onClose, onConfirm]);
-
- return undefined;
- };
-
- return { default: ConfirmImmediately };
-});
-
beforeAll(() => {
i18n.addResourceBundle("en", "dag", dagTranslations, true, true);
i18n.addResourceBundle("en", "common", commonTranslations, true, true);