diff --git a/airflow-core/newsfragments/67586.significant.rst b/airflow-core/newsfragments/67586.significant.rst
new file mode 100644
index 0000000000000..4a1c2afb999fe
--- /dev/null
+++ b/airflow-core/newsfragments/67586.significant.rst
@@ -0,0 +1,4 @@
+Add a new **Deadlines** page under the Browse menu.
+
+The page is accessible to any role that already has ``can_read`` and
+``menu_access`` on ``DAG Runs``.
diff --git a/airflow-core/src/airflow/api_fastapi/common/types.py b/airflow-core/src/airflow/api_fastapi/common/types.py
index 7d2a944c82228..462a0278b4d44 100644
--- a/airflow-core/src/airflow/api_fastapi/common/types.py
+++ b/airflow-core/src/airflow/api_fastapi/common/types.py
@@ -120,6 +120,7 @@ class MenuItem(Enum):
CONFIG = "Config"
CONNECTIONS = "Connections"
DAGS = "Dags"
+ DEADLINES = "Deadlines"
DOCS = "Docs"
JOBS = "Jobs"
PLUGINS = "Plugins"
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 52a09dcbb7b8c..e9d280855b529 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
@@ -3144,6 +3144,7 @@ components:
- Config
- Connections
- Dags
+ - Deadlines
- Docs
- Jobs
- Plugins
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 e1a6cc4790dd0..d280c500275ef 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
@@ -9257,7 +9257,7 @@ export const $LightGridTaskInstanceSummary = {
export const $MenuItem = {
type: 'string',
- enum: ['Required Actions', 'Assets', 'Audit Log', 'Config', 'Connections', 'Dags', 'Docs', 'Jobs', 'Plugins', 'Pools', 'Providers', 'Variables', 'XComs'],
+ enum: ['Required Actions', 'Assets', 'Audit Log', 'Config', 'Connections', 'Dags', 'Deadlines', 'Docs', 'Jobs', 'Plugins', 'Pools', 'Providers', 'Variables', 'XComs'],
title: 'MenuItem',
description: 'Define all menu items defined in the menu.'
} as const;
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 2fd1f98417940..11b41b04e2617 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
@@ -2337,7 +2337,7 @@ export type LightGridTaskInstanceSummary = {
/**
* Define all menu items defined in the menu.
*/
-export type MenuItem = 'Required Actions' | 'Assets' | 'Audit Log' | 'Config' | 'Connections' | 'Dags' | 'Docs' | 'Jobs' | 'Plugins' | 'Pools' | 'Providers' | 'Variables' | 'XComs';
+export type MenuItem = 'Required Actions' | 'Assets' | 'Audit Log' | 'Config' | 'Connections' | 'Dags' | 'Deadlines' | 'Docs' | 'Jobs' | 'Plugins' | 'Pools' | 'Providers' | 'Variables' | 'XComs';
/**
* Menu Item Collection serializer for responses.
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json
index d92d59b9272c8..6c6b1016b5b6d 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json
@@ -11,6 +11,24 @@
},
"title": "Audit Log"
},
+ "deadlines": {
+ "columns": {
+ "alertName": "Alert Name",
+ "deadlineTime": "Deadline Time",
+ "status": "Status"
+ },
+ "deadline_one": "Deadline",
+ "deadline_other": "Deadlines",
+ "filters": {
+ "status": "Status",
+ "statusOptions": {
+ "all": "All",
+ "missed": "Missed",
+ "pending": "Pending"
+ }
+ },
+ "title": "Deadlines"
+ },
"xcom": {
"add": {
"error": "Failed to add XCom",
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 4e87c45e30513..7b370ca2d45a4 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
@@ -22,6 +22,7 @@
"backfill_other": "Backfills",
"browse": {
"auditLog": "Audit Log",
+ "deadlines": "Deadlines",
"jobs": "Jobs",
"requiredActions": "Required Actions",
"xcoms": "XComs"
diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
index 7d160ad5999b7..1ce5daa23e660 100644
--- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
+++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
@@ -129,6 +129,13 @@ export const useFilterConfigs = () => {
min: 1,
type: FilterTypes.NUMBER,
},
+ [SearchParamsKeys.DEADLINE_TIME_RANGE]: {
+ endKey: SearchParamsKeys.DEADLINE_TIME_LTE,
+ icon: ,
+ label: translate("browse:deadlines.columns.deadlineTime"),
+ startKey: SearchParamsKeys.DEADLINE_TIME_GTE,
+ type: FilterTypes.DATERANGE,
+ },
[SearchParamsKeys.DURATION_GTE]: {
icon: ,
label: translate("common:filters.durationFrom"),
@@ -209,6 +216,16 @@ export const useFilterConfigs = () => {
min: -1,
type: FilterTypes.NUMBER,
},
+ [SearchParamsKeys.MISSED]: {
+ icon: ,
+ label: translate("browse:deadlines.filters.status"),
+ options: [
+ { label: translate("browse:deadlines.filters.statusOptions.all"), value: "" },
+ { label: translate("browse:deadlines.filters.statusOptions.pending"), value: "false" },
+ { label: translate("browse:deadlines.filters.statusOptions.missed"), value: "true" },
+ ],
+ type: FilterTypes.SELECT,
+ },
[SearchParamsKeys.NAME_PATTERN]: {
hotkeyDisabled: true,
icon: ,
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index b49d1cad846ab..f3583d449ced7 100644
--- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
@@ -33,6 +33,9 @@ export enum SearchParamsKeys {
DAG_ID_PATTERN = "dag_id_pattern",
DAG_VERSION = "dag_version",
DAG_VIEW = "view",
+ DEADLINE_TIME_GTE = "deadline_time_gte",
+ DEADLINE_TIME_LTE = "deadline_time_lte",
+ DEADLINE_TIME_RANGE = "deadline_time_range",
DEPENDENCIES = "dependencies",
DURATION_GTE = "duration_gte",
DURATION_LTE = "duration_lte",
@@ -64,6 +67,7 @@ export enum SearchParamsKeys {
LOGICAL_DATE_RANGE = "logical_date_range",
MAP_INDEX = "map_index",
MAPPED = "mapped",
+ MISSED = "missed",
NAME_PATTERN = "name_pattern",
NEEDS_REVIEW = "needs_review",
OFFSET = "offset",
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx b/airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx
index a9de356ff9d8d..d95f9e0f31815 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/BrowseButton.tsx
@@ -33,6 +33,11 @@ const links = [
key: "auditLog",
title: "Audit Log",
},
+ {
+ href: "/deadlines",
+ key: "deadlines",
+ title: "Deadlines",
+ },
{
href: "/jobs",
key: "jobs",
diff --git a/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx b/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
new file mode 100644
index 0000000000000..720648c4247b0
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
@@ -0,0 +1,156 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Badge, Box, Heading, Link, VStack } from "@chakra-ui/react";
+import type { ColumnDef } from "@tanstack/react-table";
+import type { TFunction } from "i18next";
+import { useTranslation } from "react-i18next";
+import { Link as RouterLink, useSearchParams } from "react-router-dom";
+
+import { useDeadlinesServiceGetDeadlines } from "openapi/queries";
+import type { DeadlineResponse } from "openapi/requests/types.gen";
+import { DataTable } from "src/components/DataTable";
+import { useTableURLState } from "src/components/DataTable/useTableUrlState";
+import { ErrorAlert } from "src/components/ErrorAlert";
+import { FilterBar } from "src/components/FilterBar";
+import Time from "src/components/Time";
+import { TruncatedText } from "src/components/TruncatedText";
+import { SearchParamsKeys } from "src/constants/searchParams";
+import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils";
+
+type DeadlineRow = { row: { original: DeadlineResponse } };
+
+const createColumns = (translate: TFunction): Array> => [
+ {
+ accessorKey: "dag_id",
+ cell: ({ row: { original } }: DeadlineRow) => (
+
+
+
+
+
+ ),
+ header: translate("common:dagId"),
+ },
+ {
+ accessorKey: "dag_run_id",
+ cell: ({ row: { original } }: DeadlineRow) => (
+
+
+
+
+
+ ),
+ enableSorting: false,
+ header: translate("common:dagRunId"),
+ },
+ {
+ accessorKey: "deadline_time",
+ cell: ({ row: { original } }: DeadlineRow) => ,
+ header: translate("browse:deadlines.columns.deadlineTime"),
+ },
+ {
+ accessorKey: "missed",
+ cell: ({
+ row: {
+ original: { missed },
+ },
+ }) => (
+
+ {missed
+ ? translate("browse:deadlines.filters.statusOptions.missed")
+ : translate("browse:deadlines.filters.statusOptions.pending")}
+
+ ),
+ header: translate("browse:deadlines.columns.status"),
+ },
+ {
+ accessorKey: "alert_name",
+ cell: ({ row: { original } }) => original.alert_name ?? "",
+ enableSorting: false,
+ header: translate("browse:deadlines.columns.alertName"),
+ },
+ {
+ accessorKey: "created_at",
+ cell: ({ row: { original } }: DeadlineRow) => ,
+ header: translate("common:table.createdAt"),
+ },
+];
+
+const deadlinesFilterKeys: Array = [
+ SearchParamsKeys.DAG_ID,
+ SearchParamsKeys.DEADLINE_TIME_RANGE,
+ SearchParamsKeys.MISSED,
+];
+
+export const Deadlines = () => {
+ const { t: translate } = useTranslation(["browse", "common"]);
+ const { setTableURLState, tableURLState } = useTableURLState();
+ const [searchParams] = useSearchParams();
+
+ const { filterConfigs, handleFiltersChange, initialValues } = useFiltersHandler(deadlinesFilterKeys);
+
+ const columns = createColumns(translate);
+
+ const { pagination, sorting } = tableURLState;
+ const [sort] = sorting;
+ const orderBy = sort ? [`${sort.desc ? "-" : ""}${sort.id}`] : ["-deadline_time"];
+
+ const filteredDagId = searchParams.get(SearchParamsKeys.DAG_ID);
+ const filteredMissed = searchParams.get(SearchParamsKeys.MISSED);
+ const deadlineTimeGte = searchParams.get(SearchParamsKeys.DEADLINE_TIME_GTE);
+ const deadlineTimeLte = searchParams.get(SearchParamsKeys.DEADLINE_TIME_LTE);
+
+ const missedFilter = filteredMissed === "true" ? true : filteredMissed === "false" ? false : undefined;
+
+ const { data, error, isFetching, isLoading } = useDeadlinesServiceGetDeadlines({
+ dagId: filteredDagId !== null && filteredDagId !== "" ? filteredDagId : "~",
+ dagRunId: "~",
+ deadlineTimeGte: deadlineTimeGte ?? undefined,
+ deadlineTimeLte: deadlineTimeLte ?? undefined,
+ limit: pagination.pageSize,
+ missed: missedFilter,
+ offset: pagination.pageIndex * pagination.pageSize,
+ orderBy,
+ });
+
+ return (
+
+ {translate("browse:deadlines.title")}
+
+
+
+ }
+ initialState={tableURLState}
+ isFetching={isFetching}
+ isLoading={isLoading}
+ modelName="browse:deadlines.deadline"
+ onStateChange={setTableURLState}
+ showRowCountHeading={false}
+ total={data?.total_entries}
+ />
+
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/router.tsx b/airflow-core/src/airflow/ui/src/router.tsx
index e7bcd4bdfbf67..f54a785f3e5d4 100644
--- a/airflow-core/src/airflow/ui/src/router.tsx
+++ b/airflow-core/src/airflow/ui/src/router.tsx
@@ -37,6 +37,7 @@ import { Tasks } from "src/pages/Dag/Tasks";
import { DagRuns } from "src/pages/DagRuns";
import { DagsList } from "src/pages/DagsList";
import { Dashboard } from "src/pages/Dashboard";
+import { Deadlines } from "src/pages/Deadlines";
import { ErrorPage } from "src/pages/Error";
import { Events } from "src/pages/Events";
import { ExternalView } from "src/pages/ExternalView";
@@ -125,6 +126,10 @@ export const routerConfig = [
element: ,
path: "assets/:assetId",
},
+ {
+ element: ,
+ path: "deadlines",
+ },
{
element: ,
path: "events",
diff --git a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
index c97e75155ef51..ae5f48e5ef19a 100644
--- a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
+++ b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
@@ -66,6 +66,7 @@ export type FilterableSearchParamsKeys =
| SearchParamsKeys.DAG_ID
| SearchParamsKeys.DAG_ID_PATTERN
| SearchParamsKeys.DAG_VERSION
+ | SearchParamsKeys.DEADLINE_TIME_RANGE
| SearchParamsKeys.DURATION_GTE
| SearchParamsKeys.DURATION_LTE
| SearchParamsKeys.END_DATE_RANGE
@@ -78,6 +79,7 @@ export type FilterableSearchParamsKeys =
| SearchParamsKeys.KEY_PATTERN
| SearchParamsKeys.LOGICAL_DATE_RANGE
| SearchParamsKeys.MAP_INDEX
+ | SearchParamsKeys.MISSED
| SearchParamsKeys.NAME_PATTERN
| SearchParamsKeys.OPERATOR_NAME_PATTERN
| SearchParamsKeys.PARTITION_KEY_PATTERN
diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
index 4c8c81dc067c1..20b3be74846c0 100644
--- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
+++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
@@ -168,6 +168,9 @@
_MAP_MENU_ITEM_TO_FAB_RESOURCE_TYPE[MenuItem.REQUIRED_ACTIONS] = RESOURCE_HITL_DETAIL
_MAP_DAG_ACCESS_ENTITY_TO_FAB_RESOURCE_TYPE[DagAccessEntity.HITL_DETAIL] = (RESOURCE_HITL_DETAIL,)
+if hasattr(MenuItem, "DEADLINES"):
+ _MAP_MENU_ITEM_TO_FAB_RESOURCE_TYPE[MenuItem.DEADLINES] = RESOURCE_DAG_RUN
+
class FabAuthManager(BaseAuthManager[User]):
"""
diff --git a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
index 28c95a772734d..ab3cb8ee26429 100644
--- a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
+++ b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
@@ -831,6 +831,17 @@ def test_is_authorized_custom_view(
[(ACTION_CAN_ACCESS_MENU, RESOURCE_AUDIT_LOG), (ACTION_CAN_READ, RESOURCE_VARIABLE)],
[MenuItem.AUDIT_LOG],
),
+ *(
+ [
+ (
+ [MenuItem.DEADLINES],
+ [(ACTION_CAN_ACCESS_MENU, RESOURCE_DAG_RUN)],
+ [MenuItem.DEADLINES],
+ )
+ ]
+ if hasattr(MenuItem, "DEADLINES")
+ else []
+ ),
(
[],
[],