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 }, - }; -}