diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index d4da2809a4d13..cc7ae250cba66 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -53,6 +53,7 @@ from airflow.models.dag_favorite import DagFavorite from airflow.models.dag_version import DagVersion from airflow.models.dagrun import DagRun +from airflow.models.errors import ParseImportError from airflow.models.hitl import HITLDetail from airflow.models.pool import Pool from airflow.models.taskinstance import TaskInstance @@ -1058,3 +1059,8 @@ def _optional_boolean(value: bool | None) -> bool | None: ) ), ] + +# Parse Import Errors +QueryParseImportErrorFilenamePatternSearch = Annotated[ + _SearchParam, Depends(search_param_factory(ParseImportError.filename, "filename_pattern")) +] diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index 95fa0fcda547f..80c8d485df994 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -4052,6 +4052,18 @@ paths: default: - id title: Order By + - name: filename_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ + \ Regular expressions are **not** supported." + title: Filename Pattern + description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ + \ Regular expressions are **not** supported." responses: '200': description: Successful Response diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py index 8061adba8a964..e9d6eaddb0216 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/import_error.py @@ -36,6 +36,7 @@ from airflow.api_fastapi.common.parameters import ( QueryLimit, QueryOffset, + QueryParseImportErrorFilenamePatternSearch, SortParam, ) from airflow.api_fastapi.common.router import AirflowRouter @@ -126,12 +127,14 @@ def get_import_errors( ).dynamic_depends() ), ], + filename_pattern: QueryParseImportErrorFilenamePatternSearch, session: SessionDep, user: GetUserDep, ) -> ImportErrorCollectionResponse: """Get all import errors.""" import_errors_select, total_entries = paginated_select( statement=select(ParseImportError), + filters=[filename_pattern], order_by=order_by, offset=offset, limit=limit, @@ -174,6 +177,7 @@ def get_import_errors( # Paginate the import errors query import_errors_select, total_entries = paginated_select( statement=import_errors_stmt, + filters=[filename_pattern], order_by=order_by, offset=offset, limit=limit, 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 874ff6b4264cf..dd46ddb015add 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -596,11 +596,12 @@ export const UseImportErrorServiceGetImportErrorKeyFn = ({ importErrorId }: { export type ImportErrorServiceGetImportErrorsDefaultResponse = Awaited>; export type ImportErrorServiceGetImportErrorsQueryResult = UseQueryResult; export const useImportErrorServiceGetImportErrorsKey = "ImportErrorServiceGetImportErrors"; -export const UseImportErrorServiceGetImportErrorsKeyFn = ({ limit, offset, orderBy }: { +export const UseImportErrorServiceGetImportErrorsKeyFn = ({ filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: Array) => [useImportErrorServiceGetImportErrorsKey, ...(queryKey ?? [{ limit, offset, orderBy }])]; +} = {}, queryKey?: Array) => [useImportErrorServiceGetImportErrorsKey, ...(queryKey ?? [{ filenamePattern, limit, offset, orderBy }])]; export type JobServiceGetJobsDefaultResponse = Awaited>; export type JobServiceGetJobsQueryResult = UseQueryResult; export const useJobServiceGetJobsKey = "JobServiceGetJobs"; 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 54cf0dca70953..ed45244bdbf1e 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -1134,14 +1134,16 @@ export const ensureUseImportErrorServiceGetImportErrorData = (queryClient: Query * @param data.limit * @param data.offset * @param data.orderBy +* @param data.filenamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns ImportErrorCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseImportErrorServiceGetImportErrorsData = (queryClient: QueryClient, { limit, offset, orderBy }: { +export const ensureUseImportErrorServiceGetImportErrorsData = (queryClient: QueryClient, { filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ limit, offset, orderBy }), queryFn: () => ImportErrorService.getImportErrors({ limit, offset, orderBy }) }); +} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, limit, offset, orderBy }), queryFn: () => ImportErrorService.getImportErrors({ filenamePattern, limit, offset, orderBy }) }); /** * Get Jobs * Get all jobs. 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 66a08f901b3ef..595733362cf70 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -1134,14 +1134,16 @@ export const prefetchUseImportErrorServiceGetImportError = (queryClient: QueryCl * @param data.limit * @param data.offset * @param data.orderBy +* @param data.filenamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns ImportErrorCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseImportErrorServiceGetImportErrors = (queryClient: QueryClient, { limit, offset, orderBy }: { +export const prefetchUseImportErrorServiceGetImportErrors = (queryClient: QueryClient, { filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ limit, offset, orderBy }), queryFn: () => ImportErrorService.getImportErrors({ limit, offset, orderBy }) }); +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, limit, offset, orderBy }), queryFn: () => ImportErrorService.getImportErrors({ filenamePattern, limit, offset, orderBy }) }); /** * Get Jobs * Get all jobs. 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 9c04bac85d4da..b664457a66eaf 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -1134,14 +1134,16 @@ export const useImportErrorServiceGetImportError = = unknown[]>({ limit, offset, orderBy }: { +export const useImportErrorServiceGetImportErrors = = unknown[]>({ filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ limit, offset, orderBy }, queryKey), queryFn: () => ImportErrorService.getImportErrors({ limit, offset, orderBy }) as TData, ...options }); +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, limit, offset, orderBy }, queryKey), queryFn: () => ImportErrorService.getImportErrors({ filenamePattern, limit, offset, orderBy }) as TData, ...options }); /** * Get Jobs * Get all jobs. 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 ba7f0fc93c985..295371b0385aa 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -1134,14 +1134,16 @@ export const useImportErrorServiceGetImportErrorSuspense = = unknown[]>({ limit, offset, orderBy }: { +export const useImportErrorServiceGetImportErrorsSuspense = = unknown[]>({ filenamePattern, limit, offset, orderBy }: { + filenamePattern?: string; limit?: number; offset?: number; orderBy?: string[]; -} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ limit, offset, orderBy }, queryKey), queryFn: () => ImportErrorService.getImportErrors({ limit, offset, orderBy }) as TData, ...options }); +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, limit, offset, orderBy }, queryKey), queryFn: () => ImportErrorService.getImportErrors({ filenamePattern, limit, offset, orderBy }) as TData, ...options }); /** * Get Jobs * Get all jobs. 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 8b4cb8c21e4a9..2b161775d455f 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 @@ -2873,6 +2873,7 @@ export class ImportErrorService { * @param data.limit * @param data.offset * @param data.orderBy + * @param data.filenamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns ImportErrorCollectionResponse Successful Response * @throws ApiError */ @@ -2883,7 +2884,8 @@ export class ImportErrorService { query: { limit: data.limit, offset: data.offset, - order_by: data.orderBy + order_by: data.orderBy, + filename_pattern: data.filenamePattern }, errors: { 401: 'Unauthorized', 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 256812659a84e..96911409ea21e 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 @@ -2895,6 +2895,10 @@ export type GetImportErrorData = { export type GetImportErrorResponse = ImportErrorResponse; export type GetImportErrorsData = { + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + filenamePattern?: string | null; limit?: number; offset?: number; orderBy?: Array<(string)>; diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx index 2966fee23cd2c..e6fc9d1f3e267 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx @@ -35,7 +35,6 @@ export const DAGImportErrors = ({ iconOnly = false }: { readonly iconOnly?: bool const { data, error, isLoading } = useImportErrorServiceGetImportErrors(); const importErrorsCount = data?.total_entries ?? 0; - const importErrors = data?.import_errors ?? []; if (isLoading) { return ; @@ -70,7 +69,7 @@ export const DAGImportErrors = ({ iconOnly = false }: { readonly iconOnly?: bool onClick={onOpen} /> )} - + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx index f52d51086e6c8..e38d06a26f4cd 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx @@ -17,34 +17,35 @@ * under the License. */ import { Heading, Text, HStack } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { LuFileWarning } from "react-icons/lu"; import { PiFilePy } from "react-icons/pi"; -import type { ImportErrorResponse } from "openapi/requests/types.gen"; +import { useImportErrorServiceGetImportErrors } from "openapi/queries"; import { SearchBar } from "src/components/SearchBar"; import Time from "src/components/Time"; import { Accordion, Dialog } from "src/components/ui"; import { Pagination } from "src/components/ui/Pagination"; type ImportDAGErrorModalProps = { - importErrors: Array; onClose: () => void; open: boolean; }; const PAGE_LIMIT = 15; -export const DAGImportErrorsModal: React.FC = ({ importErrors, onClose, open }) => { +export const DAGImportErrorsModal: React.FC = ({ onClose, open }) => { const [page, setPage] = useState(1); const [searchQuery, setSearchQuery] = useState(""); - const [filteredErrors, setFilteredErrors] = useState(importErrors); - const { t: translate } = useTranslation(["dashboard", "components"]); - const startRange = (page - 1) * PAGE_LIMIT; - const endRange = startRange + PAGE_LIMIT; - const visibleItems = filteredErrors.slice(startRange, endRange); + const { data } = useImportErrorServiceGetImportErrors({ + filenamePattern: searchQuery || undefined, + limit: PAGE_LIMIT, + offset: PAGE_LIMIT * (page - 1), + }); + + const { t: translate } = useTranslation(["dashboard", "components"]); const onOpenChange = () => { setSearchQuery(""); @@ -52,13 +53,10 @@ export const DAGImportErrorsModal: React.FC = ({ impor onClose(); }; - useEffect(() => { - const query = searchQuery.toLowerCase(); - const filtered = importErrors.filter((error) => error.filename.toLowerCase().includes(query)); - - setFilteredErrors(filtered); + const handleSearchChange = (value: string) => { + setSearchQuery(value); setPage(1); - }, [searchQuery, importErrors]); + }; return ( @@ -66,13 +64,13 @@ export const DAGImportErrorsModal: React.FC = ({ impor - {translate("importErrors.dagImportError", { count: importErrors.length })} + {translate("importErrors.dagImportError", { count: data?.total_entries ?? 0 })} @@ -81,7 +79,7 @@ export const DAGImportErrorsModal: React.FC = ({ impor - {visibleItems.map((importError) => ( + {data?.import_errors.map((importError) => ( @@ -108,7 +106,7 @@ export const DAGImportErrorsModal: React.FC = ({ impor setPage(event.page)} p={4} page={page}