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 e97ae32756e Update time duration format (#49914)
e97ae32756e is described below

commit e97ae32756e4d5a77bef1129880216f702f850ce
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Apr 29 14:55:15 2025 -0400

    Update time duration format (#49914)
    
    * Update time duration format
    
    * Simplift datetimeUtils and remove extra s
    
    * Fix tests
---
 .../src/airflow/ui/src/components/DagRunInfo.tsx   |  6 +--
 .../airflow/ui/src/components/DurationChart.tsx    |  3 +-
 .../ui/src/components/TaskInstanceTooltip.tsx      |  5 +-
 .../ui/src/pages/Dag/Backfills/Backfills.tsx       |  2 +-
 .../airflow/ui/src/pages/DagsList/RecentRuns.tsx   |  3 +-
 .../ui/src/pages/MappedTaskInstance/Header.tsx     |  2 +-
 .../src/airflow/ui/src/pages/Run/Details.tsx       |  2 +-
 .../src/airflow/ui/src/pages/Run/Header.tsx        |  2 +-
 .../airflow/ui/src/pages/TaskInstance/Details.tsx  |  2 +-
 .../airflow/ui/src/pages/TaskInstance/Header.tsx   |  2 +-
 .../ui/src/pages/TaskInstances/TaskInstances.tsx   |  2 +-
 .../src/airflow/ui/src/utils/datetimeUtils.test.ts | 56 ++++++++++++++++++++++
 .../utils/{datetime_utils.ts => datetimeUtils.ts}  | 23 +++++++--
 airflow-core/src/airflow/ui/src/utils/index.ts     |  2 +-
 14 files changed, 90 insertions(+), 22 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx 
b/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx
index 80742a38687..fef11e9f66e 100644
--- a/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagRunInfo.tsx
@@ -17,12 +17,12 @@
  * under the License.
  */
 import { VStack, Text, Box } from "@chakra-ui/react";
-import dayjs from "dayjs";
 
 import type { DAGRunResponse } from "openapi/requests/types.gen";
 import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 import { Tooltip } from "src/components/ui";
+import { getDuration } from "src/utils";
 
 type Props = {
   readonly endDate?: string | null;
@@ -52,9 +52,7 @@ const DagRunInfo = ({ endDate, logicalDate, runAfter, 
startDate, state }: Props)
             End Date: <Time datetime={endDate} />
           </Text>
         ) : undefined}
-        {Boolean(startDate) ? (
-          <Text>Duration: 
{dayjs.duration(dayjs(endDate).diff(startDate)).asSeconds()}s</Text>
-        ) : undefined}
+        {Boolean(startDate) ? <Text>Duration: {getDuration(startDate, 
endDate)}</Text> : undefined}
       </VStack>
     }
   >
diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx 
b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
index 6eefcd48005..4e525c55e41 100644
--- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx
@@ -34,8 +34,7 @@ import { Bar } from "react-chartjs-2";
 
 import type { TaskInstanceResponse, DAGRunResponse } from 
"openapi/requests/types.gen";
 import { system } from "src/theme";
-import { pluralize } from "src/utils";
-import { getDuration } from "src/utils/datetime_utils";
+import { pluralize, getDuration } from "src/utils";
 
 ChartJS.register(
   CategoryScale,
diff --git a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx 
b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
index ed09f9a9f93..f2152d468ef 100644
--- a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
@@ -25,6 +25,7 @@ import type {
 } from "openapi/requests/types.gen";
 import Time from "src/components/Time";
 import { Tooltip, type TooltipProps } from "src/components/ui";
+import { getDuration } from "src/utils";
 
 type Props = {
   readonly taskInstance?: GridTaskInstanceSummary | 
TaskInstanceHistoryResponse | TaskInstanceResponse;
@@ -47,8 +48,8 @@ const TaskInstanceTooltip = ({ children, positioning, 
taskInstance, ...rest }: P
             End Date: <Time datetime={taskInstance.end_date} />
           </Text>
           {taskInstance.try_number > 1 && <Text>Try Number: 
{taskInstance.try_number}</Text>}
-          {"duration" in taskInstance ? (
-            <Text>Duration: {taskInstance.duration?.toFixed(2) ?? 0}s</Text>
+          {"start_date" in taskInstance ? (
+            <Text>Duration: {getDuration(taskInstance.start_date, 
taskInstance.end_date)}</Text>
           ) : undefined}
         </Box>
       }
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx
index a84cb6a8dce..5aadf546843 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx
@@ -89,7 +89,7 @@ const columns: Array<ColumnDef<BackfillResponse>> = [
       <Text>
         {row.original.completed_at === null
           ? ""
-          : `${getDuration(row.original.created_at, 
row.original.completed_at)}s`}
+          : getDuration(row.original.created_at, row.original.completed_at)}
       </Text>
     ),
     enableSorting: false,
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
index 045a85d45fc..5e7def01650 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -24,6 +24,7 @@ import { Link } from "react-router-dom";
 import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
 import Time from "src/components/Time";
 import { Tooltip } from "src/components/ui";
+import { getDuration } from "src/utils";
 
 dayjs.extend(duration);
 
@@ -68,7 +69,7 @@ export const RecentRuns = ({
                   End Date: <Time datetime={run.end_date} />
                 </Text>
               )}
-              <Text>Duration: {run.duration.toFixed(2)}s</Text>
+              <Text>Duration: {getDuration(run.start_date, 
run.end_date)}</Text>
             </Box>
           }
           key={run.dag_run_id}
diff --git 
a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx
index 636bc5fa6b1..ec23200ccd8 100644
--- a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx
@@ -46,7 +46,7 @@ export const Header = ({
     { label: "Start", value: <Time datetime={taskInstance.start_date} /> },
     { label: "End", value: <Time datetime={taskInstance.end_date} /> },
     ...(Boolean(taskInstance.start_date)
-      ? [{ label: "Duration", value: `${getDuration(taskInstance.start_date, 
taskInstance.end_date)}s` }]
+      ? [{ label: "Duration", value: getDuration(taskInstance.start_date, 
taskInstance.end_date) }]
       : []),
   ];
 
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx
index cfb9c48be99..f20ada86035 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx
@@ -90,7 +90,7 @@ export const Details = () => {
             </Table.Row>
             <Table.Row>
               <Table.Cell>Run Duration</Table.Cell>
-              <Table.Cell>{getDuration(dagRun.start_date, 
dagRun.end_date)}s</Table.Cell>
+              <Table.Cell>{getDuration(dagRun.start_date, 
dagRun.end_date)}</Table.Cell>
             </Table.Row>
             <Table.Row>
               <Table.Cell>Last Scheduling Decision</Table.Cell>
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
index 8dc53cf113c..d5bd8d7a655 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
@@ -104,7 +104,7 @@ export const Header = ({
           },
           { label: "Start", value: <Time datetime={dagRun.start_date} /> },
           { label: "End", value: <Time datetime={dagRun.end_date} /> },
-          { label: "Duration", value: `${getDuration(dagRun.start_date, 
dagRun.end_date)}s` },
+          { label: "Duration", value: getDuration(dagRun.start_date, 
dagRun.end_date) },
           {
             label: "Dag Version(s)",
             value: (
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx
index bf2a7b7ba21..f02b27dbcfc 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx
@@ -160,7 +160,7 @@ export const Details = () => {
             <Table.Cell>Duration</Table.Cell>
             <Table.Cell>
               {Boolean(tryInstance?.start_date) // eslint-disable-next-line 
unicorn/no-null
-                ? `${getDuration(tryInstance?.start_date ?? null, 
tryInstance?.end_date ?? null)}s`
+                ? getDuration(tryInstance?.start_date ?? null, 
tryInstance?.end_date ?? null)
                 : ""}
             </Table.Cell>
           </Table.Row>
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
index bdeeba3a34c..40c35319b8f 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
@@ -48,7 +48,7 @@ export const Header = ({
     { label: "Start", value: <Time datetime={taskInstance.start_date} /> },
     { label: "End", value: <Time datetime={taskInstance.end_date} /> },
     ...(Boolean(taskInstance.start_date)
-      ? [{ label: "Duration", value: `${getDuration(taskInstance.start_date, 
taskInstance.end_date)}s` }]
+      ? [{ label: "Duration", value: getDuration(taskInstance.start_date, 
taskInstance.end_date) }]
       : []),
     {
       label: "DAG Version",
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
index 11ae32f092b..54e3139c573 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
@@ -140,7 +140,7 @@ const taskInstanceColumns = (
   },
   {
     cell: ({ row: { original } }) =>
-      Boolean(original.start_date) ? `${getDuration(original.start_date, 
original.end_date)}s` : "",
+      Boolean(original.start_date) ? getDuration(original.start_date, 
original.end_date) : "",
     header: "Duration",
   },
   {
diff --git a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts 
b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
new file mode 100644
index 00000000000..3ecf3c0b564
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
@@ -0,0 +1,56 @@
+/*!
+ * 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 { describe, it, expect } from "vitest";
+
+import { getDuration } from "./datetimeUtils";
+
+describe("getDuration", () => {
+  it("handles durations less than 10 seconds", () => {
+    const start = "2024-03-14T10:00:00.000Z";
+    const end = "2024-03-14T10:00:05.500Z";
+
+    expect(getDuration(start, end)).toBe("5.50s");
+  });
+
+  it("handles durations spanning multiple days", () => {
+    const start = "2024-03-14T10:00:00.000Z";
+    const end = "2024-03-17T15:30:45.000Z";
+
+    expect(getDuration(start, end)).toBe("3d05:30:45");
+  });
+
+  it("handles exactly 24 hours", () => {
+    const start = "2024-03-14T10:00:00.000Z";
+    const end = "2024-03-15T10:00:00.000Z";
+
+    expect(getDuration(start, end)).toBe("1d00:00:00");
+  });
+
+  it("handles hours and minutes without days", () => {
+    const start = "2024-03-14T10:00:00.000Z";
+    const end = "2024-03-14T12:30:00.000Z";
+
+    expect(getDuration(start, end)).toBe("02:30:00");
+  });
+
+  it("handles null or undefined dates", () => {
+    expect(getDuration(null, null)).toBe("00:00:00");
+    expect(getDuration(undefined, undefined)).toBe("00:00:00");
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/utils/datetime_utils.ts 
b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
similarity index 61%
rename from airflow-core/src/airflow/ui/src/utils/datetime_utils.ts
rename to airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
index d1660050be7..43e7ada6085 100644
--- a/airflow-core/src/airflow/ui/src/utils/datetime_utils.ts
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
@@ -17,9 +17,22 @@
  * under the License.
  */
 import dayjs from "dayjs";
+import dayjsDuration from "dayjs/plugin/duration";
 
-export const getDuration = (startDate: string | null, endDate: string | null) 
=>
-  dayjs
-    .duration(dayjs(endDate ?? undefined).diff(startDate ?? undefined))
-    .asSeconds()
-    .toFixed(2);
+dayjs.extend(dayjsDuration);
+
+export const getDuration = (startDate?: string | null, endDate?: string | 
null) => {
+  const seconds = dayjs.duration(dayjs(endDate ?? undefined).diff(startDate ?? 
undefined)).asSeconds();
+
+  if (!seconds) {
+    return "00:00:00";
+  }
+
+  if (seconds < 10) {
+    return `${seconds.toFixed(2)}s`;
+  }
+
+  return seconds < 86_400
+    ? dayjs.duration(seconds, "seconds").format("HH:mm:ss")
+    : dayjs.duration(seconds, "seconds").format("D[d]HH:mm:ss");
+};
diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts 
b/airflow-core/src/airflow/ui/src/utils/index.ts
index 03268cfd503..93f604e57e1 100644
--- a/airflow-core/src/airflow/ui/src/utils/index.ts
+++ b/airflow-core/src/airflow/ui/src/utils/index.ts
@@ -19,7 +19,7 @@
 
 export { capitalize } from "./capitalize";
 export { pluralize } from "./pluralize";
-export { getDuration } from "./datetime_utils";
+export { getDuration } from "./datetimeUtils";
 export { getMetaKey } from "./getMetaKey";
 export { useContainerWidth } from "./useContainerWidth";
 export * from "./query";

Reply via email to