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

choo121600 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 cd1ed360b9a Refresh Dag cards after deleting Dag runs (#75931)
cd1ed360b9a is described below

commit cd1ed360b9a3ad86d3d0885f515cf32fff8f5146
Author: Anzin Mohammed <[email protected]>
AuthorDate: Thu Oct 29 19:04:05 2026 +0530

    Refresh Dag cards after deleting Dag runs (#75931)
---
 .../airflow/ui/src/queries/useBulkDeleteDagRuns.ts |  6 ++
 .../ui/src/queries/useDeleteDagRun.test.tsx        | 82 ++++++++++++++++++++++
 .../src/airflow/ui/src/queries/useDeleteDagRun.ts  |  6 ++
 .../src/queries/useRecentTaskStateCounts.test.tsx  | 20 ++++++
 .../ui/src/queries/useRecentTaskStateCounts.tsx    |  9 ++-
 5 files changed, 122 insertions(+), 1 deletion(-)

diff --git a/airflow-core/src/airflow/ui/src/queries/useBulkDeleteDagRuns.ts 
b/airflow-core/src/airflow/ui/src/queries/useBulkDeleteDagRuns.ts
index 428b77d5113..ced786d71c4 100644
--- a/airflow-core/src/airflow/ui/src/queries/useBulkDeleteDagRuns.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useBulkDeleteDagRuns.ts
@@ -24,6 +24,9 @@ import { useTranslation } from "react-i18next";
 import {
   useDagRunServiceBulkDagRuns,
   useDagRunServiceGetDagRunsKey,
+  useDagServiceGetDagRunStateCountsUiKey,
+  useDagServiceGetDagsUiKey,
+  useDagServiceGetRecentTaskInstanceStateCountsUiKey,
   useTaskInstanceServiceGetTaskInstancesKey,
 } from "openapi/queries";
 import type { BulkBody_BulkDAGRunBody_, BulkResponse } from 
"openapi/requests/types.gen";
@@ -45,6 +48,9 @@ export const useBulkDeleteDagRuns = ({ deselectKeys, 
onSuccessConfirm }: Props)
   const onSuccess = async (responseData: BulkResponse) => {
     await Promise.all([
       queryClient.invalidateQueries({ queryKey: 
[useDagRunServiceGetDagRunsKey] }),
+      queryClient.invalidateQueries({ queryKey: [useDagServiceGetDagsUiKey] }),
+      queryClient.invalidateQueries({ queryKey: 
[useDagServiceGetDagRunStateCountsUiKey] }),
+      queryClient.invalidateQueries({ queryKey: 
[useDagServiceGetRecentTaskInstanceStateCountsUiKey] }),
       queryClient.invalidateQueries({ queryKey: 
[useTaskInstanceServiceGetTaskInstancesKey] }),
       ...tiPerAttemptQueryKeys.map((key) => queryClient.invalidateQueries({ 
queryKey: key })),
       ...[...affectedDagIds.current].flatMap((dagId) =>
diff --git a/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.test.tsx 
b/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.test.tsx
new file mode 100644
index 00000000000..70aec8a5c9c
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.test.tsx
@@ -0,0 +1,82 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { PropsWithChildren } from "react";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { act, renderHook, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import {
+  UseDagServiceGetDagRunStateCountsUiKeyFn,
+  UseDagServiceGetDagsUiKeyFn,
+  UseDagServiceGetRecentTaskInstanceStateCountsUiKeyFn,
+} from "openapi/queries";
+import { DagRunService } from "openapi/requests";
+
+import { useBulkDeleteDagRuns } from "./useBulkDeleteDagRuns";
+import { useDeleteDagRun } from "./useDeleteDagRun";
+
+const useDeleteSingleRun = (onSuccessConfirm: VoidFunction) => {
+  const { mutate } = useDeleteDagRun({ dagId: "dag", dagRunId: "run", 
onSuccessConfirm });
+
+  return () => mutate({ dagId: "dag", dagRunId: "run" });
+};
+
+const useDeleteMultipleRuns = (onSuccessConfirm: VoidFunction) => {
+  const { bulkAction } = useBulkDeleteDagRuns({ deselectKeys: vi.fn(), 
onSuccessConfirm });
+
+  return () =>
+    bulkAction({ actions: [{ action: "delete", entities: [{ dag_id: "dag", 
dag_run_id: "run" }] }] });
+};
+
+describe.each([
+  { operation: "single-run deletion", useDelete: useDeleteSingleRun },
+  { operation: "bulk-run deletion", useDelete: useDeleteMultipleRuns },
+])("Dag card cache after $operation", ({ useDelete }) => {
+  beforeEach(() => {
+    vi.spyOn(DagRunService, "deleteDagRun").mockResolvedValue(undefined);
+    vi.spyOn(DagRunService, "bulkDagRuns").mockResolvedValue({
+      delete: { errors: [], success: ["dag.run"] },
+    });
+  });
+  afterEach(() => vi.restoreAllMocks());
+
+  it.each([
+    { name: "Dag list", queryKey: UseDagServiceGetDagsUiKeyFn({ limit: 50 }) },
+    { name: "run-state counts", queryKey: 
UseDagServiceGetDagRunStateCountsUiKeyFn({ dagIds: ["dag"] }) },
+    {
+      name: "recent task-state counts",
+      queryKey: UseDagServiceGetRecentTaskInstanceStateCountsUiKeyFn({ 
dagRunIds: [1] }),
+    },
+  ])("invalidates the cached $name before navigating back", async ({ queryKey 
}) => {
+    const queryClient = new QueryClient({ defaultOptions: { queries: { 
staleTime: Infinity } } });
+
+    queryClient.setQueryData(queryKey, { cached: true });
+    const wrapper = ({ children }: PropsWithChildren) => (
+      <QueryClientProvider 
client={queryClient}>{children}</QueryClientProvider>
+    );
+    const onSuccessConfirm = vi.fn();
+    const { result } = renderHook(() => useDelete(onSuccessConfirm), { wrapper 
});
+
+    act(() => result.current());
+    await waitFor(() => expect(onSuccessConfirm).toHaveBeenCalled());
+    expect(queryClient.getQueryState(queryKey)?.isInvalidated).toBe(true);
+    queryClient.clear();
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.ts 
b/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.ts
index 3f442a474e8..0cb9fc84135 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.ts
@@ -23,6 +23,9 @@ import {
   useDagRunServiceDeleteDagRun,
   useDagRunServiceGetDagRunsKey,
   UseDagRunServiceGetDagRunKeyFn,
+  useDagServiceGetDagRunStateCountsUiKey,
+  useDagServiceGetDagsUiKey,
+  useDagServiceGetRecentTaskInstanceStateCountsUiKey,
   UseGanttServiceGetGanttDataKeyFn,
   useTaskInstanceServiceGetHitlDetailsKey,
   useTaskInstanceServiceGetMappedTaskInstanceKey,
@@ -59,6 +62,9 @@ export const useDeleteDagRun = ({ dagId, dagRunId, 
onSuccessConfirm }: DeleteDag
     const queryKeys = [
       UseDagRunServiceGetDagRunKeyFn({ dagId, dagRunId }),
       [useDagRunServiceGetDagRunsKey],
+      [useDagServiceGetDagsUiKey],
+      [useDagServiceGetDagRunStateCountsUiKey],
+      [useDagServiceGetRecentTaskInstanceStateCountsUiKey],
       [useTaskInstanceServiceGetTaskInstancesKey],
       [useTaskInstanceServiceGetHitlDetailsKey],
       UseGanttServiceGetGanttDataKeyFn({ dagId, runId: dagRunId }),
diff --git 
a/airflow-core/src/airflow/ui/src/queries/useRecentTaskStateCounts.test.tsx 
b/airflow-core/src/airflow/ui/src/queries/useRecentTaskStateCounts.test.tsx
index 2bd2eaf235b..92d3c258ce3 100644
--- a/airflow-core/src/airflow/ui/src/queries/useRecentTaskStateCounts.test.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useRecentTaskStateCounts.test.tsx
@@ -43,6 +43,26 @@ const makeDag = (overrides: 
Partial<DAGWithLatestDagRunsResponse>) =>
   }) as DAGWithLatestDagRunsResponse;
 
 describe("useRecentTaskStateCounts", () => {
+  it.each([false, true])("discards deleted-run counts when another Dag has 
runs: %s", (hasOtherDag) => {
+    const cachedResponse = {
+      data: { dags: [{ dag_id: "my_dag", run_ids: ["run"], state_counts: { 
success: 60 } }] },
+      isLoading: false,
+    } as ReturnType<typeof useDagServiceGetRecentTaskInstanceStateCountsUi>;
+
+    vi.mocked(useDagServiceGetRecentTaskInstanceStateCountsUi)
+      .mockReturnValueOnce(cachedResponse)
+      .mockReturnValueOnce(cachedResponse);
+    const { rerender, result } = renderHook(({ dags }) => 
useRecentTaskStateCounts(dags), {
+      initialProps: { dags: [makeDag({})] },
+    });
+
+    expect(result.current.entriesByDag.my_dag?.state_counts).toEqual({ 
success: 60 });
+    rerender({
+      dags: [makeDag({ latest_dag_runs: [] }), ...(hasOtherDag ? [makeDag({ 
dag_id: "other_dag" })] : [])],
+    });
+    expect(result.current.entriesByDag).toEqual({});
+  });
+
   it.each([
     { expected: 3000, has_unfinished_runs: true, is_paused: false },
     { expected: false, has_unfinished_runs: false, is_paused: false },
diff --git 
a/airflow-core/src/airflow/ui/src/queries/useRecentTaskStateCounts.tsx 
b/airflow-core/src/airflow/ui/src/queries/useRecentTaskStateCounts.tsx
index a814e67c0d1..999fed3f8ee 100644
--- a/airflow-core/src/airflow/ui/src/queries/useRecentTaskStateCounts.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useRecentTaskStateCounts.tsx
@@ -47,6 +47,9 @@ export const useRecentTaskStateCounts = (
     .map((dag) => dag.latest_dag_runs[0]?.id)
     .filter((id): id is number => id !== undefined)
     .sort((left, right) => left - right);
+  const dagIdsWithRuns = new Set(
+    (dags ?? []).filter((dag) => dag.latest_dag_runs.length > 0).map((dag) => 
dag.dag_id),
+  );
 
   const { data, isLoading } = 
useDagServiceGetRecentTaskInstanceStateCountsUi({ dagRunIds }, undefined, {
     enabled: show && dagRunIds.length > 0,
@@ -55,7 +58,11 @@ export const useRecentTaskStateCounts = (
   });
 
   return {
-    entriesByDag: Object.fromEntries((data?.dags ?? []).map((entry) => 
[entry.dag_id, entry])),
+    entriesByDag: Object.fromEntries(
+      (data?.dags ?? [])
+        .filter((entry) => dagIdsWithRuns.has(entry.dag_id))
+        .map((entry) => [entry.dag_id, entry]),
+    ),
     isLoading,
     show,
   };

Reply via email to