From 48d6628af90597c484c137c80243cbbf2fa2b6d5 Mon Sep 17 00:00:00 2001 From: Mitchell Hentges Date: Wed, 13 Mar 2024 11:42:20 +0100 Subject: [PATCH] Add `loadingComponent` parameter to `createApp()` This allows Backstage instances to show their own "fallback" content when the app is loading, such as to avoid the "flicker of white" on-reload. Signed-off-by: Mitchell Hentges --- .changeset/neat-mails-teach.md | 5 +++ packages/frontend-app-api/api-report.md | 2 ++ .../src/wiring/createApp.test.tsx | 32 +++++++++++++++++++ .../frontend-app-api/src/wiring/createApp.tsx | 16 ++++++++-- 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 .changeset/neat-mails-teach.md 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 ( - + );