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 e29b44f0a40 Show expected duration based on historical avg in DAG Run 
details (#65722)
e29b44f0a40 is described below

commit e29b44f0a4020885750e23619e112f77c1100f2b
Author: Software Developer <[email protected]>
AuthorDate: Thu May 14 16:51:42 2026 +0200

    Show expected duration based on historical avg in DAG Run details (#65722)
    
    * *add new `expected_duration` field.
    
    * *address PR comments
    
    * *fix build pipeline.
    
    * *address PR comments.
    
    * *add missing files
---
 .../api_fastapi/core_api/datamodels/ui/dag_runs.py | 17 +++++
 .../api_fastapi/core_api/openapi/_private_ui.yaml  | 86 ++++++++++++++++++++++
 .../api_fastapi/core_api/routes/public/dag_run.py  |  1 -
 .../api_fastapi/core_api/routes/ui/__init__.py     |  2 +
 .../api_fastapi/core_api/routes/ui/dag_runs.py     | 63 ++++++++++++++++
 .../api_fastapi/core_api/services/ui/dag_run.py    | 59 +++++++++++++++
 .../src/airflow/ui/openapi-gen/queries/common.ts   |  7 ++
 .../ui/openapi-gen/queries/ensureQueryData.ts      | 13 ++++
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts | 13 ++++
 .../src/airflow/ui/openapi-gen/queries/queries.ts  | 13 ++++
 .../src/airflow/ui/openapi-gen/queries/suspense.ts | 13 ++++
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 59 +++++++++++++++
 .../ui/openapi-gen/requests/services.gen.ts        | 26 ++++++-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   | 45 +++++++++++
 .../airflow/ui/public/i18n/locales/ar/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/ca/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/de/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/el/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/en/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/es/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/fr/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/he/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/hi/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/hu/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/it/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/ja/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/ko/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/nl/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/pl/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/pt/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/ru/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/th/common.json  |  5 ++
 .../airflow/ui/public/i18n/locales/tr/common.json  |  5 ++
 .../ui/public/i18n/locales/zh-CN/common.json       |  5 ++
 .../ui/public/i18n/locales/zh-TW/common.json       |  5 ++
 .../src/airflow/ui/src/pages/Run/Details.tsx       | 29 +++++++-
 36 files changed, 547 insertions(+), 4 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dag_runs.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dag_runs.py
index 6deaf958f42..b08b3beee58 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dag_runs.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dag_runs.py
@@ -41,3 +41,20 @@ class DAGRunLightResponse(BaseModel):
         if self.end_date and self.start_date:
             return (self.end_date - self.start_date).total_seconds()
         return None
+
+
+class DurationStats(BaseModel):
+    """Duration statistics for a DAG across historical runs."""
+
+    mean: float
+    mode: float | None
+    p50: float
+    p90: float
+    p95: float
+    p99: float
+
+
+class DagRunStatsResponse(BaseModel):
+    """DAG Run statistics serializer for responses."""
+
+    duration: DurationStats | None
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 2983263bbc5..52b8100a944 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -100,6 +100,49 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
+  /ui/dags/{dag_id}/dagRuns/{dag_run_id}/stats:
+    get:
+      tags:
+      - DagRun
+      summary: Get Dag Run Stats
+      description: Get duration statistics for a DAG based on its historical 
completed
+        runs.
+      operationId: get_dag_run_stats
+      security:
+      - OAuth2PasswordBearer: []
+      - HTTPBearer: []
+      parameters:
+      - name: dag_id
+        in: path
+        required: true
+        schema:
+          type: string
+          title: Dag Id
+      - name: dag_run_id
+        in: path
+        required: true
+        schema:
+          type: string
+          title: Dag Run Id
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/DagRunStatsResponse'
+        '404':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Not Found
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
   /ui/partitioned_dag_runs:
     get:
       tags:
@@ -2319,6 +2362,17 @@ components:
         so please ensure that their values always match the ones with the
 
         same name in TaskInstanceState.'
+    DagRunStatsResponse:
+      properties:
+        duration:
+          anyOf:
+          - $ref: '#/components/schemas/DurationStats'
+          - type: 'null'
+      type: object
+      required:
+      - duration
+      title: DagRunStatsResponse
+      description: DAG Run statistics serializer for responses.
     DagRunType:
       type: string
       enum:
@@ -2522,6 +2576,38 @@ components:
       - dag_run_id
       title: DeadlineResponse
       description: Deadline serializer for responses.
+    DurationStats:
+      properties:
+        mean:
+          type: number
+          title: Mean
+        mode:
+          anyOf:
+          - type: number
+          - type: 'null'
+          title: Mode
+        p50:
+          type: number
+          title: P50
+        p90:
+          type: number
+          title: P90
+        p95:
+          type: number
+          title: P95
+        p99:
+          type: number
+          title: P99
+      type: object
+      required:
+      - mean
+      - mode
+      - p50
+      - p90
+      - p95
+      - p99
+      title: DurationStats
+      description: Duration statistics for a DAG across historical runs.
     EdgeResponse:
       properties:
         source_id:
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
index b6896b4278b..0e3670409b4 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py
@@ -131,7 +131,6 @@ def get_dag_run(dag_id: str, dag_run_id: str, session: 
SessionDep) -> DAGRunResp
             status.HTTP_404_NOT_FOUND,
             f"The DagRun with dag_id: `{dag_id}` and run_id: `{dag_run_id}` 
was not found",
         )
-
     return dag_run
 
 
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py
index 6bce499e38c..f1780bfffb9 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/__init__.py
@@ -23,6 +23,7 @@ from airflow.api_fastapi.core_api.routes.ui.backfills import 
backfills_router
 from airflow.api_fastapi.core_api.routes.ui.calendar import calendar_router
 from airflow.api_fastapi.core_api.routes.ui.config import config_router
 from airflow.api_fastapi.core_api.routes.ui.connections import 
connections_router
+from airflow.api_fastapi.core_api.routes.ui.dag_runs import dag_runs_router
 from airflow.api_fastapi.core_api.routes.ui.dags import dags_router
 from airflow.api_fastapi.core_api.routes.ui.dashboard import dashboard_router
 from airflow.api_fastapi.core_api.routes.ui.deadlines import deadlines_router
@@ -37,6 +38,7 @@ ui_router = AirflowRouter(prefix="/ui", 
include_in_schema=False)
 
 ui_router.include_router(auth_router)
 ui_router.include_router(assets_router)
+ui_router.include_router(dag_runs_router)
 ui_router.include_router(partitioned_dag_runs_router)
 ui_router.include_router(config_router)
 ui_router.include_router(connections_router)
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dag_runs.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dag_runs.py
new file mode 100644
index 00000000000..a3529423cd2
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dag_runs.py
@@ -0,0 +1,63 @@
+# 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.
+
+from __future__ import annotations
+
+from fastapi import Depends, HTTPException, status
+from sqlalchemy import select
+
+from airflow.api_fastapi.auth.managers.models.resource_details import 
DagAccessEntity
+from airflow.api_fastapi.common.db.common import SessionDep
+from airflow.api_fastapi.common.router import AirflowRouter
+from airflow.api_fastapi.core_api.datamodels.ui.dag_runs import 
DagRunStatsResponse
+from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
+from airflow.api_fastapi.core_api.security import requires_access_dag
+from airflow.api_fastapi.core_api.services.ui.dag_run import 
compute_duration_stats
+from airflow.models.dagrun import DagRun
+from airflow.utils.state import DagRunState
+
+dag_runs_router = AirflowRouter(prefix="/dags/{dag_id}/dagRuns", 
tags=["DagRun"])
+
+
+@dag_runs_router.get(
+    "/{dag_run_id}/stats",
+    responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
+    dependencies=[Depends(requires_access_dag(method="GET", 
access_entity=DagAccessEntity.RUN))],
+)
+def get_dag_run_stats(dag_id: str, dag_run_id: str, session: SessionDep) -> 
DagRunStatsResponse:
+    """Get duration statistics for a DAG based on its historical completed 
runs."""
+    if not session.scalar(select(DagRun.id).filter_by(dag_id=dag_id, 
run_id=dag_run_id)):
+        raise HTTPException(
+            status.HTTP_404_NOT_FOUND,
+            f"The DagRun with dag_id: `{dag_id}` and run_id: `{dag_run_id}` 
was not found",
+        )
+
+    durations = [
+        d
+        for d in session.scalars(
+            select(DagRun.duration.expression)  # type: ignore[attr-defined]
+            .where(
+                DagRun.dag_id == dag_id,
+                DagRun.state.in_([DagRunState.SUCCESS, DagRunState.FAILED]),
+            )
+            .order_by(DagRun.run_after.desc())
+            .limit(100)
+        )
+        if d is not None
+    ]
+
+    return DagRunStatsResponse(duration=compute_duration_stats(durations))
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dag_run.py 
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dag_run.py
new file mode 100644
index 00000000000..a9ba75417c8
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dag_run.py
@@ -0,0 +1,59 @@
+# 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.
+
+from __future__ import annotations
+
+import statistics
+from collections import Counter
+
+from airflow.api_fastapi.core_api.datamodels.ui.dag_runs import DurationStats
+
+
+def compute_duration_stats(durations: list[float]) -> DurationStats | None:
+    """
+    Compute duration statistics from a list of completed DAG run durations (in 
seconds).
+
+    Returns None when the list is empty (no completed runs exist yet).
+    Mode is computed on second-rounded values to avoid float precision noise; 
returns None
+    when every run has a unique duration.
+    Percentiles use linear interpolation between adjacent sorted values.
+    """
+    if not durations:
+        return None
+
+    sorted_d = sorted(durations)
+
+    counts = Counter(round(d) for d in sorted_d)
+    max_count = max(counts.values())
+    mode_val: float | None = (
+        float(min(k for k, v in counts.items() if v == max_count)) if 
max_count > 1 else None
+    )
+
+    def _percentile(p: float) -> float:
+        idx = (len(sorted_d) - 1) * p / 100
+        lo = int(idx)
+        hi = min(lo + 1, len(sorted_d) - 1)
+        return sorted_d[lo] + (sorted_d[hi] - sorted_d[lo]) * (idx - lo)
+
+    return DurationStats(
+        mean=round(statistics.mean(sorted_d), 3),
+        mode=round(mode_val, 3) if mode_val is not None else None,
+        p50=round(_percentile(50), 3),
+        p90=round(_percentile(90), 3),
+        p95=round(_percentile(95), 3),
+        p99=round(_percentile(99), 3),
+    )
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index e0976d31b72..0e44f9f63bd 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -202,6 +202,13 @@ export const UseDagRunServiceWaitDagRunUntilFinishedKeyFn 
= ({ dagId, dagRunId,
   interval: number;
   result?: string[];
 }, queryKey?: Array<unknown>) => [useDagRunServiceWaitDagRunUntilFinishedKey, 
...(queryKey ?? [{ dagId, dagRunId, interval, result }])];
+export type DagRunServiceGetDagRunStatsDefaultResponse = 
Awaited<ReturnType<typeof DagRunService.getDagRunStats>>;
+export type DagRunServiceGetDagRunStatsQueryResult<TData = 
DagRunServiceGetDagRunStatsDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
+export const useDagRunServiceGetDagRunStatsKey = "DagRunServiceGetDagRunStats";
+export const UseDagRunServiceGetDagRunStatsKeyFn = ({ dagId, dagRunId }: {
+  dagId: string;
+  dagRunId: string;
+}, queryKey?: Array<unknown>) => [useDagRunServiceGetDagRunStatsKey, 
...(queryKey ?? [{ dagId, dagRunId }])];
 export type ExperimentalServiceWaitDagRunUntilFinishedDefaultResponse = 
Awaited<ReturnType<typeof ExperimentalService.waitDagRunUntilFinished>>;
 export type ExperimentalServiceWaitDagRunUntilFinishedQueryResult<TData = 
ExperimentalServiceWaitDagRunUntilFinishedDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useExperimentalServiceWaitDagRunUntilFinishedKey = 
"ExperimentalServiceWaitDagRunUntilFinished";
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 5e0595ec8f6..bf9087edc65 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -408,6 +408,19 @@ export const 
ensureUseDagRunServiceWaitDagRunUntilFinishedData = (queryClient: Q
   result?: string[];
 }) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagRunServiceWaitDagRunUntilFinishedKeyFn({ dagId, dagRunId, 
interval, result }), queryFn: () => DagRunService.waitDagRunUntilFinished({ 
dagId, dagRunId, interval, result }) });
 /**
+* Get Dag Run Stats
+* Get duration statistics for a DAG based on its historical completed runs.
+* @param data The data for the request.
+* @param data.dagId
+* @param data.dagRunId
+* @returns DagRunStatsResponse Successful Response
+* @throws ApiError
+*/
+export const ensureUseDagRunServiceGetDagRunStatsData = (queryClient: 
QueryClient, { dagId, dagRunId }: {
+  dagId: string;
+  dagRunId: string;
+}) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagRunServiceGetDagRunStatsKeyFn({ dagId, dagRunId }), queryFn: () => 
DagRunService.getDagRunStats({ dagId, dagRunId }) });
+/**
 * Experimental: Wait for a dag run to complete, and return task results if 
requested.
 * 🚧 This is an experimental endpoint and may change or be removed without 
notice.Successful response are streamed as newline-delimited JSON (NDJSON). 
Each line is a JSON object representing the Dag run state.
 * @param data The data for the request.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index 2a17cdfefc0..18dc0374565 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -408,6 +408,19 @@ export const 
prefetchUseDagRunServiceWaitDagRunUntilFinished = (queryClient: Que
   result?: string[];
 }) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagRunServiceWaitDagRunUntilFinishedKeyFn({ dagId, dagRunId, 
interval, result }), queryFn: () => DagRunService.waitDagRunUntilFinished({ 
dagId, dagRunId, interval, result }) });
 /**
+* Get Dag Run Stats
+* Get duration statistics for a DAG based on its historical completed runs.
+* @param data The data for the request.
+* @param data.dagId
+* @param data.dagRunId
+* @returns DagRunStatsResponse Successful Response
+* @throws ApiError
+*/
+export const prefetchUseDagRunServiceGetDagRunStats = (queryClient: 
QueryClient, { dagId, dagRunId }: {
+  dagId: string;
+  dagRunId: string;
+}) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagRunServiceGetDagRunStatsKeyFn({ dagId, dagRunId }), queryFn: () => 
DagRunService.getDagRunStats({ dagId, dagRunId }) });
+/**
 * Experimental: Wait for a dag run to complete, and return task results if 
requested.
 * 🚧 This is an experimental endpoint and may change or be removed without 
notice.Successful response are streamed as newline-delimited JSON (NDJSON). 
Each line is a JSON object representing the Dag run state.
 * @param data The data for the request.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 52e4776d510..dc43dd038af 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -408,6 +408,19 @@ export const useDagRunServiceWaitDagRunUntilFinished = 
<TData = Common.DagRunSer
   result?: string[];
 }, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseDagRunServiceWaitDagRunUntilFinishedKeyFn({ dagId, dagRunId, 
interval, result }, queryKey), queryFn: () => 
DagRunService.waitDagRunUntilFinished({ dagId, dagRunId, interval, result }) as 
TData, ...options });
 /**
+* Get Dag Run Stats
+* Get duration statistics for a DAG based on its historical completed runs.
+* @param data The data for the request.
+* @param data.dagId
+* @param data.dagRunId
+* @returns DagRunStatsResponse Successful Response
+* @throws ApiError
+*/
+export const useDagRunServiceGetDagRunStats = <TData = 
Common.DagRunServiceGetDagRunStatsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ dagId, dagRunId }: {
+  dagId: string;
+  dagRunId: string;
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseDagRunServiceGetDagRunStatsKeyFn({ dagId, dagRunId }, queryKey), 
queryFn: () => DagRunService.getDagRunStats({ dagId, dagRunId }) as TData, 
...options });
+/**
 * Experimental: Wait for a dag run to complete, and return task results if 
requested.
 * 🚧 This is an experimental endpoint and may change or be removed without 
notice.Successful response are streamed as newline-delimited JSON (NDJSON). 
Each line is a JSON object representing the Dag run state.
 * @param data The data for the request.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 9d42191b886..9fdc0792870 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -408,6 +408,19 @@ export const 
useDagRunServiceWaitDagRunUntilFinishedSuspense = <TData = Common.D
   result?: string[];
 }, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseDagRunServiceWaitDagRunUntilFinishedKeyFn({ dagId, dagRunId, 
interval, result }, queryKey), queryFn: () => 
DagRunService.waitDagRunUntilFinished({ dagId, dagRunId, interval, result }) as 
TData, ...options });
 /**
+* Get Dag Run Stats
+* Get duration statistics for a DAG based on its historical completed runs.
+* @param data The data for the request.
+* @param data.dagId
+* @param data.dagRunId
+* @returns DagRunStatsResponse Successful Response
+* @throws ApiError
+*/
+export const useDagRunServiceGetDagRunStatsSuspense = <TData = 
Common.DagRunServiceGetDagRunStatsDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ dagId, dagRunId }: {
+  dagId: string;
+  dagRunId: string;
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseDagRunServiceGetDagRunStatsKeyFn({ dagId, dagRunId }, queryKey), 
queryFn: () => DagRunService.getDagRunStats({ dagId, dagRunId }) as TData, 
...options });
+/**
 * Experimental: Wait for a dag run to complete, and return task results if 
requested.
 * 🚧 This is an experimental endpoint and may change or be removed without 
notice.Successful response are streamed as newline-delimited JSON (NDJSON). 
Each line is a JSON object representing the Dag run state.
 * @param data The data for the request.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index bc4c7179c2d..99b1c493455 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -8097,6 +8097,25 @@ export const $DAGWithLatestDagRunsResponse = {
     description: 'DAG with latest dag runs response serializer.'
 } as const;
 
+export const $DagRunStatsResponse = {
+    properties: {
+        duration: {
+            anyOf: [
+                {
+                    '$ref': '#/components/schemas/DurationStats'
+                },
+                {
+                    type: 'null'
+                }
+            ]
+        }
+    },
+    type: 'object',
+    required: ['duration'],
+    title: 'DagRunStatsResponse',
+    description: 'DAG Run statistics serializer for responses.'
+} as const;
+
 export const $DashboardDagStatsResponse = {
     properties: {
         active_dag_count: {
@@ -8260,6 +8279,46 @@ export const $DeadlineResponse = {
     description: 'Deadline serializer for responses.'
 } as const;
 
+export const $DurationStats = {
+    properties: {
+        mean: {
+            type: 'number',
+            title: 'Mean'
+        },
+        mode: {
+            anyOf: [
+                {
+                    type: 'number'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Mode'
+        },
+        p50: {
+            type: 'number',
+            title: 'P50'
+        },
+        p90: {
+            type: 'number',
+            title: 'P90'
+        },
+        p95: {
+            type: 'number',
+            title: 'P95'
+        },
+        p99: {
+            type: 'number',
+            title: 'P99'
+        }
+    },
+    type: 'object',
+    required: ['mean', 'mode', 'p50', 'p90', 'p95', 'p99'],
+    title: 'DurationStats',
+    description: 'Duration statistics for a DAG across historical runs.'
+} as const;
+
 export const $EdgeResponse = {
     properties: {
         source_id: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index 83c5d5c17b7..ec044ac3085 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -3,7 +3,7 @@
 import type { CancelablePromise } from './core/CancelablePromise';
 import { OpenAPI } from './core/OpenAPI';
 import { request as __request } from './core/request';
-import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, 
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, 
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, 
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, 
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, 
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, 
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, 
Dele [...]
+import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, 
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, 
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, 
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, 
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, 
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, 
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, 
Dele [...]
 
 export class AssetService {
     /**
@@ -1207,6 +1207,30 @@ export class DagRunService {
         });
     }
     
+    /**
+     * Get Dag Run Stats
+     * Get duration statistics for a DAG based on its historical completed 
runs.
+     * @param data The data for the request.
+     * @param data.dagId
+     * @param data.dagRunId
+     * @returns DagRunStatsResponse Successful Response
+     * @throws ApiError
+     */
+    public static getDagRunStats(data: GetDagRunStatsData): 
CancelablePromise<GetDagRunStatsResponse> {
+        return __request(OpenAPI, {
+            method: 'GET',
+            url: '/ui/dags/{dag_id}/dagRuns/{dag_run_id}/stats',
+            path: {
+                dag_id: data.dagId,
+                dag_run_id: data.dagRunId
+            },
+            errors: {
+                404: 'Not Found',
+                422: 'Validation Error'
+            }
+        });
+    }
+    
 }
 
 export class ExperimentalService {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 5b216da03a5..4d401bfc1a4 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1998,6 +1998,13 @@ export type DAGWithLatestDagRunsResponse = {
     readonly file_token: string;
 };
 
+/**
+ * DAG Run statistics serializer for responses.
+ */
+export type DagRunStatsResponse = {
+    duration: DurationStats | null;
+};
+
 /**
  * Dashboard DAG Stats serializer for responses.
  */
@@ -2052,6 +2059,18 @@ export type DeadlineResponse = {
     alert_name?: string | null;
 };
 
+/**
+ * Duration statistics for a DAG across historical runs.
+ */
+export type DurationStats = {
+    mean: number;
+    mode: number | null;
+    p50: number;
+    p90: number;
+    p95: number;
+    p99: number;
+};
+
 /**
  * Edge serializer for responses.
  */
@@ -2806,6 +2825,13 @@ export type GetListDagRunsBatchData = {
 
 export type GetListDagRunsBatchResponse = DAGRunCollectionResponse;
 
+export type GetDagRunStatsData = {
+    dagId: string;
+    dagRunId: string;
+};
+
+export type GetDagRunStatsResponse = DagRunStatsResponse;
+
 export type GetDagSourceData = {
     accept?: 'application/json' | 'text/plain' | '*/*';
     dagId: string;
@@ -5181,6 +5207,25 @@ export type $OpenApiTs = {
             };
         };
     };
+    '/ui/dags/{dag_id}/dagRuns/{dag_run_id}/stats': {
+        get: {
+            req: GetDagRunStatsData;
+            res: {
+                /**
+                 * Successful Response
+                 */
+                200: DagRunStatsResponse;
+                /**
+                 * Not Found
+                 */
+                404: HTTPExceptionResponse;
+                /**
+                 * Validation Error
+                 */
+                422: HTTPValidationError;
+            };
+        };
+    };
     '/api/v2/dagSources/{dag_id}': {
         get: {
             req: GetDagSourceData;
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/ar/common.json
index 8cde4e9adcb..7c9a3afa590 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/common.json
@@ -79,6 +79,11 @@
     "dagVersions": "إصدار(ات) Dag",
     "dataIntervalEnd": "نهاية فترة البيانات",
     "dataIntervalStart": "بداية فترة البيانات",
+    "durationStats": {
+      "mean": "المتوسط",
+      "mode": "المنوال"
+    },
+    "expectedDuration": "المدة المتوقعة",
     "lastSchedulingDecision": "آخر قرار جدولة",
     "partitionKey": "مفتاح التقسيم",
     "queuedAt": "في الطابور في",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/ca/common.json
index cf6da561848..82c1d98b0d4 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/common.json
@@ -61,6 +61,11 @@
     "dagVersions": "Versió/ns del Dag",
     "dataIntervalEnd": "Fi de l'interval de dades",
     "dataIntervalStart": "Inici de l'interval de dades",
+    "durationStats": {
+      "mean": "Mitjana",
+      "mode": "Moda"
+    },
+    "expectedDuration": "Durada esperada",
     "lastSchedulingDecision": "Última decisió de programació",
     "mappedPartitionKey": "Clau de partició mapejada",
     "partitionKey": "Clau de partició",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json
index c2cdf593871..0ee18a06985 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/common.json
@@ -61,6 +61,11 @@
     "dagVersions": "Dag Versionen",
     "dataIntervalEnd": "Datenintervall Ende",
     "dataIntervalStart": "Datenintervall Start",
+    "durationStats": {
+      "mean": "Mittelwert",
+      "mode": "Modus"
+    },
+    "expectedDuration": "Erwartete mittlere Laufzeit",
     "lastSchedulingDecision": "Letzte Planungsentscheidung",
     "mappedPartitionKey": "Zugeordneter Partitionsschlüssel",
     "partitionKey": "Partitionsschlüssel",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json
index 222e52eeeb9..78d468e1d16 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/common.json
@@ -59,6 +59,11 @@
     "dagVersions": "Έκδοση(εις) Dag",
     "dataIntervalEnd": "Τέλος Διαστήματος Δεδομένων",
     "dataIntervalStart": "Έναρξη Διαστήματος Δεδομένων",
+    "durationStats": {
+      "mean": "Μέσος Όρος",
+      "mode": "Επικρατούσα Τιμή"
+    },
+    "expectedDuration": "Αναμενόμενη Διάρκεια",
     "lastSchedulingDecision": "Τελευταία Απόφαση Προγραμματισμού",
     "queuedAt": "Σε Ουρά Στις",
     "runAfter": "Εκτέλεση Μετά",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
index 36b98ce1c3c..2ec9c14e334 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
@@ -61,6 +61,11 @@
     "dagVersions": "Dag Version(s)",
     "dataIntervalEnd": "Data Interval End",
     "dataIntervalStart": "Data Interval Start",
+    "durationStats": {
+      "mean": "Mean",
+      "mode": "Mode"
+    },
+    "expectedDuration": "Expected Duration",
     "lastSchedulingDecision": "Last Scheduling Decision",
     "mappedPartitionKey": "Mapped Partition key",
     "partitionKey": "Partition key",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/es/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/es/common.json
index e9b901a4011..864713ca67e 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/es/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/es/common.json
@@ -64,6 +64,11 @@
     "dagVersions": "Versión(es) del Dag",
     "dataIntervalEnd": "Intervalo de Datos Final",
     "dataIntervalStart": "Intervalo de Datos Inicial",
+    "durationStats": {
+      "mean": "Media",
+      "mode": "Moda"
+    },
+    "expectedDuration": "Duración Esperada",
     "lastSchedulingDecision": "Última Decisión de Programación",
     "queuedAt": "En Cola en",
     "runAfter": "Ejecutar Después",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/fr/common.json
index 96650ff9400..ae18453bdb7 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/common.json
@@ -64,6 +64,11 @@
     "dagVersions": "Version(s) du Dag",
     "dataIntervalEnd": "Fin de l'intervalle de données",
     "dataIntervalStart": "Début de l'intervalle de données",
+    "durationStats": {
+      "mean": "Moyenne",
+      "mode": "Mode"
+    },
+    "expectedDuration": "Durée Attendue",
     "lastSchedulingDecision": "Dernière décision de planification",
     "queuedAt": "Mis en file à",
     "runAfter": "Exécuté après",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/he/common.json
index afcc27a3533..09096951b48 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/he/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/common.json
@@ -64,6 +64,11 @@
     "dagVersions": "גרסאות Dag",
     "dataIntervalEnd": "סיום מקטע נתונים",
     "dataIntervalStart": "תחילת מקטע נתונים",
+    "durationStats": {
+      "mean": "ממוצע",
+      "mode": "שכיח"
+    },
+    "expectedDuration": "משך זמן צפוי",
     "lastSchedulingDecision": "החלטת תזמון אחרונה",
     "partitionKey": "מפתח חלוקה",
     "queuedAt": "זמן כניסה לתור",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json
index 39a83b8a016..496f80083e5 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/hi/common.json
@@ -60,6 +60,11 @@
     "dagVersions": "डैग संस्करण",
     "dataIntervalEnd": "डेटा अंतराल अंत",
     "dataIntervalStart": "डेटा अंतराल प्रारंभ",
+    "durationStats": {
+      "mean": "माध्य",
+      "mode": "बहुलक"
+    },
+    "expectedDuration": "अपेक्षित अवधि",
     "lastSchedulingDecision": "अंतिम शेड्यूलिंग निर्णय",
     "mappedPartitionKey": "मैप्ड पार्टीशन कुंजी",
     "partitionKey": "पार्टीशन कुंजी",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json
index 123fbcc5589..4a083a44c78 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/common.json
@@ -60,6 +60,11 @@
     "dagVersions": "Dag verzió(k)",
     "dataIntervalEnd": "Adatintervallum vége",
     "dataIntervalStart": "Adatintervallum kezdete",
+    "durationStats": {
+      "mean": "Átlag",
+      "mode": "Módusz"
+    },
+    "expectedDuration": "Várható Időtartam",
     "lastSchedulingDecision": "Utolsó ütemezési döntés",
     "mappedPartitionKey": "Leképezett partíciós kulcs",
     "partitionKey": "Partíciós kulcs",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/it/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/it/common.json
index a59d9ed72ab..acd57d21362 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/it/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/it/common.json
@@ -69,6 +69,11 @@
     "dagVersions": "Versioni del Dag",
     "dataIntervalEnd": "Data Intervallo Fine",
     "dataIntervalStart": "Data Intervallo Inizio",
+    "durationStats": {
+      "mean": "Media",
+      "mode": "Moda"
+    },
+    "expectedDuration": "Durata Prevista",
     "lastSchedulingDecision": "Ultima Decisione di Programmazione",
     "queuedAt": "In Coda il",
     "runAfter": "Esegui dopo",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ja/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/ja/common.json
index 7d6377e82ae..1ec92cf9c30 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ja/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ja/common.json
@@ -59,6 +59,11 @@
     "dagVersions": "Dag バージョン",
     "dataIntervalEnd": "データ期間の終了",
     "dataIntervalStart": "データ期間の開始",
+    "durationStats": {
+      "mean": "平均",
+      "mode": "最頻値"
+    },
+    "expectedDuration": "予想所要時間",
     "lastSchedulingDecision": "最終スケジューリング決定時刻",
     "partitionKey": "パーティションキー",
     "queuedAt": "キュー登録時刻",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/ko/common.json
index 3d3eff5e34d..9e60dc858a9 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/common.json
@@ -61,6 +61,11 @@
     "dagVersions": "Dag 버전",
     "dataIntervalEnd": "데이터 구간 종료",
     "dataIntervalStart": "데이터 구간 시작",
+    "durationStats": {
+      "mean": "평균",
+      "mode": "최빈값"
+    },
+    "expectedDuration": "예상 소요 시간",
     "lastSchedulingDecision": "마지막 스케줄링 결정",
     "mappedPartitionKey": "매핑된 파티션 키",
     "partitionKey": "파티션 키",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/nl/common.json
index a64972b9811..eed0ee70048 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/common.json
@@ -61,6 +61,11 @@
     "dagVersions": "Dag Versie(s)",
     "dataIntervalEnd": "Data interval einde",
     "dataIntervalStart": "Data interval begin",
+    "durationStats": {
+      "mean": "Gemiddelde",
+      "mode": "Modus"
+    },
+    "expectedDuration": "Verwachte Duur",
     "lastSchedulingDecision": "Laatste planningsbeslissing",
     "mappedPartitionKey": "Gemapte partitie sleutel",
     "partitionKey": "Partitie sleutel",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json
index 5949dc2aeb1..4ceb26f98c1 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/common.json
@@ -71,6 +71,11 @@
     "dagVersions": "Wersje Daga",
     "dataIntervalEnd": "Koniec interwału danych",
     "dataIntervalStart": "Początek interwału danych",
+    "durationStats": {
+      "mean": "Średnia",
+      "mode": "Dominanta"
+    },
+    "expectedDuration": "Oczekiwany Czas Trwania",
     "lastSchedulingDecision": "Ostatnia decyzja harmonogramu",
     "mappedPartitionKey": "Zmapowany klucz partycji",
     "partitionKey": "Klucz partycji",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json
index 3fc89ddeb2e..eba55971775 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json
@@ -70,6 +70,11 @@
     "dagVersions": "Versão(s) do Dag",
     "dataIntervalEnd": "Fim do Intervalo de Dados",
     "dataIntervalStart": "Início do Intervalo de Dados",
+    "durationStats": {
+      "mean": "Média",
+      "mode": "Moda"
+    },
+    "expectedDuration": "Duração Esperada",
     "lastSchedulingDecision": "Última Decisão de Agendamento",
     "mappedPartitionKey": "Chave de partição mapeada",
     "partitionKey": "Chave de partição",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ru/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/ru/common.json
index 147626f6c69..be8f4c20396 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ru/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ru/common.json
@@ -60,6 +60,11 @@
     "dagVersions": "Версии Dag-а",
     "dataIntervalEnd": "Конец временного интервала",
     "dataIntervalStart": "Начало временного интервала",
+    "durationStats": {
+      "mean": "Среднее",
+      "mode": "Мода"
+    },
+    "expectedDuration": "Ожидаемая Длительность",
     "lastSchedulingDecision": "Последнее решение о расписании",
     "mappedPartitionKey": "Ключ распределенной части",
     "partitionKey": "Ключ части",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json
index 62b089c1fe2..5c461952e37 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/th/common.json
@@ -59,6 +59,11 @@
     "dagVersions": "เวอร์ชันของ Dag",
     "dataIntervalEnd": "สิ้นสุดช่วงข้อมูล",
     "dataIntervalStart": "เริ่มต้นช่วงข้อมูล",
+    "durationStats": {
+      "mean": "ค่าเฉลี่ย",
+      "mode": "ฐานนิยม"
+    },
+    "expectedDuration": "ระยะเวลาที่คาดหวัง",
     "lastSchedulingDecision": "การตัดสินใจกำหนดเวลาล่าสุด",
     "queuedAt": "เข้าคิวเมื่อ",
     "runAfter": "ทำงานหลังจาก",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/tr/common.json
index ab2b9731808..b1540655678 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/common.json
@@ -60,6 +60,11 @@
     "dagVersions": "Dag Versiyonu(ları)",
     "dataIntervalEnd": "Veri Aralığı Sonu",
     "dataIntervalStart": "Veri Aralığı Başlangıcı",
+    "durationStats": {
+      "mean": "Ortalama",
+      "mode": "Tepe Değer"
+    },
+    "expectedDuration": "Beklenen Süre",
     "lastSchedulingDecision": "Son Zamanlama Kararı",
     "mappedPartitionKey": "Haritalanmış Bölüm Anahtarı",
     "partitionKey": "Bölüm Anahtarı",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json
index c5a0d51df0c..1fe6307f6fb 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-CN/common.json
@@ -60,6 +60,11 @@
     "dagVersions": "Dag 版本",
     "dataIntervalEnd": "数据区间结束",
     "dataIntervalStart": "数据区间起始",
+    "durationStats": {
+      "mean": "平均值",
+      "mode": "众数"
+    },
+    "expectedDuration": "预计耗时",
     "lastSchedulingDecision": "最后调度决策",
     "mappedPartitionKey": "映射分区键",
     "partitionKey": "分区键",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
index 8f9d029f12a..39fb1784217 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json
@@ -61,6 +61,11 @@
     "dagVersions": "Dag 版本",
     "dataIntervalEnd": "資料區間結束",
     "dataIntervalStart": "資料區間起始",
+    "durationStats": {
+      "mean": "平均值",
+      "mode": "眾數"
+    },
+    "expectedDuration": "預計時長",
     "lastSchedulingDecision": "最後排程決策",
     "mappedPartitionKey": "映射分區鍵",
     "partitionKey": "資產分區鍵",
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 da23bd4b095..e233c528b54 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx
@@ -20,14 +20,14 @@ import { Flex, HStack, StackSeparator, Table, Text, VStack 
} from "@chakra-ui/re
 import { useTranslation } from "react-i18next";
 import { useParams } from "react-router-dom";
 
-import { useDagRunServiceGetDagRun } from "openapi/queries";
+import { useDagRunServiceGetDagRun, useDagRunServiceGetDagRunStats } from 
"openapi/queries";
 import { DagVersionDetails } from "src/components/DagVersionDetails";
 import RenderedJsonField from "src/components/RenderedJsonField";
 import { RunTypeIcon } from "src/components/RunTypeIcon";
 import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 import { ClipboardRoot, ClipboardIconButton } from "src/components/ui";
-import { getDuration, isStatePending, useAutoRefresh } from "src/utils";
+import { getDuration, isStatePending, renderDuration, useAutoRefresh } from 
"src/utils";
 
 export const Details = () => {
   const { t: translate } = useTranslation(["common", "components"]);
@@ -44,6 +44,8 @@ export const Details = () => {
     { refetchInterval: (query) => (isStatePending(query.state.data?.state) ? 
refetchInterval : false) },
   );
 
+  const { data: dagRunStats } = useDagRunServiceGetDagRunStats({ dagId, 
dagRunId: runId });
+
   if (!dagRun) {
     return undefined;
   }
@@ -84,6 +86,29 @@ export const Details = () => {
           <Table.Cell>{translate("duration")}</Table.Cell>
           <Table.Cell>{getDuration(dagRun.start_date, 
dagRun.end_date)}</Table.Cell>
         </Table.Row>
+        {dagRunStats?.duration ? (
+          <Table.Row>
+            <Table.Cell>{translate("dagRun.expectedDuration")}</Table.Cell>
+            <Table.Cell>
+              <VStack align="start" gap={1}>
+                <Text>
+                  {translate("dagRun.durationStats.mean")}: 
{renderDuration(dagRunStats.duration.mean)}
+                </Text>
+                {dagRunStats.duration.mode !== null && (
+                  <Text>
+                    {translate("dagRun.durationStats.mode")}: 
{renderDuration(dagRunStats.duration.mode)}
+                  </Text>
+                )}
+                {/* eslint-disable i18next/no-literal-string -- P-values are 
technical abbreviations not subject to translation */}
+                <Text>P50: {renderDuration(dagRunStats.duration.p50)}</Text>
+                <Text>P90: {renderDuration(dagRunStats.duration.p90)}</Text>
+                <Text>P95: {renderDuration(dagRunStats.duration.p95)}</Text>
+                <Text>P99: {renderDuration(dagRunStats.duration.p99)}</Text>
+                {/* eslint-enable i18next/no-literal-string */}
+              </VStack>
+            </Table.Cell>
+          </Table.Row>
+        ) : undefined}
         <Table.Row>
           <Table.Cell>{translate("dagRun.lastSchedulingDecision")}</Table.Cell>
           <Table.Cell>

Reply via email to