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