From 014cbf8cb9f91bbf5961aca0a5b4839c806729dd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 11 Oct 2021 22:10:47 +0200 Subject: [PATCH 01/35] core-app-api: deprecate dependency on core-components Signed-off-by: Patrik Oldsberg --- .changeset/giant-drinks-wave.md | 5 ++ .changeset/hot-walls-fail.md | 33 ++++++++ .changeset/late-rice-sit.md | 5 ++ .changeset/twenty-swans-matter.md | 16 ++++ packages/app/src/App.tsx | 2 + .../core-app-api/src/app/createApp.test.tsx | 28 +------ packages/core-app-api/src/app/createApp.tsx | 84 ++++++------------- packages/core-components/api-report.md | 4 + .../src/defaultAppComponents.test.tsx | 38 +++++++++ .../src/defaultAppComponents.tsx | 83 ++++++++++++++++++ packages/core-components/src/index.ts | 1 + .../default-app/packages/app/src/App.tsx | 3 +- packages/dev-utils/src/devApp/render.tsx | 2 + 13 files changed, 219 insertions(+), 85 deletions(-) create mode 100644 .changeset/giant-drinks-wave.md create mode 100644 .changeset/hot-walls-fail.md create mode 100644 .changeset/late-rice-sit.md create mode 100644 .changeset/twenty-swans-matter.md create mode 100644 packages/core-components/src/defaultAppComponents.test.tsx create mode 100644 packages/core-components/src/defaultAppComponents.tsx 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> = {}; From cb77f4599a2f9433bf0e87cc4350d912913ba216 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 22 Oct 2021 13:59:41 +0200 Subject: [PATCH 02/35] core-app-api: deprecate not passing in icons + add defaultAppIcons() Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/app/src/App.tsx | 2 + packages/core-app-api/src/app/createApp.tsx | 24 ++++++-- packages/core-app-api/src/app/icons.tsx | 46 ++++++++-------- packages/core-app-api/src/app/index.ts | 1 + .../core-components/src/defaultAppIcons.tsx | 55 +++++++++++++++++++ packages/core-components/src/index.ts | 1 + 6 files changed, 102 insertions(+), 27 deletions(-) create mode 100644 packages/core-components/src/defaultAppIcons.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index f0b31fb4d0..c2273156ac 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,6 +31,7 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, + defaultAppIcons, defaultAppComponents, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; @@ -91,6 +92,7 @@ const app = createApp({ apis, plugins: Object.values(plugins), icons: { + ...defaultAppIcons(), // Custom icon example alert: AlarmIcon, }, diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 1e53103152..7b93daf40f 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -16,7 +16,10 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { defaultAppComponents } from '@backstage/core-components'; +import { + defaultAppComponents, + defaultAppIcons, +} from '@backstage/core-components'; import { darkTheme, lightTheme } from '@backstage/theme'; import DarkIcon from '@material-ui/icons/Brightness2'; import LightIcon from '@material-ui/icons/WbSunny'; @@ -25,7 +28,6 @@ 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 } from './types'; import { AppComponents, BackstagePlugin } from '@backstage/core-plugin-api'; @@ -112,8 +114,22 @@ export function createApp(options?: AppOptions) { ); } + const appIcons = defaultAppIcons(); + const providedIconKeys = Object.keys(options?.icons ?? {}); + const missingIconKeys = Object.keys(appIcons).filter( + key => !providedIconKeys.includes(key), + ); + if (missingIconKeys.length > 0) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require all app icons to be provided.' + + 'These icons can be created using defaultAppIcons from @backstage/core-components ' + + 'and then passed along like this: createApp({ icons: ...defaultAppIcons() })' + + `The following icons are missing: ${missingIconKeys.join(', ')}`, + ); + } + const apis = options?.apis ?? []; - const icons = { ...defaultAppIcons, ...options?.icons }; const plugins = options?.plugins ?? []; const components = { ...defaultAppComponents(), @@ -141,7 +157,7 @@ export function createApp(options?: AppOptions) { return new PrivateAppImpl({ apis, - icons, + icons: { ...appIcons, ...options?.icons }, plugins: plugins as BackstagePlugin[], components, themes, diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/core-app-api/src/app/icons.tsx index b234a292b1..f06c9da989 100644 --- a/packages/core-app-api/src/app/icons.tsx +++ b/packages/core-app-api/src/app/icons.tsx @@ -35,30 +35,30 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; -type AppIconsKey = - | 'brokenImage' - | 'catalog' - | 'scaffolder' - | 'techdocs' - | 'search' - | 'chat' - | 'dashboard' - | 'docs' - | 'email' - | 'github' - | 'group' - | 'help' - | 'kind:api' - | 'kind:component' - | 'kind:domain' - | 'kind:group' - | 'kind:location' - | 'kind:system' - | 'kind:user' - | 'user' - | 'warning'; +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; -export type AppIcons = { [key in AppIconsKey]: IconComponent }; + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + scaffolder: IconComponent; + search: IconComponent; + techdocs: IconComponent; + user: IconComponent; + warning: IconComponent; +}; export const defaultAppIcons: AppIcons = { brokenImage: MuiBrokenImageIcon, diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index ab82774cad..2a16a90bfa 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -15,4 +15,5 @@ */ export { createApp, defaultConfigLoader } from './createApp'; +export type { AppIcons } from './icons'; export * from './types'; diff --git a/packages/core-components/src/defaultAppIcons.tsx b/packages/core-components/src/defaultAppIcons.tsx new file mode 100644 index 0000000000..1d02dd5ad2 --- /dev/null +++ b/packages/core-components/src/defaultAppIcons.tsx @@ -0,0 +1,55 @@ +/* + * 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 { IconComponent } from '@backstage/core-plugin-api'; +import MuiApartmentIcon from '@material-ui/icons/Apartment'; +import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; +import MuiCategoryIcon from '@material-ui/icons/Category'; +import MuiChatIcon from '@material-ui/icons/Chat'; +import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiDocsIcon from '@material-ui/icons/Description'; +import MuiEmailIcon from '@material-ui/icons/Email'; +import MuiExtensionIcon from '@material-ui/icons/Extension'; +import MuiGitHubIcon from '@material-ui/icons/GitHub'; +import MuiHelpIcon from '@material-ui/icons/Help'; +import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; +import MuiMemoryIcon from '@material-ui/icons/Memory'; +import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; +import MuiPeopleIcon from '@material-ui/icons/People'; +import MuiPersonIcon from '@material-ui/icons/Person'; +import MuiWarningIcon from '@material-ui/icons/Warning'; + +export const defaultAppIcons = () => ({ + brokenImage: MuiBrokenImageIcon as IconComponent, + // To be confirmed: see https://github.com/backstage/backstage/issues/4970 + catalog: MuiMenuBookIcon as IconComponent, + chat: MuiChatIcon as IconComponent, + dashboard: MuiDashboardIcon as IconComponent, + docs: MuiDocsIcon as IconComponent, + email: MuiEmailIcon as IconComponent, + github: MuiGitHubIcon as IconComponent, + group: MuiPeopleIcon as IconComponent, + help: MuiHelpIcon as IconComponent, + 'kind:api': MuiExtensionIcon as IconComponent, + 'kind:component': MuiMemoryIcon as IconComponent, + 'kind:domain': MuiApartmentIcon as IconComponent, + 'kind:group': MuiPeopleIcon as IconComponent, + 'kind:location': MuiLocationOnIcon as IconComponent, + 'kind:system': MuiCategoryIcon as IconComponent, + 'kind:user': MuiPersonIcon as IconComponent, + user: MuiPersonIcon as IconComponent, + warning: MuiWarningIcon as IconComponent, +}); diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 9bc2436559..3538cd4186 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -26,3 +26,4 @@ export * from './icons'; export * from './layout'; export * from './overridableComponents'; export { defaultAppComponents } from './defaultAppComponents'; +export { defaultAppIcons } from './defaultAppIcons'; From 9ddec7eaf14cfc77bc8244fd93023efecf25a86d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 22 Oct 2021 14:46:33 +0200 Subject: [PATCH 03/35] core-{app,plugin}-api: refactor AppTheme to use provider + defaultAppThemes Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/App.tsx | 4 +- .../core-app-api/src/app/AppThemeProvider.tsx | 11 ++++ packages/core-app-api/src/app/createApp.tsx | 32 ++++-------- packages/core-app-api/src/app/types.ts | 14 +++-- .../core-components/src/defaultAppThemes.tsx | 52 +++++++++++++++++++ packages/core-components/src/index.ts | 1 + .../src/apis/definitions/AppThemeApi.ts | 4 ++ 7 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 packages/core-components/src/defaultAppThemes.tsx diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/App.tsx index 8cb3f167fb..6fadd22808 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/App.tsx @@ -126,7 +126,7 @@ type FullAppOptions = { icons: NonNullable; plugins: BackstagePlugin[]; components: AppComponents; - themes: AppTheme[]; + themes: (Partial & Omit)[]; configLoader?: AppConfigLoader; defaultApis: Iterable; bindRoutes?: AppOptions['bindRoutes']; @@ -206,7 +206,7 @@ export class PrivateAppImpl implements BackstageApp { this.icons = options.icons; this.plugins = new Set(options.plugins); this.components = options.components; - this.themes = options.themes; + this.themes = options.themes as AppTheme[]; this.configLoader = options.configLoader; this.defaultApis = options.defaultApis; this.bindRoutes = options.bindRoutes; diff --git a/packages/core-app-api/src/app/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx index 4e3a8487a6..aa48a2187d 100644 --- a/packages/core-app-api/src/app/AppThemeProvider.tsx +++ b/packages/core-app-api/src/app/AppThemeProvider.tsx @@ -90,6 +90,17 @@ export function AppThemeProvider({ children }: PropsWithChildren<{}>) { throw new Error('App has no themes'); } + if (appTheme.Provider) { + return ; + } + + // eslint-disable-next-line no-console + console.warn( + "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + + 'and should be migrated to use a Provider instead. ' + + 'See https://backstage.io/docs/deprecations/TODO for more info.', + ); + return ( {children} diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 7b93daf40f..f5989219e8 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -19,11 +19,8 @@ import { JsonObject } from '@backstage/types'; import { defaultAppComponents, defaultAppIcons, + defaultAppThemes, } 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 from 'react'; import { BrowserRouter } from 'react-router-dom'; import { PrivateAppImpl } from './App'; import { AppThemeProvider } from './AppThemeProvider'; @@ -129,6 +126,15 @@ export function createApp(options?: AppOptions) { ); } + if (!options?.themes) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require themes to be provided. ' + + 'Themes can be created using defaultAppThemes from @backstage/core-components ' + + 'and then passed along like this: createApp({ theme: defaultAppThemes() })', + ); + } + const apis = options?.apis ?? []; const plugins = options?.plugins ?? []; const components = { @@ -137,22 +143,6 @@ export function createApp(options?: AppOptions) { ThemeProvider: AppThemeProvider, ...options?.components, }; - const themes = options?.themes ?? [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - theme: lightTheme, - icon: , - }, - { - id: 'dark', - title: 'Dark Theme', - variant: 'dark', - theme: darkTheme, - icon: , - }, - ]; const configLoader = options?.configLoader ?? defaultConfigLoader; return new PrivateAppImpl({ @@ -160,7 +150,7 @@ export function createApp(options?: AppOptions) { icons: { ...appIcons, ...options?.icons }, plugins: plugins as BackstagePlugin[], components, - themes, + themes: options?.themes ?? defaultAppThemes(), configLoader, defaultApis, bindRoutes: options?.bindRoutes, diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 407018111b..78ef277c90 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -226,18 +226,26 @@ export type AppOptions = { * id: 'light', * title: 'Light Theme', * variant: 'light', - * theme: lightTheme, * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), * }, { * id: 'dark', * title: 'Dark Theme', * variant: 'dark', - * theme: darkTheme, * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), * }] * ``` */ - themes?: AppTheme[]; + themes?: (Partial & Omit)[]; /** * A function that loads in App configuration that will be accessible via diff --git a/packages/core-components/src/defaultAppThemes.tsx b/packages/core-components/src/defaultAppThemes.tsx new file mode 100644 index 0000000000..894716942b --- /dev/null +++ b/packages/core-components/src/defaultAppThemes.tsx @@ -0,0 +1,52 @@ +/* + * 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 from 'react'; +import { darkTheme, lightTheme } from '@backstage/theme'; +import DarkIcon from '@material-ui/icons/Brightness2'; +import LightIcon from '@material-ui/icons/WbSunny'; +import { ThemeProvider } from '@material-ui/core/styles'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import { AppTheme } from '@backstage/core-plugin-api'; + +export function defaultAppThemes(): AppTheme[] { + return [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + icon: , + theme: lightTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + theme: darkTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, + ]; +} diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 3538cd4186..305783dec8 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -27,3 +27,4 @@ export * from './layout'; export * from './overridableComponents'; export { defaultAppComponents } from './defaultAppComponents'; export { defaultAppIcons } from './defaultAppIcons'; +export { defaultAppThemes } from './defaultAppThemes'; diff --git a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts index de523663f2..52cf202422 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ReactNode } from 'react'; import { ApiRef, createApiRef } from '../system'; import { BackstageTheme } from '@backstage/theme'; import { Observable } from '@backstage/types'; @@ -41,6 +42,7 @@ export type AppTheme = { /** * The specialized MaterialUI theme instance. + * @deprecated use Provider instead, see https://backstage.io/docs/deprecations/TODO */ theme: BackstageTheme; @@ -48,6 +50,8 @@ export type AppTheme = { * An Icon for the theme mode setting. */ icon?: React.ReactElement; + + Provider?(props: { children: ReactNode }): JSX.Element | null; }; /** From e551a44c65bb06a2e45c08706850925ba2e50de1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 19:08:50 +0200 Subject: [PATCH 04/35] core-components: provide withDefaults and work around type duplication Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- packages/app/src/App.tsx | 82 ++++++------ packages/core-app-api/src/app/createApp.tsx | 40 +++--- .../src/appDefaults/AppThemeProvider.tsx | 109 ++++++++++++++++ .../defaultAppComponents.test.tsx | 0 .../defaultAppComponents.tsx | 12 +- .../src/{ => appDefaults}/defaultAppIcons.tsx | 0 .../{ => appDefaults}/defaultAppThemes.tsx | 0 .../core-components/src/appDefaults/index.ts | 17 +++ .../src/appDefaults/withDefaults.tsx | 64 ++++++++++ packages/core-components/src/index.ts | 4 +- packages/core-plugin-api/src/app/types.ts | 119 ++---------------- 11 files changed, 265 insertions(+), 182 deletions(-) create mode 100644 packages/core-components/src/appDefaults/AppThemeProvider.tsx rename packages/core-components/src/{ => appDefaults}/defaultAppComponents.test.tsx (100%) rename packages/core-components/src/{ => appDefaults}/defaultAppComponents.tsx (88%) rename packages/core-components/src/{ => appDefaults}/defaultAppIcons.tsx (100%) rename packages/core-components/src/{ => appDefaults}/defaultAppThemes.tsx (100%) create mode 100644 packages/core-components/src/appDefaults/index.ts create mode 100644 packages/core-components/src/appDefaults/withDefaults.tsx diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c2273156ac..d3ab0649e9 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,8 +31,7 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, - defaultAppIcons, - defaultAppComponents, + withDefaults, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -88,47 +87,46 @@ import * as plugins from './plugins'; import { techDocsPage } from './components/techdocs/TechDocsPage'; -const app = createApp({ - apis, - plugins: Object.values(plugins), - icons: { - ...defaultAppIcons(), - // Custom icon example - alert: AlarmIcon, - }, - - components: { - ...defaultAppComponents(), - SignInPage: props => { - return ( - - ); +const app = createApp( + withDefaults({ + apis, + plugins: Object.values(plugins), + icons: { + // Custom icon example + alert: AlarmIcon, }, - }, - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(catalogGraphPlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - bind(apiDocsPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - bind(explorePlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - }); - }, -}); + components: { + SignInPage: props => { + return ( + + ); + }, + }, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(catalogGraphPlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + bind(apiDocsPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + bind(explorePlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + }, + }), +); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index f5989219e8..c1e9741ff1 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -16,20 +16,15 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { - defaultAppComponents, - defaultAppIcons, - defaultAppThemes, -} from '@backstage/core-components'; -import { BrowserRouter } from 'react-router-dom'; +import { withDefaults } from '@backstage/core-components'; import { PrivateAppImpl } from './App'; -import { AppThemeProvider } from './AppThemeProvider'; +import { AppComponents, AppConfigLoader, AppOptions } from './types'; import { defaultApis } from './defaultApis'; -import { AppConfigLoader, AppOptions } from './types'; -import { AppComponents, BackstagePlugin } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; const REQUIRED_APP_COMPONENTS: Array = [ 'Progress', + 'Router', 'NotFoundErrorPage', 'BootErrorPage', 'ErrorBoundaryFallback', @@ -95,6 +90,8 @@ export const defaultConfigLoader: AppConfigLoader = async ( * @public */ export function createApp(options?: AppOptions) { + const optionsWithDefaults = withDefaults(options); + const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( name => !options?.components?.[name], ); @@ -111,9 +108,8 @@ export function createApp(options?: AppOptions) { ); } - const appIcons = defaultAppIcons(); const providedIconKeys = Object.keys(options?.icons ?? {}); - const missingIconKeys = Object.keys(appIcons).filter( + const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter( key => !providedIconKeys.includes(key), ); if (missingIconKeys.length > 0) { @@ -135,24 +131,16 @@ export function createApp(options?: AppOptions) { ); } - const apis = options?.apis ?? []; - const plugins = options?.plugins ?? []; - const components = { - ...defaultAppComponents(), - Router: BrowserRouter, - ThemeProvider: AppThemeProvider, - ...options?.components, - }; - const configLoader = options?.configLoader ?? defaultConfigLoader; + const { icons, themes, components } = optionsWithDefaults; return new PrivateAppImpl({ - apis, - icons: { ...appIcons, ...options?.icons }, - plugins: plugins as BackstagePlugin[], - components, - themes: options?.themes ?? defaultAppThemes(), - configLoader, + icons: icons!, + themes: themes!, + components: components! as AppComponents, defaultApis, + apis: options?.apis ?? [], bindRoutes: options?.bindRoutes, + plugins: (options?.plugins as BackstagePlugin[]) ?? [], + configLoader: options?.configLoader ?? defaultConfigLoader, }); } diff --git a/packages/core-components/src/appDefaults/AppThemeProvider.tsx b/packages/core-components/src/appDefaults/AppThemeProvider.tsx new file mode 100644 index 0000000000..aa48a2187d --- /dev/null +++ b/packages/core-components/src/appDefaults/AppThemeProvider.tsx @@ -0,0 +1,109 @@ +/* + * 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 React, { useMemo, useEffect, useState, PropsWithChildren } from 'react'; +import { ThemeProvider } from '@material-ui/core/styles'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api'; +import { useObservable } from 'react-use'; + +// This tries to find the most accurate match, but also falls back to less +// accurate results in order to avoid errors. +function resolveTheme( + themeId: string | undefined, + shouldPreferDark: boolean, + themes: AppTheme[], +) { + if (themeId !== undefined) { + const selectedTheme = themes.find(theme => theme.id === themeId); + if (selectedTheme) { + return selectedTheme; + } + } + + if (shouldPreferDark) { + const darkTheme = themes.find(theme => theme.variant === 'dark'); + if (darkTheme) { + return darkTheme; + } + } + + const lightTheme = themes.find(theme => theme.variant === 'light'); + if (lightTheme) { + return lightTheme; + } + + return themes[0]; +} + +const useShouldPreferDarkTheme = () => { + const mediaQuery = useMemo( + () => window.matchMedia('(prefers-color-scheme: dark)'), + [], + ); + const [shouldPreferDark, setPrefersDark] = useState(mediaQuery.matches); + + useEffect(() => { + const listener = (event: MediaQueryListEvent) => { + setPrefersDark(event.matches); + }; + mediaQuery.addListener(listener); + return () => { + mediaQuery.removeListener(listener); + }; + }, [mediaQuery]); + + return shouldPreferDark; +}; + +export function AppThemeProvider({ children }: PropsWithChildren<{}>) { + const appThemeApi = useApi(appThemeApiRef); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + + // Browser feature detection won't change over time, so ignore lint rule + const shouldPreferDark = Boolean(window.matchMedia) + ? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks + : false; + + const appTheme = resolveTheme( + themeId, + shouldPreferDark, + appThemeApi.getInstalledThemes(), + ); + if (!appTheme) { + throw new Error('App has no themes'); + } + + if (appTheme.Provider) { + return ; + } + + // eslint-disable-next-line no-console + console.warn( + "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + + 'and should be migrated to use a Provider instead. ' + + 'See https://backstage.io/docs/deprecations/TODO for more info.', + ); + + return ( + + {children} + + ); +} diff --git a/packages/core-components/src/defaultAppComponents.test.tsx b/packages/core-components/src/appDefaults/defaultAppComponents.test.tsx similarity index 100% rename from packages/core-components/src/defaultAppComponents.test.tsx rename to packages/core-components/src/appDefaults/defaultAppComponents.test.tsx diff --git a/packages/core-components/src/defaultAppComponents.tsx b/packages/core-components/src/appDefaults/defaultAppComponents.tsx similarity index 88% rename from packages/core-components/src/defaultAppComponents.tsx rename to packages/core-components/src/appDefaults/defaultAppComponents.tsx index fb810a6272..ab55a25840 100644 --- a/packages/core-components/src/defaultAppComponents.tsx +++ b/packages/core-components/src/appDefaults/defaultAppComponents.tsx @@ -16,14 +16,16 @@ import React, { ReactNode } from 'react'; import Button from '@material-ui/core/Button'; +import { ErrorPanel, Progress } from '../components'; +import { ErrorPage } from '../layout'; +import { MemoryRouter, useInRouterContext } from 'react-router'; +import { BrowserRouter } from 'react-router-dom'; import { AppComponents, BootErrorPageProps, ErrorBoundaryFallbackProps, } from '@backstage/core-plugin-api'; -import { ErrorPanel, Progress } from './components'; -import { ErrorPage } from './layout'; -import { MemoryRouter, useInRouterContext } from 'react-router'; +import { AppThemeProvider } from './AppThemeProvider'; export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { if (useInRouterContext()) { @@ -73,9 +75,11 @@ const DefaultErrorBoundaryFallback = ({ * * @public */ -export function defaultAppComponents(): Omit { +export function defaultAppComponents(): AppComponents { return { Progress, + Router: BrowserRouter, + ThemeProvider: AppThemeProvider, NotFoundErrorPage: DefaultNotFoundPage, BootErrorPage: DefaultBootErrorPage, ErrorBoundaryFallback: DefaultErrorBoundaryFallback, diff --git a/packages/core-components/src/defaultAppIcons.tsx b/packages/core-components/src/appDefaults/defaultAppIcons.tsx similarity index 100% rename from packages/core-components/src/defaultAppIcons.tsx rename to packages/core-components/src/appDefaults/defaultAppIcons.tsx diff --git a/packages/core-components/src/defaultAppThemes.tsx b/packages/core-components/src/appDefaults/defaultAppThemes.tsx similarity index 100% rename from packages/core-components/src/defaultAppThemes.tsx rename to packages/core-components/src/appDefaults/defaultAppThemes.tsx diff --git a/packages/core-components/src/appDefaults/index.ts b/packages/core-components/src/appDefaults/index.ts new file mode 100644 index 0000000000..287ad3cac6 --- /dev/null +++ b/packages/core-components/src/appDefaults/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { withDefaults } from './withDefaults'; diff --git a/packages/core-components/src/appDefaults/withDefaults.tsx b/packages/core-components/src/appDefaults/withDefaults.tsx new file mode 100644 index 0000000000..51adaf22ef --- /dev/null +++ b/packages/core-components/src/appDefaults/withDefaults.tsx @@ -0,0 +1,64 @@ +/* + * 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 { AppTheme, IconComponent } from '@backstage/core-plugin-api'; +// This is a bit of a hack that we use to avoid having to redeclare these types +// within this package or have an explicit dependency on core-app-api. +// These types end up being inlined and duplicated into this package at build time. +// eslint-disable-next-line no-restricted-imports +import { + AppIcons, + AppComponents, + AppOptions, +} from '../../../core-app-api/src/app'; +import { defaultAppComponents } from './defaultAppComponents'; +import { defaultAppIcons } from './defaultAppIcons'; +import { defaultAppThemes } from './defaultAppThemes'; + +interface OptionalAppOptions { + icons?: Partial & { + [key in string]: IconComponent; + }; + themes?: (Partial & Omit)[]; + components?: Partial; +} + +/** + * The options required by {@link @backstage/core-app-api#createApp}, but with + * many of the fields being optional + * + * @public + */ +type DefaultAppOptions = Omit & + OptionalAppOptions; + +/** + * Provides a set of default App options with the ability to override specific options. + * + * These options populate the theme, icons and components options of {@link @backstage/core-app-api#AppOptions}. + * + * @public + */ +export function withDefaults(options?: DefaultAppOptions): AppOptions { + const { themes, icons, components } = options ?? {}; + + return { + ...options, + themes: themes ?? defaultAppThemes(), + icons: { ...defaultAppIcons(), ...icons }, + components: { ...defaultAppComponents(), ...components }, + }; +} diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index 305783dec8..ea1fea9d5c 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -25,6 +25,4 @@ export * from './hooks'; export * from './icons'; export * from './layout'; export * from './overridableComponents'; -export { defaultAppComponents } from './defaultAppComponents'; -export { defaultAppIcons } from './defaultAppIcons'; -export { defaultAppThemes } from './defaultAppThemes'; +export * from './appDefaults'; diff --git a/packages/core-plugin-api/src/app/types.ts b/packages/core-plugin-api/src/app/types.ts index d072f0a2e6..df78728a21 100644 --- a/packages/core-plugin-api/src/app/types.ts +++ b/packages/core-plugin-api/src/app/types.ts @@ -14,110 +14,15 @@ * limitations under the License. */ -import { ComponentType } from 'react'; -import { ProfileInfo } from '../apis/definitions'; -import { IconComponent } from '../icons'; -import { BackstagePlugin } from '../plugin/types'; - -/** - * Props for the BootErrorPage. - * - * @public - */ -export type BootErrorPageProps = { - step: 'load-config' | 'load-chunk'; - error: Error; -}; - -/** - * Data and handlers associated with the user sign in event. - * - * @public - */ -export type SignInResult = { - /** - * User ID that will be returned by the IdentityApi - */ - userId: string; - - profile: ProfileInfo; - - /** - * Function used to retrieve an ID token for the signed in user. - */ - getIdToken?: () => Promise; - - /** - * Sign out handler that will be called if the user requests to sign out. - */ - signOut?: () => Promise; -}; - -/** - * Props for the SignInPage. - * - * @public - */ -export type SignInPageProps = { - /** - * Set the sign-in result for the app. This should only be called once. - */ - onResult(result: SignInResult): void; -}; - -/** - * Props for the ErrorBoundaryFallback. - * - * @public - */ -export type ErrorBoundaryFallbackProps = { - plugin?: BackstagePlugin; - error: Error; - resetError: () => void; -}; - -/** - * Basic app components. - * - * @public - */ -export type AppComponents = { - NotFoundErrorPage: ComponentType<{}>; - BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{}>; - ErrorBoundaryFallback: ComponentType; - - /** - * An optional sign-in page that will be rendered instead of the AppRouter at startup. - * - * If a sign-in page is set, it will always be shown before the app, and it is up - * to the sign-in page to handle e.g. saving of login methods for subsequent visits. - * - * The sign-in page will be displayed until it has passed up a result to the parent, - * and which point the AppRouter and all of its children will be rendered instead. - */ - SignInPage?: ComponentType; -}; - -/** - * Provides plugins and components registered in the app. - * - * @public - */ -export type AppContext = { - /** - * Get a list of all plugins that are installed in the app. - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: string): IconComponent | undefined; - - /** - * Get the components registered for various purposes in the app. - */ - getComponents(): AppComponents; -}; +// This is a bit of a hack that we use to avoid having to redeclare these types +// within this package or have an explicit dependency on core-app-api. +// These types end up being inlined and duplicated into this package at build time. +// eslint-disable-next-line no-restricted-imports +export type { + BootErrorPageProps, + SignInResult, + SignInPageProps, + ErrorBoundaryFallbackProps, + AppComponents, + AppContext, +} from '../../../core-app-api/src/app/types'; From 44976c138cc491d577386a551ccd58e747e112ab Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 19:46:26 +0200 Subject: [PATCH 05/35] core-app-api, create-app, dev-utils: updates to use withDefaults Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/createApp.tsx | 19 +++++---- .../default-app/packages/app/src/App.tsx | 39 +++++++++++-------- packages/dev-utils/src/devApp/render.tsx | 31 ++++++++------- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index c1e9741ff1..c3f477035f 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -98,10 +98,9 @@ export function createApp(options?: AppOptions) { 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() }). ' + + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of components to ' + + 'be provided. You can use the default components by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' + `The following components are missing: ${missingRequiredComponents.join( ', ', )}`, @@ -115,9 +114,9 @@ export function createApp(options?: AppOptions) { if (missingIconKeys.length > 0) { // eslint-disable-next-line no-console console.warn( - 'DEPRECATION WARNING: The createApp options will soon require all app icons to be provided.' + - 'These icons can be created using defaultAppIcons from @backstage/core-components ' + - 'and then passed along like this: createApp({ icons: ...defaultAppIcons() })' + + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' + + 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' + `The following icons are missing: ${missingIconKeys.join(', ')}`, ); } @@ -125,9 +124,9 @@ export function createApp(options?: AppOptions) { if (!options?.themes) { // eslint-disable-next-line no-console console.warn( - 'DEPRECATION WARNING: The createApp options will soon require themes to be provided. ' + - 'Themes can be created using defaultAppThemes from @backstage/core-components ' + - 'and then passed along like this: createApp({ theme: defaultAppThemes() })', + 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' + + 'You can use the default themes by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ', ); } 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 7117b3ea07..a6d34e6687 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,25 +25,30 @@ import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; -import { AlertDisplay, defaultAppComponents, OAuthRequestDialog } from '@backstage/core-components'; +import { + AlertDisplay, + withDefaults, + 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, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(apiDocsPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - }); - }, -}); +const app = createApp( + withDefaults({ + apis, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(apiDocsPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + }, + }), +); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 31b6ac6169..bcaac891fe 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -27,12 +27,12 @@ import { Route } from 'react-router'; import { AlertDisplay, - defaultAppComponents, OAuthRequestDialog, Sidebar, SidebarItem, SidebarPage, SidebarSpacer, + withDefaults, } from '@backstage/core-components'; import { @@ -174,21 +174,22 @@ export class DevAppBuilder { ); } - const app = createApp({ - apis, - plugins: this.plugins, - themes: this.themes, - components: defaultAppComponents(), - bindRoutes: ({ bind }) => { - for (const plugin of this.plugins ?? []) { - const targets: Record> = {}; - for (const routeKey of Object.keys(plugin.externalRoutes)) { - targets[routeKey] = dummyRouteRef; + const app = createApp( + withDefaults({ + apis, + plugins: this.plugins, + themes: this.themes, + bindRoutes: ({ bind }) => { + for (const plugin of this.plugins ?? []) { + const targets: Record> = {}; + for (const routeKey of Object.keys(plugin.externalRoutes)) { + targets[routeKey] = dummyRouteRef; + } + bind(plugin.externalRoutes, targets); } - bind(plugin.externalRoutes, targets); - } - }, - }); + }, + }), + ); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); From 2db99234966ab3e7c964de3d8acb4f21241ed350 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 19:45:52 +0200 Subject: [PATCH 06/35] changesets: updated changesets to use withDefaults Signed-off-by: Patrik Oldsberg --- .changeset/giant-drinks-wave.md | 2 +- .changeset/hot-walls-fail.md | 25 +++++++------------------ .changeset/late-rice-sit.md | 2 +- .changeset/twenty-swans-matter.md | 15 +++++++-------- 4 files changed, 16 insertions(+), 28 deletions(-) diff --git a/.changeset/giant-drinks-wave.md b/.changeset/giant-drinks-wave.md index ab9df136fc..4c2f754c2c 100644 --- a/.changeset/giant-drinks-wave.md +++ b/.changeset/giant-drinks-wave.md @@ -2,4 +2,4 @@ '@backstage/dev-utils': patch --- -Migrated to explicit passing of components to `createApp`. +Migrated to using `withDefaults` to pass defaults to `createApp`. diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md index 055613934d..c814354a56 100644 --- a/.changeset/hot-walls-fail.md +++ b/.changeset/hot-walls-fail.md @@ -2,32 +2,21 @@ '@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. +Migrated the app template use the new `withDefaults` to construct 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'; ++import { withDefaults } from '@backstage/core-components'; // ... - const app = createApp({ +-const app = createApp({ ++const app = createApp(withDefaults({ 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 index eb074cbb18..778215038f 100644 --- a/.changeset/late-rice-sit.md +++ b/.changeset/late-rice-sit.md @@ -2,4 +2,4 @@ '@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`. +Added a new `withDefaults` method that accepts a set of `AppOptions` and add the default components, themes and icons. It is intended to be used together with `createApp` from `@backstage/core-app-api` like this: `createApp(withDefaults({ ... }))`. diff --git a/.changeset/twenty-swans-matter.md b/.changeset/twenty-swans-matter.md index 0722913e05..6dea7a089f 100644 --- a/.changeset/twenty-swans-matter.md +++ b/.changeset/twenty-swans-matter.md @@ -2,15 +2,14 @@ '@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. +Deprecated the defaulting of the `components`, `icons` and `themes` options of `createApp`, meaning it will become required in the future. When not passing the required options a deprecation warning is currently logged, and they will become required in a future release. -The keep the existing components intact, migrate to using `defaultAppComponents` from `@backstage/core-components`: +The keep using the default set of options, migrate to using `withDefaults` from `@backstage/core-components`: ```ts -const app = createApp({ - components: { - ...defaultAppComponents(), - // Place any custom components here - }, -}); +const app = createApp( + withDefaults({ + // ... + }), +); ``` From ab73a5216796a7ee62ca8af078b8b79a34292208 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 19:59:47 +0200 Subject: [PATCH 07/35] core-*: update API reports Signed-off-by: Patrik Oldsberg --- packages/core-app-api/api-report.md | 27 ++++++++++++++++++++++++-- packages/core-components/api-report.md | 20 +++++++++++++++---- packages/core-plugin-api/api-report.md | 13 +++++++++---- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 8fb3d5c788..f517f284b4 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -164,6 +164,30 @@ export type AppContext = { getComponents(): AppComponents; }; +// Warning: (ae-missing-release-tag) "AppIcons" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + user: IconComponent; + warning: IconComponent; +}; + // @public export type AppOptions = { apis?: Iterable; @@ -172,7 +196,7 @@ export type AppOptions = { }; plugins?: BackstagePluginWithAnyOutput[]; components?: Partial; - themes?: AppTheme[]; + themes?: (Partial & Omit)[]; configLoader?: AppConfigLoader; bindRoutes?(context: { bind: AppRouteBinder }): void; }; @@ -608,5 +632,4 @@ export class WebStorage implements StorageApi { // Warnings were encountered during analysis: // // src/apis/system/ApiProvider.d.ts:15:5 - (ae-forgotten-export) The symbol "ApiProviderProps" needs to be exported by the entry point index.d.ts -// src/app/types.d.ts:152:5 - (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts ``` diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 63f35873f7..b0c3e2d9b2 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -5,21 +5,26 @@ ```ts /// +import { AnyApiFactory } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; -import { AppComponents } from '@backstage/core-plugin-api'; +import { AppConfig } from '@backstage/config'; +import { AppTheme } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; import { Column } from '@material-table/core'; import { ComponentClass } from 'react'; import { ComponentProps } from 'react'; +import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; +import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { LinearProgressProps } from '@material-ui/core/LinearProgress'; import { LinkProps as LinkProps_2 } from '@material-ui/core/Link'; @@ -28,12 +33,15 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Overrides } from '@material-ui/core/styles/overrides'; +import { PluginOutput } from '@backstage/core-plugin-api'; +import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import * as React_3 from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; @@ -41,6 +49,7 @@ import { SparklinesProps } from 'react-sparklines'; import { StyledComponentProps } from '@material-ui/core/styles'; import { StyleRules } from '@material-ui/styles'; import { StyleRules as StyleRules_2 } from '@material-ui/core/styles/withStyles'; +import { SubRouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core/Tab'; import { TextTruncateProps } from 'react-text-truncate'; import { Theme } from '@material-ui/core/styles'; @@ -200,9 +209,6 @@ 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; @@ -2322,6 +2328,12 @@ export type WarningPanelClassKey = | 'message' | 'details'; +// Warning: (ae-forgotten-export) The symbol "DefaultAppOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "AppOptions" needs to be exported by the entry point index.d.ts +// +// @public +export function withDefaults(options?: DefaultAppOptions): AppOptions; + // Warnings were encountered during analysis: // // src/components/DependencyGraph/types.d.ts:16:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index f0cee2a42c..433edc57ff 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -5,11 +5,14 @@ ```ts /// +import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; +import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; import { Observable as Observable_2 } from '@backstage/types'; import { Observer as Observer_2 } from '@backstage/types'; +import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; @@ -161,13 +164,14 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; + ThemeProvider: ComponentType<{}>; SignInPage?: ComponentType; }; // @public export type AppContext = { - getPlugins(): BackstagePlugin[]; - getSystemIcon(key: string): IconComponent | undefined; + getPlugins(): BackstagePlugin_2[]; + getSystemIcon(key: string): IconComponent_2 | undefined; getComponents(): AppComponents; }; @@ -178,6 +182,7 @@ export type AppTheme = { variant: 'light' | 'dark'; theme: BackstageTheme; icon?: React.ReactElement; + Provider?(props: { children: ReactNode }): JSX.Element | null; }; // @public @@ -421,7 +426,7 @@ export const errorApiRef: ApiRef; // @public export type ErrorBoundaryFallbackProps = { - plugin?: BackstagePlugin; + plugin?: BackstagePlugin_2; error: Error; resetError: () => void; }; @@ -741,7 +746,7 @@ export type SignInPageProps = { // @public export type SignInResult = { userId: string; - profile: ProfileInfo; + profile: ProfileInfo_2; getIdToken?: () => Promise; signOut?: () => Promise; }; From 7e0b26dec09f87b535406b9e99c8a082fc5b60b3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 21:02:52 +0200 Subject: [PATCH 08/35] core-app-api: remove duplicate AppThemeProvider Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/App.test.tsx | 6 +- .../core-app-api/src/app/AppThemeProvider.tsx | 109 ------------------ 2 files changed, 3 insertions(+), 112 deletions(-) delete mode 100644 packages/core-app-api/src/app/AppThemeProvider.tsx diff --git a/packages/core-app-api/src/app/App.test.tsx b/packages/core-app-api/src/app/App.test.tsx index 6ed5d45b86..198048f8cf 100644 --- a/packages/core-app-api/src/app/App.test.tsx +++ b/packages/core-app-api/src/app/App.test.tsx @@ -38,7 +38,7 @@ import { analyticsApiRef, } from '@backstage/core-plugin-api'; import { generateBoundRoutes, PrivateAppImpl } from './App'; -import { AppThemeProvider } from './AppThemeProvider'; +import { AppComponents } from './types'; describe('generateBoundRoutes', () => { it('runs happy path', () => { @@ -178,13 +178,13 @@ describe('Integration Test', () => { }), ); - const components = { + const components: AppComponents = { NotFoundErrorPage: () => null, BootErrorPage: () => null, Progress: () => null, Router: BrowserRouter, ErrorBoundaryFallback: () => null, - ThemeProvider: AppThemeProvider, + ThemeProvider: ({ children }) => <>{children}, }; it('runs happy paths', async () => { diff --git a/packages/core-app-api/src/app/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx deleted file mode 100644 index aa48a2187d..0000000000 --- a/packages/core-app-api/src/app/AppThemeProvider.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 React, { useMemo, useEffect, useState, PropsWithChildren } from 'react'; -import { ThemeProvider } from '@material-ui/core/styles'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api'; -import { useObservable } from 'react-use'; - -// This tries to find the most accurate match, but also falls back to less -// accurate results in order to avoid errors. -function resolveTheme( - themeId: string | undefined, - shouldPreferDark: boolean, - themes: AppTheme[], -) { - if (themeId !== undefined) { - const selectedTheme = themes.find(theme => theme.id === themeId); - if (selectedTheme) { - return selectedTheme; - } - } - - if (shouldPreferDark) { - const darkTheme = themes.find(theme => theme.variant === 'dark'); - if (darkTheme) { - return darkTheme; - } - } - - const lightTheme = themes.find(theme => theme.variant === 'light'); - if (lightTheme) { - return lightTheme; - } - - return themes[0]; -} - -const useShouldPreferDarkTheme = () => { - const mediaQuery = useMemo( - () => window.matchMedia('(prefers-color-scheme: dark)'), - [], - ); - const [shouldPreferDark, setPrefersDark] = useState(mediaQuery.matches); - - useEffect(() => { - const listener = (event: MediaQueryListEvent) => { - setPrefersDark(event.matches); - }; - mediaQuery.addListener(listener); - return () => { - mediaQuery.removeListener(listener); - }; - }, [mediaQuery]); - - return shouldPreferDark; -}; - -export function AppThemeProvider({ children }: PropsWithChildren<{}>) { - const appThemeApi = useApi(appThemeApiRef); - const themeId = useObservable( - appThemeApi.activeThemeId$(), - appThemeApi.getActiveThemeId(), - ); - - // Browser feature detection won't change over time, so ignore lint rule - const shouldPreferDark = Boolean(window.matchMedia) - ? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks - : false; - - const appTheme = resolveTheme( - themeId, - shouldPreferDark, - appThemeApi.getInstalledThemes(), - ); - if (!appTheme) { - throw new Error('App has no themes'); - } - - if (appTheme.Provider) { - return ; - } - - // eslint-disable-next-line no-console - console.warn( - "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + - 'and should be migrated to use a Provider instead. ' + - 'See https://backstage.io/docs/deprecations/TODO for more info.', - ); - - return ( - - {children} - - ); -} From 8921944da11d2f1d90c480c0e62a4d96455a6f1a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 Oct 2021 21:31:28 +0200 Subject: [PATCH 09/35] core-components: tweak exports and update api reports Signed-off-by: Patrik Oldsberg --- packages/core-app-api/api-report.md | 2 -- packages/core-app-api/src/app/icons.tsx | 1 + packages/core-components/api-report.md | 21 +++++++++++++-- .../core-components/src/appDefaults/index.ts | 1 + .../src/appDefaults/withDefaults.tsx | 26 ++++++++++--------- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f517f284b4..b4b1b07ebe 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -164,8 +164,6 @@ export type AppContext = { getComponents(): AppComponents; }; -// Warning: (ae-missing-release-tag) "AppIcons" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AppIcons = { 'kind:api': IconComponent; diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/core-app-api/src/app/icons.tsx index f06c9da989..1d9bb12923 100644 --- a/packages/core-app-api/src/app/icons.tsx +++ b/packages/core-app-api/src/app/icons.tsx @@ -35,6 +35,7 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; +/** @public */ export type AppIcons = { 'kind:api': IconComponent; 'kind:component': IconComponent; diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index b0c3e2d9b2..f517b1673b 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -681,6 +681,22 @@ export type OAuthRequestDialogClassKey = // @public (undocumented) export type OpenedDropdownClassKey = 'icon'; +// @public +export interface OptionalAppOptions { + // Warning: (ae-forgotten-export) The symbol "AppComponents" needs to be exported by the entry point index.d.ts + // + // (undocumented) + components?: Partial; + // Warning: (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts + // + // (undocumented) + icons?: Partial & { + [key in string]: IconComponent; + }; + // (undocumented) + themes?: (Partial & Omit)[]; +} + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OverflowTooltip" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2328,11 +2344,12 @@ export type WarningPanelClassKey = | 'message' | 'details'; -// Warning: (ae-forgotten-export) The symbol "DefaultAppOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "AppOptions" needs to be exported by the entry point index.d.ts // // @public -export function withDefaults(options?: DefaultAppOptions): AppOptions; +export function withDefaults( + options?: Omit & OptionalAppOptions, +): AppOptions; // Warnings were encountered during analysis: // diff --git a/packages/core-components/src/appDefaults/index.ts b/packages/core-components/src/appDefaults/index.ts index 287ad3cac6..6ccc6a6257 100644 --- a/packages/core-components/src/appDefaults/index.ts +++ b/packages/core-components/src/appDefaults/index.ts @@ -15,3 +15,4 @@ */ export { withDefaults } from './withDefaults'; +export type { OptionalAppOptions } from './withDefaults'; diff --git a/packages/core-components/src/appDefaults/withDefaults.tsx b/packages/core-components/src/appDefaults/withDefaults.tsx index 51adaf22ef..9254053404 100644 --- a/packages/core-components/src/appDefaults/withDefaults.tsx +++ b/packages/core-components/src/appDefaults/withDefaults.tsx @@ -28,22 +28,22 @@ import { defaultAppComponents } from './defaultAppComponents'; import { defaultAppIcons } from './defaultAppIcons'; import { defaultAppThemes } from './defaultAppThemes'; -interface OptionalAppOptions { - icons?: Partial & { - [key in string]: IconComponent; - }; - themes?: (Partial & Omit)[]; - components?: Partial; -} +// NOTE: we don't re-export any of the types imported from core-app-api, as we +// want them to be imported from there rather than core-components. /** - * The options required by {@link @backstage/core-app-api#createApp}, but with - * many of the fields being optional + * The set of app options that will be populated by {@link withDefaults} if they + * are not passed in explicitly. * * @public */ -type DefaultAppOptions = Omit & - OptionalAppOptions; +export interface OptionalAppOptions { + icons?: Partial & { + [key in string]: IconComponent; + }; + themes?: (Partial & Omit)[]; // TODO: simplify once AppTheme is updated + components?: Partial; +} /** * Provides a set of default App options with the ability to override specific options. @@ -52,7 +52,9 @@ type DefaultAppOptions = Omit & * * @public */ -export function withDefaults(options?: DefaultAppOptions): AppOptions { +export function withDefaults( + options?: Omit & OptionalAppOptions, +): AppOptions { const { themes, icons, components } = options ?? {}; return { From a82aa54e272b03fb8b8d713909d5895f2b45e5f0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 19:10:40 +0200 Subject: [PATCH 10/35] packages: new app-defaults package + move over withDefaults Signed-off-by: Patrik Oldsberg --- packages/app-defaults/.eslintrc.js | 3 + packages/app-defaults/README.md | 17 + packages/app-defaults/package.json | 64 ++++ .../src/createApp}/AppThemeProvider.tsx | 0 .../src/createApp/createApp.test.tsx | 119 +++++++ .../app-defaults/src/createApp/createApp.tsx | 145 ++++++++ .../app-defaults/src/createApp/defaultApis.ts | 264 ++++++++++++++ .../createApp}/defaultAppComponents.test.tsx | 0 .../src/createApp}/defaultAppComponents.tsx | 0 .../src/createApp}/defaultAppIcons.tsx | 0 .../src/createApp}/defaultAppThemes.tsx | 0 packages/app-defaults/src/createApp/icons.tsx | 78 ++++ packages/app-defaults/src/createApp/index.ts | 19 + packages/app-defaults/src/createApp/types.ts | 332 ++++++++++++++++++ .../src/createApp}/withDefaults.tsx | 0 packages/app-defaults/src/index.ts | 23 ++ .../src/setupTests.ts} | 6 +- packages/core-components/src/index.ts | 1 - 18 files changed, 1067 insertions(+), 4 deletions(-) create mode 100644 packages/app-defaults/.eslintrc.js create mode 100644 packages/app-defaults/README.md create mode 100644 packages/app-defaults/package.json rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/AppThemeProvider.tsx (100%) create mode 100644 packages/app-defaults/src/createApp/createApp.test.tsx create mode 100644 packages/app-defaults/src/createApp/createApp.tsx create mode 100644 packages/app-defaults/src/createApp/defaultApis.ts rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/defaultAppComponents.test.tsx (100%) rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/defaultAppComponents.tsx (100%) rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/defaultAppIcons.tsx (100%) rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/defaultAppThemes.tsx (100%) create mode 100644 packages/app-defaults/src/createApp/icons.tsx create mode 100644 packages/app-defaults/src/createApp/index.ts create mode 100644 packages/app-defaults/src/createApp/types.ts rename packages/{core-components/src/appDefaults => app-defaults/src/createApp}/withDefaults.tsx (100%) create mode 100644 packages/app-defaults/src/index.ts rename packages/{core-components/src/appDefaults/index.ts => app-defaults/src/setupTests.ts} (79%) diff --git a/packages/app-defaults/.eslintrc.js b/packages/app-defaults/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/app-defaults/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/app-defaults/README.md b/packages/app-defaults/README.md new file mode 100644 index 0000000000..15a0bd3d24 --- /dev/null +++ b/packages/app-defaults/README.md @@ -0,0 +1,17 @@ +# @backstage/app-defaults + +This package provides a default wiring of a Backstage app that avoids boilerplate when setting up a standard Backstage app. + +## Installation + +Install the package via Yarn: + +```sh +cd packages/app +yarn add @backstage/app-defaults +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://backstage.io/docs) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json new file mode 100644 index 0000000000..c7594470d6 --- /dev/null +++ b/packages/app-defaults/package.json @@ -0,0 +1,64 @@ +{ + "name": "@backstage/app-defaults", + "description": "Provides the default wiring of a Backstage App", + "version": "0.1.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/app-defaults" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "build": "backstage-cli build --outputs types,esm", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core-components": "^0.7.1", + "@backstage/config": "^0.1.10", + "@backstage/core-plugin-api": "^0.1.11", + "@backstage/theme": "^0.2.11", + "@backstage/types": "^0.1.1", + "@backstage/version-bridge": "^0.1.0", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@types/react": "*", + "@types/prop-types": "^15.7.3", + "prop-types": "^15.7.2", + "react": "^16.12.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^17.2.4", + "zen-observable": "^0.8.15" + }, + "devDependencies": { + "@backstage/cli": "^0.8.0", + "@backstage/test-utils": "^0.1.19", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/react-hooks": "^7.0.2", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "@types/zen-observable": "^0.8.0", + "cross-fetch": "^3.0.6", + "msw": "^0.29.0" + }, + "files": [ + "dist" + ] +} diff --git a/packages/core-components/src/appDefaults/AppThemeProvider.tsx b/packages/app-defaults/src/createApp/AppThemeProvider.tsx similarity index 100% rename from packages/core-components/src/appDefaults/AppThemeProvider.tsx rename to packages/app-defaults/src/createApp/AppThemeProvider.tsx diff --git a/packages/app-defaults/src/createApp/createApp.test.tsx b/packages/app-defaults/src/createApp/createApp.test.tsx new file mode 100644 index 0000000000..0bf90987db --- /dev/null +++ b/packages/app-defaults/src/createApp/createApp.test.tsx @@ -0,0 +1,119 @@ +/* + * 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 { screen } from '@testing-library/react'; +import { renderWithEffects } from '@backstage/test-utils'; +import React, { PropsWithChildren } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { defaultConfigLoader, createApp } from './createApp'; + +(process as any).env = { NODE_ENV: 'test' }; +const anyEnv = process.env as any; +const anyWindow = window as any; + +describe('defaultConfigLoader', () => { + afterEach(() => { + delete anyEnv.APP_CONFIG; + delete anyWindow.__APP_CONFIG__; + }); + + it('loads static config', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + + const configs = await defaultConfigLoader(); + expect(configs).toEqual([ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]); + }); + + it('loads runtime config', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + ]; + + const configs = await (defaultConfigLoader as any)( + '{"my":"runtime-config"}', + ); + expect(configs).toEqual([ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + { data: { my: 'runtime-config' }, context: 'env' }, + ]); + }); + + it('fails to load invalid missing config', async () => { + await expect(defaultConfigLoader()).rejects.toThrow( + 'No static configuration provided', + ); + }); + + it('fails to load invalid static config', async () => { + anyEnv.APP_CONFIG = { my: 'invalid-config' }; + await expect(defaultConfigLoader()).rejects.toThrow( + 'Static configuration has invalid format', + ); + }); + + it('fails to load bad runtime config', async () => { + anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; + + await expect((defaultConfigLoader as any)('}')).rejects.toThrow( + 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', + ); + }); + + it('loads config from window.__APP_CONFIG__', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + const windowConfig = { app: { configKey: 'config-value' } }; + anyWindow.__APP_CONFIG__ = windowConfig; + + const configs = await defaultConfigLoader(); + + expect(configs).toEqual([ + ...anyEnv.APP_CONFIG, + { context: 'window', data: windowConfig }, + ]); + }); +}); + +describe('Optional ThemeProvider', () => { + it('should render app with user-provided ThemeProvider', async () => { + const components = { + NotFoundErrorPage: () => null, + BootErrorPage: () => null, + Progress: () => null, + Router: MemoryRouter, + ErrorBoundaryFallback: () => null, + ThemeProvider: ({ children }: PropsWithChildren<{}>) => ( +
{children}
+ ), + }; + + const App = createApp({ components }).getProvider(); + + await renderWithEffects(); + + expect(screen.getByRole('main')).toBeInTheDocument(); + }); +}); diff --git a/packages/app-defaults/src/createApp/createApp.tsx b/packages/app-defaults/src/createApp/createApp.tsx new file mode 100644 index 0000000000..c3f477035f --- /dev/null +++ b/packages/app-defaults/src/createApp/createApp.tsx @@ -0,0 +1,145 @@ +/* + * 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 { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { withDefaults } from '@backstage/core-components'; +import { PrivateAppImpl } from './App'; +import { AppComponents, AppConfigLoader, AppOptions } from './types'; +import { defaultApis } from './defaultApis'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; + +const REQUIRED_APP_COMPONENTS: Array = [ + 'Progress', + 'Router', + 'NotFoundErrorPage', + 'BootErrorPage', + 'ErrorBoundaryFallback', +]; + +/** + * The default config loader, which expects that config is available at compile-time + * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as + * returned by the config loader. + * + * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, + * which can be rewritten at runtime to contain an additional JSON config object. + * If runtime config is present, it will be placed first in the config array, overriding + * other config values. + * + * @public + */ +export const defaultConfigLoader: AppConfigLoader = async ( + // This string may be replaced at runtime to provide additional config. + // It should be replaced by a JSON-serialized config object. + // It's a param so we can test it, but at runtime this will always fall back to default. + runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', +) => { + const appConfig = process.env.APP_CONFIG; + if (!appConfig) { + throw new Error('No static configuration provided'); + } + if (!Array.isArray(appConfig)) { + throw new Error('Static configuration has invalid format'); + } + const configs = appConfig.slice() as unknown as AppConfig[]; + + // Avoiding this string also being replaced at runtime + if ( + runtimeConfigJson !== + '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') + ) { + try { + const data = JSON.parse(runtimeConfigJson) as JsonObject; + if (Array.isArray(data)) { + configs.push(...data); + } else { + configs.push({ data, context: 'env' }); + } + } catch (error) { + throw new Error(`Failed to load runtime configuration, ${error}`); + } + } + + const windowAppConfig = (window as any).__APP_CONFIG__; + if (windowAppConfig) { + configs.push({ + context: 'window', + data: windowAppConfig, + }); + } + return configs; +}; + +/** + * Creates a new Backstage App. + * + * @public + */ +export function createApp(options?: AppOptions) { + const optionsWithDefaults = withDefaults(options); + + const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( + name => !options?.components?.[name], + ); + 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. You can use the default components by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' + + `The following components are missing: ${missingRequiredComponents.join( + ', ', + )}`, + ); + } + + const providedIconKeys = Object.keys(options?.icons ?? {}); + const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter( + key => !providedIconKeys.includes(key), + ); + if (missingIconKeys.length > 0) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' + + 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' + + `The following icons are missing: ${missingIconKeys.join(', ')}`, + ); + } + + if (!options?.themes) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' + + 'You can use the default themes by using withDefaults from @backstage/core-components ' + + 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ', + ); + } + + const { icons, themes, components } = optionsWithDefaults; + + return new PrivateAppImpl({ + icons: icons!, + themes: themes!, + components: components! as AppComponents, + defaultApis, + apis: options?.apis ?? [], + bindRoutes: options?.bindRoutes, + plugins: (options?.plugins as BackstagePlugin[]) ?? [], + configLoader: options?.configLoader ?? defaultConfigLoader, + }); +} diff --git a/packages/app-defaults/src/createApp/defaultApis.ts b/packages/app-defaults/src/createApp/defaultApis.ts new file mode 100644 index 0000000000..fb02274cf0 --- /dev/null +++ b/packages/app-defaults/src/createApp/defaultApis.ts @@ -0,0 +1,264 @@ +/* + * 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 { + AlertApiForwarder, + NoOpAnalyticsApi, + ErrorApiForwarder, + ErrorAlerter, + GoogleAuth, + GithubAuth, + OAuth2, + OktaAuth, + GitlabAuth, + Auth0Auth, + MicrosoftAuth, + BitbucketAuth, + OAuthRequestManager, + WebStorage, + UrlPatternDiscovery, + SamlAuth, + OneLoginAuth, + UnhandledErrorForwarder, + AtlassianAuth, +} from '../apis'; + +import { + createApiFactory, + alertApiRef, + analyticsApiRef, + errorApiRef, + discoveryApiRef, + oauthRequestApiRef, + googleAuthApiRef, + githubAuthApiRef, + oauth2ApiRef, + oktaAuthApiRef, + gitlabAuthApiRef, + auth0AuthApiRef, + microsoftAuthApiRef, + storageApiRef, + configApiRef, + samlAuthApiRef, + oneloginAuthApiRef, + oidcAuthApiRef, + bitbucketAuthApiRef, + atlassianAuthApiRef, +} from '@backstage/core-plugin-api'; + +import OAuth2Icon from '@material-ui/icons/AcUnit'; + +export const defaultApis = [ + createApiFactory({ + api: discoveryApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + UrlPatternDiscovery.compile( + `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, + ), + }), + createApiFactory(alertApiRef, new AlertApiForwarder()), + createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()), + createApiFactory({ + api: errorApiRef, + deps: { alertApi: alertApiRef }, + factory: ({ alertApi }) => { + const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); + UnhandledErrorForwarder.forward(errorApi, { hidden: false }); + return errorApi; + }, + }), + createApiFactory({ + api: storageApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => WebStorage.create({ errorApi }), + }), + createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), + createApiFactory({ + api: googleAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GoogleAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: microsoftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + MicrosoftAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: githubAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oktaAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OktaAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: gitlabAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GitlabAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: auth0AuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + Auth0Auth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oauth2ApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: samlAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, configApi }) => + SamlAuth.create({ + discoveryApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oneloginAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OneLoginAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oidcAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: OAuth2Icon, + }, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: bitbucketAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + BitbucketAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['team'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: atlassianAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return AtlassianAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), +]; diff --git a/packages/core-components/src/appDefaults/defaultAppComponents.test.tsx b/packages/app-defaults/src/createApp/defaultAppComponents.test.tsx similarity index 100% rename from packages/core-components/src/appDefaults/defaultAppComponents.test.tsx rename to packages/app-defaults/src/createApp/defaultAppComponents.test.tsx diff --git a/packages/core-components/src/appDefaults/defaultAppComponents.tsx b/packages/app-defaults/src/createApp/defaultAppComponents.tsx similarity index 100% rename from packages/core-components/src/appDefaults/defaultAppComponents.tsx rename to packages/app-defaults/src/createApp/defaultAppComponents.tsx diff --git a/packages/core-components/src/appDefaults/defaultAppIcons.tsx b/packages/app-defaults/src/createApp/defaultAppIcons.tsx similarity index 100% rename from packages/core-components/src/appDefaults/defaultAppIcons.tsx rename to packages/app-defaults/src/createApp/defaultAppIcons.tsx diff --git a/packages/core-components/src/appDefaults/defaultAppThemes.tsx b/packages/app-defaults/src/createApp/defaultAppThemes.tsx similarity index 100% rename from packages/core-components/src/appDefaults/defaultAppThemes.tsx rename to packages/app-defaults/src/createApp/defaultAppThemes.tsx diff --git a/packages/app-defaults/src/createApp/icons.tsx b/packages/app-defaults/src/createApp/icons.tsx new file mode 100644 index 0000000000..df1e69da6a --- /dev/null +++ b/packages/app-defaults/src/createApp/icons.tsx @@ -0,0 +1,78 @@ +/* + * 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 { IconComponent } from '@backstage/core-plugin-api'; +import MuiApartmentIcon from '@material-ui/icons/Apartment'; +import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; +import MuiCategoryIcon from '@material-ui/icons/Category'; +import MuiChatIcon from '@material-ui/icons/Chat'; +import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiDocsIcon from '@material-ui/icons/Description'; +import MuiEmailIcon from '@material-ui/icons/Email'; +import MuiExtensionIcon from '@material-ui/icons/Extension'; +import MuiGitHubIcon from '@material-ui/icons/GitHub'; +import MuiHelpIcon from '@material-ui/icons/Help'; +import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; +import MuiMemoryIcon from '@material-ui/icons/Memory'; +import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; +import MuiPeopleIcon from '@material-ui/icons/People'; +import MuiPersonIcon from '@material-ui/icons/Person'; +import MuiWarningIcon from '@material-ui/icons/Warning'; + +/** @public */ +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; + + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + user: IconComponent; + warning: IconComponent; +}; + +export const defaultAppIcons: AppIcons = { + brokenImage: MuiBrokenImageIcon, + // To be confirmed: see https://github.com/backstage/backstage/issues/4970 + catalog: MuiMenuBookIcon, + chat: MuiChatIcon, + dashboard: MuiDashboardIcon, + docs: MuiDocsIcon, + email: MuiEmailIcon, + github: MuiGitHubIcon, + group: MuiPeopleIcon, + help: MuiHelpIcon, + 'kind:api': MuiExtensionIcon, + 'kind:component': MuiMemoryIcon, + 'kind:domain': MuiApartmentIcon, + 'kind:group': MuiPeopleIcon, + 'kind:location': MuiLocationOnIcon, + 'kind:system': MuiCategoryIcon, + 'kind:user': MuiPersonIcon, + user: MuiPersonIcon, + warning: MuiWarningIcon, +}; diff --git a/packages/app-defaults/src/createApp/index.ts b/packages/app-defaults/src/createApp/index.ts new file mode 100644 index 0000000000..2a16a90bfa --- /dev/null +++ b/packages/app-defaults/src/createApp/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export { createApp, defaultConfigLoader } from './createApp'; +export type { AppIcons } from './icons'; +export * from './types'; diff --git a/packages/app-defaults/src/createApp/types.ts b/packages/app-defaults/src/createApp/types.ts new file mode 100644 index 0000000000..78ef277c90 --- /dev/null +++ b/packages/app-defaults/src/createApp/types.ts @@ -0,0 +1,332 @@ +/* + * 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 { ComponentType } from 'react'; +import { + AnyApiFactory, + AppTheme, + ProfileInfo, + IconComponent, + BackstagePlugin, + RouteRef, + SubRouteRef, + ExternalRouteRef, + PluginOutput, +} from '@backstage/core-plugin-api'; +import { AppConfig } from '@backstage/config'; +import { AppIcons } from './icons'; + +/** + * Props for the `BootErrorPage` component of {@link AppComponents}. + * + * @public + */ +export type BootErrorPageProps = { + step: 'load-config' | 'load-chunk'; + error: Error; +}; + +/** + * The outcome of signing in on the sign-in page. + * + * @public + */ +export type SignInResult = { + /** + * User ID that will be returned by the IdentityApi + */ + userId: string; + + profile: ProfileInfo; + + /** + * Function used to retrieve an ID token for the signed in user. + */ + getIdToken?: () => Promise; + + /** + * Sign out handler that will be called if the user requests to sign out. + */ + signOut?: () => Promise; +}; + +/** + * Props for the `SignInPage` component of {@link AppComponents}. + * + * @public + */ +export type SignInPageProps = { + /** + * Set the sign-in result for the app. This should only be called once. + */ + onResult(result: SignInResult): void; +}; + +/** + * Props for the fallback error boundary. + * + * @public + */ +export type ErrorBoundaryFallbackProps = { + plugin?: BackstagePlugin; + error: Error; + resetError: () => void; +}; + +/** + * A set of replaceable core components that are part of every Backstage app. + * + * @public + */ +export type AppComponents = { + NotFoundErrorPage: ComponentType<{}>; + BootErrorPage: ComponentType; + Progress: ComponentType<{}>; + Router: ComponentType<{}>; + ErrorBoundaryFallback: ComponentType; + ThemeProvider: ComponentType<{}>; + + /** + * An optional sign-in page that will be rendered instead of the AppRouter at startup. + * + * If a sign-in page is set, it will always be shown before the app, and it is up + * to the sign-in page to handle e.g. saving of login methods for subsequent visits. + * + * The sign-in page will be displayed until it has passed up a result to the parent, + * and which point the AppRouter and all of its children will be rendered instead. + */ + SignInPage?: ComponentType; +}; + +/** + * A function that loads in the App config that will be accessible via the ConfigApi. + * + * If multiple config objects are returned in the array, values in the earlier configs + * will override later ones. + * + * @public + */ +export type AppConfigLoader = () => Promise; + +/** + * Extracts a union of the keys in a map whose value extends the given type + */ +type KeysWithType = { + [key in keyof Obj]: Obj[key] extends Type ? key : never; +}[keyof Obj]; + +/** + * Takes a map Map required values and makes all keys matching Keys optional + */ +type PartialKeys< + Map extends { [name in string]: any }, + Keys extends keyof Map, +> = Partial> & Required>; + +/** + * Creates a map of target routes with matching parameters based on a map of external routes. + */ +type TargetRouteMap< + ExternalRoutes extends { [name: string]: ExternalRouteRef }, +> = { + [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< + infer Params, + any + > + ? RouteRef | SubRouteRef + : never; +}; + +/** + * A function that can bind from external routes of a given plugin, to concrete + * routes of other plugins. See {@link createApp}. + * + * @public + */ +export type AppRouteBinder = < + ExternalRoutes extends { [name: string]: ExternalRouteRef }, +>( + externalRoutes: ExternalRoutes, + targetRoutes: PartialKeys< + TargetRouteMap, + KeysWithType> + >, +) => void; + +/** + * Internal helper type that represents a plugin with any type of output. + * + * @public + * @remarks + * + * The `type: string` type is there to handle output from newer or older plugin + * API versions that might not be supported by this version of the app API, but + * we don't want to break at the type checking level. We only use this more + * permissive type for the `createApp` options, as we otherwise want to stick + * to using the type for the outputs that we know about in this version of the + * app api. + * + * TODO(freben): This should be marked internal but that's not supported by the api report generation tools yet + */ +export type BackstagePluginWithAnyOutput = Omit< + BackstagePlugin, + 'output' +> & { + output(): (PluginOutput | { type: string })[]; +}; + +/** + * The options accepted by {@link createApp}. + * + * @public + */ +export type AppOptions = { + /** + * A collection of ApiFactories to register in the application to either + * add add new ones, or override factories provided by default or by plugins. + */ + apis?: Iterable; + + /** + * Supply icons to override the default ones. + */ + icons?: Partial & { [key in string]: IconComponent }; + + /** + * A list of all plugins to include in the app. + */ + plugins?: BackstagePluginWithAnyOutput[]; + + /** + * Supply components to the app to override the default ones. + */ + components?: Partial; + + /** + * Themes provided as a part of the app. By default two themes are included, one + * light variant of the default backstage theme, and one dark. + * + * This is the default config: + * + * ``` + * [{ + * id: 'light', + * title: 'Light Theme', + * variant: 'light', + * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), + * }, { + * id: 'dark', + * title: 'Dark Theme', + * variant: 'dark', + * icon: , + * Provider: ({ children }) => ( + * + * {children} + * + * ), + * }] + * ``` + */ + themes?: (Partial & Omit)[]; + + /** + * A function that loads in App configuration that will be accessible via + * the ConfigApi. + * + * Defaults to an empty config. + * + * TODO(Rugvip): Omitting this should instead default to loading in configuration + * that was packaged by the backstage-cli and default docker container boot script. + */ + configLoader?: AppConfigLoader; + + /** + * A function that is used to register associations between cross-plugin route + * references, enabling plugins to navigate between each other. + * + * The `bind` function that is passed in should be used to bind all external + * routes of all used plugins. + * + * ```ts + * bindRoutes({ bind }) { + * bind(docsPlugin.externalRoutes, { + * homePage: managePlugin.routes.managePage, + * }) + * bind(homePagePlugin.externalRoutes, { + * settingsPage: settingsPlugin.routes.settingsPage, + * }) + * } + * ``` + */ + bindRoutes?(context: { bind: AppRouteBinder }): void; +}; + +/** + * The public API of the output of {@link createApp}. + * + * @public + */ +export type BackstageApp = { + /** + * Returns all plugins registered for the app. + */ + getPlugins(): BackstagePlugin[]; + + /** + * Get a common or custom icon for this app. + */ + getSystemIcon(key: string): IconComponent | undefined; + + /** + * Provider component that should wrap the Router created with getRouter() + * and any other components that need to be within the app context. + */ + getProvider(): ComponentType<{}>; + + /** + * Router component that should wrap the App Routes create with getRoutes() + * and any other components that should only be available while signed in. + */ + getRouter(): ComponentType<{}>; +}; + +/** + * The central context providing runtime app specific state that plugin views + * want to consume. + * + * @public + */ +export type AppContext = { + /** + * Get a list of all plugins that are installed in the app. + */ + getPlugins(): BackstagePlugin[]; + + /** + * Get a common or custom icon for this app. + */ + getSystemIcon(key: string): IconComponent | undefined; + + /** + * Get the components registered for various purposes in the app. + */ + getComponents(): AppComponents; +}; diff --git a/packages/core-components/src/appDefaults/withDefaults.tsx b/packages/app-defaults/src/createApp/withDefaults.tsx similarity index 100% rename from packages/core-components/src/appDefaults/withDefaults.tsx rename to packages/app-defaults/src/createApp/withDefaults.tsx diff --git a/packages/app-defaults/src/index.ts b/packages/app-defaults/src/index.ts new file mode 100644 index 0000000000..62ef4d9077 --- /dev/null +++ b/packages/app-defaults/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Provides the default wiring of a Backstage App + * + * @packageDocumentation + */ + +export * from './createApp'; diff --git a/packages/core-components/src/appDefaults/index.ts b/packages/app-defaults/src/setupTests.ts similarity index 79% rename from packages/core-components/src/appDefaults/index.ts rename to packages/app-defaults/src/setupTests.ts index 6ccc6a6257..c1d649f2ad 100644 --- a/packages/core-components/src/appDefaults/index.ts +++ b/packages/app-defaults/src/setupTests.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -14,5 +14,5 @@ * limitations under the License. */ -export { withDefaults } from './withDefaults'; -export type { OptionalAppOptions } from './withDefaults'; +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/packages/core-components/src/index.ts b/packages/core-components/src/index.ts index ea1fea9d5c..3c5e708360 100644 --- a/packages/core-components/src/index.ts +++ b/packages/core-components/src/index.ts @@ -25,4 +25,3 @@ export * from './hooks'; export * from './icons'; export * from './layout'; export * from './overridableComponents'; -export * from './appDefaults'; From 60d1b30c215d6a0e7843447b7833229d7a7769e2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 19:35:39 +0200 Subject: [PATCH 11/35] app-defaults: refactor things into folders and move things closer to home Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 1 + .../{ => components}/AppThemeProvider.tsx | 0 .../src/createApp/createApp.test.tsx | 79 +---- .../app-defaults/src/createApp/createApp.tsx | 183 ++++------ .../{defaultApis.ts => defaults/apis.ts} | 4 +- .../components.test.tsx} | 2 +- .../components.tsx} | 31 +- .../createApp/defaults/configLoader.test.ts | 92 +++++ .../src/createApp/defaults/configLoader.ts | 73 ++++ .../icons.tsx} | 2 +- .../src/createApp/defaults/index.ts | 21 ++ .../themes.tsx} | 54 ++- packages/app-defaults/src/createApp/icons.tsx | 78 ---- packages/app-defaults/src/createApp/index.ts | 4 +- packages/app-defaults/src/createApp/types.ts | 332 ------------------ .../src/createApp/withDefaults.tsx | 66 ---- 16 files changed, 301 insertions(+), 721 deletions(-) rename packages/app-defaults/src/createApp/{ => components}/AppThemeProvider.tsx (100%) rename packages/app-defaults/src/createApp/{defaultApis.ts => defaults/apis.ts} (99%) rename packages/app-defaults/src/createApp/{defaultAppComponents.test.tsx => defaults/components.test.tsx} (95%) rename packages/app-defaults/src/createApp/{defaultAppComponents.tsx => defaults/components.tsx} (79%) create mode 100644 packages/app-defaults/src/createApp/defaults/configLoader.test.ts create mode 100644 packages/app-defaults/src/createApp/defaults/configLoader.ts rename packages/app-defaults/src/createApp/{defaultAppIcons.tsx => defaults/icons.tsx} (98%) create mode 100644 packages/app-defaults/src/createApp/defaults/index.ts rename packages/app-defaults/src/createApp/{defaultAppThemes.tsx => defaults/themes.tsx} (59%) delete mode 100644 packages/app-defaults/src/createApp/icons.tsx delete mode 100644 packages/app-defaults/src/createApp/types.ts delete mode 100644 packages/app-defaults/src/createApp/withDefaults.tsx diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index c7594470d6..19f4e1432e 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -31,6 +31,7 @@ "dependencies": { "@backstage/core-components": "^0.7.1", "@backstage/config": "^0.1.10", + "@backstage/core-app-api": "^0.1.11", "@backstage/core-plugin-api": "^0.1.11", "@backstage/theme": "^0.2.11", "@backstage/types": "^0.1.1", diff --git a/packages/app-defaults/src/createApp/AppThemeProvider.tsx b/packages/app-defaults/src/createApp/components/AppThemeProvider.tsx similarity index 100% rename from packages/app-defaults/src/createApp/AppThemeProvider.tsx rename to packages/app-defaults/src/createApp/components/AppThemeProvider.tsx diff --git a/packages/app-defaults/src/createApp/createApp.test.tsx b/packages/app-defaults/src/createApp/createApp.test.tsx index 0bf90987db..45988a4840 100644 --- a/packages/app-defaults/src/createApp/createApp.test.tsx +++ b/packages/app-defaults/src/createApp/createApp.test.tsx @@ -18,84 +18,7 @@ import { screen } from '@testing-library/react'; import { renderWithEffects } from '@backstage/test-utils'; import React, { PropsWithChildren } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { defaultConfigLoader, createApp } from './createApp'; - -(process as any).env = { NODE_ENV: 'test' }; -const anyEnv = process.env as any; -const anyWindow = window as any; - -describe('defaultConfigLoader', () => { - afterEach(() => { - delete anyEnv.APP_CONFIG; - delete anyWindow.__APP_CONFIG__; - }); - - it('loads static config', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]; - - const configs = await defaultConfigLoader(); - expect(configs).toEqual([ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]); - }); - - it('loads runtime config', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - ]; - - const configs = await (defaultConfigLoader as any)( - '{"my":"runtime-config"}', - ); - expect(configs).toEqual([ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - { data: { my: 'runtime-config' }, context: 'env' }, - ]); - }); - - it('fails to load invalid missing config', async () => { - await expect(defaultConfigLoader()).rejects.toThrow( - 'No static configuration provided', - ); - }); - - it('fails to load invalid static config', async () => { - anyEnv.APP_CONFIG = { my: 'invalid-config' }; - await expect(defaultConfigLoader()).rejects.toThrow( - 'Static configuration has invalid format', - ); - }); - - it('fails to load bad runtime config', async () => { - anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; - - await expect((defaultConfigLoader as any)('}')).rejects.toThrow( - 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', - ); - }); - - it('loads config from window.__APP_CONFIG__', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]; - const windowConfig = { app: { configKey: 'config-value' } }; - anyWindow.__APP_CONFIG__ = windowConfig; - - const configs = await defaultConfigLoader(); - - expect(configs).toEqual([ - ...anyEnv.APP_CONFIG, - { context: 'window', data: windowConfig }, - ]); - }); -}); +import { createApp } from './createApp'; describe('Optional ThemeProvider', () => { it('should render app with user-provided ThemeProvider', async () => { diff --git a/packages/app-defaults/src/createApp/createApp.tsx b/packages/app-defaults/src/createApp/createApp.tsx index c3f477035f..417c41d3af 100644 --- a/packages/app-defaults/src/createApp/createApp.tsx +++ b/packages/app-defaults/src/createApp/createApp.tsx @@ -14,132 +14,81 @@ * limitations under the License. */ -import { AppConfig } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; -import { withDefaults } from '@backstage/core-components'; -import { PrivateAppImpl } from './App'; -import { AppComponents, AppConfigLoader, AppOptions } from './types'; -import { defaultApis } from './defaultApis'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; - -const REQUIRED_APP_COMPONENTS: Array = [ - 'Progress', - 'Router', - 'NotFoundErrorPage', - 'BootErrorPage', - 'ErrorBoundaryFallback', -]; +import { apis, components, configLoader, icons, themes } from './defaults'; +import { + AppTheme, + BackstagePlugin, + IconComponent, +} from '@backstage/core-plugin-api'; +import { + AppComponents, + AppOptions, + AppIcons, + PrivateAppImpl, +} from '@backstage/core-app-api'; /** - * The default config loader, which expects that config is available at compile-time - * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as - * returned by the config loader. - * - * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, - * which can be rewritten at runtime to contain an additional JSON config object. - * If runtime config is present, it will be placed first in the config array, overriding - * other config values. - * - * @public - */ -export const defaultConfigLoader: AppConfigLoader = async ( - // This string may be replaced at runtime to provide additional config. - // It should be replaced by a JSON-serialized config object. - // It's a param so we can test it, but at runtime this will always fall back to default. - runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', -) => { - const appConfig = process.env.APP_CONFIG; - if (!appConfig) { - throw new Error('No static configuration provided'); - } - if (!Array.isArray(appConfig)) { - throw new Error('Static configuration has invalid format'); - } - const configs = appConfig.slice() as unknown as AppConfig[]; - - // Avoiding this string also being replaced at runtime - if ( - runtimeConfigJson !== - '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') - ) { - try { - const data = JSON.parse(runtimeConfigJson) as JsonObject; - if (Array.isArray(data)) { - configs.push(...data); - } else { - configs.push({ data, context: 'env' }); - } - } catch (error) { - throw new Error(`Failed to load runtime configuration, ${error}`); - } - } - - const windowAppConfig = (window as any).__APP_CONFIG__; - if (windowAppConfig) { - configs.push({ - context: 'window', - data: windowAppConfig, - }); - } - return configs; -}; - -/** - * Creates a new Backstage App. + * Creates a new Backstage App using a default set of components, icons and themes unless + * they are explicitly provided. * * @public */ export function createApp(options?: AppOptions) { - const optionsWithDefaults = withDefaults(options); - - const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( - name => !options?.components?.[name], - ); - 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. You can use the default components by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' + - `The following components are missing: ${missingRequiredComponents.join( - ', ', - )}`, - ); - } - - const providedIconKeys = Object.keys(options?.icons ?? {}); - const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter( - key => !providedIconKeys.includes(key), - ); - if (missingIconKeys.length > 0) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' + - 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' + - `The following icons are missing: ${missingIconKeys.join(', ')}`, - ); - } - - if (!options?.themes) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' + - 'You can use the default themes by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ', - ); - } - - const { icons, themes, components } = optionsWithDefaults; - return new PrivateAppImpl({ - icons: icons!, - themes: themes!, - components: components! as AppComponents, - defaultApis, + ...options, apis: options?.apis ?? [], bindRoutes: options?.bindRoutes, + components: { + ...components, + ...options?.components, + }, + configLoader: options?.configLoader ?? configLoader, + defaultApis: apis, + icons: { + ...icons, + ...options?.icons, + }, plugins: (options?.plugins as BackstagePlugin[]) ?? [], - configLoader: options?.configLoader ?? defaultConfigLoader, + themes: options?.themes ?? themes, }); } + +// NOTE: we don't re-export any of the types imported from core-app-api, as we +// want them to be imported from there rather than core-components. + +/** + * The set of app options that will be populated by {@link withDefaults} if they + * are not passed in explicitly. + * + * @public + */ +export interface OptionalAppOptions { + /** + * A set of icons to override the default icons with. + * + * The override is applied for each icon individually. + * + * @public + */ + icons?: Partial & { + [key in string]: IconComponent; + }; + + /** + * A set of themes that override all of the default app themes. + * + * If this option is provided none of the default themes will be used. + * + * @public + */ + themes?: (Partial & Omit)[]; // TODO: simplify once AppTheme is updated + + /** + * A set of components to override the default components with. + * + * The override is applied for each icon individually. + * + * @public + */ + components?: Partial; +} diff --git a/packages/app-defaults/src/createApp/defaultApis.ts b/packages/app-defaults/src/createApp/defaults/apis.ts similarity index 99% rename from packages/app-defaults/src/createApp/defaultApis.ts rename to packages/app-defaults/src/createApp/defaults/apis.ts index fb02274cf0..2596ac2593 100644 --- a/packages/app-defaults/src/createApp/defaultApis.ts +++ b/packages/app-defaults/src/createApp/defaults/apis.ts @@ -34,7 +34,7 @@ import { OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, -} from '../apis'; +} from '@backstage/core-app-api'; import { createApiFactory, @@ -61,7 +61,7 @@ import { import OAuth2Icon from '@material-ui/icons/AcUnit'; -export const defaultApis = [ +export const apis = [ createApiFactory({ api: discoveryApiRef, deps: { configApi: configApiRef }, diff --git a/packages/app-defaults/src/createApp/defaultAppComponents.test.tsx b/packages/app-defaults/src/createApp/defaults/components.test.tsx similarity index 95% rename from packages/app-defaults/src/createApp/defaultAppComponents.test.tsx rename to packages/app-defaults/src/createApp/defaults/components.test.tsx index db0e2d50c1..865eeed8a7 100644 --- a/packages/app-defaults/src/createApp/defaultAppComponents.test.tsx +++ b/packages/app-defaults/src/createApp/defaults/components.test.tsx @@ -17,7 +17,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { OptionallyWrapInRouter } from './defaultAppComponents'; +import { OptionallyWrapInRouter } from './components'; describe('OptionallyWrapInRouter', () => { it('should wrap with router if not yet inside a router', async () => { diff --git a/packages/app-defaults/src/createApp/defaultAppComponents.tsx b/packages/app-defaults/src/createApp/defaults/components.tsx similarity index 79% rename from packages/app-defaults/src/createApp/defaultAppComponents.tsx rename to packages/app-defaults/src/createApp/defaults/components.tsx index ab55a25840..659af21cd7 100644 --- a/packages/app-defaults/src/createApp/defaultAppComponents.tsx +++ b/packages/app-defaults/src/createApp/defaults/components.tsx @@ -16,16 +16,18 @@ import React, { ReactNode } from 'react'; import Button from '@material-ui/core/Button'; -import { ErrorPanel, Progress } from '../components'; -import { ErrorPage } from '../layout'; -import { MemoryRouter, useInRouterContext } from 'react-router'; -import { BrowserRouter } from 'react-router-dom'; +import { ErrorPanel, Progress, ErrorPage } from '@backstage/core-components'; +import { + MemoryRouter, + useInRouterContext, + BrowserRouter, +} from 'react-router-dom'; import { AppComponents, BootErrorPageProps, ErrorBoundaryFallbackProps, } from '@backstage/core-plugin-api'; -import { AppThemeProvider } from './AppThemeProvider'; +import { AppThemeProvider } from '../components/AppThemeProvider'; export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { if (useInRouterContext()) { @@ -52,6 +54,7 @@ const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => { ); }; + const DefaultErrorBoundaryFallback = ({ error, resetError, @@ -75,13 +78,11 @@ const DefaultErrorBoundaryFallback = ({ * * @public */ -export function defaultAppComponents(): AppComponents { - return { - Progress, - Router: BrowserRouter, - ThemeProvider: AppThemeProvider, - NotFoundErrorPage: DefaultNotFoundPage, - BootErrorPage: DefaultBootErrorPage, - ErrorBoundaryFallback: DefaultErrorBoundaryFallback, - }; -} +export const components: AppComponents = { + Progress, + Router: BrowserRouter, + ThemeProvider: AppThemeProvider, + NotFoundErrorPage: DefaultNotFoundPage, + BootErrorPage: DefaultBootErrorPage, + ErrorBoundaryFallback: DefaultErrorBoundaryFallback, +}; diff --git a/packages/app-defaults/src/createApp/defaults/configLoader.test.ts b/packages/app-defaults/src/createApp/defaults/configLoader.test.ts new file mode 100644 index 0000000000..0246a1cf11 --- /dev/null +++ b/packages/app-defaults/src/createApp/defaults/configLoader.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { configLoader } from './configLoader'; + +(process as any).env = { NODE_ENV: 'test' }; +const anyEnv = process.env as any; +const anyWindow = window as any; + +describe('configLoader', () => { + afterEach(() => { + delete anyEnv.APP_CONFIG; + delete anyWindow.__APP_CONFIG__; + }); + + it('loads static config', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + + const configs = await configLoader(); + expect(configs).toEqual([ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]); + }); + + it('loads runtime config', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + ]; + + const configs = await (configLoader as any)('{"my":"runtime-config"}'); + expect(configs).toEqual([ + { data: { my: 'override-config' }, context: 'a' }, + { data: { my: 'config' }, context: 'b' }, + { data: { my: 'runtime-config' }, context: 'env' }, + ]); + }); + + it('fails to load invalid missing config', async () => { + await expect(configLoader()).rejects.toThrow( + 'No static configuration provided', + ); + }); + + it('fails to load invalid static config', async () => { + anyEnv.APP_CONFIG = { my: 'invalid-config' }; + await expect(configLoader()).rejects.toThrow( + 'Static configuration has invalid format', + ); + }); + + it('fails to load bad runtime config', async () => { + anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; + + await expect((configLoader as any)('}')).rejects.toThrow( + 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', + ); + }); + + it('loads config from window.__APP_CONFIG__', async () => { + anyEnv.APP_CONFIG = [ + { data: { my: 'config' }, context: 'a' }, + { data: { my: 'override-config' }, context: 'b' }, + ]; + const windowConfig = { app: { configKey: 'config-value' } }; + anyWindow.__APP_CONFIG__ = windowConfig; + + const configs = await configLoader(); + + expect(configs).toEqual([ + ...anyEnv.APP_CONFIG, + { context: 'window', data: windowConfig }, + ]); + }); +}); diff --git a/packages/app-defaults/src/createApp/defaults/configLoader.ts b/packages/app-defaults/src/createApp/defaults/configLoader.ts new file mode 100644 index 0000000000..3cf10ef72f --- /dev/null +++ b/packages/app-defaults/src/createApp/defaults/configLoader.ts @@ -0,0 +1,73 @@ +/* + * 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 { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { AppConfigLoader } from '@backstage/core-app-api'; + +/** + * The default config loader, which expects that config is available at compile-time + * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as + * returned by the config loader. + * + * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, + * which can be rewritten at runtime to contain an additional JSON config object. + * If runtime config is present, it will be placed first in the config array, overriding + * other config values. + * + * @public + */ +export const configLoader: AppConfigLoader = async ( + // This string may be replaced at runtime to provide additional config. + // It should be replaced by a JSON-serialized config object. + // It's a param so we can test it, but at runtime this will always fall back to default. + runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', +) => { + const appConfig = process.env.APP_CONFIG; + if (!appConfig) { + throw new Error('No static configuration provided'); + } + if (!Array.isArray(appConfig)) { + throw new Error('Static configuration has invalid format'); + } + const configs = appConfig.slice() as unknown as AppConfig[]; + + // Avoiding this string also being replaced at runtime + if ( + runtimeConfigJson !== + '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') + ) { + try { + const data = JSON.parse(runtimeConfigJson) as JsonObject; + if (Array.isArray(data)) { + configs.push(...data); + } else { + configs.push({ data, context: 'env' }); + } + } catch (error) { + throw new Error(`Failed to load runtime configuration, ${error}`); + } + } + + const windowAppConfig = (window as any).__APP_CONFIG__; + if (windowAppConfig) { + configs.push({ + context: 'window', + data: windowAppConfig, + }); + } + return configs; +}; diff --git a/packages/app-defaults/src/createApp/defaultAppIcons.tsx b/packages/app-defaults/src/createApp/defaults/icons.tsx similarity index 98% rename from packages/app-defaults/src/createApp/defaultAppIcons.tsx rename to packages/app-defaults/src/createApp/defaults/icons.tsx index 1d02dd5ad2..ba2531213d 100644 --- a/packages/app-defaults/src/createApp/defaultAppIcons.tsx +++ b/packages/app-defaults/src/createApp/defaults/icons.tsx @@ -32,7 +32,7 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; -export const defaultAppIcons = () => ({ +export const icons = () => ({ brokenImage: MuiBrokenImageIcon as IconComponent, // To be confirmed: see https://github.com/backstage/backstage/issues/4970 catalog: MuiMenuBookIcon as IconComponent, diff --git a/packages/app-defaults/src/createApp/defaults/index.ts b/packages/app-defaults/src/createApp/defaults/index.ts new file mode 100644 index 0000000000..a8a11e78ac --- /dev/null +++ b/packages/app-defaults/src/createApp/defaults/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export { apis } from './apis'; +export { components } from './components'; +export { configLoader } from './configLoader'; +export { icons } from './icons'; +export { themes } from './themes'; diff --git a/packages/app-defaults/src/createApp/defaultAppThemes.tsx b/packages/app-defaults/src/createApp/defaults/themes.tsx similarity index 59% rename from packages/app-defaults/src/createApp/defaultAppThemes.tsx rename to packages/app-defaults/src/createApp/defaults/themes.tsx index 894716942b..90a4a0f47f 100644 --- a/packages/app-defaults/src/createApp/defaultAppThemes.tsx +++ b/packages/app-defaults/src/createApp/defaults/themes.tsx @@ -22,31 +22,29 @@ import { ThemeProvider } from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; import { AppTheme } from '@backstage/core-plugin-api'; -export function defaultAppThemes(): AppTheme[] { - return [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - icon: , - theme: lightTheme, - Provider: ({ children }) => ( - - {children} - - ), - }, - { - id: 'dark', - title: 'Dark Theme', - variant: 'dark', - icon: , - theme: darkTheme, - Provider: ({ children }) => ( - - {children} - - ), - }, - ]; -} +export const themes: AppTheme[] = [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + icon: , + theme: lightTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + theme: darkTheme, + Provider: ({ children }) => ( + + {children} + + ), + }, +]; diff --git a/packages/app-defaults/src/createApp/icons.tsx b/packages/app-defaults/src/createApp/icons.tsx deleted file mode 100644 index df1e69da6a..0000000000 --- a/packages/app-defaults/src/createApp/icons.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 { IconComponent } from '@backstage/core-plugin-api'; -import MuiApartmentIcon from '@material-ui/icons/Apartment'; -import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; -import MuiCategoryIcon from '@material-ui/icons/Category'; -import MuiChatIcon from '@material-ui/icons/Chat'; -import MuiDashboardIcon from '@material-ui/icons/Dashboard'; -import MuiDocsIcon from '@material-ui/icons/Description'; -import MuiEmailIcon from '@material-ui/icons/Email'; -import MuiExtensionIcon from '@material-ui/icons/Extension'; -import MuiGitHubIcon from '@material-ui/icons/GitHub'; -import MuiHelpIcon from '@material-ui/icons/Help'; -import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; -import MuiMemoryIcon from '@material-ui/icons/Memory'; -import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; -import MuiPeopleIcon from '@material-ui/icons/People'; -import MuiPersonIcon from '@material-ui/icons/Person'; -import MuiWarningIcon from '@material-ui/icons/Warning'; - -/** @public */ -export type AppIcons = { - 'kind:api': IconComponent; - 'kind:component': IconComponent; - 'kind:domain': IconComponent; - 'kind:group': IconComponent; - 'kind:location': IconComponent; - 'kind:system': IconComponent; - 'kind:user': IconComponent; - - brokenImage: IconComponent; - catalog: IconComponent; - chat: IconComponent; - dashboard: IconComponent; - docs: IconComponent; - email: IconComponent; - github: IconComponent; - group: IconComponent; - help: IconComponent; - user: IconComponent; - warning: IconComponent; -}; - -export const defaultAppIcons: AppIcons = { - brokenImage: MuiBrokenImageIcon, - // To be confirmed: see https://github.com/backstage/backstage/issues/4970 - catalog: MuiMenuBookIcon, - chat: MuiChatIcon, - dashboard: MuiDashboardIcon, - docs: MuiDocsIcon, - email: MuiEmailIcon, - github: MuiGitHubIcon, - group: MuiPeopleIcon, - help: MuiHelpIcon, - 'kind:api': MuiExtensionIcon, - 'kind:component': MuiMemoryIcon, - 'kind:domain': MuiApartmentIcon, - 'kind:group': MuiPeopleIcon, - 'kind:location': MuiLocationOnIcon, - 'kind:system': MuiCategoryIcon, - 'kind:user': MuiPersonIcon, - user: MuiPersonIcon, - warning: MuiWarningIcon, -}; diff --git a/packages/app-defaults/src/createApp/index.ts b/packages/app-defaults/src/createApp/index.ts index 2a16a90bfa..dea36bb3e7 100644 --- a/packages/app-defaults/src/createApp/index.ts +++ b/packages/app-defaults/src/createApp/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -export { createApp, defaultConfigLoader } from './createApp'; -export type { AppIcons } from './icons'; -export * from './types'; +export { createApp } from './createApp'; diff --git a/packages/app-defaults/src/createApp/types.ts b/packages/app-defaults/src/createApp/types.ts deleted file mode 100644 index 78ef277c90..0000000000 --- a/packages/app-defaults/src/createApp/types.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - * 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 { ComponentType } from 'react'; -import { - AnyApiFactory, - AppTheme, - ProfileInfo, - IconComponent, - BackstagePlugin, - RouteRef, - SubRouteRef, - ExternalRouteRef, - PluginOutput, -} from '@backstage/core-plugin-api'; -import { AppConfig } from '@backstage/config'; -import { AppIcons } from './icons'; - -/** - * Props for the `BootErrorPage` component of {@link AppComponents}. - * - * @public - */ -export type BootErrorPageProps = { - step: 'load-config' | 'load-chunk'; - error: Error; -}; - -/** - * The outcome of signing in on the sign-in page. - * - * @public - */ -export type SignInResult = { - /** - * User ID that will be returned by the IdentityApi - */ - userId: string; - - profile: ProfileInfo; - - /** - * Function used to retrieve an ID token for the signed in user. - */ - getIdToken?: () => Promise; - - /** - * Sign out handler that will be called if the user requests to sign out. - */ - signOut?: () => Promise; -}; - -/** - * Props for the `SignInPage` component of {@link AppComponents}. - * - * @public - */ -export type SignInPageProps = { - /** - * Set the sign-in result for the app. This should only be called once. - */ - onResult(result: SignInResult): void; -}; - -/** - * Props for the fallback error boundary. - * - * @public - */ -export type ErrorBoundaryFallbackProps = { - plugin?: BackstagePlugin; - error: Error; - resetError: () => void; -}; - -/** - * A set of replaceable core components that are part of every Backstage app. - * - * @public - */ -export type AppComponents = { - NotFoundErrorPage: ComponentType<{}>; - BootErrorPage: ComponentType; - Progress: ComponentType<{}>; - Router: ComponentType<{}>; - ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; - - /** - * An optional sign-in page that will be rendered instead of the AppRouter at startup. - * - * If a sign-in page is set, it will always be shown before the app, and it is up - * to the sign-in page to handle e.g. saving of login methods for subsequent visits. - * - * The sign-in page will be displayed until it has passed up a result to the parent, - * and which point the AppRouter and all of its children will be rendered instead. - */ - SignInPage?: ComponentType; -}; - -/** - * A function that loads in the App config that will be accessible via the ConfigApi. - * - * If multiple config objects are returned in the array, values in the earlier configs - * will override later ones. - * - * @public - */ -export type AppConfigLoader = () => Promise; - -/** - * Extracts a union of the keys in a map whose value extends the given type - */ -type KeysWithType = { - [key in keyof Obj]: Obj[key] extends Type ? key : never; -}[keyof Obj]; - -/** - * Takes a map Map required values and makes all keys matching Keys optional - */ -type PartialKeys< - Map extends { [name in string]: any }, - Keys extends keyof Map, -> = Partial> & Required>; - -/** - * Creates a map of target routes with matching parameters based on a map of external routes. - */ -type TargetRouteMap< - ExternalRoutes extends { [name: string]: ExternalRouteRef }, -> = { - [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< - infer Params, - any - > - ? RouteRef | SubRouteRef - : never; -}; - -/** - * A function that can bind from external routes of a given plugin, to concrete - * routes of other plugins. See {@link createApp}. - * - * @public - */ -export type AppRouteBinder = < - ExternalRoutes extends { [name: string]: ExternalRouteRef }, ->( - externalRoutes: ExternalRoutes, - targetRoutes: PartialKeys< - TargetRouteMap, - KeysWithType> - >, -) => void; - -/** - * Internal helper type that represents a plugin with any type of output. - * - * @public - * @remarks - * - * The `type: string` type is there to handle output from newer or older plugin - * API versions that might not be supported by this version of the app API, but - * we don't want to break at the type checking level. We only use this more - * permissive type for the `createApp` options, as we otherwise want to stick - * to using the type for the outputs that we know about in this version of the - * app api. - * - * TODO(freben): This should be marked internal but that's not supported by the api report generation tools yet - */ -export type BackstagePluginWithAnyOutput = Omit< - BackstagePlugin, - 'output' -> & { - output(): (PluginOutput | { type: string })[]; -}; - -/** - * The options accepted by {@link createApp}. - * - * @public - */ -export type AppOptions = { - /** - * A collection of ApiFactories to register in the application to either - * add add new ones, or override factories provided by default or by plugins. - */ - apis?: Iterable; - - /** - * Supply icons to override the default ones. - */ - icons?: Partial & { [key in string]: IconComponent }; - - /** - * A list of all plugins to include in the app. - */ - plugins?: BackstagePluginWithAnyOutput[]; - - /** - * Supply components to the app to override the default ones. - */ - components?: Partial; - - /** - * Themes provided as a part of the app. By default two themes are included, one - * light variant of the default backstage theme, and one dark. - * - * This is the default config: - * - * ``` - * [{ - * id: 'light', - * title: 'Light Theme', - * variant: 'light', - * icon: , - * Provider: ({ children }) => ( - * - * {children} - * - * ), - * }, { - * id: 'dark', - * title: 'Dark Theme', - * variant: 'dark', - * icon: , - * Provider: ({ children }) => ( - * - * {children} - * - * ), - * }] - * ``` - */ - themes?: (Partial & Omit)[]; - - /** - * A function that loads in App configuration that will be accessible via - * the ConfigApi. - * - * Defaults to an empty config. - * - * TODO(Rugvip): Omitting this should instead default to loading in configuration - * that was packaged by the backstage-cli and default docker container boot script. - */ - configLoader?: AppConfigLoader; - - /** - * A function that is used to register associations between cross-plugin route - * references, enabling plugins to navigate between each other. - * - * The `bind` function that is passed in should be used to bind all external - * routes of all used plugins. - * - * ```ts - * bindRoutes({ bind }) { - * bind(docsPlugin.externalRoutes, { - * homePage: managePlugin.routes.managePage, - * }) - * bind(homePagePlugin.externalRoutes, { - * settingsPage: settingsPlugin.routes.settingsPage, - * }) - * } - * ``` - */ - bindRoutes?(context: { bind: AppRouteBinder }): void; -}; - -/** - * The public API of the output of {@link createApp}. - * - * @public - */ -export type BackstageApp = { - /** - * Returns all plugins registered for the app. - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: string): IconComponent | undefined; - - /** - * Provider component that should wrap the Router created with getRouter() - * and any other components that need to be within the app context. - */ - getProvider(): ComponentType<{}>; - - /** - * Router component that should wrap the App Routes create with getRoutes() - * and any other components that should only be available while signed in. - */ - getRouter(): ComponentType<{}>; -}; - -/** - * The central context providing runtime app specific state that plugin views - * want to consume. - * - * @public - */ -export type AppContext = { - /** - * Get a list of all plugins that are installed in the app. - */ - getPlugins(): BackstagePlugin[]; - - /** - * Get a common or custom icon for this app. - */ - getSystemIcon(key: string): IconComponent | undefined; - - /** - * Get the components registered for various purposes in the app. - */ - getComponents(): AppComponents; -}; diff --git a/packages/app-defaults/src/createApp/withDefaults.tsx b/packages/app-defaults/src/createApp/withDefaults.tsx deleted file mode 100644 index 9254053404..0000000000 --- a/packages/app-defaults/src/createApp/withDefaults.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 { AppTheme, IconComponent } from '@backstage/core-plugin-api'; -// This is a bit of a hack that we use to avoid having to redeclare these types -// within this package or have an explicit dependency on core-app-api. -// These types end up being inlined and duplicated into this package at build time. -// eslint-disable-next-line no-restricted-imports -import { - AppIcons, - AppComponents, - AppOptions, -} from '../../../core-app-api/src/app'; -import { defaultAppComponents } from './defaultAppComponents'; -import { defaultAppIcons } from './defaultAppIcons'; -import { defaultAppThemes } from './defaultAppThemes'; - -// NOTE: we don't re-export any of the types imported from core-app-api, as we -// want them to be imported from there rather than core-components. - -/** - * The set of app options that will be populated by {@link withDefaults} if they - * are not passed in explicitly. - * - * @public - */ -export interface OptionalAppOptions { - icons?: Partial & { - [key in string]: IconComponent; - }; - themes?: (Partial & Omit)[]; // TODO: simplify once AppTheme is updated - components?: Partial; -} - -/** - * Provides a set of default App options with the ability to override specific options. - * - * These options populate the theme, icons and components options of {@link @backstage/core-app-api#AppOptions}. - * - * @public - */ -export function withDefaults( - options?: Omit & OptionalAppOptions, -): AppOptions { - const { themes, icons, components } = options ?? {}; - - return { - ...options, - themes: themes ?? defaultAppThemes(), - icons: { ...defaultAppIcons(), ...icons }, - components: { ...defaultAppComponents(), ...components }, - }; -} From 5bfe8917c79531f2f1ab7f969390819f7ad6e48b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 21:20:57 +0200 Subject: [PATCH 12/35] app-defaults: move AppThemeProvider back to core-app-api Signed-off-by: Patrik Oldsberg --- packages/app-defaults/src/createApp/defaults/components.tsx | 2 -- packages/core-app-api/src/app/App.tsx | 5 +++-- .../components => core-app-api/src/app}/AppThemeProvider.tsx | 0 packages/core-app-api/src/app/types.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) rename packages/{app-defaults/src/createApp/components => core-app-api/src/app}/AppThemeProvider.tsx (100%) diff --git a/packages/app-defaults/src/createApp/defaults/components.tsx b/packages/app-defaults/src/createApp/defaults/components.tsx index 659af21cd7..139bea4862 100644 --- a/packages/app-defaults/src/createApp/defaults/components.tsx +++ b/packages/app-defaults/src/createApp/defaults/components.tsx @@ -27,7 +27,6 @@ import { BootErrorPageProps, ErrorBoundaryFallbackProps, } from '@backstage/core-plugin-api'; -import { AppThemeProvider } from '../components/AppThemeProvider'; export function OptionallyWrapInRouter({ children }: { children: ReactNode }) { if (useInRouterContext()) { @@ -81,7 +80,6 @@ const DefaultErrorBoundaryFallback = ({ export const components: AppComponents = { Progress, Router: BrowserRouter, - ThemeProvider: AppThemeProvider, NotFoundErrorPage: DefaultNotFoundPage, BootErrorPage: DefaultBootErrorPage, ErrorBoundaryFallback: DefaultErrorBoundaryFallback, diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/App.tsx index 6fadd22808..81da370b6c 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/App.tsx @@ -77,6 +77,7 @@ import { SignInPageProps, SignInResult, } from './types'; +import { AppThemeProvider } from './AppThemeProvider'; export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) { const result = new Map(); @@ -151,7 +152,7 @@ function useConfigLoader( noConfigNode = ; } - const { ThemeProvider } = components; + const { ThemeProvider = AppThemeProvider } = components; // Before the config is loaded we can't use a router, so exit early if (noConfigNode) { @@ -307,7 +308,7 @@ export class PrivateAppImpl implements BackstageApp { return loadedConfig.node; } - const { ThemeProvider } = this.components; + const { ThemeProvider = AppThemeProvider } = this.components; return ( diff --git a/packages/app-defaults/src/createApp/components/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx similarity index 100% rename from packages/app-defaults/src/createApp/components/AppThemeProvider.tsx rename to packages/core-app-api/src/app/AppThemeProvider.tsx diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 78ef277c90..41e997e179 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -97,7 +97,7 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; + ThemeProvider?: ComponentType<{}>; /** * An optional sign-in page that will be rendered instead of the AppRouter at startup. From 3fb98cfcbf63d7b71dba14df33513dd20ca319ec Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 21:21:58 +0200 Subject: [PATCH 13/35] app-defaults: move defaultConfigLoader back to core-app-api Signed-off-by: Patrik Oldsberg --- .../src/createApp/defaults/index.ts | 1 - .../src/app/defaultConfigLoader.test.ts} | 18 ++++++++++-------- .../src/app/defaultConfigLoader.ts} | 4 ++-- packages/core-app-api/src/app/index.ts | 3 ++- 4 files changed, 14 insertions(+), 12 deletions(-) rename packages/{app-defaults/src/createApp/defaults/configLoader.test.ts => core-app-api/src/app/defaultConfigLoader.test.ts} (83%) rename packages/{app-defaults/src/createApp/defaults/configLoader.ts => core-app-api/src/app/defaultConfigLoader.ts} (95%) diff --git a/packages/app-defaults/src/createApp/defaults/index.ts b/packages/app-defaults/src/createApp/defaults/index.ts index a8a11e78ac..d9ff18bc8c 100644 --- a/packages/app-defaults/src/createApp/defaults/index.ts +++ b/packages/app-defaults/src/createApp/defaults/index.ts @@ -16,6 +16,5 @@ export { apis } from './apis'; export { components } from './components'; -export { configLoader } from './configLoader'; export { icons } from './icons'; export { themes } from './themes'; diff --git a/packages/app-defaults/src/createApp/defaults/configLoader.test.ts b/packages/core-app-api/src/app/defaultConfigLoader.test.ts similarity index 83% rename from packages/app-defaults/src/createApp/defaults/configLoader.test.ts rename to packages/core-app-api/src/app/defaultConfigLoader.test.ts index 0246a1cf11..aebba92c43 100644 --- a/packages/app-defaults/src/createApp/defaults/configLoader.test.ts +++ b/packages/core-app-api/src/app/defaultConfigLoader.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { configLoader } from './configLoader'; +import { defaultConfigLoader } from './defaultConfigLoader'; (process as any).env = { NODE_ENV: 'test' }; const anyEnv = process.env as any; const anyWindow = window as any; -describe('configLoader', () => { +describe('defaultConfigLoader', () => { afterEach(() => { delete anyEnv.APP_CONFIG; delete anyWindow.__APP_CONFIG__; @@ -32,7 +32,7 @@ describe('configLoader', () => { { data: { my: 'override-config' }, context: 'b' }, ]; - const configs = await configLoader(); + const configs = await defaultConfigLoader(); expect(configs).toEqual([ { data: { my: 'config' }, context: 'a' }, { data: { my: 'override-config' }, context: 'b' }, @@ -45,7 +45,9 @@ describe('configLoader', () => { { data: { my: 'config' }, context: 'b' }, ]; - const configs = await (configLoader as any)('{"my":"runtime-config"}'); + const configs = await (defaultConfigLoader as any)( + '{"my":"runtime-config"}', + ); expect(configs).toEqual([ { data: { my: 'override-config' }, context: 'a' }, { data: { my: 'config' }, context: 'b' }, @@ -54,14 +56,14 @@ describe('configLoader', () => { }); it('fails to load invalid missing config', async () => { - await expect(configLoader()).rejects.toThrow( + await expect(defaultConfigLoader()).rejects.toThrow( 'No static configuration provided', ); }); it('fails to load invalid static config', async () => { anyEnv.APP_CONFIG = { my: 'invalid-config' }; - await expect(configLoader()).rejects.toThrow( + await expect(defaultConfigLoader()).rejects.toThrow( 'Static configuration has invalid format', ); }); @@ -69,7 +71,7 @@ describe('configLoader', () => { it('fails to load bad runtime config', async () => { anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; - await expect((configLoader as any)('}')).rejects.toThrow( + await expect((defaultConfigLoader as any)('}')).rejects.toThrow( 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', ); }); @@ -82,7 +84,7 @@ describe('configLoader', () => { const windowConfig = { app: { configKey: 'config-value' } }; anyWindow.__APP_CONFIG__ = windowConfig; - const configs = await configLoader(); + const configs = await defaultConfigLoader(); expect(configs).toEqual([ ...anyEnv.APP_CONFIG, diff --git a/packages/app-defaults/src/createApp/defaults/configLoader.ts b/packages/core-app-api/src/app/defaultConfigLoader.ts similarity index 95% rename from packages/app-defaults/src/createApp/defaults/configLoader.ts rename to packages/core-app-api/src/app/defaultConfigLoader.ts index 3cf10ef72f..00aee2d367 100644 --- a/packages/app-defaults/src/createApp/defaults/configLoader.ts +++ b/packages/core-app-api/src/app/defaultConfigLoader.ts @@ -16,7 +16,7 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; -import { AppConfigLoader } from '@backstage/core-app-api'; +import { AppConfigLoader } from './types'; /** * The default config loader, which expects that config is available at compile-time @@ -30,7 +30,7 @@ import { AppConfigLoader } from '@backstage/core-app-api'; * * @public */ -export const configLoader: AppConfigLoader = async ( +export const defaultConfigLoader: AppConfigLoader = async ( // This string may be replaced at runtime to provide additional config. // It should be replaced by a JSON-serialized config object. // It's a param so we can test it, but at runtime this will always fall back to default. diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index 2a16a90bfa..5514d7ee76 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ -export { createApp, defaultConfigLoader } from './createApp'; +export { createApp } from './createApp'; +export { defaultConfigLoader } from './defaultConfigLoader'; export type { AppIcons } from './icons'; export * from './types'; From 42175496486ed1f79bb4f1a0adf5eba4a1f484ef Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 Oct 2021 21:26:32 +0200 Subject: [PATCH 14/35] core-app-api: rename PrivateAppImpl to AppManager Signed-off-by: Patrik Oldsberg --- .../src/app/{App.test.tsx => AppManager.test.tsx} | 12 ++++++------ .../core-app-api/src/app/{App.tsx => AppManager.tsx} | 4 ++-- packages/core-app-api/src/app/createApp.tsx | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) rename packages/core-app-api/src/app/{App.test.tsx => AppManager.test.tsx} (98%) rename packages/core-app-api/src/app/{App.tsx => AppManager.tsx} (99%) diff --git a/packages/core-app-api/src/app/App.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx similarity index 98% rename from packages/core-app-api/src/app/App.test.tsx rename to packages/core-app-api/src/app/AppManager.test.tsx index 198048f8cf..934ca3295f 100644 --- a/packages/core-app-api/src/app/App.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -37,7 +37,7 @@ import { createRoutableExtension, analyticsApiRef, } from '@backstage/core-plugin-api'; -import { generateBoundRoutes, PrivateAppImpl } from './App'; +import { generateBoundRoutes, AppManager } from './AppManager'; import { AppComponents } from './types'; describe('generateBoundRoutes', () => { @@ -188,7 +188,7 @@ describe('Integration Test', () => { }; it('runs happy paths', async () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [noOpAnalyticsApi], defaultApis: [], themes: [ @@ -242,7 +242,7 @@ describe('Integration Test', () => { }); it('runs happy paths without optional routes', async () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [noOpAnalyticsApi], defaultApis: [], themes: [ @@ -299,7 +299,7 @@ describe('Integration Test', () => { }), ]; - const app = new PrivateAppImpl({ + const app = new AppManager({ apis, defaultApis: [], themes: [ @@ -349,7 +349,7 @@ describe('Integration Test', () => { it('should track route changes via analytics api', async () => { const mockAnalyticsApi = new MockAnalyticsApi(); const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)]; - const app = new PrivateAppImpl({ + const app = new AppManager({ apis, defaultApis: [], themes: [ @@ -409,7 +409,7 @@ describe('Integration Test', () => { }); it('should throw some error when the route has duplicate params', () => { - const app = new PrivateAppImpl({ + const app = new AppManager({ apis: [], defaultApis: [], themes: [ diff --git a/packages/core-app-api/src/app/App.tsx b/packages/core-app-api/src/app/AppManager.tsx similarity index 99% rename from packages/core-app-api/src/app/App.tsx rename to packages/core-app-api/src/app/AppManager.tsx index 81da370b6c..ff501aa100 100644 --- a/packages/core-app-api/src/app/App.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -171,7 +171,7 @@ function useConfigLoader( } class AppContextImpl implements AppContext { - constructor(private readonly app: PrivateAppImpl) {} + constructor(private readonly app: AppManager) {} getPlugins(): BackstagePlugin[] { return this.app.getPlugins(); @@ -186,7 +186,7 @@ class AppContextImpl implements AppContext { } } -export class PrivateAppImpl implements BackstageApp { +export class AppManager implements BackstageApp { private apiHolder?: ApiHolder; private configApi?: ConfigApi; diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index c3f477035f..5bb5607a37 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -17,7 +17,7 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { withDefaults } from '@backstage/core-components'; -import { PrivateAppImpl } from './App'; +import { AppManager } from './AppManager'; import { AppComponents, AppConfigLoader, AppOptions } from './types'; import { defaultApis } from './defaultApis'; import { BackstagePlugin } from '@backstage/core-plugin-api'; @@ -132,7 +132,7 @@ export function createApp(options?: AppOptions) { const { icons, themes, components } = optionsWithDefaults; - return new PrivateAppImpl({ + return new AppManager({ icons: icons!, themes: themes!, components: components! as AppComponents, From 1c3413bab07bdb45ea8ddca78aaf2a3f9c4e074c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Oct 2021 13:49:25 +0200 Subject: [PATCH 15/35] app-defaults,core-app-api: refactor into createApp/createSpecializedApp split Signed-off-by: Patrik Oldsberg --- .../src/{createApp => }/createApp.test.tsx | 0 .../src/{createApp => }/createApp.tsx | 23 +- .../src/{createApp => }/defaults/apis.ts | 0 .../defaults/components.test.tsx | 0 .../{createApp => }/defaults/components.tsx | 0 .../src/{createApp => }/defaults/icons.tsx | 4 +- .../src/{createApp => }/defaults/index.ts | 0 .../src/{createApp => }/defaults/themes.tsx | 0 packages/app-defaults/src/index.ts | 3 +- packages/core-app-api/package.json | 1 + packages/core-app-api/src/app/AppManager.tsx | 24 +- .../core-app-api/src/app/createApp.test.tsx | 119 -------- packages/core-app-api/src/app/createApp.tsx | 135 +-------- .../src/app/createSpecializedApp.tsx} | 13 +- packages/core-app-api/src/app/defaultApis.ts | 264 ------------------ packages/core-app-api/src/app/index.ts | 1 + packages/core-app-api/src/app/types.ts | 15 +- 17 files changed, 63 insertions(+), 539 deletions(-) rename packages/app-defaults/src/{createApp => }/createApp.test.tsx (100%) rename packages/app-defaults/src/{createApp => }/createApp.tsx (78%) rename packages/app-defaults/src/{createApp => }/defaults/apis.ts (100%) rename packages/app-defaults/src/{createApp => }/defaults/components.test.tsx (100%) rename packages/app-defaults/src/{createApp => }/defaults/components.tsx (100%) rename packages/app-defaults/src/{createApp => }/defaults/icons.tsx (98%) rename packages/app-defaults/src/{createApp => }/defaults/index.ts (100%) rename packages/app-defaults/src/{createApp => }/defaults/themes.tsx (100%) delete mode 100644 packages/core-app-api/src/app/createApp.test.tsx rename packages/{app-defaults/src/createApp/index.ts => core-app-api/src/app/createSpecializedApp.tsx} (64%) delete mode 100644 packages/core-app-api/src/app/defaultApis.ts diff --git a/packages/app-defaults/src/createApp/createApp.test.tsx b/packages/app-defaults/src/createApp.test.tsx similarity index 100% rename from packages/app-defaults/src/createApp/createApp.test.tsx rename to packages/app-defaults/src/createApp.test.tsx diff --git a/packages/app-defaults/src/createApp/createApp.tsx b/packages/app-defaults/src/createApp.tsx similarity index 78% rename from packages/app-defaults/src/createApp/createApp.tsx rename to packages/app-defaults/src/createApp.tsx index 417c41d3af..d85dfa04eb 100644 --- a/packages/app-defaults/src/createApp/createApp.tsx +++ b/packages/app-defaults/src/createApp.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { apis, components, configLoader, icons, themes } from './defaults'; +import { apis, components, icons, themes } from './defaults'; import { AppTheme, BackstagePlugin, @@ -24,7 +24,7 @@ import { AppComponents, AppOptions, AppIcons, - PrivateAppImpl, + createSpecializedApp, } from '@backstage/core-app-api'; /** @@ -33,8 +33,10 @@ import { * * @public */ -export function createApp(options?: AppOptions) { - return new PrivateAppImpl({ +export function createApp( + options?: Omit & OptionalAppOptions, +) { + return createSpecializedApp({ ...options, apis: options?.apis ?? [], bindRoutes: options?.bindRoutes, @@ -42,7 +44,7 @@ export function createApp(options?: AppOptions) { ...components, ...options?.components, }, - configLoader: options?.configLoader ?? configLoader, + configLoader: options?.configLoader, defaultApis: apis, icons: { ...icons, @@ -53,16 +55,13 @@ export function createApp(options?: AppOptions) { }); } -// NOTE: we don't re-export any of the types imported from core-app-api, as we -// want them to be imported from there rather than core-components. - /** - * The set of app options that will be populated by {@link withDefaults} if they - * are not passed in explicitly. + * The set of app options that {@link createApp} will provide defaults for + * if they are not passed in explicitly. * * @public */ -export interface OptionalAppOptions { +export type OptionalAppOptions = { /** * A set of icons to override the default icons with. * @@ -91,4 +90,4 @@ export interface OptionalAppOptions { * @public */ components?: Partial; -} +}; diff --git a/packages/app-defaults/src/createApp/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts similarity index 100% rename from packages/app-defaults/src/createApp/defaults/apis.ts rename to packages/app-defaults/src/defaults/apis.ts diff --git a/packages/app-defaults/src/createApp/defaults/components.test.tsx b/packages/app-defaults/src/defaults/components.test.tsx similarity index 100% rename from packages/app-defaults/src/createApp/defaults/components.test.tsx rename to packages/app-defaults/src/defaults/components.test.tsx diff --git a/packages/app-defaults/src/createApp/defaults/components.tsx b/packages/app-defaults/src/defaults/components.tsx similarity index 100% rename from packages/app-defaults/src/createApp/defaults/components.tsx rename to packages/app-defaults/src/defaults/components.tsx diff --git a/packages/app-defaults/src/createApp/defaults/icons.tsx b/packages/app-defaults/src/defaults/icons.tsx similarity index 98% rename from packages/app-defaults/src/createApp/defaults/icons.tsx rename to packages/app-defaults/src/defaults/icons.tsx index ba2531213d..fa578a263a 100644 --- a/packages/app-defaults/src/createApp/defaults/icons.tsx +++ b/packages/app-defaults/src/defaults/icons.tsx @@ -32,7 +32,7 @@ import MuiPeopleIcon from '@material-ui/icons/People'; import MuiPersonIcon from '@material-ui/icons/Person'; import MuiWarningIcon from '@material-ui/icons/Warning'; -export const icons = () => ({ +export const icons = { brokenImage: MuiBrokenImageIcon as IconComponent, // To be confirmed: see https://github.com/backstage/backstage/issues/4970 catalog: MuiMenuBookIcon as IconComponent, @@ -52,4 +52,4 @@ export const icons = () => ({ 'kind:user': MuiPersonIcon as IconComponent, user: MuiPersonIcon as IconComponent, warning: MuiWarningIcon as IconComponent, -}); +}; diff --git a/packages/app-defaults/src/createApp/defaults/index.ts b/packages/app-defaults/src/defaults/index.ts similarity index 100% rename from packages/app-defaults/src/createApp/defaults/index.ts rename to packages/app-defaults/src/defaults/index.ts diff --git a/packages/app-defaults/src/createApp/defaults/themes.tsx b/packages/app-defaults/src/defaults/themes.tsx similarity index 100% rename from packages/app-defaults/src/createApp/defaults/themes.tsx rename to packages/app-defaults/src/defaults/themes.tsx diff --git a/packages/app-defaults/src/index.ts b/packages/app-defaults/src/index.ts index 62ef4d9077..16a6a693a5 100644 --- a/packages/app-defaults/src/index.ts +++ b/packages/app-defaults/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ -export * from './createApp'; +export { createApp } from './createApp'; +export type { OptionalAppOptions } from './createApp'; diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index a0877cf933..fc6c103109 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -29,6 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/app-defaults": "^0.1.0", "@backstage/core-components": "^0.7.3", "@backstage/config": "^0.1.11", "@backstage/core-plugin-api": "^0.1.13", diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index ff501aa100..950f57bb7d 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -78,6 +78,7 @@ import { SignInResult, } from './types'; import { AppThemeProvider } from './AppThemeProvider'; +import { defaultConfigLoader } from './defaultConfigLoader'; export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) { const result = new Map(); @@ -122,17 +123,6 @@ function getBasePath(configApi: Config) { return pathname; } -type FullAppOptions = { - apis: Iterable; - icons: NonNullable; - plugins: BackstagePlugin[]; - components: AppComponents; - themes: (Partial & Omit)[]; - configLoader?: AppConfigLoader; - defaultApis: Iterable; - bindRoutes?: AppOptions['bindRoutes']; -}; - function useConfigLoader( configLoader: AppConfigLoader | undefined, components: AppComponents, @@ -202,14 +192,16 @@ export class AppManager implements BackstageApp { private readonly identityApi = new AppIdentity(); private readonly apiFactoryRegistry: ApiFactoryRegistry; - constructor(options: FullAppOptions) { - this.apis = options.apis; + constructor(options: AppOptions) { + this.apis = options.apis ?? []; this.icons = options.icons; - this.plugins = new Set(options.plugins); + this.plugins = new Set( + (options.plugins as BackstagePlugin[]) ?? [], + ); this.components = options.components; this.themes = options.themes as AppTheme[]; - this.configLoader = options.configLoader; - this.defaultApis = options.defaultApis; + this.configLoader = options.configLoader ?? defaultConfigLoader; + this.defaultApis = options.defaultApis ?? []; this.bindRoutes = options.bindRoutes; this.apiFactoryRegistry = new ApiFactoryRegistry(); } diff --git a/packages/core-app-api/src/app/createApp.test.tsx b/packages/core-app-api/src/app/createApp.test.tsx deleted file mode 100644 index 0bf90987db..0000000000 --- a/packages/core-app-api/src/app/createApp.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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 { screen } from '@testing-library/react'; -import { renderWithEffects } from '@backstage/test-utils'; -import React, { PropsWithChildren } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { defaultConfigLoader, createApp } from './createApp'; - -(process as any).env = { NODE_ENV: 'test' }; -const anyEnv = process.env as any; -const anyWindow = window as any; - -describe('defaultConfigLoader', () => { - afterEach(() => { - delete anyEnv.APP_CONFIG; - delete anyWindow.__APP_CONFIG__; - }); - - it('loads static config', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]; - - const configs = await defaultConfigLoader(); - expect(configs).toEqual([ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]); - }); - - it('loads runtime config', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - ]; - - const configs = await (defaultConfigLoader as any)( - '{"my":"runtime-config"}', - ); - expect(configs).toEqual([ - { data: { my: 'override-config' }, context: 'a' }, - { data: { my: 'config' }, context: 'b' }, - { data: { my: 'runtime-config' }, context: 'env' }, - ]); - }); - - it('fails to load invalid missing config', async () => { - await expect(defaultConfigLoader()).rejects.toThrow( - 'No static configuration provided', - ); - }); - - it('fails to load invalid static config', async () => { - anyEnv.APP_CONFIG = { my: 'invalid-config' }; - await expect(defaultConfigLoader()).rejects.toThrow( - 'Static configuration has invalid format', - ); - }); - - it('fails to load bad runtime config', async () => { - anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }]; - - await expect((defaultConfigLoader as any)('}')).rejects.toThrow( - 'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0', - ); - }); - - it('loads config from window.__APP_CONFIG__', async () => { - anyEnv.APP_CONFIG = [ - { data: { my: 'config' }, context: 'a' }, - { data: { my: 'override-config' }, context: 'b' }, - ]; - const windowConfig = { app: { configKey: 'config-value' } }; - anyWindow.__APP_CONFIG__ = windowConfig; - - const configs = await defaultConfigLoader(); - - expect(configs).toEqual([ - ...anyEnv.APP_CONFIG, - { context: 'window', data: windowConfig }, - ]); - }); -}); - -describe('Optional ThemeProvider', () => { - it('should render app with user-provided ThemeProvider', async () => { - const components = { - NotFoundErrorPage: () => null, - BootErrorPage: () => null, - Progress: () => null, - Router: MemoryRouter, - ErrorBoundaryFallback: () => null, - ThemeProvider: ({ children }: PropsWithChildren<{}>) => ( -
{children}
- ), - }; - - const App = createApp({ components }).getProvider(); - - await renderWithEffects(); - - expect(screen.getByRole('main')).toBeInTheDocument(); - }); -}); diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index 5bb5607a37..a020308d91 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -14,132 +14,25 @@ * limitations under the License. */ -import { AppConfig } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; -import { withDefaults } from '@backstage/core-components'; -import { AppManager } from './AppManager'; -import { AppComponents, AppConfigLoader, AppOptions } from './types'; -import { defaultApis } from './defaultApis'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; - -const REQUIRED_APP_COMPONENTS: Array = [ - 'Progress', - 'Router', - 'NotFoundErrorPage', - 'BootErrorPage', - 'ErrorBoundaryFallback', -]; - -/** - * The default config loader, which expects that config is available at compile-time - * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as - * returned by the config loader. - * - * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string, - * which can be rewritten at runtime to contain an additional JSON config object. - * If runtime config is present, it will be placed first in the config array, overriding - * other config values. - * - * @public - */ -export const defaultConfigLoader: AppConfigLoader = async ( - // This string may be replaced at runtime to provide additional config. - // It should be replaced by a JSON-serialized config object. - // It's a param so we can test it, but at runtime this will always fall back to default. - runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__', -) => { - const appConfig = process.env.APP_CONFIG; - if (!appConfig) { - throw new Error('No static configuration provided'); - } - if (!Array.isArray(appConfig)) { - throw new Error('Static configuration has invalid format'); - } - const configs = appConfig.slice() as unknown as AppConfig[]; - - // Avoiding this string also being replaced at runtime - if ( - runtimeConfigJson !== - '__app_injected_runtime_config__'.toLocaleUpperCase('en-US') - ) { - try { - const data = JSON.parse(runtimeConfigJson) as JsonObject; - if (Array.isArray(data)) { - configs.push(...data); - } else { - configs.push({ data, context: 'env' }); - } - } catch (error) { - throw new Error(`Failed to load runtime configuration, ${error}`); - } - } - - const windowAppConfig = (window as any).__APP_CONFIG__; - if (windowAppConfig) { - configs.push({ - context: 'window', - data: windowAppConfig, - }); - } - return configs; -}; +import { + createApp as createDefaultApp, + OptionalAppOptions, +} from '@backstage/app-defaults'; /** * Creates a new Backstage App. * + * @deprecated Use createApp from @backstage/app-defaults instead + * @param options - A set of options for creating the app * @public */ -export function createApp(options?: AppOptions) { - const optionsWithDefaults = withDefaults(options); - - const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter( - name => !options?.components?.[name], +export function createApp(options?: OptionalAppOptions) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: The createApp function from @backstage/core-app-api will soon be removed, ' + + 'migrate to importing createApp from the @backstage/app-defaults package instead. ' + + 'If you do not wish to use a standard app configuration but instead supply all options yourself ' + + ' you can use createSpecializedApp from @backstage/core-app-api instead.', ); - 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. You can use the default components by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' + - `The following components are missing: ${missingRequiredComponents.join( - ', ', - )}`, - ); - } - - const providedIconKeys = Object.keys(options?.icons ?? {}); - const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter( - key => !providedIconKeys.includes(key), - ); - if (missingIconKeys.length > 0) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' + - 'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' + - `The following icons are missing: ${missingIconKeys.join(', ')}`, - ); - } - - if (!options?.themes) { - // eslint-disable-next-line no-console - console.warn( - 'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' + - 'You can use the default themes by using withDefaults from @backstage/core-components ' + - 'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ', - ); - } - - const { icons, themes, components } = optionsWithDefaults; - - return new AppManager({ - icons: icons!, - themes: themes!, - components: components! as AppComponents, - defaultApis, - apis: options?.apis ?? [], - bindRoutes: options?.bindRoutes, - plugins: (options?.plugins as BackstagePlugin[]) ?? [], - configLoader: options?.configLoader ?? defaultConfigLoader, - }); + return createDefaultApp(options); } diff --git a/packages/app-defaults/src/createApp/index.ts b/packages/core-app-api/src/app/createSpecializedApp.tsx similarity index 64% rename from packages/app-defaults/src/createApp/index.ts rename to packages/core-app-api/src/app/createSpecializedApp.tsx index dea36bb3e7..54e8d6cddf 100644 --- a/packages/app-defaults/src/createApp/index.ts +++ b/packages/core-app-api/src/app/createSpecializedApp.tsx @@ -14,4 +14,15 @@ * limitations under the License. */ -export { createApp } from './createApp'; +import { AppManager } from './AppManager'; +import { AppOptions } from './types'; + +/** + * Creates a new Backstage App where the full set of options are required. + * + * @param options - A set of options for creating the app + * @returns + */ +export function createSpecializedApp(options: AppOptions) { + return new AppManager(options); +} diff --git a/packages/core-app-api/src/app/defaultApis.ts b/packages/core-app-api/src/app/defaultApis.ts deleted file mode 100644 index fb02274cf0..0000000000 --- a/packages/core-app-api/src/app/defaultApis.ts +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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 { - AlertApiForwarder, - NoOpAnalyticsApi, - ErrorApiForwarder, - ErrorAlerter, - GoogleAuth, - GithubAuth, - OAuth2, - OktaAuth, - GitlabAuth, - Auth0Auth, - MicrosoftAuth, - BitbucketAuth, - OAuthRequestManager, - WebStorage, - UrlPatternDiscovery, - SamlAuth, - OneLoginAuth, - UnhandledErrorForwarder, - AtlassianAuth, -} from '../apis'; - -import { - createApiFactory, - alertApiRef, - analyticsApiRef, - errorApiRef, - discoveryApiRef, - oauthRequestApiRef, - googleAuthApiRef, - githubAuthApiRef, - oauth2ApiRef, - oktaAuthApiRef, - gitlabAuthApiRef, - auth0AuthApiRef, - microsoftAuthApiRef, - storageApiRef, - configApiRef, - samlAuthApiRef, - oneloginAuthApiRef, - oidcAuthApiRef, - bitbucketAuthApiRef, - atlassianAuthApiRef, -} from '@backstage/core-plugin-api'; - -import OAuth2Icon from '@material-ui/icons/AcUnit'; - -export const defaultApis = [ - createApiFactory({ - api: discoveryApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => - UrlPatternDiscovery.compile( - `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, - ), - }), - createApiFactory(alertApiRef, new AlertApiForwarder()), - createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()), - createApiFactory({ - api: errorApiRef, - deps: { alertApi: alertApiRef }, - factory: ({ alertApi }) => { - const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); - UnhandledErrorForwarder.forward(errorApi, { hidden: false }); - return errorApi; - }, - }), - createApiFactory({ - api: storageApiRef, - deps: { errorApi: errorApiRef }, - factory: ({ errorApi }) => WebStorage.create({ errorApi }), - }), - createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), - createApiFactory({ - api: googleAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GoogleAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: microsoftAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - MicrosoftAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: githubAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GithubAuth.create({ - discoveryApi, - oauthRequestApi, - defaultScopes: ['read:user'], - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oktaAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OktaAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: gitlabAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - GitlabAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oneloginAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OneLoginAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: OAuth2Icon, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: bitbucketAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - BitbucketAuth.create({ - discoveryApi, - oauthRequestApi, - defaultScopes: ['team'], - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: atlassianAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => { - return AtlassianAuth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }); - }, - }), -]; diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index 5514d7ee76..ba6a8917a4 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -15,6 +15,7 @@ */ export { createApp } from './createApp'; +export { createSpecializedApp } from './createSpecializedApp'; export { defaultConfigLoader } from './defaultConfigLoader'; export type { AppIcons } from './icons'; export * from './types'; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 41e997e179..89a70ed566 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -200,10 +200,19 @@ export type AppOptions = { */ apis?: Iterable; + /** + * A collection of ApiFactories to register in the application as default APIs. + * Theses APIs can not be overridden by plugin factories, but can be overridden + * by plugin APIs provided through the + * A collection of ApiFactories to register in the application to either + * add add new ones, or override factories provided by default or by plugins. + */ + defaultApis?: Iterable; + /** * Supply icons to override the default ones. */ - icons?: Partial & { [key in string]: IconComponent }; + icons: AppIcons & { [key in string]: IconComponent }; /** * A list of all plugins to include in the app. @@ -213,7 +222,7 @@ export type AppOptions = { /** * Supply components to the app to override the default ones. */ - components?: Partial; + components: AppComponents; /** * Themes provided as a part of the app. By default two themes are included, one @@ -245,7 +254,7 @@ export type AppOptions = { * }] * ``` */ - themes?: (Partial & Omit)[]; + themes: (Partial & Omit)[]; /** * A function that loads in App configuration that will be accessible via From 0fb57d28e36d6c26be419e431c61665daf4eb944 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 Oct 2021 15:31:59 +0200 Subject: [PATCH 16/35] dev-utils,test-utils,app: update to use app-defaults Signed-off-by: Patrik Oldsberg --- packages/app/package.json | 1 + packages/app/src/App.tsx | 80 +++++++++---------- packages/dev-utils/package.json | 1 + packages/dev-utils/src/devApp/render.tsx | 32 ++++---- .../test-utils/src/testUtils/appWrappers.tsx | 27 ++++++- 5 files changed, 81 insertions(+), 60 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 833e146d45..9a1fb108f6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -4,6 +4,7 @@ "private": true, "bundled": true, "dependencies": { + "@backstage/app-defaults": "^0.1.0", "@backstage/catalog-model": "^0.9.5", "@backstage/cli": "^0.8.0", "@backstage/core-app-api": "^0.1.18", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index d3ab0649e9..401665b62e 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -26,12 +26,12 @@ import { RELATION_PART_OF, RELATION_PROVIDES_API, } from '@backstage/catalog-model'; -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; import { AlertDisplay, OAuthRequestDialog, SignInPage, - withDefaults, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -87,46 +87,44 @@ import * as plugins from './plugins'; import { techDocsPage } from './components/techdocs/TechDocsPage'; -const app = createApp( - withDefaults({ - apis, - plugins: Object.values(plugins), - icons: { - // Custom icon example - alert: AlarmIcon, +const app = createApp({ + apis, + plugins: Object.values(plugins), + icons: { + // Custom icon example + alert: AlarmIcon, + }, + components: { + SignInPage: props => { + return ( + + ); }, - components: { - SignInPage: props => { - return ( - - ); - }, - }, - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(catalogGraphPlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - bind(apiDocsPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - bind(explorePlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - }); - }, - }), -); + }, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(catalogGraphPlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + bind(apiDocsPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + bind(explorePlugin.externalRoutes, { + catalogEntity: catalogPlugin.routes.catalogEntity, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + }, +}); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index e01390e169..0846ee8e4f 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -29,6 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/app-defaults": "^0.1.0", "@backstage/core-app-api": "^0.1.18", "@backstage/core-components": "^0.7.1", "@backstage/core-plugin-api": "^0.1.11", diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index bcaac891fe..d4b1c8db49 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -32,7 +32,6 @@ import { SidebarItem, SidebarPage, SidebarSpacer, - withDefaults, } from '@backstage/core-components'; import { @@ -48,7 +47,8 @@ import { BackstagePlugin, } from '@backstage/core-plugin-api'; -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; const GatheringRoute: (props: { path: string; @@ -174,22 +174,20 @@ export class DevAppBuilder { ); } - const app = createApp( - withDefaults({ - apis, - plugins: this.plugins, - themes: this.themes, - bindRoutes: ({ bind }) => { - for (const plugin of this.plugins ?? []) { - const targets: Record> = {}; - for (const routeKey of Object.keys(plugin.externalRoutes)) { - targets[routeKey] = dummyRouteRef; - } - bind(plugin.externalRoutes, targets); + const app = createApp({ + apis, + plugins: this.plugins, + themes: this.themes, + bindRoutes: ({ bind }) => { + for (const plugin of this.plugins ?? []) { + const targets: Record> = {}; + for (const routeKey of Object.keys(plugin.externalRoutes)) { + targets[routeKey] = dummyRouteRef; } - }, - }), - ); + bind(plugin.externalRoutes, targets); + } + }, + }); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 6f55cc6190..890d89e3fe 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -18,7 +18,7 @@ import React, { ComponentType, ReactNode, ReactElement } from 'react'; import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; -import { createApp } from '@backstage/core-app-api'; +import { createSpecializedApp } from '@backstage/core-app-api'; import { BootErrorPageProps, RouteRef, @@ -30,6 +30,28 @@ import { RenderResult } from '@testing-library/react'; import { renderWithEffects } from './testingLibrary'; import { mockApis } from './mockApis'; +const mockIcons = { + 'kind:api': () => , + 'kind:component': () => , + 'kind:domain': () => , + 'kind:group': () => , + 'kind:location': () => , + 'kind:system': () => , + 'kind:user': () => , + + brokenImage: () => , + catalog: () => , + chat: () => , + dashboard: () => , + docs: () => , + email: () => , + github: () => , + group: () => , + help: () => , + user: () => , + warning: () => , +}; + const ErrorBoundaryFallback = ({ error }: { error: Error }) => { throw new Error(`Reached ErrorBoundaryFallback Page with error, ${error}`); }; @@ -90,7 +112,7 @@ export function wrapInTestApp( const { routeEntries = ['/'] } = options; const boundRoutes = new Map(); - const app = createApp({ + const app = createSpecializedApp({ apis: mockApis, // Bit of a hack to make sure that the default config loader isn't used // as that would force every single test to wait for config loading. @@ -104,6 +126,7 @@ export function wrapInTestApp( ), }, + icons: mockIcons, plugins: [], themes: [ { From 687fca4ff76d9a7bf8fb9edf20f3ce39d421ee0b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 16:06:42 +0200 Subject: [PATCH 17/35] changesets,create-app: update for createApp split Signed-off-by: Patrik Oldsberg --- .changeset/giant-drinks-wave.md | 2 +- .changeset/hot-walls-fail.md | 17 ++------ .changeset/late-rice-sit.md | 5 --- .changeset/twenty-swans-matter.md | 12 ++---- .../default-app/packages/app/src/App.tsx | 41 ++++++++----------- 5 files changed, 26 insertions(+), 51 deletions(-) delete mode 100644 .changeset/late-rice-sit.md diff --git a/.changeset/giant-drinks-wave.md b/.changeset/giant-drinks-wave.md index 4c2f754c2c..ecf4c8fbdd 100644 --- a/.changeset/giant-drinks-wave.md +++ b/.changeset/giant-drinks-wave.md @@ -2,4 +2,4 @@ '@backstage/dev-utils': patch --- -Migrated to using `withDefaults` to pass defaults to `createApp`. +Migrated to using `@backstage/app-defaults`. diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md index c814354a56..b0854e868c 100644 --- a/.changeset/hot-walls-fail.md +++ b/.changeset/hot-walls-fail.md @@ -2,21 +2,12 @@ '@backstage/create-app': patch --- -Migrated the app template use the new `withDefaults` to construct the `createApp` options, as not doing this has been deprecated and will need to be done in the future. +Migrated the app template use the new `@backstage/app-defaults` for the `createApp` import, since the `createApp` exported by `@backstage/app-core-api` will be removed in the future. To migrate an existing application, make the following change to `packages/app/src/App.tsx`: ```diff -+import { withDefaults } from '@backstage/core-components'; - - // ... - --const app = createApp({ -+const app = createApp(withDefaults({ - apis, - bindRoutes({ bind }) { - ... - }, --}); -+})); +-import { createApp, FlatRoutes } from '@backstage/core-app-api'; ++import { createApp } from '@backstage/app-defaults'; ++import { FlatRoutes } from '@backstage/core-app-api'; ``` diff --git a/.changeset/late-rice-sit.md b/.changeset/late-rice-sit.md deleted file mode 100644 index 778215038f..0000000000 --- a/.changeset/late-rice-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Added a new `withDefaults` method that accepts a set of `AppOptions` and add the default components, themes and icons. It is intended to be used together with `createApp` from `@backstage/core-app-api` like this: `createApp(withDefaults({ ... }))`. diff --git a/.changeset/twenty-swans-matter.md b/.changeset/twenty-swans-matter.md index 6dea7a089f..55cd3d6dc3 100644 --- a/.changeset/twenty-swans-matter.md +++ b/.changeset/twenty-swans-matter.md @@ -2,14 +2,8 @@ '@backstage/core-app-api': patch --- -Deprecated the defaulting of the `components`, `icons` and `themes` options of `createApp`, meaning it will become required in the future. When not passing the required options a deprecation warning is currently logged, and they will become required in a future release. +The `createApp` function from `@backstage/core-app-api` has been deprecated, with two new options being provided as a replacement. -The keep using the default set of options, migrate to using `withDefaults` from `@backstage/core-components`: +The first and most commonly used one is `createApp` from the new `@backstage/app-defaults` package, which behaves just like the existing `createApp`. In the future this method is likely to be expanded to add more APIs and other pieces into the default setup, for example the Utility APIs from `@backstage/integration-react`. -```ts -const app = createApp( - withDefaults({ - // ... - }), -); -``` +The other option that we now provide is to use `createSpecializedApp` from `@backstage/core-app-api`. This is a more low-level API where you need to provide a full set of options, including your own `components`, `icons`, `defaultApis`, and `themes`. The `createSpecializedApp` way of creating an app is particularly useful if you are not using `@backstage/core-components` or MUI, as it allows you to avoid those dependencies completely. 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 a6d34e6687..8a535835b4 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,30 +25,25 @@ import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; -import { - AlertDisplay, - withDefaults, - OAuthRequestDialog, -} from '@backstage/core-components'; -import { createApp, FlatRoutes } from '@backstage/core-app-api'; +import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; +import { createApp } from '@backstage/app-defaults'; +import { FlatRoutes } from '@backstage/core-app-api'; -const app = createApp( - withDefaults({ - apis, - bindRoutes({ bind }) { - bind(catalogPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - viewTechDoc: techdocsPlugin.routes.docRoot, - }); - bind(apiDocsPlugin.externalRoutes, { - createComponent: scaffolderPlugin.routes.root, - }); - bind(scaffolderPlugin.externalRoutes, { - registerComponent: catalogImportPlugin.routes.importPage, - }); - }, - }), -); +const app = createApp({ + apis, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + viewTechDoc: techdocsPlugin.routes.docRoot, + }); + bind(apiDocsPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + bind(scaffolderPlugin.externalRoutes, { + registerComponent: catalogImportPlugin.routes.importPage, + }); + }, +}); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); From b7958c36dbc2608fb18c75b2f87ed7823b064b7f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 16:46:37 +0200 Subject: [PATCH 18/35] core-components: wrap page stories in test app Signed-off-by: Patrik Oldsberg --- .../src/layout/Page/Page.stories.tsx | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/packages/core-components/src/layout/Page/Page.stories.tsx b/packages/core-components/src/layout/Page/Page.stories.tsx index 7386d198a3..12ddd51d04 100644 --- a/packages/core-components/src/layout/Page/Page.stories.tsx +++ b/packages/core-components/src/layout/Page/Page.stories.tsx @@ -20,8 +20,7 @@ import Grid from '@material-ui/core/Grid'; import Link from '@material-ui/core/Link'; import Typography from '@material-ui/core/Typography'; import React, { useState } from 'react'; -import { MemoryRouter } from 'react-router'; -import { createApp } from '@backstage/core-app-api'; +import { wrapInTestApp } from '@backstage/test-utils'; import { Content, ContentHeader, @@ -197,53 +196,44 @@ const ExampleContentHeader = ({ selectedTab }: { selectedTab?: number }) => ( ); -const app = createApp({ configLoader: async () => [] }); -const AppProvider = app.getProvider(); - export const PluginWithData = () => { const [selectedTab, setSelectedTab] = useState(2); - return ( - - -
- - - setSelectedTab(index)} - tabs={tabs.map(({ label }, index) => ({ - id: index.toString(), - label, - }))} - /> - - - - - -
-
-
- ); + return wrapInTestApp(() => ( +
+ + + setSelectedTab(index)} + tabs={tabs.map(({ label }, index) => ({ + id: index.toString(), + label, + }))} + /> + + + + + +
+ )); }; export const PluginWithTable = () => { - return ( - -
- - - - - - - - - - ); + return wrapInTestApp(() => ( +
+ + + + +
+ + + + )); }; From c1578d609b1daf42f3aaece1eb69a9fbdf391f15 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 29 Oct 2021 16:49:45 +0200 Subject: [PATCH 19/35] update API reports for createApp split Signed-off-by: Patrik Oldsberg --- packages/app-defaults/api-report.md | 26 ++++++++++++++++++++ packages/core-app-api/api-report.md | 20 +++++++++++----- packages/core-components/api-report.md | 33 -------------------------- packages/core-plugin-api/api-report.md | 2 +- 4 files changed, 41 insertions(+), 40 deletions(-) create mode 100644 packages/app-defaults/api-report.md diff --git a/packages/app-defaults/api-report.md b/packages/app-defaults/api-report.md new file mode 100644 index 0000000000..6b35dab81b --- /dev/null +++ b/packages/app-defaults/api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/app-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AppComponents } from '@backstage/core-app-api'; +import { AppIcons } from '@backstage/core-app-api'; +import { AppManager } from '@backstage/core-app-api/src/app/AppManager'; +import { AppOptions } from '@backstage/core-app-api'; +import { AppTheme } from '@backstage/core-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; + +// @public +export function createApp( + options?: Omit & OptionalAppOptions, +): AppManager; + +// @public +export type OptionalAppOptions = { + icons?: Partial & { + [key in string]: IconComponent; + }; + themes?: (Partial & Omit)[]; + components?: Partial; +}; +``` diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index b4b1b07ebe..b3b54ff1a0 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -45,6 +45,7 @@ import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { OptionalAppOptions } from '@backstage/app-defaults'; import { PendingAuthRequest } from '@backstage/core-plugin-api'; import { PluginOutput } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; @@ -150,7 +151,7 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; + ThemeProvider?: ComponentType<{}>; SignInPage?: ComponentType; }; @@ -189,12 +190,13 @@ export type AppIcons = { // @public export type AppOptions = { apis?: Iterable; - icons?: Partial & { + defaultApis?: Iterable; + icons: AppIcons & { [key in string]: IconComponent; }; plugins?: BackstagePluginWithAnyOutput[]; - components?: Partial; - themes?: (Partial & Omit)[]; + components: AppComponents; + themes: (Partial & Omit)[]; configLoader?: AppConfigLoader; bindRoutes?(context: { bind: AppRouteBinder }): void; }; @@ -308,10 +310,16 @@ export type BootErrorPageProps = { export { ConfigReader }; -// Warning: (ae-forgotten-export) The symbol "PrivateAppImpl" needs to be exported by the entry point index.d.ts +// Warning: (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" +// Warning: (ae-forgotten-export) The symbol "AppManager" needs to be exported by the entry point index.d.ts +// +// @public @deprecated +export function createApp(options?: OptionalAppOptions): AppManager; + +// Warning: (ae-missing-release-tag) "createSpecializedApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function createApp(options?: AppOptions): PrivateAppImpl; +export function createSpecializedApp(options: AppOptions): AppManager; // @public export const defaultConfigLoader: AppConfigLoader; diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index f517b1673b..0f1e3510ae 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -5,26 +5,20 @@ ```ts /// -import { AnyApiFactory } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; -import { AppConfig } from '@backstage/config'; -import { AppTheme } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; -import { BackstagePlugin } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; import { Column } from '@material-table/core'; import { ComponentClass } from 'react'; import { ComponentProps } from 'react'; -import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; -import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { LinearProgressProps } from '@material-ui/core/LinearProgress'; import { LinkProps as LinkProps_2 } from '@material-ui/core/Link'; @@ -33,15 +27,12 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Overrides } from '@material-ui/core/styles/overrides'; -import { PluginOutput } from '@backstage/core-plugin-api'; -import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import * as React_3 from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; -import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; @@ -49,7 +40,6 @@ import { SparklinesProps } from 'react-sparklines'; import { StyledComponentProps } from '@material-ui/core/styles'; import { StyleRules } from '@material-ui/styles'; import { StyleRules as StyleRules_2 } from '@material-ui/core/styles/withStyles'; -import { SubRouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core/Tab'; import { TextTruncateProps } from 'react-text-truncate'; import { Theme } from '@material-ui/core/styles'; @@ -681,22 +671,6 @@ export type OAuthRequestDialogClassKey = // @public (undocumented) export type OpenedDropdownClassKey = 'icon'; -// @public -export interface OptionalAppOptions { - // Warning: (ae-forgotten-export) The symbol "AppComponents" needs to be exported by the entry point index.d.ts - // - // (undocumented) - components?: Partial; - // Warning: (ae-forgotten-export) The symbol "AppIcons" needs to be exported by the entry point index.d.ts - // - // (undocumented) - icons?: Partial & { - [key in string]: IconComponent; - }; - // (undocumented) - themes?: (Partial & Omit)[]; -} - // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "OverflowTooltip" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2344,13 +2318,6 @@ export type WarningPanelClassKey = | 'message' | 'details'; -// Warning: (ae-forgotten-export) The symbol "AppOptions" needs to be exported by the entry point index.d.ts -// -// @public -export function withDefaults( - options?: Omit & OptionalAppOptions, -): AppOptions; - // Warnings were encountered during analysis: // // src/components/DependencyGraph/types.d.ts:16:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "DependencyNode" diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 433edc57ff..2765b02bca 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -164,7 +164,7 @@ export type AppComponents = { Progress: ComponentType<{}>; Router: ComponentType<{}>; ErrorBoundaryFallback: ComponentType; - ThemeProvider: ComponentType<{}>; + ThemeProvider?: ComponentType<{}>; SignInPage?: ComponentType; }; From 60e5d356cbab4f5049111cacd57e374bca1ec74f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 01:30:07 +0100 Subject: [PATCH 20/35] core-app-api: add missing core feature icons + move icon things around a bit Signed-off-by: Patrik Oldsberg --- packages/app-defaults/src/defaults/icons.tsx | 6 ++ packages/core-app-api/api-report.md | 5 +- .../core-app-api/src/app/AppManager.test.tsx | 15 ++-- packages/core-app-api/src/app/icons.tsx | 87 ------------------- packages/core-app-api/src/app/index.ts | 1 - packages/core-app-api/src/app/types.ts | 31 ++++++- .../test-utils/src/testUtils/appWrappers.tsx | 3 + 7 files changed, 51 insertions(+), 97 deletions(-) delete mode 100644 packages/core-app-api/src/app/icons.tsx diff --git a/packages/app-defaults/src/defaults/icons.tsx b/packages/app-defaults/src/defaults/icons.tsx index fa578a263a..c4f2d2b7e4 100644 --- a/packages/app-defaults/src/defaults/icons.tsx +++ b/packages/app-defaults/src/defaults/icons.tsx @@ -18,6 +18,9 @@ import { IconComponent } from '@backstage/core-plugin-api'; import MuiApartmentIcon from '@material-ui/icons/Apartment'; import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; import MuiCategoryIcon from '@material-ui/icons/Category'; +import MuiCreateNewFolderIcon from '@material-ui/icons/CreateNewFolder'; +import MuiSubjectIcon from '@material-ui/icons/Subject'; +import MuiSearchIcon from '@material-ui/icons/Search'; import MuiChatIcon from '@material-ui/icons/Chat'; import MuiDashboardIcon from '@material-ui/icons/Dashboard'; import MuiDocsIcon from '@material-ui/icons/Description'; @@ -36,6 +39,9 @@ export const icons = { brokenImage: MuiBrokenImageIcon as IconComponent, // To be confirmed: see https://github.com/backstage/backstage/issues/4970 catalog: MuiMenuBookIcon as IconComponent, + scaffolder: MuiCreateNewFolderIcon as IconComponent, + techdocs: MuiSubjectIcon as IconComponent, + search: MuiSearchIcon as IconComponent, chat: MuiChatIcon as IconComponent, dashboard: MuiDashboardIcon as IconComponent, docs: MuiDocsIcon as IconComponent, diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index b3b54ff1a0..f45fc349b2 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -165,7 +165,7 @@ export type AppContext = { getComponents(): AppComponents; }; -// @public (undocumented) +// @public export type AppIcons = { 'kind:api': IconComponent; 'kind:component': IconComponent; @@ -183,6 +183,9 @@ export type AppIcons = { github: IconComponent; group: IconComponent; help: IconComponent; + scaffolder: IconComponent; + search: IconComponent; + techdocs: IconComponent; user: IconComponent; warning: IconComponent; }; diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 934ca3295f..71df5219d1 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -24,7 +24,6 @@ import { lightTheme } from '@backstage/theme'; import { render, screen } from '@testing-library/react'; import React, { PropsWithChildren } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; -import { defaultAppIcons } from './icons'; import { configApiRef, createApiFactory, @@ -38,7 +37,7 @@ import { analyticsApiRef, } from '@backstage/core-plugin-api'; import { generateBoundRoutes, AppManager } from './AppManager'; -import { AppComponents } from './types'; +import { AppComponents, AppIcons } from './types'; describe('generateBoundRoutes', () => { it('runs happy path', () => { @@ -187,6 +186,8 @@ describe('Integration Test', () => { ThemeProvider: ({ children }) => <>{children}, }; + const icons = {} as AppIcons; + it('runs happy paths', async () => { const app = new AppManager({ apis: [noOpAnalyticsApi], @@ -199,7 +200,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, bindRoutes: ({ bind }) => { @@ -253,7 +254,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, bindRoutes: ({ bind }) => { @@ -310,7 +311,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [ createPlugin({ id: 'test', @@ -360,7 +361,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, bindRoutes: ({ bind }) => { @@ -420,7 +421,7 @@ describe('Integration Test', () => { theme: lightTheme, }, ], - icons: defaultAppIcons, + icons, plugins: [], components, bindRoutes: ({ bind }) => { diff --git a/packages/core-app-api/src/app/icons.tsx b/packages/core-app-api/src/app/icons.tsx deleted file mode 100644 index 1d9bb12923..0000000000 --- a/packages/core-app-api/src/app/icons.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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 { IconComponent } from '@backstage/core-plugin-api'; -import MuiApartmentIcon from '@material-ui/icons/Apartment'; -import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; -import MuiCategoryIcon from '@material-ui/icons/Category'; -import MuiCreateNewFolderIcon from '@material-ui/icons/CreateNewFolder'; -import MuiSubjectIcon from '@material-ui/icons/Subject'; -import MuiSearchIcon from '@material-ui/icons/Search'; -import MuiChatIcon from '@material-ui/icons/Chat'; -import MuiDashboardIcon from '@material-ui/icons/Dashboard'; -import MuiDocsIcon from '@material-ui/icons/Description'; -import MuiEmailIcon from '@material-ui/icons/Email'; -import MuiExtensionIcon from '@material-ui/icons/Extension'; -import MuiGitHubIcon from '@material-ui/icons/GitHub'; -import MuiHelpIcon from '@material-ui/icons/Help'; -import MuiLocationOnIcon from '@material-ui/icons/LocationOn'; -import MuiMemoryIcon from '@material-ui/icons/Memory'; -import MuiMenuBookIcon from '@material-ui/icons/MenuBook'; -import MuiPeopleIcon from '@material-ui/icons/People'; -import MuiPersonIcon from '@material-ui/icons/Person'; -import MuiWarningIcon from '@material-ui/icons/Warning'; - -/** @public */ -export type AppIcons = { - 'kind:api': IconComponent; - 'kind:component': IconComponent; - 'kind:domain': IconComponent; - 'kind:group': IconComponent; - 'kind:location': IconComponent; - 'kind:system': IconComponent; - 'kind:user': IconComponent; - - brokenImage: IconComponent; - catalog: IconComponent; - chat: IconComponent; - dashboard: IconComponent; - docs: IconComponent; - email: IconComponent; - github: IconComponent; - group: IconComponent; - help: IconComponent; - scaffolder: IconComponent; - search: IconComponent; - techdocs: IconComponent; - user: IconComponent; - warning: IconComponent; -}; - -export const defaultAppIcons: AppIcons = { - brokenImage: MuiBrokenImageIcon, - // To be confirmed: see https://github.com/backstage/backstage/issues/4970 - catalog: MuiMenuBookIcon, - scaffolder: MuiCreateNewFolderIcon, - techdocs: MuiSubjectIcon, - search: MuiSearchIcon, - chat: MuiChatIcon, - dashboard: MuiDashboardIcon, - docs: MuiDocsIcon, - email: MuiEmailIcon, - github: MuiGitHubIcon, - group: MuiPeopleIcon, - help: MuiHelpIcon, - 'kind:api': MuiExtensionIcon, - 'kind:component': MuiMemoryIcon, - 'kind:domain': MuiApartmentIcon, - 'kind:group': MuiPeopleIcon, - 'kind:location': MuiLocationOnIcon, - 'kind:system': MuiCategoryIcon, - 'kind:user': MuiPersonIcon, - user: MuiPersonIcon, - warning: MuiWarningIcon, -}; diff --git a/packages/core-app-api/src/app/index.ts b/packages/core-app-api/src/app/index.ts index ba6a8917a4..5ea5405632 100644 --- a/packages/core-app-api/src/app/index.ts +++ b/packages/core-app-api/src/app/index.ts @@ -17,5 +17,4 @@ export { createApp } from './createApp'; export { createSpecializedApp } from './createSpecializedApp'; export { defaultConfigLoader } from './defaultConfigLoader'; -export type { AppIcons } from './icons'; export * from './types'; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 89a70ed566..86389a0105 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -27,7 +27,6 @@ import { PluginOutput, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; -import { AppIcons } from './icons'; /** * Props for the `BootErrorPage` component of {@link AppComponents}. @@ -111,6 +110,36 @@ export type AppComponents = { SignInPage?: ComponentType; }; +/** + * A set of well-known icons that should be available within an app. + * + * @public + */ +export type AppIcons = { + 'kind:api': IconComponent; + 'kind:component': IconComponent; + 'kind:domain': IconComponent; + 'kind:group': IconComponent; + 'kind:location': IconComponent; + 'kind:system': IconComponent; + 'kind:user': IconComponent; + + brokenImage: IconComponent; + catalog: IconComponent; + chat: IconComponent; + dashboard: IconComponent; + docs: IconComponent; + email: IconComponent; + github: IconComponent; + group: IconComponent; + help: IconComponent; + scaffolder: IconComponent; + search: IconComponent; + techdocs: IconComponent; + user: IconComponent; + warning: IconComponent; +}; + /** * A function that loads in the App config that will be accessible via the ConfigApi. * diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 890d89e3fe..b84c06c77b 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -41,6 +41,9 @@ const mockIcons = { brokenImage: () => , catalog: () => , + scaffolder: () => , + techdocs: () => , + search: () => , chat: () => , dashboard: () => , docs: () => , From a3b6707eb2f6a6e793ccd27906ebd62c2beba800 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 10:25:29 +0100 Subject: [PATCH 21/35] app-defaults: bump internal deps Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 19f4e1432e..6c06927daf 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.1", - "@backstage/config": "^0.1.10", - "@backstage/core-app-api": "^0.1.11", - "@backstage/core-plugin-api": "^0.1.11", - "@backstage/theme": "^0.2.11", + "@backstage/core-components": "^0.7.2", + "@backstage/config": "^0.1.11", + "@backstage/core-app-api": "^0.1.19", + "@backstage/core-plugin-api": "^0.1.12", + "@backstage/theme": "^0.2.12", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", @@ -47,8 +47,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.8.0", - "@backstage/test-utils": "^0.1.19", + "@backstage/cli": "^0.8.1", + "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", From 7ca03b85ab02792e2af5c5909c01610de0166123 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 10:25:53 +0100 Subject: [PATCH 22/35] core-app-api: createSpecializedApp returns a BackstageApp Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/createSpecializedApp.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-app-api/src/app/createSpecializedApp.tsx b/packages/core-app-api/src/app/createSpecializedApp.tsx index 54e8d6cddf..a437835f39 100644 --- a/packages/core-app-api/src/app/createSpecializedApp.tsx +++ b/packages/core-app-api/src/app/createSpecializedApp.tsx @@ -15,7 +15,7 @@ */ import { AppManager } from './AppManager'; -import { AppOptions } from './types'; +import { AppOptions, BackstageApp } from './types'; /** * Creates a new Backstage App where the full set of options are required. @@ -23,6 +23,6 @@ import { AppOptions } from './types'; * @param options - A set of options for creating the app * @returns */ -export function createSpecializedApp(options: AppOptions) { +export function createSpecializedApp(options: AppOptions): BackstageApp { return new AppManager(options); } From c98c5c3d000ad5452a85b8054afca1e1fd3b0f1d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 10:40:41 +0100 Subject: [PATCH 23/35] core-app-api: switch auth provider icons to be empty by default Signed-off-by: Patrik Oldsberg --- packages/app-defaults/src/defaults/apis.ts | 4 +--- .../implementations/OAuthRequestApi/MockOAuthApi.test.ts | 9 ++++----- .../OAuthRequestApi/OAuthRequestManager.test.ts | 3 +-- .../apis/implementations/auth/atlassian/AtlassianAuth.ts | 3 +-- .../src/apis/implementations/auth/auth0/Auth0Auth.ts | 3 +-- .../apis/implementations/auth/bitbucket/BitbucketAuth.ts | 3 +-- .../src/apis/implementations/auth/github/GithubAuth.ts | 3 +-- .../src/apis/implementations/auth/gitlab/GitlabAuth.ts | 3 +-- .../src/apis/implementations/auth/google/GoogleAuth.ts | 3 +-- .../apis/implementations/auth/microsoft/MicrosoftAuth.ts | 3 +-- .../src/apis/implementations/auth/oauth2/OAuth2.ts | 3 +-- .../src/apis/implementations/auth/okta/OktaAuth.ts | 3 +-- .../apis/implementations/auth/onelogin/OneLoginAuth.ts | 3 +-- .../src/apis/implementations/auth/saml/SamlAuth.ts | 3 +-- .../src/lib/AuthConnector/DefaultAuthConnector.test.ts | 3 +-- 15 files changed, 18 insertions(+), 34 deletions(-) diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 2596ac2593..85c004eece 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -59,8 +59,6 @@ import { atlassianAuthApiRef, } from '@backstage/core-plugin-api'; -import OAuth2Icon from '@material-ui/icons/AcUnit'; - export const apis = [ createApiFactory({ api: discoveryApiRef, @@ -226,7 +224,7 @@ export const apis = [ provider: { id: 'oidc', title: 'Your Identity Provider', - icon: OAuth2Icon, + icon: () => null, }, environment: configApi.getOptionalString('auth.environment'), }), diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts index d0f137f0be..76cf01d20a 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts @@ -15,7 +15,6 @@ */ import MockOAuthApi from './MockOAuthApi'; -import PowerIcon from '@material-ui/icons/Power'; describe('MockOAuthApi', () => { it('should trigger all requests', async () => { @@ -24,13 +23,13 @@ describe('MockOAuthApi', () => { const authHandler1 = jest.fn().mockImplementation(() => authResult); const requester1 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler1, }); const authHandler2 = jest.fn().mockResolvedValue('other'); const requester2 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler2, }); @@ -67,13 +66,13 @@ describe('MockOAuthApi', () => { const authHandler1 = jest.fn(); const requester1 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler1, }); const authHandler2 = jest.fn(); const requester2 = mock.createAuthRequester({ - provider: { icon: PowerIcon, title: 'Test' }, + provider: { icon: () => null, title: 'Test' }, onAuthRequest: authHandler2, }); diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts index d8faf90229..3110197ad9 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import ProviderIcon from '@material-ui/icons/AcUnit'; import { OAuthRequestManager } from './OAuthRequestManager'; describe('OAuthRequestManager', () => { @@ -27,7 +26,7 @@ describe('OAuthRequestManager', () => { const requester = manager.createAuthRequester({ provider: { title: 'My Provider', - icon: ProviderIcon, + icon: () => null, }, onAuthRequest: async () => 'hello', }); diff --git a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts index 227d70e494..caad40ddbf 100644 --- a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import AtlassianIcon from '@material-ui/icons/AcUnit'; import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'atlassian', title: 'Atlassian', - icon: AtlassianIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts index 69462942e4..0a158ccb9e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import Auth0Icon from '@material-ui/icons/AcUnit'; import { auth0AuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'auth0', title: 'Auth0', - icon: Auth0Icon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts index c9db93906a..e488580c4d 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import BitbucketIcon from '@material-ui/icons/FormatBold'; import { BackstageIdentity, bitbucketAuthApiRef, @@ -37,7 +36,7 @@ export type BitbucketAuthResponse = { const DEFAULT_PROVIDER = { id: 'bitbucket', title: 'Bitbucket', - icon: BitbucketIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index 01e7e77b8e..885e80da7c 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import GithubIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { GithubSession } from './types'; import { @@ -48,7 +47,7 @@ export type GithubAuthResponse = { const DEFAULT_PROVIDER = { id: 'github', title: 'GitHub', - icon: GithubIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts index af581d0018..f46f2ef72e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import GitlabIcon from '@material-ui/icons/AcUnit'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'gitlab', title: 'GitLab', - icon: GitlabIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts index 916c52382e..a6f37b250f 100644 --- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import GoogleIcon from '@material-ui/icons/AcUnit'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'google', title: 'Google', - icon: GoogleIcon, + icon: () => null, }; const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index 09bd7f67a2..148be66873 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import MicrosoftIcon from '@material-ui/icons/AcUnit'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'microsoft', title: 'Microsoft', - icon: MicrosoftIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 6c0759d967..45937a68f8 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import OAuth2Icon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -51,7 +50,7 @@ export type OAuth2Response = { const DEFAULT_PROVIDER = { id: 'oauth2', title: 'Your Identity Provider', - icon: OAuth2Icon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts index 294fb496af..465a124051 100644 --- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import OktaIcon from '@material-ui/icons/AcUnit'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; @@ -22,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'okta', title: 'Okta', - icon: OktaIcon, + icon: () => null, }; const OKTA_OIDC_SCOPES: Set = new Set([ diff --git a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts index f49ea4cde8..5f933b9c6e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import OneLoginIcon from '@material-ui/icons/AcUnit'; import { oneloginAuthApiRef, OAuthRequestApi, @@ -33,7 +32,7 @@ type CreateOptions = { const DEFAULT_PROVIDER = { id: 'onelogin', title: 'onelogin', - icon: OneLoginIcon, + icon: () => null, }; const OIDC_SCOPES: Set = new Set([ diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts index f2668dedd2..63985979de 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import SamlIcon from '@material-ui/icons/AcUnit'; import { DirectAuthConnector } from '../../../../lib/AuthConnector'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { @@ -42,7 +41,7 @@ export type SamlAuthResponse = { const DEFAULT_PROVIDER = { id: 'saml', title: 'SAML', - icon: SamlIcon, + icon: () => null, }; /** diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 7e448586e3..baf89a59d8 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import ProviderIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from './DefaultAuthConnector'; import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; @@ -29,7 +28,7 @@ const defaultOptions = { provider: { id: 'my-provider', title: 'My Provider', - icon: ProviderIcon, + icon: () => null, }, oauthRequestApi: new MockOAuthApi(), sessionTransform: ({ expiresInSeconds, ...res }: any) => ({ From 03da4c09469ddf94760925d5b9d20f233eedfa78 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 11:23:46 +0100 Subject: [PATCH 24/35] create-app: add app-defaults dependency to app + notice in changeset Signed-off-by: Patrik Oldsberg --- .changeset/hot-walls-fail.md | 2 +- packages/create-app/src/lib/versions.ts | 2 ++ .../templates/default-app/packages/app/package.json.hbs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.changeset/hot-walls-fail.md b/.changeset/hot-walls-fail.md index b0854e868c..3728e10ff6 100644 --- a/.changeset/hot-walls-fail.md +++ b/.changeset/hot-walls-fail.md @@ -4,7 +4,7 @@ Migrated the app template use the new `@backstage/app-defaults` for the `createApp` import, since the `createApp` exported by `@backstage/app-core-api` will be removed in the future. -To migrate an existing application, make the following change to `packages/app/src/App.tsx`: +To migrate an existing application, add the latest version of `@backstage/app-defaults` as a dependency in `packages/app/package.json`, and make the following change to `packages/app/src/App.tsx`: ```diff -import { createApp, FlatRoutes } from '@backstage/core-app-api'; diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 97904cc15b..6c85652c64 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -30,6 +30,7 @@ Rollup will extract the value of the version field in each package at build time leaving any imports in place. */ +import { version as appDefaults } from '../../../app-defaults/package.json'; import { version as backendCommon } from '../../../backend-common/package.json'; import { version as catalogClient } from '../../../catalog-client/package.json'; import { version as catalogModel } from '../../../catalog-model/package.json'; @@ -68,6 +69,7 @@ import { version as pluginTechdocsBackend } from '../../../../plugins/techdocs-b import { version as pluginUserSettings } from '../../../../plugins/user-settings/package.json'; export const packageVersions = { + '@backstage/app-defaults': appDefaults, '@backstage/backend-common': backendCommon, '@backstage/catalog-client': catalogClient, '@backstage/catalog-model': catalogModel, diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index c443b45dc7..9e15608a01 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -4,6 +4,7 @@ "private": true, "bundled": true, "dependencies": { + "@backstage/app-defaults": "^{{version '@backstage/app-defaults'}}", "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", "@backstage/cli": "^{{version '@backstage/cli'}}", "@backstage/core-app-api": "^{{version '@backstage/core-app-api'}}", From f2bd2419726a0025b1af699f437f628556bb1e3b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 11:41:31 +0100 Subject: [PATCH 25/35] docs: introduce deprecation docs + app-theme deprecation Signed-off-by: Patrik Oldsberg --- docs/api/deprecations.md | 56 +++++++++++++++++++ microsite/sidebars.json | 3 +- mkdocs.yml | 1 + .../core-app-api/src/app/AppThemeProvider.tsx | 2 +- .../src/apis/definitions/AppThemeApi.ts | 2 +- 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 docs/api/deprecations.md diff --git a/docs/api/deprecations.md b/docs/api/deprecations.md new file mode 100644 index 0000000000..774c8ef03d --- /dev/null +++ b/docs/api/deprecations.md @@ -0,0 +1,56 @@ +--- +id: deprecations +title: Deprecations +description: A list of active and past deprecations +--- + +## Introduction + +This page contains extended documentation for some of the deprecations in +various parts of Backstage. It is not an exhaustive list as most deprecation +only come in the form of a changelog notice and a console warning. The +deprecations listed here are the ones that need a bit more guidance than what +fits in a console message. + +### App Theme + +`Released 2021-11-12 in @backstage/core-plugin-api v0.1.13` + +In order to provide more flexibility in what types of themes can be used and how +they are applied, the `theme` property on the `AppTheme` type is being +deprecated and replaced by a `Provider` property instead. The `Provider` +property is a React component that will be mounted at the root of the app +whenever that theme is active. This also removes the tight connection to MUI and +opens up for other type of themes, and removes the hardcoded usage of +``. + +To migrate an existing theme, remove the `theme` property and move it over to a +new `Provider` component, using `ThemeProvider` from MUI to provide the new +theme, along with ``. For example a theme that currently looks like +this: + +```tsx +const darkTheme = { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + theme: darkTheme, +}; +``` + +Would be migrated to the following: + +```tsx +const darkTheme = { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + Provider: ({ children }) => ( + + {children} + + ), +}; +``` diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 91525d4938..ec5c5d8d68 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -256,7 +256,8 @@ "type": "subcategory", "label": "API Reference", "ids": ["reference/index"] - } + }, + "api/deprecations" ], "Tutorials": [ "tutorials/journey", diff --git a/mkdocs.yml b/mkdocs.yml index 0ddae446e6..8c7114abbc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -164,6 +164,7 @@ nav: - API Reference: - Guides: - Utility APIs: 'api/utility-apis.md' + - Deprecations: 'api/deprecations.md' - Tutorials: - Future developer journey: 'tutorials/journey.md' - Migrating away from @backstage/core: 'tutorials/migrating-away-from-core.md' diff --git a/packages/core-app-api/src/app/AppThemeProvider.tsx b/packages/core-app-api/src/app/AppThemeProvider.tsx index aa48a2187d..2e145025d5 100644 --- a/packages/core-app-api/src/app/AppThemeProvider.tsx +++ b/packages/core-app-api/src/app/AppThemeProvider.tsx @@ -98,7 +98,7 @@ export function AppThemeProvider({ children }: PropsWithChildren<{}>) { console.warn( "DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " + 'and should be migrated to use a Provider instead. ' + - 'See https://backstage.io/docs/deprecations/TODO for more info.', + 'See https://backstage.io/docs/api/deprecations#app-theme for more info.', ); return ( diff --git a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts index 52cf202422..8053c449a1 100644 --- a/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AppThemeApi.ts @@ -42,7 +42,7 @@ export type AppTheme = { /** * The specialized MaterialUI theme instance. - * @deprecated use Provider instead, see https://backstage.io/docs/deprecations/TODO + * @deprecated use Provider instead, see https://backstage.io/docs/api/deprecations#app-theme */ theme: BackstageTheme; From 465dffb05c704d8922acd2ea58383b357be93a41 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 11:48:52 +0100 Subject: [PATCH 26/35] core-app-api,app-defaults: API report and doc fixups Signed-off-by: Patrik Oldsberg --- packages/app-defaults/api-report.md | 4 ++-- packages/core-app-api/api-report.md | 9 ++------- packages/core-app-api/src/app/createApp.tsx | 2 +- packages/core-app-api/src/app/createSpecializedApp.tsx | 1 + 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/app-defaults/api-report.md b/packages/app-defaults/api-report.md index 6b35dab81b..57ea61ad8b 100644 --- a/packages/app-defaults/api-report.md +++ b/packages/app-defaults/api-report.md @@ -5,15 +5,15 @@ ```ts import { AppComponents } from '@backstage/core-app-api'; import { AppIcons } from '@backstage/core-app-api'; -import { AppManager } from '@backstage/core-app-api/src/app/AppManager'; import { AppOptions } from '@backstage/core-app-api'; import { AppTheme } from '@backstage/core-plugin-api'; +import { BackstageApp } from '@backstage/core-app-api'; import { IconComponent } from '@backstage/core-plugin-api'; // @public export function createApp( options?: Omit & OptionalAppOptions, -): AppManager; +): BackstageApp; // @public export type OptionalAppOptions = { diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index f45fc349b2..d98b5ef5f8 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -313,16 +313,11 @@ export type BootErrorPageProps = { export { ConfigReader }; -// Warning: (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" -// Warning: (ae-forgotten-export) The symbol "AppManager" needs to be exported by the entry point index.d.ts -// // @public @deprecated -export function createApp(options?: OptionalAppOptions): AppManager; +export function createApp(options?: OptionalAppOptions): BackstageApp; -// Warning: (ae-missing-release-tag) "createSpecializedApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function createSpecializedApp(options: AppOptions): AppManager; +export function createSpecializedApp(options: AppOptions): BackstageApp; // @public export const defaultConfigLoader: AppConfigLoader; diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index a020308d91..e11076099b 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -22,7 +22,7 @@ import { /** * Creates a new Backstage App. * - * @deprecated Use createApp from @backstage/app-defaults instead + * @deprecated Use {@link @backstage/app-defaults#createApp} from `@backstage/app-defaults` instead * @param options - A set of options for creating the app * @public */ diff --git a/packages/core-app-api/src/app/createSpecializedApp.tsx b/packages/core-app-api/src/app/createSpecializedApp.tsx index a437835f39..9bda223722 100644 --- a/packages/core-app-api/src/app/createSpecializedApp.tsx +++ b/packages/core-app-api/src/app/createSpecializedApp.tsx @@ -20,6 +20,7 @@ import { AppOptions, BackstageApp } from './types'; /** * Creates a new Backstage App where the full set of options are required. * + * @public * @param options - A set of options for creating the app * @returns */ From cdbddf196f6ac9e4aeca2bc854ffc425b8ff937d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 11:56:29 +0100 Subject: [PATCH 27/35] app-defaults: clean up dependencies Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 16 ++-------------- packages/app-defaults/src/setupTests.ts | 1 - 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 6c06927daf..76101f0ad4 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -30,34 +30,22 @@ }, "dependencies": { "@backstage/core-components": "^0.7.2", - "@backstage/config": "^0.1.11", "@backstage/core-app-api": "^0.1.19", "@backstage/core-plugin-api": "^0.1.12", "@backstage/theme": "^0.2.12", - "@backstage/types": "^0.1.1", - "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@types/react": "*", - "@types/prop-types": "^15.7.3", - "prop-types": "^15.7.2", "react": "^16.12.0", - "react-router-dom": "6.0.0-beta.0", - "react-use": "^17.2.4", - "zen-observable": "^0.8.15" + "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { "@backstage/cli": "^0.8.1", "@backstage/test-utils": "^0.1.20", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", - "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/zen-observable": "^0.8.0", - "cross-fetch": "^3.0.6", - "msw": "^0.29.0" + "@types/react": "*" }, "files": [ "dist" diff --git a/packages/app-defaults/src/setupTests.ts b/packages/app-defaults/src/setupTests.ts index c1d649f2ad..963c0f188b 100644 --- a/packages/app-defaults/src/setupTests.ts +++ b/packages/app-defaults/src/setupTests.ts @@ -15,4 +15,3 @@ */ import '@testing-library/jest-dom'; -import 'cross-fetch/polyfill'; From 2dd2a7b2ccc2cd063d086b93ae2b7b8d68661104 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 12:04:27 +0100 Subject: [PATCH 28/35] changesets: add changesets for test-utils createApp and AppTheme deprecation Signed-off-by: Patrik Oldsberg --- .changeset/sharp-moons-jog.md | 5 +++++ .changeset/slow-moles-act.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/sharp-moons-jog.md create mode 100644 .changeset/slow-moles-act.md diff --git a/.changeset/sharp-moons-jog.md b/.changeset/sharp-moons-jog.md new file mode 100644 index 0000000000..d3329af08e --- /dev/null +++ b/.changeset/sharp-moons-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Deprecated the `theme` property on `AppTheme`, replacing it with `Provider`. See https://backstage.io/docs/api/deprecations#app-theme for more details. diff --git a/.changeset/slow-moles-act.md b/.changeset/slow-moles-act.md new file mode 100644 index 0000000000..ab920bfa5d --- /dev/null +++ b/.changeset/slow-moles-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +Migrated to using `createSpecializedApp`. From 29eb8d7dbee876056be94268ea68ff152a083612 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 15:08:32 +0100 Subject: [PATCH 29/35] test-utils: switch mock icons to be plain svg elements Signed-off-by: Patrik Oldsberg --- .../test-utils/src/testUtils/appWrappers.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index b84c06c77b..7a8d913273 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -31,28 +31,28 @@ import { renderWithEffects } from './testingLibrary'; import { mockApis } from './mockApis'; const mockIcons = { - 'kind:api': () => , - 'kind:component': () => , - 'kind:domain': () => , - 'kind:group': () => , - 'kind:location': () => , - 'kind:system': () => , - 'kind:user': () => , + 'kind:api': () => , + 'kind:component': () => , + 'kind:domain': () => , + 'kind:group': () => , + 'kind:location': () => , + 'kind:system': () => , + 'kind:user': () => , - brokenImage: () => , - catalog: () => , - scaffolder: () => , - techdocs: () => , - search: () => , - chat: () => , - dashboard: () => , - docs: () => , - email: () => , - github: () => , - group: () => , - help: () => , - user: () => , - warning: () => , + brokenImage: () => , + catalog: () => , + scaffolder: () => , + techdocs: () => , + search: () => , + chat: () => , + dashboard: () => , + docs: () => , + email: () => , + github: () => , + group: () => , + help: () => , + user: () => , + warning: () => , }; const ErrorBoundaryFallback = ({ error }: { error: Error }) => { From 569acdb8f8bd7b352d02274466390044c4085725 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 15:19:45 +0100 Subject: [PATCH 30/35] core-app-api: fix fix docs docs Signed-off-by: Patrik Oldsberg --- packages/core-app-api/src/app/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 86389a0105..f2d1976f87 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -225,7 +225,7 @@ export type BackstagePluginWithAnyOutput = Omit< export type AppOptions = { /** * A collection of ApiFactories to register in the application to either - * add add new ones, or override factories provided by default or by plugins. + * add new ones, or override factories provided by default or by plugins. */ apis?: Iterable; @@ -234,7 +234,7 @@ export type AppOptions = { * Theses APIs can not be overridden by plugin factories, but can be overridden * by plugin APIs provided through the * A collection of ApiFactories to register in the application to either - * add add new ones, or override factories provided by default or by plugins. + * add new ones, or override factories provided by default or by plugins. */ defaultApis?: Iterable; From 6d790f0f637c9c12e1f0ab4a38dc675aeaeefc14 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 15:21:08 +0100 Subject: [PATCH 31/35] test-utils: reintroduce defaultApis Signed-off-by: Patrik Oldsberg --- .../test-utils/src/testUtils/appWrappers.tsx | 2 + .../test-utils/src/testUtils/defaultApis.ts | 264 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 packages/test-utils/src/testUtils/defaultApis.ts diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 7a8d913273..ab96887a78 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -28,6 +28,7 @@ import { } from '@backstage/core-plugin-api'; import { RenderResult } from '@testing-library/react'; import { renderWithEffects } from './testingLibrary'; +import { defaultApis } from './defaultApis'; import { mockApis } from './mockApis'; const mockIcons = { @@ -117,6 +118,7 @@ export function wrapInTestApp( const app = createSpecializedApp({ apis: mockApis, + defaultApis, // Bit of a hack to make sure that the default config loader isn't used // as that would force every single test to wait for config loading. configLoader: false as unknown as undefined, diff --git a/packages/test-utils/src/testUtils/defaultApis.ts b/packages/test-utils/src/testUtils/defaultApis.ts new file mode 100644 index 0000000000..f06e1ba6b7 --- /dev/null +++ b/packages/test-utils/src/testUtils/defaultApis.ts @@ -0,0 +1,264 @@ +/* + * 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 { + AlertApiForwarder, + NoOpAnalyticsApi, + ErrorApiForwarder, + ErrorAlerter, + GoogleAuth, + GithubAuth, + OAuth2, + OktaAuth, + GitlabAuth, + Auth0Auth, + MicrosoftAuth, + BitbucketAuth, + OAuthRequestManager, + WebStorage, + UrlPatternDiscovery, + SamlAuth, + OneLoginAuth, + UnhandledErrorForwarder, + AtlassianAuth, +} from '@backstage/core-app-api'; + +import { + createApiFactory, + alertApiRef, + analyticsApiRef, + errorApiRef, + discoveryApiRef, + oauthRequestApiRef, + googleAuthApiRef, + githubAuthApiRef, + oauth2ApiRef, + oktaAuthApiRef, + gitlabAuthApiRef, + auth0AuthApiRef, + microsoftAuthApiRef, + storageApiRef, + configApiRef, + samlAuthApiRef, + oneloginAuthApiRef, + oidcAuthApiRef, + bitbucketAuthApiRef, + atlassianAuthApiRef, +} from '@backstage/core-plugin-api'; + +// TODO(Rugvip): This is just a copy of the createApp default APIs for now, but +// we should clean up this list a bit move more things over to mocks. +export const defaultApis = [ + createApiFactory({ + api: discoveryApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => + UrlPatternDiscovery.compile( + `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, + ), + }), + createApiFactory(alertApiRef, new AlertApiForwarder()), + createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()), + createApiFactory({ + api: errorApiRef, + deps: { alertApi: alertApiRef }, + factory: ({ alertApi }) => { + const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); + UnhandledErrorForwarder.forward(errorApi, { hidden: false }); + return errorApi; + }, + }), + createApiFactory({ + api: storageApiRef, + deps: { errorApi: errorApiRef }, + factory: ({ errorApi }) => WebStorage.create({ errorApi }), + }), + createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), + createApiFactory({ + api: googleAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GoogleAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: microsoftAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + MicrosoftAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: githubAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oktaAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OktaAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: gitlabAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GitlabAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: auth0AuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + Auth0Auth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oauth2ApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: samlAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, configApi }) => + SamlAuth.create({ + discoveryApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oneloginAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OneLoginAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: oidcAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: () => null, + }, + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: bitbucketAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + BitbucketAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['team'], + environment: configApi.getOptionalString('auth.environment'), + }), + }), + createApiFactory({ + api: atlassianAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => { + return AtlassianAuth.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }); + }, + }), +]; From eb489db0c1169dcc356d39683eb096fcbe888535 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 17:31:15 +0100 Subject: [PATCH 32/35] app-defaults: lazier api initialization to work around import cycle Signed-off-by: Patrik Oldsberg --- packages/app-defaults/src/defaults/apis.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 85c004eece..d3c76d52bd 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -68,8 +68,16 @@ export const apis = [ `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, ), }), - createApiFactory(alertApiRef, new AlertApiForwarder()), - createApiFactory(analyticsApiRef, new NoOpAnalyticsApi()), + createApiFactory({ + api: alertApiRef, + deps: {}, + factory: () => new AlertApiForwarder(), + }), + createApiFactory({ + api: analyticsApiRef, + deps: {}, + factory: () => new NoOpAnalyticsApi(), + }), createApiFactory({ api: errorApiRef, deps: { alertApi: alertApiRef }, @@ -84,7 +92,11 @@ export const apis = [ deps: { errorApi: errorApiRef }, factory: ({ errorApi }) => WebStorage.create({ errorApi }), }), - createApiFactory(oauthRequestApiRef, new OAuthRequestManager()), + createApiFactory({ + api: oauthRequestApiRef, + deps: {}, + factory: () => new OAuthRequestManager(), + }), createApiFactory({ api: googleAuthApiRef, deps: { From 1be02e09dc73fe1e747101d9bf802816cd9e762c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 18:20:47 +0100 Subject: [PATCH 33/35] core-app-api: fix AppManager tests Signed-off-by: Patrik Oldsberg --- .../core-app-api/src/app/AppManager.test.tsx | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 71df5219d1..07401660e8 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -20,7 +20,6 @@ import { renderWithEffects, withLogCollector, } from '@backstage/test-utils'; -import { lightTheme } from '@backstage/theme'; import { render, screen } from '@testing-library/react'; import React, { PropsWithChildren } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; @@ -127,7 +126,7 @@ describe('Integration Test', () => { const HiddenComponent = plugin2.provide( createRoutableExtension({ name: 'HiddenComponent', - component: () => Promise.resolve((_: { path?: string }) =>
), + component: () => Promise.resolve(() =>
), mountPoint: plugin2RouteRef, }), ); @@ -136,7 +135,7 @@ describe('Integration Test', () => { createRoutableExtension({ name: 'ExposedComponent', component: () => - Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { + Promise.resolve(() => { const link1 = useRouteRef(plugin1RouteRef); const link2 = useRouteRef(plugin2RouteRef); const subLink1 = useRouteRef(subRouteRef1); @@ -197,12 +196,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -220,8 +220,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -251,12 +251,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -272,8 +273,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -308,7 +309,7 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, @@ -319,6 +320,7 @@ describe('Integration Test', () => { }), ], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -334,8 +336,8 @@ describe('Integration Test', () => { - - + } /> + } /> , @@ -358,12 +360,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -380,7 +383,7 @@ describe('Integration Test', () => { } /> - } /> + } /> , @@ -418,12 +421,13 @@ describe('Integration Test', () => { id: 'light', title: 'Light Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => <>{children}, }, ], icons, plugins: [], components, + configLoader: async () => [], bindRoutes: ({ bind }) => { bind(plugin1.externalRoutes, { extRouteRef1: plugin1RouteRef, @@ -440,9 +444,9 @@ describe('Integration Test', () => { - - - + }> + } /> + , From 46570fecbc1076cf9b2d1c13241c05924e2c20d6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Nov 2021 18:50:22 +0100 Subject: [PATCH 34/35] test-utils: migrate theme usage and switch to actual MUI icons Signed-off-by: Patrik Oldsberg --- packages/test-utils/package.json | 1 + .../test-utils/src/testUtils/appWrappers.tsx | 51 +++++++++++-------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ab78eab683..6f5f78de8c 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -34,6 +34,7 @@ "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.11.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index ab96887a78..cad1df4ecc 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -18,6 +18,9 @@ import React, { ComponentType, ReactNode, ReactElement } from 'react'; import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core/styles'; +import { CssBaseline } from '@material-ui/core'; +import MockIcon from '@material-ui/icons/AcUnit'; import { createSpecializedApp } from '@backstage/core-app-api'; import { BootErrorPageProps, @@ -32,28 +35,28 @@ import { defaultApis } from './defaultApis'; import { mockApis } from './mockApis'; const mockIcons = { - 'kind:api': () => , - 'kind:component': () => , - 'kind:domain': () => , - 'kind:group': () => , - 'kind:location': () => , - 'kind:system': () => , - 'kind:user': () => , + 'kind:api': MockIcon, + 'kind:component': MockIcon, + 'kind:domain': MockIcon, + 'kind:group': MockIcon, + 'kind:location': MockIcon, + 'kind:system': MockIcon, + 'kind:user': MockIcon, - brokenImage: () => , - catalog: () => , - scaffolder: () => , - techdocs: () => , - search: () => , - chat: () => , - dashboard: () => , - docs: () => , - email: () => , - github: () => , - group: () => , - help: () => , - user: () => , - warning: () => , + brokenImage: MockIcon, + catalog: MockIcon, + scaffolder: MockIcon, + techdocs: MockIcon, + search: MockIcon, + chat: MockIcon, + dashboard: MockIcon, + docs: MockIcon, + email: MockIcon, + github: MockIcon, + group: MockIcon, + help: MockIcon, + user: MockIcon, + warning: MockIcon, }; const ErrorBoundaryFallback = ({ error }: { error: Error }) => { @@ -136,9 +139,13 @@ export function wrapInTestApp( themes: [ { id: 'light', - theme: lightTheme, title: 'Test App Theme', variant: 'light', + Provider: ({ children }) => ( + + {children} + + ), }, ], bindRoutes: ({ bind }) => { From b85ccce84df919c10d01cacce536211bb3e46a02 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 11 Nov 2021 17:28:52 +0100 Subject: [PATCH 35/35] app-defaults: update internal deps Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 76101f0ad4..9438966b3b 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -29,18 +29,18 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.2", - "@backstage/core-app-api": "^0.1.19", - "@backstage/core-plugin-api": "^0.1.12", - "@backstage/theme": "^0.2.12", + "@backstage/core-components": "^0.7.3", + "@backstage/core-app-api": "^0.1.20", + "@backstage/core-plugin-api": "^0.1.13", + "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.8.1", - "@backstage/test-utils": "^0.1.20", + "@backstage/cli": "^0.8.2", + "@backstage/test-utils": "^0.1.21", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7",