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..209a93baed90f 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 @@ -18,7 +18,6 @@ */ import { Badge, Box, Flex, Text } from "@chakra-ui/react"; import { useVirtualizer } from "@tanstack/react-virtual"; -import dayjs from "dayjs"; import type { RefObject } from "react"; import { Fragment, useLayoutEffect, useRef, useState } from "react"; import { Link, useLocation, useParams } from "react-router-dom"; @@ -42,6 +41,7 @@ import { buildMaxTryByTaskId, getGanttSegmentTo, gridSummariesToTaskIdMap, + toTooltipSummary, } from "./utils"; /** Size of the state icon rendered inside each Gantt bar (px). The minimum bar width is derived @@ -74,32 +74,6 @@ type Props = { readonly virtualizerScrollPaddingStart: number; }; -const toTooltipSummary = ( - segment: GanttDataItem, - node: GridTask, - gridSummary: LightGridTaskInstanceSummary | undefined, -) => { - if (gridSummary !== undefined && (node.isGroup ?? node.is_mapped)) { - return gridSummary; - } - - return { - child_states: null, - max_end_date: 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, - task_id: segment.taskId, - try_number: segment.tryNumber, - ...(segment.tryNumber === undefined - ? {} - : { - queued_when: segment.queued_when, - scheduled_when: segment.scheduled_when, - }), - }; -}; - export const GanttTimeline = ({ dagId, flatNodes, 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..0471de884ac4f 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 @@ -29,6 +29,7 @@ import { buildMaxTryByTaskId, GANTT_TIME_AXIS_TICK_COUNT, gridSummariesToTaskIdMap, + toTooltipSummary, transformGanttData, } from "./utils"; @@ -213,10 +214,11 @@ describe("transformGanttData", () => { }); it("produces 3 segments when scheduled_dttm and queued_dttm are present", () => { + const taskEndDate = "2024-03-14T10:05:00+00:00"; const result = transformGanttData({ allTries: [ { - end_date: "2024-03-14T10:05:00+00:00", + end_date: taskEndDate, is_mapped: false, queued_dttm: "2024-03-14T09:59:00+00:00", scheduled_dttm: "2024-03-14T09:58:00+00:00", @@ -235,6 +237,40 @@ describe("transformGanttData", () => { expect(result[0]?.state).toBe("scheduled"); expect(result[1]?.state).toBe("queued"); expect(result[2]?.state).toBe("success"); + expect(result.map((segment) => segment.end_when)).toEqual([taskEndDate, taskEndDate, taskEndDate]); + }); + + it("uses the task end date in tooltips for queued segments", () => { + 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: null, + 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: [], + }); + const [queuedSegment] = result; + + expect(queuedSegment?.state).toBe("queued"); + expect(queuedSegment?.x[1]).toBe(dayjs("2024-03-14T10:00:00+00:00").valueOf()); + + const summary = toTooltipSummary( + queuedSegment as GanttDataItem, + { depth: 0, id: "task_1", is_mapped: false, label: "task_1" }, + undefined, + ); + + expect(summary.min_start_date).toBe("2024-03-14T10:00:00+00:00"); + expect(summary.max_end_date).toBe("2024-03-14T10:05:00+00:00"); }); it("produces 2 segments when only queued_dttm is present", () => { 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..88a6717c1e6f5 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_date for tooltips; distinct from segment end for scheduled/queued bars. */ + end_when?: string | null; isGroup?: boolean | null; isMapped?: boolean | null; /** Source try times for tooltips (matches TaskInstance `*_when` fields). */ @@ -77,6 +79,32 @@ export const gridSummariesToTaskIdMap = ( return byId; }; +export const toTooltipSummary = ( + segment: GanttDataItem, + node: GridTask, + gridSummary: LightGridTaskInstanceSummary | undefined, +) => { + if (gridSummary !== undefined && (node.isGroup ?? node.is_mapped)) { + return gridSummary; + } + + return { + child_states: null, + 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, + task_id: segment.taskId, + try_number: segment.tryNumber, + ...(segment.tryNumber === undefined + ? {} + : { + queued_when: segment.queued_when, + scheduled_when: segment.scheduled_when, + }), + }; +}; + export const transformGanttData = ({ allTries, flatNodes, @@ -152,12 +180,15 @@ export const transformGanttData = ({ endMs = dayjs(endDate).valueOf(); } + const effectiveEndDate = hasTaskRunning ? dayjs(endMs).toISOString() : endDate; + if (scheduledMs !== undefined) { const scheduledEndMs = queuedMs ?? startMs ?? (hasTaskRunning || tryRow.state === "scheduled" ? Date.now() : endMs); if (scheduledEndMs > scheduledMs) { items.push({ + end_when: effectiveEndDate, isGroup: false, isMapped: tryRow.is_mapped, state: "scheduled", @@ -175,6 +206,7 @@ export const transformGanttData = ({ if (queueEndMs > queuedMs) { items.push({ + end_when: effectiveEndDate, isGroup: false, isMapped: tryRow.is_mapped, state: "queued", @@ -192,6 +224,7 @@ export const transformGanttData = ({ const execEndMs = Math.max(startMs, endMs); items.push({ + end_when: effectiveEndDate, isGroup: false, isMapped: tryRow.is_mapped, state: tryRow.state,