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.