From a2bcbe9430906078646ce3bc9c4b186c86652a04 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 25 Mar 2026 06:03:09 +0000 Subject: [PATCH 1/7] Add time range selector to Gantt view --- .../ui/src/layouts/Details/Gantt/Gantt.tsx | 105 ++++++++++++++---- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx index 62de32cdb98bb..1de108c04ba68 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, useToken } from "@chakra-ui/react"; +import { Box, Input, Text, useToken } from "@chakra-ui/react"; import { Chart as ChartJS, CategoryScale, @@ -33,11 +33,10 @@ import { import "chart.js/auto"; import "chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm"; import annotationPlugin from "chartjs-plugin-annotation"; -import { useDeferredValue } from "react"; +import { useDeferredValue, useState } from "react"; import { Bar } from "react-chartjs-2"; import { useTranslation } from "react-i18next"; import { useParams, useNavigate, useLocation, useSearchParams } from "react-router-dom"; - import { useGanttServiceGetGanttData } from "openapi/queries"; import type { DagRunState, DagRunType } from "openapi/requests/types.gen"; import { useColorMode } from "src/context/colorMode"; @@ -83,6 +82,8 @@ const MIN_BAR_WIDTH = 10; export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, triggeringUser }: Props) => { const { dagId = "", groupId: selectedGroupId, runId = "", taskId: selectedTaskId } = useParams(); + const [filterStartDate, setFilterStartDate] = useState(""); + const [filterEndDate, setFilterEndDate] = useState(""); const [searchParams] = useSearchParams(); const { openGroupIds } = useOpenGroups(); const deferredOpenGroupIds = useDeferredValue(openGroupIds); @@ -161,9 +162,36 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t const allTries = ganttData?.task_instances ?? []; const gridSummaries = gridTiSummaries?.task_instances ?? []; - const data = isLoading || runId === "" ? [] : transformGanttData({ allTries, flatNodes, gridSummaries }); + const rawData = + isLoading || runId === "" + ? [] + : transformGanttData({ allTries, flatNodes, gridSummaries }); + + // Filter data by date range if specified + const data = rawData.filter((task) => { + if (!filterStartDate && !filterEndDate) return true; + + // task.x contains [start_date, end_date] as ISO strings + const taskStartDate = task.x[0]; + const taskEndDate = task.x[1]; + + if (!taskStartDate) return true; - const labels = flatNodes.map((node) => node.id); + if (filterStartDate && taskEndDate && new Date(taskEndDate) < new Date(filterStartDate)) return false; + if (filterEndDate && new Date(taskStartDate) > new Date(filterEndDate + "T23:59:59")) return false; + + return true; + }); + + // Get unique task IDs from filtered data for labels + const filteredTaskIds = new Set(data.map((item) => item.taskId)); + const labels = flatNodes + .filter((node) => { + // Show all tasks if no filter, otherwise only show filtered tasks + if (!filterStartDate && !filterEndDate) return true; + return filteredTaskIds.has(node.id); + }) + .map((node) => node.id); // Get all unique states and their colors const states = [...new Set(data.map((item) => item.state ?? "none"))]; @@ -228,21 +256,56 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t }; return ( - - - + <> + + + + {translate("startDate")} + + ) => setFilterStartDate(e.target.value)} + placeholder="YYYY-MM-DD" + size="sm" + type="date" + value={filterStartDate} + /> + + + + {translate("endDate")} + + ) => setFilterEndDate(e.target.value)} + placeholder="YYYY-MM-DD" + size="sm" + type="date" + value={filterEndDate} + /> + + + + + + + ); }; From a71aa5b610d6f1f08aaffa804121859a4b5cbf6c Mon Sep 17 00:00:00 2001 From: root Date: Sat, 28 Mar 2026 21:08:32 +0000 Subject: [PATCH 2/7] Add date range filter to Gantt view with server-side filtering and validation --- .../api_fastapi/core_api/routes/ui/gantt.py | 36 ++++++++++- .../ui/src/layouts/Details/Gantt/Gantt.tsx | 64 +++++++++---------- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py index f33b12e6f7e8a..231f09615072b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py @@ -17,7 +17,10 @@ from __future__ import annotations -from fastapi import Depends, HTTPException, status +from datetime import datetime +from typing import Annotated + +from fastapi import Depends, HTTPException, Query, status from sqlalchemy import or_, select, union_all from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity @@ -59,8 +62,17 @@ def get_gantt_data( dag_id: str, run_id: str, session: SessionDep, + start_date: Annotated[datetime | None, Query()] = None, + end_date: Annotated[datetime | None, Query()] = None, ) -> GanttResponse: + """Get all task instance tries for Gantt chart.""" + + if start_date and end_date and start_date > end_date: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + "start_date cannot be greater than end_date", + ) # Exclude mapped tasks (use grid summaries) and UP_FOR_RETRY (already in history) current_tis = select( TaskInstance.task_id.label("task_id"), @@ -74,6 +86,16 @@ def get_gantt_data( TaskInstance.run_id == run_id, TaskInstance.map_index == -1, or_(TaskInstance.state != TaskInstanceState.UP_FOR_RETRY, TaskInstance.state.is_(None)), + *( + [TaskInstance.start_date <= end_date] + if end_date is not None + else [] + ), + *( + [TaskInstance.end_date >= start_date] + if start_date is not None + else [] + ), ) history_tis = select( @@ -87,6 +109,16 @@ def get_gantt_data( TaskInstanceHistory.dag_id == dag_id, TaskInstanceHistory.run_id == run_id, TaskInstanceHistory.map_index == -1, + *( + [TaskInstanceHistory.start_date <= end_date] + if end_date is not None + else [] + ), + *( + [TaskInstanceHistory.end_date >= start_date] + if start_date is not None + else [] + ), ) combined = union_all(current_tis, history_tis).subquery() @@ -112,4 +144,4 @@ def get_gantt_data( for row in results ] - return GanttResponse(dag_id=dag_id, run_id=run_id, task_instances=task_instances) + return GanttResponse(dag_id=dag_id, run_id=run_id, task_instances=task_instances) \ No newline at end of file diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx index 1de108c04ba68..b597019b8c983 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx @@ -37,6 +37,7 @@ import { useDeferredValue, useState } from "react"; import { Bar } from "react-chartjs-2"; import { useTranslation } from "react-i18next"; import { useParams, useNavigate, useLocation, useSearchParams } from "react-router-dom"; + import { useGanttServiceGetGanttData } from "openapi/queries"; import type { DagRunState, DagRunType } from "openapi/requests/types.gen"; import { useColorMode } from "src/context/colorMode"; @@ -84,6 +85,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t const { dagId = "", groupId: selectedGroupId, runId = "", taskId: selectedTaskId } = useParams(); const [filterStartDate, setFilterStartDate] = useState(""); const [filterEndDate, setFilterEndDate] = useState(""); + const [searchParams] = useSearchParams(); const { openGroupIds } = useOpenGroups(); const deferredOpenGroupIds = useDeferredValue(openGroupIds); @@ -145,8 +147,15 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t const summariesLoading = Boolean(runId && selectedRun && !summariesByRunId.has(runId)); // Single fetch for all Gantt data (individual task tries) + // startDate and endDate are sent to the backend as query parameters. + // The server filters the data — NOT the browser. const { data: ganttData, isLoading: ganttLoading } = useGanttServiceGetGanttData( - { dagId, runId }, + { + dagId, + runId, + startDate: filterStartDate ? `${filterStartDate}T00:00:00Z` : undefined, + endDate: filterEndDate ? `${filterEndDate}T23:59:59Z` : undefined, + }, undefined, { enabled: Boolean(dagId) && Boolean(runId) && Boolean(selectedRun), @@ -162,36 +171,9 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t const allTries = ganttData?.task_instances ?? []; const gridSummaries = gridTiSummaries?.task_instances ?? []; - const rawData = - isLoading || runId === "" - ? [] - : transformGanttData({ allTries, flatNodes, gridSummaries }); - - // Filter data by date range if specified - const data = rawData.filter((task) => { - if (!filterStartDate && !filterEndDate) return true; - - // task.x contains [start_date, end_date] as ISO strings - const taskStartDate = task.x[0]; - const taskEndDate = task.x[1]; + const data = isLoading || runId === "" ? [] : transformGanttData({ allTries, flatNodes, gridSummaries }); - if (!taskStartDate) return true; - - if (filterStartDate && taskEndDate && new Date(taskEndDate) < new Date(filterStartDate)) return false; - if (filterEndDate && new Date(taskStartDate) > new Date(filterEndDate + "T23:59:59")) return false; - - return true; - }); - - // Get unique task IDs from filtered data for labels - const filteredTaskIds = new Set(data.map((item) => item.taskId)); - const labels = flatNodes - .filter((node) => { - // Show all tasks if no filter, otherwise only show filtered tasks - if (!filterStartDate && !filterEndDate) return true; - return filteredTaskIds.has(node.id); - }) - .map((node) => node.id); + const labels = flatNodes.map((node) => node.id); // Get all unique states and their colors const states = [...new Set(data.map((item) => item.state ?? "none"))]; @@ -257,6 +239,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t return ( <> + {/* Date inputs that trigger a new API fetch when changed */} @@ -266,7 +249,14 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t fontSize="sm" fontWeight="medium" maxW="200px" - onChange={(e: React.ChangeEvent) => setFilterStartDate(e.target.value)} + onChange={(e) => { + const value = e.target.value; + if (filterEndDate && value > filterEndDate) { + alert("Start date cannot be after end date"); + return; + } + setFilterStartDate(value); + }} placeholder="YYYY-MM-DD" size="sm" type="date" @@ -281,7 +271,14 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t fontSize="sm" fontWeight="medium" maxW="200px" - onChange={(e: React.ChangeEvent) => setFilterEndDate(e.target.value)} + onChange={(e) => { + const value = e.target.value; + if (filterStartDate && value < filterStartDate) { + alert("End date cannot be before start date"); + return; + } + setFilterEndDate(value); + }} placeholder="YYYY-MM-DD" size="sm" type="date" @@ -289,7 +286,6 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t /> - ); -}; +}; \ No newline at end of file From 28846e9c94b35d5a9f690541bafb3b421fc15486 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 29 Mar 2026 10:39:16 +0000 Subject: [PATCH 3/7] Add newsfragment for Gantt date filter --- newsfragments/64387.significant.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/64387.significant.rst diff --git a/newsfragments/64387.significant.rst b/newsfragments/64387.significant.rst new file mode 100644 index 0000000000000..a2a2b2d4acf8c --- /dev/null +++ b/newsfragments/64387.significant.rst @@ -0,0 +1 @@ +Add start date and end date filter to the Gantt view with server-side filtering. From e46373c6e25bd643813efab864329f66c55e1dac Mon Sep 17 00:00:00 2001 From: root Date: Sun, 29 Mar 2026 10:56:31 +0000 Subject: [PATCH 4/7] Final fixes --- .../api_fastapi/core_api/routes/ui/gantt.py | 14 ++--------- .../core_api/routes/ui/test_gantt.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py index 231f09615072b..cbadd8bb3e4f3 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py @@ -87,12 +87,7 @@ def get_gantt_data( TaskInstance.map_index == -1, or_(TaskInstance.state != TaskInstanceState.UP_FOR_RETRY, TaskInstance.state.is_(None)), *( - [TaskInstance.start_date <= end_date] - if end_date is not None - else [] - ), - *( - [TaskInstance.end_date >= start_date] + [or_(TaskInstance.end_date >= start_date, TaskInstance.end_date.is_(None))] if start_date is not None else [] ), @@ -110,12 +105,7 @@ def get_gantt_data( TaskInstanceHistory.run_id == run_id, TaskInstanceHistory.map_index == -1, *( - [TaskInstanceHistory.start_date <= end_date] - if end_date is not None - else [] - ), - *( - [TaskInstanceHistory.end_date >= start_date] + [or_(TaskInstanceHistory.end_date >= start_date, TaskInstanceHistory.end_date.is_(None))] if start_date is not None else [] ), diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py index 162c82682afcf..0e942efacf127 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py @@ -326,3 +326,27 @@ def test_should_response_404(self, test_client, dag_id, run_id): with assert_queries_count(3): response = test_client.get(f"/gantt/{dag_id}/{run_id}") assert response.status_code == 404 + + def test_gantt_with_start_date(self, test_client): + response = test_client.get( + f"/gantt/{DAG_ID}/run_1", + params={"start_date": "2024-11-30T10:06:00"}, + ) + assert response.status_code == 200 + + data = response.json() + + # running task (NULL end_date) should STILL be included + task_ids = [ti["task_id"] for ti in data["task_instances"]] + assert "task3" in task_ids + + + def test_gantt_invalid_date_range(self, test_client): + response = test_client.get( + f"/gantt/{DAG_ID}/run_1", + params={ + "start_date": "2024-12-01T00:00:00", + "end_date": "2024-11-01T00:00:00", + }, + ) + assert response.status_code == 400 \ No newline at end of file From 58250f436bb49d3460c056f7a05913b4711d527b Mon Sep 17 00:00:00 2001 From: root Date: Sun, 29 Mar 2026 11:07:15 +0000 Subject: [PATCH 5/7] Final fixes --- .../airflow/api_fastapi/core_api/routes/ui/gantt.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py index cbadd8bb3e4f3..2af01b44b3f6b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py @@ -91,6 +91,11 @@ def get_gantt_data( if start_date is not None else [] ), + *( + [TaskInstance.start_date <= end_date] + if end_date is not None + else [] + ), ) history_tis = select( @@ -109,6 +114,11 @@ def get_gantt_data( if start_date is not None else [] ), + *( + [TaskInstanceHistory.start_date <= end_date] + if end_date is not None + else [] + ), ) combined = union_all(current_tis, history_tis).subquery() From c57cbca3e7fc7e607b680e647360b4ccc61e6998 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 6 Apr 2026 18:49:03 +0000 Subject: [PATCH 6/7] fix: address review comments - use Field.Root for Chakra v3, fix query count, improve tests --- .../api_fastapi/core_api/routes/ui/gantt.py | 27 +++++----- .../ui/public/i18n/locales/en/common.json | 2 + .../ui/src/layouts/Details/Gantt/Gantt.tsx | 40 ++++++++------- .../core_api/routes/ui/test_gantt.py | 50 ++++++++++++++++--- newsfragments/64387.significant.rst | 1 - 5 files changed, 81 insertions(+), 39 deletions(-) delete mode 100644 newsfragments/64387.significant.rst diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py index 2af01b44b3f6b..088e99c27fca8 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py @@ -62,17 +62,16 @@ def get_gantt_data( dag_id: str, run_id: str, session: SessionDep, - start_date: Annotated[datetime | None, Query()] = None, - end_date: Annotated[datetime | None, Query()] = None, + start_date_range: Annotated[datetime | None, Query(alias="start_date")] = None, + end_date_range: Annotated[datetime | None, Query(alias="end_date")] = None, ) -> GanttResponse: - """Get all task instance tries for Gantt chart.""" - - if start_date and end_date and start_date > end_date: + if start_date_range and end_date_range and start_date_range > end_date_range: raise HTTPException( status.HTTP_400_BAD_REQUEST, "start_date cannot be greater than end_date", ) + # Exclude mapped tasks (use grid summaries) and UP_FOR_RETRY (already in history) current_tis = select( TaskInstance.task_id.label("task_id"), @@ -87,13 +86,13 @@ def get_gantt_data( TaskInstance.map_index == -1, or_(TaskInstance.state != TaskInstanceState.UP_FOR_RETRY, TaskInstance.state.is_(None)), *( - [or_(TaskInstance.end_date >= start_date, TaskInstance.end_date.is_(None))] - if start_date is not None + [or_(TaskInstance.end_date >= start_date_range, TaskInstance.end_date.is_(None))] + if start_date_range is not None else [] ), *( - [TaskInstance.start_date <= end_date] - if end_date is not None + [TaskInstance.start_date <= end_date_range] + if end_date_range is not None else [] ), ) @@ -110,13 +109,13 @@ def get_gantt_data( TaskInstanceHistory.run_id == run_id, TaskInstanceHistory.map_index == -1, *( - [or_(TaskInstanceHistory.end_date >= start_date, TaskInstanceHistory.end_date.is_(None))] - if start_date is not None + [or_(TaskInstanceHistory.end_date >= start_date_range, TaskInstanceHistory.end_date.is_(None))] + if start_date_range is not None else [] ), *( - [TaskInstanceHistory.start_date <= end_date] - if end_date is not None + [TaskInstanceHistory.start_date <= end_date_range] + if end_date_range is not None else [] ), ) @@ -129,7 +128,7 @@ def get_gantt_data( if not results: raise HTTPException( status.HTTP_404_NOT_FOUND, - f"No task instances for dag_id={dag_id} run_id={run_id}", + detail="DAG or DagRun not found", ) task_instances = [ diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index e44f9cc3856e1..624d0e70c900a 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -96,6 +96,7 @@ "duration": "Duration", "edit": "Edit", "endDate": "End Date", + "endDateBeforeStartDate": "End date cannot be before start date", "error": { "back": "Back", "defaultMessage": "An unexpected error occurred", @@ -212,6 +213,7 @@ "sourceAssetEvent_one": "Source Asset Event", "sourceAssetEvent_other": "Source Asset Events", "startDate": "Start Date", + "startDateAfterEndDate": "Start date cannot be after end date", "state": "State", "states": { "deferred": "Deferred", diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx index b597019b8c983..492bd274b44fb 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Input, Text, useToken } from "@chakra-ui/react"; +import { Box, Field, Input, useToken } from "@chakra-ui/react"; import { Chart as ChartJS, CategoryScale, @@ -85,6 +85,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t const { dagId = "", groupId: selectedGroupId, runId = "", taskId: selectedTaskId } = useParams(); const [filterStartDate, setFilterStartDate] = useState(""); const [filterEndDate, setFilterEndDate] = useState(""); + const [dateError, setDateError] = useState(""); const [searchParams] = useSearchParams(); const { openGroupIds } = useOpenGroups(); @@ -146,7 +147,6 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t const gridTiSummaries = summariesByRunId.get(runId); const summariesLoading = Boolean(runId && selectedRun && !summariesByRunId.has(runId)); - // Single fetch for all Gantt data (individual task tries) // startDate and endDate are sent to the backend as query parameters. // The server filters the data — NOT the browser. const { data: ganttData, isLoading: ganttLoading } = useGanttServiceGetGanttData( @@ -239,22 +239,23 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t return ( <> - {/* Date inputs that trigger a new API fetch when changed */} - - - + {/* Date range inputs — values are sent to backend as query params, no client-side filtering */} + + + {translate("startDate")} - + { const value = e.target.value; - if (filterEndDate && value > filterEndDate) { - alert("Start date cannot be after end date"); + + if (value && filterEndDate && value > filterEndDate) { + setDateError(translate("startDateAfterEndDate")); return; } + setDateError(""); setFilterStartDate(value); }} placeholder="YYYY-MM-DD" @@ -262,21 +263,23 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t type="date" value={filterStartDate} /> - - - + + + + {translate("endDate")} - + { const value = e.target.value; - if (filterStartDate && value < filterStartDate) { - alert("End date cannot be before start date"); + + if (value && filterStartDate && value < filterStartDate) { + setDateError(translate("endDateBeforeStartDate")); return; } + setDateError(""); setFilterEndDate(value); }} placeholder="YYYY-MM-DD" @@ -284,7 +287,8 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t type="date" value={filterEndDate} /> - + {dateError ? {dateError} : undefined} + = 10:06 (or NULL end_date for running tasks) + # task ends at 10:05 → excluded + # task2 ends at 10:10 → included + # task3 has NULL end_date (running) → always included response = test_client.get( f"/gantt/{DAG_ID}/run_1", - params={"start_date": "2024-11-30T10:06:00"}, + params={"start_date": "2024-11-30T10:06:00Z"}, ) assert response.status_code == 200 data = response.json() - - # running task (NULL end_date) should STILL be included task_ids = [ti["task_id"] for ti in data["task_instances"]] + + # running task (NULL end_date) must always be included assert "task3" in task_ids + # task2 ended after the filter start → included + assert "task2" in task_ids + # task ended before the filter start → excluded + assert "task" not in task_ids + # filtered result must have fewer tasks than unfiltered + assert len(task_ids) < len(unfiltered_ids) + + def test_gantt_with_end_date(self, test_client): + """Filtering by end_date excludes tasks that started after the filter.""" + # Filter: only tasks with start_date <= 10:04 + # task starts at 10:00 → included + # task2 starts at 10:05 → excluded + # task3 starts at 10:10 → excluded + response = test_client.get( + f"/gantt/{DAG_ID}/run_1", + params={"end_date": "2024-11-30T10:04:00Z"}, + ) + assert response.status_code == 200 + + data = response.json() + task_ids = [ti["task_id"] for ti in data["task_instances"]] + # task started before end_date → included + assert "task" in task_ids + # task3 started after end_date → excluded + assert "task3" not in task_ids + # task2 started after end_date → excluded + assert "task2" not in task_ids - def test_gantt_invalid_date_range(self, test_client): + def test_invalid_date_range_returns_400(self, test_client): + """start_date after end_date must return 400.""" response = test_client.get( f"/gantt/{DAG_ID}/run_1", params={ - "start_date": "2024-12-01T00:00:00", - "end_date": "2024-11-01T00:00:00", + "start_date": "2024-12-01T00:00:00Z", + "end_date": "2024-11-01T00:00:00Z", }, ) assert response.status_code == 400 \ No newline at end of file diff --git a/newsfragments/64387.significant.rst b/newsfragments/64387.significant.rst deleted file mode 100644 index a2a2b2d4acf8c..0000000000000 --- a/newsfragments/64387.significant.rst +++ /dev/null @@ -1 +0,0 @@ -Add start date and end date filter to the Gantt view with server-side filtering. From df15b673928cec74f05dae33baee6342298c6c7d Mon Sep 17 00:00:00 2001 From: Smitaambiger Date: Wed, 8 Apr 2026 18:32:57 +0000 Subject: [PATCH 7/7] fix: restore start_date filter and handle NULL start_date for end_date filter --- airflow-core/package-lock.json | 6 ++++++ .../airflow/api_fastapi/core_api/routes/ui/gantt.py | 11 ++++++++--- .../airflow/ui/src/layouts/Details/Gantt/Gantt.tsx | 10 +++------- .../unit/api_fastapi/core_api/routes/ui/test_gantt.py | 2 +- scripts/ci/prek/check_excluded_provider_markers.py | 0 .../ci/prek/check_metrics_synced_with_the_registry.py | 0 scripts/ci/prek/check_registry_types_json_sync.py | 0 7 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 airflow-core/package-lock.json mode change 100644 => 100755 scripts/ci/prek/check_excluded_provider_markers.py mode change 100644 => 100755 scripts/ci/prek/check_metrics_synced_with_the_registry.py mode change 100644 => 100755 scripts/ci/prek/check_registry_types_json_sync.py diff --git a/airflow-core/package-lock.json b/airflow-core/package-lock.json new file mode 100644 index 0000000000000..47020b70c4680 --- /dev/null +++ b/airflow-core/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "airflow-core", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py index 088e99c27fca8..2207370e0965a 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/gantt.py @@ -91,7 +91,7 @@ def get_gantt_data( else [] ), *( - [TaskInstance.start_date <= end_date_range] + [or_(TaskInstance.start_date <= end_date_range, TaskInstance.start_date.is_(None))] if end_date_range is not None else [] ), @@ -114,7 +114,12 @@ def get_gantt_data( else [] ), *( - [TaskInstanceHistory.start_date <= end_date_range] + [ + or_( + TaskInstanceHistory.start_date <= end_date_range, + TaskInstanceHistory.start_date.is_(None), + ) + ] if end_date_range is not None else [] ), @@ -143,4 +148,4 @@ def get_gantt_data( for row in results ] - return GanttResponse(dag_id=dag_id, run_id=run_id, task_instances=task_instances) \ No newline at end of file + return GanttResponse(dag_id=dag_id, run_id=run_id, task_instances=task_instances) diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx index 492bd274b44fb..f994020c5d92a 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx @@ -71,8 +71,6 @@ ChartJS.register( type Props = { readonly dagRunState?: DagRunState | undefined; readonly limit: number; - readonly runAfterGte?: string | undefined; - readonly runAfterLte?: string | undefined; readonly runType?: DagRunType | undefined; readonly triggeringUser?: string | undefined; }; @@ -81,7 +79,7 @@ const CHART_PADDING = 36; const CHART_ROW_HEIGHT = 20; const MIN_BAR_WIDTH = 10; -export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, triggeringUser }: Props) => { +export const Gantt = ({ dagRunState, limit, runType, triggeringUser }: Props) => { const { dagId = "", groupId: selectedGroupId, runId = "", taskId: selectedTaskId } = useParams(); const [filterStartDate, setFilterStartDate] = useState(""); const [filterEndDate, setFilterEndDate] = useState(""); @@ -120,8 +118,6 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t const { data: gridRuns, isLoading: runsLoading } = useGridRuns({ dagRunState, limit, - runAfterGte, - runAfterLte, runType, triggeringUser, }); @@ -222,7 +218,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t translate, }); - if (runId === "" || (!isLoading && !selectedRun)) { + if (runId === "") { return undefined; } @@ -308,4 +304,4 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t ); -}; \ No newline at end of file +}; diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py index 79659bd652302..ade423c7fb73b 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_gantt.py @@ -387,4 +387,4 @@ def test_invalid_date_range_returns_400(self, test_client): "end_date": "2024-11-01T00:00:00Z", }, ) - assert response.status_code == 400 \ No newline at end of file + assert response.status_code == 400 diff --git a/scripts/ci/prek/check_excluded_provider_markers.py b/scripts/ci/prek/check_excluded_provider_markers.py old mode 100644 new mode 100755 diff --git a/scripts/ci/prek/check_metrics_synced_with_the_registry.py b/scripts/ci/prek/check_metrics_synced_with_the_registry.py old mode 100644 new mode 100755 diff --git a/scripts/ci/prek/check_registry_types_json_sync.py b/scripts/ci/prek/check_registry_types_json_sync.py old mode 100644 new mode 100755