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 f7a2f4e6b0d UI: Show duration chart tooltips in the selected timezone
(#72024)
f7a2f4e6b0d is described below
commit f7a2f4e6b0de898f97768313583cddf0cf1c61ef
Author: rjgoyln <[email protected]>
AuthorDate: Tue Sep 1 00:31:43 2026 +0800
UI: Show duration chart tooltips in the selected timezone (#72024)
The Last N Dag Runs / Task Instances chart formatted its bar labels in the
browser's local timezone while the x-axis ticks and the rest of the UI
follow
the timezone picked in the header. Anyone whose machine sits in a different
timezone read a tooltip timestamp that did not identify the run they were
pointing at, which makes the chart misleading exactly when it is used to
compare runs.
Related: #72018
---
.../ui/src/components/DurationChart.test.tsx | 65 ++++++++++++++++++++++
.../airflow/ui/src/components/DurationChart.tsx | 5 +-
2 files changed, 66 insertions(+), 4 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.test.tsx
b/airflow-core/src/airflow/ui/src/components/DurationChart.test.tsx
new file mode 100644
index 00000000000..0eea99c0535
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/DurationChart.test.tsx
@@ -0,0 +1,65 @@
+/*!
+ * 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 { render } from "@testing-library/react";
+import { Bar } from "react-chartjs-2";
+import { describe, expect, it, vi } from "vitest";
+
+import type { GridRunsResponse } from "openapi/requests/types.gen";
+import { TimezoneContext } from "src/context/timezone";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { DurationChart } from "./DurationChart";
+
+const makeRun = (runAfter: string): GridRunsResponse => ({
+ dag_id: "tutorial_dag",
+ duration: 60,
+ end_date: `${runAfter.slice(0, -1)}:01:00Z`,
+ has_missed_deadline: false,
+ has_note: false,
+ queued_at: runAfter,
+ run_after: runAfter,
+ run_id: runAfter,
+ run_type: "scheduled",
+ start_date: runAfter,
+ state: "success",
+});
+
+// A Dag scheduled twice a day: the two runs land 12 hours apart on the same
date.
+const entries = [makeRun("2026-08-20T08:30:00Z"),
makeRun("2026-08-20T20:30:00Z")];
+
+const renderChart = (selectedTimezone: string) => {
+ render(
+ <TimezoneContext.Provider value={{ selectedTimezone, setSelectedTimezone:
vi.fn() }}>
+ <DurationChart entries={entries} kind="Dag Run" />
+ </TimezoneContext.Provider>,
+ { wrapper: Wrapper },
+ );
+
+ return vi.mocked(Bar).mock.calls.at(-1)?.[0].data.labels;
+};
+
+describe("DurationChart", () => {
+ it.each([
+ { expected: ["2026-08-20 08:30:00", "2026-08-20 20:30:00"], timezone:
"UTC" },
+ { expected: ["2026-08-20 17:30:00", "2026-08-21 05:30:00"], timezone:
"Asia/Tokyo" },
+ { expected: ["2026-08-20 04:30:00", "2026-08-20 16:30:00"], timezone:
"America/New_York" },
+ ])("labels each bar in the $timezone timezone", ({ expected, timezone }) => {
+ expect(renderChart(timezone)).toEqual(expected);
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
index b558249ff1d..2153e4cbfe9 100644
--- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
@@ -38,7 +38,6 @@ import type { TaskInstanceResponse, GridRunsResponse } from
"openapi/requests/ty
import { useTimezone } from "src/context/timezone";
import { getComputedCSSVariableValue } from "src/theme";
import {
- DEFAULT_DATETIME_FORMAT,
formatDate,
getDurationTickStep,
renderCompactDuration,
@@ -195,9 +194,7 @@ export const DurationChart = ({
label: translate("durationChart.runDuration"),
},
],
- labels: entries.map((entry: RunResponse) =>
- dayjs(entry.run_after).format(DEFAULT_DATETIME_FORMAT),
- ),
+ labels: entries.map((entry: RunResponse) =>
formatDate(entry.run_after, selectedTimezone)),
}}
datasetIdKey="id"
options={{