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 []
+            ),
             (
                 [],
                 [],

Reply via email to