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>