Merge pull request #7790 from backstage/mob/decouple-core

core-app-api: deprecate dependency on core-components, theme, and MUI
This commit is contained in:
Patrik Oldsberg
2021-11-11 18:31:24 +01:00
committed by GitHub
61 changed files with 1266 additions and 528 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/dev-utils': patch
---
Migrated to using `@backstage/app-defaults`.
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/create-app': patch
---
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, 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';
+import { createApp } from '@backstage/app-defaults';
+import { FlatRoutes } from '@backstage/core-app-api';
```
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': patch
---
Migrated to using `createSpecializedApp`.
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/core-app-api': patch
---
The `createApp` function from `@backstage/core-app-api` has been deprecated, with two new options being provided as a replacement.
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`.
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.
+56
View File
@@ -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
`<CssBaseline>`.
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 `<CssBaseline>`. For example a theme that currently looks like
this:
```tsx
const darkTheme = {
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
theme: darkTheme,
};
```
Would be migrated to the following:
```tsx
const darkTheme = {
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
Provider: ({ children }) => (
<ThemeProvider theme={darkTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
};
```
+2 -1
View File
@@ -256,7 +256,8 @@
"type": "subcategory",
"label": "API Reference",
"ids": ["reference/index"]
}
},
"api/deprecations"
],
"Tutorials": [
"tutorials/journey",
+1
View File
@@ -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'
+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)
+26
View File
@@ -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 { 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<AppOptions, keyof OptionalAppOptions> & OptionalAppOptions,
): BackstageApp;
// @public
export type OptionalAppOptions = {
icons?: Partial<AppIcons> & {
[key in string]: IconComponent;
};
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
components?: Partial<AppComponents>;
};
```
+53
View File
@@ -0,0 +1,53 @@
{
"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.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.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",
"@types/node": "^14.14.32",
"@types/react": "*"
},
"files": [
"dist"
]
}
@@ -0,0 +1,42 @@
/*
* 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 { createApp } from './createApp';
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();
});
});
+93
View File
@@ -0,0 +1,93 @@
/*
* 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 { apis, components, icons, themes } from './defaults';
import {
AppTheme,
BackstagePlugin,
IconComponent,
} from '@backstage/core-plugin-api';
import {
AppComponents,
AppOptions,
AppIcons,
createSpecializedApp,
} from '@backstage/core-app-api';
/**
* Creates a new Backstage App using a default set of components, icons and themes unless
* they are explicitly provided.
*
* @public
*/
export function createApp(
options?: Omit<AppOptions, keyof OptionalAppOptions> & OptionalAppOptions,
) {
return createSpecializedApp({
...options,
apis: options?.apis ?? [],
bindRoutes: options?.bindRoutes,
components: {
...components,
...options?.components,
},
configLoader: options?.configLoader,
defaultApis: apis,
icons: {
...icons,
...options?.icons,
},
plugins: (options?.plugins as BackstagePlugin<any, any>[]) ?? [],
themes: options?.themes ?? themes,
});
}
/**
* The set of app options that {@link createApp} will provide defaults for
* if they are not passed in explicitly.
*
* @public
*/
export type OptionalAppOptions = {
/**
* A set of icons to override the default icons with.
*
* The override is applied for each icon individually.
*
* @public
*/
icons?: Partial<AppIcons> & {
[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<AppTheme> & Omit<AppTheme, 'theme'>)[]; // 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<AppComponents>;
};
+274
View File
@@ -0,0 +1,274 @@
/*
* 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 '@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';
export const apis = [
createApiFactory({
api: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
UrlPatternDiscovery.compile(
`${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`,
),
}),
createApiFactory({
api: alertApiRef,
deps: {},
factory: () => new AlertApiForwarder(),
}),
createApiFactory({
api: analyticsApiRef,
deps: {},
factory: () => 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({
api: oauthRequestApiRef,
deps: {},
factory: () => 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'),
});
},
}),
];
@@ -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 './components';
describe('OptionallyWrapInRouter', () => {
it('should wrap with router if not yet inside a router', async () => {
render(<OptionallyWrapInRouter>Test</OptionallyWrapInRouter>);
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('should not wrap with router if already inside a router', async () => {
render(
<MemoryRouter>
<OptionallyWrapInRouter>Test</OptionallyWrapInRouter>
</MemoryRouter>,
);
expect(screen.getByText('Test')).toBeInTheDocument();
});
});
@@ -0,0 +1,86 @@
/*
* 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 { ErrorPanel, Progress, ErrorPage } from '@backstage/core-components';
import {
MemoryRouter,
useInRouterContext,
BrowserRouter,
} from 'react-router-dom';
import {
AppComponents,
BootErrorPageProps,
ErrorBoundaryFallbackProps,
} from '@backstage/core-plugin-api';
export function OptionallyWrapInRouter({ children }: { children: ReactNode }) {
if (useInRouterContext()) {
return <>{children}</>;
}
return <MemoryRouter>{children}</MemoryRouter>;
}
const DefaultNotFoundPage = () => (
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />
);
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 (
<OptionallyWrapInRouter>
<ErrorPage status="501" statusMessage={message} />
</OptionallyWrapInRouter>
);
};
const DefaultErrorBoundaryFallback = ({
error,
resetError,
plugin,
}: ErrorBoundaryFallbackProps) => {
return (
<ErrorPanel
title={`Error in ${plugin?.getId()}`}
defaultExpanded
error={error}
>
<Button variant="outlined" onClick={resetError}>
Retry
</Button>
</ErrorPanel>
);
};
/**
* Creates a set of default components to pass along to {@link @backstage/core-app-api#createApp}.
*
* @public
*/
export const components: AppComponents = {
Progress,
Router: BrowserRouter,
NotFoundErrorPage: DefaultNotFoundPage,
BootErrorPage: DefaultBootErrorPage,
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
};
@@ -35,52 +35,27 @@ 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 = { [key in AppIconsKey]: IconComponent };
export const defaultAppIcons: AppIcons = {
brokenImage: MuiBrokenImageIcon,
export const icons = {
brokenImage: MuiBrokenImageIcon as IconComponent,
// 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,
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,
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,
};
@@ -0,0 +1,20 @@
/*
* 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 { icons } from './icons';
export { themes } from './themes';
@@ -0,0 +1,50 @@
/*
* 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 const themes: AppTheme[] = [
{
id: 'light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
theme: lightTheme,
Provider: ({ children }) => (
<ThemeProvider theme={lightTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
{
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
theme: darkTheme,
Provider: ({ children }) => (
<ThemeProvider theme={darkTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
];
+24
View File
@@ -0,0 +1,24 @@
/*
* 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 { createApp } from './createApp';
export type { OptionalAppOptions } from './createApp';
+17
View File
@@ -0,0 +1,17 @@
/*
* 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 '@testing-library/jest-dom';
+1
View File
@@ -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",
+2 -2
View File
@@ -26,7 +26,8 @@ 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,
@@ -93,7 +94,6 @@ const app = createApp({
// Custom icon example
alert: AlarmIcon,
},
components: {
SignInPage: props => {
return (
+35 -8
View File
@@ -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<ErrorBoundaryFallbackProps>;
ThemeProvider: ComponentType<{}>;
ThemeProvider?: ComponentType<{}>;
SignInPage?: ComponentType<SignInPageProps>;
};
@@ -164,15 +165,41 @@ export type AppContext = {
getComponents(): AppComponents;
};
// @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;
};
// @public
export type AppOptions = {
apis?: Iterable<AnyApiFactory>;
icons?: Partial<AppIcons> & {
defaultApis?: Iterable<AnyApiFactory>;
icons: AppIcons & {
[key in string]: IconComponent;
};
plugins?: BackstagePluginWithAnyOutput[];
components?: Partial<AppComponents>;
themes?: AppTheme[];
components: AppComponents;
themes: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
configLoader?: AppConfigLoader;
bindRoutes?(context: { bind: AppRouteBinder }): void;
};
@@ -286,10 +313,11 @@ export type BootErrorPageProps = {
export { ConfigReader };
// Warning: (ae-forgotten-export) The symbol "PrivateAppImpl" needs to be exported by the entry point index.d.ts
//
// @public @deprecated
export function createApp(options?: OptionalAppOptions): BackstageApp;
// @public
export function createApp(options?: AppOptions): PrivateAppImpl;
export function createSpecializedApp(options: AppOptions): BackstageApp;
// @public
export const defaultConfigLoader: AppConfigLoader;
@@ -608,5 +636,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
```
+1
View File
@@ -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",
@@ -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,
});
@@ -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',
});
@@ -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,
};
/**
@@ -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,
};
/**
@@ -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,
};
/**
@@ -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,
};
/**
@@ -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,
};
/**
@@ -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/';
@@ -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,
};
/**
@@ -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,
};
/**
@@ -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<String> = new Set([
@@ -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<String> = new Set([
@@ -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,
};
/**
@@ -20,11 +20,9 @@ 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';
import { defaultAppIcons } from './icons';
import {
configApiRef,
createApiFactory,
@@ -37,8 +35,8 @@ import {
createRoutableExtension,
analyticsApiRef,
} from '@backstage/core-plugin-api';
import { generateBoundRoutes, PrivateAppImpl } from './App';
import { AppThemeProvider } from './AppThemeProvider';
import { generateBoundRoutes, AppManager } from './AppManager';
import { AppComponents, AppIcons } from './types';
describe('generateBoundRoutes', () => {
it('runs happy path', () => {
@@ -128,7 +126,7 @@ describe('Integration Test', () => {
const HiddenComponent = plugin2.provide(
createRoutableExtension({
name: 'HiddenComponent',
component: () => Promise.resolve((_: { path?: string }) => <div />),
component: () => Promise.resolve(() => <div />),
mountPoint: plugin2RouteRef,
}),
);
@@ -137,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);
@@ -178,17 +176,19 @@ describe('Integration Test', () => {
}),
);
const components = {
const components: AppComponents = {
NotFoundErrorPage: () => null,
BootErrorPage: () => null,
Progress: () => null,
Router: BrowserRouter,
ErrorBoundaryFallback: () => null,
ThemeProvider: AppThemeProvider,
ThemeProvider: ({ children }) => <>{children}</>,
};
const icons = {} as AppIcons;
it('runs happy paths', async () => {
const app = new PrivateAppImpl({
const app = new AppManager({
apis: [noOpAnalyticsApi],
defaultApis: [],
themes: [
@@ -196,12 +196,13 @@ describe('Integration Test', () => {
id: 'light',
title: 'Light Theme',
variant: 'light',
theme: lightTheme,
Provider: ({ children }) => <>{children}</>,
},
],
icons: defaultAppIcons,
icons,
plugins: [],
components,
configLoader: async () => [],
bindRoutes: ({ bind }) => {
bind(plugin1.externalRoutes, {
extRouteRef1: plugin1RouteRef,
@@ -219,8 +220,8 @@ describe('Integration Test', () => {
<Provider>
<Router>
<Routes>
<ExposedComponent path="/" />
<HiddenComponent path="/foo/:x" />
<Route path="/" element={<ExposedComponent />} />
<Route path="/foo/:x" element={<HiddenComponent />} />
</Routes>
</Router>
</Provider>,
@@ -242,7 +243,7 @@ describe('Integration Test', () => {
});
it('runs happy paths without optional routes', async () => {
const app = new PrivateAppImpl({
const app = new AppManager({
apis: [noOpAnalyticsApi],
defaultApis: [],
themes: [
@@ -250,12 +251,13 @@ describe('Integration Test', () => {
id: 'light',
title: 'Light Theme',
variant: 'light',
theme: lightTheme,
Provider: ({ children }) => <>{children}</>,
},
],
icons: defaultAppIcons,
icons,
plugins: [],
components,
configLoader: async () => [],
bindRoutes: ({ bind }) => {
bind(plugin1.externalRoutes, {
extRouteRef1: plugin1RouteRef,
@@ -271,8 +273,8 @@ describe('Integration Test', () => {
<Provider>
<Router>
<Routes>
<ExposedComponent path="/" />
<HiddenComponent path="/foo" />
<Route path="/" element={<ExposedComponent />} />
<Route path="/foo" element={<HiddenComponent />} />
</Routes>
</Router>
</Provider>,
@@ -299,7 +301,7 @@ describe('Integration Test', () => {
}),
];
const app = new PrivateAppImpl({
const app = new AppManager({
apis,
defaultApis: [],
themes: [
@@ -307,10 +309,10 @@ describe('Integration Test', () => {
id: 'light',
title: 'Light Theme',
variant: 'light',
theme: lightTheme,
Provider: ({ children }) => <>{children}</>,
},
],
icons: defaultAppIcons,
icons,
plugins: [
createPlugin({
id: 'test',
@@ -318,6 +320,7 @@ describe('Integration Test', () => {
}),
],
components,
configLoader: async () => [],
bindRoutes: ({ bind }) => {
bind(plugin1.externalRoutes, {
extRouteRef1: plugin1RouteRef,
@@ -333,8 +336,8 @@ describe('Integration Test', () => {
<Provider>
<Router>
<Routes>
<ExposedComponent path="/" />
<HiddenComponent path="/foo" />
<Route path="/" element={<ExposedComponent />} />
<Route path="/foo" element={<HiddenComponent />} />
</Routes>
</Router>
</Provider>,
@@ -349,7 +352,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: [
@@ -357,12 +360,13 @@ describe('Integration Test', () => {
id: 'light',
title: 'Light Theme',
variant: 'light',
theme: lightTheme,
Provider: ({ children }) => <>{children}</>,
},
],
icons: defaultAppIcons,
icons,
plugins: [],
components,
configLoader: async () => [],
bindRoutes: ({ bind }) => {
bind(plugin1.externalRoutes, {
extRouteRef1: plugin1RouteRef,
@@ -379,7 +383,7 @@ describe('Integration Test', () => {
<Router>
<Routes>
<Route path="/" element={<NavigateComponent />} />
<Route path="/foo" element={<HiddenComponent path="/foo" />} />
<Route path="/foo" element={<HiddenComponent />} />
</Routes>
</Router>
</Provider>,
@@ -409,7 +413,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: [
@@ -417,12 +421,13 @@ describe('Integration Test', () => {
id: 'light',
title: 'Light Theme',
variant: 'light',
theme: lightTheme,
Provider: ({ children }) => <>{children}</>,
},
],
icons: defaultAppIcons,
icons,
plugins: [],
components,
configLoader: async () => [],
bindRoutes: ({ bind }) => {
bind(plugin1.externalRoutes, {
extRouteRef1: plugin1RouteRef,
@@ -439,9 +444,9 @@ describe('Integration Test', () => {
<Provider>
<Router>
<Routes>
<ExposedComponent path="/test/:thing">
<HiddenComponent path="/some/:thing" />
</ExposedComponent>
<Route path="/test/:thing" element={<ExposedComponent />}>
<Route path="/some/:thing" element={<HiddenComponent />} />
</Route>
</Routes>
</Router>
</Provider>,
@@ -77,6 +77,8 @@ import {
SignInPageProps,
SignInResult,
} from './types';
import { AppThemeProvider } from './AppThemeProvider';
import { defaultConfigLoader } from './defaultConfigLoader';
export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) {
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
@@ -121,17 +123,6 @@ function getBasePath(configApi: Config) {
return pathname;
}
type FullAppOptions = {
apis: Iterable<AnyApiFactory>;
icons: NonNullable<AppOptions['icons']>;
plugins: BackstagePlugin<any, any>[];
components: AppComponents;
themes: AppTheme[];
configLoader?: AppConfigLoader;
defaultApis: Iterable<AnyApiFactory>;
bindRoutes?: AppOptions['bindRoutes'];
};
function useConfigLoader(
configLoader: AppConfigLoader | undefined,
components: AppComponents,
@@ -151,7 +142,7 @@ function useConfigLoader(
noConfigNode = <BootErrorPage step="load-config" error={config.error} />;
}
const { ThemeProvider } = components;
const { ThemeProvider = AppThemeProvider } = components;
// Before the config is loaded we can't use a router, so exit early
if (noConfigNode) {
@@ -170,7 +161,7 @@ function useConfigLoader(
}
class AppContextImpl implements AppContext {
constructor(private readonly app: PrivateAppImpl) {}
constructor(private readonly app: AppManager) {}
getPlugins(): BackstagePlugin<any, any>[] {
return this.app.getPlugins();
@@ -185,7 +176,7 @@ class AppContextImpl implements AppContext {
}
}
export class PrivateAppImpl implements BackstageApp {
export class AppManager implements BackstageApp {
private apiHolder?: ApiHolder;
private configApi?: ConfigApi;
@@ -201,14 +192,16 @@ export class PrivateAppImpl 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<any, any>[]) ?? [],
);
this.components = options.components;
this.themes = options.themes;
this.configLoader = options.configLoader;
this.defaultApis = options.defaultApis;
this.themes = options.themes as AppTheme[];
this.configLoader = options.configLoader ?? defaultConfigLoader;
this.defaultApis = options.defaultApis ?? [];
this.bindRoutes = options.bindRoutes;
this.apiFactoryRegistry = new ApiFactoryRegistry();
}
@@ -307,7 +300,7 @@ export class PrivateAppImpl implements BackstageApp {
return loadedConfig.node;
}
const { ThemeProvider } = this.components;
const { ThemeProvider = AppThemeProvider } = this.components;
return (
<ApiProvider apis={this.getApiHolder()}>
@@ -90,6 +90,17 @@ export function AppThemeProvider({ children }: PropsWithChildren<{}>) {
throw new Error('App has no themes');
}
if (appTheme.Provider) {
return <appTheme.Provider children={children} />;
}
// 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/api/deprecations#app-theme for more info.',
);
return (
<ThemeProvider theme={appTheme.theme}>
<CssBaseline>{children}</CssBaseline>
+13 -159
View File
@@ -14,171 +14,25 @@
* limitations under the License.
*/
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 { 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 { 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';
/**
* 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;
};
export function OptionallyWrapInRouter({ children }: PropsWithChildren<{}>) {
if (useInRouterContext()) {
return <>{children}</>;
}
return <MemoryRouter>{children}</MemoryRouter>;
}
createApp as createDefaultApp,
OptionalAppOptions,
} from '@backstage/app-defaults';
/**
* Creates a new Backstage App.
*
* @deprecated Use {@link @backstage/app-defaults#createApp} from `@backstage/app-defaults` instead
* @param options - A set of options for creating the app
* @public
*/
export function createApp(options?: AppOptions) {
const DefaultNotFoundPage = () => (
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />
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.',
);
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 (
<OptionallyWrapInRouter>
<ErrorPage status="501" statusMessage={message} />
</OptionallyWrapInRouter>
);
};
const DefaultErrorBoundaryFallback = ({
error,
resetError,
plugin,
}: ErrorBoundaryFallbackProps) => {
return (
<ErrorPanel
title={`Error in ${plugin?.getId()}`}
defaultExpanded
error={error}
>
<Button variant="outlined" onClick={resetError}>
Retry
</Button>
</ErrorPanel>
);
};
const apis = options?.apis ?? [];
const icons = { ...defaultAppIcons, ...options?.icons };
const plugins = options?.plugins ?? [];
const components = {
NotFoundErrorPage: DefaultNotFoundPage,
BootErrorPage: DefaultBootErrorPage,
Progress: Progress,
Router: BrowserRouter,
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
ThemeProvider: AppThemeProvider,
...options?.components,
};
const themes = options?.themes ?? [
{
id: 'light',
title: 'Light Theme',
variant: 'light',
theme: lightTheme,
icon: <LightIcon />,
},
{
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
theme: darkTheme,
icon: <DarkIcon />,
},
];
const configLoader = options?.configLoader ?? defaultConfigLoader;
return new PrivateAppImpl({
apis,
icons,
plugins: plugins as BackstagePlugin<any, any>[],
components,
themes,
configLoader,
defaultApis,
bindRoutes: options?.bindRoutes,
});
return createDefaultApp(options);
}
@@ -0,0 +1,29 @@
/*
* 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 { AppManager } from './AppManager';
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
*/
export function createSpecializedApp(options: AppOptions): BackstageApp {
return new AppManager(options);
}
@@ -14,15 +14,7 @@
* limitations under the License.
*/
import { render, screen } from '@testing-library/react';
import { renderWithEffects } from '@backstage/test-utils';
import React, { PropsWithChildren } from 'react';
import { MemoryRouter } from 'react-router-dom';
import {
defaultConfigLoader,
OptionallyWrapInRouter,
createApp,
} from './createApp';
import { defaultConfigLoader } from './defaultConfigLoader';
(process as any).env = { NODE_ENV: 'test' };
const anyEnv = process.env as any;
@@ -100,44 +92,3 @@ describe('defaultConfigLoader', () => {
]);
});
});
describe('OptionallyWrapInRouter', () => {
it('should wrap with router if not yet inside a router', async () => {
const { getByText } = render(
<OptionallyWrapInRouter>Test</OptionallyWrapInRouter>,
);
expect(getByText('Test')).toBeInTheDocument();
});
it('should not wrap with router if already inside a router', async () => {
const { getByText } = render(
<MemoryRouter>
<OptionallyWrapInRouter>Test</OptionallyWrapInRouter>
</MemoryRouter>,
);
expect(getByText('Test')).toBeInTheDocument();
});
});
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,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 './types';
/**
* 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;
};
+3 -1
View File
@@ -14,5 +14,7 @@
* limitations under the License.
*/
export { createApp, defaultConfigLoader } from './createApp';
export { createApp } from './createApp';
export { createSpecializedApp } from './createSpecializedApp';
export { defaultConfigLoader } from './defaultConfigLoader';
export * from './types';
+54 -8
View File
@@ -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}.
@@ -97,7 +96,7 @@ export type AppComponents = {
Progress: ComponentType<{}>;
Router: ComponentType<{}>;
ErrorBoundaryFallback: ComponentType<ErrorBoundaryFallbackProps>;
ThemeProvider: ComponentType<{}>;
ThemeProvider?: ComponentType<{}>;
/**
* An optional sign-in page that will be rendered instead of the AppRouter at startup.
@@ -111,6 +110,36 @@ export type AppComponents = {
SignInPage?: ComponentType<SignInPageProps>;
};
/**
* 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.
*
@@ -196,14 +225,23 @@ 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<AnyApiFactory>;
/**
* 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 new ones, or override factories provided by default or by plugins.
*/
defaultApis?: Iterable<AnyApiFactory>;
/**
* Supply icons to override the default ones.
*/
icons?: Partial<AppIcons> & { [key in string]: IconComponent };
icons: AppIcons & { [key in string]: IconComponent };
/**
* A list of all plugins to include in the app.
@@ -213,7 +251,7 @@ export type AppOptions = {
/**
* Supply components to the app to override the default ones.
*/
components?: Partial<AppComponents>;
components: AppComponents;
/**
* Themes provided as a part of the app. By default two themes are included, one
@@ -226,18 +264,26 @@ export type AppOptions = {
* id: 'light',
* title: 'Light Theme',
* variant: 'light',
* theme: lightTheme,
* icon: <LightIcon />,
* Provider: ({ children }) => (
* <ThemeProvider theme={lightTheme}>
* <CssBaseline>{children}</CssBaseline>
* </ThemeProvider>
* ),
* }, {
* id: 'dark',
* title: 'Dark Theme',
* variant: 'dark',
* theme: darkTheme,
* icon: <DarkIcon />,
* Provider: ({ children }) => (
* <ThemeProvider theme={darkTheme}>
* <CssBaseline>{children}</CssBaseline>
* </ThemeProvider>
* ),
* }]
* ```
*/
themes?: AppTheme[];
themes: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
/**
* A function that loads in App configuration that will be accessible via
@@ -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) => ({
@@ -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 }) => (
</ContentHeader>
);
const app = createApp({ configLoader: async () => [] });
const AppProvider = app.getProvider();
export const PluginWithData = () => {
const [selectedTab, setSelectedTab] = useState<number>(2);
return (
<AppProvider>
<MemoryRouter>
<div style={{ border: '1px solid #ddd' }}>
<Page themeId="tool">
<ExampleHeader />
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
tabs={tabs.map(({ label }, index) => ({
id: index.toString(),
label,
}))}
/>
<Content>
<ExampleContentHeader selectedTab={selectedTab} />
<DataGrid />
</Content>
</Page>
</div>
</MemoryRouter>
</AppProvider>
);
return wrapInTestApp(() => (
<div style={{ border: '1px solid #ddd' }}>
<Page themeId="tool">
<ExampleHeader />
<HeaderTabs
selectedIndex={selectedTab}
onChange={index => setSelectedTab(index)}
tabs={tabs.map(({ label }, index) => ({
id: index.toString(),
label,
}))}
/>
<Content>
<ExampleContentHeader selectedTab={selectedTab} />
<DataGrid />
</Content>
</Page>
</div>
));
};
export const PluginWithTable = () => {
return (
<AppProvider>
<div style={{ border: '1px solid #ddd' }}>
<Page themeId="tool">
<ExampleHeader />
<Content>
<ExampleContentHeader />
<Table
options={{ paging: true, padding: 'dense' }}
data={generateTestData(10)}
columns={columns}
title="Example Content"
/>
</Content>
</Page>
</div>
</AppProvider>
);
return wrapInTestApp(() => (
<div style={{ border: '1px solid #ddd' }}>
<Page themeId="tool">
<ExampleHeader />
<Content>
<ExampleContentHeader />
<Table
options={{ paging: true, padding: 'dense' }}
data={generateTestData(10)}
columns={columns}
title="Example Content"
/>
</Content>
</Page>
</div>
));
};
+9 -4
View File
@@ -5,11 +5,14 @@
```ts
/// <reference types="react" />
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<ErrorBoundaryFallbackProps>;
ThemeProvider?: ComponentType<{}>;
SignInPage?: ComponentType<SignInPageProps>;
};
// @public
export type AppContext = {
getPlugins(): BackstagePlugin<any, any>[];
getSystemIcon(key: string): IconComponent | undefined;
getPlugins(): BackstagePlugin_2<any, any>[];
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<ErrorApi>;
// @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<string>;
signOut?: () => Promise<void>;
};
@@ -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/api/deprecations#app-theme
*/
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;
};
/**
+12 -107
View File
@@ -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<string>;
/**
* Sign out handler that will be called if the user requests to sign out.
*/
signOut?: () => Promise<void>;
};
/**
* 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<BootErrorPageProps>;
Progress: ComponentType<{}>;
Router: ComponentType<{}>;
ErrorBoundaryFallback: ComponentType<ErrorBoundaryFallbackProps>;
/**
* 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>;
};
/**
* 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<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;
};
// 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';
+2
View File
@@ -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,
@@ -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'}}",
@@ -26,7 +26,8 @@ import { searchPage } from './components/search/SearchPage';
import { Root } from './components/Root';
import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
import { createApp, FlatRoutes } from '@backstage/core-app-api';
import { createApp } from '@backstage/app-defaults';
import { FlatRoutes } from '@backstage/core-app-api';
const app = createApp({
apis,
+1
View File
@@ -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",
+2 -1
View File
@@ -47,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;
+1
View File
@@ -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",
@@ -18,7 +18,10 @@ 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 { 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,
RouteRef,
@@ -28,8 +31,34 @@ 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 = {
'kind:api': MockIcon,
'kind:component': MockIcon,
'kind:domain': MockIcon,
'kind:group': MockIcon,
'kind:location': MockIcon,
'kind:system': MockIcon,
'kind:user': MockIcon,
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 }) => {
throw new Error(`Reached ErrorBoundaryFallback Page with error, ${error}`);
};
@@ -90,8 +119,9 @@ export function wrapInTestApp(
const { routeEntries = ['/'] } = options;
const boundRoutes = new Map<ExternalRouteRef, RouteRef>();
const app = createApp({
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,
@@ -104,13 +134,18 @@ export function wrapInTestApp(
<MemoryRouter initialEntries={routeEntries} children={children} />
),
},
icons: mockIcons,
plugins: [],
themes: [
{
id: 'light',
theme: lightTheme,
title: 'Test App Theme',
variant: 'light',
Provider: ({ children }) => (
<ThemeProvider theme={lightTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
],
bindRoutes: ({ bind }) => {
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* 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.
@@ -34,7 +34,7 @@ import {
OneLoginAuth,
UnhandledErrorForwarder,
AtlassianAuth,
} from '../apis';
} from '@backstage/core-app-api';
import {
createApiFactory,
@@ -59,8 +59,8 @@ import {
atlassianAuthApiRef,
} from '@backstage/core-plugin-api';
import OAuth2Icon from '@material-ui/icons/AcUnit';
// 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,
@@ -226,7 +226,7 @@ export const defaultApis = [
provider: {
id: 'oidc',
title: 'Your Identity Provider',
icon: OAuth2Icon,
icon: () => null,
},
environment: configApi.getOptionalString('auth.environment'),
}),