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

pierrejeambrun pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-2-test by this push:
     new 255bf4d87cd Invalidate queries on dag run add/delete (#64269) (#64464)
255bf4d87cd is described below

commit 255bf4d87cd19f22133b30a37ceeb1eb96912f2a
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Mon Mar 30 13:53:06 2026 +0200

    Invalidate queries on dag run add/delete (#64269) (#64464)
    
    (cherry picked from commit a5fc63858331f95c145b3d761f747e776c694bbf)
    
    Co-authored-by: Brent Bovenzi <[email protected]>
---
 airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx  | 23 ++++++--
 .../airflow/ui/src/queries/gridViewQueryKeys.ts    | 34 +++++++++++
 .../src/airflow/ui/src/queries/useClearRun.ts      | 10 ++--
 .../ui/src/queries/useClearTaskInstances.ts        |  8 ++-
 .../src/airflow/ui/src/queries/useDeleteDag.ts     |  7 ++-
 .../src/airflow/ui/src/queries/useDeleteDagRun.ts  |  6 +-
 .../src/airflow/ui/src/queries/usePatchDagRun.ts   |  8 ++-
 .../airflow/ui/src/queries/usePatchTaskInstance.ts |  8 ++-
 .../ui/src/queries/useRefreshOnNewDagRuns.ts       | 65 +++++++++++-----------
 .../src/airflow/ui/src/queries/useTrigger.ts       | 21 ++-----
 10 files changed, 124 insertions(+), 66 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx
index 15fa137bc62..f6039c68f32 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import { ReactFlowProvider } from "@xyflow/react";
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
 import { useTranslation } from "react-i18next";
 import { FiBarChart, FiCode, FiUser, FiCalendar } from "react-icons/fi";
 import { LuChartColumn } from "react-icons/lu";
@@ -59,6 +59,11 @@ export const Dag = () => {
 
   const refetchInterval = useAutoRefresh({ dagId });
   const [hasPendingRuns, setHasPendingRuns] = useState<boolean | 
undefined>(false);
+  const previousLatestRunIdRef = useRef("");
+
+  useEffect(() => {
+    previousLatestRunIdRef.current = "";
+  }, [dagId]);
 
   const {
     data: dag,
@@ -96,9 +101,19 @@ export const Dag = () => {
     undefined,
     {
       enabled: Boolean(dagId),
-      refetchInterval: (query) => {
-        if (query.state.data && isStatePending(query.state.data.state)) {
-          setHasPendingRuns(true);
+      refetchInterval: ({ state: { data } }) => {
+        if (data) {
+          const { run_id: runId, state } = data;
+          const runIdChanged =
+            previousLatestRunIdRef.current !== "" && 
previousLatestRunIdRef.current !== runId;
+
+          if (runIdChanged || isStatePending(state)) {
+            setHasPendingRuns(true);
+          }
+
+          previousLatestRunIdRef.current = runId;
+        } else {
+          previousLatestRunIdRef.current = "";
         }
 
         return hasPendingRuns ? refetchInterval : false;
diff --git a/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.ts 
b/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.ts
new file mode 100644
index 00000000000..42060cac610
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.ts
@@ -0,0 +1,34 @@
+/*!
+ * 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 {
+  UseDagRunServiceGetDagRunsKeyFn,
+  UseDagServiceGetDagDetailsKeyFn,
+  UseDagServiceGetLatestRunInfoKeyFn,
+  UseGridServiceGetGridRunsKeyFn,
+  UseTaskInstanceServiceGetTaskInstancesKeyFn,
+} from "openapi/queries";
+
+export const gridQueryKeys = (dagId: string) =>
+  [
+    UseGridServiceGetGridRunsKeyFn({ dagId }, [{ dagId }]),
+    UseDagServiceGetDagDetailsKeyFn({ dagId }, [{ dagId }]),
+    UseDagServiceGetLatestRunInfoKeyFn({ dagId }, [{ dagId }]),
+    UseDagRunServiceGetDagRunsKeyFn({ dagId }, [{ dagId }]),
+    UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId: "~" }, [{ 
dagId, dagRunId: "~" }]),
+  ] as const;
diff --git a/airflow-core/src/airflow/ui/src/queries/useClearRun.ts 
b/airflow-core/src/airflow/ui/src/queries/useClearRun.ts
index 1749955525e..f4b23c90bd4 100644
--- a/airflow-core/src/airflow/ui/src/queries/useClearRun.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useClearRun.ts
@@ -23,13 +23,12 @@ import {
   useDagRunServiceClearDagRun,
   UseDagRunServiceGetDagRunKeyFn,
   useDagRunServiceGetDagRunsKey,
-  UseDagServiceGetDagDetailsKeyFn,
   UseGanttServiceGetGanttDataKeyFn,
   useTaskInstanceServiceGetTaskInstancesKey,
-  UseGridServiceGetGridRunsKeyFn,
 } from "openapi/queries";
 import { createErrorToaster } from "src/utils";
 
+import { gridQueryKeys } from "./gridViewQueryKeys";
 import { useClearDagRunDryRunKey } from "./useClearDagRunDryRun";
 
 export const useClearDagRun = ({
@@ -58,15 +57,16 @@ export const useClearDagRun = ({
   const onSuccess = async () => {
     const queryKeys = [
       [useTaskInstanceServiceGetTaskInstancesKey],
-      UseDagServiceGetDagDetailsKeyFn({ dagId }),
       UseDagRunServiceGetDagRunKeyFn({ dagId, dagRunId }),
       [useDagRunServiceGetDagRunsKey],
       [useClearDagRunDryRunKey, dagId],
-      UseGridServiceGetGridRunsKeyFn({ dagId }, [{ dagId }]),
       UseGanttServiceGetGanttDataKeyFn({ dagId, runId: dagRunId }),
     ];
 
-    await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ 
queryKey: key })));
+    await Promise.all([
+      ...gridQueryKeys(dagId).map((key) => queryClient.invalidateQueries({ 
queryKey: key })),
+      ...queryKeys.map((key) => queryClient.invalidateQueries({ queryKey: key 
})),
+    ]);
 
     onSuccessConfirm();
   };
diff --git a/airflow-core/src/airflow/ui/src/queries/useClearTaskInstances.ts 
b/airflow-core/src/airflow/ui/src/queries/useClearTaskInstances.ts
index b3dc26c9de7..b02cc1bbac6 100644
--- a/airflow-core/src/airflow/ui/src/queries/useClearTaskInstances.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useClearTaskInstances.ts
@@ -25,12 +25,12 @@ import {
   UseGanttServiceGetGanttDataKeyFn,
   UseTaskInstanceServiceGetMappedTaskInstanceKeyFn,
   useTaskInstanceServicePostClearTaskInstances,
-  UseGridServiceGetGridRunsKeyFn,
 } from "openapi/queries";
 import type { ApiError } from "openapi/requests";
 import type { ClearTaskInstancesBody, TaskInstanceCollectionResponse } from 
"openapi/requests/types.gen";
 import { toaster } from "src/components/ui";
 
+import { gridQueryKeys } from "./gridViewQueryKeys";
 import { useClearTaskInstancesDryRunKey } from "./useClearTaskInstancesDryRun";
 import { usePatchTaskInstanceDryRunKey } from "./usePatchTaskInstanceDryRun";
 
@@ -116,11 +116,13 @@ export const useClearTaskInstances = ({
       [useDagRunServiceGetDagRunsKey],
       [useClearTaskInstancesDryRunKey, dagId],
       [usePatchTaskInstanceDryRunKey, dagId, dagRunId],
-      UseGridServiceGetGridRunsKeyFn({ dagId }, [{ dagId }]),
       UseGanttServiceGetGanttDataKeyFn({ dagId, runId: dagRunId }),
     ];
 
-    await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ 
queryKey: key })));
+    await Promise.all([
+      ...gridQueryKeys(variables.dagId).map((key) => 
queryClient.invalidateQueries({ queryKey: key })),
+      ...queryKeys.map((key) => queryClient.invalidateQueries({ queryKey: key 
})),
+    ]);
 
     onSuccessConfirm();
   };
diff --git a/airflow-core/src/airflow/ui/src/queries/useDeleteDag.ts 
b/airflow-core/src/airflow/ui/src/queries/useDeleteDag.ts
index 28518dd01ef..7ff6b10496e 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDeleteDag.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useDeleteDag.ts
@@ -23,6 +23,7 @@ import { useDagServiceDeleteDag, useDagServiceGetDagsUiKey } 
from "openapi/queri
 import { useDagServiceGetDagKey } from "openapi/queries";
 import { toaster } from "src/components/ui";
 import { createErrorToaster } from "src/utils";
+import { gridQueryKeys } from "src/queries/gridViewQueryKeys";
 
 export const useDeleteDag = ({
   dagId,
@@ -46,7 +47,11 @@ export const useDeleteDag = ({
   };
 
   const onSuccess = async () => {
-    const queryKeys = [[useDagServiceGetDagKey, { dagId }], 
[useDagServiceGetDagsUiKey]];
+    const queryKeys = [
+      [useDagServiceGetDagKey, { dagId }],
+      [useDagServiceGetDagsUiKey],
+      ...gridQueryKeys(dagId),
+    ];
 
     await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ 
queryKey: key })));
 
diff --git a/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.ts 
b/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.ts
index 91de2ba4f75..b98bbbe5120 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useDeleteDagRun.ts
@@ -28,6 +28,7 @@ import {
 } from "openapi/queries";
 import { toaster } from "src/components/ui";
 import { createErrorToaster } from "src/utils";
+import { gridQueryKeys } from "src/queries/gridViewQueryKeys";
 
 type DeleteDagRunParams = {
   dagId: string;
@@ -58,7 +59,10 @@ export const useDeleteDagRun = ({ dagId, dagRunId, 
onSuccessConfirm }: DeleteDag
       [useTaskInstanceServiceGetHitlDetailsKey],
     ];
 
-    await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ 
queryKey: key })));
+    await Promise.all([
+      ...queryKeys.map((key) => queryClient.invalidateQueries({ queryKey: key 
})),
+      ...gridQueryKeys(dagId).map((key) => queryClient.invalidateQueries({ 
queryKey: key })),
+    ]);
 
     toaster.create({
       description: 
translate("dags:runAndTaskActions.delete.success.description", {
diff --git a/airflow-core/src/airflow/ui/src/queries/usePatchDagRun.ts 
b/airflow-core/src/airflow/ui/src/queries/usePatchDagRun.ts
index 0c46147a7b4..6b78396f891 100644
--- a/airflow-core/src/airflow/ui/src/queries/usePatchDagRun.ts
+++ b/airflow-core/src/airflow/ui/src/queries/usePatchDagRun.ts
@@ -24,10 +24,10 @@ import {
   useDagRunServiceGetDagRunsKey,
   useDagRunServicePatchDagRun,
   useTaskInstanceServiceGetTaskInstancesKey,
-  UseGridServiceGetGridRunsKeyFn,
 } from "openapi/queries";
 import { createErrorToaster } from "src/utils";
 
+import { gridQueryKeys } from "./gridViewQueryKeys";
 import { useClearDagRunDryRunKey } from "./useClearDagRunDryRun";
 
 export const usePatchDagRun = ({
@@ -59,10 +59,12 @@ export const usePatchDagRun = ({
       [useDagRunServiceGetDagRunsKey],
       [useTaskInstanceServiceGetTaskInstancesKey, { dagId, dagRunId }],
       [useClearDagRunDryRunKey, dagId],
-      UseGridServiceGetGridRunsKeyFn({ dagId }, [{ dagId }]),
     ];
 
-    await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ 
queryKey: key })));
+    await Promise.all([
+      ...gridQueryKeys(dagId).map((key) => queryClient.invalidateQueries({ 
queryKey: key })),
+      ...queryKeys.map((key) => queryClient.invalidateQueries({ queryKey: key 
})),
+    ]);
 
     if (onSuccess) {
       onSuccess();
diff --git a/airflow-core/src/airflow/ui/src/queries/usePatchTaskInstance.ts 
b/airflow-core/src/airflow/ui/src/queries/usePatchTaskInstance.ts
index 402e8d63540..6b7a2a1d1ca 100644
--- a/airflow-core/src/airflow/ui/src/queries/usePatchTaskInstance.ts
+++ b/airflow-core/src/airflow/ui/src/queries/usePatchTaskInstance.ts
@@ -24,10 +24,10 @@ import {
   UseTaskInstanceServiceGetTaskInstanceKeyFn,
   useTaskInstanceServiceGetTaskInstancesKey,
   useTaskInstanceServicePatchTaskInstance,
-  UseGridServiceGetGridRunsKeyFn,
 } from "openapi/queries";
 import { createErrorToaster } from "src/utils";
 
+import { gridQueryKeys } from "./gridViewQueryKeys";
 import { useClearTaskInstancesDryRunKey } from "./useClearTaskInstancesDryRun";
 import { usePatchTaskInstanceDryRunKey } from "./usePatchTaskInstanceDryRun";
 
@@ -65,10 +65,12 @@ export const usePatchTaskInstance = ({
       [useTaskInstanceServiceGetTaskInstancesKey],
       [usePatchTaskInstanceDryRunKey, dagId, dagRunId, { mapIndex, taskId }],
       [useClearTaskInstancesDryRunKey, dagId],
-      UseGridServiceGetGridRunsKeyFn({ dagId }, [{ dagId }]),
     ];
 
-    await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ 
queryKey: key })));
+    await Promise.all([
+      ...gridQueryKeys(dagId).map((key) => queryClient.invalidateQueries({ 
queryKey: key })),
+      ...queryKeys.map((key) => queryClient.invalidateQueries({ queryKey: key 
})),
+    ]);
 
     if (onSuccess) {
       onSuccess();
diff --git a/airflow-core/src/airflow/ui/src/queries/useRefreshOnNewDagRuns.ts 
b/airflow-core/src/airflow/ui/src/queries/useRefreshOnNewDagRuns.ts
index 8c3314ea940..2fb7673ad29 100644
--- a/airflow-core/src/airflow/ui/src/queries/useRefreshOnNewDagRuns.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useRefreshOnNewDagRuns.ts
@@ -21,56 +21,59 @@ import { useEffect, useRef } from "react";
 
 import {
   useDagServiceGetDagDetailsKey,
-  UseDagRunServiceGetDagRunsKeyFn,
-  UseDagServiceGetDagDetailsKeyFn,
-  useDagServiceGetDagsUi,
-  UseTaskInstanceServiceGetTaskInstancesKeyFn,
-  UseGridServiceGetDagStructureKeyFn,
-  UseGridServiceGetGridRunsKeyFn,
+  useDagServiceGetDagsUiKey,
   useDagServiceGetLatestRunInfo,
 } from "openapi/queries";
 
+import { gridQueryKeys } from "./gridViewQueryKeys";
 import { useConfig } from "./useConfig";
 
 export const useRefreshOnNewDagRuns = (dagId: string, hasPendingRuns: boolean 
| undefined) => {
   const queryClient = useQueryClient();
-  const previousDagRunIdRef = useRef<string>("");
+  const hasSyncedLatestRunRef = useRef(false);
+  const previousLatestRunSignatureRef = useRef("");
   const autoRefreshInterval = useConfig("auto_refresh_interval") as number;
 
+  const pollIntervalMs = Boolean(autoRefreshInterval) ? autoRefreshInterval * 
1000 : 5000;
+
   const { data: latestDagRun } = useDagServiceGetLatestRunInfo({ dagId }, 
undefined, {
-    enabled: Boolean(dagId) && !hasPendingRuns,
-    refetchInterval: Boolean(autoRefreshInterval) ? autoRefreshInterval * 1000 
: 5000,
+    enabled: Boolean(dagId),
+    refetchInterval: Boolean(dagId) && !hasPendingRuns ? pollIntervalMs : 
false,
   });
 
   useEffect(() => {
-    const latestDagRunId = latestDagRun?.run_id;
+    hasSyncedLatestRunRef.current = false;
+    previousLatestRunSignatureRef.current = "";
+  }, [dagId]);
 
-    if (latestDagRunId !== undefined && previousDagRunIdRef.current === "") {
-      previousDagRunIdRef.current = latestDagRunId;
+  useEffect(() => {
+    if (!dagId) {
+      return;
+    }
 
+    if (latestDagRun === undefined) {
       return;
     }
 
-    if (
-      latestDagRunId !== undefined &&
-      previousDagRunIdRef.current !== "" &&
-      previousDagRunIdRef.current !== latestDagRunId
-    ) {
-      previousDagRunIdRef.current = latestDagRunId;
+    const signature = latestDagRun?.run_id ?? "";
+
+    if (!hasSyncedLatestRunRef.current) {
+      hasSyncedLatestRunRef.current = true;
+      previousLatestRunSignatureRef.current = signature;
 
-      const queryKeys = [
-        [useDagServiceGetDagsUi],
-        [useDagServiceGetDagDetailsKey],
-        UseDagServiceGetDagDetailsKeyFn({ dagId }, [{ dagId }]),
-        UseDagRunServiceGetDagRunsKeyFn({ dagId }, [{ dagId }]),
-        UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId: "~" }, 
[{ dagId, dagRunId: "~" }]),
-        UseGridServiceGetDagStructureKeyFn({ dagId }, [{ dagId }]),
-        UseGridServiceGetGridRunsKeyFn({ dagId }, [{ dagId }]),
-      ];
+      return;
+    }
 
-      queryKeys.forEach((key) => {
-        void queryClient.invalidateQueries({ queryKey: key });
-      });
+    if (previousLatestRunSignatureRef.current === signature) {
+      return;
     }
-  }, [latestDagRun, dagId, queryClient]);
+
+    previousLatestRunSignatureRef.current = signature;
+
+    void Promise.all([
+      queryClient.invalidateQueries({ queryKey: [useDagServiceGetDagsUiKey] }),
+      queryClient.invalidateQueries({ queryKey: 
[useDagServiceGetDagDetailsKey] }),
+      ...gridQueryKeys(dagId).map((key) => queryClient.invalidateQueries({ 
queryKey: key })),
+    ]);
+  }, [dagId, latestDagRun, queryClient]);
 };
diff --git a/airflow-core/src/airflow/ui/src/queries/useTrigger.ts 
b/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
index 16252358afa..c7af554129c 100644
--- a/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useTrigger.ts
@@ -21,17 +21,12 @@ import { useState } from "react";
 import { useTranslation } from "react-i18next";
 import { useNavigate, useParams } from "react-router-dom";
 
-import {
-  UseDagRunServiceGetDagRunsKeyFn,
-  useDagRunServiceTriggerDagRun,
-  useDagServiceGetDagsUiKey,
-  UseTaskInstanceServiceGetTaskInstancesKeyFn,
-  UseGridServiceGetGridRunsKeyFn,
-} from "openapi/queries";
+import { useDagRunServiceTriggerDagRun, useDagServiceGetDagsUiKey } from 
"openapi/queries";
 import type { TriggerDagRunResponse } from "openapi/requests/types.gen";
 import type { DagRunTriggerParams } from "src/components/TriggerDag/types";
 import { toaster } from "src/components/ui";
 import { createErrorToaster } from "src/utils";
+import { gridQueryKeys } from "src/queries/gridViewQueryKeys";
 
 export const useTrigger = ({ dagId, onSuccessConfirm }: { dagId: string; 
onSuccessConfirm: () => void }) => {
   const queryClient = useQueryClient();
@@ -41,14 +36,10 @@ export const useTrigger = ({ dagId, onSuccessConfirm }: { 
dagId: string; onSucce
   const { dagId: selectedDagId } = useParams();
 
   const onSuccess = async (dagRun: TriggerDagRunResponse) => {
-    const queryKeys = [
-      [useDagServiceGetDagsUiKey],
-      UseDagRunServiceGetDagRunsKeyFn({ dagId }, [{ dagId }]),
-      UseTaskInstanceServiceGetTaskInstancesKeyFn({ dagId, dagRunId: "~" }, [{ 
dagId, dagRunId: "~" }]),
-      UseGridServiceGetGridRunsKeyFn({ dagId }, [{ dagId }]),
-    ];
-
-    await Promise.all(queryKeys.map((key) => queryClient.invalidateQueries({ 
queryKey: key })));
+    await Promise.all([
+      queryClient.invalidateQueries({ queryKey: [useDagServiceGetDagsUiKey] }),
+      ...gridQueryKeys(dagId).map((key) => queryClient.invalidateQueries({ 
queryKey: key })),
+    ]);
 
     toaster.create({
       description: translate("triggerDag.toaster.success.description"),

Reply via email to