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; +};