core-app-api: deprecate dependency on core-components
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/dev-utils': patch
|
||||
---
|
||||
|
||||
Migrated to explicit passing of components to `createApp`.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Migrated the app template to pass on explicit `components` to the `createApp` options, as not doing this has been deprecated and will need to be done in the future.
|
||||
|
||||
To migrate an existing application, make the following change to `packages/app/src/App.tsx`:
|
||||
|
||||
```diff
|
||||
+import { defaultAppComponents } from '@backstage/core-components';
|
||||
|
||||
// ...
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
+ components: defaultAppComponents(),
|
||||
bindRoutes({ bind }) {
|
||||
```
|
||||
|
||||
If you already supply custom app components, you can use the following:
|
||||
|
||||
```diff
|
||||
|
||||
// ...
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
+ components: {
|
||||
...defaultAppComponents(),
|
||||
+ Progress: MyCustomProgressComponent,
|
||||
},
|
||||
bindRoutes({ bind }) {
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Added a new `defaultAppComponents` method that creates a minimal set of components to pass on to `createApp` from `@backstage/core-app-api`.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Deprecated the defaulting of the `components` options of `createApp`, meaning it will become required in the future. When not passing the required components options a deprecation warning is currently logged, and it will become required in a future release.
|
||||
|
||||
The keep the existing components intact, migrate to using `defaultAppComponents` from `@backstage/core-components`:
|
||||
|
||||
```ts
|
||||
const app = createApp({
|
||||
components: {
|
||||
...defaultAppComponents(),
|
||||
// Place any custom components here
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
AlertDisplay,
|
||||
OAuthRequestDialog,
|
||||
SignInPage,
|
||||
defaultAppComponents,
|
||||
} from '@backstage/core-components';
|
||||
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
|
||||
import {
|
||||
@@ -95,6 +96,7 @@ const app = createApp({
|
||||
},
|
||||
|
||||
components: {
|
||||
...defaultAppComponents(),
|
||||
SignInPage: props => {
|
||||
return (
|
||||
<SignInPage
|
||||
|
||||
@@ -14,15 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithEffects } from '@backstage/test-utils';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
defaultConfigLoader,
|
||||
OptionallyWrapInRouter,
|
||||
createApp,
|
||||
} from './createApp';
|
||||
import { defaultConfigLoader, createApp } from './createApp';
|
||||
|
||||
(process as any).env = { NODE_ENV: 'test' };
|
||||
const anyEnv = process.env as any;
|
||||
@@ -101,26 +97,6 @@ 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 = {
|
||||
|
||||
@@ -16,28 +16,25 @@
|
||||
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { ErrorPage, ErrorPanel, Progress } from '@backstage/core-components';
|
||||
import { defaultAppComponents } from '@backstage/core-components';
|
||||
import { darkTheme, lightTheme } from '@backstage/theme';
|
||||
import DarkIcon from '@material-ui/icons/Brightness2';
|
||||
import LightIcon from '@material-ui/icons/WbSunny';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import {
|
||||
BrowserRouter,
|
||||
MemoryRouter,
|
||||
useInRouterContext,
|
||||
} from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { PrivateAppImpl } from './App';
|
||||
import { AppThemeProvider } from './AppThemeProvider';
|
||||
import { defaultApis } from './defaultApis';
|
||||
import { defaultAppIcons } from './icons';
|
||||
import {
|
||||
AppConfigLoader,
|
||||
AppOptions,
|
||||
BootErrorPageProps,
|
||||
ErrorBoundaryFallbackProps,
|
||||
} from './types';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { AppConfigLoader, AppOptions } from './types';
|
||||
import { AppComponents, BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
const REQUIRED_APP_COMPONENTS: Array<keyof AppComponents> = [
|
||||
'Progress',
|
||||
'NotFoundErrorPage',
|
||||
'BootErrorPage',
|
||||
'ErrorBoundaryFallback',
|
||||
];
|
||||
|
||||
/**
|
||||
* The default config loader, which expects that config is available at compile-time
|
||||
@@ -93,63 +90,34 @@ export const defaultConfigLoader: AppConfigLoader = async (
|
||||
return configs;
|
||||
};
|
||||
|
||||
export function OptionallyWrapInRouter({ children }: PropsWithChildren<{}>) {
|
||||
if (useInRouterContext()) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return <MemoryRouter>{children}</MemoryRouter>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Backstage App.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createApp(options?: AppOptions) {
|
||||
const DefaultNotFoundPage = () => (
|
||||
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />
|
||||
const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter(
|
||||
name => !options?.components?.[name],
|
||||
);
|
||||
const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => {
|
||||
let message = '';
|
||||
if (step === 'load-config') {
|
||||
message = `The configuration failed to load, someone should have a look at this error: ${error.message}`;
|
||||
} else if (step === 'load-chunk') {
|
||||
message = `Lazy loaded chunk failed to load, try to reload the page: ${error.message}`;
|
||||
}
|
||||
// TODO: figure out a nicer way to handle routing on the error page, when it can be done.
|
||||
return (
|
||||
<OptionallyWrapInRouter>
|
||||
<ErrorPage status="501" statusMessage={message} />
|
||||
</OptionallyWrapInRouter>
|
||||
if (missingRequiredComponents.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'DEPRECATION WARNING: The createApp options will soon require a minimal set of ' +
|
||||
'components to be provided in the components option. These components can be ' +
|
||||
'created using defaultAppComponents from @backstage/core-components and ' +
|
||||
'passed along like this: createApp({ components: defaultAppComponents() }). ' +
|
||||
`The following components are missing: ${missingRequiredComponents.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
};
|
||||
const DefaultErrorBoundaryFallback = ({
|
||||
error,
|
||||
resetError,
|
||||
plugin,
|
||||
}: ErrorBoundaryFallbackProps) => {
|
||||
return (
|
||||
<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,
|
||||
...defaultAppComponents(),
|
||||
Router: BrowserRouter,
|
||||
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
|
||||
ThemeProvider: AppThemeProvider,
|
||||
...options?.components,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
/// <reference types="react" />
|
||||
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { AppComponents } from '@backstage/core-plugin-api';
|
||||
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
|
||||
import { BackstagePalette } from '@backstage/theme';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
@@ -199,6 +200,9 @@ export type CustomProviderClassKey = 'form' | 'button';
|
||||
// @public (undocumented)
|
||||
export function DashboardIcon(props: IconComponentProps): JSX.Element;
|
||||
|
||||
// @public
|
||||
export function defaultAppComponents(): Omit<AppComponents, 'Router'>;
|
||||
|
||||
// @public
|
||||
type DependencyEdge<T = {}> = T & {
|
||||
from: string;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { OptionallyWrapInRouter } from './defaultAppComponents';
|
||||
|
||||
describe('OptionallyWrapInRouter', () => {
|
||||
it('should wrap with router if not yet inside a router', async () => {
|
||||
render(<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,83 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import {
|
||||
AppComponents,
|
||||
BootErrorPageProps,
|
||||
ErrorBoundaryFallbackProps,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ErrorPanel, Progress } from './components';
|
||||
import { ErrorPage } from './layout';
|
||||
import { MemoryRouter, useInRouterContext } from 'react-router';
|
||||
|
||||
export function OptionallyWrapInRouter({ children }: { children: ReactNode }) {
|
||||
if (useInRouterContext()) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return <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 function defaultAppComponents(): Omit<AppComponents, 'Router'> {
|
||||
return {
|
||||
Progress,
|
||||
NotFoundErrorPage: DefaultNotFoundPage,
|
||||
BootErrorPage: DefaultBootErrorPage,
|
||||
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
|
||||
};
|
||||
}
|
||||
@@ -25,3 +25,4 @@ export * from './hooks';
|
||||
export * from './icons';
|
||||
export * from './layout';
|
||||
export * from './overridableComponents';
|
||||
export { defaultAppComponents } from './defaultAppComponents';
|
||||
|
||||
@@ -25,11 +25,12 @@ import { entityPage } from './components/catalog/EntityPage';
|
||||
import { searchPage } from './components/search/SearchPage';
|
||||
import { Root } from './components/Root';
|
||||
|
||||
import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
|
||||
import { AlertDisplay, defaultAppComponents, OAuthRequestDialog } from '@backstage/core-components';
|
||||
import { createApp, FlatRoutes } from '@backstage/core-app-api';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
components: defaultAppComponents(),
|
||||
bindRoutes({ bind }) {
|
||||
bind(catalogPlugin.externalRoutes, {
|
||||
createComponent: scaffolderPlugin.routes.root,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Route } from 'react-router';
|
||||
|
||||
import {
|
||||
AlertDisplay,
|
||||
defaultAppComponents,
|
||||
OAuthRequestDialog,
|
||||
Sidebar,
|
||||
SidebarItem,
|
||||
@@ -177,6 +178,7 @@ export class DevAppBuilder {
|
||||
apis,
|
||||
plugins: this.plugins,
|
||||
themes: this.themes,
|
||||
components: defaultAppComponents(),
|
||||
bindRoutes: ({ bind }) => {
|
||||
for (const plugin of this.plugins ?? []) {
|
||||
const targets: Record<string, RouteRef<any>> = {};
|
||||
|
||||
Reference in New Issue
Block a user