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
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
buildMaxTryByTaskId,
GANTT_TIME_AXIS_TICK_COUNT,
gridSummariesToTaskIdMap,
toTooltipSummary,
transformGanttData,
} from "./utils";

Expand Down Expand Up @@ -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",
Expand All @@ -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", () => {
Expand Down
33 changes: 33 additions & 0 deletions airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -175,6 +206,7 @@ export const transformGanttData = ({

if (queueEndMs > queuedMs) {
items.push({
end_when: effectiveEndDate,
isGroup: false,
isMapped: tryRow.is_mapped,
state: "queued",
Expand All @@ -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,
Expand Down
Loading