From 4841870485a5f64f88e24e0db50cc3357f62db81 Mon Sep 17 00:00:00 2001 From: Shivam Rastogi <6463385+shivaam@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:03:35 -0700 Subject: [PATCH] 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. --- .../core_api/openapi/_private_ui.yaml | 8 ++ .../api_fastapi/core_api/routes/ui/dags.py | 5 ++ .../airflow/ui/openapi-gen/queries/common.ts | 5 +- .../ui/openapi-gen/queries/ensureQueryData.ts | 6 +- .../ui/openapi-gen/queries/prefetch.ts | 6 +- .../airflow/ui/openapi-gen/queries/queries.ts | 6 +- .../ui/openapi-gen/queries/suspense.ts | 6 +- .../ui/openapi-gen/requests/services.gen.ts | 2 + .../ui/openapi-gen/requests/types.gen.ts | 1 + .../ui/public/i18n/locales/en/dags.json | 4 +- .../airflow/ui/src/constants/searchParams.ts | 1 + .../src/airflow/ui/src/mocks/handlers/dags.ts | 14 ++- .../DagsList/DagsFilters/DagsFilters.test.tsx | 18 +++- .../DagsList/DagsFilters/DagsFilters.tsx | 14 +++ .../DagsFilters/TimetableTypeFilter.tsx | 89 +++++++++++++++++++ .../ui/src/pages/DagsList/DagsList.tsx | 3 + .../src/airflow/ui/src/queries/useDags.tsx | 3 + .../core_api/routes/ui/test_dags.py | 14 +++ 18 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx 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 e01a6f6e3c566..185e210817278 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 @@ -545,6 +545,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 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 38d0c93cef88d..86c2113e6f280 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 @@ -124,6 +124,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, @@ -158,6 +162,7 @@ def get_dags( is_favorite, has_asset_schedule, asset_dependency, + timetable_type, has_pending_actions, readable_dags_filter, bundle_name, 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 a8f08d3df78c0..5dba811322991 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -328,7 +328,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 }: { +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, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -352,7 +352,8 @@ export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, bundl paused?: boolean; tags?: string[]; tagsMatchMode?: "any" | "all"; -} = {}, 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 }])]; + 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, timetableType }])]; 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 34eff20314070..c4014b82b68df 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 }: { +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, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -702,7 +703,8 @@ export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { ass paused?: boolean; tags?: string[]; tagsMatchMode?: "any" | "all"; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }), 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 }) }); + 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, 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, timetableType }) }); /** * 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 170a17f9e58ae..f0869c9900a9b 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 }: { +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, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -702,7 +703,8 @@ export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { asset paused?: boolean; tags?: string[]; tagsMatchMode?: "any" | "all"; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }), 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 }) }); + 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, 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, timetableType }) }); /** * 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 7efadc2e4f8fa..0294fef5e5456 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 = = unknown[]>({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: { +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, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -702,7 +703,8 @@ 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 }, 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 }) 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, 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, timetableType }) 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 d4e694554d0ff..c0e1885c4f94a 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 = = unknown[]>({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: { +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, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -702,7 +703,8 @@ 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 }, 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 }) 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, 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, timetableType }) as TData, ...options }); /** * Get Latest Run Info * Get latest run. diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index 0356a4a364197..cf7a7db7db8d9 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 @@ -1934,6 +1934,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 @@ -1965,6 +1966,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: { 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 750202cd7fb0b..10d025b2f0cba 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 @@ -3523,6 +3523,7 @@ export type GetDagsUiData = { paused?: boolean | null; tags?: Array<(string)>; tagsMatchMode?: 'any' | 'all' | null; + timetableType?: Array<(string)>; }; export type GetDagsUiResponse = DAGWithLatestDagRunsCollectionResponse; 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 c8b195c93b8cd..cfbcb1f89eead 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 @@ -22,7 +22,9 @@ "all": "All", "paused": "Paused" }, - "runIdPatternFilter": "Search Dag Runs" + "runIdPatternFilter": "Search Dag Runs", + "timetableType": "Timetable type", + "timetableTypeClear": "Clear timetable type filter" }, "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 b6f4d88e3d0e5..da83040cad47e 100644 --- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts +++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts @@ -104,6 +104,7 @@ export enum SearchParamsKeys { TASK_ID = "task_id", TASK_ID_PATTERN = "task_id_pattern", TASK_STATE = "task_state", + 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 2c97bef0c8b60..9e21770c5e53a 100644 --- a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts @@ -48,6 +48,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 = { @@ -80,6 +81,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 = { @@ -102,10 +104,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); @@ -122,6 +129,7 @@ export const handlers: Array = [ const url = new URL(request.url); const lastDagRunState = url.searchParams.get("last_dag_run_state"); const paused = url.searchParams.get("paused"); + const timetableTypes = url.searchParams.getAll("timetable_type"); if (lastDagRunState === "success") { return HttpResponse.json({ @@ -135,7 +143,7 @@ export const handlers: Array = [ }); } - const dags = filterDagsByPaused(paused); + const dags = filterDags({ paused, timetableTypes }); return HttpResponse.json({ dags, 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..714a9ef234bec 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,7 +17,7 @@ * under the License. */ import "@testing-library/jest-dom"; -import { render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { AppWrapper } from "src/utils/AppWrapper"; @@ -67,4 +67,20 @@ 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 dags by timetable type", async () => { + render(); + + await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument()); + expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); + + fireEvent.change(screen.getByTestId("dags-timetable-type-filter"), { + target: { value: "CronTriggerTimetable" }, + }); + + await waitFor(() => { + expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument(); + expect(screen.getByText("tutorial_taskflow_api_failed")).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 9f1863bc12048..c3be6660a2953 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 @@ -33,6 +33,7 @@ import { PausedFilter } from "./PausedFilter"; import { RequiredActionFilter } from "./RequiredActionFilter"; import { RunStateSelect } from "./RunStateSelect"; import { TagFilter } from "./TagFilter"; +import { TimetableTypeFilter } from "./TimetableTypeFilter"; const { DAG_RUN_STATE: DAG_RUN_STATE_PARAM, @@ -41,6 +42,7 @@ const { NEEDS_REVIEW: NEEDS_REVIEW_PARAM, OFFSET: OFFSET_PARAM, PAUSED: PAUSED_PARAM, + TIMETABLE_TYPE: TIMETABLE_TYPE_PARAM, }: SearchParamsKeysType = SearchParamsKeys; type BooleanFilterValue = "all" | "false" | "true"; @@ -66,6 +68,7 @@ export const DagsFilters = () => { const needsReview = searchParams.get(NEEDS_REVIEW_PARAM); const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM); const activeRunState = searchParams.get(DAG_RUN_STATE_PARAM); + const timetableType = searchParams.get(TIMETABLE_TYPE_PARAM) ?? ""; const [pattern, setPattern] = useState(""); @@ -139,6 +142,16 @@ export const DagsFilters = () => { setSearchParams(searchParams); }; + const handleTimetableTypeChange = (value: string) => { + if (value) { + searchParams.set(TIMETABLE_TYPE_PARAM, value); + } else { + searchParams.delete(TIMETABLE_TYPE_PARAM); + } + resetPagination(); + setSearchParams(searchParams); + }; + const handleSelectTagsChange = ( tags: MultiValue<{ label: string; @@ -173,6 +186,7 @@ export const DagsFilters = () => { /> + { void fetchNextPage(); 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..06d325e4fc527 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx @@ -0,0 +1,89 @@ +/*! + * 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 { CloseButton, Input, InputGroup } from "@chakra-ui/react"; +import { useEffect, useRef, useState, type ChangeEvent } from "react"; +import { useTranslation } from "react-i18next"; +import { FiClock } from "react-icons/fi"; +import { useDebouncedCallback } from "use-debounce"; + +const debounceDelay = 200; + +type Props = { + readonly onChange: (value: string) => void; + readonly value: string; +}; + +export const TimetableTypeFilter = ({ onChange, value: initialValue }: Props) => { + const [value, setValue] = useState(initialValue); + const lastSentValue = useRef(initialValue); + const { t: translate } = useTranslation("dags"); + + const handleFilterChange = useDebouncedCallback((newValue: string) => { + lastSentValue.current = newValue; + onChange(newValue); + }, debounceDelay); + + useEffect(() => { + if (initialValue !== lastSentValue.current) { + setValue(initialValue); + lastSentValue.current = initialValue; + } + }, [initialValue]); + + const onInputChange = (event: ChangeEvent) => { + const newValue = event.target.value; + + setValue(newValue); + handleFilterChange(newValue.trim()); + }; + + const clearFilter = () => { + handleFilterChange.cancel(); + lastSentValue.current = ""; + setValue(""); + onChange(""); + }; + + return ( + + ) : undefined + } + maxWidth="260px" + minWidth="220px" + startElement={} + > + + + ); +}; 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 e9c5215cc5a06..c101ec6922758 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx @@ -202,6 +202,7 @@ const { OFFSET, OWNERS, PAUSED, + TIMETABLE_TYPE, }: SearchParamsKeysType = SearchParamsKeys; const createCardDef = (runStateContext: RunStateCountsContext): CardDef => ({ @@ -238,6 +239,7 @@ export const DagsList = () => { const { selectedTags, tagFilterMode: selectedMatchMode } = useTagFilter(); const pendingReviews = searchParams.get(NEEDS_REVIEW); const owners = searchParams.getAll(OWNERS); + const timetableType = searchParams.getAll(TIMETABLE_TYPE); const { setTableURLState, tableURLState } = useTableURLState(); @@ -301,6 +303,7 @@ export const DagsList = () => { pendingHitl, tags: selectedTags, tagsMatchMode: selectedMatchMode, + timetableType: timetableType.length > 0 ? timetableType : undefined, }); const { data: runStateCountsData, isLoading: runStateCountsLoading } = useDagRunStateCounts({ diff --git a/airflow-core/src/airflow/ui/src/queries/useDags.tsx b/airflow-core/src/airflow/ui/src/queries/useDags.tsx index ec4dfb18774e7..04b372638bb37 100644 --- a/airflow-core/src/airflow/ui/src/queries/useDags.tsx +++ b/airflow-core/src/airflow/ui/src/queries/useDags.tsx @@ -41,6 +41,7 @@ export const useDags = ({ pendingHitl, tags, tagsMatchMode, + timetableType, }: { advancedSearch?: boolean; dagDisplayNamePattern?: string; @@ -58,6 +59,7 @@ export const useDags = ({ pendingHitl?: boolean; tags?: Array; tagsMatchMode?: "all" | "any"; + timetableType?: Array; }) => { const refetchInterval = useAutoRefresh({}); @@ -79,6 +81,7 @@ export const useDags = ({ paused, tags, tagsMatchMode, + 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 4e561fd343664..84693b677b375 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") def test_dag_run_state_matches_any_run_not_only_latest(self, test_client, session): # Backwards backfill: an older run is still running while the latest run already finished.