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);