This is an automated email from the ASF dual-hosted git repository.
vatsrahul1001 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 820e9d2a98e fix graph view not auto-refreshing task states during DAG
run (#65518)
820e9d2a98e is described below
commit 820e9d2a98ee9dcadfa67c91eb412445db9b6402
Author: Rahul Vats <[email protected]>
AuthorDate: Mon Apr 20 14:35:25 2026 +0530
fix graph view not auto-refreshing task states during DAG run (#65518)
---
.../ui/src/layouts/Details/Graph/Graph.test.tsx | 147 +++++++++++++++++++++
.../airflow/ui/src/layouts/Details/Graph/Graph.tsx | 12 +-
2 files changed, 157 insertions(+), 2 deletions(-)
diff --git
a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.test.tsx
b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.test.tsx
new file mode 100644
index 00000000000..8fa32dcc2c8
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.test.tsx
@@ -0,0 +1,147 @@
+/*!
+ * 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 { render } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { useDagRunServiceGetDagRun } from "openapi/queries";
+import { useGridTiSummariesStream } from "src/queries/useGridTISummaries.ts";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { Graph } from "./Graph";
+
+let mockParams: Record<string, string> = { dagId: "test_dag" };
+
+vi.mock("react-router-dom", async () => {
+ const actual = await vi.importActual("react-router-dom");
+
+ return {
+ ...actual,
+ useParams: () => mockParams,
+ useSearchParams: () => [new URLSearchParams()],
+ };
+});
+
+vi.mock("openapi/queries", () => ({
+ useDagRunServiceGetDagRun: vi.fn(() => ({ data: undefined, isLoading: false
})),
+ useStructureServiceStructureData: vi.fn(() => ({ data: { edges: [], nodes:
[] } })),
+}));
+
+vi.mock("src/queries/useDependencyGraph", () => ({
+ useDependencyGraph: vi.fn(() => ({ data: { edges: [], nodes: [] } })),
+}));
+
+vi.mock("src/components/Graph/useGraphLayout", () => ({
+ useGraphLayout: vi.fn(() => ({ data: { edges: [], nodes: [] } })),
+}));
+
+vi.mock("src/queries/useGridTISummaries.ts", () => ({
+ useGridTiSummariesStream: vi.fn(() => ({ summariesByRunId: new Map() })),
+}));
+
+vi.mock("src/hooks/useSelectedVersion", () => ({
+ default: vi.fn(() => 1),
+}));
+
+vi.mock("src/context/openGroups", () => ({
+ useOpenGroups: vi.fn(() => ({ allGroupIds: [], openGroupIds: [],
setAllGroupIds: vi.fn() })),
+}));
+
+vi.mock("@xyflow/react", async () => {
+ const actual = await vi.importActual("@xyflow/react");
+
+ return {
+ ...actual,
+ ReactFlow: vi.fn(() => null),
+ useReactFlow: vi.fn(() => ({ fitView: vi.fn(), getZoom: vi.fn() })),
+ };
+});
+
+const mockDagRun = {
+ bundle_version: null,
+ conf: null,
+ dag_display_name: "test_dag",
+ dag_id: "test_dag",
+ dag_versions: [],
+ end_date: null,
+ has_missed_deadline: false,
+ logical_date: null,
+ note: null,
+ partition_key: null,
+ queued_at: null,
+ run_after: "2025-01-01T00:00:00Z",
+ run_id: "run_1",
+ run_type: "manual" as const,
+ start_date: "2025-01-01T00:00:01Z",
+ state: "running" as const,
+ triggered_by: "ui" as const,
+ triggering_user_name: null,
+};
+
+describe("Graph", () => {
+ it("passes states to useGridTiSummariesStream when a runId is present", ()
=> {
+ mockParams = { dagId: "test_dag", runId: "run_1" };
+ vi.mocked(useDagRunServiceGetDagRun).mockReturnValue({
+ data: mockDagRun,
+ isLoading: false,
+ } as ReturnType<typeof useDagRunServiceGetDagRun>);
+
+ render(<Graph />, { wrapper: Wrapper });
+
+ expect(vi.mocked(useGridTiSummariesStream)).toHaveBeenCalledWith(
+ expect.objectContaining({
+ runIds: ["run_1"],
+ states: ["running"],
+ }),
+ );
+ });
+
+ it("passes undefined states while dag run is still loading", () => {
+ mockParams = { dagId: "test_dag", runId: "run_1" };
+ vi.mocked(useDagRunServiceGetDagRun).mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ } as ReturnType<typeof useDagRunServiceGetDagRun>);
+
+ render(<Graph />, { wrapper: Wrapper });
+
+ expect(vi.mocked(useGridTiSummariesStream)).toHaveBeenCalledWith(
+ expect.objectContaining({
+ runIds: ["run_1"],
+ states: undefined,
+ }),
+ );
+ });
+
+ it("passes undefined states to useGridTiSummariesStream when there is no
runId", () => {
+ mockParams = { dagId: "test_dag" };
+ vi.mocked(useDagRunServiceGetDagRun).mockReturnValue({
+ data: undefined,
+ isLoading: false,
+ } as ReturnType<typeof useDagRunServiceGetDagRun>);
+
+ render(<Graph />, { wrapper: Wrapper });
+
+ expect(vi.mocked(useGridTiSummariesStream)).toHaveBeenCalledWith(
+ expect.objectContaining({
+ runIds: [],
+ states: undefined,
+ }),
+ );
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
index 6e3fe794021..fb16cf04c84 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx
@@ -23,7 +23,7 @@ import { useEffect } from "react";
import { useParams, useSearchParams } from "react-router-dom";
import { useLocalStorage } from "usehooks-ts";
-import { useStructureServiceStructureData } from "openapi/queries";
+import { useDagRunServiceGetDagRun, useStructureServiceStructureData } from
"openapi/queries";
import { DownloadButton } from "src/components/Graph/DownloadButton";
import { edgeTypes, nodeTypes } from "src/components/Graph/graphTypes";
import type { CustomNodeProps } from "src/components/Graph/reactflowUtils";
@@ -134,7 +134,15 @@ export const Graph = () => {
versionNumber: selectedVersion,
});
- const { summariesByRunId } = useGridTiSummariesStream({ dagId, runIds: runId
? [runId] : [] });
+ const { data: dagRun } = useDagRunServiceGetDagRun({ dagId, dagRunId: runId
}, undefined, {
+ enabled: Boolean(runId),
+ });
+
+ const { summariesByRunId } = useGridTiSummariesStream({
+ dagId,
+ runIds: runId ? [runId] : [],
+ states: dagRun ? [dagRun.state] : undefined,
+ });
const gridTISummaries = runId ? summariesByRunId.get(runId) : undefined;
// Add task instances to the node data but without having to recalculate how
the graph is laid out