From 53e7229fa42bfd4c34ecad6d432fecfcd351296d Mon Sep 17 00:00:00 2001 From: Ward Schodts <4020491+warreee@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:33:59 +0200 Subject: [PATCH] Respect the limit search param in the task overview duration chart The duration chart on the task overview always requested the last 14 task instances, ignoring the number of dag runs the user picked. The dag details layout already writes that choice to the limit search param, and the dag overview page reads a value of its own, so the task page was the only view that could not be widened. Read the same limit search param the details layout writes, falling back to the same default of 10, so the chart follows the selector and can be shared through the URL. The remaining limit on the failed task instance query is left alone: that query only reads total_entries and never renders the rows it fetches. --- .../src/pages/Task/Overview/Overview.test.tsx | 54 ++++++++++++++++--- .../ui/src/pages/Task/Overview/Overview.tsx | 7 ++- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.test.tsx b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.test.tsx index 90c39ec41eb0d..add9a7796a2fc 100644 --- a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.test.tsx @@ -18,13 +18,32 @@ */ import "@testing-library/jest-dom"; import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import type { PropsWithChildren } from "react"; +import { MemoryRouter } from "react-router-dom"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ReactAppResponse } from "openapi/requests/types.gen"; -import { Wrapper } from "src/utils/Wrapper"; +import { BaseWrapper, Wrapper } from "src/utils/Wrapper"; import { Overview } from "./Overview"; +const { mockUseTaskInstanceServiceGetTaskInstances } = vi.hoisted(() => ({ + mockUseTaskInstanceServiceGetTaskInstances: vi.fn(() => ({ + data: { task_instances: [], total_entries: 0 }, + isLoading: false, + })), +})); + +const wrapperWithSearch = (search: string) => { + const RouterWrapper = ({ children }: PropsWithChildren) => ( + + {children} + + ); + + return RouterWrapper; +}; + vi.mock("openapi/queries", () => ({ usePluginServiceGetPlugins: () => ({ data: { @@ -44,10 +63,7 @@ vi.mock("openapi/queries", () => ({ ], }, }), - useTaskInstanceServiceGetTaskInstances: () => ({ - data: { task_instances: [], total_entries: 0 }, - isLoading: false, - }), + useTaskInstanceServiceGetTaskInstances: mockUseTaskInstanceServiceGetTaskInstances, })); vi.mock("src/components/DurationChart", () => ({ DurationChart: () => null })); @@ -79,3 +95,29 @@ describe("Task overview plugins", () => { expect(screen.queryByText("Scoped overview plugin")).not.toBeInTheDocument(); }); }); + +describe("Task overview duration chart limit", () => { + beforeEach(() => { + mockUseTaskInstanceServiceGetTaskInstances.mockClear(); + }); + + it("requests the default number of task instances when no limit is set", () => { + render(, { wrapper: wrapperWithSearch("") }); + + expect(mockUseTaskInstanceServiceGetTaskInstances).toHaveBeenCalledWith( + expect.objectContaining({ limit: 10, orderBy: ["-run_after"] }), + undefined, + expect.anything(), + ); + }); + + it("requests the number of task instances given by the limit search param", () => { + render(, { wrapper: wrapperWithSearch("?limit=50") }); + + expect(mockUseTaskInstanceServiceGetTaskInstances).toHaveBeenCalledWith( + expect.objectContaining({ limit: 50, orderBy: ["-run_after"] }), + undefined, + expect.anything(), + ); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx index 470877d1742fc..377a674f36869 100644 --- a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx @@ -20,7 +20,7 @@ import { Box, HStack, Skeleton, VStack } from "@chakra-ui/react"; import dayjs from "dayjs"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; +import { useParams, useSearchParams } from "react-router-dom"; import { usePluginServiceGetPlugins, useTaskInstanceServiceGetTaskInstances } from "openapi/queries"; import { DurationChart } from "src/components/DurationChart"; @@ -38,6 +38,9 @@ export const Overview = () => { const { dagId = "", groupId, taskId } = useParams(); const { t: translate } = useTranslation("dag"); + const [searchParams] = useSearchParams(); + const limit = Number(searchParams.get(SearchParamsKeys.LIMIT) ?? "10"); + const now = dayjs(); const [startDate, setStartDate] = useState(now.subtract(Number(defaultHour), "hour").toISOString()); const [endDate, setEndDate] = useState(now.toISOString()); @@ -62,7 +65,7 @@ export const Overview = () => { { dagId, dagRunId: "~", - limit: 14, + limit, orderBy: ["-run_after"], taskGroupId: groupId ?? undefined, taskId: Boolean(groupId) ? undefined : taskId,