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 f54c8eef484 UI: Add timetable type filter to Dag list (#69728)
f54c8eef484 is described below

commit f54c8eef484d4547ea0780ae56344b7c834f3753
Author: Shivam Rastogi <[email protected]>
AuthorDate: Wed Jul 29 08:15:56 2026 -0700

    UI: Add timetable type filter to Dag list (#69728)
    
    * UI: Add timetable type filter to Dag list
    
    Users could filter by timetable type through the public API, but the Dag 
list uses the private UI endpoint. This makes the same capability available 
from the page users actually use.
    
    * UI: Keep timetable filtering consistent with Dag search
    
    Keeping these text inputs on one implementation prevents their debounce, 
clearing, and URL synchronization behavior from drifting.
    
    * UI: Add discoverable timetable type filtering
    
    Dags can use built-in or plugin timetables whose persisted type names are 
not discoverable from free text alone. Exposing readable values lets users 
narrow large Dag lists without knowing the exact class path.
    
    * UI: Reuse multi-select for Dag filters
    
    Allow selecting multiple timetable types with OR semantics and share the 
tag-style select presentation between tag and timetable filters.
    
    * Remove timetable filter newsfragment
    
    * Regenerate private UI API specification
---
 .../src/airflow/api_fastapi/common/parameters.py   |  4 +
 .../api_fastapi/core_api/datamodels/ui/dags.py     |  7 ++
 .../api_fastapi/core_api/openapi/_private_ui.yaml  | 76 ++++++++++++++++++
 .../airflow/api_fastapi/core_api/routes/ui/dags.py | 44 +++++++++-
 .../src/airflow/ui/openapi-gen/queries/common.ts   | 13 ++-
 .../ui/openapi-gen/queries/ensureQueryData.ts      | 21 ++++-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts | 21 ++++-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  | 21 ++++-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts | 21 ++++-
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 20 +++++
 .../ui/openapi-gen/requests/services.gen.ts        | 29 ++++++-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   | 35 ++++++++
 .../airflow/ui/public/i18n/locales/en/dags.json    |  4 +-
 .../src/airflow/ui/src/constants/searchParams.ts   |  1 +
 .../src/airflow/ui/src/mocks/handlers/dags.ts      | 26 +++++-
 .../DagsList/DagsFilters/DagsFilterSelect.tsx      | 83 +++++++++++++++++++
 .../DagsList/DagsFilters/DagsFilters.test.tsx      | 80 ++++++++++++++++++-
 .../src/pages/DagsList/DagsFilters/DagsFilters.tsx | 70 +++++++++++-----
 .../src/pages/DagsList/DagsFilters/TagFilter.tsx   | 60 ++++----------
 .../src/pages/DagsList/DagsFilters/TeamFilter.tsx  |  5 +-
 .../DagsList/DagsFilters/TimetableTypeFilter.tsx   | 58 ++++++++++++++
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |  3 +
 .../ui/src/queries/useDagTimetableTypesInfinite.ts | 68 ++++++++++++++++
 .../src/airflow/ui/src/queries/useDags.tsx         |  3 +
 .../api_fastapi/core_api/routes/ui/test_dags.py    | 93 ++++++++++++++++++++++
 25 files changed, 783 insertions(+), 83 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py 
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index 72b096c2b75..1284fdacb66 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -1304,6 +1304,10 @@ QueryDagDisplayNamePrefixPatternSearch = Annotated[
     _PrefixSearchParam,
     Depends(prefix_search_param_factory(DagModel.dag_display_name, 
"dag_display_name_prefix_pattern")),
 ]
+QueryTimetableTypePrefixPatternSearch = Annotated[
+    _PrefixSearchParam,
+    Depends(prefix_search_param_factory(DagModel.timetable_type, 
"timetable_type_prefix_pattern")),
+]
 QueryBundleNameFilter = Annotated[
     FilterParam[str | None],
     Depends(filter_param_factory(DagModel.bundle_name, str | None, 
filter_name="bundle_name")),
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 e49e89087da..6612906a6eb 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
@@ -42,6 +42,13 @@ class DAGWithLatestDagRunsCollectionResponse(BaseModel):
     dags: list[DAGWithLatestDagRunsResponse]
 
 
+class DagTimetableTypeCollectionResponse(BaseModel):
+    """Timetable types used by Dags."""
+
+    timetable_types: list[str]
+    total_entries: int
+
+
 class DAGRunStateCountsResponse(BaseModel):
     """Per-Dag counts of DagRuns grouped by state."""
 
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 75980eefb7b..317eabd56ee 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
@@ -525,6 +525,14 @@ paths:
           description: Filter Dags by asset dependency (name or URI)
           title: Asset Dependency
         description: Filter Dags by asset dependency (name or URI)
+      - name: timetable_type
+        in: query
+        required: false
+        schema:
+          type: array
+          items:
+            type: string
+          title: Timetable Type
       - name: has_pending_actions
         in: query
         required: false
@@ -546,6 +554,58 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
+  /ui/dags/timetable_types:
+    get:
+      tags:
+      - DAG
+      summary: Get Dag Timetable Types
+      description: Get timetable types used by readable Dags.
+      operationId: get_dag_timetable_types_ui
+      security:
+      - OAuth2PasswordBearer: []
+      - HTTPBearer: []
+      parameters:
+      - name: limit
+        in: query
+        required: false
+        schema:
+          type: integer
+          minimum: 0
+          default: 50
+          title: Limit
+      - name: offset
+        in: query
+        required: false
+        schema:
+          type: integer
+          minimum: 0
+          default: 0
+          title: Offset
+      - name: timetable_type_prefix_pattern
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+          - type: 'null'
+          description: Case-sensitive, index-friendly prefix match. See 
"Filtering
+            with pattern parameters".
+          title: Timetable Type Prefix Pattern
+        description: Case-sensitive, index-friendly prefix match. See 
"Filtering with
+          pattern parameters".
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/DagTimetableTypeCollectionResponse'
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
   /ui/dags/{dag_id}/latest_run:
     get:
       tags:
@@ -2749,6 +2809,22 @@ components:
       - dag_display_name
       title: DagTagResponse
       description: Dag Tag serializer for responses.
+    DagTimetableTypeCollectionResponse:
+      properties:
+        timetable_types:
+          items:
+            type: string
+          type: array
+          title: Timetable Types
+        total_entries:
+          type: integer
+          title: Total Entries
+      type: object
+      required:
+      - timetable_types
+      - total_entries
+      title: DagTimetableTypeCollectionResponse
+      description: Timetable types used by Dags.
     DagVersionResponse:
       properties:
         id:
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 38421d387b1..962196e51ef 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
@@ -17,10 +17,11 @@
 
 from __future__ import annotations
 
+from collections.abc import Sequence
 from typing import Annotated
 
 from fastapi import Depends, HTTPException, Query, status
-from sqlalchemy import func, literal, select, union_all
+from sqlalchemy import false, func, literal, select, union_all
 from sqlalchemy.orm import defaultload
 
 from airflow.api_fastapi.auth.managers.models.resource_details import 
DagAccessEntity
@@ -52,6 +53,7 @@ from airflow.api_fastapi.common.parameters import (
     QueryPendingActionsFilter,
     QueryTagsFilter,
     QueryTeamsFilter,
+    QueryTimetableTypePrefixPatternSearch,
     SortParam,
     filter_param_factory,
 )
@@ -61,6 +63,7 @@ from airflow.api_fastapi.core_api.datamodels.ui.dag_runs 
import DAGRunLightRespo
 from airflow.api_fastapi.core_api.datamodels.ui.dags import (
     DAGRunStateCountsResponse,
     DAGsRunStateCountsCollectionResponse,
+    DagTimetableTypeCollectionResponse,
     DAGWithLatestDagRunsCollectionResponse,
     DAGWithLatestDagRunsResponse,
 )
@@ -130,6 +133,10 @@ def get_dags(
     is_favorite: QueryFavoriteFilter,
     has_asset_schedule: QueryHasAssetScheduleFilter,
     asset_dependency: QueryAssetDependencyFilter,
+    timetable_type: Annotated[
+        FilterParam[list[str] | None],
+        Depends(filter_param_factory(DagModel.timetable_type, list[str], 
FilterOptionEnum.IN)),
+    ],
     has_pending_actions: QueryPendingActionsFilter,
     readable_dags_filter: ReadableDagsFilterDep,
     session: SessionDep,
@@ -165,6 +172,7 @@ def get_dags(
             is_favorite,
             has_asset_schedule,
             asset_dependency,
+            timetable_type,
             has_pending_actions,
             readable_dags_filter,
             bundle_name,
@@ -275,6 +283,40 @@ def get_dags(
     )
 
 
+@dags_router.get(
+    "/timetable_types",
+    dependencies=[Depends(requires_access_dag(method="GET"))],
+    operation_id="get_dag_timetable_types_ui",
+)
+def get_dag_timetable_types(
+    limit: QueryLimit,
+    offset: QueryOffset,
+    timetable_type_prefix_pattern: QueryTimetableTypePrefixPatternSearch,
+    readable_dags_filter: ReadableDagsFilterDep,
+    session: SessionDep,
+) -> DagTimetableTypeCollectionResponse:
+    """Get timetable types used by readable Dags."""
+    query = (
+        select(DagModel.timetable_type)
+        .where(DagModel.is_stale == false(), DagModel.timetable_type != "")
+        .group_by(DagModel.timetable_type)
+    )
+    timetable_types_select, total_entries = paginated_select(
+        statement=query,
+        filters=[timetable_type_prefix_pattern, readable_dags_filter],
+        offset=offset,
+        limit=limit,
+        session=session,
+    )
+    timetable_types: Sequence[str] = session.scalars(
+        timetable_types_select.order_by(DagModel.timetable_type)
+    ).all()
+    return DagTimetableTypeCollectionResponse(
+        timetable_types=list(timetable_types),
+        total_entries=total_entries,
+    )
+
+
 @dags_router.get(
     "/{dag_id}/latest_run",
     responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
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 2761037fcc2..ec9da9f0535 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -342,7 +342,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, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, 
teams }: {
+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, 
teams, timetableType }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -367,7 +367,16 @@ export const UseDagServiceGetDagsUiKeyFn = ({ 
assetDependency, bundleName, bundl
   tags?: string[];
   tagsMatchMode?: "any" | "all";
   teams?: string[];
-} = {}, 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, teams }])];
+  timetableType?: string[];
+} = {}, 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, teams, timetableType }])];
+export type DagServiceGetDagTimetableTypesUiDefaultResponse = 
Awaited<ReturnType<typeof DagService.getDagTimetableTypesUi>>;
+export type DagServiceGetDagTimetableTypesUiQueryResult<TData = 
DagServiceGetDagTimetableTypesUiDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
+export const useDagServiceGetDagTimetableTypesUiKey = 
"DagServiceGetDagTimetableTypesUi";
+export const UseDagServiceGetDagTimetableTypesUiKeyFn = ({ limit, offset, 
timetableTypePrefixPattern }: {
+  limit?: number;
+  offset?: number;
+  timetableTypePrefixPattern?: string;
+} = {}, queryKey?: Array<unknown>) => [useDagServiceGetDagTimetableTypesUiKey, 
...(queryKey ?? [{ limit, offset, timetableTypePrefixPattern }])];
 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 d25f9977981..9482e9ce6d0 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -674,11 +674,12 @@ export const ensureUseDagServiceGetDagTagsData = 
(queryClient: QueryClient, { li
 * @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.timetableType
 * @param data.hasPendingActions
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-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, teams }: {
+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, teams, timetableType }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -703,7 +704,23 @@ export const ensureUseDagServiceGetDagsUiData = 
(queryClient: QueryClient, { ass
   tags?: string[];
   tagsMatchMode?: "any" | "all";
   teams?: string[];
-} = {}) => 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, 
teams }), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bund [...]
+  timetableType?: string[];
+} = {}) => 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, 
teams, timetableType }), queryFn: () => DagService.getDagsUi({ assetDependency, 
b [...]
+/**
+* Get Dag Timetable Types
+* Get timetable types used by readable Dags.
+* @param data The data for the request.
+* @param data.limit
+* @param data.offset
+* @param data.timetableTypePrefixPattern Case-sensitive, index-friendly prefix 
match. See "Filtering with pattern parameters".
+* @returns DagTimetableTypeCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const ensureUseDagServiceGetDagTimetableTypesUiData = (queryClient: 
QueryClient, { limit, offset, timetableTypePrefixPattern }: {
+  limit?: number;
+  offset?: number;
+  timetableTypePrefixPattern?: string;
+} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, offset, 
timetableTypePrefixPattern }), queryFn: () => 
DagService.getDagTimetableTypesUi({ limit, offset, timetableTypePrefixPattern 
}) });
 /**
 * 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 a15d7bfd4ed..1eaac7e3395 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -674,11 +674,12 @@ export const prefetchUseDagServiceGetDagTags = 
(queryClient: QueryClient, { limi
 * @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.timetableType
 * @param data.hasPendingActions
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-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, teams }: {
+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, teams, timetableType }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -703,7 +704,23 @@ export const prefetchUseDagServiceGetDagsUi = 
(queryClient: QueryClient, { asset
   tags?: string[];
   tagsMatchMode?: "any" | "all";
   teams?: string[];
-} = {}) => 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, 
teams }), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bundle [...]
+  timetableType?: string[];
+} = {}) => 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, 
teams, timetableType }), queryFn: () => DagService.getDagsUi({ assetDependency, 
bun [...]
+/**
+* Get Dag Timetable Types
+* Get timetable types used by readable Dags.
+* @param data The data for the request.
+* @param data.limit
+* @param data.offset
+* @param data.timetableTypePrefixPattern Case-sensitive, index-friendly prefix 
match. See "Filtering with pattern parameters".
+* @returns DagTimetableTypeCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const prefetchUseDagServiceGetDagTimetableTypesUi = (queryClient: 
QueryClient, { limit, offset, timetableTypePrefixPattern }: {
+  limit?: number;
+  offset?: number;
+  timetableTypePrefixPattern?: string;
+} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, offset, 
timetableTypePrefixPattern }), queryFn: () => 
DagService.getDagTimetableTypesUi({ limit, offset, timetableTypePrefixPattern 
}) });
 /**
 * 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 1eb20ba00fa..386cd4aca74 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -674,11 +674,12 @@ export const useDagServiceGetDagTags = <TData = 
Common.DagServiceGetDagTagsDefau
 * @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.timetableType
 * @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, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, 
teams }: {
+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, 
teams, timeta [...]
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -703,7 +704,23 @@ export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefault
   tags?: string[];
   tagsMatchMode?: "any" | "all";
   teams?: string[];
-} = {}, 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 [...]
+  timetableType?: string[];
+} = {}, 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 Dag Timetable Types
+* Get timetable types used by readable Dags.
+* @param data The data for the request.
+* @param data.limit
+* @param data.offset
+* @param data.timetableTypePrefixPattern Case-sensitive, index-friendly prefix 
match. See "Filtering with pattern parameters".
+* @returns DagTimetableTypeCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const useDagServiceGetDagTimetableTypesUi = <TData = 
Common.DagServiceGetDagTimetableTypesUiDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ limit, offset, 
timetableTypePrefixPattern }: {
+  limit?: number;
+  offset?: number;
+  timetableTypePrefixPattern?: string;
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, offset, 
timetableTypePrefixPattern }, queryKey), queryFn: () => 
DagService.getDagTimetableTypesUi({ limit, offset, timetableTypePrefixPattern 
}) as TData, ...options });
 /**
 * 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 dd209e6fb01..e4834f3ac0a 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -674,11 +674,12 @@ export const useDagServiceGetDagTagsSuspense = <TData = 
Common.DagServiceGetDagT
 * @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.timetableType
 * @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, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, 
teams }: {
+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, 
teams [...]
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -703,7 +704,23 @@ export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsU
   tags?: string[];
   tagsMatchMode?: "any" | "all";
   teams?: string[];
-} = {}, 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 [...]
+  timetableType?: string[];
+} = {}, 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 Dag Timetable Types
+* Get timetable types used by readable Dags.
+* @param data The data for the request.
+* @param data.limit
+* @param data.offset
+* @param data.timetableTypePrefixPattern Case-sensitive, index-friendly prefix 
match. See "Filtering with pattern parameters".
+* @returns DagTimetableTypeCollectionResponse Successful Response
+* @throws ApiError
+*/
+export const useDagServiceGetDagTimetableTypesUiSuspense = <TData = 
Common.DagServiceGetDagTimetableTypesUiDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ limit, offset, 
timetableTypePrefixPattern }: {
+  limit?: number;
+  offset?: number;
+  timetableTypePrefixPattern?: string;
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, offset, 
timetableTypePrefixPattern }, queryKey), queryFn: () => 
DagService.getDagTimetableTypesUi({ limit, offset, timetableTypePrefixPattern 
}) as TData, ...options });
 /**
 * 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 7795ef0983f..fedb68983f6 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
@@ -9533,6 +9533,26 @@ export const $DagRunStatsResponse = {
     description: 'DAG Run statistics serializer for responses.'
 } as const;
 
+export const $DagTimetableTypeCollectionResponse = {
+    properties: {
+        timetable_types: {
+            items: {
+                type: 'string'
+            },
+            type: 'array',
+            title: 'Timetable Types'
+        },
+        total_entries: {
+            type: 'integer',
+            title: 'Total Entries'
+        }
+    },
+    type: 'object',
+    required: ['timetable_types', 'total_entries'],
+    title: 'DagTimetableTypeCollectionResponse',
+    description: 'Timetable types used by Dags.'
+} as const;
+
 export const $DashboardDagStatsResponse = {
     properties: {
         active_dag_count: {
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 f353ba627b4..1c7365f7ea8 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
@@ -3,7 +3,7 @@
 import type { CancelablePromise } from './core/CancelablePromise';
 import { OpenAPI } from './core/OpenAPI';
 import { request as __request } from './core/request';
-import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, 
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, 
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, 
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, 
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, 
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, 
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, 
Dele [...]
+import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, 
GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, 
GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, 
CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, 
GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, 
DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, 
GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, 
Dele [...]
 
 export class AssetService {
     /**
@@ -1947,6 +1947,7 @@ export class DagService {
      * @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.timetableType
      * @param data.hasPendingActions
      * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
      * @throws ApiError
@@ -1979,6 +1980,7 @@ export class DagService {
                 is_favorite: data.isFavorite,
                 has_asset_schedule: data.hasAssetSchedule,
                 asset_dependency: data.assetDependency,
+                timetable_type: data.timetableType,
                 has_pending_actions: data.hasPendingActions
             },
             errors: {
@@ -1987,6 +1989,31 @@ export class DagService {
         });
     }
     
+    /**
+     * Get Dag Timetable Types
+     * Get timetable types used by readable Dags.
+     * @param data The data for the request.
+     * @param data.limit
+     * @param data.offset
+     * @param data.timetableTypePrefixPattern Case-sensitive, index-friendly 
prefix match. See "Filtering with pattern parameters".
+     * @returns DagTimetableTypeCollectionResponse Successful Response
+     * @throws ApiError
+     */
+    public static getDagTimetableTypesUi(data: GetDagTimetableTypesUiData = 
{}): CancelablePromise<GetDagTimetableTypesUiResponse> {
+        return __request(OpenAPI, {
+            method: 'GET',
+            url: '/ui/dags/timetable_types',
+            query: {
+                limit: data.limit,
+                offset: data.offset,
+                timetable_type_prefix_pattern: data.timetableTypePrefixPattern
+            },
+            errors: {
+                422: 'Validation Error'
+            }
+        });
+    }
+    
     /**
      * Get Latest Run Info
      * Get latest run.
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 e26a2abdee7..f0b9e09aa46 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
@@ -2407,6 +2407,14 @@ export type DagRunStatsResponse = {
     duration: DurationStats | null;
 };
 
+/**
+ * Timetable types used by Dags.
+ */
+export type DagTimetableTypeCollectionResponse = {
+    timetable_types: Array<(string)>;
+    total_entries: number;
+};
+
 /**
  * Dashboard DAG Stats serializer for responses.
  */
@@ -3550,10 +3558,22 @@ export type GetDagsUiData = {
     tags?: Array<(string)>;
     tagsMatchMode?: 'any' | 'all' | null;
     teams?: Array<(string)>;
+    timetableType?: Array<(string)>;
 };
 
 export type GetDagsUiResponse = DAGWithLatestDagRunsCollectionResponse;
 
+export type GetDagTimetableTypesUiData = {
+    limit?: number;
+    offset?: number;
+    /**
+     * Case-sensitive, index-friendly prefix match. See "Filtering with 
pattern parameters".
+     */
+    timetableTypePrefixPattern?: string | null;
+};
+
+export type GetDagTimetableTypesUiResponse = 
DagTimetableTypeCollectionResponse;
+
 export type GetLatestRunInfoData = {
     dagId: string;
 };
@@ -6419,6 +6439,21 @@ export type $OpenApiTs = {
             };
         };
     };
+    '/ui/dags/timetable_types': {
+        get: {
+            req: GetDagTimetableTypesUiData;
+            res: {
+                /**
+                 * Successful Response
+                 */
+                200: DagTimetableTypeCollectionResponse;
+                /**
+                 * Validation Error
+                 */
+                422: HTTPValidationError;
+            };
+        };
+    };
     '/ui/dags/{dag_id}/latest_run': {
         get: {
             req: GetLatestRunInfoData;
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 78b8681b423..753f7fe53fd 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
@@ -17,12 +17,14 @@
       "unfavorite": "Unfavorite"
     },
     "lastRunState": "Last run",
+    "noTimetableTypesFound": "No timetable types found",
     "paused": {
       "active": "Active",
       "all": "All",
       "paused": "Paused"
     },
-    "runIdPatternFilter": "Search Dag Runs"
+    "runIdPatternFilter": "Search Dag Runs",
+    "timetableType": "Timetable type"
   },
   "ownerLink": "Owner link for {{owner}}",
   "runAndTaskActions": {
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts 
b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index eee952f0207..cf1174b881e 100644
--- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
@@ -105,6 +105,7 @@ export enum SearchParamsKeys {
   TASK_ID_PATTERN = "task_id_pattern",
   TASK_STATE = "task_state",
   TEAMS = "teams",
+  TIMETABLE_TYPE = "timetable_type",
   TRIGGER_RULE = "trigger_rule",
   TRIGGERING_USER = "triggering_user",
   TRIGGERING_USER_NAME_PATTERN = "triggering_user_name_pattern",
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 b3d4d1fea35..6a6116420ea 100644
--- a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
+++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
@@ -49,6 +49,7 @@ const successDag = {
   pending_actions: [],
   tags: [{ dag_id: "tutorial_taskflow_api_success", name: "example" }],
   timetable_description: "Never, external triggers only",
+  timetable_type: "NullTimetable",
 };
 
 const failedDag = {
@@ -82,6 +83,7 @@ const failedDag = {
   pending_actions: [],
   tags: [{ dag_id: "tutorial_taskflow_api_failed", name: "example" }],
   timetable_description: "Never, external triggers only",
+  timetable_type: "CronTriggerTimetable",
 };
 
 const pausedDag = {
@@ -104,10 +106,15 @@ const pausedDag = {
   pending_actions: [],
   tags: [{ dag_id: "paused_dag", name: "example" }],
   timetable_description: "Never, external triggers only",
+  timetable_type: "NullTimetable",
 };
 
-const filterDagsByPaused = (paused: string | null) => {
-  const allDags = [successDag, failedDag, pausedDag];
+const filterDags = ({ paused, timetableTypes }: { paused: string | null; 
timetableTypes: Array<string> }) => {
+  let allDags = [successDag, failedDag, pausedDag];
+
+  if (timetableTypes.length > 0) {
+    allDags = allDags.filter((dag) => 
timetableTypes.includes(dag.timetable_type));
+  }
 
   if (paused === "true") {
     return allDags.filter((dag) => dag.is_paused);
@@ -120,11 +127,24 @@ const filterDagsByPaused = (paused: string | null) => {
 };
 
 export const handlers: Array<HttpHandler> = [
+  http.get("/ui/dags/timetable_types", ({ request }) => {
+    const url = new URL(request.url);
+    const prefix = url.searchParams.get("timetable_type_prefix_pattern") ?? "";
+    const timetableTypes = ["CronTriggerTimetable", 
"NullTimetable"].filter((timetableType) =>
+      timetableType.startsWith(prefix),
+    );
+
+    return HttpResponse.json({
+      timetable_types: timetableTypes,
+      total_entries: timetableTypes.length,
+    });
+  }),
   http.get("/ui/dags", ({ request }) => {
     const url = new URL(request.url);
     const lastDagRunState = url.searchParams.get("last_dag_run_state");
     const orderBy = url.searchParams.get("order_by");
     const paused = url.searchParams.get("paused");
+    const timetableTypes = url.searchParams.getAll("timetable_type");
 
     if (lastDagRunState === "success") {
       return HttpResponse.json({
@@ -138,7 +158,7 @@ export const handlers: Array<HttpHandler> = [
       });
     }
 
-    let dags = filterDagsByPaused(paused);
+    let dags = filterDags({ paused, timetableTypes });
 
     if (orderBy === "last_run_run_after" || orderBy === "-last_run_run_after") 
{
       dags = [failedDag, successDag, pausedDag].filter((dag) => 
dags.includes(dag));
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilterSelect.tsx
 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilterSelect.tsx
new file mode 100644
index 00000000000..3a6858fdb9a
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilterSelect.tsx
@@ -0,0 +1,83 @@
+/*!
+ * 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 { Field } from "@chakra-ui/react";
+import { Select as ReactSelect } from "chakra-react-select";
+
+type FilterOption = {
+  label: string;
+  value: string;
+};
+
+type Props = {
+  readonly ariaLabel: string;
+  readonly noOptionsMessage: string;
+  readonly onChange: (values: Array<string>) => void;
+  readonly onInputChange?: (value: string) => void;
+  readonly onMenuScrollToBottom?: () => void;
+  readonly onMenuScrollToTop?: () => void;
+  readonly options: Array<string>;
+  readonly placeholder: string;
+  readonly values: Array<string>;
+};
+
+export const DagsFilterSelect = ({
+  ariaLabel,
+  noOptionsMessage,
+  onChange,
+  onInputChange,
+  onMenuScrollToBottom,
+  onMenuScrollToTop,
+  options,
+  placeholder,
+  values,
+}: Props) => (
+  <Field.Root>
+    <ReactSelect<FilterOption, true>
+      aria-label={ariaLabel}
+      chakraStyles={{
+        clearIndicator: (provided) => ({
+          ...provided,
+          color: "gray.fg",
+        }),
+        container: (provided) => ({
+          ...provided,
+          width: "100%",
+        }),
+        control: (provided) => ({
+          ...provided,
+          colorPalette: "brand",
+        }),
+        menu: (provided) => ({
+          ...provided,
+          zIndex: 2,
+        }),
+      }}
+      isClearable
+      isMulti
+      noOptionsMessage={() => noOptionsMessage}
+      onChange={(selected) => onChange(selected.map(({ value }) => value))}
+      onInputChange={onInputChange}
+      onMenuScrollToBottom={onMenuScrollToBottom}
+      onMenuScrollToTop={onMenuScrollToTop}
+      options={options.map((option) => ({ label: option, value: option }))}
+      placeholder={placeholder}
+      value={values.map((value) => ({ label: value, value }))}
+    />
+  </Field.Root>
+);
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 aac7b895ae4..14cdfa4c212 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
@@ -17,8 +17,8 @@
  * under the License.
  */
 import "@testing-library/jest-dom";
-import { render, screen, waitFor } from "@testing-library/react";
-import { describe, it, expect, vi } from "vitest";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { afterEach, describe, it, expect, vi } from "vitest";
 
 import { AppWrapper } from "src/utils/AppWrapper";
 
@@ -39,6 +39,10 @@ vi.mock("src/queries/useConfig", () => ({
 }));
 
 describe("Paused filter with hide_paused_dags_by_default enabled", () => {
+  afterEach(() => {
+    mockConfig.multi_team = false;
+  });
+
   it("defaults to showing only active dags", async () => {
     render(<AppWrapper initialEntries={["/dags"]} />);
 
@@ -67,4 +71,76 @@ describe("Paused filter with hide_paused_dags_by_default 
enabled", () => {
     await waitFor(() => 
expect(screen.getByText("paused_dag")).toBeInTheDocument());
     await waitFor(() => 
expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument());
   });
+
+  it("filters and clears dags by timetable types", async () => {
+    render(<AppWrapper initialEntries={["/dags"]} />);
+
+    await waitFor(() => 
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument());
+    
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument();
+
+    const timetableTypeFilter = screen.getByLabelText("filters.timetableType");
+
+    fireEvent.change(timetableTypeFilter, { target: { value: "Cron" } });
+    const cronTriggerTimetable = await 
screen.findByText("CronTriggerTimetable");
+
+    expect(screen.queryByText("NullTimetable")).not.toBeInTheDocument();
+    fireEvent.click(cronTriggerTimetable);
+
+    await waitFor(() => {
+      
expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument();
+      
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument();
+    });
+
+    fireEvent.change(timetableTypeFilter, { target: { value: "Null" } });
+    fireEvent.click(await screen.findByText("NullTimetable"));
+
+    await waitFor(() => {
+      
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument();
+      
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument();
+    });
+
+    fireEvent.keyDown(timetableTypeFilter, { code: "Backspace", key: 
"Backspace" });
+
+    await waitFor(() => {
+      
expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument();
+      
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument();
+    });
+
+    fireEvent.keyDown(timetableTypeFilter, { code: "Backspace", key: 
"Backspace" });
+
+    await waitFor(() => {
+      
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument();
+      
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument();
+    });
+  });
+
+  it("restores timetable types from the URL", async () => {
+    render(
+      <AppWrapper
+        
initialEntries={["/dags?timetable_type=CronTriggerTimetable&timetable_type=NullTimetable"]}
+      />,
+    );
+
+    await waitFor(() => {
+      
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument();
+      
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument();
+    });
+    expect(screen.getByText("CronTriggerTimetable")).toBeInTheDocument();
+    expect(screen.getByText("NullTimetable")).toBeInTheDocument();
+  });
+
+  it("ignores an empty timetable type from the URL", async () => {
+    render(<AppWrapper initialEntries={["/dags?timetable_type="]} />);
+
+    await waitFor(() => 
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument());
+    
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument();
+  });
+
+  it("renders the team filter when multi-team is enabled", async () => {
+    mockConfig.multi_team = true;
+
+    render(<AppWrapper initialEntries={["/dags"]} />);
+
+    expect(await 
screen.findByLabelText("dagDetails.team")).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 424dbb1b615..7480a064692 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,8 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { HStack } from "@chakra-ui/react";
-import type { MultiValue } from "chakra-react-select";
+import { Box, HStack } from "@chakra-ui/react";
 import { useState } from "react";
 import { useTranslation } from "react-i18next";
 import { useSearchParams } from "react-router-dom";
@@ -26,6 +25,7 @@ import { useTableURLState } from 
"src/components/DataTable/useTableUrlState";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
 import { useConfig } from "src/queries/useConfig";
 import { useDagTagsInfinite } from "src/queries/useDagTagsInfinite";
+import { useDagTimetableTypesInfinite } from 
"src/queries/useDagTimetableTypesInfinite";
 
 import { useTagFilter } from "../useTagFilter";
 import { FavoriteFilter } from "./FavoriteFilter";
@@ -34,6 +34,7 @@ import { RequiredActionFilter } from "./RequiredActionFilter";
 import { RunStateSelect } from "./RunStateSelect";
 import { TagFilter } from "./TagFilter";
 import { TeamFilter } from "./TeamFilter";
+import { TimetableTypeFilter } from "./TimetableTypeFilter";
 
 const {
   DAG_RUN_STATE: DAG_RUN_STATE_PARAM,
@@ -43,6 +44,7 @@ const {
   OFFSET: OFFSET_PARAM,
   PAUSED: PAUSED_PARAM,
   TEAMS: TEAMS_PARAM,
+  TIMETABLE_TYPE: TIMETABLE_TYPE_PARAM,
 }: SearchParamsKeysType = SearchParamsKeys;
 
 type BooleanFilterValue = "all" | "false" | "true";
@@ -68,13 +70,27 @@ export const DagsFilters = () => {
   const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
   const activeRunState = searchParams.get(DAG_RUN_STATE_PARAM);
   const selectedTeams = searchParams.getAll(TEAMS_PARAM);
+  const timetableTypes = 
searchParams.getAll(TIMETABLE_TYPE_PARAM).filter(Boolean);
 
-  const [pattern, setPattern] = useState("");
+  const [tagPattern, setTagPattern] = useState("");
+  const [timetableTypePattern, setTimetableTypePattern] = useState("");
 
-  const { data, fetchNextPage, fetchPreviousPage } = useDagTagsInfinite({
+  const {
+    data: tagData,
+    fetchNextPage: fetchNextTagPage,
+    fetchPreviousPage: fetchPreviousTagPage,
+  } = useDagTagsInfinite({
     limit: 10,
     orderBy: ["name"],
-    tagNamePattern: pattern,
+    tagNamePattern: tagPattern,
+  });
+  const {
+    data: timetableTypeData,
+    fetchNextPage: fetchNextTimetableTypePage,
+    fetchPreviousPage: fetchPreviousTimetableTypePage,
+  } = useDagTimetableTypesInfinite({
+    limit: 10,
+    timetableTypePrefixPattern: timetableTypePattern,
   });
 
   const hidePausedDagsByDefault = 
Boolean(useConfig("hide_paused_dags_by_default"));
@@ -141,13 +157,17 @@ export const DagsFilters = () => {
     setSearchParams(searchParams);
   };
 
-  const handleSelectTagsChange = (
-    tags: MultiValue<{
-      label: string;
-      value: string;
-    }>,
-  ) => {
-    setSelectedTags(tags.map(({ value }) => value));
+  const handleTimetableTypeChange = (selectedTimetableTypes: Array<string>) => 
{
+    searchParams.delete(TIMETABLE_TYPE_PARAM);
+    for (const timetableType of selectedTimetableTypes) {
+      searchParams.append(TIMETABLE_TYPE_PARAM, timetableType);
+    }
+    resetPagination();
+    setSearchParams(searchParams);
+  };
+
+  const handleSelectTagsChange = (tags: Array<string>) => {
+    setSelectedTags(tags);
   };
 
   const handleTagModeChange = ({ checked }: { checked: boolean }) => {
@@ -167,7 +187,7 @@ export const DagsFilters = () => {
   const favoriteValue = toBooleanFilterValue(showFavorites);
 
   return (
-    <HStack flexWrap="wrap" gap={2} justifyContent="space-between">
+    <HStack alignItems="flex-start" flexWrap="wrap" gap={2}>
       <RunStateSelect
         dataTestId="dags-last-run-state-filter"
         label={translate("filters.lastRunState")}
@@ -184,24 +204,38 @@ export const DagsFilters = () => {
       />
       <RequiredActionFilter needsReview={needsReview === "true"} 
onToggle={handleNeedsReviewToggle} />
       <PausedFilter onChange={handlePausedChange} value={pausedValue} />
+      <TimetableTypeFilter
+        onChange={handleTimetableTypeChange}
+        onInputChange={setTimetableTypePattern}
+        onMenuScrollToBottom={() => {
+          void fetchNextTimetableTypePage();
+        }}
+        onMenuScrollToTop={() => {
+          void fetchPreviousTimetableTypePage();
+        }}
+        timetableTypes={timetableTypeData?.pages.flatMap((response) => 
response.timetable_types) ?? []}
+        values={timetableTypes}
+      />
       <TagFilter
         onMenuScrollToBottom={() => {
-          void fetchNextPage();
+          void fetchNextTagPage();
         }}
         onMenuScrollToTop={() => {
-          void fetchPreviousPage();
+          void fetchPreviousTagPage();
         }}
         onSelectTagsChange={handleSelectTagsChange}
         onTagModeChange={handleTagModeChange}
-        onUpdate={setPattern}
+        onUpdate={setTagPattern}
         selectedTags={selectedTags}
         tagFilterMode={tagFilterMode}
-        tags={data?.pages.flatMap((dagResponse) => dagResponse.tags) ?? []}
+        tags={tagData?.pages.flatMap((dagResponse) => dagResponse.tags) ?? []}
       />
-      <FavoriteFilter onChange={handleFavoriteChange} value={favoriteValue} />
       {multiTeamEnabled ? (
         <TeamFilter onChange={handleTeamsChange} selectedTeams={selectedTeams} 
/>
       ) : undefined}
+      <Box marginInlineStart="auto">
+        <FavoriteFilter onChange={handleFavoriteChange} value={favoriteValue} 
/>
+      </Box>
     </HStack>
   );
 };
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx
index f40d5829e32..c50a3361bab 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx
@@ -16,16 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Field, HStack, Text } from "@chakra-ui/react";
-import { Select as ReactSelect, type MultiValue } from "chakra-react-select";
+import { Box, HStack, Text } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
 
 import { Switch } from "src/components/ui";
 
+import { DagsFilterSelect } from "./DagsFilterSelect";
+
 type Props = {
   readonly onMenuScrollToBottom: () => void;
   readonly onMenuScrollToTop: () => void;
-  readonly onSelectTagsChange: (tags: MultiValue<{ label: string; value: 
string }>) => void;
+  readonly onSelectTagsChange: (tags: Array<string>) => void;
   readonly onTagModeChange: ({ checked }: { checked: boolean }) => void;
   readonly onUpdate: (newValue: string) => void;
   readonly selectedTags: Array<string>;
@@ -46,47 +47,18 @@ export const TagFilter = ({
   const { t: translate } = useTranslation("common");
 
   return (
-    <Box maxWidth="300px" minWidth="64px">
-      <Field.Root>
-        <ReactSelect
-          aria-label={translate("table.filterByTag")}
-          chakraStyles={{
-            clearIndicator: (provided) => ({
-              ...provided,
-              color: "gray.fg",
-            }),
-            container: (provided) => ({
-              ...provided,
-              maxWidth: 300,
-              minWidth: 64,
-            }),
-            control: (provided) => ({
-              ...provided,
-              colorPalette: "brand",
-            }),
-            menu: (provided) => ({
-              ...provided,
-              zIndex: 2,
-            }),
-          }}
-          isClearable
-          isMulti
-          noOptionsMessage={() => translate("table.noTagsFound")}
-          onChange={onSelectTagsChange}
-          onInputChange={(newValue) => onUpdate(newValue)}
-          onMenuScrollToBottom={onMenuScrollToBottom}
-          onMenuScrollToTop={onMenuScrollToTop}
-          options={tags.map((tag) => ({
-            label: tag,
-            value: tag,
-          }))}
-          placeholder={translate("table.tagPlaceholder")}
-          value={selectedTags.map((tag) => ({
-            label: tag,
-            value: tag,
-          }))}
-        />
-      </Field.Root>
+    <Box flex="0 1 300px" maxWidth="100%" width="300px">
+      <DagsFilterSelect
+        ariaLabel={translate("table.filterByTag")}
+        noOptionsMessage={translate("table.noTagsFound")}
+        onChange={onSelectTagsChange}
+        onInputChange={onUpdate}
+        onMenuScrollToBottom={onMenuScrollToBottom}
+        onMenuScrollToTop={onMenuScrollToTop}
+        options={tags}
+        placeholder={translate("table.tagPlaceholder")}
+        values={selectedTags}
+      />
       {selectedTags.length >= 2 && (
         <HStack align="center" gap={1}>
           <Text
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx
index 3b315aa8317..469503b09ae 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx
@@ -41,7 +41,7 @@ export const TeamFilter = ({ onChange, selectedTeams }: 
Props) => {
   };
 
   return (
-    <Box>
+    <Box flex="0 1 200px" maxWidth="100%" width="200px">
       <Field.Root>
         <ReactSelect
           aria-label={translate("dagDetails.team")}
@@ -52,8 +52,7 @@ export const TeamFilter = ({ onChange, selectedTeams }: 
Props) => {
             }),
             container: (provided) => ({
               ...provided,
-              maxWidth: 200,
-              minWidth: 64,
+              width: "100%",
             }),
             control: (provided) => ({
               ...provided,
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx
 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx
new file mode 100644
index 00000000000..954f76706b1
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx
@@ -0,0 +1,58 @@
+/*!
+ * 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 { Box } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { DagsFilterSelect } from "./DagsFilterSelect";
+
+type Props = {
+  readonly onChange: (timetableTypes: Array<string>) => void;
+  readonly onInputChange: (value: string) => void;
+  readonly onMenuScrollToBottom: () => void;
+  readonly onMenuScrollToTop: () => void;
+  readonly timetableTypes: Array<string>;
+  readonly values: Array<string>;
+};
+
+export const TimetableTypeFilter = ({
+  onChange,
+  onInputChange,
+  onMenuScrollToBottom,
+  onMenuScrollToTop,
+  timetableTypes,
+  values,
+}: Props) => {
+  const { t: translate } = useTranslation("dags");
+
+  return (
+    <Box flex="0 1 300px" maxWidth="100%" width="300px">
+      <DagsFilterSelect
+        ariaLabel={translate("filters.timetableType")}
+        noOptionsMessage={translate("filters.noTimetableTypesFound")}
+        onChange={onChange}
+        onInputChange={onInputChange}
+        onMenuScrollToBottom={onMenuScrollToBottom}
+        onMenuScrollToTop={onMenuScrollToTop}
+        options={timetableTypes}
+        placeholder={translate("filters.timetableType")}
+        values={values}
+      />
+    </Box>
+  );
+};
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 f82a9e90c42..5faaa5321ae 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -221,6 +221,7 @@ const {
   OWNERS,
   PAUSED,
   TEAMS,
+  TIMETABLE_TYPE,
 }: SearchParamsKeysType = SearchParamsKeys;
 
 const createCardDef = (runStateContext: RunStateCountsContext): 
CardDef<DAGWithLatestDagRunsResponse> => ({
@@ -259,6 +260,7 @@ export const DagsList = () => {
   const pendingReviews = searchParams.get(NEEDS_REVIEW);
   const owners = searchParams.getAll(OWNERS);
   const teams = searchParams.getAll(TEAMS);
+  const timetableType = searchParams.getAll(TIMETABLE_TYPE).filter((value) => 
value !== "");
 
   const { setTableURLState, tableURLState } = useTableURLState();
 
@@ -323,6 +325,7 @@ export const DagsList = () => {
     tags: selectedTags,
     tagsMatchMode: selectedMatchMode,
     teams: teams.length > 0 ? teams : undefined,
+    timetableType: timetableType.length > 0 ? timetableType : undefined,
   });
 
   const { data: runStateCountsData, isLoading: runStateCountsLoading } = 
useDagRunStateCounts({
diff --git 
a/airflow-core/src/airflow/ui/src/queries/useDagTimetableTypesInfinite.ts 
b/airflow-core/src/airflow/ui/src/queries/useDagTimetableTypesInfinite.ts
new file mode 100644
index 00000000000..17fa4355a9a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useDagTimetableTypesInfinite.ts
@@ -0,0 +1,68 @@
+/*!
+ * 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 { type InfiniteData, useInfiniteQuery, type UseInfiniteQueryOptions } 
from "@tanstack/react-query";
+
+import { UseDagServiceGetDagTimetableTypesUiKeyFn } from "openapi/queries";
+import { DagService } from "openapi/requests/services.gen";
+import type { DagTimetableTypeCollectionResponse } from 
"openapi/requests/types.gen";
+
+export const useDagTimetableTypesInfinite = <TError = unknown>(
+  {
+    limit,
+    timetableTypePrefixPattern,
+  }: {
+    limit?: number;
+    timetableTypePrefixPattern?: string;
+  } = {},
+  queryKey?: Array<unknown>,
+  options?: Omit<
+    UseInfiniteQueryOptions<
+      DagTimetableTypeCollectionResponse,
+      TError,
+      InfiniteData<DagTimetableTypeCollectionResponse>,
+      Array<unknown>,
+      number
+    >,
+    "queryFn" | "queryKey"
+  >,
+) =>
+  useInfiniteQuery({
+    getNextPageParam: (
+      lastPage: DagTimetableTypeCollectionResponse,
+      _allPages: Array<DagTimetableTypeCollectionResponse>,
+      lastPageParam: number,
+    ) =>
+      lastPageParam + lastPage.timetable_types.length < lastPage.total_entries
+        ? lastPageParam + lastPage.timetable_types.length
+        : undefined,
+    getPreviousPageParam: (
+      firstPage: DagTimetableTypeCollectionResponse,
+      _allPages: Array<DagTimetableTypeCollectionResponse>,
+      firstPageParam: number,
+    ) => (firstPageParam > 0 ? Math.max(0, firstPageParam - 
firstPage.timetable_types.length) : undefined),
+    initialPageParam: 0,
+    queryFn: ({ pageParam }: { pageParam: number }) =>
+      DagService.getDagTimetableTypesUi({
+        limit,
+        offset: pageParam,
+        timetableTypePrefixPattern,
+      }),
+    queryKey: UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, 
timetableTypePrefixPattern }, queryKey),
+    ...options,
+  });
diff --git a/airflow-core/src/airflow/ui/src/queries/useDags.tsx 
b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
index 43d2cab5769..759180cb16a 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDags.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
@@ -38,6 +38,7 @@ export const useDags = ({
   tags,
   tagsMatchMode,
   teams,
+  timetableType,
 }: {
   advancedSearch?: boolean;
   dagDisplayNamePattern?: string;
@@ -56,6 +57,7 @@ export const useDags = ({
   tags?: Array<string>;
   tagsMatchMode?: "all" | "any";
   teams?: Array<string>;
+  timetableType?: Array<string>;
 }) => {
   const refetchInterval = useAutoRefresh({});
 
@@ -78,6 +80,7 @@ export const useDags = ({
       tags,
       tagsMatchMode,
       teams,
+      timetableType,
     },
     undefined,
     {
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 837e7805c69..14c7c73f657 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
@@ -135,6 +135,20 @@ 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")
+    @pytest.mark.parametrize(
+        ("query_params", "expected_ids"),
+        [
+            ({"timetable_type": ["CronTriggerTimetable"], "exclude_stale": 
False}, [DAG3_ID]),
+            ({"timetable_type": ["NullTimetable"], "exclude_stale": False}, 
[DAG1_ID, DAG2_ID]),
+        ],
+    )
+    def test_timetable_type_filter(self, test_client: TestClient, 
query_params, expected_ids):
+        response = test_client.get("/dags", params=query_params)
+
+        assert response.status_code == 200
+        assert [dag["dag_id"] for dag in response.json()["dags"]] == 
expected_ids
+
     @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
     @pytest.mark.parametrize("state", ["queued", "running", "failed", 
"success"])
     def test_dag_run_state_matches_any_run_not_only_latest(self, test_client, 
session, state):
@@ -496,6 +510,85 @@ _PARENT_DAG3_FAILED = 1  # via 
``_create_deactivated_paused_dag``
 _PARENT_DAG3_SUCCESS = 1  # via ``_create_deactivated_paused_dag``
 
 
+class TestGetDagTimetableTypes(TestPublicDagEndpoint):
+    @pytest.fixture(autouse=True)
+    def setup_timetable_types(self, session):
+        session.get(DagModel, DAG1_ID).timetable_type = "CronTriggerTimetable"
+        session.get(DagModel, DAG2_ID).timetable_type = 
"example.CustomTimetable"
+        session.get(DagModel, DAG3_ID).timetable_type = "StaleOnlyTimetable"
+        session.commit()
+
+    @pytest.mark.parametrize(
+        ("query_params", "expected_types", "expected_total_entries"),
+        [
+            ({}, ["CronTriggerTimetable", "example.CustomTimetable"], 2),
+            ({"limit": 1}, ["CronTriggerTimetable"], 2),
+            ({"offset": 1}, ["example.CustomTimetable"], 2),
+            (
+                {"timetable_type_prefix_pattern": "example"},
+                ["example.CustomTimetable"],
+                1,
+            ),
+            ({"timetable_type_prefix_pattern": "missing"}, [], 0),
+        ],
+    )
+    def test_get_dag_timetable_types(
+        self,
+        test_client,
+        query_params,
+        expected_types,
+        expected_total_entries,
+    ):
+        response = test_client.get("/dags/timetable_types", 
params=query_params)
+
+        assert response.status_code == 200
+        assert response.json() == {
+            "timetable_types": expected_types,
+            "total_entries": expected_total_entries,
+        }
+
+    
@mock.patch("airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids")
+    def test_only_returns_types_from_readable_dags(
+        self,
+        mock_get_authorized_dag_ids,
+        test_client,
+    ):
+        mock_get_authorized_dag_ids.return_value = {DAG2_ID}
+
+        response = test_client.get("/dags/timetable_types")
+
+        mock_get_authorized_dag_ids.assert_called_once_with(user=mock.ANY, 
method="GET")
+        assert response.status_code == 200
+        assert response.json() == {
+            "timetable_types": ["example.CustomTimetable"],
+            "total_entries": 1,
+        }
+
+    def test_excludes_blank_timetable_types(self, test_client, session):
+        session.get(DagModel, DAG1_ID).timetable_type = ""
+        session.commit()
+
+        response = test_client.get("/dags/timetable_types")
+
+        assert response.status_code == 200
+        assert response.json() == {
+            "timetable_types": ["example.CustomTimetable"],
+            "total_entries": 1,
+        }
+
+    def test_returns_distinct_timetable_types(self, test_client, session):
+        session.get(DagModel, DAG2_ID).timetable_type = "CronTriggerTimetable"
+        session.commit()
+
+        response = test_client.get("/dags/timetable_types")
+
+        assert response.status_code == 200
+        assert response.json() == {
+            "timetable_types": ["CronTriggerTimetable"],
+            "total_entries": 1,
+        }
+
+
 class TestGetDagRunStateCounts(TestPublicDagEndpoint):
     """Tests for ``GET /ui/dags/run_state_counts``."""
 

Reply via email to