diff --git a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
index ae5df5b5a97e2..025463dc955f6 100644
--- a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
@@ -26,7 +26,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";
+import { renderDuration } from "src/utils";
type Props = {
readonly taskInstance?: LightGridTaskInstanceSummary | TaskInstanceHistoryResponse | TaskInstanceResponse;
@@ -64,7 +64,7 @@ const TaskInstanceTooltip = ({ children, positioning, taskInstance, ...rest }: P
{translate("endDate")}:
- {translate("duration")}: {getDuration(taskInstance.start_date, taskInstance.end_date)}
+ {translate("duration")}: {renderDuration(taskInstance.duration)}
>
) : undefined}
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 3d98c30dccd1c..d52ca59f53631 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -25,7 +25,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";
+import { renderDuration } from "src/utils";
dayjs.extend(duration);
@@ -42,19 +42,14 @@ export const RecentRuns = ({
return undefined;
}
- const runsWithDuration = latestRuns.map((run) => ({
- ...run,
- duration: dayjs.duration(dayjs(run.end_date).diff(run.start_date)).asSeconds(),
- }));
-
const max = Math.max.apply(
undefined,
- runsWithDuration.map((run) => run.duration),
+ latestRuns.map((run) => run.duration ?? 0),
);
return (
- {runsWithDuration.map((run) => (
+ {latestRuns.map((run) => (
@@ -75,7 +70,7 @@ export const RecentRuns = ({
)}
- {translate("duration")}: {getDuration(run.start_date, run.end_date)}
+ {translate("duration")}: {renderDuration(run.duration)}
}
@@ -93,7 +88,7 @@ export const RecentRuns = ({
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 400846d1e2aa7..a769498f6d05e 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx
@@ -30,7 +30,7 @@ import { TaskTrySelect } from "src/components/TaskTrySelect";
import Time from "src/components/Time";
import { ClipboardRoot, ClipboardIconButton } from "src/components/ui";
import { SearchParamsKeys } from "src/constants/searchParams";
-import { getDuration, useAutoRefresh, isStatePending } from "src/utils";
+import { useAutoRefresh, isStatePending, renderDuration } from "src/utils";
import { BlockingDeps } from "./BlockingDeps";
import { ExtraLinks } from "./ExtraLinks";
@@ -147,11 +147,7 @@ export const Details = () => {
{translate("duration")}
-
- {Boolean(tryInstance?.start_date) // eslint-disable-next-line unicorn/no-null
- ? getDuration(tryInstance?.start_date ?? null, tryInstance?.end_date ?? null)
- : ""}
-
+ {renderDuration(tryInstance?.duration)}
{translate("startDate")}
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 feeaf1da33e50..37349d8f030e5 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx
@@ -29,7 +29,7 @@ import { HeaderCard } from "src/components/HeaderCard";
import { MarkTaskInstanceAsButton } from "src/components/MarkAs";
import Time from "src/components/Time";
import { usePatchTaskInstance } from "src/queries/usePatchTaskInstance";
-import { getDuration, useContainerWidth } from "src/utils";
+import { renderDuration, useContainerWidth } from "src/utils";
export const Header = ({
isRefreshing,
@@ -53,7 +53,7 @@ export const Header = ({
{ label: translate("startDate"), value: },
{ label: translate("endDate"), value: },
...(Boolean(taskInstance.start_date)
- ? [{ label: translate("duration"), value: getDuration(taskInstance.start_date, taskInstance.end_date) }]
+ ? [{ label: translate("duration"), value: renderDuration(taskInstance.duration) }]
: []),
{
label: translate("taskInstance.dagVersion"),
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 072472471cb74..f7965e696bbb2 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
@@ -37,7 +37,7 @@ import { StateBadge } from "src/components/StateBadge";
import Time from "src/components/Time";
import { TruncatedText } from "src/components/TruncatedText";
import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams";
-import { getDuration, useAutoRefresh, isStatePending } from "src/utils";
+import { useAutoRefresh, isStatePending, renderDuration } from "src/utils";
import { getTaskInstanceLink } from "src/utils/links";
import DeleteTaskInstanceButton from "./DeleteTaskInstanceButton";
@@ -183,8 +183,8 @@ const taskInstanceColumns = ({
header: translate("task.operator"),
},
{
- cell: ({ row: { original } }) =>
- Boolean(original.start_date) ? getDuration(original.start_date, original.end_date) : "",
+ accessorKey: "duration",
+ cell: ({ row: { original } }) => renderDuration(original.duration),
header: translate("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
index 3ecf3c0b5641b..229936d925fdc 100644
--- a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
@@ -18,7 +18,7 @@
*/
import { describe, it, expect } from "vitest";
-import { getDuration } from "./datetimeUtils";
+import { getDuration, renderDuration } from "./datetimeUtils";
describe("getDuration", () => {
it("handles durations less than 10 seconds", () => {
@@ -49,8 +49,10 @@ describe("getDuration", () => {
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");
+ it("handles small, null or undefined values", () => {
+ // eslint-disable-next-line unicorn/no-null
+ expect(getDuration(null, null)).toBe(undefined);
+ expect(getDuration(undefined, undefined)).toBe(undefined);
+ expect(renderDuration(0.000_01)).toBe(undefined);
});
});
diff --git a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
index f1871b4d4fe6f..a5495111d69db 100644
--- a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
@@ -26,20 +26,17 @@ dayjs.extend(tz);
export const DEFAULT_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
export const DEFAULT_DATETIME_FORMAT_WITH_TZ = `${DEFAULT_DATETIME_FORMAT} z`;
-export const renderDuration = (durationSeconds: number | null | undefined): string => {
- if (
- durationSeconds === null ||
- durationSeconds === undefined ||
- isNaN(durationSeconds) ||
- durationSeconds <= 0
- ) {
- return "00:00:00";
+export const renderDuration = (durationSeconds: number | null | undefined): string | undefined => {
+ if (durationSeconds === null || durationSeconds === undefined || durationSeconds <= 0.01) {
+ return undefined;
}
+ // If under 10 seconds, render as 9s
if (durationSeconds < 10) {
return `${durationSeconds.toFixed(2)}s`;
}
+ // If under 1 day, render as HH:mm:ss otherwise include the number of days
return durationSeconds < 86_400
? dayjs.duration(durationSeconds, "seconds").format("HH:mm:ss")
: dayjs.duration(durationSeconds, "seconds").format("D[d]HH:mm:ss");