Skip to content
Merged
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
18 changes: 12 additions & 6 deletions airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +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 dayjs from "dayjs";
import { useMemo, useRef, useDeferredValue } from "react";
import { Bar } from "react-chartjs-2";
import { useTranslation } from "react-i18next";
Expand All @@ -48,7 +49,7 @@ import { useGridStructure } from "src/queries/useGridStructure";
import { useGridTiSummaries } from "src/queries/useGridTISummaries";
import { getComputedCSSVariableValue } from "src/theme";
import { isStatePending, useAutoRefresh } from "src/utils";
import { DEFAULT_DATETIME_FORMAT, formatDate } from "src/utils/datetimeUtils";
import { DEFAULT_DATETIME_FORMAT_WITH_TZ, formatDate } from "src/utils/datetimeUtils";

import { createHandleBarClick, createChartOptions } from "./utils";

Expand Down Expand Up @@ -127,6 +128,8 @@ export const Gantt = ({ limit }: Props) => {

const isLoading = runsLoading || structureLoading || summariesLoading || tiLoading;

const currentTime = dayjs().tz(selectedTimezone).format(DEFAULT_DATETIME_FORMAT_WITH_TZ);

const data = useMemo(() => {
if (isLoading || runId === "") {
return [];
Expand All @@ -147,8 +150,8 @@ export const Gantt = ({ limit }: Props) => {
state: gridSummary.state,
taskId: gridSummary.task_id,
x: [
formatDate(gridSummary.min_start_date, selectedTimezone, DEFAULT_DATETIME_FORMAT),
formatDate(gridSummary.max_end_date, selectedTimezone, DEFAULT_DATETIME_FORMAT),
formatDate(gridSummary.min_start_date, selectedTimezone, DEFAULT_DATETIME_FORMAT_WITH_TZ),
formatDate(gridSummary.max_end_date, selectedTimezone, DEFAULT_DATETIME_FORMAT_WITH_TZ),
],
y: gridSummary.task_id,
};
Expand All @@ -157,14 +160,17 @@ export const Gantt = ({ limit }: Props) => {
const taskInstance = taskInstances.find((ti) => ti.task_id === node.id);

if (taskInstance) {
const hasTaskRunning = isStatePending(taskInstance.state);
const endTime = hasTaskRunning ? currentTime : taskInstance.end_date;

return {
isGroup: node.isGroup,
isMapped: node.is_mapped,
state: taskInstance.state,
taskId: taskInstance.task_id,
x: [
formatDate(taskInstance.start_date, selectedTimezone, DEFAULT_DATETIME_FORMAT),
formatDate(taskInstance.end_date, selectedTimezone, DEFAULT_DATETIME_FORMAT),
formatDate(taskInstance.start_date, selectedTimezone, DEFAULT_DATETIME_FORMAT_WITH_TZ),
formatDate(endTime, selectedTimezone, DEFAULT_DATETIME_FORMAT_WITH_TZ),
],
y: taskInstance.task_id,
};
Expand All @@ -174,7 +180,7 @@ export const Gantt = ({ limit }: Props) => {
return undefined;
})
.filter((item) => item !== undefined);
}, [flatNodes, gridTiSummaries, taskInstancesData, selectedTimezone, isLoading, runId]);
}, [flatNodes, gridTiSummaries, taskInstancesData, selectedTimezone, isLoading, runId, currentTime]);

// Get all unique states and their colors
const states = [...new Set(data.map((item) => item.state ?? "none"))];
Expand Down
194 changes: 101 additions & 93 deletions airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
* under the License.
*/
import type { ChartEvent, ActiveElement, TooltipItem } from "chart.js";
import dayjs from "dayjs";
import type { TFunction } from "i18next";
import type { NavigateFunction, Location } from "react-router-dom";

import type { GridRunsResponse, TaskInstanceState } from "openapi/requests";
import { getDuration } from "src/utils";
import { getDuration, isStatePending } from "src/utils";
import { formatDate } from "src/utils/datetimeUtils";
import { buildTaskInstanceUrl } from "src/utils/links";

Expand Down Expand Up @@ -91,107 +92,114 @@ export const createChartOptions = ({
selectedRun,
selectedTimezone,
translate,
}: ChartOptionsParams) => ({
animation: {
duration: 150,
easing: "linear" as const,
},
indexAxis: "y" as const,
maintainAspectRatio: false,
onClick: handleBarClick,
onHover: (event: ChartEvent, elements: Array<ActiveElement>) => {
const target = event.native?.target as HTMLElement | undefined;
}: ChartOptionsParams) => {
const isActivePending = isStatePending(selectedRun?.state);
const effectiveEndDate = isActivePending
? dayjs().tz(selectedTimezone).format("YYYY-MM-DD HH:mm:ss")
: selectedRun?.end_date;

if (target) {
target.style.cursor = elements.length > 0 ? "pointer" : "default";
}
},
plugins: {
annotation: {
annotations:
selectedId === undefined || selectedId === ""
? []
: [
{
backgroundColor: selectedItemColor,
borderWidth: 0,
drawTime: "beforeDatasetsDraw" as const,
type: "box" as const,
xMax: "max" as const,
xMin: "min" as const,
yMax: data.findIndex((dataItem) => dataItem.y === selectedId) + 0.5,
yMin: data.findIndex((dataItem) => dataItem.y === selectedId) - 0.5,
},
],
return {
animation: {
duration: 150,
easing: "linear" as const,
},
legend: {
display: false,
indexAxis: "y" as const,
maintainAspectRatio: false,
onClick: handleBarClick,
onHover: (event: ChartEvent, elements: Array<ActiveElement>) => {
const target = event.native?.target as HTMLElement | undefined;

if (target) {
target.style.cursor = elements.length > 0 ? "pointer" : "default";
}
},
tooltip: {
callbacks: {
afterBody(tooltipItems: Array<TooltipItem<"bar">>) {
const taskInstance = data.find((dataItem) => dataItem.y === tooltipItems[0]?.label);
const startDate = formatDate(taskInstance?.x[0], selectedTimezone);
const endDate = formatDate(taskInstance?.x[1], selectedTimezone);
plugins: {
annotation: {
annotations:
selectedId === undefined || selectedId === ""
? []
: [
{
backgroundColor: selectedItemColor,
borderWidth: 0,
drawTime: "beforeDatasetsDraw" as const,
type: "box" as const,
xMax: "max" as const,
xMin: "min" as const,
yMax: data.findIndex((dataItem) => dataItem.y === selectedId) + 0.5,
yMin: data.findIndex((dataItem) => dataItem.y === selectedId) - 0.5,
},
],
},
legend: {
display: false,
},
tooltip: {
callbacks: {
afterBody(tooltipItems: Array<TooltipItem<"bar">>) {
const taskInstance = data.find((dataItem) => dataItem.y === tooltipItems[0]?.label);
const startDate = formatDate(taskInstance?.x[0], selectedTimezone);
const endDate = formatDate(taskInstance?.x[1], selectedTimezone);

return [
`${translate("startDate")}: ${startDate}`,
`${translate("endDate")}: ${endDate}`,
`${translate("duration")}: ${getDuration(taskInstance?.x[0], taskInstance?.x[1])}`,
];
},
label(tooltipItem: TooltipItem<"bar">) {
const { label } = tooltipItem;
const taskInstance = data.find((dataItem) => dataItem.y === label);
return [
`${translate("startDate")}: ${startDate}`,
`${translate("endDate")}: ${endDate}`,
`${translate("duration")}: ${getDuration(taskInstance?.x[0], taskInstance?.x[1])}`,
];
},
label(tooltipItem: TooltipItem<"bar">) {
const { label } = tooltipItem;
const taskInstance = data.find((dataItem) => dataItem.y === label);

return `${translate("state")}: ${translate(`states.${taskInstance?.state}`)}`;
return `${translate("state")}: ${translate(`states.${taskInstance?.state}`)}`;
},
},
},
},
},
resizeDelay: 100,
responsive: true,
scales: {
x: {
grid: {
color: gridColor,
display: true,
},
max:
data.length > 0
? (() => {
const maxTime = Math.max(...data.map((item) => new Date(item.x[1] ?? "").getTime()));
const minTime = Math.min(...data.map((item) => new Date(item.x[0] ?? "").getTime()));
const totalDuration = maxTime - minTime;
resizeDelay: 100,
responsive: true,
scales: {
x: {
grid: {
color: gridColor,
display: true,
},
max:
data.length > 0
? (() => {
const maxTime = Math.max(...data.map((item) => new Date(item.x[1] ?? "").getTime()));
const minTime = Math.min(...data.map((item) => new Date(item.x[0] ?? "").getTime()));
const totalDuration = maxTime - minTime;

// add 5% to the max time to avoid the last tick being cut off
return maxTime + totalDuration * 0.05;
})()
: formatDate(selectedRun?.end_date, selectedTimezone),
min:
data.length > 0
? Math.min(...data.map((item) => new Date(item.x[0] ?? "").getTime()))
: formatDate(selectedRun?.start_date, selectedTimezone),
position: "top" as const,
stacked: true,
ticks: {
align: "start" as const,
callback: (value: number | string) => formatDate(value, selectedTimezone, "HH:mm:ss"),
maxRotation: 8,
maxTicksLimit: 8,
minRotation: 8,
},
type: "time" as const,
},
y: {
grid: {
color: gridColor,
display: true,
// add 5% to the max time to avoid the last tick being cut off
return maxTime + totalDuration * 0.05;
})()
: formatDate(effectiveEndDate, selectedTimezone),
min:
data.length > 0
? Math.min(...data.map((item) => new Date(item.x[0] ?? "").getTime()))
: formatDate(selectedRun?.start_date, selectedTimezone),
position: "top" as const,
stacked: true,
ticks: {
align: "start" as const,
callback: (value: number | string) => formatDate(value, selectedTimezone, "HH:mm:ss"),
maxRotation: 8,
maxTicksLimit: 8,
minRotation: 8,
},
type: "time" as const,
},
stacked: true,
ticks: {
display: false,
y: {
grid: {
color: gridColor,
display: true,
},
stacked: true,
ticks: {
display: false,
},
},
},
},
});
};
};