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 f0833b06300 Add Browse > Deadlines page (#67586)
f0833b06300 is described below
commit f0833b06300893fd6f19a4c8e6626058d4959075
Author: Richard Wu <[email protected]>
AuthorDate: Thu Jun 4 07:46:29 2026 -0700
Add Browse > Deadlines page (#67586)
* feat/browser: Add DataTable and browse UI components
* Add FAB permissions for Deadlines page
* Added AIRFLOW_V_3_2_PLUS to the version_compat impor
* Remove AIRFLOW_V_3_2_PLUS import and update conditional check for
MenuItem.DEADLINES in fab_auth_manager.py
* Add RESOURCE_DEADLINE permission for MenuItem.DEADLINES in
test_fab_auth_manager.py
* Refactor Deadlines permissions and update related tests
* Add conditional check for MenuItem.DEADLINES in TestFabAuthManager
* Update airflow-core/src/airflow/ui/src/pages/Deadlines/index.tsx
Co-authored-by: Brent Bovenzi <[email protected]>
* fix indent
---------
Co-authored-by: Brent Bovenzi <[email protected]>
---
airflow-core/newsfragments/67586.significant.rst | 4 +
.../src/airflow/api_fastapi/common/types.py | 1 +
.../api_fastapi/core_api/openapi/_private_ui.yaml | 1 +
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 2 +-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 2 +-
.../airflow/ui/public/i18n/locales/en/browse.json | 18 +++
.../airflow/ui/public/i18n/locales/en/common.json | 1 +
.../src/airflow/ui/src/constants/filterConfigs.tsx | 17 +++
.../src/airflow/ui/src/constants/searchParams.ts | 4 +
.../airflow/ui/src/layouts/Nav/BrowseButton.tsx | 5 +
.../src/airflow/ui/src/pages/Deadlines/index.tsx | 156 +++++++++++++++++++++
airflow-core/src/airflow/ui/src/router.tsx | 5 +
.../src/airflow/ui/src/utils/useFiltersHandler.ts | 2 +
.../providers/fab/auth_manager/fab_auth_manager.py | 3 +
.../unit/fab/auth_manager/test_fab_auth_manager.py | 11 ++
15 files changed, 230 insertions(+), 2 deletions(-)
diff --git a/airflow-core/newsfragments/67586.significant.rst
b/airflow-core/newsfragments/67586.significant.rst
new file mode 100644
index 00000000000..4a1c2afb999
--- /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 7d2a944c822..462a0278b4d 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 52a09dcbb7b..e9d280855b5 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 9acda604e28..e1fc8821cbb 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
@@ -9535,7 +9535,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 4f13f95a2f8..371d06c8695 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
@@ -2410,7 +2410,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 d92d59b9272..6c6b1016b5b 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 4e87c45e305..7b370ca2d45 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 7d160ad5999..1ce5daa23e6 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: <MdDateRange />,
+ label: translate("browse:deadlines.columns.deadlineTime"),
+ startKey: SearchParamsKeys.DEADLINE_TIME_GTE,
+ type: FilterTypes.DATERANGE,
+ },
[SearchParamsKeys.DURATION_GTE]: {
icon: <MdHourglassEmpty />,
label: translate("common:filters.durationFrom"),
@@ -209,6 +216,16 @@ export const useFilterConfigs = () => {
min: -1,
type: FilterTypes.NUMBER,
},
+ [SearchParamsKeys.MISSED]: {
+ icon: <MdCheckCircle />,
+ 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: <TaskIcon />,
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index b49d1cad846..f3583d449ce 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 a9de356ff9d..d95f9e0f318 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 00000000000..720648c4247
--- /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<ColumnDef<DeadlineResponse>> => [
+ {
+ accessorKey: "dag_id",
+ cell: ({ row: { original } }: DeadlineRow) => (
+ <Link asChild color="fg.info">
+ <RouterLink to={`/dags/${original.dag_id}`}>
+ <TruncatedText text={original.dag_id} />
+ </RouterLink>
+ </Link>
+ ),
+ header: translate("common:dagId"),
+ },
+ {
+ accessorKey: "dag_run_id",
+ cell: ({ row: { original } }: DeadlineRow) => (
+ <Link asChild color="fg.info">
+ <RouterLink
to={`/dags/${original.dag_id}/runs/${original.dag_run_id}`}>
+ <TruncatedText text={original.dag_run_id} />
+ </RouterLink>
+ </Link>
+ ),
+ enableSorting: false,
+ header: translate("common:dagRunId"),
+ },
+ {
+ accessorKey: "deadline_time",
+ cell: ({ row: { original } }: DeadlineRow) => <Time
datetime={original.deadline_time} />,
+ header: translate("browse:deadlines.columns.deadlineTime"),
+ },
+ {
+ accessorKey: "missed",
+ cell: ({
+ row: {
+ original: { missed },
+ },
+ }) => (
+ <Badge colorPalette={missed ? "red" : "blue"} size="sm" variant="solid">
+ {missed
+ ? translate("browse:deadlines.filters.statusOptions.missed")
+ : translate("browse:deadlines.filters.statusOptions.pending")}
+ </Badge>
+ ),
+ 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) => <Time
datetime={original.created_at} />,
+ header: translate("common:table.createdAt"),
+ },
+];
+
+const deadlinesFilterKeys: Array<FilterableSearchParamsKeys> = [
+ 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 (
+ <Box p={2}>
+ <Heading>{translate("browse:deadlines.title")}</Heading>
+ <VStack align="start" gap={4} paddingY="4px">
+ <FilterBar
+ configs={filterConfigs}
+ initialValues={initialValues}
+ onFiltersChange={handleFiltersChange}
+ />
+ </VStack>
+ <DataTable
+ columns={columns}
+ data={data?.deadlines ?? []}
+ errorMessage={<ErrorAlert error={error} />}
+ initialState={tableURLState}
+ isFetching={isFetching}
+ isLoading={isLoading}
+ modelName="browse:deadlines.deadline"
+ onStateChange={setTableURLState}
+ showRowCountHeading={false}
+ total={data?.total_entries}
+ />
+ </Box>
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/router.tsx
b/airflow-core/src/airflow/ui/src/router.tsx
index e7bcd4bdfbf..f54a785f3e5 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: <Asset />,
path: "assets/:assetId",
},
+ {
+ element: <Deadlines />,
+ path: "deadlines",
+ },
{
element: <Events />,
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 c97e75155ef..ae5f48e5ef1 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 4c8c81dc067..20b3be74846 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 @@ if AIRFLOW_V_3_1_PLUS:
_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 28c95a77273..ab3cb8ee264 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 @@ class TestFabAuthManager:
[(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 []
+ ),
(
[],
[],