This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-2-test by this push:
new 1514e296e1a [v3-2-test] Fix Gantt view "Error invalid date" on running
DagRun (#64752) (#64853)
1514e296e1a is described below
commit 1514e296e1a591387ae4e4aa835d364bdc8e11ed
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Apr 7 15:06:28 2026 -0400
[v3-2-test] Fix Gantt view "Error invalid date" on running DagRun (#64752)
(#64853)
* Fix Gantt view "Error invalid date" on running DagRun (#64599)
The Gantt chart scale calculation used new Date() to parse date strings,
which fails for non-UTC timezone abbreviations, returning NaN and
crashing Chart.js's time scale.
Changes:
- Replace new Date().getTime() with dayjs().valueOf() for reliable date
parsing in the x-axis scale min/max calculations.
- Add unit tests for Gantt chart scale calculations and data
transformation covering completed tasks, running tasks with null end
dates, groups with null dates, and ISO date string validity.
* Address review feedback: fix static checks and consistency
- Fix object property ordering in test data (alphabetical)
- Import vi as type-only from vitest
- Fix duration field position in selectedRun objects
- Make dayjs null handling consistent between max and min scale
calculations (remove unnecessary ?? undefined)
* Fix ESLint no-empty-function error in Gantt utils test
* Fix TypeScript type errors in Gantt utils tests
Update test fixtures to match current generated types:
- Add has_missed_deadline to GridRunsResponse objects
- Remove dag_id and map_index from GanttTaskInstance objects
- Add depth to GridTask objects
- Add task_display_name to LightGridTaskInstanceSummary objects
* Fix translate type to use TFunction in Gantt utils tests
(cherry picked from commit 0b2efb99e6ff4857adcdb0acd3519451c57ad61e)
Co-authored-by: Ashir Alam <[email protected]>
---
.../ui/src/layouts/Details/Gantt/utils.test.ts | 261 +++++++++++++++++++++
.../airflow/ui/src/layouts/Details/Gantt/utils.ts | 8 +-
2 files changed, 265 insertions(+), 4 deletions(-)
diff --git
a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.test.ts
b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.test.ts
new file mode 100644
index 00000000000..b9fa6a491be
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/utils.test.ts
@@ -0,0 +1,261 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { ChartEvent, ActiveElement } from "chart.js";
+import dayjs from "dayjs";
+import type { TFunction } from "i18next";
+import { describe, it, expect } from "vitest";
+
+import type { GanttDataItem } from "./utils";
+import { createChartOptions, transformGanttData } from "./utils";
+
+// eslint-disable-next-line no-empty-function,
@typescript-eslint/no-empty-function
+const noop = () => {};
+
+const defaultChartParams = {
+ gridColor: "#ccc",
+ handleBarClick: noop as (event: ChartEvent, elements: Array<ActiveElement>)
=> void,
+ handleBarHover: noop as (event: ChartEvent, elements: Array<ActiveElement>)
=> void,
+ hoveredId: undefined,
+ hoveredItemColor: "#eee",
+ labels: ["task_1", "task_2"],
+ selectedId: undefined,
+ selectedItemColor: "#ddd",
+ selectedTimezone: "UTC",
+ translate: ((key: string) => key) as unknown as TFunction,
+};
+
+describe("createChartOptions", () => {
+ describe("x-axis scale min/max with ISO date strings", () => {
+ it("should compute valid min/max for completed tasks with ISO dates", ()
=> {
+ const data: Array<GanttDataItem> = [
+ {
+ state: "success",
+ taskId: "task_1",
+ x: ["2024-03-14T10:00:00.000Z", "2024-03-14T10:05:00.000Z"],
+ y: "task_1",
+ },
+ {
+ state: "success",
+ taskId: "task_2",
+ x: ["2024-03-14T10:03:00.000Z", "2024-03-14T10:10:00.000Z"],
+ y: "task_2",
+ },
+ ];
+
+ const options = createChartOptions({
+ ...defaultChartParams,
+ data,
+ selectedRun: {
+ dag_id: "test_dag",
+ duration: 600,
+ end_date: "2024-03-14T10:10:00+00:00",
+ has_missed_deadline: false,
+ queued_at: "2024-03-14T09:59:00+00:00",
+ run_after: "2024-03-14T10:00:00+00:00",
+ run_id: "run_1",
+ run_type: "manual",
+ start_date: "2024-03-14T10:00:00+00:00",
+ state: "success",
+ },
+ });
+
+ const xScale = options.scales.x;
+
+ expect(xScale.min).toBeTypeOf("number");
+ expect(xScale.max).toBeTypeOf("number");
+ expect(Number.isNaN(xScale.min)).toBe(false);
+ expect(Number.isNaN(xScale.max)).toBe(false);
+ // max should be slightly beyond the latest end date (5% padding)
+ expect(xScale.max).toBeGreaterThan(new
Date("2024-03-14T10:10:00.000Z").getTime());
+ });
+
+ it("should compute valid min/max for running tasks", () => {
+ const now = dayjs().toISOString();
+ const data: Array<GanttDataItem> = [
+ {
+ state: "success",
+ taskId: "task_1",
+ x: ["2024-03-14T10:00:00.000Z", "2024-03-14T10:05:00.000Z"],
+ y: "task_1",
+ },
+ {
+ state: "running",
+ taskId: "task_2",
+ x: ["2024-03-14T10:05:00.000Z", now],
+ y: "task_2",
+ },
+ ];
+
+ const options = createChartOptions({
+ ...defaultChartParams,
+ data,
+ selectedRun: {
+ dag_id: "test_dag",
+ duration: 0,
+ // eslint-disable-next-line unicorn/no-null
+ end_date: null,
+ has_missed_deadline: false,
+ queued_at: "2024-03-14T09:59:00+00:00",
+ run_after: "2024-03-14T10:00:00+00:00",
+ run_id: "run_1",
+ run_type: "manual",
+ start_date: "2024-03-14T10:00:00+00:00",
+ state: "running",
+ },
+ });
+
+ const xScale = options.scales.x;
+
+ expect(xScale.min).toBeTypeOf("number");
+ expect(xScale.max).toBeTypeOf("number");
+ expect(Number.isNaN(xScale.min)).toBe(false);
+ expect(Number.isNaN(xScale.max)).toBe(false);
+ });
+
+ it("should handle empty data with running DagRun (fallback to formatted
dates)", () => {
+ const options = createChartOptions({
+ ...defaultChartParams,
+ data: [],
+ labels: [],
+ selectedRun: {
+ dag_id: "test_dag",
+ duration: 0,
+ // eslint-disable-next-line unicorn/no-null
+ end_date: null,
+ has_missed_deadline: false,
+ queued_at: "2024-03-14T09:59:00+00:00",
+ run_after: "2024-03-14T10:00:00+00:00",
+ run_id: "run_1",
+ run_type: "manual",
+ start_date: "2024-03-14T10:00:00+00:00",
+ state: "running",
+ },
+ });
+
+ const xScale = options.scales.x;
+
+ // With empty data, min/max are formatted date strings (fallback branch)
+ expect(xScale.min).toBeTypeOf("string");
+ expect(xScale.max).toBeTypeOf("string");
+ });
+ });
+});
+
+describe("transformGanttData", () => {
+ it("should skip tasks with null start_date", () => {
+ const result = transformGanttData({
+ allTries: [
+ {
+ // eslint-disable-next-line unicorn/no-null
+ end_date: null,
+ is_mapped: false,
+ // eslint-disable-next-line unicorn/no-null
+ start_date: null,
+ // eslint-disable-next-line unicorn/no-null
+ state: null,
+ 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: [],
+ });
+
+ expect(result).toHaveLength(0);
+ });
+
+ it("should include running tasks with valid start_date and use current time
as end", () => {
+ const before = dayjs();
+ const result = transformGanttData({
+ allTries: [
+ {
+ // eslint-disable-next-line unicorn/no-null
+ end_date: null,
+ is_mapped: false,
+ start_date: "2024-03-14T10:00:00+00:00",
+ state: "running",
+ 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: [],
+ });
+
+ expect(result).toHaveLength(1);
+ expect(result[0]?.state).toBe("running");
+ // End time should be approximately now (ISO string)
+ const endTime = dayjs(result[0]?.x[1]);
+
+ expect(endTime.valueOf()).toBeGreaterThanOrEqual(before.valueOf());
+ });
+
+ it("should skip groups with null min_start_date or max_end_date", () => {
+ const result = transformGanttData({
+ allTries: [],
+ flatNodes: [{ depth: 0, id: "group_1", is_mapped: false, isGroup: true,
label: "group_1" }],
+ gridSummaries: [
+ {
+ // eslint-disable-next-line unicorn/no-null
+ child_states: null,
+ // eslint-disable-next-line unicorn/no-null
+ max_end_date: null,
+ // eslint-disable-next-line unicorn/no-null
+ min_start_date: null,
+ // eslint-disable-next-line unicorn/no-null
+ state: null,
+ task_display_name: "group_1",
+ task_id: "group_1",
+ },
+ ],
+ });
+
+ expect(result).toHaveLength(0);
+ });
+
+ it("should produce ISO date strings parseable by dayjs", () => {
+ const result = transformGanttData({
+ allTries: [
+ {
+ end_date: "2024-03-14T10:05:00+00:00",
+ is_mapped: false,
+ 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: [],
+ });
+
+ expect(result).toHaveLength(1);
+ // x values should be valid ISO strings that dayjs can parse without NaN
+ const start = dayjs(result[0]?.x[0]);
+ const end = dayjs(result[0]?.x[1]);
+
+ expect(start.isValid()).toBe(true);
+ expect(end.isValid()).toBe(true);
+ expect(Number.isNaN(start.valueOf())).toBe(false);
+ expect(Number.isNaN(end.valueOf())).toBe(false);
+ });
+});
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 22df4eb28cf..fab1d1bcf77 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
@@ -347,8 +347,8 @@ export const createChartOptions = ({
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 maxTime = Math.max(...data.map((item) =>
dayjs(item.x[1]).valueOf()));
+ const minTime = Math.min(...data.map((item) =>
dayjs(item.x[0]).valueOf()));
const totalDuration = maxTime - minTime;
// add 5% to the max time to avoid the last tick being cut off
@@ -358,8 +358,8 @@ export const createChartOptions = ({
min:
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 maxTime = Math.max(...data.map((item) =>
dayjs(item.x[1]).valueOf()));
+ const minTime = Math.min(...data.map((item) =>
dayjs(item.x[0]).valueOf()));
const totalDuration = maxTime - minTime;
// subtract 2% from min time so background color shows before
data