From 232eab30a4b96ce46471b683d0d64c9a4bcd2e59 Mon Sep 17 00:00:00 2001 From: pwd Date: Sat, 6 Sep 2025 22:21:56 -0400 Subject: [PATCH 1/5] Improve duration chart --- .../airflow/ui/src/components/DurationChart.tsx | 10 +++++++++- .../airflow/ui/src/pages/Dag/Overview/Overview.tsx | 14 +++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx index c0fd7cf796b97..2ad4b94a0e386 100644 --- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx +++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx @@ -60,9 +60,11 @@ type RunResponse = GridRunsResponse | TaskInstanceResponse; const getDuration = (start: string, end: string | null) => dayjs.duration(dayjs(end).diff(start)).asSeconds(); export const DurationChart = ({ + autoRefreshEnabled, entries, kind, }: { + readonly autoRefreshEnabled?: boolean; readonly entries: Array | undefined; readonly kind: "Dag Run" | "Task Instance"; }) => { @@ -161,10 +163,16 @@ 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) => dayjs(entry.run_after).format("YYYY-MM-DD, hh:mm")), }} datasetIdKey="id" options={{ + animation: { + delay: 0, + duration: autoRefreshEnabled ? 0 : 1000, + easing: "easeOutQuart", + loop: false, + }, onClick: (_event, elements) => { const [element] = elements; diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx index 8de8220a4170f..4a40595c6a907 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx @@ -76,7 +76,15 @@ export const Overview = () => { timestampLte: endDate, }); +<<<<<<< HEAD const refetchInterval = useAutoRefresh({}); +======= + const autoRefreshEnabled = + Boolean(useAutoRefresh({ dagId })) && + gridRuns && + gridRuns.length > 0 && + isStatePending(gridRuns[0]?.state); +>>>>>>> 9c73a294d3 (Improve duration chart) return ( @@ -130,7 +138,11 @@ export const Overview = () => { {isLoadingRuns ? ( ) : ( - + )} {assetEventsData && assetEventsData.total_entries > 0 ? ( From 8f3be61ac5d5901cc6eba9a1c85c4a9e0831680d Mon Sep 17 00:00:00 2001 From: pwd Date: Thu, 11 Sep 2025 19:04:28 -0400 Subject: [PATCH 2/5] Dynamic label format for DurationChart --- .../ui/src/components/DurationChart.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx index 2ad4b94a0e386..b67241f74cdd4 100644 --- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx +++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx @@ -30,6 +30,7 @@ import { import type { PartialEventContext } from "chartjs-plugin-annotation"; import annotationPlugin from "chartjs-plugin-annotation"; import dayjs from "dayjs"; +import minMax from "dayjs/plugin/minMax"; import { Bar } from "react-chartjs-2"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -38,6 +39,8 @@ import type { TaskInstanceResponse, GridRunsResponse } from "openapi/requests/ty import { getComputedCSSVariableValue } from "src/theme"; import { DEFAULT_DATETIME_FORMAT } from "src/utils/datetimeUtils"; +dayjs.extend(minMax); + ChartJS.register( CategoryScale, LinearScale, @@ -116,6 +119,24 @@ export const DurationChart = ({ value: (ctx: PartialEventContext) => average(ctx, 0), }; + const getLabelFormat = (entries: Array) => { + const timestamps = entries.map(entry => dayjs(entry.run_after)); + const minTime = dayjs.min(timestamps); + const maxTime = dayjs.max(timestamps); + + // satisfy null typecheck for dayjs.min/max + if (minTime === null || maxTime === null) { + return "MM-DD"; + } + const diffInDays = maxTime.diff(minTime, 'days'); + + if (diffInDays < 1) { + return "hh:mm:ss" + } else { + return "MM-DD" + } + }; + return ( @@ -163,7 +184,7 @@ export const DurationChart = ({ label: translate("durationChart.runDuration"), }, ], - labels: entries.map((entry: RunResponse) => dayjs(entry.run_after).format("YYYY-MM-DD, hh:mm")), + labels: entries.map((entry: RunResponse) => dayjs(entry.run_after).format(getLabelFormat(entries))), }} datasetIdKey="id" options={{ From a22e91205a41eef54cb82e3d0d17fc8c4c5d65c8 Mon Sep 17 00:00:00 2001 From: pwd Date: Mon, 15 Sep 2025 17:06:21 -0400 Subject: [PATCH 3/5] Resolve merge conflict for autoRefreshEnabled --- .../src/airflow/ui/src/pages/Dag/Overview/Overview.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx index 4a40595c6a907..a763efe5b19d8 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx @@ -76,15 +76,13 @@ export const Overview = () => { timestampLte: endDate, }); -<<<<<<< HEAD const refetchInterval = useAutoRefresh({}); -======= + const autoRefreshEnabled = Boolean(useAutoRefresh({ dagId })) && gridRuns && gridRuns.length > 0 && isStatePending(gridRuns[0]?.state); ->>>>>>> 9c73a294d3 (Improve duration chart) return ( From 2ff7a317210b9ee91e4fb549052fa59a240362dd Mon Sep 17 00:00:00 2001 From: pwd Date: Sat, 20 Sep 2025 12:20:42 -0400 Subject: [PATCH 4/5] Move getLabelFormat out, full date on hover --- .../ui/src/components/DurationChart.tsx | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx index b67241f74cdd4..764f715774f9c 100644 --- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx +++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx @@ -62,6 +62,24 @@ type RunResponse = GridRunsResponse | TaskInstanceResponse; const getDuration = (start: string, end: string | null) => dayjs.duration(dayjs(end).diff(start)).asSeconds(); +const getLabelFormat = (entries: Array) => { + const timestamps = entries.map(entry => dayjs(entry.run_after)); + const minTime = dayjs.min(timestamps); + const maxTime = dayjs.max(timestamps); + + // satisfy null typecheck for dayjs.min/max + if (minTime === null || maxTime === null) { + return "MM-DD"; + } + const diffInDays = maxTime.diff(minTime, 'days'); + + if (diffInDays < 1) { + return "hh:mm:ss" + } else { + return "MM-DD" + } +}; + export const DurationChart = ({ autoRefreshEnabled, entries, @@ -119,24 +137,6 @@ export const DurationChart = ({ value: (ctx: PartialEventContext) => average(ctx, 0), }; - const getLabelFormat = (entries: Array) => { - const timestamps = entries.map(entry => dayjs(entry.run_after)); - const minTime = dayjs.min(timestamps); - const maxTime = dayjs.max(timestamps); - - // satisfy null typecheck for dayjs.min/max - if (minTime === null || maxTime === null) { - return "MM-DD"; - } - const diffInDays = maxTime.diff(minTime, 'days'); - - if (diffInDays < 1) { - return "hh:mm:ss" - } else { - return "MM-DD" - } - }; - return ( @@ -184,7 +184,7 @@ export const DurationChart = ({ label: translate("durationChart.runDuration"), }, ], - labels: entries.map((entry: RunResponse) => dayjs(entry.run_after).format(getLabelFormat(entries))), + labels: entries.map((entry: RunResponse) => dayjs(entry.run_after).format("YYYY-MM-DD HH:mm:ss")), }} datasetIdKey="id" options={{ @@ -236,6 +236,9 @@ export const DurationChart = ({ stacked: true, ticks: { maxTicksLimit: 3, + callback: (value) => { + return(dayjs(value).format(getLabelFormat(entries))) + } }, title: { align: "end", display: true, text: translate("common:dagRun.runAfter") }, }, From ceac5e535ef328a92c5530d248d1f3fe4b9374a8 Mon Sep 17 00:00:00 2001 From: pwd Date: Mon, 22 Sep 2025 20:14:33 -0400 Subject: [PATCH 5/5] Change back to default date format --- airflow-core/src/airflow/ui/src/components/DurationChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx index 764f715774f9c..ccec75b939a9b 100644 --- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx +++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx @@ -184,7 +184,7 @@ export const DurationChart = ({ label: translate("durationChart.runDuration"), }, ], - labels: entries.map((entry: RunResponse) => dayjs(entry.run_after).format("YYYY-MM-DD HH:mm:ss")), + labels: entries.map((entry: RunResponse) => dayjs(entry.run_after).format(DEFAULT_DATETIME_FORMAT)), }} datasetIdKey="id" options={{