diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.tsx index 43c28dba56b52..940a80854e29b 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/GanttTimeline.tsx @@ -85,7 +85,7 @@ const toTooltipSummary = ( return { child_states: null, - max_end_date: dayjs(segment.x[1]).toISOString(), + max_end_date: segment.end_when ?? dayjs(segment.x[1]).toISOString(), min_start_date: segment.start_when ?? dayjs(segment.x[0]).toISOString(), state: segment.state ?? null, task_display_name: segment.y, diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.test.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.test.ts index d725dfc8be60d..c2a99132587ad 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.test.ts +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.test.ts @@ -237,6 +237,65 @@ describe("transformGanttData", () => { expect(result[2]?.state).toBe("success"); }); + it("carries the task's actual start and end on every segment of the try", () => { + const result = transformGanttData({ + allTries: [ + { + end_date: "2024-03-14T10:05:00+00:00", + is_mapped: false, + queued_dttm: "2024-03-14T09:59:00+00:00", + scheduled_dttm: "2024-03-14T09:58:00+00:00", + start_date: "2024-03-14T10:00:00+00:00", + state: "success", + task_display_name: "task_1", + task_id: "task_1", + try_number: 1, + }, + ], + flatNodes: [{ depth: 0, id: "task_1", is_mapped: false, label: "task_1" }], + gridSummaries: [], + }); + + expect(result).toHaveLength(3); + // The scheduled, queued, and execution bars all report the task's real start_date/end_date so + // the tooltip is consistent no matter which segment is hovered (regression from #68174). + const expectedEnd = new Date("2024-03-14T10:05:00+00:00").toISOString(); + + for (const segment of result) { + expect(segment.start_when).toBe("2024-03-14T10:00:00+00:00"); + expect(segment.end_when).toBe(expectedEnd); + } + }); + + it("uses the current time as end_when on every segment while the task is still running", () => { + const result = transformGanttData({ + allTries: [ + { + end_date: null, + is_mapped: false, + queued_dttm: "2024-03-14T09:59:00+00:00", + scheduled_dttm: null, + start_date: "2024-03-14T10:00:00+00:00", + state: "running", + task_display_name: "task_1", + task_id: "task_1", + try_number: 1, + }, + ], + flatNodes: [{ depth: 0, id: "task_1", is_mapped: false, label: "task_1" }], + gridSummaries: [], + }); + + // Queued + execution bars, both reporting the same (running) end so the tooltip is consistent. + expect(result.length).toBeGreaterThan(0); + const [firstEnd] = result.map((segment) => segment.end_when); + + for (const segment of result) { + expect(segment.end_when).toBe(firstEnd); + expect(segment.end_when).not.toBeUndefined(); + } + }); + it("produces 2 segments when only queued_dttm is present", () => { const result = transformGanttData({ allTries: [ diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts index 9502cd31e7843..70885649da06f 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts @@ -28,6 +28,8 @@ import { renderDuration } from "src/utils/datetimeUtils"; import { buildTaskInstanceUrl } from "src/utils/links"; export type GanttDataItem = { + /** Effective task end (end_date, or "now" while running) — consistent across all segments of the same try. */ + end_when?: string | null; isGroup?: boolean | null; isMapped?: boolean | null; /** Source try times for tooltips (matches TaskInstance `*_when` fields). */ @@ -135,13 +137,6 @@ export const transformGanttData = ({ const queuedMs = queuedDttm === null ? undefined : dayjs(queuedDttm).valueOf(); const scheduledMs = scheduledDttm === null ? undefined : dayjs(scheduledDttm).valueOf(); - // Include scheduled/queued/start times in tooltip data whenever the timestamps exist. - const tryWhenForTooltip = { - ...(scheduledMs === undefined ? {} : { scheduled_when: scheduledDttm }), - ...(queuedMs === undefined ? {} : { queued_when: queuedDttm }), - ...(startDate === null ? {} : { start_when: startDate }), - }; - let endMs: number; if (hasTaskRunning) { @@ -152,6 +147,17 @@ export const transformGanttData = ({ endMs = dayjs(endDate).valueOf(); } + // Include scheduled/queued/start/end times in tooltip data whenever the timestamps exist. + // start_when/end_when are carried on every segment of a try so the tooltip reports the + // task's actual start and end on the scheduled and queued bars too, not just the + // execution bar's own bounds. + const tryWhenForTooltip = { + ...(scheduledMs === undefined ? {} : { scheduled_when: scheduledDttm }), + ...(queuedMs === undefined ? {} : { queued_when: queuedDttm }), + ...(startDate === null ? {} : { start_when: startDate }), + ...(startDate === null && !hasTaskRunning ? {} : { end_when: dayjs(endMs).toISOString() }), + }; + if (scheduledMs !== undefined) { const scheduledEndMs = queuedMs ?? startMs ?? (hasTaskRunning || tryRow.state === "scheduled" ? Date.now() : endMs);