packages: new app-defaults package + move over withDefaults

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-10-27 19:10:40 +02:00
parent 8921944da1
commit a82aa54e27
18 changed files with 1067 additions and 4 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+17
View File
@@ -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)
+64
View File
@@ -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"
]
}
@@ -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<{}>) => (
<main role="main">{children}</main>
),
};
const App = createApp({ components }).getProvider();
await renderWithEffects(<App />);
expect(screen.getByRole('main')).toBeInTheDocument();
});
});
@@ -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<keyof AppComponents> = [
'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<any, any>[]) ?? [],
configLoader: options?.configLoader ?? defaultConfigLoader,
});
}
@@ -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'),
});
},
}),
];
@@ -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,
};
@@ -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';
@@ -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<string>;
/**
* Sign out handler that will be called if the user requests to sign out.
*/
signOut?: () => Promise<void>;
};
/**
* 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<BootErrorPageProps>;
Progress: ComponentType<{}>;
Router: ComponentType<{}>;
ErrorBoundaryFallback: ComponentType<ErrorBoundaryFallbackProps>;
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<SignInPageProps>;
};
/**
* 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<AppConfig[]>;
/**
* Extracts a union of the keys in a map whose value extends the given type
*/
type KeysWithType<Obj extends { [key in string]: any }, Type> = {
[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<Pick<Map, Keys>> & Required<Omit<Map, Keys>>;
/**
* 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<Params> | SubRouteRef<Params>
: 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<ExternalRoutes>,
KeysWithType<ExternalRoutes, ExternalRouteRef<any, true>>
>,
) => 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<any, any>,
'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<AnyApiFactory>;
/**
* Supply icons to override the default ones.
*/
icons?: Partial<AppIcons> & { [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<AppComponents>;
/**
* 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: <LightIcon />,
* Provider: ({ children }) => (
* <ThemeProvider theme={lightTheme}>
* <CssBaseline>{children}</CssBaseline>
* </ThemeProvider>
* ),
* }, {
* id: 'dark',
* title: 'Dark Theme',
* variant: 'dark',
* icon: <DarkIcon />,
* Provider: ({ children }) => (
* <ThemeProvider theme={darkTheme}>
* <CssBaseline>{children}</CssBaseline>
* </ThemeProvider>
* ),
* }]
* ```
*/
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
/**
* 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<any, any>[];
/**
* 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<any, any>[];
/**
* 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;
};
+23
View File
@@ -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';
@@ -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';
-1
View File
@@ -25,4 +25,3 @@ export * from './hooks';
export * from './icons';
export * from './layout';
export * from './overridableComponents';
export * from './appDefaults';