diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx index 655e86e19ce87..bdad6bcc9e0b1 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx @@ -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"; @@ -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"; @@ -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 []; @@ -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, }; @@ -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, }; @@ -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"))]; 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 d5f937ab42ed8..df93b96391012 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 @@ -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"; @@ -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) => { - 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) => { + const target = event.native?.target as HTMLElement | undefined; + + if (target) { + target.style.cursor = elements.length > 0 ? "pointer" : "default"; + } }, - tooltip: { - callbacks: { - afterBody(tooltipItems: Array>) { - 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>) { + 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, + }, }, }, - }, -}); + }; +};