This is an automated email from the ASF dual-hosted git repository.

guan404ming 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 e05d6cd3215 Add support for filtering Dags by any DagRun state (#68657)
e05d6cd3215 is described below

commit e05d6cd3215f3ee71388cc5cc91ef058e2d016c2
Author: Guan-Ming Chiu <[email protected]>
AuthorDate: Thu Jul 9 16:00:14 2026 +0800

    Add support for filtering Dags by any DagRun state (#68657)
    
    * Add support for filtering Dags by any DagRun state
    
    * Update 
airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
    
    Co-authored-by: Pierre Jeambrun <[email protected]>
    
    * Switched the fixed width to fit-content
    
    ---------
    
    Co-authored-by: Pierre Jeambrun <[email protected]>
---
 .../src/airflow/api_fastapi/common/parameters.py   | 31 ++++++++
 .../api_fastapi/core_api/openapi/_private_ui.yaml  | 12 +++
 .../airflow/api_fastapi/core_api/routes/ui/dags.py |  3 +
 .../api_fastapi/core_api/routes/ui/dashboard.py    | 44 +++++------
 .../src/airflow/ui/openapi-gen/queries/common.ts   |  5 +-
 .../ui/openapi-gen/queries/ensureQueryData.ts      |  6 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |  6 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |  6 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |  6 +-
 .../ui/openapi-gen/requests/services.gen.ts        |  2 +
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  4 +
 .../airflow/ui/public/i18n/locales/en/dags.json    |  2 +
 .../src/airflow/ui/src/constants/searchParams.ts   |  1 +
 .../DagsList/DagsFilters/DagsFilters.test.tsx      |  7 +-
 .../src/pages/DagsList/DagsFilters/DagsFilters.tsx | 48 ++++++++----
 .../pages/DagsList/DagsFilters/RunStateSelect.tsx  | 87 ++++++++++++++++++++++
 .../pages/DagsList/DagsFilters/StateFilters.tsx    | 75 -------------------
 .../ui/src/pages/DagsList/DagsList.test.tsx        | 14 ++--
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |  3 +
 .../airflow/ui/src/pages/Dashboard/Stats/Stats.tsx |  4 +-
 .../src/airflow/ui/src/queries/useDags.tsx         |  3 +
 .../src/airflow/ui/tests/e2e/pages/DagsPage.ts     | 26 +++----
 .../airflow/ui/tests/e2e/specs/dags-list.spec.ts   |  7 +-
 .../ui/tests/e2e/specs/home-dashboard.spec.ts      |  2 +-
 .../api_fastapi/core_api/routes/ui/test_dags.py    | 26 +++++++
 .../core_api/routes/ui/test_dashboard.py           |  2 +-
 26 files changed, 273 insertions(+), 159 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py 
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index 7b235f07de3..c3a543a2b90 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -1338,11 +1338,42 @@ class _PendingActionsFilter(BaseParam[bool]):
 
 QueryPendingActionsFilter = Annotated[_PendingActionsFilter, 
Depends(_PendingActionsFilter.depends)]
 
+
+class _AnyDagRunStateFilter(BaseParam[DagRunState | None]):
+    """Filter Dags that have any DagRun in the given state, not only the 
latest one."""
+
+    # Only these states have a partial index on dag_run; others would force a 
full table scan.
+    SUPPORTED_STATES = (DagRunState.QUEUED, DagRunState.RUNNING)
+
+    def to_orm(self, select: Select) -> Select:
+        if self.value is None and self.skip_none:
+            return select
+
+        run_subquery = sql_select(DagRun.dag_id).where(DagRun.state == 
self.value).distinct()
+        return select.where(DagModel.dag_id.in_(run_subquery))
+
+    @classmethod
+    def depends(
+        cls,
+        dag_run_state: DagRunState | None = Query(
+            None,
+            description="Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.",
+        ),
+    ) -> _AnyDagRunStateFilter:
+        if dag_run_state is not None and dag_run_state not in 
cls.SUPPORTED_STATES:
+            raise HTTPException(
+                status.HTTP_400_BAD_REQUEST,
+                detail=f"dag_run_state only supports {[state.value for state 
in cls.SUPPORTED_STATES]}.",
+            )
+        return cls().set_value(dag_run_state)
+
+
 # DagRun
 QueryLastDagRunStateFilter = Annotated[
     FilterParam[DagRunState | None],
     Depends(filter_param_factory(DagRun.state, DagRunState | None, 
filter_name="last_dag_run_state")),
 ]
+QueryAnyDagRunStateFilter = Annotated[_AnyDagRunStateFilter, 
Depends(_AnyDagRunStateFilter.depends)]
 
 
 def _transform_dag_run_states(states: Iterable[str] | None) -> 
list[DagRunState | None] | None:
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 173f231a098..3defcad70f9 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
@@ -473,6 +473,18 @@ paths:
           - $ref: '#/components/schemas/DagRunState'
           - type: 'null'
           title: Last Dag Run State
+      - name: dag_run_state
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - $ref: '#/components/schemas/DagRunState'
+          - type: 'null'
+          description: Filter Dags that have any DagRun in the given state. 
Only ``queued``
+            and ``running`` are supported.
+          title: Dag Run State
+        description: Filter Dags that have any DagRun in the given state. Only 
``queued``
+          and ``running`` are supported.
       - name: bundle_name
         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 15515a92bb8..38d0c93cef8 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
@@ -32,6 +32,7 @@ from airflow.api_fastapi.common.db.dags import 
generate_dag_with_latest_run_quer
 from airflow.api_fastapi.common.parameters import (
     FilterOptionEnum,
     FilterParam,
+    QueryAnyDagRunStateFilter,
     QueryAssetDependencyFilter,
     QueryBundleNameFilter,
     QueryBundleVersionFilter,
@@ -107,6 +108,7 @@ def get_dags(
     paused: QueryPausedFilter,
     has_import_errors: QueryHasImportErrorsFilter,
     last_dag_run_state: QueryLastDagRunStateFilter,
+    dag_run_state: QueryAnyDagRunStateFilter,
     bundle_name: QueryBundleNameFilter,
     bundle_version: QueryBundleVersionFilter,
     order_by: Annotated[
@@ -152,6 +154,7 @@ def get_dags(
             tags,
             owners,
             last_dag_run_state,
+            dag_run_state,
             is_favorite,
             has_asset_schedule,
             asset_dependency,
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py
index 612d5c7c35c..0498082a4ed 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py
@@ -129,16 +129,6 @@ def dag_stats(
     """Return basic Dag stats with counts of Dags in various states."""
     permitted_dag_ids = cast("set[str]", readable_dags_filter.value)
 
-    # Active Dags need another query from DagModel, as a Dag may not have any 
runs but still be active
-    active_count_query = (
-        select(func.count())
-        .select_from(DagModel)
-        .where(DagModel.is_stale == false())
-        .where(DagModel.is_paused == false())
-        .where(DagModel.dag_id.in_(permitted_dag_ids))
-    )
-    active_count = session.execute(active_count_query).scalar_one()
-
     latest_state = (
         select(DagRun.state)
         .where(DagRun.dag_id == DagModel.dag_id, 
DagRun.logical_date.is_not(None))
@@ -147,26 +137,30 @@ def dag_stats(
         .correlate(DagModel)
         .scalar_subquery()
     )
-    latest_runs_subq = (
-        select(latest_state.label("state"))
+    dag_counts_query = (
+        select(
+            func.coalesce(func.sum(case((DagModel.is_paused == false(), 1))), 
0).label("active"),
+            func.coalesce(func.sum(case((latest_state == DagRunState.FAILED, 
1))), 0).label("failed"),
+        )
         .select_from(DagModel)
         .where(DagModel.is_stale == false())
         .where(DagModel.dag_id.in_(permitted_dag_ids))
-        .subquery()
     )
-    combined_runs_query = select(
-        func.coalesce(func.sum(case((latest_runs_subq.c.state == 
DagRunState.FAILED, 1))), 0).label("failed"),
-        func.coalesce(func.sum(case((latest_runs_subq.c.state == 
DagRunState.RUNNING, 1))), 0).label(
-            "running"
-        ),
-        func.coalesce(func.sum(case((latest_runs_subq.c.state == 
DagRunState.QUEUED, 1))), 0).label("queued"),
-    ).select_from(latest_runs_subq)
+    dag_counts = session.execute(dag_counts_query).one()
 
-    counts = session.execute(combined_runs_query).one()
+    active_states_query = (
+        select(DagRun.state, func.count(func.distinct(DagRun.dag_id)))
+        .join(DagModel, DagModel.dag_id == DagRun.dag_id)
+        .where(DagModel.is_stale == false())
+        .where(DagRun.dag_id.in_(permitted_dag_ids))
+        .where(DagRun.state.in_([DagRunState.RUNNING, DagRunState.QUEUED]))
+        .group_by(DagRun.state)
+    )
+    active_counts = {state: count for state, count in 
session.execute(active_states_query)}
 
     return DashboardDagStatsResponse(
-        active_dag_count=active_count,
-        failed_dag_count=counts.failed,
-        running_dag_count=counts.running,
-        queued_dag_count=counts.queued,
+        active_dag_count=dag_counts.active,
+        failed_dag_count=dag_counts.failed,
+        running_dag_count=active_counts.get(DagRunState.RUNNING, 0),
+        queued_dag_count=active_counts.get(DagRunState.QUEUED, 0),
     )
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 769f3eb49fa..2d6dc7ffe7d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -327,7 +327,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, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
+export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -337,6 +337,7 @@ export const UseDagServiceGetDagsUiKeyFn = ({ 
assetDependency, bundleName, bundl
   dagIdPrefixPattern?: string;
   dagIds?: string[];
   dagRunsLimit?: number;
+  dagRunState?: DagRunState;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
   hasImportErrors?: boolean;
@@ -350,7 +351,7 @@ export const UseDagServiceGetDagsUiKeyFn = ({ 
assetDependency, bundleName, bundl
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}, queryKey?: Array<unknown>) => [useDagServiceGetDagsUiKey, ...(queryKey 
?? [{ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }])];
+} = {}, queryKey?: Array<unknown>) => [useDagServiceGetDagsUiKey, ...(queryKey 
?? [{ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
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 712d2ee99dd..6eba7e9a9db 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -665,6 +665,7 @@ export const ensureUseDagServiceGetDagTagsData = 
(queryClient: QueryClient, { li
 * @param data.paused
 * @param data.hasImportErrors Filter Dags by having import errors. Only Dags 
that have been successfully loaded before will be returned.
 * @param data.lastDagRunState
+* @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
 * @param data.bundleName
 * @param data.bundleVersion
 * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
@@ -675,7 +676,7 @@ export const ensureUseDagServiceGetDagTagsData = 
(queryClient: QueryClient, { li
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
+export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -685,6 +686,7 @@ export const ensureUseDagServiceGetDagsUiData = 
(queryClient: QueryClient, { ass
   dagIdPrefixPattern?: string;
   dagIds?: string[];
   dagRunsLimit?: number;
+  dagRunState?: DagRunState;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
   hasImportErrors?: boolean;
@@ -698,7 +700,7 @@ export const ensureUseDagServiceGetDagsUiData = 
(queryClient: QueryClient, { ass
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode 
}), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bundleVersion, dagDispla [...]
+} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode 
}), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bundleVersi [...]
 /**
 * 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 1614a33ac5d..037f6d01a39 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -665,6 +665,7 @@ export const prefetchUseDagServiceGetDagTags = 
(queryClient: QueryClient, { limi
 * @param data.paused
 * @param data.hasImportErrors Filter Dags by having import errors. Only Dags 
that have been successfully loaded before will be returned.
 * @param data.lastDagRunState
+* @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
 * @param data.bundleName
 * @param data.bundleVersion
 * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
@@ -675,7 +676,7 @@ export const prefetchUseDagServiceGetDagTags = 
(queryClient: QueryClient, { limi
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
+export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -685,6 +686,7 @@ export const prefetchUseDagServiceGetDagsUi = (queryClient: 
QueryClient, { asset
   dagIdPrefixPattern?: string;
   dagIds?: string[];
   dagRunsLimit?: number;
+  dagRunState?: DagRunState;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
   hasImportErrors?: boolean;
@@ -698,7 +700,7 @@ export const prefetchUseDagServiceGetDagsUi = (queryClient: 
QueryClient, { asset
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode 
}), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bundleVersion, dagDisplayN [...]
+} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode 
}), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bundleVersion [...]
 /**
 * 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 9f326c03b1b..09ddcdd3837 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -665,6 +665,7 @@ export const useDagServiceGetDagTags = <TData = 
Common.DagServiceGetDagTagsDefau
 * @param data.paused
 * @param data.hasImportErrors Filter Dags by having import errors. Only Dags 
that have been successfully loaded before will be returned.
 * @param data.lastDagRunState
+* @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
 * @param data.bundleName
 * @param data.bundleVersion
 * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
@@ -675,7 +676,7 @@ export const useDagServiceGetDagTags = <TData = 
Common.DagServiceGetDagTagsDefau
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, excludeStale, hasAssetSchedule, 
hasImportErrors, hasPendingActions, 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, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -685,6 +686,7 @@ export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefault
   dagIdPrefixPattern?: string;
   dagIds?: string[];
   dagRunsLimit?: number;
+  dagRunState?: DagRunState;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
   hasImportErrors?: boolean;
@@ -698,7 +700,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, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }, 
qu [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMa [...]
 /**
 * 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 d2d51a73acd..e3ec8836309 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -665,6 +665,7 @@ export const useDagServiceGetDagTagsSuspense = <TData = 
Common.DagServiceGetDagT
 * @param data.paused
 * @param data.hasImportErrors Filter Dags by having import errors. Only Dags 
that have been successfully loaded before will be returned.
 * @param data.lastDagRunState
+* @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
 * @param data.bundleName
 * @param data.bundleVersion
 * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
@@ -675,7 +676,7 @@ export const useDagServiceGetDagTagsSuspense = <TData = 
Common.DagServiceGetDagT
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, excludeStale, hasAssetSchedule, 
hasImportErrors, hasPendingActions, 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, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -685,6 +686,7 @@ export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsU
   dagIdPrefixPattern?: string;
   dagIds?: string[];
   dagRunsLimit?: number;
+  dagRunState?: DagRunState;
   excludeStale?: boolean;
   hasAssetSchedule?: boolean;
   hasImportErrors?: boolean;
@@ -698,7 +700,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, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMo [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags [...]
 /**
 * Get Latest Run Info
 * Get latest run.
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 06842b72e7d..78241796546 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
@@ -1923,6 +1923,7 @@ export class DagService {
      * @param data.paused
      * @param data.hasImportErrors Filter Dags by having import errors. Only 
Dags that have been successfully loaded before will be returned.
      * @param data.lastDagRunState
+     * @param data.dagRunState Filter Dags that have any DagRun in the given 
state. Only ``queued`` and ``running`` are supported.
      * @param data.bundleName
      * @param data.bundleVersion
      * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
@@ -1953,6 +1954,7 @@ export class DagService {
                 paused: data.paused,
                 has_import_errors: data.hasImportErrors,
                 last_dag_run_state: data.lastDagRunState,
+                dag_run_state: data.dagRunState,
                 bundle_name: data.bundleName,
                 bundle_version: data.bundleVersion,
                 order_by: data.orderBy,
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 103c071ec87..e1bf23b1602 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
@@ -3490,6 +3490,10 @@ export type GetDagsUiData = {
     dagIdPrefixPattern?: string | null;
     dagIds?: Array<(string)> | null;
     dagRunsLimit?: number;
+    /**
+     * Filter Dags that have any DagRun in the given state. Only ``queued`` 
and ``running`` are supported.
+     */
+    dagRunState?: DagRunState | null;
     excludeStale?: boolean;
     /**
      * Filter Dags with asset-based scheduling
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json
index 990d6c7848f..c8b195c93b8 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json
@@ -10,11 +10,13 @@
   "filters": {
     "allRunTypes": "All Run Types",
     "allStates": "All States",
+    "anyRunState": "Any run",
     "favorite": {
       "all": "All",
       "favorite": "Favorite",
       "unfavorite": "Unfavorite"
     },
+    "lastRunState": "Last run",
     "paused": {
       "active": "Active",
       "all": "All",
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts 
b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index f3583d449ce..b6f4d88e3d0 100644
--- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
@@ -31,6 +31,7 @@ export enum SearchParamsKeys {
   DAG_DISPLAY_NAME_PATTERN = "dag_display_name_pattern",
   DAG_ID = "dag_id",
   DAG_ID_PATTERN = "dag_id_pattern",
+  DAG_RUN_STATE = "dag_run_state",
   DAG_VERSION = "dag_version",
   DAG_VIEW = "view",
   DEADLINE_TIME_GTE = "deadline_time_gte",
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx
 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx
index 4a28ebe8087..aac7b895ae4 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx
@@ -52,11 +52,8 @@ describe("Paused filter with hide_paused_dags_by_default 
enabled", () => {
     await waitFor(() => 
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument());
     expect(screen.queryByText("paused_dag")).not.toBeInTheDocument();
 
-    // There are two "All" buttons (StateFilters and PausedFilter).
-    // The second one belongs to PausedFilter.
-    const allButtons = screen.getAllByText("filters.paused.all");
-
-    allButtons[1]?.click();
+    // PausedFilter is the only filter using the "All" (filters.paused.all) 
label.
+    screen.getByText("filters.paused.all").click();
     await waitFor(() => 
expect(screen.getByText("paused_dag")).toBeInTheDocument());
     
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument();
   });
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 a8e018bc121..9f1863bc120 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
@@ -16,9 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, HStack } from "@chakra-ui/react";
+import { HStack } from "@chakra-ui/react";
 import type { MultiValue } from "chakra-react-select";
 import { useState } from "react";
+import { useTranslation } from "react-i18next";
 import { useSearchParams } from "react-router-dom";
 
 import { useTableURLState } from "src/components/DataTable/useTableUrlState";
@@ -30,10 +31,11 @@ import { useTagFilter } from "../useTagFilter";
 import { FavoriteFilter } from "./FavoriteFilter";
 import { PausedFilter } from "./PausedFilter";
 import { RequiredActionFilter } from "./RequiredActionFilter";
-import { StateFilters } from "./StateFilters";
+import { RunStateSelect } from "./RunStateSelect";
 import { TagFilter } from "./TagFilter";
 
 const {
+  DAG_RUN_STATE: DAG_RUN_STATE_PARAM,
   FAVORITE: FAVORITE_PARAM,
   LAST_DAG_RUN_STATE: LAST_DAG_RUN_STATE_PARAM,
   NEEDS_REVIEW: NEEDS_REVIEW_PARAM,
@@ -41,15 +43,13 @@ const {
   PAUSED: PAUSED_PARAM,
 }: SearchParamsKeysType = SearchParamsKeys;
 
-type StateValue = "all" | "failed" | "queued" | "running" | "success";
 type BooleanFilterValue = "all" | "false" | "true";
 
-const stateValues: ReadonlyArray<StateValue> = ["failed", "queued", "running", 
"success"];
+const lastRunStates = ["failed", "queued", "running", "success"] as const;
+// Mirrors the backend limit (see _AnyDagRunStateFilter): only running/queued 
are indexed on dag_run.
+const anyRunStates = ["queued", "running"] as const;
 const booleanFilterValues: ReadonlyArray<BooleanFilterValue> = ["all", "true", 
"false"];
 
-const toStateValue = (value: string | null): StateValue =>
-  stateValues.includes(value as StateValue) ? (value as StateValue) : "all";
-
 const toBooleanFilterValue = (
   value: string | null,
   defaultValue: BooleanFilterValue = "all",
@@ -58,12 +58,14 @@ const toBooleanFilterValue = (
 
 export const DagsFilters = () => {
   const [searchParams, setSearchParams] = useSearchParams();
+  const { t: translate } = useTranslation("dags");
   const { selectedTags, setSelectedTags, setTagFilterMode, tagFilterMode } = 
useTagFilter();
 
   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 activeRunState = searchParams.get(DAG_RUN_STATE_PARAM);
 
   const [pattern, setPattern] = useState("");
 
@@ -107,8 +109,8 @@ export const DagsFilters = () => {
     setSearchParams(searchParams);
   };
 
-  const handleStateChange = (value: StateValue) => {
-    if (value === "all") {
+  const handleStateChange = (value: string | undefined) => {
+    if (value === undefined) {
       searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
     } else {
       searchParams.set(LAST_DAG_RUN_STATE_PARAM, value);
@@ -117,6 +119,16 @@ export const DagsFilters = () => {
     setSearchParams(searchParams);
   };
 
+  const handleActiveRunChange = (value: string | undefined) => {
+    if (value === undefined) {
+      searchParams.delete(DAG_RUN_STATE_PARAM);
+    } else {
+      searchParams.set(DAG_RUN_STATE_PARAM, value);
+    }
+    resetPagination();
+    setSearchParams(searchParams);
+  };
+
   const handleNeedsReviewToggle = () => {
     if (needsReview === "true") {
       searchParams.delete(NEEDS_REVIEW_PARAM);
@@ -140,15 +152,25 @@ export const DagsFilters = () => {
     setTagFilterMode(checked ? "all" : "any");
   };
 
-  const stateValue = toStateValue(state);
   const pausedValue = toBooleanFilterValue(showPaused, defaultShowPaused);
   const favoriteValue = toBooleanFilterValue(showFavorites);
 
   return (
     <HStack flexWrap="wrap" gap={2} justifyContent="space-between">
-      <Box overflowX="auto">
-        <StateFilters onChange={handleStateChange} value={stateValue} />
-      </Box>
+      <RunStateSelect
+        dataTestId="dags-last-run-state-filter"
+        label={translate("filters.lastRunState")}
+        onChange={handleStateChange}
+        states={lastRunStates}
+        value={state ?? undefined}
+      />
+      <RunStateSelect
+        dataTestId="dags-any-run-state-filter"
+        label={translate("filters.anyRunState")}
+        onChange={handleActiveRunChange}
+        states={anyRunStates}
+        value={activeRunState ?? undefined}
+      />
       <RequiredActionFilter needsReview={needsReview === "true"} 
onToggle={handleNeedsReviewToggle} />
       <PausedFilter onChange={handlePausedChange} value={pausedValue} />
       <TagFilter
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RunStateSelect.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RunStateSelect.tsx
new file mode 100644
index 00000000000..38a7cf59ede
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RunStateSelect.tsx
@@ -0,0 +1,87 @@
+/*!
+ * 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 { HStack, Text } from "@chakra-ui/react";
+import { createListCollection } from "@chakra-ui/react/collection";
+import { useTranslation } from "react-i18next";
+import { FiList } from "react-icons/fi";
+
+import { StateBadge } from "src/components/StateBadge";
+import { Select } from "src/components/ui";
+
+type RunState = "failed" | "queued" | "running" | "success";
+
+type Props = {
+  readonly dataTestId?: string;
+  readonly label: string;
+  readonly onChange: (value: string | undefined) => void;
+  readonly states: ReadonlyArray<RunState>;
+  readonly value: string | undefined;
+};
+
+const ALL_VALUE = "all";
+
+export const RunStateSelect = ({ dataTestId, label, onChange, states, value }: 
Props) => {
+  const { t: translate } = useTranslation(["dags", "common"]);
+
+  const collection = createListCollection({
+    items: [
+      { label: translate("dags:filters.allStates"), state: undefined, value: 
ALL_VALUE },
+      ...states.map((state) => ({ label: translate(`common:states.${state}`), 
state, value: state })),
+    ],
+  });
+
+  const current = collection.items.find((item) => item.value === (value ?? 
ALL_VALUE));
+
+  return (
+    <Select.Root
+      collection={collection}
+      data-testid={dataTestId}
+      minWidth="232px"
+      onValueChange={({ value: selected }) => onChange(selected[0] === 
ALL_VALUE ? undefined : selected[0])}
+      value={[value ?? ALL_VALUE]}
+      width="fit-content"
+    >
+      <Select.Trigger>
+        <HStack gap={2} justifyContent="space-between" pe={5} width="full">
+          <Text color="fg.muted" whiteSpace="nowrap">
+            {label}:
+          </Text>
+          <HStack gap={2}>
+            {current?.state === undefined ? <FiList /> : <StateBadge 
state={current.state} />}
+            <Text whiteSpace="nowrap">{current?.label}</Text>
+          </HStack>
+        </HStack>
+      </Select.Trigger>
+      <Select.Content>
+        {collection.items.map((item) => (
+          <Select.Item
+            data-testid={dataTestId === undefined ? undefined : 
`${dataTestId}-${item.value}`}
+            item={item}
+            key={item.value}
+          >
+            <HStack gap={2}>
+              {item.state === undefined ? <FiList /> : <StateBadge 
state={item.state} />}
+              <Text>{item.label}</Text>
+            </HStack>
+          </Select.Item>
+        ))}
+      </Select.Content>
+    </Select.Root>
+  );
+};
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
deleted file mode 100644
index 47dc79953b8..00000000000
--- 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-/*!
- * 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 { StateBadge } from "src/components/StateBadge";
-import { ButtonGroupToggle, type ButtonGroupOption } from 
"src/components/ui/ButtonGroupToggle";
-
-type StateValue = "all" | "failed" | "queued" | "running" | "success";
-
-type Props = {
-  readonly onChange: (value: StateValue) => void;
-  readonly value: StateValue;
-};
-
-export const StateFilters = ({ onChange, value }: Props) => {
-  const { t: translate } = useTranslation(["dags", "common"]);
-
-  const options: Array<ButtonGroupOption<StateValue>> = [
-    { label: translate("dags:filters.paused.all"), value: "all" },
-    {
-      label: (
-        <>
-          <StateBadge state="failed" />
-          {translate("common:states.failed")}
-        </>
-      ),
-      value: "failed",
-    },
-    {
-      label: (
-        <>
-          <StateBadge state="queued" />
-          {translate("common:states.queued")}
-        </>
-      ),
-      value: "queued",
-    },
-    {
-      label: (
-        <>
-          <StateBadge state="running" />
-          {translate("common:states.running")}
-        </>
-      ),
-      value: "running",
-    },
-    {
-      label: (
-        <>
-          <StateBadge state="success" />
-          {translate("common:states.success")}
-        </>
-      ),
-      value: "success",
-    },
-  ];
-
-  return <ButtonGroupToggle<StateValue> onChange={onChange} options={options} 
value={value} />;
-};
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
index 1f840cc5d40..271542fc5c0 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import "@testing-library/jest-dom";
-import { render, screen, waitFor } from "@testing-library/react";
+import { render, screen, waitFor, within } from "@testing-library/react";
 import { describe, it, expect } from "vitest";
 
 import { AppWrapper } from "src/utils/AppWrapper";
@@ -26,12 +26,16 @@ describe("Dag Filters", () => {
   it("Filter by selected last run state", async () => {
     render(<AppWrapper initialEntries={["/dags"]} />);
 
-    await waitFor(() => 
expect(screen.getByText("states.success")).toBeInTheDocument());
-    await waitFor(() => screen.getByText("states.success").click());
     await waitFor(() => 
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument());
 
-    await waitFor(() => 
expect(screen.getByText("states.failed")).toBeInTheDocument());
-    await waitFor(() => screen.getByText("states.failed").click());
+    const trigger = 
within(screen.getByTestId("dags-last-run-state-filter")).getByRole("combobox");
+
+    await waitFor(() => trigger.click());
+    await waitFor(() => 
screen.getByTestId("dags-last-run-state-filter-success").click());
+    await waitFor(() => 
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument());
+
+    await waitFor(() => trigger.click());
+    await waitFor(() => 
screen.getByTestId("dags-last-run-state-filter-failed").click());
     await waitFor(() => 
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument());
   });
 });
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 12e08fdb51b..8b315e99342 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -193,6 +193,7 @@ const createColumns = (
 ];
 
 const {
+  DAG_RUN_STATE,
   FAVORITE,
   LAST_DAG_RUN_STATE,
   NAME_PATTERN,
@@ -229,6 +230,7 @@ export const DagsList = () => {
   const showFavorites = searchParams.get(FAVORITE);
 
   const lastDagRunState = searchParams.get(LAST_DAG_RUN_STATE) as DagRunState;
+  const dagRunState = searchParams.get(DAG_RUN_STATE) as DagRunState;
   const { selectedTags, tagFilterMode: selectedMatchMode } = useTagFilter();
   const pendingReviews = searchParams.get(NEEDS_REVIEW);
   const owners = searchParams.getAll(OWNERS);
@@ -284,6 +286,7 @@ export const DagsList = () => {
     advancedSearch: advancedSearch.enabled,
     dagDisplayNamePattern: Boolean(dagDisplayNamePattern) ? 
dagDisplayNamePattern : undefined,
     dagRunsLimit,
+    dagRunState,
     isFavorite,
     lastDagRunState,
     limit: pagination.pageSize,
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 25545dfac50..cedbdb7ba64 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
@@ -75,7 +75,7 @@ export const Stats = () => {
             isLoading={isStatsLoading}
             isRTL={isRTL}
             label={translate("stats.queuedDags")}
-            link="dags?last_dag_run_state=queued"
+            link="dags?dag_run_state=queued"
             state="queued"
           />
         ) : undefined}
@@ -86,7 +86,7 @@ export const Stats = () => {
           isLoading={isStatsLoading}
           isRTL={isRTL}
           label={translate("stats.runningDags")}
-          link="dags?last_dag_run_state=running"
+          link="dags?dag_run_state=running"
           state="running"
         />
 
diff --git a/airflow-core/src/airflow/ui/src/queries/useDags.tsx 
b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
index 4be8244ce73..ec4dfb18774 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDags.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
@@ -29,6 +29,7 @@ export const useDags = ({
   dagDisplayNamePattern,
   dagIdPattern,
   dagRunsLimit,
+  dagRunState,
   excludeStale = true,
   isFavorite,
   lastDagRunState,
@@ -45,6 +46,7 @@ export const useDags = ({
   dagDisplayNamePattern?: string;
   dagIdPattern?: string;
   dagRunsLimit: number;
+  dagRunState?: DagRunState;
   excludeStale?: boolean;
   isFavorite?: boolean;
   lastDagRunState?: DagRunState;
@@ -65,6 +67,7 @@ export const useDags = ({
         ? { dagDisplayNamePattern, dagIdPattern }
         : { dagDisplayNamePrefixPattern: dagDisplayNamePattern, 
dagIdPrefixPattern: dagIdPattern }),
       dagRunsLimit,
+      dagRunState,
       excludeStale,
       hasPendingActions: pendingHitl,
       isFavorite,
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
index f83134d6f93..bcf9d0114d3 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
@@ -32,17 +32,14 @@ export class DagsPage extends BasePage {
 
   public readonly cardViewButton: Locator;
   public readonly confirmButton: Locator;
-  public readonly failedFilter: Locator;
   public readonly hitlReviewModal: HITLReviewModal;
+  public readonly lastRunStateFilter: Locator;
   public readonly needsReviewBadges: Locator;
   public readonly needsReviewFilter: Locator;
   public readonly operatorFilter: Locator;
-  public readonly queuedFilter: Locator;
   public readonly retriesFilter: Locator;
-  public readonly runningFilter: Locator;
   public readonly searchBox: Locator;
   public readonly searchInput: Locator;
-  public readonly successFilter: Locator;
   public readonly tableViewButton: Locator;
   public readonly triggerButton: Locator;
   public readonly triggerRuleFilter: Locator;
@@ -64,12 +61,9 @@ export class DagsPage extends BasePage {
     this.retriesFilter = page.getByTestId("retries-filter");
     this.cardViewButton = page.getByRole("button", { name: "Show card view" });
     this.tableViewButton = page.getByRole("button", { name: "Show table view" 
});
-    this.successFilter = page.getByRole("button", { name: "Success" });
-    this.failedFilter = page.getByRole("button", { name: "Failed" });
+    this.lastRunStateFilter = page.getByTestId("dags-last-run-state-filter");
     this.hitlReviewModal = new HITLReviewModal(page);
     this.needsReviewBadges = page.getByTestId("needs-review-badge");
-    this.runningFilter = page.getByRole("button", { name: "Running" });
-    this.queuedFilter = page.getByRole("button", { name: "Queued" });
     // Uses testId because this button's text is driven by an i18n key.
     this.needsReviewFilter = page.getByTestId("dags-needs-review-filter");
   }
@@ -118,14 +112,6 @@ export class DagsPage extends BasePage {
   public async filterByStatus(
     status: "failed" | "needs_review" | "queued" | "running" | "success",
   ): Promise<void> {
-    const filterMap: Record<typeof status, Locator> = {
-      failed: this.failedFilter,
-      needs_review: this.needsReviewFilter,
-      queued: this.queuedFilter,
-      running: this.runningFilter,
-      success: this.successFilter,
-    };
-
     // Set up response listener before the click so we don't miss a fast 
response.
     const responsePromise = this.page
       .waitForResponse((resp: Response) => resp.url().includes("/api/v2/dags") 
&& resp.status() === 200, {
@@ -137,7 +123,13 @@ export class DagsPage extends BasePage {
         }
       });
 
-    await filterMap[status].click();
+    if (status === "needs_review") {
+      await this.needsReviewFilter.click();
+    } else {
+      // Run-state filters live in the "Last run" dropdown.
+      await this.lastRunStateFilter.getByRole("combobox").click();
+      await 
this.page.getByTestId(`dags-last-run-state-filter-${status}`).click();
+    }
     await responsePromise;
   }
 
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
index 798a910a52e..5198bb6e26a 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
@@ -192,15 +192,12 @@ test.describe("Dags Search", () => {
 });
 
 test.describe("Dags Status Filtering", () => {
-  test("should display status filter buttons", async ({ dagsPage }) => {
+  test("should filter Dags by run state", async ({ dagsPage }) => {
     test.slow();
     await dagsPage.navigate();
     await dagsPage.waitForDagList();
 
-    await expect(dagsPage.successFilter).toBeVisible();
-    await expect(dagsPage.failedFilter).toBeVisible();
-    await expect(dagsPage.runningFilter).toBeVisible();
-    await expect(dagsPage.queuedFilter).toBeVisible();
+    await expect(dagsPage.lastRunStateFilter).toBeVisible();
 
     await dagsPage.filterByStatus("success");
     await dagsPage.waitForDagList();
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
index e74669e6b77..01169854e90 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
@@ -64,7 +64,7 @@ test.describe("Dashboard Metrics Display", () => {
     await homePage.waitForDashboardLoad();
 
     await homePage.runningDagsCard.click();
-    await expect(homePage.page).toHaveURL(/last_dag_run_state=running/);
+    await expect(homePage.page).toHaveURL(/dag_run_state=running/);
   });
 
   test("should display welcome heading on dashboard", async ({ homePage }) => {
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 50444e04914..4e561fd3436 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
@@ -22,6 +22,7 @@ from unittest import mock
 import pendulum
 import pytest
 from fastapi.testclient import TestClient
+from sqlalchemy import select
 from sqlalchemy.orm import Session
 
 from airflow.api_fastapi.auth.managers.models.resource_details import 
DagAccessEntity
@@ -134,6 +135,31 @@ class TestGetDagRuns(TestPublicDagEndpoint):
                     assert previous_run_after > dag_run["run_after"]
                 previous_run_after = dag_run["run_after"]
 
+    @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
+    def test_dag_run_state_matches_any_run_not_only_latest(self, test_client, 
session):
+        # Backwards backfill: an older run is still running while the latest 
run already finished.
+        older_run = session.scalar(
+            select(DagRun).where(DagRun.dag_id == DAG1_ID, DagRun.run_id == 
"run_id_1")
+        )
+        older_run.state = DagRunState.RUNNING
+        session.commit()
+
+        # last_dag_run_state only looks at the latest run, which is not running
+        last_state = test_client.get("/dags", params={"last_dag_run_state": 
"running"})
+        assert last_state.status_code == 200
+        assert [dag["dag_id"] for dag in last_state.json()["dags"]] == []
+
+        # dag_run_state matches a Dag that has any run in the state
+        any_state = test_client.get("/dags", params={"dag_run_state": 
"running"})
+        assert any_state.status_code == 200
+        assert [dag["dag_id"] for dag in any_state.json()["dags"]] == [DAG1_ID]
+
+    @pytest.mark.parametrize("unsupported_state", ["success", "failed"])
+    def test_dag_run_state_rejects_unsupported_states(self, test_client, 
unsupported_state):
+        # Only running/queued have a partial index; other states would force a 
full table scan.
+        response = test_client.get("/dags", params={"dag_run_state": 
unsupported_state})
+        assert response.status_code == 400
+
     @pytest.fixture
     def setup_hitl_data(self, create_task_instance: TaskInstance, session: 
Session):
         """Setup HITL test data for parametrized tests."""
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dashboard.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dashboard.py
index da46c51da1b..50869e0ee9f 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dashboard.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dashboard.py
@@ -381,7 +381,7 @@ class TestDagStatsEndpoint:
         assert response.json() == {
             "active_dag_count": 1,
             "failed_dag_count": 0,
-            "running_dag_count": 0,
+            "running_dag_count": 1,
             "queued_dag_count": 1,
         }
 

Reply via email to