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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/dev-utils': patch
|
||||
---
|
||||
|
||||
Migrated to using `@backstage/app-defaults`.
|
||||
@@ -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';
|
||||
```
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/test-utils': patch
|
||||
---
|
||||
|
||||
Migrated to using `createSpecializedApp`.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
```
|
||||
@@ -256,7 +256,8 @@
|
||||
"type": "subcategory",
|
||||
"label": "API Reference",
|
||||
"ids": ["reference/index"]
|
||||
}
|
||||
},
|
||||
"api/deprecations"
|
||||
],
|
||||
"Tutorials": [
|
||||
"tutorials/journey",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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>;
|
||||
};
|
||||
```
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
+22
-47
@@ -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>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+1
-2
@@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+38
-33
@@ -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>,
|
||||
+14
-21
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+1
-50
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
+5
-5
@@ -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'),
|
||||
}),
|
||||
Reference in New Issue
Block a user