This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new e51289d6892 Add import error to deactivated dag (#65687)
e51289d6892 is described below

commit e51289d689236b125112ed1b195854714ab8418a
Author: Brent Bovenzi <[email protected]>
AuthorDate: Wed Jun 10 14:15:28 2026 -0400

    Add import error to deactivated dag (#65687)
    
    * # This is a combination of 2 commits.
    # This is the 1st commit message:
    
    Add import error to dag page when deactivated
    
    # This is the commit message #2:
    
    Move to modal
    
    * Add import error to dag page when deactivated
    
    * Change badge to a banner
    
    * Fix test
    
    * Cleanup banner code
    
    * Add bundleName param
    
    * Fix CI
---
 .../src/airflow/api_fastapi/common/parameters.py   |  22 +++
 .../core_api/openapi/v2-rest-api-generated.yaml    |  24 ++++
 .../core_api/routes/public/import_error.py         |   7 +-
 .../src/airflow/ui/openapi-gen/queries/common.ts   |   6 +-
 .../ui/openapi-gen/queries/ensureQueryData.ts      |   8 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |   8 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |   8 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |   8 +-
 .../ui/openapi-gen/requests/services.gen.ts        |   6 +-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |   8 ++
 .../ui/src/components/DagDeactivatedBadge.tsx      |  26 ----
 .../src/components/DagDeactivatedBanner.test.tsx   | 147 +++++++++++++++++++++
 .../ui/src/components/DagDeactivatedBanner.tsx     |  70 ++++++++++
 .../src/airflow/ui/src/components/HeaderCard.tsx   |  47 ++++---
 .../ui/src/layouts/Details/DagBreadcrumb.tsx       |   5 +-
 .../ui/src/layouts/Details/DetailsLayout.tsx       |   4 +-
 .../ui/src/pages/Dag/DagImportErrorModal.tsx       |  72 ++++++++++
 .../src/airflow/ui/src/pages/Dag/Header.test.tsx   |   1 -
 .../src/airflow/ui/src/pages/Dag/Header.tsx        |  11 +-
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |   4 +-
 .../{DAGImportErrors.tsx => DagImportErrors.tsx}   |   7 +-
 ...ortErrorsModal.tsx => DagImportErrorsModal.tsx} |  38 +++---
 .../airflow/ui/src/pages/Dashboard/Stats/Stats.tsx |   4 +-
 .../core_api/routes/public/test_import_error.py    | 105 +++++++++++++++
 24 files changed, 553 insertions(+), 93 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py 
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index 685736ed2f4..d18f8dc7ff5 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -1756,3 +1756,25 @@ QueryParseImportErrorFilenamePrefixPatternSearch = 
Annotated[
     _PrefixSearchParam,
     Depends(prefix_search_param_factory(ParseImportError.filename, 
"filename_prefix_pattern")),
 ]
+QueryParseImportErrorFilenameFilter = Annotated[
+    FilterParam,
+    Depends(
+        filter_param_factory(
+            ParseImportError.filename,
+            str | None,
+            filter_name="filename",
+            description="Exact filename match. Returns only the import error 
for this specific file path.",
+        )
+    ),
+]
+QueryParseImportErrorBundleNameFilter = Annotated[
+    FilterParam,
+    Depends(
+        filter_param_factory(
+            ParseImportError.bundle_name,
+            str | None,
+            filter_name="bundle_name",
+            description="Exact bundle name match. Returns only import errors 
from this specific bundle.",
+        )
+    ),
+]
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 87fc2a858b9..96f5bdec1ff 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
@@ -5021,6 +5021,30 @@ paths:
           \ stays index-compatible under locale-aware collations \u2014 e.g. 
`test_`\
           \ effectively matches items starting with `test`, and `s3://` 
matches items\
           \ starting with `s3`."
+      - name: filename
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+          - type: 'null'
+          description: Exact filename match. Returns only the import error for 
this
+            specific file path.
+          title: Filename
+        description: Exact filename match. Returns only the import error for 
this
+          specific file path.
+      - name: bundle_name
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+          - type: 'null'
+          description: Exact bundle name match. Returns only import errors 
from this
+            specific bundle.
+          title: Bundle Name
+        description: Exact bundle name match. Returns only import errors from 
this
+          specific bundle.
       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 2fcc94436b2..63d97a9b7b9 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
@@ -37,6 +37,8 @@ from airflow.api_fastapi.common.db.common import (
 from airflow.api_fastapi.common.parameters import (
     QueryLimit,
     QueryOffset,
+    QueryParseImportErrorBundleNameFilter,
+    QueryParseImportErrorFilenameFilter,
     QueryParseImportErrorFilenamePatternSearch,
     QueryParseImportErrorFilenamePrefixPatternSearch,
     SortParam,
@@ -145,6 +147,8 @@ def get_import_errors(
     ],
     filename_pattern: QueryParseImportErrorFilenamePatternSearch,
     filename_prefix_pattern: QueryParseImportErrorFilenamePrefixPatternSearch,
+    filename: QueryParseImportErrorFilenameFilter,
+    bundle_name: QueryParseImportErrorBundleNameFilter,
     session: SessionDep,
     user: GetUserDep,
 ) -> ImportErrorCollectionResponse:
@@ -211,7 +215,7 @@ def get_import_errors(
 
     filtered_import_errors_stmt = apply_filters_to_select(
         statement=import_errors_stmt,
-        filters=[filename_pattern, filename_prefix_pattern],
+        filters=[filename_pattern, filename_prefix_pattern, filename, 
bundle_name],
     )
     import_error_ids_stmt = (
         filtered_import_errors_stmt.with_only_columns(
@@ -230,6 +234,7 @@ def get_import_errors(
     # import error objects, not to the joined Dag rows.
     paginated_import_error_ids_select, _ = paginated_select(
         statement=import_error_ids_stmt,
+        filters=[],
         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 0933a3fea4d..82c63b58a69 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -671,13 +671,15 @@ export const UseImportErrorServiceGetImportErrorKeyFn = 
({ importErrorId }: {
 export type ImportErrorServiceGetImportErrorsDefaultResponse = 
Awaited<ReturnType<typeof ImportErrorService.getImportErrors>>;
 export type ImportErrorServiceGetImportErrorsQueryResult<TData = 
ImportErrorServiceGetImportErrorsDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useImportErrorServiceGetImportErrorsKey = 
"ImportErrorServiceGetImportErrors";
-export const UseImportErrorServiceGetImportErrorsKeyFn = ({ filenamePattern, 
filenamePrefixPattern, limit, offset, orderBy }: {
+export const UseImportErrorServiceGetImportErrorsKeyFn = ({ bundleName, 
filename, filenamePattern, filenamePrefixPattern, limit, offset, orderBy }: {
+  bundleName?: string;
+  filename?: string;
   filenamePattern?: string;
   filenamePrefixPattern?: string;
   limit?: number;
   offset?: number;
   orderBy?: string[];
-} = {}, queryKey?: Array<unknown>) => 
[useImportErrorServiceGetImportErrorsKey, ...(queryKey ?? [{ filenamePattern, 
filenamePrefixPattern, limit, offset, orderBy }])];
+} = {}, queryKey?: Array<unknown>) => 
[useImportErrorServiceGetImportErrorsKey, ...(queryKey ?? [{ bundleName, 
filename, filenamePattern, filenamePrefixPattern, limit, offset, orderBy }])];
 export type JobServiceGetJobsDefaultResponse = Awaited<ReturnType<typeof 
JobService.getJobs>>;
 export type JobServiceGetJobsQueryResult<TData = 
JobServiceGetJobsDefaultResponse, TError = unknown> = UseQueryResult<TData, 
TError>;
 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 30096ac546b..48f61be34d1 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1372,16 +1372,20 @@ export const 
ensureUseImportErrorServiceGetImportErrorData = (queryClient: Query
 *
 * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``filename_prefix_pattern`` parameter when possible.
 * @param data.filenamePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting with [...]
+* @param data.filename Exact filename match. Returns only the import error for 
this specific file path.
+* @param data.bundleName Exact bundle name match. Returns only import errors 
from this specific bundle.
 * @returns ImportErrorCollectionResponse Successful Response
 * @throws ApiError
 */
-export const ensureUseImportErrorServiceGetImportErrorsData = (queryClient: 
QueryClient, { filenamePattern, filenamePrefixPattern, limit, offset, orderBy 
}: {
+export const ensureUseImportErrorServiceGetImportErrorsData = (queryClient: 
QueryClient, { bundleName, filename, filenamePattern, filenamePrefixPattern, 
limit, offset, orderBy }: {
+  bundleName?: string;
+  filename?: string;
   filenamePattern?: string;
   filenamePrefixPattern?: string;
   limit?: number;
   offset?: number;
   orderBy?: string[];
-} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, 
filenamePrefixPattern, limit, offset, orderBy }), queryFn: () => 
ImportErrorService.getImportErrors({ filenamePattern, filenamePrefixPattern, 
limit, offset, orderBy }) });
+} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseImportErrorServiceGetImportErrorsKeyFn({ bundleName, filename, 
filenamePattern, filenamePrefixPattern, limit, offset, orderBy }), queryFn: () 
=> ImportErrorService.getImportErrors({ bundleName, filename, filenamePattern, 
filenamePrefixPattern, 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 8f78029a31c..d4fb4ae8896 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1372,16 +1372,20 @@ export const 
prefetchUseImportErrorServiceGetImportError = (queryClient: QueryCl
 *
 * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``filename_prefix_pattern`` parameter when possible.
 * @param data.filenamePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting with [...]
+* @param data.filename Exact filename match. Returns only the import error for 
this specific file path.
+* @param data.bundleName Exact bundle name match. Returns only import errors 
from this specific bundle.
 * @returns ImportErrorCollectionResponse Successful Response
 * @throws ApiError
 */
-export const prefetchUseImportErrorServiceGetImportErrors = (queryClient: 
QueryClient, { filenamePattern, filenamePrefixPattern, limit, offset, orderBy 
}: {
+export const prefetchUseImportErrorServiceGetImportErrors = (queryClient: 
QueryClient, { bundleName, filename, filenamePattern, filenamePrefixPattern, 
limit, offset, orderBy }: {
+  bundleName?: string;
+  filename?: string;
   filenamePattern?: string;
   filenamePrefixPattern?: string;
   limit?: number;
   offset?: number;
   orderBy?: string[];
-} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, 
filenamePrefixPattern, limit, offset, orderBy }), queryFn: () => 
ImportErrorService.getImportErrors({ filenamePattern, filenamePrefixPattern, 
limit, offset, orderBy }) });
+} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseImportErrorServiceGetImportErrorsKeyFn({ bundleName, filename, 
filenamePattern, filenamePrefixPattern, limit, offset, orderBy }), queryFn: () 
=> ImportErrorService.getImportErrors({ bundleName, filename, filenamePattern, 
filenamePrefixPattern, 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 8f5c660029c..91662271aca 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -1372,16 +1372,20 @@ export const useImportErrorServiceGetImportError = 
<TData = Common.ImportErrorSe
 *
 * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``filename_prefix_pattern`` parameter when possible.
 * @param data.filenamePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting with [...]
+* @param data.filename Exact filename match. Returns only the import error for 
this specific file path.
+* @param data.bundleName Exact bundle name match. Returns only import errors 
from this specific bundle.
 * @returns ImportErrorCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useImportErrorServiceGetImportErrors = <TData = 
Common.ImportErrorServiceGetImportErrorsDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ filenamePattern, 
filenamePrefixPattern, limit, offset, orderBy }: {
+export const useImportErrorServiceGetImportErrors = <TData = 
Common.ImportErrorServiceGetImportErrorsDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ bundleName, filename, 
filenamePattern, filenamePrefixPattern, limit, offset, orderBy }: {
+  bundleName?: string;
+  filename?: string;
   filenamePattern?: string;
   filenamePrefixPattern?: string;
   limit?: number;
   offset?: number;
   orderBy?: string[];
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, 
filenamePrefixPattern, limit, offset, orderBy }, queryKey), queryFn: () => 
ImportErrorService.getImportErrors({ filenamePattern, filenamePrefixPattern, 
limit, offset, orderBy }) as TData, ...options });
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseImportErrorServiceGetImportErrorsKeyFn({ bundleName, filename, 
filenamePattern, filenamePrefixPattern, limit, offset, orderBy }, queryKey), 
queryFn: () => ImportErrorService.getImportErrors({ bundleName, filename, 
filenamePattern, filenamePrefixPattern, 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 3df940743cc..b810526782c 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -1372,16 +1372,20 @@ export const 
useImportErrorServiceGetImportErrorSuspense = <TData = Common.Impor
 *
 * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``filename_prefix_pattern`` parameter when possible.
 * @param data.filenamePrefixPattern Prefix match — returns items whose value 
starts with the given string (case-sensitive, index-friendly). Use the pipe `|` 
operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard 
characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting with [...]
+* @param data.filename Exact filename match. Returns only the import error for 
this specific file path.
+* @param data.bundleName Exact bundle name match. Returns only import errors 
from this specific bundle.
 * @returns ImportErrorCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useImportErrorServiceGetImportErrorsSuspense = <TData = 
Common.ImportErrorServiceGetImportErrorsDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ filenamePattern, 
filenamePrefixPattern, limit, offset, orderBy }: {
+export const useImportErrorServiceGetImportErrorsSuspense = <TData = 
Common.ImportErrorServiceGetImportErrorsDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ bundleName, filename, 
filenamePattern, filenamePrefixPattern, limit, offset, orderBy }: {
+  bundleName?: string;
+  filename?: string;
   filenamePattern?: string;
   filenamePrefixPattern?: string;
   limit?: number;
   offset?: number;
   orderBy?: string[];
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseImportErrorServiceGetImportErrorsKeyFn({ filenamePattern, 
filenamePrefixPattern, limit, offset, orderBy }, queryKey), queryFn: () => 
ImportErrorService.getImportErrors({ filenamePattern, filenamePrefixPattern, 
limit, offset, orderBy }) as TData, ...options });
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseImportErrorServiceGetImportErrorsKeyFn({ bundleName, filename, 
filenamePattern, filenamePrefixPattern, limit, offset, orderBy }, queryKey), 
queryFn: () => ImportErrorService.getImportErrors({ bundleName, filename, 
filenamePattern, filenamePrefixPattern, 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 d69a06aa60c..f8205d72084 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
@@ -3291,6 +3291,8 @@ export class ImportErrorService {
      *
      * **Performance note:** this full-match pattern is evaluated as ``ILIKE 
'%term%'`` and most of the time prevents the database from using B-tree 
indexes, which can be very slow on large tables. Prefer the equivalent 
``filename_prefix_pattern`` parameter when possible.
      * @param data.filenamePrefixPattern Prefix match — returns items whose 
value starts with the given string (case-sensitive, index-friendly). Use the 
pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. 
Wildcard characters (`%`, `_`) are treated as literal characters. Trailing 
non-alphanumeric characters in the prefix are stripped before matching so the 
range scan stays index-compatible under locale-aware collations — e.g. `test_` 
effectively matches items starting [...]
+     * @param data.filename Exact filename match. Returns only the import 
error for this specific file path.
+     * @param data.bundleName Exact bundle name match. Returns only import 
errors from this specific bundle.
      * @returns ImportErrorCollectionResponse Successful Response
      * @throws ApiError
      */
@@ -3303,7 +3305,9 @@ export class ImportErrorService {
                 offset: data.offset,
                 order_by: data.orderBy,
                 filename_pattern: data.filenamePattern,
-                filename_prefix_pattern: data.filenamePrefixPattern
+                filename_prefix_pattern: data.filenamePrefixPattern,
+                filename: data.filename,
+                bundle_name: data.bundleName
             },
             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 62260fa6113..e8317de2300 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
@@ -3948,6 +3948,14 @@ export type GetImportErrorData = {
 export type GetImportErrorResponse = ImportErrorResponse;
 
 export type GetImportErrorsData = {
+    /**
+     * Exact bundle name match. Returns only import errors from this specific 
bundle.
+     */
+    bundleName?: string | null;
+    /**
+     * Exact filename match. Returns only the import error for this specific 
file path.
+     */
+    filename?: string | null;
     /**
      * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). or 
the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). Regular expressions 
are **not** supported.
      *
diff --git a/airflow-core/src/airflow/ui/src/components/DagDeactivatedBadge.tsx 
b/airflow-core/src/airflow/ui/src/components/DagDeactivatedBadge.tsx
deleted file mode 100644
index ad915b256be..00000000000
--- a/airflow-core/src/airflow/ui/src/components/DagDeactivatedBadge.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-/*!
- * 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 { Badge } from "@chakra-ui/react";
-import { useTranslation } from "react-i18next";
-
-export const DagDeactivatedBadge = () => {
-  const { t: translate } = useTranslation("dag");
-
-  return <Badge 
colorPalette="orange">{translate("header.status.deactivated")}</Badge>;
-};
diff --git 
a/airflow-core/src/airflow/ui/src/components/DagDeactivatedBanner.test.tsx 
b/airflow-core/src/airflow/ui/src/components/DagDeactivatedBanner.test.tsx
new file mode 100644
index 00000000000..91530a0d476
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/DagDeactivatedBanner.test.tsx
@@ -0,0 +1,147 @@
+/*!
+ * 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 { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import i18n from "src/i18n/config";
+import { Wrapper } from "src/utils/Wrapper";
+
+import dagLocale from "../../public/i18n/locales/en/dag.json";
+import dashboardLocale from "../../public/i18n/locales/en/dashboard.json";
+import { DagDeactivatedBanner } from "./DagDeactivatedBanner";
+
+const { mockUseDagServiceGetDag, mockUseImportErrorServiceGetImportErrors } = 
vi.hoisted(() => ({
+  mockUseDagServiceGetDag: vi.fn(),
+  mockUseImportErrorServiceGetImportErrors: vi.fn(),
+}));
+
+vi.mock("react-router-dom", async () => {
+  const actual = await vi.importActual("react-router-dom");
+
+  return {
+    ...actual,
+    useParams: () => ({ dagId: "stale_dag" }),
+  };
+});
+
+vi.mock("openapi/queries", async (importOriginal) => {
+  // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- 
`import()` type is the standard pattern for typing `importOriginal` in Vitest 
mocks.
+  const actual = await importOriginal<typeof import("openapi/queries")>();
+
+  return {
+    ...actual,
+    useDagServiceGetDag: mockUseDagServiceGetDag,
+    useImportErrorServiceGetImportErrors: 
mockUseImportErrorServiceGetImportErrors,
+  };
+});
+
+const staleDagQuery = {
+  data: { is_stale: true, relative_fileloc: "stale_dag.py" },
+  isLoading: false,
+};
+
+const notStaleDagQuery = {
+  data: { is_stale: false, relative_fileloc: "stale_dag.py" },
+  isLoading: false,
+};
+
+const emptyImportErrorsQuery = {
+  data: { import_errors: [], total_entries: 0 },
+  error: null,
+  isError: false,
+  isLoading: false,
+  isPending: false,
+};
+
+describe("DagDeactivatedBanner", () => {
+  beforeEach(() => {
+    i18n.addResourceBundle("en", "dag", dagLocale, true, true);
+    i18n.addResourceBundle("en", "dashboard", dashboardLocale, true, true);
+    mockUseDagServiceGetDag.mockReturnValue(staleDagQuery);
+    
mockUseImportErrorServiceGetImportErrors.mockReturnValue(emptyImportErrorsQuery);
+  });
+
+  it("does not render when the dag is not stale", () => {
+    mockUseDagServiceGetDag.mockReturnValue(notStaleDagQuery);
+
+    render(
+      <Wrapper>
+        <DagDeactivatedBanner />
+      </Wrapper>,
+    );
+
+    expect(screen.queryByText(i18n.t("header.status.deactivated", { ns: "dag" 
}))).not.toBeInTheDocument();
+  });
+
+  it("shows a deactivated banner when stale with no import error", () => {
+    render(
+      <Wrapper>
+        <DagDeactivatedBanner />
+      </Wrapper>,
+    );
+
+    expect(screen.getByText(i18n.t("header.status.deactivated", { ns: "dag" 
}))).toBeInTheDocument();
+    expect(
+      screen.queryByRole("button", {
+        name: i18n.t("importErrors.dagImportError", { count: 1, ns: 
"dashboard" }),
+      }),
+    ).not.toBeInTheDocument();
+  });
+
+  it("shows a deactivated banner with an import error button when the API 
returns a file-scoped error", async () => {
+    mockUseImportErrorServiceGetImportErrors.mockReturnValue({
+      ...emptyImportErrorsQuery,
+      data: {
+        import_errors: [
+          {
+            bundle_name: "dags-folder",
+            filename: "stale_dag.py",
+            import_error_id: 42,
+            stack_trace: "Traceback (most recent call last):\nSyntaxError: 
invalid syntax",
+            timestamp: "2025-02-01T12:00:00Z",
+          },
+        ],
+        total_entries: 1,
+      },
+    });
+
+    render(
+      <Wrapper>
+        <DagDeactivatedBanner />
+      </Wrapper>,
+    );
+
+    expect(screen.getByText(i18n.t("header.status.deactivated", { ns: "dag" 
}))).toBeInTheDocument();
+
+    const importErrorButton = screen.getByRole("button", {
+      name: i18n.t("importErrors.dagImportError", { count: 1, ns: "dashboard" 
}),
+    });
+
+    expect(importErrorButton).toBeInTheDocument();
+    expect(screen.queryByText(/SyntaxError: invalid 
syntax/u)).not.toBeInTheDocument();
+
+    fireEvent.click(importErrorButton);
+
+    await waitFor(() => {
+      expect(screen.getByText("stale_dag.py")).toBeInTheDocument();
+    });
+    expect(screen.getByText(/SyntaxError: invalid 
syntax/u)).toBeInTheDocument();
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/components/DagDeactivatedBanner.tsx 
b/airflow-core/src/airflow/ui/src/components/DagDeactivatedBanner.tsx
new file mode 100644
index 00000000000..5ac812a78e1
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/DagDeactivatedBanner.tsx
@@ -0,0 +1,70 @@
+/*!
+ * 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 { Button, HStack, Text, useDisclosure } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+import { LuFileWarning } from "react-icons/lu";
+import { useParams } from "react-router-dom";
+
+import { useDagServiceGetDag, useImportErrorServiceGetImportErrors } from 
"openapi/queries";
+import { DagImportErrorModal } from "src/pages/Dag/DagImportErrorModal";
+
+export const DagDeactivatedBanner = () => {
+  const { t: translate } = useTranslation(["dag", "dashboard"]);
+  const { dagId = "" } = useParams();
+  const { onClose, onOpen, open } = useDisclosure();
+
+  const { data: dag } = useDagServiceGetDag({ dagId }, undefined, { enabled: 
dagId !== "" });
+  const relativeFileloc = dag?.relative_fileloc ?? "";
+  const bundleName = dag?.bundle_name ?? undefined;
+
+  const { data } = useImportErrorServiceGetImportErrors(
+    { bundleName, filename: relativeFileloc },
+    undefined,
+    { enabled: dag?.is_stale && relativeFileloc.length > 0 },
+  );
+
+  if (dagId === "" || !dag?.is_stale) {
+    return undefined;
+  }
+
+  const importError = data?.import_errors[0];
+
+  return (
+    <HStack bg="bg.warning" color="fg.warning" justifyContent="space-between" 
px={3} py={1}>
+      <Text>{translate("header.status.deactivated")}</Text>
+      {importError ? (
+        <>
+          <Button
+            borderColor="fg.warning"
+            colorPalette="warning"
+            onClick={onOpen}
+            size="xs"
+            variant="outline"
+          >
+            <HStack gap={1}>
+              <LuFileWarning size={14} />
+              {translate("dashboard:importErrors.dagImportError", { count: 1 
})}
+            </HStack>
+          </Button>
+          <DagImportErrorModal importError={importError} onClose={onClose} 
open={open} />
+        </>
+      ) : undefined}
+    </HStack>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/components/HeaderCard.tsx 
b/airflow-core/src/airflow/ui/src/components/HeaderCard.tsx
index bc08d42b59d..62b2c86c605 100644
--- a/airflow-core/src/airflow/ui/src/components/HeaderCard.tsx
+++ b/airflow-core/src/airflow/ui/src/components/HeaderCard.tsx
@@ -24,6 +24,8 @@ import type { TaskInstanceState } from 
"openapi/requests/types.gen";
 import { Stat } from "src/components/Stat";
 import { StateBadge } from "src/components/StateBadge";
 
+import { DagDeactivatedBanner } from "./DagDeactivatedBanner";
+
 type Props = {
   readonly actions?: ReactNode;
   readonly icon: ReactNode;
@@ -37,26 +39,35 @@ export const HeaderCard = ({ actions, icon, state, stats, 
subTitle, title }: Pro
   const { t: translate } = useTranslation();
 
   return (
-    <Box borderColor="border.emphasized" borderRadius={8} borderWidth={1} 
data-testid="header-card" p={2}>
-      <Flex alignItems="center" flexWrap="wrap" justifyContent="space-between" 
mb={2}>
-        <Flex alignItems="center" flexWrap="wrap" gap={2}>
-          <Heading size="xl">{icon}</Heading>
-          <Heading size="lg">{title}</Heading>
-          <Heading size="lg">{subTitle}</Heading>
-          {state === undefined ? undefined : (
-            <StateBadge state={state}>{state ? 
translate(`common:states.${state}`) : undefined}</StateBadge>
-          )}
+    <Box
+      borderColor="border.emphasized"
+      borderRadius={8}
+      borderWidth={1}
+      data-testid="header-card"
+      overflow="hidden"
+    >
+      <DagDeactivatedBanner />
+      <Box p={2}>
+        <Flex alignItems="center" flexWrap="wrap" 
justifyContent="space-between" mb={2}>
+          <Flex alignItems="center" flexWrap="wrap" gap={2}>
+            <Heading size="xl">{icon}</Heading>
+            <Heading size="lg">{title}</Heading>
+            <Heading size="lg">{subTitle}</Heading>
+            {state === undefined ? undefined : (
+              <StateBadge state={state}>{state ? 
translate(`common:states.${state}`) : undefined}</StateBadge>
+            )}
+          </Flex>
+          <HStack gap={1}>{actions}</HStack>
         </Flex>
-        <HStack gap={1}>{actions}</HStack>
-      </Flex>
 
-      <HStack alignItems="flex-start" flexWrap="wrap" gap={5} 
justifyContent="space-between" my={2}>
-        {stats.map(({ label, value }) => (
-          <GridItem key={label}>
-            <Stat label={label}>{value}</Stat>
-          </GridItem>
-        ))}
-      </HStack>
+        <HStack alignItems="flex-start" flexWrap="wrap" gap={5} 
justifyContent="space-between" my={2}>
+          {stats.map(({ label, value }) => (
+            <GridItem key={label}>
+              <Stat label={label}>{value}</Stat>
+            </GridItem>
+          ))}
+        </HStack>
+      </Box>
     </Box>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx
index 822e8d7e663..8b2d46fa3a6 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx
@@ -27,7 +27,6 @@ import {
   useTaskServiceGetTask,
 } from "openapi/queries";
 import { BreadcrumbStats } from "src/components/BreadcrumbStats";
-import { DagDeactivatedBadge } from "src/components/DagDeactivatedBadge";
 import { StateBadge } from "src/components/StateBadge";
 import { TogglePause } from "src/components/TogglePause";
 import { isStatePending, useAutoRefresh } from "src/utils";
@@ -66,9 +65,7 @@ export const DagBreadcrumb = () => {
     [
       {
         label: dag?.dag_display_name ?? dagId,
-        labelExtra: dag?.is_stale ? (
-          <DagDeactivatedBadge />
-        ) : (
+        labelExtra: dag?.is_stale ? undefined : (
           <TogglePause
             dagDisplayName={dag?.dag_display_name}
             dagId={dagId}
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
index 5cfd5832c67..034d51e0c37 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -218,7 +218,9 @@ export const DetailsLayout = ({ children, error, isLoading, 
tabs }: Props) => {
       <GroupsProvider dagId={dagId}>
         <Box display="flex" flex={1} flexDirection="column" minH={0} minW={{ 
base: "1280px", md: "auto" }}>
           <HStack justifyContent="space-between" mb={2}>
-            <DagBreadcrumb />
+            <Flex alignItems="center" gap={1}>
+              <DagBreadcrumb />
+            </Flex>
             <Flex gap={1}>
               <SearchDagsButton />
               {dag === undefined ? undefined : (
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/DagImportErrorModal.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/DagImportErrorModal.tsx
new file mode 100644
index 00000000000..2d5a6a47595
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/DagImportErrorModal.tsx
@@ -0,0 +1,72 @@
+/*!
+ * 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 { ClipboardRoot, Heading, HStack, Text } from "@chakra-ui/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 Time from "src/components/Time";
+import { ClipboardIconButton, Dialog } from "src/components/ui";
+
+type Props = {
+  readonly importError: ImportErrorResponse;
+  readonly onClose: () => void;
+  readonly open: boolean;
+};
+
+export const DagImportErrorModal = ({ importError, onClose, open }: Props) => {
+  const { t: translate } = useTranslation(["dashboard", "components"]);
+
+  return (
+    <Dialog.Root lazyMount onOpenChange={onClose} open={open} 
scrollBehavior="inside" size="lg">
+      <Dialog.Content backdrop p={4}>
+        <Dialog.Header>
+          <HStack fontSize="xl" gap={2}>
+            <LuFileWarning />
+            <Heading>{translate("importErrors.dagImportError", { count: 1 
})}</Heading>
+          </HStack>
+        </Dialog.Header>
+        <Dialog.CloseTrigger />
+        <Dialog.Body>
+          <HStack alignItems="center" flexWrap="wrap" gap={2} mb={2}>
+            <Text fontWeight="bold">
+              {translate("components:versionDetails.bundleName")}
+              {": "}
+              {importError.bundle_name}
+            </Text>
+            <PiFilePy />
+            {importError.filename}
+            <ClipboardRoot value={importError.filename}>
+              <ClipboardIconButton variant="outline" />
+            </ClipboardRoot>
+          </HStack>
+          <Text color="fg.muted" fontSize="sm" mb={2}>
+            {translate("importErrors.timestamp")}
+            {": "}
+            <Time datetime={importError.timestamp} />
+          </Text>
+          <Text color="fg.error" fontSize="sm" whiteSpace="pre-wrap">
+            <code>{importError.stack_trace}</code>
+          </Text>
+        </Dialog.Body>
+      </Dialog.Content>
+    </Dialog.Root>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
index d17683892eb..4be93388cca 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
@@ -57,7 +57,6 @@ describe("Header", () => {
       </Wrapper>,
     );
 
-    
expect(screen.getByText(i18n.t("dag:header.status.deactivated"))).toBeInTheDocument();
     
expect(screen.queryByText(i18n.t("dag:dagDetails.nextRun"))).not.toBeInTheDocument();
     expect(screen.queryByRole("button", { name: "Reparse Dag" 
})).not.toBeInTheDocument();
   });
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
index c18671b3c6b..9e3d915b079 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
@@ -25,7 +25,6 @@ import { DagIcon } from "src/assets/DagIcon";
 import { DeleteDagButton } from "src/components/DagActions/DeleteDagButton";
 import { FavoriteDagButton } from 
"src/components/DagActions/FavoriteDagButton";
 import { ParseDagButton } from "src/components/DagActions/ParseDagButton";
-import { DagDeactivatedBadge } from "src/components/DagDeactivatedBadge";
 import DagRunInfo from "src/components/DagRunInfo";
 import { DagVersion } from "src/components/DagVersion";
 import DisplayMarkdownButton from "src/components/DisplayMarkdownButton";
@@ -149,13 +148,9 @@ export const Header = ({
       icon={<DagIcon />}
       stats={stats}
       subTitle={
-        isStale ? (
-          <DagDeactivatedBadge />
-        ) : (
-          dag !== undefined && (
-            <TogglePause dagDisplayName={dag.dag_display_name} 
dagId={dag.dag_id} isPaused={dag.is_paused} />
-          )
-        )
+        dag !== undefined && !isStale ? (
+          <TogglePause dagDisplayName={dag.dag_display_name} 
dagId={dag.dag_id} isPaused={dag.is_paused} />
+        ) : undefined
       }
       title={dag?.dag_display_name ?? dagId}
     />
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 29af6d24f28..0e26e363c66 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -42,7 +42,7 @@ import { DagsLayout } from "src/layouts/DagsLayout";
 import { useConfig } from "src/queries/useConfig";
 import { useDags } from "src/queries/useDags";
 
-import { DAGImportErrors } from "../Dashboard/Stats/DAGImportErrors";
+import { DagImportErrors } from "../Dashboard/Stats/DagImportErrors";
 import { DagCard } from "./DagCard";
 import { DagTags } from "./DagTags";
 import { DagsFilters } from "./DagsFilters";
@@ -297,7 +297,7 @@ export const DagsList = () => {
             <Heading py={3} size="md">
               {`${totalEntries} ${translate("dag", { count: totalEntries })}`}
             </Heading>
-            <DAGImportErrors iconOnly />
+            <DagImportErrors iconOnly />
           </HStack>
           {display === "card" ? (
             <SortSelect handleSortChange={handleSortChange} orderBy={orderBy} 
/>
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
similarity index 89%
rename from 
airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx
rename to 
airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagImportErrors.tsx
index 9819987aa63..df2bf5755ae 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
@@ -25,9 +25,9 @@ import { ErrorAlert } from "src/components/ErrorAlert";
 import { StateBadge } from "src/components/StateBadge";
 import { StatsCard } from "src/components/StatsCard";
 
-import { DAGImportErrorsModal } from "./DAGImportErrorsModal";
+import { DagImportErrorsModal } from "./DagImportErrorsModal";
 
-export const DAGImportErrors = ({ iconOnly = false }: { readonly iconOnly?: 
boolean }) => {
+export const DagImportErrors = ({ iconOnly = false }: { readonly iconOnly?: 
boolean }) => {
   const { onClose, onOpen, open } = useDisclosure();
   const { i18n, t: translate } = useTranslation("dashboard");
 
@@ -49,6 +49,7 @@ export const DAGImportErrors = ({ iconOnly = false }: { 
readonly iconOnly?: bool
       <ErrorAlert error={error} />
       {iconOnly ? (
         <StateBadge
+          aria-label={translate("importErrors.dagImportError", { count: 
importErrorsCount })}
           as={Button}
           colorPalette="failed"
           height={7}
@@ -69,7 +70,7 @@ export const DAGImportErrors = ({ iconOnly = false }: { 
readonly iconOnly?: bool
           onClick={onOpen}
         />
       )}
-      <DAGImportErrorsModal onClose={onClose} open={open} />
+      <DagImportErrorsModal onClose={onClose} open={open} />
     </Box>
   );
 };
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
similarity index 76%
rename from 
airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrorsModal.tsx
rename to 
airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagImportErrorsModal.tsx
index 9a31250dcac..61c0bd57bc4 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
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Heading, Text, HStack, ClipboardRoot } from "@chakra-ui/react";
+import { Box, ClipboardRoot, Heading, HStack, Text } from "@chakra-ui/react";
 import { useState } from "react";
 import { useTranslation } from "react-i18next";
 import { LuFileWarning } from "react-icons/lu";
@@ -35,7 +35,7 @@ type ImportDAGErrorModalProps = {
 
 const PAGE_LIMIT = 15;
 
-export const DAGImportErrorsModal: React.FC<ImportDAGErrorModalProps> = ({ 
onClose, open }) => {
+export const DagImportErrorsModal: React.FC<ImportDAGErrorModalProps> = ({ 
onClose, open }) => {
   const [page, setPage] = useState(1);
   const [searchQuery, setSearchQuery] = useState("");
 
@@ -81,20 +81,26 @@ export const DAGImportErrorsModal: 
React.FC<ImportDAGErrorModalProps> = ({ onClo
 
         <Dialog.Body>
           <Accordion.Root collapsible multiple size="md" variant="enclosed">
-            {data?.import_errors.map((importError) => (
-              <Accordion.Item key={importError.import_error_id} 
value={importError.filename}>
-                <Accordion.ItemTrigger cursor="pointer">
-                  <Text display="flex" fontWeight="bold">
-                    {translate("components:versionDetails.bundleName")}
-                    {": "}
-                    {importError.bundle_name}
-                  </Text>
-                  <PiFilePy />
-                  {importError.filename}
-                  <ClipboardRoot onClick={(event) => event.stopPropagation()} 
value={importError.filename}>
-                    <ClipboardIconButton variant="outline" />
-                  </ClipboardRoot>
-                </Accordion.ItemTrigger>
+            {(data?.import_errors ?? []).map((importError) => (
+              <Accordion.Item key={importError.import_error_id} 
value={String(importError.import_error_id)}>
+                <HStack align="stretch" gap={0} w="100%">
+                  <Accordion.ItemTrigger cursor="pointer" flex="1">
+                    <HStack alignItems="center" flexWrap="wrap" gap={2} 
w="100%">
+                      <Text display="flex" fontWeight="bold">
+                        {translate("components:versionDetails.bundleName")}
+                        {": "}
+                        {importError.bundle_name}
+                      </Text>
+                      <PiFilePy />
+                      {importError.filename}
+                    </HStack>
+                  </Accordion.ItemTrigger>
+                  <Box alignItems="center" display="flex" flexShrink={0} 
pr={2}>
+                    <ClipboardRoot value={importError.filename}>
+                      <ClipboardIconButton variant="outline" />
+                    </ClipboardRoot>
+                  </Box>
+                </HStack>
                 <Accordion.ItemContent>
                   <Text color="fg.muted" fontSize="sm" mb={1}>
                     {translate("importErrors.timestamp")}
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
index 4192ad7b886..6fb64d2659d 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
@@ -25,7 +25,7 @@ import { NeedsReviewButton } from 
"src/components/NeedsReviewButton";
 import { StatsCard } from "src/components/StatsCard";
 import { useAutoRefresh } from "src/utils";
 
-import { DAGImportErrors } from "./DAGImportErrors";
+import { DagImportErrors } from "./DagImportErrors";
 import { PluginImportErrors } from "./PluginImportErrors";
 
 export const Stats = () => {
@@ -64,7 +64,7 @@ export const Stats = () => {
           state="failed"
         />
 
-        <DAGImportErrors />
+        <DagImportErrors />
 
         <PluginImportErrors />
 
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py
 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py
index a595d139f92..0db0c07c5f7 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_import_error.py
@@ -387,6 +387,111 @@ class TestGetImportErrors:
             import_error["filename"] for import_error in 
response_json["import_errors"]
         ] == expected_filenames
 
+    @pytest.mark.parametrize(
+        ("filename", "expected_total_entries", "expected_filenames"),
+        [
+            (FILENAME1, 1, [FILENAME1]),
+            (FILENAME2, 1, [FILENAME2]),
+            ("nonexistent.py", 0, []),
+        ],
+    )
+    
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
+    def test_get_import_errors_exact_filename_filter(
+        self,
+        mock_get_auth_manager,
+        test_client,
+        filename,
+        expected_total_entries,
+        expected_filenames,
+        permitted_dag_model_all,
+    ):
+        """Test that the exact ``filename`` filter returns only the matching 
file."""
+        set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
permitted_dag_model_all)
+        set_mock_auth_manager__batch_is_authorized_dag(mock_get_auth_manager, 
True)
+
+        response = test_client.get("/importErrors", params={"filename": 
filename})
+
+        assert response.status_code == 200
+        response_json = response.json()
+        assert response_json["total_entries"] == expected_total_entries
+        assert [ie["filename"] for ie in response_json["import_errors"]] == 
expected_filenames
+
+    @pytest.mark.parametrize(
+        ("bundle_name", "expected_total_entries", "expected_filenames"),
+        [
+            (BUNDLE_NAME, 3, [FILENAME1, FILENAME2, FILENAME3]),
+            ("nonexistent_bundle", 0, []),
+        ],
+    )
+    
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
+    def test_get_import_errors_bundle_name_filter(
+        self,
+        mock_get_auth_manager,
+        test_client,
+        bundle_name,
+        expected_total_entries,
+        expected_filenames,
+        permitted_dag_model_all,
+    ):
+        """Test that the ``bundle_name`` filter returns only errors from the 
matching bundle."""
+        set_mock_auth_manager__get_authorized_dag_ids(mock_get_auth_manager, 
permitted_dag_model_all)
+        set_mock_auth_manager__batch_is_authorized_dag(mock_get_auth_manager, 
True)
+
+        response = test_client.get("/importErrors", params={"bundle_name": 
bundle_name})
+
+        assert response.status_code == 200
+        response_json = response.json()
+        assert response_json["total_entries"] == expected_total_entries
+        assert [ie["filename"] for ie in response_json["import_errors"]] == 
expected_filenames
+
+    
@mock.patch("airflow.api_fastapi.core_api.routes.public.import_error.get_auth_manager")
+    def test_get_import_errors_bundle_name_and_filename_filter(
+        self,
+        mock_get_auth_manager,
+        test_client,
+        permitted_dag_model_all,
+        session,
+    ):
+        """Test that combining ``bundle_name`` and ``filename`` filters finds 
the right error
+        when two bundles share the same relative_fileloc (the multi-bundle 
cross-contamination case).
+        """
+        other_bundle = "other_bundle"
+        session.add(DagBundleModel(name=other_bundle))
+        session.flush()
+
+        shared_filename = FILENAME1
+        other_dag = DagModel(
+            fileloc=f"/other/{shared_filename}",
+            relative_fileloc=shared_filename,
+            dag_id="dag_in_other_bundle",
+            is_paused=False,
+            bundle_name=other_bundle,
+        )
+        session.add(other_dag)
+        session.add(
+            ParseImportError(
+                bundle_name=other_bundle,
+                filename=shared_filename,
+                stacktrace="wrong bundle error",
+                timestamp=TIMESTAMP1,
+            )
+        )
+        session.commit()
+
+        set_mock_auth_manager__get_authorized_dag_ids(
+            mock_get_auth_manager, permitted_dag_model_all | {other_dag.dag_id}
+        )
+        set_mock_auth_manager__batch_is_authorized_dag(mock_get_auth_manager, 
True)
+
+        response = test_client.get(
+            "/importErrors", params={"filename": shared_filename, 
"bundle_name": BUNDLE_NAME}
+        )
+        assert response.status_code == 200
+        response_json = response.json()
+        assert response_json["total_entries"] == 1
+        assert response_json["import_errors"][0]["bundle_name"] == BUNDLE_NAME
+        assert response_json["import_errors"][0]["stack_trace"] == STACKTRACE1
+
     def test_should_raises_401_unauthenticated(self, 
unauthenticated_test_client):
         response = unauthenticated_test_client.get("/importErrors")
         assert response.status_code == 401

Reply via email to