diff --git a/.changeset/neat-mails-teach.md b/.changeset/neat-mails-teach.md new file mode 100644 index 0000000000..162cbfef93 --- /dev/null +++ b/.changeset/neat-mails-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': minor +--- + +Add `loadingComponent` parameter to `createApp()` diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 3236b05a01..0375a67482 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -8,6 +8,7 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; +import { ReactNode } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { SubRouteRef } from '@backstage/frontend-plugin-api'; @@ -21,6 +22,7 @@ export function createApp(options?: { config: ConfigApi; }>; bindRoutes?(context: { bind: CreateAppRouteBinder }): void; + loadingComponent?: ReactNode; }): { createRoot(): JSX_2.Element; }; diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 70f8cfa2ae..94254a2e01 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -297,4 +297,36 @@ describe('createApp', () => { " `); }); + + it('should use "Loading..." as the default suspense fallback', async () => { + const app = createApp({ + configLoader: () => new Promise(() => {}), + }); + + await renderWithEffects(app.createRoot()); + + await expect(screen.findByText('Loading...')).resolves.toBeInTheDocument(); + }); + + it('should use no suspense fallback if the "loadingComponent" is null', async () => { + const app = createApp({ + configLoader: () => new Promise(() => {}), + loadingComponent: null, + }); + + await renderWithEffects(app.createRoot()); + + expect(screen.queryByText('Loading...')).toBeNull(); + }); + + it('should use a custom "loadingComponent"', async () => { + const app = createApp({ + configLoader: () => new Promise(() => {}), + loadingComponent: "Custom loading message", + }); + + await renderWithEffects(app.createRoot()); + + expect(screen.queryByText('Custom loading message')).toBeNull(); + }); }); diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 591e1cbf25..6e0a2c7cd7 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { JSX } from 'react'; +import React, { JSX, ReactNode } from 'react'; import { ConfigReader } from '@backstage/config'; import { AppTree, @@ -170,9 +170,21 @@ export function createApp(options?: { features?: (FrontendFeature | CreateAppFeatureLoader)[]; configLoader?: () => Promise<{ config: ConfigApi }>; bindRoutes?(context: { bind: CreateAppRouteBinder }): void; + /** + * The component to render while loading the app (waiting for config, features, etc) + * + * Is the text "Loading..." by default. + * If set to "null" then no loading fallback component is rendered. * + */ + loadingComponent?: ReactNode; }): { createRoot(): JSX.Element; } { + let suspenseFallback = options?.loadingComponent; + if (suspenseFallback === undefined) { + suspenseFallback = 'Loading...'; + } + async function appLoader() { const config = (await options?.configLoader?.().then(c => c.config)) ?? @@ -214,7 +226,7 @@ export function createApp(options?: { createRoot() { const LazyApp = React.lazy(appLoader); return ( - + );