diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
index 698b017bf8dcc..8cf3b42bb95ab 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
@@ -54,6 +54,7 @@
},
"history": "History",
"importErrors": {
+ "bundle": "Bundle",
"dagImportError_one": "Dag Import Error",
"dagImportError_other": "Dag Import Errors",
"searchByFile": "Search by file",
diff --git a/airflow-core/src/airflow/ui/src/components/DagImportErrorDetails.tsx b/airflow-core/src/airflow/ui/src/components/DagImportErrorDetails.tsx
index 6eb5f825ce9c7..1cb0947ee7362 100644
--- a/airflow-core/src/airflow/ui/src/components/DagImportErrorDetails.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagImportErrorDetails.tsx
@@ -28,11 +28,13 @@ import { ClipboardIconButton, IconButton, toaster } from "src/system-components"
import Time from "src/components/Time";
+import { useHasMultipleDagBundles } from "src/queries/useHasMultipleDagBundles";
import { createErrorToaster } from "src/utils";
/** The parse failure itself — where it came from, when, the traceback — and a way to reparse. */
export const DagImportErrorDetails = ({ importError }: { readonly importError: ImportErrorResponse }) => {
const { t: translate } = useTranslation(["dashboard", "components", "dag"]);
+ const hasMultipleBundles = useHasMultipleDagBundles();
const { isPending, mutate } = useDagParsingServiceReparseDagFile({
onError: (error) => createErrorToaster(error, { titleKey: "dag:parse.toaster.error.title" }, translate),
@@ -47,11 +49,6 @@ export const DagImportErrorDetails = ({ importError }: { readonly importError: I
return (
<>
-
- {translate("components:versionDetails.bundleName")}
- {": "}
- {importError.bundle_name}
-
{importError.filename}
@@ -66,6 +63,13 @@ export const DagImportErrorDetails = ({ importError }: { readonly importError: I
>
+ {hasMultipleBundles ? (
+
+ {translate("importErrors.bundle")}
+ {": "}
+ {importError.bundle_name}
+
+ ) : undefined}
{translate("importErrors.timestamp")}
diff --git a/airflow-core/src/airflow/ui/src/components/DagImportErrorModal.test.tsx b/airflow-core/src/airflow/ui/src/components/DagImportErrorModal.test.tsx
index 5d4a576531598..96600a5f7061f 100644
--- a/airflow-core/src/airflow/ui/src/components/DagImportErrorModal.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagImportErrorModal.test.tsx
@@ -18,10 +18,12 @@
*/
import "@testing-library/jest-dom/vitest";
import { render, screen, within } from "@testing-library/react";
+import { http, HttpResponse } from "msw";
import { describe, expect, it, vi } from "vitest";
import type { ImportErrorResponse } from "openapi/requests/types.gen";
+import { server } from "src/mocks/server";
import { Wrapper } from "src/utils/Wrapper";
import { DagImportErrorModal } from "./DagImportErrorModal";
@@ -43,8 +45,31 @@ describe("DagImportErrorModal", () => {
// i18n resources are not loaded in unit tests, so the title renders as its key.
const dialog = screen.getByRole("dialog", { name: "importErrors.dagImportError" });
- expect(dialog).toHaveTextContent("dags-folder");
expect(dialog).toHaveTextContent("dags/broken_dag.py");
expect(within(dialog).getByText(/Traceback \(most recent call last\)/u)).toBeInTheDocument();
});
+
+ it.each([
+ { shown: false, totalEntries: 1 },
+ { shown: true, totalEntries: 2 },
+ ])("shows the bundle name only with several bundles ($totalEntries)", async ({ shown, totalEntries }) => {
+ server.use(
+ http.get("/api/v2/dagBundles", () =>
+ HttpResponse.json({ dag_bundles: [], total_entries: totalEntries }),
+ ),
+ );
+
+ render(, { wrapper: Wrapper });
+
+ const dialog = screen.getByRole("dialog", { name: "importErrors.dagImportError" });
+
+ if (shown) {
+ expect(await within(dialog).findByText(/dags-folder/u)).toBeInTheDocument();
+ } else {
+ await new Promise((resolve) => {
+ setTimeout(resolve, 50);
+ });
+ expect(dialog).not.toHaveTextContent("dags-folder");
+ }
+ });
});
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 7adea18d4fe5b..5bf9dfe1545d7 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
@@ -39,6 +39,7 @@ import { SearchBar } from "src/components/SearchBar";
import Time from "src/components/Time";
import { useConfig } from "src/queries/useConfig";
+import { useHasMultipleDagBundles } from "src/queries/useHasMultipleDagBundles";
import { createErrorToaster } from "src/utils";
type ImportDAGErrorModalProps = {
@@ -89,6 +90,7 @@ export const DagImportErrorsModal = ({ onClose, open }: ImportDAGErrorModalProps
const [searchQuery, setSearchQuery] = useState("");
const [pollAfterReparse, setPollAfterReparse] = useState(false);
const autoRefreshInterval = useConfig("auto_refresh_interval") as number | undefined;
+ const hasMultipleBundles = useHasMultipleDagBundles();
const { data } = useImportErrorServiceGetImportErrors(
{
@@ -168,13 +170,15 @@ export const DagImportErrorsModal = ({ onClose, open }: ImportDAGErrorModalProps
-
- {translate("components:versionDetails.bundleName")}
- {": "}
- {importError.bundle_name}
-
{importError.filename}
+ {hasMultipleBundles ? (
+
+ {translate("importErrors.bundle")}
+ {": "}
+ {importError.bundle_name}
+
+ ) : undefined}
diff --git a/airflow-core/src/airflow/ui/src/queries/useHasMultipleDagBundles.ts b/airflow-core/src/airflow/ui/src/queries/useHasMultipleDagBundles.ts
new file mode 100644
index 0000000000000..99edd6a050684
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useHasMultipleDagBundles.ts
@@ -0,0 +1,26 @@
+/*!
+ * 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 { useDagBundleServiceGetDagBundles } from "openapi/queries";
+
+/** Whether the user can see more than one Dag bundle, i.e. whether a bundle name tells anything apart. */
+export const useHasMultipleDagBundles = (): boolean => {
+ const { data } = useDagBundleServiceGetDagBundles({ limit: 1 });
+
+ return (data?.total_entries ?? 0) > 1;
+};