Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions airflow-core/package-lock.json
Comment thread
Smitaambiger marked this conversation as resolved.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -59,8 +62,16 @@ def get_gantt_data(
dag_id: str,
run_id: str,
session: SessionDep,
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_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",
)
Comment on lines 68 to +73

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint now raises HTTP 400 for invalid start_date/end_date ranges, but the route decorator’s responses=create_openapi_http_exception_doc(...) still only documents 404. Please update the documented responses accordingly so the OpenAPI spec/UI client generation includes the 400 case.

Copilot uses AI. Check for mistakes.

# Exclude mapped tasks (use grid summaries) and UP_FOR_RETRY (already in history)
current_tis = select(
TaskInstance.task_id.label("task_id"),
Expand All @@ -74,6 +85,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)),
*(
[or_(TaskInstance.end_date >= start_date_range, TaskInstance.end_date.is_(None))]
if start_date_range is not None
else []
),
*(
[or_(TaskInstance.start_date <= end_date_range, TaskInstance.start_date.is_(None))]
if end_date_range is not None
else []
),
)

history_tis = select(
Expand All @@ -87,6 +108,21 @@ def get_gantt_data(
TaskInstanceHistory.dag_id == dag_id,
TaskInstanceHistory.run_id == run_id,
TaskInstanceHistory.map_index == -1,
*(
[or_(TaskInstanceHistory.end_date >= start_date_range, TaskInstanceHistory.end_date.is_(None))]
if start_date_range is not None
else []
),
*(
[
or_(
TaskInstanceHistory.start_date <= end_date_range,
TaskInstanceHistory.start_date.is_(None),
)
]
if end_date_range is not None
else []
),
)

combined = union_all(current_tis, history_tis).subquery()
Expand All @@ -97,7 +133,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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
111 changes: 85 additions & 26 deletions airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Box, useToken } from "@chakra-ui/react";
import { Box, Field, Input, useToken } from "@chakra-ui/react";
import {
Chart as ChartJS,
CategoryScale,
Expand All @@ -33,7 +33,7 @@ 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";
Expand Down Expand Up @@ -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;
};
Expand All @@ -81,8 +79,12 @@ 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();
Comment on lines 71 to 83

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gantt Props no longer include runAfterGte/runAfterLte, but callers still pass them (e.g. DetailsLayout.tsx around lines 201-208). This will fail TypeScript compilation unless the call sites are updated or the props are kept.

Copilot uses AI. Check for mistakes.
const [filterStartDate, setFilterStartDate] = useState("");
const [filterEndDate, setFilterEndDate] = useState("");
const [dateError, setDateError] = useState("");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need state management for the error. It can just be a variable:

const isRangeValid = [startDate is before endDate]


const [searchParams] = useSearchParams();
const { openGroupIds } = useOpenGroups();
const deferredOpenGroupIds = useDeferredValue(openGroupIds);
Expand Down Expand Up @@ -116,8 +118,6 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
const { data: gridRuns, isLoading: runsLoading } = useGridRuns({
dagRunState,
limit,
runAfterGte,
runAfterLte,
runType,
triggeringUser,
});
Expand All @@ -143,9 +143,15 @@ 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(
{ dagId, runId },
{
dagId,
runId,
startDate: filterStartDate ? `${filterStartDate}T00:00:00Z` : undefined,
endDate: filterEndDate ? `${filterEndDate}T23:59:59Z` : undefined,
Comment on lines +152 to +153

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dates should include the full time. Most dags run in less than a day, so if the most precise you can be is day, then this filter is entirely useless.

},
Comment on lines +146 to +154

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useGanttServiceGetGanttData is being called with startDate/endDate, but the generated OpenAPI client currently only accepts { dagId, runId } (see airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts and .../requests/services.gen.ts). Either regenerate the OpenAPI UI client after adding these query params to the API spec, or avoid passing unsupported params.

Copilot uses AI. Check for mistakes.
undefined,
{
enabled: Boolean(dagId) && Boolean(runId) && Boolean(selectedRun),
Expand Down Expand Up @@ -212,7 +218,7 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
translate,
});

if (runId === "" || (!isLoading && !selectedRun)) {
if (runId === "") {
return undefined;
}

Expand All @@ -228,21 +234,74 @@ export const Gantt = ({ dagRunState, limit, runAfterGte, runAfterLte, runType, t
};

return (
<Box
height={`${fixedHeight}px`}
minW="250px"
ml={-2}
mt={`${GRID_BODY_OFFSET_PX}px`}
onMouseLeave={handleChartMouseLeave}
w="100%"
>
<Bar
data={chartData}
options={chartOptions}
style={{
paddingTop: flatNodes.length === 1 ? 15 : 1.5,
}}
/>
</Box>
<>
{/* Date range inputs — values are sent to backend as query params, no client-side filtering */}
<Box alignItems="flex-start" display="flex" gap="4" mb="4">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this a box in the gantt chart messes up all of the positioning of the element.

<Field.Root invalid={Boolean(dateError)} maxW="200px">
<Field.Label color="fg.muted" fontSize="xs">
{translate("startDate")}
</Field.Label>
<Input
fontSize="sm"
fontWeight="medium"
onChange={(e) => {
const value = e.target.value;

if (value && filterEndDate && value > filterEndDate) {
setDateError(translate("startDateAfterEndDate"));
return;
}
setDateError("");
setFilterStartDate(value);
}}
placeholder="YYYY-MM-DD"
size="sm"
type="date"
value={filterStartDate}
/>
</Field.Root>

<Field.Root invalid={Boolean(dateError)} maxW="200px">
<Field.Label color="fg.muted" fontSize="xs">
{translate("endDate")}
</Field.Label>
<Input
fontSize="sm"
fontWeight="medium"
onChange={(e) => {
const value = e.target.value;

if (value && filterStartDate && value < filterStartDate) {
setDateError(translate("endDateBeforeStartDate"));
return;
}
setDateError("");
setFilterEndDate(value);
}}
Comment thread
Smitaambiger marked this conversation as resolved.
placeholder="YYYY-MM-DD"
size="sm"
type="date"
value={filterEndDate}
/>
{dateError ? <Field.ErrorText fontSize="xs">{dateError}</Field.ErrorText> : undefined}
</Field.Root>
Comment on lines +240 to +287

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a date range filter component. Let's use it.

</Box>
<Box
height={`${fixedHeight}px`}
minW="250px"
ml={-2}
mt={`${GRID_BODY_OFFSET_PX}px`}
onMouseLeave={handleChartMouseLeave}
w="100%"
>
<Bar
data={chartData}
options={chartOptions}
style={{
paddingTop: flatNodes.length === 1 ? 15 : 1.5,
}}
/>
</Box>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,65 @@ 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):
"""Filtering by start_date excludes tasks that ended before the filter."""
# Get all tasks without filter
unfiltered = test_client.get(f"/gantt/{DAG_ID}/run_1")
assert unfiltered.status_code == 200
unfiltered_ids = [ti["task_id"] for ti in unfiltered.json()["task_instances"]]

# Filter: only tasks with end_date >= 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:00Z"},
)
assert response.status_code == 200

data = response.json()
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
Comment thread
Smitaambiger marked this conversation as resolved.
# 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_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:00Z",
"end_date": "2024-11-01T00:00:00Z",
},
)
assert response.status_code == 400
Empty file modified scripts/ci/prek/check_excluded_provider_markers.py
100644 → 100755
Comment thread
Smitaambiger marked this conversation as resolved.
Empty file.
Empty file modified scripts/ci/prek/check_metrics_synced_with_the_registry.py
100644 → 100755
Empty file.
Empty file modified scripts/ci/prek/check_registry_types_json_sync.py
100644 → 100755
Empty file.
Loading