diff --git a/airflow-core/docs/img/ui-dark/dag_overview_backfills.png b/airflow-core/docs/img/ui-dark/dag_overview_backfills.png new file mode 100644 index 0000000000000..074ae7078eb24 Binary files /dev/null and b/airflow-core/docs/img/ui-dark/dag_overview_backfills.png differ diff --git a/airflow-core/docs/img/ui-light/dag_overview_backfills.png b/airflow-core/docs/img/ui-light/dag_overview_backfills.png new file mode 100644 index 0000000000000..7a8271caa539f Binary files /dev/null and b/airflow-core/docs/img/ui-light/dag_overview_backfills.png differ diff --git a/airflow-core/docs/ui.rst b/airflow-core/docs/ui.rst index 23df5ce29b320..2856091318208 100644 --- a/airflow-core/docs/ui.rst +++ b/airflow-core/docs/ui.rst @@ -229,6 +229,22 @@ The **Events** tab surfaces structured events related to the Dag, such as Dag tr .. image:: img/ui-light/dag_overview_events.png :alt: Dag Events Tab (Light Mode) +Backfills Tab +'''''''' + +The **Backfills** tab displays historical backfill jobs for a specific DAG, including their date ranges, reprocessing behavior, and execution status. + +.. image:: img/ui-dark/dag_overview_backfills.png + :alt: Dag Backfills Tab (Dark Mode) + +| + +.. image:: img/ui-light/dag_overview_backfills.png + :alt: Dag Overview Tab (Light Mode) + +| + + Code Tab '''''''' 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..0eed75d55ff54 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 @@ -1102,6 +1102,190 @@ paths: minimum: 0 default: 0 title: Offset + - name: from_date_gte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: From Date Gte + - name: from_date_gt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: From Date Gt + - name: from_date_lte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: From Date Lte + - name: from_date_lt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: From Date Lt + - name: to_date_gte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: To Date Gte + - name: to_date_gt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: To Date Gt + - name: to_date_lte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: To Date Lte + - name: to_date_lt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: To Date Lt + - name: created_at_gte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Gte + - name: created_at_gt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Gt + - name: created_at_lte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Lte + - name: created_at_lt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Created At Lt + - name: completed_at_gte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Completed At Gte + - name: completed_at_gt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Completed At Gt + - name: completed_at_lte + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Completed At Lte + - name: completed_at_lt + in: query + required: false + schema: + anyOf: + - type: string + format: date-time + - type: 'null' + title: Completed At Lt + - name: max_active_runs_gte + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Active Runs Gte + - name: max_active_runs_gt + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Active Runs Gt + - name: max_active_runs_lte + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Active Runs Lte + - name: max_active_runs_lt + in: query + required: false + schema: + anyOf: + - type: number + - type: 'null' + title: Max Active Runs Lt + - name: reprocess_behavior + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Reprocess Behavior - name: order_by in: query required: false diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py index 02583a8355b9d..2d0deeb94762b 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/backfills.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import logging from typing import Annotated from fastapi import Depends, status @@ -28,8 +29,11 @@ FilterParam, QueryLimit, QueryOffset, + RangeFilter, SortParam, + datetime_range_filter_factory, filter_param_factory, + float_range_filter_factory, ) from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.datamodels.backfills import BackfillCollectionResponse, BackfillResponse @@ -39,6 +43,8 @@ from airflow.api_fastapi.core_api.security import ReadableBackfillsFilterDep, requires_access_backfill from airflow.models.backfill import Backfill +log = logging.getLogger(__name__) + backfills_router = AirflowRouter(tags=["Backfill"], prefix="/backfills") @@ -52,6 +58,14 @@ def list_backfills_ui( limit: QueryLimit, offset: QueryOffset, + start_date_range: Annotated[RangeFilter, Depends(datetime_range_filter_factory("from_date", Backfill))], + end_date_range: Annotated[RangeFilter, Depends(datetime_range_filter_factory("to_date", Backfill))], + created_at: Annotated[RangeFilter, Depends(datetime_range_filter_factory("created_at", Backfill))], + completed_at: Annotated[RangeFilter, Depends(datetime_range_filter_factory("completed_at", Backfill))], + max_active_runs: Annotated[RangeFilter, Depends(float_range_filter_factory("max_active_runs", Backfill))], + reprocess_behavior: Annotated[ + FilterParam, Depends(filter_param_factory(Backfill.reprocess_behavior, str | None)) + ], order_by: Annotated[ SortParam, Depends(SortParam(["id"], Backfill).dynamic_depends()), @@ -66,7 +80,17 @@ def list_backfills_ui( ) -> BackfillCollectionResponse: select_stmt, total_entries = paginated_select( statement=select(Backfill).options(joinedload(Backfill.dag_model)), - filters=[dag_id, active, readable_backfills_filter], + filters=[ + dag_id, + active, + readable_backfills_filter, + start_date_range, + end_date_range, + created_at, + completed_at, + max_active_runs, + reprocess_behavior, + ], order_by=order_by, offset=offset, limit=limit, @@ -76,6 +100,7 @@ def list_backfills_ui( BackfillResponse(**row._mapping) if not isinstance(row, Backfill) else row for row in session.scalars(select_stmt) ] + log.warning("inside backfill*******************************************") return BackfillCollectionResponse( backfills=backfills, total_entries=total_entries, 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..eb65f31fb9af9 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -117,13 +117,34 @@ export const UseBackfillServiceListBackfillDagRunsKeyFn = ({ backfillId, limit, export type BackfillServiceListBackfillsUiDefaultResponse = Awaited>; export type BackfillServiceListBackfillsUiQueryResult = UseQueryResult; export const useBackfillServiceListBackfillsUiKey = "BackfillServiceListBackfillsUi"; -export const UseBackfillServiceListBackfillsUiKeyFn = ({ active, dagId, limit, offset, orderBy }: { +export const UseBackfillServiceListBackfillsUiKeyFn = ({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }: { active?: boolean; + completedAtGt?: string; + completedAtGte?: string; + completedAtLt?: string; + completedAtLte?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId?: string; + fromDateGt?: string; + fromDateGte?: string; + fromDateLt?: string; + fromDateLte?: string; limit?: number; + maxActiveRunsGt?: number; + maxActiveRunsGte?: number; + maxActiveRunsLt?: number; + maxActiveRunsLte?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: Array) => [useBackfillServiceListBackfillsUiKey, ...(queryKey ?? [{ active, dagId, limit, offset, orderBy }])]; + reprocessBehavior?: string; + toDateGt?: string; + toDateGte?: string; + toDateLt?: string; + toDateLte?: string; +} = {}, queryKey?: Array) => [useBackfillServiceListBackfillsUiKey, ...(queryKey ?? [{ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }])]; export type ConnectionServiceGetConnectionDefaultResponse = Awaited>; export type ConnectionServiceGetConnectionQueryResult = UseQueryResult; export const useConnectionServiceGetConnectionKey = "ConnectionServiceGetConnection"; 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..9cb7d6c335f57 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -224,19 +224,61 @@ export const ensureUseBackfillServiceListBackfillDagRunsData = (queryClient: Que * @param data The data for the request. * @param data.limit * @param data.offset +* @param data.fromDateGte +* @param data.fromDateGt +* @param data.fromDateLte +* @param data.fromDateLt +* @param data.toDateGte +* @param data.toDateGt +* @param data.toDateLte +* @param data.toDateLt +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt +* @param data.completedAtGte +* @param data.completedAtGt +* @param data.completedAtLte +* @param data.completedAtLt +* @param data.maxActiveRunsGte +* @param data.maxActiveRunsGt +* @param data.maxActiveRunsLte +* @param data.maxActiveRunsLt +* @param data.reprocessBehavior * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` * @param data.dagId * @param data.active * @returns BackfillCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseBackfillServiceListBackfillsUiData = (queryClient: QueryClient, { active, dagId, limit, offset, orderBy }: { +export const ensureUseBackfillServiceListBackfillsUiData = (queryClient: QueryClient, { active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }: { active?: boolean; + completedAtGt?: string; + completedAtGte?: string; + completedAtLt?: string; + completedAtLte?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId?: string; + fromDateGt?: string; + fromDateGte?: string; + fromDateLt?: string; + fromDateLte?: string; limit?: number; + maxActiveRunsGt?: number; + maxActiveRunsGte?: number; + maxActiveRunsLt?: number; + maxActiveRunsLte?: number; offset?: number; orderBy?: string[]; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseBackfillServiceListBackfillsUiKeyFn({ active, dagId, limit, offset, orderBy }), queryFn: () => BackfillService.listBackfillsUi({ active, dagId, limit, offset, orderBy }) }); + reprocessBehavior?: string; + toDateGt?: string; + toDateGte?: string; + toDateLt?: string; + toDateLte?: string; +} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseBackfillServiceListBackfillsUiKeyFn({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }), queryFn: () => BackfillService.listBackfillsUi({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }) }); /** * Get Connection * Get a connection entry. 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..7b378c3015881 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -224,19 +224,61 @@ export const prefetchUseBackfillServiceListBackfillDagRuns = (queryClient: Query * @param data The data for the request. * @param data.limit * @param data.offset +* @param data.fromDateGte +* @param data.fromDateGt +* @param data.fromDateLte +* @param data.fromDateLt +* @param data.toDateGte +* @param data.toDateGt +* @param data.toDateLte +* @param data.toDateLt +* @param data.createdAtGte +* @param data.createdAtGt +* @param data.createdAtLte +* @param data.createdAtLt +* @param data.completedAtGte +* @param data.completedAtGt +* @param data.completedAtLte +* @param data.completedAtLt +* @param data.maxActiveRunsGte +* @param data.maxActiveRunsGt +* @param data.maxActiveRunsLte +* @param data.maxActiveRunsLt +* @param data.reprocessBehavior * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` * @param data.dagId * @param data.active * @returns BackfillCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseBackfillServiceListBackfillsUi = (queryClient: QueryClient, { active, dagId, limit, offset, orderBy }: { +export const prefetchUseBackfillServiceListBackfillsUi = (queryClient: QueryClient, { active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }: { active?: boolean; + completedAtGt?: string; + completedAtGte?: string; + completedAtLt?: string; + completedAtLte?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId?: string; + fromDateGt?: string; + fromDateGte?: string; + fromDateLt?: string; + fromDateLte?: string; limit?: number; + maxActiveRunsGt?: number; + maxActiveRunsGte?: number; + maxActiveRunsLt?: number; + maxActiveRunsLte?: number; offset?: number; orderBy?: string[]; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseBackfillServiceListBackfillsUiKeyFn({ active, dagId, limit, offset, orderBy }), queryFn: () => BackfillService.listBackfillsUi({ active, dagId, limit, offset, orderBy }) }); + reprocessBehavior?: string; + toDateGt?: string; + toDateGte?: string; + toDateLt?: string; + toDateLte?: string; +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseBackfillServiceListBackfillsUiKeyFn({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }), queryFn: () => BackfillService.listBackfillsUi({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }) }); /** * Get Connection * Get a connection entry. 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..ceb0f9ca2bb9c 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -224,19 +224,61 @@ export const useBackfillServiceListBackfillDagRuns = = unknown[]>({ active, dagId, limit, offset, orderBy }: { +export const useBackfillServiceListBackfillsUi = = unknown[]>({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }: { active?: boolean; + completedAtGt?: string; + completedAtGte?: string; + completedAtLt?: string; + completedAtLte?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId?: string; + fromDateGt?: string; + fromDateGte?: string; + fromDateLt?: string; + fromDateLte?: string; limit?: number; + maxActiveRunsGt?: number; + maxActiveRunsGte?: number; + maxActiveRunsLt?: number; + maxActiveRunsLte?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseBackfillServiceListBackfillsUiKeyFn({ active, dagId, limit, offset, orderBy }, queryKey), queryFn: () => BackfillService.listBackfillsUi({ active, dagId, limit, offset, orderBy }) as TData, ...options }); + reprocessBehavior?: string; + toDateGt?: string; + toDateGte?: string; + toDateLt?: string; + toDateLte?: string; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseBackfillServiceListBackfillsUiKeyFn({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }, queryKey), queryFn: () => BackfillService.listBackfillsUi({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }) as TData, ...options }); /** * Get Connection * Get a connection entry. 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..93f45554bca62 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -224,19 +224,61 @@ export const useBackfillServiceListBackfillDagRunsSuspense = = unknown[]>({ active, dagId, limit, offset, orderBy }: { +export const useBackfillServiceListBackfillsUiSuspense = = unknown[]>({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }: { active?: boolean; + completedAtGt?: string; + completedAtGte?: string; + completedAtLt?: string; + completedAtLte?: string; + createdAtGt?: string; + createdAtGte?: string; + createdAtLt?: string; + createdAtLte?: string; dagId?: string; + fromDateGt?: string; + fromDateGte?: string; + fromDateLt?: string; + fromDateLte?: string; limit?: number; + maxActiveRunsGt?: number; + maxActiveRunsGte?: number; + maxActiveRunsLt?: number; + maxActiveRunsLte?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseBackfillServiceListBackfillsUiKeyFn({ active, dagId, limit, offset, orderBy }, queryKey), queryFn: () => BackfillService.listBackfillsUi({ active, dagId, limit, offset, orderBy }) as TData, ...options }); + reprocessBehavior?: string; + toDateGt?: string; + toDateGte?: string; + toDateLt?: string; + toDateLte?: string; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseBackfillServiceListBackfillsUiKeyFn({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }, queryKey), queryFn: () => BackfillService.listBackfillsUi({ active, completedAtGt, completedAtGte, completedAtLt, completedAtLte, createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, fromDateGt, fromDateGte, fromDateLt, fromDateLte, limit, maxActiveRunsGt, maxActiveRunsGte, maxActiveRunsLt, maxActiveRunsLte, offset, orderBy, reprocessBehavior, toDateGt, toDateGte, toDateLt, toDateLte }) as TData, ...options }); /** * Get Connection * Get a connection entry. 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..d1675973761f0 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 @@ -647,6 +647,27 @@ export class BackfillService { * @param data The data for the request. * @param data.limit * @param data.offset + * @param data.fromDateGte + * @param data.fromDateGt + * @param data.fromDateLte + * @param data.fromDateLt + * @param data.toDateGte + * @param data.toDateGt + * @param data.toDateLte + * @param data.toDateLt + * @param data.createdAtGte + * @param data.createdAtGt + * @param data.createdAtLte + * @param data.createdAtLt + * @param data.completedAtGte + * @param data.completedAtGt + * @param data.completedAtLte + * @param data.completedAtLt + * @param data.maxActiveRunsGte + * @param data.maxActiveRunsGt + * @param data.maxActiveRunsLte + * @param data.maxActiveRunsLt + * @param data.reprocessBehavior * @param data.orderBy Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` * @param data.dagId * @param data.active @@ -660,6 +681,27 @@ export class BackfillService { query: { limit: data.limit, offset: data.offset, + from_date_gte: data.fromDateGte, + from_date_gt: data.fromDateGt, + from_date_lte: data.fromDateLte, + from_date_lt: data.fromDateLt, + to_date_gte: data.toDateGte, + to_date_gt: data.toDateGt, + to_date_lte: data.toDateLte, + to_date_lt: data.toDateLt, + created_at_gte: data.createdAtGte, + created_at_gt: data.createdAtGt, + created_at_lte: data.createdAtLte, + created_at_lt: data.createdAtLt, + completed_at_gte: data.completedAtGte, + completed_at_gt: data.completedAtGt, + completed_at_lte: data.completedAtLte, + completed_at_lt: data.completedAtLt, + max_active_runs_gte: data.maxActiveRunsGte, + max_active_runs_gt: data.maxActiveRunsGt, + max_active_runs_lte: data.maxActiveRunsLte, + max_active_runs_lt: data.maxActiveRunsLt, + reprocess_behavior: data.reprocessBehavior, order_by: data.orderBy, dag_id: data.dagId, active: data.active 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..4abed4c4bd61d 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 @@ -3055,13 +3055,34 @@ export type CreateBackfillDryRunResponse = DryRunBackfillCollectionResponse; export type ListBackfillsUiData = { active?: boolean | null; + completedAtGt?: string | null; + completedAtGte?: string | null; + completedAtLt?: string | null; + completedAtLte?: string | null; + createdAtGt?: string | null; + createdAtGte?: string | null; + createdAtLt?: string | null; + createdAtLte?: string | null; dagId?: string | null; + fromDateGt?: string | null; + fromDateGte?: string | null; + fromDateLt?: string | null; + fromDateLte?: string | null; limit?: number; + maxActiveRunsGt?: number | null; + maxActiveRunsGte?: number | null; + maxActiveRunsLt?: number | null; + maxActiveRunsLte?: number | null; offset?: number; /** * Attributes to order by, multi criteria sort is supported. Prefix with `-` for descending order. Supported attributes: `id` */ orderBy?: Array<(string)>; + reprocessBehavior?: string | null; + toDateGt?: string | null; + toDateGte?: string | null; + toDateLt?: string | null; + toDateLte?: string | null; }; export type ListBackfillsUiResponse = BackfillCollectionResponse; diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx index 7c29e276d7b2f..bc2c23aaec678 100644 --- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx +++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx @@ -41,6 +41,7 @@ import { TaskIcon } from "src/assets/TaskIcon"; import type { FilterConfig } from "src/components/FilterBar"; import { RunTypeIcon } from "src/components/RunTypeIcon"; import { StateBadge } from "src/components/StateBadge"; +import { reprocessBehaviors } from "src/constants/reprocessBehaviourParams"; import { dagRunStateOptions, dagRunTypeOptions, @@ -87,6 +88,13 @@ export const useFilterConfigs = () => { label: translate("components:versionDetails.bundleVersion"), type: FilterTypes.TEXT, }, + [SearchParamsKeys.COMPLETED_AT_RANGE]: { + endKey: SearchParamsKeys.COMPLETED_AT_LTE, + icon: , + label: translate("hitl:filters.completedAt"), + startKey: SearchParamsKeys.COMPLETED_AT_GTE, + type: FilterTypes.DATERANGE, + }, [SearchParamsKeys.CONF_CONTAINS]: { hotkeyDisabled: true, icon: , @@ -222,6 +230,18 @@ export const useFilterConfigs = () => { min: -1, type: FilterTypes.NUMBER, }, + [SearchParamsKeys.MAX_ACTIVE_RUNS_GTE]: { + icon: , + label: translate("common:filters.maxActiveRunsFrom"), + min: 1, + type: FilterTypes.NUMBER, + }, + [SearchParamsKeys.MAX_ACTIVE_RUNS_LTE]: { + icon: , + label: translate("common:filters.maxActiveRunsTo"), + min: 1, + type: FilterTypes.NUMBER, + }, [SearchParamsKeys.MISSED]: { icon: , label: translate("browse:deadlines.filters.status"), @@ -274,6 +294,15 @@ export const useFilterConfigs = () => { supportsAdvancedSearch: true, type: FilterTypes.TEXT, }, + [SearchParamsKeys.REPROCESS_BEHAVIOR]: { + icon: , + label: translate("components:backfill.reprocessBehavior"), + options: reprocessBehaviors.map((option) => ({ + label: translate(option.label), + value: option.value, + })), + type: FilterTypes.SELECT, + }, [SearchParamsKeys.RESPONDED_BY_USER_NAME]: { hotkeyDisabled: true, icon: , diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts b/airflow-core/src/airflow/ui/src/constants/searchParams.ts index eee952f020786..4086021fecd54 100644 --- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts +++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts @@ -22,6 +22,9 @@ export enum SearchParamsKeys { BEFORE = "before", BODY_SEARCH = "body_search", BUNDLE_VERSION = "bundle_version", + COMPLETED_AT_GTE = "completed_at_gte", + COMPLETED_AT_LTE = "completed_at_lte", + COMPLETED_AT_RANGE = "completed_at_range", CONF_CONTAINS = "conf_contains", CONSUMING_ASSET_PATTERN = "consuming_asset_pattern", CREATED_AT_GTE = "created_at_gte", @@ -68,6 +71,9 @@ export enum SearchParamsKeys { LOGICAL_DATE_RANGE = "logical_date_range", MAP_INDEX = "map_index", MAPPED = "mapped", + MAX_ACTIVE_RUNS_GTE = "max_active_runs_gte", + MAX_ACTIVE_RUNS_LTE = "max_active_runs_lte", + MAX_ACTIVE_RUNS_RANGE = "max_active_runs_range", MISSED = "missed", NAME_PATTERN = "name_pattern", NEEDS_REVIEW = "needs_review", @@ -81,6 +87,7 @@ export enum SearchParamsKeys { POOL_NAME_PATTERN = "pool_name_pattern", QUEUE_NAME_PATTERN = "queue_name_pattern", RENDERED_MAP_INDEX = "rendered_map_index", + REPROCESS_BEHAVIOR = "reprocess_behavior", RESPONDED_BY_USER_NAME = "responded_by_user_name", RESPONSE_RECEIVED = "response_received", RETRIES = "retries", diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/backfills.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/backfills.ts new file mode 100644 index 0000000000000..ee895e16987e0 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/backfills.ts @@ -0,0 +1,86 @@ +/*! + * 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 { http, HttpResponse, type HttpHandler } from "msw"; + +const backfillBeforeFilter = { + completed_at: "2024-12-31T01:00:00Z", + created_at: "2024-12-31T00:00:00Z", + dag_display_name: "tutorial_taskflow_api", + dag_id: "tutorial_taskflow_api", + dag_run_conf: {}, + from_date: "2024-12-01T00:00:00Z", + id: 1, + is_paused: false, + max_active_runs: 10, + reprocess_behavior: "none", + to_date: "2024-12-31T00:00:00Z", + updated_at: "2024-12-31T01:00:00Z", +}; + +const backfillInRange = { + completed_at: "2025-01-16T01:00:00Z", + created_at: "2025-01-16T00:00:00Z", + dag_display_name: "tutorial_taskflow_api", + dag_id: "tutorial_taskflow_api", + dag_run_conf: {}, + from_date: "2025-01-01T00:00:00Z", + id: 2, + is_paused: false, + max_active_runs: 10, + reprocess_behavior: "none", + to_date: "2025-01-15T00:00:00Z", + updated_at: "2025-01-16T01:00:00Z", +}; + +export const handlers: Array = [ + http.get("/ui/backfills", ({ request }) => { + const url = new URL(request.url); + const fromDateGte = url.searchParams.get("from_date_gte"); + const fromDateLte = url.searchParams.get("from_date_lte"); + const toDateGte = url.searchParams.get("to_date_gte"); + const toDateLte = url.searchParams.get("to_date_lte"); + + const allBackfills = [backfillBeforeFilter, backfillInRange]; + + const filtered = allBackfills.filter((backfill) => { + const fromDate = new Date(backfill.from_date); + const toDate = new Date(backfill.to_date); + + if (fromDateGte !== null && fromDate < new Date(fromDateGte)) { + return false; + } + if (fromDateLte !== null && fromDate > new Date(fromDateLte)) { + return false; + } + if (toDateGte !== null && toDate < new Date(toDateGte)) { + return false; + } + if (toDateLte !== null && toDate > new Date(toDateLte)) { + return false; + } + + return true; + }); + + return HttpResponse.json({ + backfills: filtered, + total_entries: filtered.length, + }); + }), +]; diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/index.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/index.ts index 9f60605f2b6dc..21a5ea4180442 100644 --- a/airflow-core/src/airflow/ui/src/mocks/handlers/index.ts +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/index.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handlers as backfillsHandlers } from "./backfills"; import { handlers as configHandlers } from "./config"; import { handlers as dagHandlers } from "./dag"; import { handlers as dagRunsHandlers } from "./dag_runs"; @@ -23,6 +24,7 @@ import { handlers as dagsHandlers } from "./dags"; import { handlers as logHandlers } from "./log"; export const handlers = [ + ...backfillsHandlers, ...configHandlers, ...dagHandlers, ...dagRunsHandlers, diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.test.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.test.tsx new file mode 100644 index 0000000000000..3524fd68644fe --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.test.tsx @@ -0,0 +1,65 @@ +/*! + * 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 "@testing-library/jest-dom"; +import { render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { describe, expect, it } from "vitest"; + +import { TimezoneProvider } from "src/context/timezone"; +import { BaseWrapper } from "src/utils/Wrapper"; + +import "../../../i18n/config"; +import { Backfills } from "./Backfills"; + +// The backfills mock handler (see src/mocks/handlers/backfills.ts) returns: +// - id 1 (start_date: 2024-12-01, end_date: 2024-12-31) — excluded when filtering Jan 2025 +// - id 2 (start_date: 2025-01-01, end_date: 2025-01-15) — included when filtering Jan 2025 +const renderBackfills = (initialEntry: string) => + render( + + + + + } path="dags/:dagId/backfills" /> + + + + , + ); + +const getFromDateCells = () => + screen.getAllByTestId("time-display").map((element) => element.getAttribute("datetime")); + +describe("Backfills start/end date filter", () => { + it("shows all backfills when no date filter is applied", async () => { + renderBackfills("/dags/tutorial_taskflow_api/backfills"); + + await waitFor(() => expect(getFromDateCells()).toContain("2025-01-01T00:00:00Z")); + expect(getFromDateCells()).toContain("2024-12-01T00:00:00Z"); + }); + + it("filters backfills by start_date_gte and end_date_lte URL params", async () => { + renderBackfills( + "/dags/tutorial_taskflow_api/backfills?start_date_gte=2025-01-01T00%3A00%3A00Z&end_date_lte=2025-01-31T23%3A59%3A59Z", + ); + + await waitFor(() => expect(getFromDateCells()).toContain("2025-01-01T00:00:00Z")); + expect(getFromDateCells()).not.toContain("2024-12-01T00:00:00Z"); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx index c4ffbc8e20f68..30c699f8615e8 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx @@ -19,7 +19,7 @@ import { Box, Heading, Text } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; +import { useParams, useSearchParams } from "react-router-dom"; import { useBackfillServiceListBackfillsUi } from "openapi/queries"; import type { BackfillResponse } from "openapi/requests/types.gen"; @@ -27,8 +27,25 @@ import { DataTable } from "src/components/DataTable"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ErrorAlert } from "src/components/ErrorAlert"; import Time from "src/components/Time"; +import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; import { getDuration } from "src/utils"; +import { BackfillsFilters } from "./BackfillsFilters"; + +const { + COMPLETED_AT_GTE: COMPLETED_AT_GTE_PARAM, + COMPLETED_AT_LTE: COMPLETED_AT_LTE_PARAM, + CREATED_AT_GTE: CREATED_AT_GTE_PARAM, + CREATED_AT_LTE: CREATED_AT_LTE_PARAM, + END_DATE_GTE: END_DATE_GTE_PARAM, + END_DATE_LTE: END_DATE_LTE_PARAM, + MAX_ACTIVE_RUNS_GTE: MAX_ACTIVE_RUNS_GTE_PARAM, + MAX_ACTIVE_RUNS_LTE: MAX_ACTIVE_RUNS_LTE_PARAM, + REPROCESS_BEHAVIOR: REPROCESS_BEHAVIOR_PARAM, + START_DATE_GTE: START_DATE_GTE_PARAM, + START_DATE_LTE: START_DATE_LTE_PARAM, +}: SearchParamsKeysType = SearchParamsKeys; + const getColumns = (translate: (key: string) => string): Array> => [ { accessorKey: "date_from", @@ -110,17 +127,44 @@ export const Backfills = () => { const { pagination } = tableURLState; const { dagId = "" } = useParams(); + const [searchParams] = useSearchParams(); + + const startDateGte = searchParams.get(START_DATE_GTE_PARAM); + const startDateLte = searchParams.get(START_DATE_LTE_PARAM); + const endDateGte = searchParams.get(END_DATE_GTE_PARAM); + const endDateLte = searchParams.get(END_DATE_LTE_PARAM); + const createdAtGte = searchParams.get(CREATED_AT_GTE_PARAM); + const createdAtLte = searchParams.get(CREATED_AT_LTE_PARAM); + const completedAtGte = searchParams.get(COMPLETED_AT_GTE_PARAM); + const completedAtLte = searchParams.get(COMPLETED_AT_LTE_PARAM); + const maxActiveRunsGte = searchParams.get(MAX_ACTIVE_RUNS_GTE_PARAM); + const maxActiveRunsLte = searchParams.get(MAX_ACTIVE_RUNS_LTE_PARAM); + const reprocessBehavior = searchParams.get(REPROCESS_BEHAVIOR_PARAM); const { data, error, isFetching, isLoading } = useBackfillServiceListBackfillsUi({ + completedAtGte: completedAtGte ?? undefined, + completedAtLte: completedAtLte ?? undefined, + createdAtGte: createdAtGte ?? undefined, + createdAtLte: createdAtLte ?? undefined, dagId, + fromDateGte: startDateGte ?? undefined, + fromDateLte: startDateLte ?? undefined, limit: pagination.pageSize, + maxActiveRunsGte: + maxActiveRunsGte !== null && maxActiveRunsGte !== "" ? Number(maxActiveRunsGte) : undefined, + maxActiveRunsLte: + maxActiveRunsLte !== null && maxActiveRunsLte !== "" ? Number(maxActiveRunsLte) : undefined, offset: pagination.pageIndex * pagination.pageSize, + reprocessBehavior: reprocessBehavior ?? undefined, + toDateGte: endDateGte ?? undefined, + toDateLte: endDateLte ?? undefined, }); const columns = getColumns(translate); return ( + {translate("backfill", { count: data ? data.total_entries : 0 })} diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/BackfillsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/BackfillsFilters.tsx new file mode 100644 index 0000000000000..af2d1f31d2aaf --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/BackfillsFilters.tsx @@ -0,0 +1,47 @@ +/*! + * 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 { VStack } from "@chakra-ui/react"; + +import { FilterBar } from "src/components/FilterBar"; +import { SearchParamsKeys } from "src/constants/searchParams"; +import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils"; + +export const BackfillsFilters = () => { + const searchParamKeys: Array = [ + SearchParamsKeys.START_DATE_RANGE, + SearchParamsKeys.END_DATE_RANGE, + SearchParamsKeys.CREATED_AT_RANGE, + SearchParamsKeys.COMPLETED_AT_RANGE, + SearchParamsKeys.MAX_ACTIVE_RUNS_GTE, + SearchParamsKeys.MAX_ACTIVE_RUNS_LTE, + SearchParamsKeys.REPROCESS_BEHAVIOR, + ]; + + const { filterConfigs, handleFiltersChange, initialValues } = useFiltersHandler(searchParamKeys); + + return ( + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts index 54f71369370b6..1ff745fa87b49 100644 --- a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts +++ b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts @@ -59,6 +59,7 @@ export type FilterableSearchParamsKeys = | SearchParamsKeys.ASSET_EVENT_DATE_RANGE | SearchParamsKeys.BODY_SEARCH | SearchParamsKeys.BUNDLE_VERSION + | SearchParamsKeys.COMPLETED_AT_RANGE | SearchParamsKeys.CONF_CONTAINS | SearchParamsKeys.CONSUMING_ASSET_PATTERN | SearchParamsKeys.CREATED_AT_RANGE @@ -79,6 +80,8 @@ export type FilterableSearchParamsKeys = | SearchParamsKeys.KEY_PATTERN | SearchParamsKeys.LOGICAL_DATE_RANGE | SearchParamsKeys.MAP_INDEX + | SearchParamsKeys.MAX_ACTIVE_RUNS_GTE + | SearchParamsKeys.MAX_ACTIVE_RUNS_LTE | SearchParamsKeys.MISSED | SearchParamsKeys.NAME_PATTERN | SearchParamsKeys.OPERATOR_NAME_PATTERN @@ -86,6 +89,7 @@ export type FilterableSearchParamsKeys = | SearchParamsKeys.POOL_NAME_PATTERN | SearchParamsKeys.QUEUE_NAME_PATTERN | SearchParamsKeys.RENDERED_MAP_INDEX + | SearchParamsKeys.REPROCESS_BEHAVIOR | SearchParamsKeys.RESPONDED_BY_USER_NAME | SearchParamsKeys.RESPONSE_RECEIVED | SearchParamsKeys.RUN_AFTER_RANGE diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py index ebb23efff7d4a..36e2d94a757a5 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_backfills.py @@ -16,13 +16,14 @@ # under the License. from __future__ import annotations +from datetime import timedelta from unittest import mock import pytest from airflow._shared.timezones import timezone from airflow.models import DagModel -from airflow.models.backfill import Backfill +from airflow.models.backfill import Backfill, ReprocessBehavior from airflow.utils.session import provide_session from tests_common.test_utils.asserts import assert_queries_count @@ -160,6 +161,88 @@ def test_should_response_200( "total_entries": total_entries, } + def test_list_backfill_by_start_end_date(self, test_client, session, testing_dag_bundle): + (dag,) = self._create_dag_models(count=1) + from_date = timezone.utcnow() + b1 = Backfill(dag_id=dag.dag_id, from_date=from_date, to_date=from_date) + other_date = from_date + timedelta(days=1) + b2 = Backfill(dag_id=dag.dag_id, from_date=other_date, to_date=other_date) + session.add_all([b1, b2]) + session.commit() + + response = test_client.get( + "/backfills", + params={ + "from_date_gte": from_datetime_to_zulu(other_date), + "to_date_lte": from_datetime_to_zulu(other_date), + }, + ) + assert response.status_code == 200 + assert [each["id"] for each in response.json()["backfills"]] == [b2.id] + + def test_list_backfill_by_completed_at(self, test_client, session, testing_dag_bundle): + (dag,) = self._create_dag_models(count=1) + from_date = timezone.utcnow() + b1 = Backfill(dag_id=dag.dag_id, from_date=from_date, to_date=from_date) + other_completed_at = timezone.utcnow() + timedelta(days=1) + b2 = Backfill( + dag_id=dag.dag_id, from_date=from_date, to_date=from_date, completed_at=other_completed_at + ) + session.add_all([b1, b2]) + session.commit() + + # b1's completed_at is NULL, so it never matches a bound + response = test_client.get( + "/backfills", params={"completed_at_gte": from_datetime_to_zulu(other_completed_at)} + ) + assert response.status_code == 200 + assert [each["id"] for each in response.json()["backfills"]] == [b2.id] + + def test_list_backfill_by_created_at(self, test_client, session, testing_dag_bundle): + (dag,) = self._create_dag_models(count=1) + from_date = timezone.utcnow() + b1 = Backfill(dag_id=dag.dag_id, from_date=from_date, to_date=from_date) + other_created_at = timezone.utcnow() - timedelta(days=1) + b2 = Backfill(dag_id=dag.dag_id, from_date=from_date, to_date=from_date, created_at=other_created_at) + session.add_all([b1, b2]) + session.commit() + + # b1's created_at defaults to ~now, b2's is 1 day in the past + response = test_client.get( + "/backfills", params={"created_at_lte": from_datetime_to_zulu(other_created_at)} + ) + assert response.status_code == 200 + assert [each["id"] for each in response.json()["backfills"]] == [b2.id] + + def test_list_backfill_by_reprocess_behavior(self, test_client, session, testing_dag_bundle): + (dag,) = self._create_dag_models(count=1) + from_date = timezone.utcnow() + b1 = Backfill(dag_id=dag.dag_id, from_date=from_date, to_date=from_date) + b2 = Backfill( + dag_id=dag.dag_id, + from_date=from_date, + to_date=from_date, + reprocess_behavior=ReprocessBehavior.COMPLETED, + ) + session.add_all([b1, b2]) + session.commit() + + response = test_client.get("/backfills", params={"reprocess_behavior": "completed"}) + assert response.status_code == 200 + assert [each["id"] for each in response.json()["backfills"]] == [b2.id] + + def test_list_backfill_by_max_active_runs(self, test_client, session, testing_dag_bundle): + (dag,) = self._create_dag_models(count=1) + from_date = timezone.utcnow() + b1 = Backfill(dag_id=dag.dag_id, from_date=from_date, to_date=from_date) # default max_active_runs=10 + b2 = Backfill(dag_id=dag.dag_id, from_date=from_date, to_date=from_date, max_active_runs=3) + session.add_all([b1, b2]) + session.commit() + + response = test_client.get("/backfills", params={"max_active_runs_lte": 5}) + assert response.status_code == 200 + assert [each["id"] for each in response.json()["backfills"]] == [b2.id] + def test_should_response_401(self, unauthenticated_test_client): response = unauthenticated_test_client.get("/backfills", params={}) assert response.status_code == 401