This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 67b0c79ef70 #55020 gantt view is not getting updatedin realtime
(#55130)
67b0c79ef70 is described below
commit 67b0c79ef704ce412cf9b4055a66de0eacbf251f
Author: Yiming Peng <[email protected]>
AuthorDate: Tue Sep 16 08:52:55 2025 +1200
#55020 gantt view is not getting updatedin realtime (#55130)
* Fix real-time updates for running tasks in Gantt chart view
* Fix linting
* Use DEFAULT_DATETIME_FORMAT_WITH_TZ as recommended
* Fix linting issue
---
.../airflow/ui/src/layouts/Details/Gantt/Gantt.tsx | 18 +-
.../airflow/ui/src/layouts/Details/Gantt/utils.ts | 194 +++++++++++----------
2 files changed, 113 insertions(+), 99 deletions(-)
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 154b6e016cd..9d794cc9af6 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";
@@ -128,6 +129,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 [];
@@ -148,8 +151,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,
};
@@ -158,14 +161,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,
};
@@ -175,7 +181,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 d5f937ab42e..df93b963910 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<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,
+ },
},
},
- },
-});
+ };
+};