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,
+        },
       },
     },
-  },
-});
+  };
+};

Reply via email to