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