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 e068c6e0934 Update grid view refresh (#71004)
e068c6e0934 is described below

commit e068c6e09348435b253a28e96389c0f096bc4d96
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Aug 11 11:57:05 2026 -0400

    Update grid view refresh (#71004)
---
 .../airflow/ui/src/layouts/Details/Grid/Grid.tsx   |  2 -
 .../ui/src/queries/gridViewQueryKeys.test.ts       | 39 ++++++++++++++++++
 .../airflow/ui/src/queries/gridViewQueryKeys.ts    |  3 ++
 .../src/airflow/ui/src/queries/useGridStructure.ts | 38 +++++++----------
 .../ui/src/queries/useGridTISummaries.test.tsx     | 47 +++++++++++++++++++++-
 .../airflow/ui/src/queries/useGridTISummaries.ts   |  7 +++-
 6 files changed, 107 insertions(+), 29 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
index d3402432191..4a73ef31e38 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx
@@ -31,7 +31,6 @@ import { NavigationModes, useNavigation } from 
"src/hooks/navigation";
 import { useGridRuns } from "src/queries/useGridRuns.ts";
 import { useGridStructure } from "src/queries/useGridStructure.ts";
 import { useGridTiSummariesStream } from "src/queries/useGridTISummaries.ts";
-import { isStatePending } from "src/utils";
 
 import { Bar } from "./Bar";
 import { DurationAxis } from "./DurationAxis";
@@ -120,7 +119,6 @@ export const Grid = ({
   const { data: dagStructure } = useGridStructure({
     dagRunState,
     depth,
-    hasActiveRun: gridRuns?.some((dr) => isStatePending(dr.state)),
     includeDownstream,
     includeUpstream,
     limit,
diff --git a/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.test.ts 
b/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.test.ts
new file mode 100644
index 00000000000..d4d170062c9
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.test.ts
@@ -0,0 +1,39 @@
+/*!
+ * 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 { describe, expect, it } from "vitest";
+
+import { useGridServiceGetDagStructureKey } from "openapi/queries";
+
+import { gridQueryKeys } from "./gridViewQueryKeys";
+
+describe("gridQueryKeys", () => {
+  it("includes the Dag structure key so structure refreshes on invalidation 
instead of polling", () => {
+    const keys = gridQueryKeys("dag_1");
+
+    expect(keys.some((key) => key[0] === 
useGridServiceGetDagStructureKey)).toBe(true);
+  });
+
+  it("scopes every key to the given dagId", () => {
+    const keys = gridQueryKeys("dag_1");
+
+    keys.forEach((key) => {
+      expect(key[1]).toMatchObject({ dagId: "dag_1" });
+    });
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.ts 
b/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.ts
index 2c43ff47eae..ebc396cc6e3 100644
--- a/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.ts
+++ b/airflow-core/src/airflow/ui/src/queries/gridViewQueryKeys.ts
@@ -20,6 +20,7 @@ import {
   UseDagRunServiceGetDagRunsKeyFn,
   UseDagServiceGetDagDetailsKeyFn,
   UseDagServiceGetLatestRunInfoKeyFn,
+  UseGridServiceGetDagStructureKeyFn,
   UseGridServiceGetGridRunsKeyFn,
   useTaskInstanceServiceGetExtraLinksKey,
   useTaskInstanceServiceGetLogKey,
@@ -30,6 +31,8 @@ import {
 export const gridQueryKeys = (dagId: string) =>
   [
     UseGridServiceGetGridRunsKeyFn({ dagId }, [{ dagId }]),
+    // Structure is topology only, so it's invalidated on run changes here 
rather than polled.
+    UseGridServiceGetDagStructureKeyFn({ dagId }, [{ dagId }]),
     UseDagServiceGetDagDetailsKeyFn({ dagId }, [{ dagId }]),
     UseDagServiceGetLatestRunInfoKeyFn({ dagId }, [{ dagId }]),
     UseDagRunServiceGetDagRunsKeyFn({ dagId }, [{ dagId }]),
diff --git a/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts 
b/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts
index b2fb353330a..8f1c3cc4cc2 100644
--- a/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts
@@ -22,12 +22,11 @@ import { useGridServiceGetDagStructure } from 
"openapi/queries";
 import type { DagRunState, DagRunType } from "openapi/requests/types.gen";
 import { SearchParamsKeys } from "src/constants/searchParams";
 import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
-import { useAutoRefresh } from "src/utils";
 
+// Topology only (no per-run state), so it refreshes via `gridQueryKeys` 
invalidation, not polling.
 export const useGridStructure = ({
   dagRunState,
   depth,
-  hasActiveRun,
   includeDownstream,
   includeUpstream,
   limit,
@@ -38,7 +37,6 @@ export const useGridStructure = ({
 }: {
   dagRunState?: DagRunState | undefined;
   depth?: number | undefined;
-  hasActiveRun?: boolean;
   includeDownstream?: boolean;
   includeUpstream?: boolean;
   limit?: number;
@@ -48,7 +46,6 @@ export const useGridStructure = ({
   triggeringUser?: string | undefined;
 }) => {
   const { dagId = "" } = useParams();
-  const refetchInterval = useAutoRefresh({ dagId });
 
   // Advanced-search toggle picks between the substring ``runIdPattern`` and 
the
   // index-friendly ``runIdPrefixPattern`` variants of the Run ID filter.
@@ -68,26 +65,19 @@ export const useGridStructure = ({
     value: triggeringUser,
   });
 
-  // This is necessary for keepPreviousData
-  const { data: dagStructure, ...rest } = useGridServiceGetDagStructure(
-    {
-      dagId,
-      depth,
-      includeDownstream,
-      includeUpstream,
-      limit,
-      orderBy: ["-run_after"],
-      root,
-      ...runIdPatternArg,
-      runType: runType ? [runType] : undefined,
-      state: dagRunState ? [dagRunState] : undefined,
-      ...triggeringUserArg,
-    },
-    undefined,
-    {
-      refetchInterval: hasActiveRun ? refetchInterval : false,
-    },
-  );
+  const { data: dagStructure, ...rest } = useGridServiceGetDagStructure({
+    dagId,
+    depth,
+    includeDownstream,
+    includeUpstream,
+    limit,
+    orderBy: ["-run_after"],
+    root,
+    ...runIdPatternArg,
+    runType: runType ? [runType] : undefined,
+    state: dagRunState ? [dagRunState] : undefined,
+    ...triggeringUserArg,
+  });
 
   return { data: dagStructure, ...rest };
 };
diff --git 
a/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.test.tsx 
b/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.test.tsx
index c0ea5457ac5..50cbd3c7d93 100644
--- a/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.test.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.test.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { focusManager, QueryClient, QueryClientProvider } from 
"@tanstack/react-query";
 import { act, renderHook } from "@testing-library/react";
 import React from "react";
 import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 
"vitest";
@@ -77,6 +77,8 @@ describe("useGridTiSummariesStream", () => {
     vi.unstubAllGlobals();
     vi.restoreAllMocks();
     vi.useRealTimers();
+    focusManager.setFocused(undefined);
+    (useAutoRefresh as Mock).mockReturnValue(false);
   });
 
   it("streams summaries correctly on mount", async () => {
@@ -350,4 +352,47 @@ describe("useGridTiSummariesStream", () => {
 
     expect(mockFetch).toHaveBeenCalledTimes(1);
   });
+
+  it("does not re-stream on the interval while the tab is hidden, and resumes 
when visible", async () => {
+    const interval = 30_000;
+
+    (useAutoRefresh as Mock).mockReturnValue(interval);
+
+    const queryClient = new QueryClient({
+      defaultOptions: {
+        queries: {
+          gcTime: Infinity,
+          staleTime: Infinity,
+        },
+      },
+    });
+    const wrapper = createWrapper(queryClient);
+
+    // A pending run arms the interval effect.
+    renderHook(() => useGridTiSummariesStream({ dagId: "dag_1", runIds: 
["run_1"], states: ["running"] }), {
+      wrapper,
+    });
+
+    // Flush the mount stream without advancing far enough to fire the 
interval.
+    await act(async () => {
+      await vi.advanceTimersByTimeAsync(1);
+    });
+    expect(mockFetch).toHaveBeenCalledTimes(1);
+
+    act(() => {
+      focusManager.setFocused(false);
+    });
+    await act(async () => {
+      await vi.advanceTimersByTimeAsync(interval);
+    });
+    expect(mockFetch).toHaveBeenCalledTimes(1);
+
+    act(() => {
+      focusManager.setFocused(true);
+    });
+    await act(async () => {
+      await vi.advanceTimersByTimeAsync(interval);
+    });
+    expect(mockFetch).toHaveBeenCalledTimes(2);
+  });
 });
diff --git a/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts 
b/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts
index eab4485c86d..dd226b38080 100644
--- a/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useGridTISummaries.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useQueryClient } from "@tanstack/react-query";
+import { focusManager, useQueryClient } from "@tanstack/react-query";
 import { useEffect, useRef, useState } from "react";
 
 import {
@@ -149,7 +149,10 @@ export const useGridTiSummariesStream = ({
     // reopen it — a redundant connection plus an AbortError on every grid 
mount — so let the interval be
     // the only re-stream trigger.
     const timer = setInterval(() => {
-      setRefreshTick((tick) => tick + 1);
+      // Skip re-streams while the tab is hidden, matching React Query's 
refetchIntervalInBackground: false.
+      if (focusManager.isFocused()) {
+        setRefreshTick((tick) => tick + 1);
+      }
     }, baseRefetchInterval);
 
     return () => clearInterval(timer);

Reply via email to