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";