diff --git a/.changeset/giant-drinks-wave.md b/.changeset/giant-drinks-wave.md new file mode 100644 index 0000000000..ab9df136fc --- /dev/null +++ b/.changeset/giant-drinks-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/dev-utils': patch +--- + +Migrated to explicit passing of components to `createApp`. diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md new file mode 100644 index 0000000000..055613934d --- /dev/null +++ b/.changeset/hot-walls-fail.md @@ -0,0 +1,33 @@ +--- +'@backstage/create-app': patch +--- + +Migrated the app template to pass on explicit `components` to the `createApp` options, as not doing this has been deprecated and will need to be done in the future. + +To migrate an existing application, make the following change to `packages/app/src/App.tsx`: + +```diff ++import { defaultAppComponents } from '@backstage/core-components'; + + // ... + + const app = createApp({ + apis, ++ components: defaultAppComponents(), + bindRoutes({ bind }) { +``` + +If you already supply custom app components, you can use the following: + +```diff + + // ... + + const app = createApp({ + apis, ++ components: { + ...defaultAppComponents(), ++ Progress: MyCustomProgressComponent, + }, + bindRoutes({ bind }) { +``` diff --git a/.changeset/late-rice-sit.md b/.changeset/late-rice-sit.md new file mode 100644 index 0000000000..eb074cbb18 --- /dev/null +++ b/.changeset/late-rice-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added a new `defaultAppComponents` method that creates a minimal set of components to pass on to `createApp` from `@backstage/core-app-api`. diff --git a/.changeset/twenty-swans-matter.md b/.changeset/twenty-swans-matter.md new file mode 100644 index 0000000000..0722913e05 --- /dev/null +++ b/.changeset/twenty-swans-matter.md @@ -0,0 +1,16 @@ +--- +'@backstage/core-app-api': patch +--- + +Deprecated the defaulting of the `components` options of `createApp`, meaning it will become required in the future. When not passing the required components options a deprecation warning is currently logged, and it will become required in a future release. + +The keep the existing components intact, migrate to using `defaultAppComponents` from `@backstage/core-components`: + +```ts +const app = createApp({ + components: { + ...defaultAppComponents(), + // Place any custom components here + }, +}); +``` diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 109ae7922e..f0b31fb4d0 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,6 +31,7 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, + defaultAppComponents, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -95,6 +96,7 @@ const app = createApp({ }, components: { + ...defaultAppComponents(), SignInPage: props => { return ( { }); }); -describe('OptionallyWrapInRouter', () => { - it('should wrap with router if not yet inside a router', async () => { - const { getByText } = render( - Test, - ); - - expect(getByText('Test')).toBeInTheDocument(); - }); - - it('should not wrap with router if already inside a router', async () => { - const { getByText } = render( - - Test - , - ); - - expect(getByText('Test')).toBeInTheDocument(); - }); -}); - describe('Optional ThemeProvider', () => { it('should render app with user-provided ThemeProvider', async () => { const components = { diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 65aa15b50c..1e53103152 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -16,28 +16,25 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { Button } from '@material-ui/core'; -import { ErrorPage, ErrorPanel, Progress } from '@backstage/core-components'; +import { defaultAppComponents } from '@backstage/core-components'; import { darkTheme, lightTheme } from '@backstage/theme'; import DarkIcon from '@material-ui/icons/Brightness2'; import LightIcon from '@material-ui/icons/WbSunny'; -import React, { PropsWithChildren } from 'react'; -import { - BrowserRouter, - MemoryRouter, - useInRouterContext, -} from 'react-router-dom'; +import React from 'react'; +import { BrowserRouter } from 'react-router-dom'; import { PrivateAppImpl } from './App'; import { AppThemeProvider } from './AppThemeProvider'; import { defaultApis } from './defaultApis'; import { defaultAppIcons } from './icons'; -import { - AppConfigLoader, - AppOptions, - BootErrorPageProps, - ErrorBoundaryFallbackProps, -} from './types'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { AppConfigLoader, AppOptions } from './types'; +import { AppComponents, BackstagePlugin } from '@backstage/core-plugin-api'; + +const REQUIRED_APP_COMPONENTS: Array = [ + 'Progress', + 'NotFoundErrorPage', + 'BootErrorPage', + 'ErrorBoundaryFallback', +]; /** * The default config loader, which expects that config is available at compile-time @@ -93,63 +90,34 @@ export const defaultConfigLoader: AppConfigLoader = async ( return configs; }; -export function OptionallyWrapInRouter({ children }: PropsWithChildren<{}>) { - if (useInRouterContext()) { - return <>{children}; - } - return {children}; -} - /** * Creates a new Backstage App. * * @public */ export function createApp(options?: AppOptions) { - const DefaultNotFoundPage = () => ( - + const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( + name => !options?.components?.[name], ); - const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { - let message = ''; - if (step === 'load-config') { - message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; - } else if (step === 'load-chunk') { - message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`; - } - // TODO: figure out a nicer way to handle routing on the error page, when it can be done. - return ( - - - + if (missingRequiredComponents.length > 0) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of ' + + 'components to be provided in the components option. These components can be ' + + 'created using defaultAppComponents from @backstage/core-components and ' + + 'passed along like this: createApp({ components: defaultAppComponents() }). ' + + `The following components are missing: ${missingRequiredComponents.join( + ', ', + )}`, ); - }; - const DefaultErrorBoundaryFallback = ({ - error, - resetError, - plugin, - }: ErrorBoundaryFallbackProps) => { - return ( - - - - ); - }; + } const apis = options?.apis ?? []; const icons = { ...defaultAppIcons, ...options?.icons }; const plugins = options?.plugins ?? []; const components = { - NotFoundErrorPage: DefaultNotFoundPage, - BootErrorPage: DefaultBootErrorPage, - Progress: Progress, + ...defaultAppComponents(), Router: BrowserRouter, - ErrorBoundaryFallback: DefaultErrorBoundaryFallback, ThemeProvider: AppThemeProvider, ...options?.components, }; diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 0f1e3510ae..63f35873f7 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -6,6 +6,7 @@ /// import { ApiRef } from '@backstage/core-plugin-api'; +import { AppComponents } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; import { BackstageTheme } from '@backstage/theme'; @@ -199,6 +200,9 @@ export type CustomProviderClassKey = 'form' | 'button'; // @public (undocumented) export function DashboardIcon(props: IconComponentProps): JSX.Element; +// @public +export function defaultAppComponents(): Omit; + // @public type DependencyEdge = T & { from: string; diff --git a/packages/core-components/src/defaultAppComponents.test.tsx b/packages/core-components/src/defaultAppComponents.test.tsx new file mode 100644 index 0000000000..db0e2d50c1 --- /dev/null +++ b/packages/core-components/src/defaultAppComponents.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { OptionallyWrapInRouter } from './defaultAppComponents'; + +describe('OptionallyWrapInRouter', () => { + it('should wrap with router if not yet inside a router', async () => { + render(Test); + + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + + it('should not wrap with router if already inside a router', async () => { + render( + + Test + , + ); + + expect(screen.getByText('Test')).toBeInTheDocument(); + }); +}); diff --git a/packages/core-components/src/defaultAppComponents.tsx b/packages/core-components/src/defaultAppComponents.tsx new file mode 100644 index 0000000000..fb810a6272 --- /dev/null +++ b/packages/core-components/src/defaultAppComponents.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed 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 React, { ReactNode } from 'react'; +import Button from '@material-ui/core/Button'; +import { + AppComponents, + BootErrorPageProps, + ErrorBoundaryFallbackProps, +} from '@backstage/core-plugin-api'; +import { ErrorPanel, Progress } from './components'; +import { ErrorPage } from './layout'; +import { MemoryRouter, useInRouterContext } from 'react-router'; + +export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { + if (useInRouterContext()) { + return <>{children}; + } + return {children}; +} + +const DefaultNotFoundPage = () => ( + +); + +const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { + let message = ''; + if (step === 'load-config') { + message = `The configuration failed to load, someone should have a look at this error: ${error.message}`; + } else if (step === 'load-chunk') { + message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`; + } + // TODO: figure out a nicer way to handle routing on the error page, when it can be done. + return ( + + + + ); +}; +const DefaultErrorBoundaryFallback = ({ + error, + resetError, + plugin, +}: ErrorBoundaryFallbackProps) => { + return ( + + + + ); +}; + +/** + * Creates a set of default components to pass along to {@link @backstage/core-app-api#createApp}. + * + * @public + */ +export function defaultAppComponents(): Omit { + return { + Progress, + NotFoundErrorPage: DefaultNotFoundPage, + BootErrorPage: DefaultBootErrorPage, + ErrorBoundaryFallback: DefaultErrorBoundaryFallback, + }; +} diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 3c5e708360..9bc2436559 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -25,3 +25,4 @@ export * from './hooks'; export * from './icons'; export * from './layout'; export * from './overridableComponents'; +export { defaultAppComponents } from './defaultAppComponents'; diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 4cd83685a6..7117b3ea07 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -25,11 +25,12 @@ import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; -import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; +import { AlertDisplay, defaultAppComponents, OAuthRequestDialog } from '@backstage/core-components'; import { createApp, FlatRoutes } from '@backstage/core-app-api'; const app = createApp({ apis, + components: defaultAppComponents(), bindRoutes({ bind }) { bind(catalogPlugin.externalRoutes, { createComponent: scaffolderPlugin.routes.root, diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 755289a060..31b6ac6169 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -27,6 +27,7 @@ import { Route } from 'react-router'; import { AlertDisplay, + defaultAppComponents, OAuthRequestDialog, Sidebar, SidebarItem, @@ -177,6 +178,7 @@ export class DevAppBuilder { apis, plugins: this.plugins, themes: this.themes, + components: defaultAppComponents(), bindRoutes: ({ bind }) => { for (const plugin of this.plugins ?? []) { const targets: Record> = {};