From af4f70f18cadec1c86505a32ba9537298474384c Mon Sep 17 00:00:00 2001 From: Julian LaNeve Date: Wed, 14 Oct 2026 15:56:10 -0400 Subject: [PATCH] UI: Drop the loading spinner for base React plugins A plugin with destination "base" renders above the content of every page. While its bundle loads, ReactPlugin's Suspense fallback put a bare spinner in the top left corner, so every full page load showed a stray spinner until the bundle arrived. Base plugins now render nothing until they are ready. Plugins on other destinations keep the spinner, since they fill a page or tab that would otherwise be empty. --- .../airflow/ui/src/pages/ReactPlugin.test.tsx | 17 +++++++++++++++++ .../src/airflow/ui/src/pages/ReactPlugin.tsx | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx index 6878743c55c76..adbf65ee8cecc 100644 --- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx @@ -189,3 +189,20 @@ describe("loadPlugin", () => { expect(component).not.toBe(componentA); }); }); + +const loading = (destination: ReactAppResponse["destination"]) => + ({ bundle_url: "http://localhost/never-loaded.js", destination, name: "Unloaded" }) as ReactAppResponse; + +describe("ReactPlugin loading fallback", () => { + it("shows a spinner while a page plugin's bundle loads", () => { + const { container } = render(, { wrapper: Wrapper }); + + expect(container.querySelector(".chakra-spinner")).toBeInTheDocument(); + }); + + it("shows nothing while a base plugin's bundle loads", () => { + const { container } = render(, { wrapper: Wrapper }); + + expect(container.querySelector(".chakra-spinner")).not.toBeInTheDocument(); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx index f454b6c806c32..3d7b55a2d929f 100644 --- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx +++ b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx @@ -150,9 +150,11 @@ export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse // Otherwise, lazy-load the bundle once. When it resolves, it must set a function component // under globalThis[reactApp.name], which we then use as the default export. const LazyPlugin = lazy(() => loadPlugin(reactApp)); + // A base plugin sits above every page, so a spinner there shows on every full page load. + const fallback = reactApp.destination === "base" ? null : ; return ( - }> + );