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 06ce1bb69ab Add pendingActions onto ui/dags (#55041)
06ce1bb69ab is described below

commit 06ce1bb69ab7c33762cb5acb6005cd376c6a5005
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Sep 2 17:10:16 2025 -0500

    Add pendingActions onto ui/dags (#55041)
    
    * Add pendingActions onto ui/dags
    
    * refactor(api_fastapi): unify usage of the term Dag
    
    * refactor(api_fastapi): simplify HITL loading logic
    
    * test: add test case test_should_return_200_with_hitl
    
    * Refactor naming scheme and filter
    
    * fix(hitl): fix has_pending_actions to use >= 1 and == 0 to query
    
    * feat(zh-tw): update Taiwanese Mandarin translation
    
    * Reduce i18n surface area and parameterize dags test
    
    ---------
    
    Co-authored-by: Wei Lee <[email protected]>
---
 .../src/airflow/api_fastapi/common/parameters.py   |  54 ++-
 .../api_fastapi/core_api/datamodels/ui/dags.py     |   2 +
 .../api_fastapi/core_api/openapi/_private_ui.yaml  | 380 ++++++++++++++++++++-
 .../core_api/openapi/v2-rest-api-generated.yaml    |   8 +-
 .../airflow/api_fastapi/core_api/routes/ui/dags.py |  26 ++
 .../src/airflow/ui/openapi-gen/queries/common.ts   |   5 +-
 .../ui/openapi-gen/queries/ensureQueryData.ts      |  14 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |  14 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |  14 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |  14 +-
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts |   9 +-
 .../ui/openapi-gen/requests/services.gen.ts        |  12 +-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  10 +-
 .../public/i18n/locales/en/_freeze_exemptions.json |   2 +
 .../airflow/ui/public/i18n/locales/en/hitl.json    |   2 +
 .../i18n/locales/zh-TW/_freeze_exemptions.json     |   5 +
 .../airflow/ui/public/i18n/locales/zh-TW/hitl.json |   2 +-
 .../airflow/ui/src/components/NeedsReviewBadge.tsx |  49 +++
 ...dingActionsButton.tsx => NeedsReviewButton.tsx} |   8 +-
 .../src/airflow/ui/src/constants/searchParams.ts   |   1 +
 .../src/airflow/ui/src/mocks/handlers/dags.ts      |   4 +
 .../airflow/ui/src/pages/Dag/Overview/Overview.tsx |   4 +-
 .../airflow/ui/src/pages/DagsList/DagCard.test.tsx |   1 +
 .../src/airflow/ui/src/pages/DagsList/DagCard.tsx  |   2 +
 .../src/pages/DagsList/DagsFilters/DagsFilters.tsx |   8 +
 .../pages/DagsList/DagsFilters/StateFilters.tsx    |  25 +-
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |  20 +-
 .../airflow/ui/src/pages/DagsList/RecentRuns.tsx   |   7 +-
 .../airflow/ui/src/pages/Dashboard/Stats/Stats.tsx |   4 +-
 .../ui/src/pages/Task/Overview/Overview.tsx        |   4 +-
 .../src/airflow/ui/src/queries/useDags.tsx         |   3 +
 .../api_fastapi/core_api/routes/ui/test_dags.py    | 101 ++++++
 32 files changed, 742 insertions(+), 72 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py 
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index 592f08cdf12..a5ec88ba2bc 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -114,7 +114,7 @@ class OffsetFilter(BaseParam[NonNegativeInt]):
 
 
 class _FavoriteFilter(BaseParam[bool]):
-    """Filter DAGs by favorite status."""
+    """Filter Dags by favorite status."""
 
     def __init__(self, user_id: str, value: T | None = None, skip_none: bool = 
True) -> None:
         super().__init__(skip_none=skip_none)
@@ -609,7 +609,7 @@ def float_range_filter_factory(
 DateTimeQuery = Annotated[str, AfterValidator(_safe_parse_datetime)]
 OptionalDateTimeQuery = Annotated[str | None, 
AfterValidator(_safe_parse_datetime_optional)]
 
-# DAG
+# Dag
 QueryLimit = Annotated[LimitFilter, Depends(LimitFilter.depends)]
 QueryOffset = Annotated[OffsetFilter, Depends(OffsetFilter.depends)]
 QueryPausedFilter = Annotated[
@@ -640,7 +640,7 @@ QueryOwnersFilter = Annotated[_OwnersFilter, 
Depends(_OwnersFilter.depends)]
 
 
 class _HasAssetScheduleFilter(BaseParam[bool]):
-    """Filter DAGs that have asset-based scheduling."""
+    """Filter Dags that have asset-based scheduling."""
 
     def to_orm(self, select: Select) -> Select:
         if self.value is None and self.skip_none:
@@ -649,22 +649,22 @@ class _HasAssetScheduleFilter(BaseParam[bool]):
         asset_ref_subquery = 
sql_select(DagScheduleAssetReference.dag_id).distinct()
 
         if self.value:
-            # Filter DAGs that have asset-based scheduling
+            # Filter Dags that have asset-based scheduling
             return select.where(DagModel.dag_id.in_(asset_ref_subquery))
 
-        # Filter DAGs that do NOT have asset-based scheduling
+        # Filter Dags that do NOT have asset-based scheduling
         return select.where(DagModel.dag_id.notin_(asset_ref_subquery))
 
     @classmethod
     def depends(
         cls,
-        has_asset_schedule: bool | None = Query(None, description="Filter DAGs 
with asset-based scheduling"),
+        has_asset_schedule: bool | None = Query(None, description="Filter Dags 
with asset-based scheduling"),
     ) -> _HasAssetScheduleFilter:
         return cls().set_value(has_asset_schedule)
 
 
 class _AssetDependencyFilter(BaseParam[str]):
-    """Filter DAGs by specific asset dependencies."""
+    """Filter Dags by specific asset dependencies."""
 
     def to_orm(self, select: Select) -> Select:
         if self.value is None and self.skip_none:
@@ -683,7 +683,7 @@ class _AssetDependencyFilter(BaseParam[str]):
     def depends(
         cls,
         asset_dependency: str | None = Query(
-            None, description="Filter DAGs by asset dependency (name or URI)"
+            None, description="Filter Dags by asset dependency (name or URI)"
         ),
     ) -> _AssetDependencyFilter:
         return cls().set_value(asset_dependency)
@@ -692,6 +692,42 @@ class _AssetDependencyFilter(BaseParam[str]):
 QueryHasAssetScheduleFilter = Annotated[_HasAssetScheduleFilter, 
Depends(_HasAssetScheduleFilter.depends)]
 QueryAssetDependencyFilter = Annotated[_AssetDependencyFilter, 
Depends(_AssetDependencyFilter.depends)]
 
+
+class _PendingActionsFilter(BaseParam[bool]):
+    """Filter Dags by having pending HITL actions (more than 1)."""
+
+    def to_orm(self, select: Select) -> Select:
+        if self.value is None and self.skip_none:
+            return select
+
+        from airflow.models.hitl import HITLDetail
+        from airflow.models.taskinstance import TaskInstance
+
+        # Join with HITLDetail and TaskInstance to find Dags
+        pending_actions_count_subquery = (
+            sql_select(func.count(HITLDetail.ti_id))
+            .join(TaskInstance, HITLDetail.ti_id == TaskInstance.id)
+            .where(HITLDetail.response_at.is_(None))
+            .where(TaskInstance.dag_id == DagModel.dag_id)
+            .scalar_subquery()
+        )
+
+        if self.value is True:
+            # Filter to show only Dags with pending actions
+            where_clause = pending_actions_count_subquery >= 1
+        else:
+            # Filter to show only Dags without pending actions
+            where_clause = pending_actions_count_subquery == 0
+
+        return select.where(where_clause)
+
+    @classmethod
+    def depends(cls, has_pending_actions: bool | None = Query(None)) -> 
_PendingActionsFilter:
+        return cls().set_value(has_pending_actions)
+
+
+QueryPendingActionsFilter = Annotated[_PendingActionsFilter, 
Depends(_PendingActionsFilter.depends)]
+
 # DagRun
 QueryLastDagRunStateFilter = Annotated[
     FilterParam[DagRunState | None],
@@ -750,7 +786,7 @@ QueryDagRunRunTypesFilter = Annotated[
     ),
 ]
 
-# DAGTags
+# DagTags
 QueryDagTagPatternSearch = Annotated[
     _SearchParam, Depends(search_param_factory(DagTag.name, 
"tag_name_pattern"))
 ]
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
index 08588ef7856..e76c5414d6d 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 from airflow.api_fastapi.core_api.base import BaseModel
 from airflow.api_fastapi.core_api.datamodels.dag_run import DAGRunResponse
 from airflow.api_fastapi.core_api.datamodels.dags import DAGResponse
+from airflow.api_fastapi.core_api.datamodels.hitl import HITLDetail
 
 
 class DAGWithLatestDagRunsResponse(DAGResponse):
@@ -27,6 +28,7 @@ class DAGWithLatestDagRunsResponse(DAGResponse):
 
     asset_expression: dict | None
     latest_dag_runs: list[DAGRunResponse]
+    pending_actions: list[HITLDetail]
 
 
 class DAGWithLatestDagRunsCollectionResponse(BaseModel):
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 4027378a1c0..d6fb4b079ef 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
@@ -257,9 +257,9 @@ paths:
           anyOf:
           - type: boolean
           - type: 'null'
-          description: Filter DAGs with asset-based scheduling
+          description: Filter Dags with asset-based scheduling
           title: Has Asset Schedule
-        description: Filter DAGs with asset-based scheduling
+        description: Filter Dags with asset-based scheduling
       - name: asset_dependency
         in: query
         required: false
@@ -267,9 +267,17 @@ paths:
           anyOf:
           - type: string
           - type: 'null'
-          description: Filter DAGs by asset dependency (name or URI)
+          description: Filter Dags by asset dependency (name or URI)
           title: Asset Dependency
-        description: Filter DAGs by asset dependency (name or URI)
+        description: Filter Dags by asset dependency (name or URI)
+      - name: has_pending_actions
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: boolean
+          - type: 'null'
+          title: Has Pending Actions
       responses:
         '200':
           description: Successful Response
@@ -1578,6 +1586,11 @@ components:
             $ref: '#/components/schemas/DAGRunResponse'
           type: array
           title: Latest Dag Runs
+        pending_actions:
+          items:
+            $ref: '#/components/schemas/HITLDetail'
+          type: array
+          title: Pending Actions
         file_token:
           type: string
           title: File Token
@@ -1611,6 +1624,7 @@ components:
       - owners
       - asset_expression
       - latest_dag_runs
+      - pending_actions
       - file_token
       title: DAGWithLatestDagRunsResponse
       description: DAG with latest dag runs response serializer.
@@ -1889,6 +1903,79 @@ components:
       - task_instances
       title: GridTISummaries
       description: DAG Run model for the Grid UI.
+    HITLDetail:
+      properties:
+        task_instance:
+          $ref: '#/components/schemas/TaskInstanceResponse'
+        options:
+          items:
+            type: string
+          type: array
+          minItems: 1
+          title: Options
+        subject:
+          type: string
+          title: Subject
+        body:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Body
+        defaults:
+          anyOf:
+          - items:
+              type: string
+            type: array
+          - type: 'null'
+          title: Defaults
+        multiple:
+          type: boolean
+          title: Multiple
+          default: false
+        params:
+          additionalProperties: true
+          type: object
+          title: Params
+        respondents:
+          anyOf:
+          - items:
+              type: string
+            type: array
+          - type: 'null'
+          title: Respondents
+        user_id:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: User Id
+        response_at:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Response At
+        chosen_options:
+          anyOf:
+          - items:
+              type: string
+            type: array
+          - type: 'null'
+          title: Chosen Options
+        params_input:
+          additionalProperties: true
+          type: object
+          title: Params Input
+        response_received:
+          type: boolean
+          title: Response Received
+          default: false
+      type: object
+      required:
+      - task_instance
+      - options
+      - subject
+      title: HITLDetail
+      description: Schema for Human-in-the-loop detail.
     HTTPExceptionResponse:
       properties:
         detail:
@@ -1926,6 +2013,78 @@ components:
       - task_instance_states
       title: HistoricalMetricDataResponse
       description: Historical Metric Data serializer for responses.
+    JobResponse:
+      properties:
+        id:
+          type: integer
+          title: Id
+        dag_id:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Dag Id
+        state:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: State
+        job_type:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Job Type
+        start_date:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Start Date
+        end_date:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: End Date
+        latest_heartbeat:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Latest Heartbeat
+        executor_class:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Executor Class
+        hostname:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Hostname
+        unixname:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Unixname
+        dag_display_name:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Dag Display Name
+      type: object
+      required:
+      - id
+      - dag_id
+      - state
+      - job_type
+      - start_date
+      - end_date
+      - latest_heartbeat
+      - executor_class
+      - hostname
+      - unixname
+      title: JobResponse
+      description: Job serializer for responses.
     LightGridTaskInstanceSummary:
       properties:
         task_id:
@@ -2130,6 +2289,190 @@ components:
       - nodes
       title: StructureDataResponse
       description: Structure Data serializer for responses.
+    TaskInstanceResponse:
+      properties:
+        id:
+          type: string
+          title: Id
+        task_id:
+          type: string
+          title: Task Id
+        dag_id:
+          type: string
+          title: Dag Id
+        dag_run_id:
+          type: string
+          title: Dag Run Id
+        map_index:
+          type: integer
+          title: Map Index
+        logical_date:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Logical Date
+        run_after:
+          type: string
+          format: date-time
+          title: Run After
+        start_date:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Start Date
+        end_date:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: End Date
+        duration:
+          anyOf:
+          - type: number
+          - type: 'null'
+          title: Duration
+        state:
+          anyOf:
+          - $ref: '#/components/schemas/TaskInstanceState'
+          - type: 'null'
+        try_number:
+          type: integer
+          title: Try Number
+        max_tries:
+          type: integer
+          title: Max Tries
+        task_display_name:
+          type: string
+          title: Task Display Name
+        dag_display_name:
+          type: string
+          title: Dag Display Name
+        hostname:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Hostname
+        unixname:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Unixname
+        pool:
+          type: string
+          title: Pool
+        pool_slots:
+          type: integer
+          title: Pool Slots
+        queue:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Queue
+        priority_weight:
+          anyOf:
+          - type: integer
+          - type: 'null'
+          title: Priority Weight
+        operator:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Operator
+        operator_name:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Operator Name
+        queued_when:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Queued When
+        scheduled_when:
+          anyOf:
+          - type: string
+            format: date-time
+          - type: 'null'
+          title: Scheduled When
+        pid:
+          anyOf:
+          - type: integer
+          - type: 'null'
+          title: Pid
+        executor:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Executor
+        executor_config:
+          type: string
+          title: Executor Config
+        note:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Note
+        rendered_map_index:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Rendered Map Index
+        rendered_fields:
+          additionalProperties: true
+          type: object
+          title: Rendered Fields
+        trigger:
+          anyOf:
+          - $ref: '#/components/schemas/TriggerResponse'
+          - type: 'null'
+        triggerer_job:
+          anyOf:
+          - $ref: '#/components/schemas/JobResponse'
+          - type: 'null'
+        dag_version:
+          anyOf:
+          - $ref: '#/components/schemas/DagVersionResponse'
+          - type: 'null'
+      type: object
+      required:
+      - id
+      - task_id
+      - dag_id
+      - dag_run_id
+      - map_index
+      - logical_date
+      - run_after
+      - start_date
+      - end_date
+      - duration
+      - state
+      - try_number
+      - max_tries
+      - task_display_name
+      - dag_display_name
+      - hostname
+      - unixname
+      - pool
+      - pool_slots
+      - queue
+      - priority_weight
+      - operator
+      - operator_name
+      - queued_when
+      - scheduled_when
+      - pid
+      - executor
+      - executor_config
+      - note
+      - rendered_map_index
+      - trigger
+      - triggerer_job
+      - dag_version
+      title: TaskInstanceResponse
+      description: TaskInstance serializer for responses.
     TaskInstanceState:
       type: string
       enum:
@@ -2208,6 +2551,35 @@ components:
       - deferred
       title: TaskInstanceStateCount
       description: TaskInstance serializer for responses.
+    TriggerResponse:
+      properties:
+        id:
+          type: integer
+          title: Id
+        classpath:
+          type: string
+          title: Classpath
+        kwargs:
+          type: string
+          title: Kwargs
+        created_date:
+          type: string
+          format: date-time
+          title: Created Date
+        triggerer_id:
+          anyOf:
+          - type: integer
+          - type: 'null'
+          title: Triggerer Id
+      type: object
+      required:
+      - id
+      - classpath
+      - kwargs
+      - created_date
+      - triggerer_id
+      title: TriggerResponse
+      description: Trigger serializer for responses.
     UIAlert:
       properties:
         text:
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 81f0a2acb2a..ac0f676108f 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -3011,9 +3011,9 @@ paths:
           anyOf:
           - type: boolean
           - type: 'null'
-          description: Filter DAGs with asset-based scheduling
+          description: Filter Dags with asset-based scheduling
           title: Has Asset Schedule
-        description: Filter DAGs with asset-based scheduling
+        description: Filter Dags with asset-based scheduling
       - name: asset_dependency
         in: query
         required: false
@@ -3021,9 +3021,9 @@ paths:
           anyOf:
           - type: string
           - type: 'null'
-          description: Filter DAGs by asset dependency (name or URI)
+          description: Filter Dags by asset dependency (name or URI)
           title: Asset Dependency
-        description: Filter DAGs by asset dependency (name or URI)
+        description: Filter Dags by asset dependency (name or URI)
       - name: dag_run_start_date_gte
         in: query
         required: false
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
index 7c580e42181..6a4d96f9bdc 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
@@ -45,6 +45,7 @@ from airflow.api_fastapi.common.parameters import (
     QueryOffset,
     QueryOwnersFilter,
     QueryPausedFilter,
+    QueryPendingActionsFilter,
     QueryTagsFilter,
     SortParam,
     filter_param_factory,
@@ -63,6 +64,8 @@ from airflow.api_fastapi.core_api.security import (
     requires_access_dag,
 )
 from airflow.models import DagModel, DagRun
+from airflow.models.hitl import HITLDetail
+from airflow.models.taskinstance import TaskInstance
 
 dags_router = AirflowRouter(prefix="/dags", tags=["DAG"])
 
@@ -105,6 +108,7 @@ def get_dags(
     is_favorite: QueryFavoriteFilter,
     has_asset_schedule: QueryHasAssetScheduleFilter,
     asset_dependency: QueryAssetDependencyFilter,
+    has_pending_actions: QueryPendingActionsFilter,
     readable_dags_filter: ReadableDagsFilterDep,
     session: SessionDep,
     dag_runs_limit: int = 10,
@@ -132,6 +136,7 @@ def get_dags(
             is_favorite,
             has_asset_schedule,
             asset_dependency,
+            has_pending_actions,
             readable_dags_filter,
             bundle_name,
             bundle_version,
@@ -184,6 +189,26 @@ def get_dags(
 
     recent_dag_runs = session.execute(recent_dag_runs_select)
 
+    # Fetch pending HITL actions for each Dag if we are not certain whether 
some of the Dag might contain HITL actions
+    pending_actions_by_dag_id: dict[str, list[HITLDetail]] = {dag.dag_id: [] 
for dag in dags}
+    if has_pending_actions.value is not False:
+        pending_actions_select = (
+            select(
+                TaskInstance.dag_id,
+                HITLDetail,
+            )
+            .join(TaskInstance, HITLDetail.ti_id == TaskInstance.id)
+            .where(HITLDetail.response_at.is_(None))
+            .where(TaskInstance.dag_id.in_([dag.dag_id for dag in dags]))
+            .order_by(TaskInstance.dag_id)
+        )
+
+        pending_actions = session.execute(pending_actions_select)
+
+        # Group pending actions by dag_id
+        for dag_id, hitl_detail in pending_actions:
+            pending_actions_by_dag_id[dag_id].append(hitl_detail)
+
     # aggregate rows by dag_id
     dag_runs_by_dag_id: dict[str, DAGWithLatestDagRunsResponse] = {
         dag.dag_id: DAGWithLatestDagRunsResponse.model_validate(
@@ -191,6 +216,7 @@ def get_dags(
                 **DAGResponse.model_validate(dag).model_dump(),
                 "asset_expression": dag.asset_expression,
                 "latest_dag_runs": [],
+                "pending_actions": pending_actions_by_dag_id[dag.dag_id],
             }
         )
         for dag in dags
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 ba486a353aa..4b7096aeff1 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -293,7 +293,7 @@ export const UseDagServiceGetDagTagsKeyFn = ({ limit, 
offset, orderBy, tagNamePa
 export type DagServiceGetDagsUiDefaultResponse = Awaited<ReturnType<typeof 
DagService.getDagsUi>>;
 export type DagServiceGetDagsUiQueryResult<TData = 
DagServiceGetDagsUiDefaultResponse, TError = unknown> = UseQueryResult<TData, 
TError>;
 export const useDagServiceGetDagsUiKey = "DagServiceGetDagsUi";
-export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, isFavorite, lastDagRunState, limit, offset, 
orderBy, owners, paused, tags, tagsMatchMode }: {
+export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, hasPendingActions, isFavorite, lastDagRunState, 
limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -303,6 +303,7 @@ export const UseDagServiceGetDagsUiKeyFn = ({ 
assetDependency, bundleName, bundl
   dagRunsLimit?: number;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
+  hasPendingActions?: boolean;
   isFavorite?: boolean;
   lastDagRunState?: DagRunState;
   limit?: number;
@@ -312,7 +313,7 @@ export const UseDagServiceGetDagsUiKeyFn = ({ 
assetDependency, bundleName, bundl
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}, queryKey?: Array<unknown>) => [useDagServiceGetDagsUiKey, ...(queryKey 
?? [{ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagIdPattern, dagIds, dagRunsLimit, excludeStale, hasAssetSchedule, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode 
}])];
+} = {}, queryKey?: Array<unknown>) => [useDagServiceGetDagsUiKey, ...(queryKey 
?? [{ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagIdPattern, dagIds, dagRunsLimit, excludeStale, hasAssetSchedule, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }])];
 export type DagServiceGetLatestRunInfoDefaultResponse = 
Awaited<ReturnType<typeof DagService.getLatestRunInfo>>;
 export type DagServiceGetLatestRunInfoQueryResult<TData = 
DagServiceGetLatestRunInfoDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useDagServiceGetLatestRunInfoKey = "DagServiceGetLatestRunInfo";
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 2f44048e710..fc4152ba7f4 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -460,8 +460,8 @@ export const ensureUseDagWarningServiceListDagWarningsData 
= (queryClient: Query
 * @param data.lastDagRunState
 * @param data.bundleName
 * @param data.bundleVersion
-* @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-* @param data.assetDependency Filter DAGs by asset dependency (name or URI)
+* @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+* @param data.assetDependency Filter Dags by asset dependency (name or URI)
 * @param data.dagRunStartDateGte
 * @param data.dagRunStartDateGt
 * @param data.dagRunStartDateLte
@@ -562,12 +562,13 @@ export const ensureUseDagServiceGetDagTagsData = 
(queryClient: QueryClient, { li
 * @param data.bundleVersion
 * @param data.orderBy
 * @param data.isFavorite
-* @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-* @param data.assetDependency Filter DAGs by asset dependency (name or URI)
+* @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+* @param data.assetDependency Filter Dags by asset dependency (name or URI)
+* @param data.hasPendingActions
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagIdPattern, dagIds, dagRunsLimit, excludeStale, hasAssetSchedule, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
+export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagIdPattern, dagIds, dagRunsLimit, excludeStale, hasAssetSchedule, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -577,6 +578,7 @@ export const ensureUseDagServiceGetDagsUiData = 
(queryClient: QueryClient, { ass
   dagRunsLimit?: number;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
+  hasPendingActions?: boolean;
   isFavorite?: boolean;
   lastDagRunState?: DagRunState;
   limit?: number;
@@ -586,7 +588,7 @@ export const ensureUseDagServiceGetDagsUiData = 
(queryClient: QueryClient, { ass
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, isFavorite, lastDagRunState, limit, offset, 
orderBy, owners, paused, tags, tagsMatchMode }), queryFn: () => 
DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, isF [...]
+} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, hasPendingActions, isFavorite, lastDagRunState, 
limit, offset, orderBy, owners, paused, tags, tagsMatchMode }), queryFn: () => 
DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, excludeStale, ha 
[...]
 /**
 * Get Latest Run Info
 * Get latest run.
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 d91da2d7f07..d2486398364 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -460,8 +460,8 @@ export const prefetchUseDagWarningServiceListDagWarnings = 
(queryClient: QueryCl
 * @param data.lastDagRunState
 * @param data.bundleName
 * @param data.bundleVersion
-* @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-* @param data.assetDependency Filter DAGs by asset dependency (name or URI)
+* @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+* @param data.assetDependency Filter Dags by asset dependency (name or URI)
 * @param data.dagRunStartDateGte
 * @param data.dagRunStartDateGt
 * @param data.dagRunStartDateLte
@@ -562,12 +562,13 @@ export const prefetchUseDagServiceGetDagTags = 
(queryClient: QueryClient, { limi
 * @param data.bundleVersion
 * @param data.orderBy
 * @param data.isFavorite
-* @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-* @param data.assetDependency Filter DAGs by asset dependency (name or URI)
+* @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+* @param data.assetDependency Filter Dags by asset dependency (name or URI)
+* @param data.hasPendingActions
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagIdPattern, dagIds, dagRunsLimit, excludeStale, hasAssetSchedule, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
+export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagIdPattern, dagIds, dagRunsLimit, excludeStale, hasAssetSchedule, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -577,6 +578,7 @@ export const prefetchUseDagServiceGetDagsUi = (queryClient: 
QueryClient, { asset
   dagRunsLimit?: number;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
+  hasPendingActions?: boolean;
   isFavorite?: boolean;
   lastDagRunState?: DagRunState;
   limit?: number;
@@ -586,7 +588,7 @@ export const prefetchUseDagServiceGetDagsUi = (queryClient: 
QueryClient, { asset
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, isFavorite, lastDagRunState, limit, offset, 
orderBy, owners, paused, tags, tagsMatchMode }), queryFn: () => 
DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, isFav [...]
+} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, hasPendingActions, isFavorite, lastDagRunState, 
limit, offset, orderBy, owners, paused, tags, tagsMatchMode }), queryFn: () => 
DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, excludeStale, hasA 
[...]
 /**
 * Get Latest Run Info
 * Get latest run.
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 be2271f380f..4b104f5bf73 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -460,8 +460,8 @@ export const useDagWarningServiceListDagWarnings = <TData = 
Common.DagWarningSer
 * @param data.lastDagRunState
 * @param data.bundleName
 * @param data.bundleVersion
-* @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-* @param data.assetDependency Filter DAGs by asset dependency (name or URI)
+* @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+* @param data.assetDependency Filter Dags by asset dependency (name or URI)
 * @param data.dagRunStartDateGte
 * @param data.dagRunStartDateGt
 * @param data.dagRunStartDateLte
@@ -562,12 +562,13 @@ export const useDagServiceGetDagTags = <TData = 
Common.DagServiceGetDagTagsDefau
 * @param data.bundleVersion
 * @param data.orderBy
 * @param data.isFavorite
-* @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-* @param data.assetDependency Filter DAGs by asset dependency (name or URI)
+* @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+* @param data.assetDependency Filter Dags by asset dependency (name or URI)
+* @param data.hasPendingActions
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
+export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, hasPendingActions, isFavorite, lastDagRunState, limit, 
offset, orderBy, owners, paused, tags, tagsMatchMode }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -577,6 +578,7 @@ export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefault
   dagRunsLimit?: number;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
+  hasPendingActions?: boolean;
   isFavorite?: boolean;
   lastDagRunState?: DagRunState;
   limit?: number;
@@ -586,7 +588,7 @@ export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefault
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, isFavorite, lastDagRunState, limit, offset, 
orderBy, owners, paused, tags, tagsMatchMode }, queryKey), queryFn: () => 
DagService.getDagsUi({ assetDependency, bundleName, bundleVer [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, hasPendingActions, isFavorite, lastDagRunState, 
limit, offset, orderBy, owners, paused, tags, tagsMatchMode }, queryKey), 
queryFn: () => DagService.getDagsUi({ assetDependency, bu [...]
 /**
 * Get Latest Run Info
 * Get latest run.
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 59612c36898..3f4686c1258 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -460,8 +460,8 @@ export const useDagWarningServiceListDagWarningsSuspense = 
<TData = Common.DagWa
 * @param data.lastDagRunState
 * @param data.bundleName
 * @param data.bundleVersion
-* @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-* @param data.assetDependency Filter DAGs by asset dependency (name or URI)
+* @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+* @param data.assetDependency Filter Dags by asset dependency (name or URI)
 * @param data.dagRunStartDateGte
 * @param data.dagRunStartDateGt
 * @param data.dagRunStartDateLte
@@ -562,12 +562,13 @@ export const useDagServiceGetDagTagsSuspense = <TData = 
Common.DagServiceGetDagT
 * @param data.bundleVersion
 * @param data.orderBy
 * @param data.isFavorite
-* @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-* @param data.assetDependency Filter DAGs by asset dependency (name or URI)
+* @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+* @param data.assetDependency Filter Dags by asset dependency (name or URI)
+* @param data.hasPendingActions
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
+export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, hasPendingActions, isFavorite, lastDagRunState, limit, 
offset, orderBy, owners, paused, tags, tagsMatchMode }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -577,6 +578,7 @@ export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsU
   dagRunsLimit?: number;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
+  hasPendingActions?: boolean;
   isFavorite?: boolean;
   lastDagRunState?: DagRunState;
   limit?: number;
@@ -586,7 +588,7 @@ export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsU
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, isFavorite, lastDagRunState, limit, offset, 
orderBy, owners, paused, tags, tagsMatchMode }, queryKey), queryFn: () => 
DagService.getDagsUi({ assetDependency, bundleName, b [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagIdPattern, dagIds, dagRunsLimit, 
excludeStale, hasAssetSchedule, hasPendingActions, isFavorite, lastDagRunState, 
limit, offset, orderBy, owners, paused, tags, tagsMatchMode }, queryKey), 
queryFn: () => DagService.getDagsUi({ assetDepend [...]
 /**
 * Get Latest Run Info
 * Get latest run.
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 033cfa654a9..c8811eced9b 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
@@ -7135,6 +7135,13 @@ export const $DAGWithLatestDagRunsResponse = {
             type: 'array',
             title: 'Latest Dag Runs'
         },
+        pending_actions: {
+            items: {
+                '$ref': '#/components/schemas/HITLDetail'
+            },
+            type: 'array',
+            title: 'Pending Actions'
+        },
         file_token: {
             type: 'string',
             title: 'File Token',
@@ -7143,7 +7150,7 @@ export const $DAGWithLatestDagRunsResponse = {
         }
     },
     type: 'object',
-    required: ['dag_id', 'dag_display_name', 'is_paused', 'is_stale', 
'last_parsed_time', 'last_expired', 'bundle_name', 'bundle_version', 
'relative_fileloc', 'fileloc', 'description', 'timetable_summary', 
'timetable_description', 'tags', 'max_active_tasks', 'max_active_runs', 
'max_consecutive_failed_dag_runs', 'has_task_concurrency_limits', 
'has_import_errors', 'next_dagrun_logical_date', 
'next_dagrun_data_interval_start', 'next_dagrun_data_interval_end', 
'next_dagrun_run_after', 'owner [...]
+    required: ['dag_id', 'dag_display_name', 'is_paused', 'is_stale', 
'last_parsed_time', 'last_expired', 'bundle_name', 'bundle_version', 
'relative_fileloc', 'fileloc', 'description', 'timetable_summary', 
'timetable_description', 'tags', 'max_active_tasks', 'max_active_runs', 
'max_consecutive_failed_dag_runs', 'has_task_concurrency_limits', 
'has_import_errors', 'next_dagrun_logical_date', 
'next_dagrun_data_interval_start', 'next_dagrun_data_interval_end', 
'next_dagrun_run_after', 'owner [...]
     title: 'DAGWithLatestDagRunsResponse',
     description: 'DAG with latest dag runs response serializer.'
 } as const;
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 f04bbe72a78..d8ba1016897 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
@@ -1392,8 +1392,8 @@ export class DagService {
      * @param data.lastDagRunState
      * @param data.bundleName
      * @param data.bundleVersion
-     * @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-     * @param data.assetDependency Filter DAGs by asset dependency (name or 
URI)
+     * @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+     * @param data.assetDependency Filter Dags by asset dependency (name or 
URI)
      * @param data.dagRunStartDateGte
      * @param data.dagRunStartDateGt
      * @param data.dagRunStartDateLte
@@ -1696,8 +1696,9 @@ export class DagService {
      * @param data.bundleVersion
      * @param data.orderBy
      * @param data.isFavorite
-     * @param data.hasAssetSchedule Filter DAGs with asset-based scheduling
-     * @param data.assetDependency Filter DAGs by asset dependency (name or 
URI)
+     * @param data.hasAssetSchedule Filter Dags with asset-based scheduling
+     * @param data.assetDependency Filter Dags by asset dependency (name or 
URI)
+     * @param data.hasPendingActions
      * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
      * @throws ApiError
      */
@@ -1723,7 +1724,8 @@ export class DagService {
                 order_by: data.orderBy,
                 is_favorite: data.isFavorite,
                 has_asset_schedule: data.hasAssetSchedule,
-                asset_dependency: data.assetDependency
+                asset_dependency: data.assetDependency,
+                has_pending_actions: data.hasPendingActions
             },
             errors: {
                 422: 'Validation Error'
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 ea0b228d786..108a0b4abd2 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
@@ -1789,6 +1789,7 @@ export type DAGWithLatestDagRunsResponse = {
     [key: string]: unknown;
 } | null;
     latest_dag_runs: Array<DAGRunResponse>;
+    pending_actions: Array<HITLDetail>;
     /**
      * Return file token.
      */
@@ -2342,7 +2343,7 @@ export type ListDagWarningsResponse = 
DAGWarningCollectionResponse;
 
 export type GetDagsData = {
     /**
-     * Filter DAGs by asset dependency (name or URI)
+     * Filter Dags by asset dependency (name or URI)
      */
     assetDependency?: string | null;
     bundleName?: string | null;
@@ -2366,7 +2367,7 @@ export type GetDagsData = {
     dagRunState?: Array<(string)>;
     excludeStale?: boolean;
     /**
-     * Filter DAGs with asset-based scheduling
+     * Filter Dags with asset-based scheduling
      */
     hasAssetSchedule?: boolean | null;
     isFavorite?: boolean | null;
@@ -2452,7 +2453,7 @@ export type GetDagTagsResponse = DAGTagCollectionResponse;
 
 export type GetDagsUiData = {
     /**
-     * Filter DAGs by asset dependency (name or URI)
+     * Filter Dags by asset dependency (name or URI)
      */
     assetDependency?: string | null;
     bundleName?: string | null;
@@ -2469,9 +2470,10 @@ export type GetDagsUiData = {
     dagRunsLimit?: number;
     excludeStale?: boolean;
     /**
-     * Filter DAGs with asset-based scheduling
+     * Filter Dags with asset-based scheduling
      */
     hasAssetSchedule?: boolean | null;
+    hasPendingActions?: boolean | null;
     isFavorite?: boolean | null;
     lastDagRunState?: DagRunState | null;
     limit?: number;
diff --git 
a/airflow-core/src/airflow/ui/public/i18n/locales/en/_freeze_exemptions.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/_freeze_exemptions.json
index cf759b2f5b8..d6f0e612043 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/_freeze_exemptions.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/_freeze_exemptions.json
@@ -8,6 +8,8 @@
   "dags": {},
   "dashboard": {},
   "hitl": {
+    "requiredActionCount_one": "Required Action ({{count}})",
+    "requiredActionCount_other": "Required Actions ({{count}})",
     "state": {
       "noResponseReceived": "No Response Received"
     }
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/hitl.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/hitl.json
index a69a473c35a..3abb51e0c4b 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/hitl.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/hitl.json
@@ -8,6 +8,8 @@
   },
   "requiredAction_one": "Required Action",
   "requiredAction_other": "Required Actions",
+  "requiredActionCount_one": "Required Action ({{count}})",
+  "requiredActionCount_other": "Required Actions ({{count}})",
   "requiredActionState": "Required Action State",
   "response": {
     "error": "Response failed",
diff --git 
a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/_freeze_exemptions.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/_freeze_exemptions.json
index 012b4f2af33..86b26aaa64b 100644
--- 
a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/_freeze_exemptions.json
+++ 
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/_freeze_exemptions.json
@@ -8,6 +8,11 @@
   "dags": {},
   "dashboard": {},
   "hitl": {
+    "filters": {
+      "response": {
+        "received": "已回應"
+      }
+    },
     "state": {
       "noResponseReceived": "未收到回應"
     }
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/hitl.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/hitl.json
index 9046e90310e..47e12b27aa5 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/hitl.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/hitl.json
@@ -3,7 +3,7 @@
     "response": {
       "all": "全部",
       "pending": "待回應",
-      "received": "已回覆"
+      "received": "已回應"
     }
   },
   "requiredAction_one": "待回應的任務實例",
diff --git a/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx 
b/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
new file mode 100644
index 00000000000..c422ff0fcf2
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
@@ -0,0 +1,49 @@
+/*!
+ * 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 { useTranslation } from "react-i18next";
+import { LuUserRoundPen } from "react-icons/lu";
+import { Link as RouterLink } from "react-router-dom";
+
+import type { HITLDetail } from "openapi/requests/types.gen";
+import { StateBadge } from "src/components/StateBadge";
+import { Tooltip } from "src/components/ui";
+
+type Props = {
+  readonly dagId: string;
+  readonly pendingActions: Array<HITLDetail>;
+};
+
+export const NeedsReviewBadge = ({ dagId, pendingActions }: Props) => {
+  const { t: translate } = useTranslation("hitl");
+
+  if (pendingActions.length === 0) {
+    return undefined;
+  }
+
+  return (
+    <Tooltip content={translate("requiredActionCount", { count: 
pendingActions.length })}>
+      <RouterLink 
to={`/dags/${dagId}/required_actions?response_received=false`}>
+        <StateBadge colorPalette="deferred" fontSize="md" variant="solid">
+          <LuUserRoundPen />
+          {pendingActions.length}
+        </StateBadge>
+      </RouterLink>
+    </Tooltip>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/components/PendingActionsButton.tsx 
b/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
similarity index 89%
rename from airflow-core/src/airflow/ui/src/components/PendingActionsButton.tsx
rename to airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
index 1ce7be7fd51..5e1b110fa65 100644
--- a/airflow-core/src/airflow/ui/src/components/PendingActionsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
@@ -24,7 +24,7 @@ import { useHumanInTheLoopServiceGetHitlDetails } from 
"openapi/queries";
 
 import { StatsCard } from "./StatsCard";
 
-export const PendingActionsButton = ({
+export const NeedsReviewButton = ({
   dagId,
   runId,
   taskId,
@@ -41,7 +41,7 @@ export const PendingActionsButton = ({
   });
 
   const hitlTIsCount = hitlStatsData?.hitl_details.length ?? 0;
-  const { t: translate } = useTranslation("dashboard");
+  const { t: translate } = useTranslation("hitl");
 
   return hitlTIsCount > 0 ? (
     <Box maxW="250px">
@@ -50,8 +50,8 @@ export const PendingActionsButton = ({
         count={hitlTIsCount}
         icon={<LuUserRoundPen />}
         isLoading={isLoading}
-        label={translate("stats.requiredActions")}
-        link="required_actions?response_received=false"
+        label={translate("requiredAction_other")}
+        link="dags?needs_review=true"
       />
     </Box>
   ) : undefined;
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts 
b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index cc68b52843d..cebc3d074ee 100644
--- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
@@ -37,6 +37,7 @@ export enum SearchParamsKeys {
   MAP_INDEX = "map_index",
   MAPPED = "mapped",
   NAME_PATTERN = "name_pattern",
+  NEEDS_REVIEW = "needs_review",
   OFFSET = "offset",
   OPERATOR = "operator",
   OWNERS = "owners",
diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts 
b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
index 953babc729f..35ba89a46ce 100644
--- a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
+++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
@@ -57,6 +57,7 @@ export const handlers: Array<HttpHandler> = [
       max_active_tasks: 16,
       max_consecutive_failed_dag_runs: 0,
       owners: ["airflow"],
+      pending_actions: [],
       tags: [{ dag_id: "tutorial_taskflow_api_success", name: "example" }],
       timetable_description: "Never, external triggers only",
     };
@@ -93,6 +94,7 @@ export const handlers: Array<HttpHandler> = [
       max_active_tasks: 16,
       max_consecutive_failed_dag_runs: 0,
       owners: ["airflow"],
+      pending_actions: [],
       tags: [{ dag_id: "tutorial_taskflow_api_failed", name: "example" }],
       timetable_description: "Never, external triggers only",
     };
@@ -138,6 +140,7 @@ export const handlers: Array<HttpHandler> = [
       next_dagrun_data_interval_end: null,
       next_dagrun_data_interval_start: null,
       owners: ["airflow"],
+      pending_actions: [],
       tags: [{ dag_id: "tutorial_taskflow_api_failed", name: "example" }],
       timetable_description: "Never, external triggers only",
       timetable_summary: null,
@@ -164,6 +167,7 @@ export const handlers: Array<HttpHandler> = [
       next_dagrun_data_interval_end: null,
       next_dagrun_data_interval_start: null,
       owners: ["airflow"],
+      pending_actions: [],
       tags: [{ dag_id: "tutorial_taskflow_api_success", name: "example" }],
       timetable_description: "Never, external triggers only",
       timetable_summary: null,
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
index 7148f284343..dfb13ef118c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
@@ -30,7 +30,7 @@ import {
 } from "openapi/queries";
 import { AssetEvents } from "src/components/Assets/AssetEvents";
 import { DurationChart } from "src/components/DurationChart";
-import { PendingActionsButton } from "src/components/PendingActionsButton";
+import { NeedsReviewButton } from "src/components/NeedsReviewButton";
 import TimeRangeSelector from "src/components/TimeRangeSelector";
 import { TrendCountButton } from "src/components/TrendCountButton";
 import { useGridRuns } from "src/queries/useGridRuns.ts";
@@ -76,7 +76,7 @@ export const Overview = () => {
 
   return (
     <Box m={4} spaceY={4}>
-      <PendingActionsButton dagId={dagId} />
+      <NeedsReviewButton dagId={dagId} />
       <Box my={2}>
         <TimeRangeSelector
           defaultValue={defaultHour}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
index 5337645d256..b90554f9918 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
@@ -52,6 +52,7 @@ const mockDag = {
   next_dagrun_logical_date: "2024-08-22T00:00:00+00:00",
   next_dagrun_run_after: "2024-08-23T00:00:00+00:00",
   owners: ["airflow"],
+  pending_actions: [],
   relative_fileloc: "nested_task_groups.py",
   tags: [],
   timetable_description: "",
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index e1a8fcf6d4e..b9646d4f2b7 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -24,6 +24,7 @@ import type { DAGWithLatestDagRunsResponse } from 
"openapi/requests/types.gen";
 import DeleteDagButton from "src/components/DagActions/DeleteDagButton";
 import { FavoriteDagButton } from 
"src/components/DagActions/FavoriteDagButton";
 import DagRunInfo from "src/components/DagRunInfo";
+import { NeedsReviewBadge } from "src/components/NeedsReviewBadge";
 import { Stat } from "src/components/Stat";
 import { TogglePause } from "src/components/TogglePause";
 import TriggerDAGButton from "src/components/TriggerDag/TriggerDAGButton";
@@ -56,6 +57,7 @@ export const DagCard = ({ dag }: Props) => {
           <DagTags tags={dag.tags} />
         </HStack>
         <HStack>
+          <NeedsReviewBadge dagId={dag.dag_id} 
pendingActions={dag.pending_actions} />
           <TogglePause
             dagDisplayName={dag.dag_display_name}
             dagId={dag.dag_id}
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
index c2c45eff35c..334f467b77b 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
@@ -36,6 +36,7 @@ import { TagFilter } from "./TagFilter";
 const {
   FAVORITE: FAVORITE_PARAM,
   LAST_DAG_RUN_STATE: LAST_DAG_RUN_STATE_PARAM,
+  NEEDS_REVIEW: NEEDS_REVIEW_PARAM,
   OFFSET: OFFSET_PARAM,
   PAUSED: PAUSED_PARAM,
   TAGS: TAGS_PARAM,
@@ -47,6 +48,7 @@ export const DagsFilters = () => {
 
   const showPaused = searchParams.get(PAUSED_PARAM);
   const showFavorites = searchParams.get(FAVORITE_PARAM);
+  const needsReview = searchParams.get(NEEDS_REVIEW_PARAM);
   const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
   const selectedTags = searchParams.getAll(TAGS_PARAM);
   const tagFilterMode = searchParams.get(TAGS_MATCH_MODE_PARAM) ?? "any";
@@ -112,6 +114,9 @@ export const DagsFilters = () => {
     ({ currentTarget: { value } }) => {
       if (value === "all") {
         searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
+        searchParams.delete(NEEDS_REVIEW_PARAM);
+      } else if (value === "needs_review") {
+        searchParams.set(NEEDS_REVIEW_PARAM, "true");
       } else {
         searchParams.set(LAST_DAG_RUN_STATE_PARAM, value);
       }
@@ -148,6 +153,7 @@ export const DagsFilters = () => {
   const onClearFilters = () => {
     searchParams.delete(PAUSED_PARAM);
     searchParams.delete(FAVORITE_PARAM);
+    searchParams.delete(NEEDS_REVIEW_PARAM);
     searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
     searchParams.delete(TAGS_PARAM);
     searchParams.delete(TAGS_MATCH_MODE_PARAM);
@@ -167,6 +173,7 @@ export const DagsFilters = () => {
   );
 
   const filterCount = getFilterCount({
+    needsReview,
     selectedTags,
     showFavorites,
     showPaused,
@@ -182,6 +189,7 @@ export const DagsFilters = () => {
           isQueued={isQueued}
           isRunning={isRunning}
           isSuccess={isSuccess}
+          needsReview={needsReview === "true"}
           onStateChange={handleStateChange}
         />
         <PausedFilter
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
index 531309e4d58..df9d1df5e04 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
@@ -18,6 +18,7 @@
  */
 import { HStack } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
+import { LuUserRoundPen } from "react-icons/lu";
 
 import { QuickFilterButton } from "src/components/QuickFilterButton";
 import { StateBadge } from "src/components/StateBadge";
@@ -28,11 +29,20 @@ type Props = {
   readonly isQueued: boolean;
   readonly isRunning: boolean;
   readonly isSuccess: boolean;
+  readonly needsReview: boolean;
   readonly onStateChange: React.MouseEventHandler<HTMLButtonElement>;
 };
 
-export const StateFilters = ({ isAll, isFailed, isQueued, isRunning, 
isSuccess, onStateChange }: Props) => {
-  const { t: translate } = useTranslation(["dags", "common"]);
+export const StateFilters = ({
+  isAll,
+  isFailed,
+  isQueued,
+  isRunning,
+  isSuccess,
+  needsReview,
+  onStateChange,
+}: Props) => {
+  const { t: translate } = useTranslation(["dags", "common", "hitl"]);
 
   return (
     <HStack>
@@ -75,6 +85,17 @@ export const StateFilters = ({ isAll, isFailed, isQueued, 
isRunning, isSuccess,
         <StateBadge state="success" />
         {translate("common:states.success")}
       </QuickFilterButton>
+      <QuickFilterButton
+        data-testid="dags-needs-review-filter"
+        isActive={needsReview}
+        onClick={onStateChange}
+        value="needs_review"
+      >
+        <StateBadge colorPalette="deferred">
+          <LuUserRoundPen />
+        </StateBadge>
+        {translate("hitl:requiredAction_other")}
+      </QuickFilterButton>
     </HStack>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
index ac4b32f3ef9..7c3aca6d4ad 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -42,6 +42,7 @@ import { ToggleTableDisplay } from 
"src/components/DataTable/ToggleTableDisplay"
 import type { CardDef } from "src/components/DataTable/types";
 import { useTableURLState } from "src/components/DataTable/useTableUrlState";
 import { ErrorAlert } from "src/components/ErrorAlert";
+import { NeedsReviewBadge } from "src/components/NeedsReviewBadge";
 import { SearchBar } from "src/components/SearchBar";
 import { TogglePause } from "src/components/TogglePause";
 import TriggerDAGButton from "src/components/TriggerDag/TriggerDAGButton";
@@ -141,6 +142,14 @@ const createColumns = (
     enableSorting: false,
     header: () => translate("dagDetails.tags"),
   },
+  {
+    accessorKey: "pending_actions",
+    cell: ({ row: { original: dag } }) => (
+      <NeedsReviewBadge dagId={dag.dag_id} 
pendingActions={dag.pending_actions} />
+    ),
+    enableSorting: false,
+    header: "",
+  },
   {
     accessorKey: "trigger",
     cell: ({ row: { original } }) => (
@@ -171,7 +180,7 @@ const createColumns = (
   },
 ];
 
-const { FAVORITE, LAST_DAG_RUN_STATE, NAME_PATTERN, OWNERS, PAUSED, TAGS, 
TAGS_MATCH_MODE } =
+const { FAVORITE, LAST_DAG_RUN_STATE, NAME_PATTERN, NEEDS_REVIEW, OWNERS, 
PAUSED, TAGS, TAGS_MATCH_MODE } =
   SearchParamsKeys;
 
 const cardDef: CardDef<DAGWithLatestDagRunsResponse> = {
@@ -198,6 +207,7 @@ export const DagsList = () => {
   const lastDagRunState = searchParams.get(LAST_DAG_RUN_STATE) as DagRunState;
   const selectedTags = searchParams.getAll(TAGS);
   const selectedMatchMode = searchParams.get(TAGS_MATCH_MODE) as "all" | "any";
+  const pendingReviews = searchParams.get(NEEDS_REVIEW);
   const owners = searchParams.getAll(OWNERS);
 
   const { setTableURLState, tableURLState } = useTableURLState();
@@ -225,6 +235,7 @@ export const DagsList = () => {
 
   let paused = defaultShowPaused;
   let isFavorite = undefined;
+  let pendingHitl = undefined;
 
   if (showPaused === "all") {
     paused = undefined;
@@ -240,6 +251,12 @@ export const DagsList = () => {
     isFavorite = false;
   }
 
+  if (pendingReviews === "true") {
+    pendingHitl = true;
+  } else if (pendingReviews === "false") {
+    pendingHitl = false;
+  }
+
   const { data, error, isLoading } = useDags({
     dagDisplayNamePattern: Boolean(dagDisplayNamePattern) ? 
dagDisplayNamePattern : undefined,
     dagRunsLimit,
@@ -250,6 +267,7 @@ export const DagsList = () => {
     orderBy: [orderBy],
     owners,
     paused,
+    pendingHitl,
     tags: selectedTags,
     tagsMatchMode: selectedMatchMode,
   });
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
index c53edaea44a..3d98c30dccd 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -38,14 +38,11 @@ export const RecentRuns = ({
 }) => {
   const { t: translate } = useTranslation();
 
-  // Because of the styling (`row-reverse`), we need to reverse the runs so 
that the most recent run is on the right.
-  const reversedRuns = [...latestRuns].reverse();
-
-  if (!reversedRuns.length) {
+  if (!latestRuns.length) {
     return undefined;
   }
 
-  const runsWithDuration = reversedRuns.map((run) => ({
+  const runsWithDuration = latestRuns.map((run) => ({
     ...run,
     duration: 
dayjs.duration(dayjs(run.end_date).diff(run.start_date)).asSeconds(),
   }));
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
index 3b6fb032830..2dc47403b38 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
@@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next";
 import { FiClipboard, FiZap } from "react-icons/fi";
 
 import { useDashboardServiceDagStats } from "openapi/queries";
-import { PendingActionsButton } from "src/components/PendingActionsButton";
+import { NeedsReviewButton } from "src/components/NeedsReviewButton";
 import { StatsCard } from "src/components/StatsCard";
 import { useAutoRefresh } from "src/utils";
 
@@ -50,7 +50,7 @@ export const Stats = () => {
       </Flex>
 
       <HStack gap={4}>
-        <PendingActionsButton />
+        <NeedsReviewButton />
 
         <StatsCard
           colorScheme="failed"
diff --git a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx 
b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx
index ee86309f8cb..86a7ca56e5e 100644
--- a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx
@@ -24,7 +24,7 @@ import { useParams } from "react-router-dom";
 
 import { useTaskInstanceServiceGetTaskInstances } from "openapi/queries";
 import { DurationChart } from "src/components/DurationChart";
-import { PendingActionsButton } from "src/components/PendingActionsButton";
+import { NeedsReviewButton } from "src/components/NeedsReviewButton";
 import TimeRangeSelector from "src/components/TimeRangeSelector";
 import { TrendCountButton } from "src/components/TrendCountButton";
 import { isStatePending, useAutoRefresh } from "src/utils";
@@ -71,7 +71,7 @@ export const Overview = () => {
 
   return (
     <Box m={4} spaceY={4}>
-      <PendingActionsButton taskId={taskId} />
+      <NeedsReviewButton taskId={taskId} />
       <Box my={2}>
         <TimeRangeSelector
           defaultValue={defaultHour}
diff --git a/airflow-core/src/airflow/ui/src/queries/useDags.tsx 
b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
index 349c12f97b5..0fee754ede4 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDags.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
@@ -36,6 +36,7 @@ export const useDags = ({
   orderBy,
   owners,
   paused,
+  pendingHitl,
   tags,
   tagsMatchMode,
 }: {
@@ -50,6 +51,7 @@ export const useDags = ({
   orderBy?: Array<string>;
   owners?: Array<string>;
   paused?: boolean;
+  pendingHitl?: boolean;
   tags?: Array<string>;
   tagsMatchMode?: "all" | "any";
 }) => {
@@ -61,6 +63,7 @@ export const useDags = ({
       dagIdPattern,
       dagRunsLimit,
       excludeStale,
+      hasPendingActions: pendingHitl,
       isFavorite,
       lastDagRunState,
       limit,
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
index 192e461a9f6..36373a84a74 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
@@ -17,12 +17,17 @@
 from __future__ import annotations
 
 from datetime import datetime, timezone
+from typing import TYPE_CHECKING
 from unittest import mock
 
 import pendulum
 import pytest
+from fastapi.testclient import TestClient
+from sqlalchemy.orm import Session
 
 from airflow.models import DagRun
+from airflow.models.hitl import HITLDetail
+from airflow.sdk.timezone import utcnow
 from airflow.utils.session import provide_session
 from airflow.utils.state import DagRunState
 from airflow.utils.types import DagRunTriggeredByType, DagRunType
@@ -36,6 +41,9 @@ from unit.api_fastapi.core_api.routes.public.test_dags import 
(
     TestDagEndpoint as TestPublicDagEndpoint,
 )
 
+if TYPE_CHECKING:
+    from tests_common.pytest_plugin import TaskInstance
+
 pytestmark = pytest.mark.db_test
 
 
@@ -116,6 +124,99 @@ class TestGetDagRuns(TestPublicDagEndpoint):
                     assert previous_run_after > dag_run["run_after"]
                 previous_run_after = dag_run["run_after"]
 
+    @pytest.fixture
+    def setup_hitl_data(self, create_task_instance: TaskInstance, session: 
Session):
+        """Setup HITL test data for parametrized tests."""
+        # 3 Dags (test_dag0 created here and test_dag1, test_dag2 created in 
setup_dag_runs)
+        # 5 task instances in test_dag0
+        TI_COUNT = 5
+        tis = [
+            create_task_instance(
+                dag_id="test_dag0",
+                run_id=f"hitl_run_{ti_i}",
+                task_id=f"test_task_{ti_i}",
+                session=session,
+            )
+            for ti_i in range(TI_COUNT)
+        ]
+        session.add_all(tis)
+        session.commit()
+
+        # test_dag_0 has 3 HITL details not responded, 2 already responded
+        # test_dag_0 has 3 HITL details that have not been responded to, while 
2 have already received responses.
+        hitl_detail_models = [
+            HITLDetail(
+                ti_id=tis[i].id,
+                options=["Approve", "Reject"],
+                subject=f"This is subject {i}",
+                defaults=["Approve"],
+            )
+            for i in range(3)
+        ] + [
+            HITLDetail(
+                ti_id=tis[i].id,
+                options=["Approve", "Reject"],
+                subject=f"This is subject {i}",
+                defaults=["Approve"],
+                response_at=utcnow(),
+                chosen_options=["Approve"],
+                user_id="test",
+            )
+            for i in range(3, 5)
+        ]
+        session.add_all(hitl_detail_models)
+        session.commit()
+
+    @pytest.mark.parametrize(
+        "has_pending_actions, expected_total_entries, 
expected_pending_actions",
+        [
+            # Without has_pending_actions param, should query all DAGs
+            (None, 3, None),
+            # With has_pending_actions=True, should only query DAGs with 
pending actions
+            (
+                True,
+                1,
+                [
+                    {
+                        "task_instance": mock.ANY,
+                        "options": ["Approve", "Reject"],
+                        "subject": f"This is subject {i}",
+                        "defaults": ["Approve"],
+                        "multiple": False,
+                        "params": {},
+                        "params_input": {},
+                        "response_received": False,
+                    }
+                    for i in range(3)
+                ],
+            ),
+        ],
+    )
+    def test_should_return_200_with_hitl(
+        self,
+        test_client: TestClient,
+        setup_hitl_data,
+        has_pending_actions,
+        expected_total_entries,
+        expected_pending_actions,
+    ):
+        # Build query params
+        params = {}
+        if has_pending_actions is not None:
+            params["has_pending_actions"] = has_pending_actions
+
+        # Make request
+        response = test_client.get("/dags", params=params)
+        assert response.status_code == 200
+
+        body = response.json()
+        assert body["total_entries"] == expected_total_entries
+
+        # Check pending_actions structure when specified
+        if expected_pending_actions is not None:
+            for dag_json in body["dags"]:
+                assert dag_json["pending_actions"] == expected_pending_actions
+
     def test_should_response_401(self, unauthenticated_test_client):
         response = unauthenticated_test_client.get("/dags", params={})
         assert response.status_code == 401

Reply via email to