Skip to content

Commit

Permalink
core-app-api: deprecate dependency on core-components
Browse files Browse the repository at this point in the history
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
  • Loading branch information
Rugvip committed Nov 11, 2021
1 parent f979fcf commit 014cbf8
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 85 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-drinks-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage/dev-utils': patch
---

Migrated to explicit passing of components to `createApp`.
33 changes: 33 additions & 0 deletions .changeset/hot-walls-fail.md
Original file line number Diff line number Diff line change
@@ -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 }) {
```
5 changes: 5 additions & 0 deletions .changeset/late-rice-sit.md
Original file line number Diff line number Diff line change
@@ -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`.
16 changes: 16 additions & 0 deletions .changeset/twenty-swans-matter.md
Original file line number Diff line number Diff line change
@@ -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
},
});
```
2 changes: 2 additions & 0 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
defaultAppComponents,
} from '@backstage/core-components';
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
import {
Expand Down Expand Up @@ -95,6 +96,7 @@ const app = createApp({
},

components: {
...defaultAppComponents(),
SignInPage: props => {
return (
<SignInPage
Expand Down
28 changes: 2 additions & 26 deletions packages/core-app-api/src/app/createApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down
84 changes: 26 additions & 58 deletions packages/core-app-api/src/app/createApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
};
Expand Down
4 changes: 4 additions & 0 deletions packages/core-components/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
38 changes: 38 additions & 0 deletions packages/core-components/src/defaultAppComponents.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
83 changes: 83 additions & 0 deletions packages/core-components/src/defaultAppComponents.tsx
Original file line number Diff line number Diff line change
@@ -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,
};
}
1 change: 1 addition & 0 deletions packages/core-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export * from './hooks';
export * from './icons';
export * from './layout';
export * from './overridableComponents';
export { defaultAppComponents } from './defaultAppComponents';

0 comments on commit 014cbf8

Please sign in to comment.