diff --git a/airflow-core/newsfragments/69728.feature.rst b/airflow-core/newsfragments/69728.feature.rst new file mode 100644 index 0000000000000..01ab7439c18ba --- /dev/null +++ b/airflow-core/newsfragments/69728.feature.rst @@ -0,0 +1 @@ +Add timetable type filtering to the Dags list. diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index 1f940dcfd5a57..286fa75ec49db 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -1321,6 +1321,10 @@ def depends_float( _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 e49e89087daeb..6612906a6eb55 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 ca8f729998250..553e25520585d 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 @@ -551,6 +551,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 @@ -572,6 +580,70 @@ 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: "Prefix match \u2014 returns items whose value starts with\ + \ the given string (case-sensitive, index-friendly). Use the pipe `|`\ + \ operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard\ + \ characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric\ + \ characters in the prefix are stripped before matching so the range scan\ + \ stays index-compatible under locale-aware collations \u2014 e.g. `test_`\ + \ effectively matches items starting with `test`, and `s3://` matches\ + \ items starting with `s3`." + title: Timetable Type Prefix Pattern + description: "Prefix match \u2014 returns items whose value starts with the\ + \ given string (case-sensitive, index-friendly). Use the pipe `|` operator\ + \ for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters\ + \ (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric\ + \ characters in the prefix are stripped before matching so the range scan\ + \ stays index-compatible under locale-aware collations \u2014 e.g. `test_`\ + \ effectively matches items starting with `test`, and `s3://` matches items\ + \ starting with `s3`." + 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: @@ -2760,6 +2832,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 38421d387b10a..962196e51efdd 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 @@ QueryPendingActionsFilter, QueryTagsFilter, QueryTeamsFilter, + QueryTimetableTypePrefixPatternSearch, SortParam, filter_param_factory, ) @@ -61,6 +63,7 @@ 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 4f4852029734e..0fd23014323be 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>; export type DagServiceGetDagsUiQueryResult = UseQueryResult; 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) => [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) => [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>; +export type DagServiceGetDagTimetableTypesUiQueryResult = UseQueryResult; +export const useDagServiceGetDagTimetableTypesUiKey = "DagServiceGetDagTimetableTypesUi"; +export const UseDagServiceGetDagTimetableTypesUiKeyFn = ({ limit, offset, timetableTypePrefixPattern }: { + limit?: number; + offset?: number; + timetableTypePrefixPattern?: string; +} = {}, queryKey?: Array) => [useDagServiceGetDagTimetableTypesUiKey, ...(queryKey ?? [{ limit, offset, timetableTypePrefixPattern }])]; export type DagServiceGetLatestRunInfoDefaultResponse = Awaited>; export type DagServiceGetLatestRunInfoQueryResult = UseQueryResult; 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 08ecd2f13f369..d02b1ae25bafe 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -702,11 +702,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; @@ -731,7 +732,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, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }) }); + 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, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }) }); +/** +* 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 Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. +* @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 0b0d7b46eeb94..988e98b7ba2dc 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -702,11 +702,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; @@ -731,7 +732,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, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }) }); + 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, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }) }); +/** +* 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 Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. +* @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 59d5c2dce19f1..98f368ce9cadf 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -702,11 +702,12 @@ export const useDagServiceGetDagTags = = 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 = = 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, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -731,7 +732,23 @@ export const useDagServiceGetDagsUi = , "queryKey" | "queryFn">) => useQuery({ 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 }, queryKey), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }) as TData, ...options }); + timetableType?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ 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 }, queryKey), queryFn: () => DagService.getDagsUi({ 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 }) as TData, ...options }); +/** +* 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 Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. +* @returns DagTimetableTypeCollectionResponse Successful Response +* @throws ApiError +*/ +export const useDagServiceGetDagTimetableTypesUi = = unknown[]>({ limit, offset, timetableTypePrefixPattern }: { + limit?: number; + offset?: number; + timetableTypePrefixPattern?: string; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ 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 26965f1a28612..172bdf927e13e 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -702,11 +702,12 @@ export const useDagServiceGetDagTagsSuspense = = 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 = = 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, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -731,7 +732,23 @@ export const useDagServiceGetDagsUiSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ 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 }, queryKey), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }) as TData, ...options }); + timetableType?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ 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 }, queryKey), queryFn: () => DagService.getDagsUi({ 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 }) as TData, ...options }); +/** +* 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 Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. +* @returns DagTimetableTypeCollectionResponse Successful Response +* @throws ApiError +*/ +export const useDagServiceGetDagTimetableTypesUiSuspense = = unknown[]>({ limit, offset, timetableTypePrefixPattern }: { + limit?: number; + offset?: number; + timetableTypePrefixPattern?: string; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ 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 2e4c24e117063..b5009ba3bad1a 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 @@ -9522,6 +9522,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 b2e288a93f173..10cc6fff1c6c6 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, DeleteDagAssetQueuedEventsData, DeleteDagAssetQueuedEventsResponse, GetDagAssetQueuedEventData, GetDagAssetQueuedEventResponse, DeleteDagAssetQueuedEventData, DeleteDagAssetQueuedEventResponse, NextRunAssetsData, NextRunAssetsResponse2, ListBackfillsData, ListBackfillsResponse, CreateBackfillData, CreateBackfillResponse, GetBackfillData, GetBackfillResponse, ListBackfillDagRunsData, ListBackfillDagRunsResponse, PauseBackfillData, PauseBackfillResponse, UnpauseBackfillData, UnpauseBackfillResponse, CancelBackfillData, CancelBackfillResponse, CreateBackfillDryRunData, CreateBackfillDryRunResponse, ListBackfillsUiData, ListBackfillsUiResponse, DeleteConnectionData, DeleteConnectionResponse, GetConnectionData, GetConnectionResponse, PatchConnectionData, PatchConnectionResponse, GetConnectionTestData, GetConnectionTestResponse, EnqueueConnectionTestData, EnqueueConnectionTestResponse, GetConnectionsData, GetConnectionsResponse, PostConnectionData, PostConnectionResponse, BulkConnectionsData, BulkConnectionsResponse, TestConnectionData, TestConnectionResponse, CreateDefaultConnectionsResponse, HookMetaDataResponse, GetDagRunData, GetDagRunResponse, DeleteDagRunData, DeleteDagRunResponse, PatchDagRunData, PatchDagRunResponse, BulkDagRunsData, BulkDagRunsResponse, GetDagRunsData, GetDagRunsResponse, TriggerDagRunData, TriggerDagRunResponse, GetUpstreamAssetEventsData, GetUpstreamAssetEventsResponse, ClearDagRunData, ClearDagRunResponse, WaitDagRunUntilFinishedData, WaitDagRunUntilFinishedResponse, GetListDagRunsBatchData, GetListDagRunsBatchResponse, ClearDagRunsData, ClearDagRunsResponse, ClearDagRunPartitionsData, ClearDagRunPartitionsResponse, GetDagRunStatsData, GetDagRunStatsResponse, GetDagSourceData, GetDagSourceResponse, GetDagStatsData, GetDagStatsResponse, GetConfigData, GetConfigResponse, GetConfigValueData, GetConfigValueResponse, GetConfigsResponse, ListDagWarningsData, ListDagWarningsResponse, GetDagsData, GetDagsResponse, PatchDagsData, PatchDagsResponse, GetDagData, GetDagResponse, PatchDagData, PatchDagResponse, DeleteDagData, DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, FavoriteDagData, FavoriteDagResponse, UnfavoriteDagData, UnfavoriteDagResponse, GetDagTagsData, GetDagTagsResponse, GetDagsUiData, GetDagsUiResponse, GetLatestRunInfoData, GetLatestRunInfoResponse, GetDagRunStateCountsUiData, GetDagRunStateCountsUiResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, GetEventLogsResponse, GetExtraLinksData, GetExtraLinksResponse, GetTaskInstanceData, GetTaskInstanceResponse, PatchTaskInstanceData, PatchTaskInstanceResponse, DeleteTaskInstanceData, DeleteTaskInstanceResponse, GetMappedTaskInstancesData, GetMappedTaskInstancesResponse, GetTaskInstanceDependenciesByMapIndexData, GetTaskInstanceDependenciesByMapIndexResponse, GetTaskInstanceDependenciesData, GetTaskInstanceDependenciesResponse, GetTaskInstanceTriesData, GetTaskInstanceTriesResponse, GetMappedTaskInstanceTriesData, GetMappedTaskInstanceTriesResponse, GetMappedTaskInstanceData, GetMappedTaskInstanceResponse, PatchTaskInstanceByMapIndexData, PatchTaskInstanceByMapIndexResponse, GetTaskInstancesData, GetTaskInstancesResponse, BulkTaskInstancesData, BulkTaskInstancesResponse, GetTaskInstancesBatchData, GetTaskInstancesBatchResponse, GetTaskInstanceTryDetailsData, GetTaskInstanceTryDetailsResponse, GetMappedTaskInstanceTryDetailsData, GetMappedTaskInstanceTryDetailsResponse, PostClearTaskInstancesData, PostClearTaskInstancesResponse, PatchTaskGroupInstancesData, PatchTaskGroupInstancesResponse, PatchTaskGroupInstancesDryRunData, PatchTaskGroupInstancesDryRunResponse, PatchTaskInstanceDryRunByMapIndexData, PatchTaskInstanceDryRunByMapIndexResponse, PatchTaskInstanceDryRunData, PatchTaskInstanceDryRunResponse, GetLogData, GetLogResponse, GetExternalLogUrlData, GetExternalLogUrlResponse, UpdateHitlDetailData, UpdateHitlDetailResponse, GetHitlDetailData, GetHitlDetailResponse, GetHitlDetailTryDetailData, GetHitlDetailTryDetailResponse, GetHitlDetailsData, GetHitlDetailsResponse, GetImportErrorData, GetImportErrorResponse, GetImportErrorsData, GetImportErrorsResponse, GetJobsData, GetJobsResponse, GetPluginsData, GetPluginsResponse, ImportErrorsResponse, DeletePoolData, DeletePoolResponse, GetPoolData, GetPoolResponse, PatchPoolData, PatchPoolResponse, GetPoolsData, GetPoolsResponse, PostPoolData, PostPoolResponse, BulkPoolsData, BulkPoolsResponse, GetProvidersData, GetProvidersResponse, ListAssetStateStoreData, ListAssetStateStoreResponse, ClearAssetStateStoreData, ClearAssetStateStoreResponse, GetAssetStateStoreData, GetAssetStateStoreResponse, SetAssetStateStoreData, SetAssetStateStoreResponse, DeleteAssetStateStoreData, DeleteAssetStateStoreResponse, ListTaskStateStoreData, ListTaskStateStoreResponse, ClearTaskStateStoreData, ClearTaskStateStoreResponse, GetTaskStateStoreData, GetTaskStateStoreResponse, SetTaskStateStoreData, SetTaskStateStoreResponse, PatchTaskStateStoreData, PatchTaskStateStoreResponse, DeleteTaskStateStoreData, DeleteTaskStateStoreResponse, GetXcomEntryData, GetXcomEntryResponse, UpdateXcomEntryData, UpdateXcomEntryResponse, DeleteXcomEntryData, DeleteXcomEntryResponse, GetXcomEntriesData, GetXcomEntriesResponse, CreateXcomEntryData, CreateXcomEntryResponse, GetTasksData, GetTasksResponse, GetTaskData, GetTaskResponse, DeleteVariableData, DeleteVariableResponse, GetVariableData, GetVariableResponse, PatchVariableData, PatchVariableResponse, GetVariablesData, GetVariablesResponse, PostVariableData, PostVariableResponse, BulkVariablesData, BulkVariablesResponse, ReparseDagFileData, ReparseDagFileResponse, GetDagVersionData, GetDagVersionResponse, GetDagVersionsData, GetDagVersionsResponse, GetHealthResponse, GetVersionResponse, LoginData, LoginResponse, LogoutResponse, GetAuthMenusResponse, GetCurrentUserInfoResponse, GenerateTokenData, GenerateTokenResponse2, GetPartitionedDagRunsData, GetPartitionedDagRunsResponse, GetPendingPartitionedDagRunData, GetPendingPartitionedDagRunResponse, GetDependenciesData, GetDependenciesResponse, HistoricalMetricsData, HistoricalMetricsResponse, DagStatsResponse2, GetDeadlinesData, GetDeadlinesResponse, GetDagDeadlineAlertsData, GetDagDeadlineAlertsResponse, StructureDataData, StructureDataResponse2, GetDagStructureData, GetDagStructureResponse, GetGridRunsData, GetGridRunsResponse, GetGridTiSummariesStreamData, GetGridTiSummariesStreamResponse, GetGanttDataData, GetGanttDataResponse, GetCalendarData, GetCalendarResponse, GetCalendarDeadlinesData, GetCalendarDeadlinesResponse, ListTeamsData, ListTeamsResponse } from './types.gen'; +import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, DeleteDagAssetQueuedEventsData, DeleteDagAssetQueuedEventsResponse, GetDagAssetQueuedEventData, GetDagAssetQueuedEventResponse, DeleteDagAssetQueuedEventData, DeleteDagAssetQueuedEventResponse, NextRunAssetsData, NextRunAssetsResponse2, ListBackfillsData, ListBackfillsResponse, CreateBackfillData, CreateBackfillResponse, GetBackfillData, GetBackfillResponse, ListBackfillDagRunsData, ListBackfillDagRunsResponse, PauseBackfillData, PauseBackfillResponse, UnpauseBackfillData, UnpauseBackfillResponse, CancelBackfillData, CancelBackfillResponse, CreateBackfillDryRunData, CreateBackfillDryRunResponse, ListBackfillsUiData, ListBackfillsUiResponse, DeleteConnectionData, DeleteConnectionResponse, GetConnectionData, GetConnectionResponse, PatchConnectionData, PatchConnectionResponse, GetConnectionTestData, GetConnectionTestResponse, EnqueueConnectionTestData, EnqueueConnectionTestResponse, GetConnectionsData, GetConnectionsResponse, PostConnectionData, PostConnectionResponse, BulkConnectionsData, BulkConnectionsResponse, TestConnectionData, TestConnectionResponse, CreateDefaultConnectionsResponse, HookMetaDataResponse, GetDagRunData, GetDagRunResponse, DeleteDagRunData, DeleteDagRunResponse, PatchDagRunData, PatchDagRunResponse, BulkDagRunsData, BulkDagRunsResponse, GetDagRunsData, GetDagRunsResponse, TriggerDagRunData, TriggerDagRunResponse, GetUpstreamAssetEventsData, GetUpstreamAssetEventsResponse, ClearDagRunData, ClearDagRunResponse, WaitDagRunUntilFinishedData, WaitDagRunUntilFinishedResponse, GetListDagRunsBatchData, GetListDagRunsBatchResponse, ClearDagRunsData, ClearDagRunsResponse, ClearDagRunPartitionsData, ClearDagRunPartitionsResponse, GetDagRunStatsData, GetDagRunStatsResponse, GetDagSourceData, GetDagSourceResponse, GetDagStatsData, GetDagStatsResponse, GetConfigData, GetConfigResponse, GetConfigValueData, GetConfigValueResponse, GetConfigsResponse, ListDagWarningsData, ListDagWarningsResponse, GetDagsData, GetDagsResponse, PatchDagsData, PatchDagsResponse, GetDagData, GetDagResponse, PatchDagData, PatchDagResponse, DeleteDagData, DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, FavoriteDagData, FavoriteDagResponse, UnfavoriteDagData, UnfavoriteDagResponse, GetDagTagsData, GetDagTagsResponse, GetDagsUiData, GetDagsUiResponse, GetDagTimetableTypesUiData, GetDagTimetableTypesUiResponse, GetLatestRunInfoData, GetLatestRunInfoResponse, GetDagRunStateCountsUiData, GetDagRunStateCountsUiResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, GetEventLogsResponse, GetExtraLinksData, GetExtraLinksResponse, GetTaskInstanceData, GetTaskInstanceResponse, PatchTaskInstanceData, PatchTaskInstanceResponse, DeleteTaskInstanceData, DeleteTaskInstanceResponse, GetMappedTaskInstancesData, GetMappedTaskInstancesResponse, GetTaskInstanceDependenciesByMapIndexData, GetTaskInstanceDependenciesByMapIndexResponse, GetTaskInstanceDependenciesData, GetTaskInstanceDependenciesResponse, GetTaskInstanceTriesData, GetTaskInstanceTriesResponse, GetMappedTaskInstanceTriesData, GetMappedTaskInstanceTriesResponse, GetMappedTaskInstanceData, GetMappedTaskInstanceResponse, PatchTaskInstanceByMapIndexData, PatchTaskInstanceByMapIndexResponse, GetTaskInstancesData, GetTaskInstancesResponse, BulkTaskInstancesData, BulkTaskInstancesResponse, GetTaskInstancesBatchData, GetTaskInstancesBatchResponse, GetTaskInstanceTryDetailsData, GetTaskInstanceTryDetailsResponse, GetMappedTaskInstanceTryDetailsData, GetMappedTaskInstanceTryDetailsResponse, PostClearTaskInstancesData, PostClearTaskInstancesResponse, PatchTaskGroupInstancesData, PatchTaskGroupInstancesResponse, PatchTaskGroupInstancesDryRunData, PatchTaskGroupInstancesDryRunResponse, PatchTaskInstanceDryRunByMapIndexData, PatchTaskInstanceDryRunByMapIndexResponse, PatchTaskInstanceDryRunData, PatchTaskInstanceDryRunResponse, GetLogData, GetLogResponse, GetExternalLogUrlData, GetExternalLogUrlResponse, UpdateHitlDetailData, UpdateHitlDetailResponse, GetHitlDetailData, GetHitlDetailResponse, GetHitlDetailTryDetailData, GetHitlDetailTryDetailResponse, GetHitlDetailsData, GetHitlDetailsResponse, GetImportErrorData, GetImportErrorResponse, GetImportErrorsData, GetImportErrorsResponse, GetJobsData, GetJobsResponse, GetPluginsData, GetPluginsResponse, ImportErrorsResponse, DeletePoolData, DeletePoolResponse, GetPoolData, GetPoolResponse, PatchPoolData, PatchPoolResponse, GetPoolsData, GetPoolsResponse, PostPoolData, PostPoolResponse, BulkPoolsData, BulkPoolsResponse, GetProvidersData, GetProvidersResponse, ListAssetStateStoreData, ListAssetStateStoreResponse, ClearAssetStateStoreData, ClearAssetStateStoreResponse, GetAssetStateStoreData, GetAssetStateStoreResponse, SetAssetStateStoreData, SetAssetStateStoreResponse, DeleteAssetStateStoreData, DeleteAssetStateStoreResponse, ListTaskStateStoreData, ListTaskStateStoreResponse, ClearTaskStateStoreData, ClearTaskStateStoreResponse, GetTaskStateStoreData, GetTaskStateStoreResponse, SetTaskStateStoreData, SetTaskStateStoreResponse, PatchTaskStateStoreData, PatchTaskStateStoreResponse, DeleteTaskStateStoreData, DeleteTaskStateStoreResponse, GetXcomEntryData, GetXcomEntryResponse, UpdateXcomEntryData, UpdateXcomEntryResponse, DeleteXcomEntryData, DeleteXcomEntryResponse, GetXcomEntriesData, GetXcomEntriesResponse, CreateXcomEntryData, CreateXcomEntryResponse, GetTasksData, GetTasksResponse, GetTaskData, GetTaskResponse, DeleteVariableData, DeleteVariableResponse, GetVariableData, GetVariableResponse, PatchVariableData, PatchVariableResponse, GetVariablesData, GetVariablesResponse, PostVariableData, PostVariableResponse, BulkVariablesData, BulkVariablesResponse, ReparseDagFileData, ReparseDagFileResponse, GetDagVersionData, GetDagVersionResponse, GetDagVersionsData, GetDagVersionsResponse, GetHealthResponse, GetVersionResponse, LoginData, LoginResponse, LogoutResponse, GetAuthMenusResponse, GetCurrentUserInfoResponse, GenerateTokenData, GenerateTokenResponse2, GetPartitionedDagRunsData, GetPartitionedDagRunsResponse, GetPendingPartitionedDagRunData, GetPendingPartitionedDagRunResponse, GetDependenciesData, GetDependenciesResponse, HistoricalMetricsData, HistoricalMetricsResponse, DagStatsResponse2, GetDeadlinesData, GetDeadlinesResponse, GetDagDeadlineAlertsData, GetDagDeadlineAlertsResponse, StructureDataData, StructureDataResponse2, GetDagStructureData, GetDagStructureResponse, GetGridRunsData, GetGridRunsResponse, GetGridTiSummariesStreamData, GetGridTiSummariesStreamResponse, GetGanttDataData, GetGanttDataResponse, GetCalendarData, GetCalendarResponse, GetCalendarDeadlinesData, GetCalendarDeadlinesResponse, ListTeamsData, ListTeamsResponse } from './types.gen'; export class AssetService { /** @@ -1977,6 +1977,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 @@ -2009,6 +2010,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: { @@ -2017,6 +2019,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 Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. + * @returns DagTimetableTypeCollectionResponse Successful Response + * @throws ApiError + */ + public static getDagTimetableTypesUi(data: GetDagTimetableTypesUiData = {}): CancelablePromise { + 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 0d36910280ac8..5387c4f0ca8fd 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 @@ -2406,6 +2406,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. */ @@ -3579,10 +3587,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; + /** + * Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. + */ + timetableTypePrefixPattern?: string | null; +}; + +export type GetDagTimetableTypesUiResponse = DagTimetableTypeCollectionResponse; + export type GetLatestRunInfoData = { dagId: string; }; @@ -6482,6 +6502,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 78b8681b423a5..753f7fe53fd85 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 eee952f020786..cf1174b881e81 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 b3d4d1fea35de..6a6116420ea76 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 }) => { + 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 = [ + 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 = [ }); } - 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 0000000000000..3a6858fdb9a66 --- /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) => void; + readonly onInputChange?: (value: string) => void; + readonly onMenuScrollToBottom?: () => void; + readonly onMenuScrollToTop?: () => void; + readonly options: Array; + readonly placeholder: string; + readonly values: Array; +}; + +export const DagsFilterSelect = ({ + ariaLabel, + noOptionsMessage, + onChange, + onInputChange, + onMenuScrollToBottom, + onMenuScrollToTop, + options, + placeholder, + values, +}: Props) => ( + + + 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 }))} + /> + +); 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 aac7b895ae4c8..14cdfa4c212cf 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(); @@ -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(); + + 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( + , + ); + + 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(); + + 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(); + + 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 424dbb1b6159d..7480a064692fa 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) => { + searchParams.delete(TIMETABLE_TYPE_PARAM); + for (const timetableType of selectedTimetableTypes) { + searchParams.append(TIMETABLE_TYPE_PARAM, timetableType); + } + resetPagination(); + setSearchParams(searchParams); + }; + + const handleSelectTagsChange = (tags: Array) => { + setSelectedTags(tags); }; const handleTagModeChange = ({ checked }: { checked: boolean }) => { @@ -167,7 +187,7 @@ export const DagsFilters = () => { const favoriteValue = toBooleanFilterValue(showFavorites); return ( - + { /> + { + void fetchNextTimetableTypePage(); + }} + onMenuScrollToTop={() => { + void fetchPreviousTimetableTypePage(); + }} + timetableTypes={timetableTypeData?.pages.flatMap((response) => response.timetable_types) ?? []} + values={timetableTypes} + /> { - 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) ?? []} /> - {multiTeamEnabled ? ( ) : undefined} + + + ); }; 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 f40d5829e3237..c50a3361babd8 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) => void; readonly onTagModeChange: ({ checked }: { checked: boolean }) => void; readonly onUpdate: (newValue: string) => void; readonly selectedTags: Array; @@ -46,47 +47,18 @@ export const TagFilter = ({ const { t: translate } = useTranslation("common"); return ( - - - ({ - ...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, - }))} - /> - + + {selectedTags.length >= 2 && ( { }; return ( - + { }), 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 0000000000000..954f76706b1cc --- /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) => void; + readonly onInputChange: (value: string) => void; + readonly onMenuScrollToBottom: () => void; + readonly onMenuScrollToTop: () => void; + readonly timetableTypes: Array; + readonly values: Array; +}; + +export const TimetableTypeFilter = ({ + onChange, + onInputChange, + onMenuScrollToBottom, + onMenuScrollToTop, + timetableTypes, + values, +}: Props) => { + const { t: translate } = useTranslation("dags"); + + return ( + + + + ); +}; 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 f82a9e90c428a..5faaa5321ae88 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 => ({ @@ -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 0000000000000..17fa4355a9a22 --- /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 = ( + { + limit, + timetableTypePrefixPattern, + }: { + limit?: number; + timetableTypePrefixPattern?: string; + } = {}, + queryKey?: Array, + options?: Omit< + UseInfiniteQueryOptions< + DagTimetableTypeCollectionResponse, + TError, + InfiniteData, + Array, + number + >, + "queryFn" | "queryKey" + >, +) => + useInfiniteQuery({ + getNextPageParam: ( + lastPage: DagTimetableTypeCollectionResponse, + _allPages: Array, + lastPageParam: number, + ) => + lastPageParam + lastPage.timetable_types.length < lastPage.total_entries + ? lastPageParam + lastPage.timetable_types.length + : undefined, + getPreviousPageParam: ( + firstPage: DagTimetableTypeCollectionResponse, + _allPages: Array, + 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 43d2cab576969..759180cb16a4a 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; tagsMatchMode?: "all" | "any"; teams?: Array; + timetableType?: Array; }) => { 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 837e7805c6994..14c7c73f657e9 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 @@ def test_should_return_200(self, test_client, query_params, expected_ids, expect 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 @@ def test_is_favorite_field_user_specific(self, test_client, session): _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``."""