plugins/tech-radar: migrate to new plugin pattern and avoid using api for config

This commit is contained in:
Patrik Oldsberg
2020-09-07 20:04:23 +02:00
parent 817d958026
commit 66a2a870bc
13 changed files with 102 additions and 269 deletions
+5
View File
@@ -28,6 +28,7 @@ import { hot } from 'react-hot-loader/root';
import { providers } from './identityProviders';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { Route, Routes, Navigate } from 'react-router';
import { EntityPage } from './components/catalog/EntityPage';
@@ -60,6 +61,10 @@ const AppRoutes = () => (
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Route path="/docs/*" element={<DocsRouter />} />
<Route
path="/tech-radar"
element={<TechRadarRouter width={1500} height={800} />}
/>
<Navigate key="/" to="/catalog" />
{...deprecatedAppRoutes}
</Routes>
-10
View File
@@ -51,8 +51,6 @@ import {
LighthouseRestApi,
} from '@backstage/plugin-lighthouse';
import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar';
import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
@@ -182,14 +180,6 @@ export const apis = (config: ConfigApi) => {
}),
);
builder.add(
techRadarApiRef,
new TechRadar({
width: 1500,
height: 800,
}),
);
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
@@ -11,6 +11,7 @@ import { AppSidebar } from './sidebar';
import { Route, Routes, Navigate } from 'react-router';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { EntityPage } from './components/catalog/EntityPage';
const app = createApp({
@@ -35,6 +36,10 @@ const App: FC<{}> = () => (
element={<CatalogRouter EntityPage={EntityPage} />}
/>
<Route path="/docs/*" element={<DocsRouter />} />
<Route
path="/tech-radar"
element={<TechRadarRouter width={1500} height={800} />}
/>
<Navigate key="/" to="/catalog" />
{deprecatedAppRoutes}
</Routes>
@@ -29,8 +29,6 @@ import {
TechDocsStorageApi,
} from '@backstage/plugin-techdocs';
import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar';
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
@@ -72,14 +70,6 @@ export const apis = (config: ConfigApi) => {
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
builder.add(
techRadarApiRef,
new TechRadar({
width: 1500,
height: 800,
}),
);
builder.add(
techdocsStorageApiRef,
new TechDocsStorageApi({ apiOrigin: techdocsStorageUrl }),
+24 -84
View File
@@ -29,71 +29,34 @@ For either simple or advanced installations, you'll need to add the dependency u
yarn add @backstage/plugin-tech-radar
```
### Simple Configuration
### Configuration
In your `apis.ts` set up the simple "out of the box" implementation for Tech Radar:
```ts
import { ApiHolder, ApiRegistry } from '@backstage/core';
import {
techRadarApiRef,
TechRadar,
} from '@backstage/plugin-tech-radar';
const builder = ApiRegistry.builder();
builder.add(techRadarApiRef, new TechRadar({
width: 1400,
height: 800
));
export default builder.build() as ApiHolder;
```
Congrats, you're done! We'll just load it with [example data](src/sampleData.ts) to get you started. Just go to <http://localhost:3000/tech-radar> to see it live in action.
And if you'd like to configure it more, such as providing it with your own data, see the `TechRadarApi` TypeScript interface below for the options:
```ts
export interface TechRadarComponentProps {
width: number;
height: number;
getData?: () => Promise<TechRadarLoaderResponse>;
svgProps?: object;
}
export interface TechRadarApi extends TechRadarComponentProps {
title?: string;
subtitle?: string;
}
```
You can see the API directly over at [src/api.ts](./src/api.ts).
### Advanced Configuration
This way won't expose an `/tech-radar` path. Instead, you'll need to create your own Backstage plugin and use the Tech Radar as any other React UI component.
In your Backstage app, run the following command:
```sh
yarn create-plugin
```
In your plugin, in any React component you'd like to import the Tech Radar, do the following:
Modify your app routes to include the Router component exported from the tech radar, for example:
```tsx
import { TechRadarComponent } from '@backstage/plugin-tech-radar';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
function MyCustomRadar() {
return <TechRadarComponent width={1400} height={800} />;
}
// Inside App component
<Routes>
{/* other routes ... */}
<Route
path="/tech-radar"
element={<TechRadarRouter width={1500} height={800} />}
/>
{/* other routes ... */}
</Routes>;
```
If you'd like to configure it more, see the `TechRadarComponentProps` TypeScript interface for options:
If you'd like to configure it more, see the `TechRadarPageProps` and `TechRadarComponentProps` types for options:
```ts
export interface TechRadarComponentProps {
export type TechRadarPageProps = TechRadarComponentProps & {
title?: string;
subtitle?: string;
pageTitle?: string;
};
export interface TechRadarPageProps {
width: number;
height: number;
getData?: () => Promise<TechRadarLoaderResponse>;
@@ -101,8 +64,6 @@ export interface TechRadarComponentProps {
}
```
You can see the API directly over at [src/api.ts](./src/api.ts).
## Frequently Asked Questions
### Who created the Tech Radar?
@@ -111,7 +72,7 @@ You can see the API directly over at [src/api.ts](./src/api.ts).
### How do I load in my own data?
It's simple. In both the Simple (Backstage plugin) and Advanced (React component) configurations, you can pass through a `getData` prop which expects a `Promise<TechRadarLoaderResponse>` signature. See more in [src/api.ts](./src/api.ts).
It's simple, you can pass through a `getData` prop which expects a `Promise<TechRadarLoaderResponse>` signature.
Here's an example:
@@ -133,42 +94,21 @@ const getHardCodedData = () =>
],
});
// Simple
builder.add(techRadarApiRef, new TechRadar({
width: 1400,
height: 800,
getData: getHardCodedData
));
// Advanced
<TechRadarComponent width={1400} height={800} getData={getHardCodedData} />
<TechRadarComponent width={1400} height={800} getData={getHardCodedData} />;
```
### How do I write tests?
You can use the `svgProps` option to pass custom React props to the `<svg>` element we create for the Tech Radar. This complements well with the `data-testid` attribute and the `@testing-library/react` library we use in Backstage.
```ts
// Simple
builder.add(
techRadarApiRef,
new TechRadar({
width: 1400,
height: 800,
svgProps: {
'data-testid': 'tech-radar-svg',
},
}),
);
// Advanced
```tsx
<TechRadarComponent
width={1400}
height={800}
svgProps={{
'data-testid': 'tech-radar-svg',
}}
/>;
/>
// Then, in your tests...
// const { getByTestId } = render(...);
+2 -10
View File
@@ -15,14 +15,6 @@
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
import { techRadarApiRef, TechRadar } from '../src';
import { plugin } from '../src';
createDevApp()
.registerPlugin(plugin)
.registerApiFactory({
implements: techRadarApiRef,
deps: {},
factory: () => new TechRadar({ width: 1500, height: 800 }),
})
.render();
createDevApp().registerPlugin(plugin).render();
+1 -1
View File
@@ -22,7 +22,7 @@
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/test-utils-core": "^0.1.1-alpha.21",
"@backstage/test-utils": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
-35
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { createApiRef } from '@backstage/core';
import { MovedState } from './utils/types';
/**
@@ -72,37 +71,3 @@ export interface TechRadarApi extends TechRadarComponentProps {
subtitle?: string;
pageTitle?: string;
}
export const techRadarApiRef = createApiRef<TechRadarApi>({
id: 'plugin.techradar',
description: 'Used by the Tech Radar to render the visualization',
});
export class TechRadar implements TechRadarApi {
// Default columns
public width: TechRadarApi['width'];
public height: TechRadarApi['height'];
public getData: TechRadarApi['getData'];
public svgProps: TechRadarApi['svgProps'];
public title: TechRadarApi['title'];
public subtitle: TechRadarApi['subtitle'];
public pageTitle: TechRadarApi['pageTitle'];
constructor(overrideOptions: TechRadarApi) {
const defaultOptions: Partial<TechRadarApi> = {
title: 'Tech Radar',
subtitle: 'Pick the recommended technologies for your projects',
pageTitle: 'Company Radar',
};
const options = { ...defaultOptions, ...overrideOptions };
this.width = options.width;
this.height = options.height;
this.getData = options.getData;
this.svgProps = options.svgProps;
this.title = options.title;
this.subtitle = options.subtitle;
this.pageTitle = options.pageTitle;
}
}
@@ -19,7 +19,7 @@ import { render, waitForElement } from '@testing-library/react';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { ApiRegistry, ApiProvider, errorApiRef } from '@backstage/core';
import { withLogCollector } from '@backstage/test-utils-core';
import { withLogCollector } from '@backstage/test-utils';
import GetBBoxPolyfill from '../utils/polyfills/getBBox';
import RadarComponent from './RadarComponent';
@@ -19,11 +19,10 @@ import { render, waitForElement } from '@testing-library/react';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { ApiRegistry, ApiProvider, errorApiRef } from '@backstage/core';
import { withLogCollector } from '@backstage/test-utils-core';
import GetBBoxPolyfill from '../utils/polyfills/getBBox';
import { techRadarApiRef, TechRadar } from '../index';
import RadarPage from './RadarPage';
import { RadarPage } from './RadarPage';
import { MockErrorApi, wrapInTestApp } from '@backstage/test-utils';
describe('RadarPage', () => {
beforeAll(() => {
@@ -35,24 +34,18 @@ describe('RadarPage', () => {
});
it('should render a progress bar', async () => {
const errorApi = { post: () => {} };
const techRadarApi = new TechRadar({
const techRadarProps = {
width: 1200,
height: 800,
svgProps: { 'data-testid': 'tech-radar-svg' },
});
};
const { getByTestId, queryByTestId } = render(
<ThemeProvider theme={lightTheme}>
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[techRadarApiRef, techRadarApi],
])}
>
<RadarPage />
</ApiProvider>
</ThemeProvider>,
wrapInTestApp(
<ThemeProvider theme={lightTheme}>
<RadarPage {...techRadarProps} />
</ThemeProvider>,
),
);
expect(getByTestId('progress')).toBeInTheDocument();
@@ -61,24 +54,18 @@ describe('RadarPage', () => {
});
it('should render a header with a svg', async () => {
const errorApi = { post: () => {} };
const techRadarApi = new TechRadar({
const techRadarProps = {
width: 1200,
height: 800,
svgProps: { 'data-testid': 'tech-radar-svg' },
});
};
const { getByText, getByTestId } = render(
<ThemeProvider theme={lightTheme}>
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[techRadarApiRef, techRadarApi],
])}
>
<RadarPage />
</ApiProvider>
</ThemeProvider>,
wrapInTestApp(
<ThemeProvider theme={lightTheme}>
<RadarPage {...techRadarProps} />
</ThemeProvider>,
),
);
await waitForElement(() => getByTestId('tech-radar-svg'));
@@ -90,78 +77,29 @@ describe('RadarPage', () => {
});
it('should call the errorApi if load fails', async () => {
const errorApi = { post: jest.fn() };
const errorApi = new MockErrorApi({ collect: true });
const techRadarLoadFail = () =>
Promise.reject(new Error('404 Page Not Found'));
const techRadarApi = new TechRadar({
const techRadarProps = {
width: 1200,
height: 800,
getData: techRadarLoadFail,
svgProps: { 'data-testid': 'tech-radar-svg' },
});
};
const { queryByTestId } = render(
<ThemeProvider theme={lightTheme}>
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[techRadarApiRef, techRadarApi],
])}
>
<RadarPage />
<ApiProvider apis={ApiRegistry.with(errorApiRef, errorApi)}>
<RadarPage {...techRadarProps} />
</ApiProvider>
</ThemeProvider>,
);
await waitForElement(() => !queryByTestId('progress'));
expect(errorApi.post).toHaveBeenCalledTimes(1);
expect(errorApi.post).toHaveBeenCalledWith(new Error('404 Page Not Found'));
expect(errorApi.getErrors()).toEqual([
{ error: new Error('404 Page Not Found'), context: undefined },
]);
expect(queryByTestId('tech-radar-svg')).not.toBeInTheDocument();
});
it('should not render without errorApiRef', () => {
const techRadarApi = new TechRadar({
width: 1200,
height: 800,
});
expect(
withLogCollector(['error'], () => {
expect(() => {
render(
<ThemeProvider theme={lightTheme}>
<ApiProvider
apis={ApiRegistry.from([[techRadarApiRef, techRadarApi]])}
>
<RadarPage />
</ApiProvider>
</ThemeProvider>,
);
}).toThrow();
}).error[0],
).toMatch(
/^Error: Uncaught \[Error: No implementation available for apiRef{core.error}\]/,
);
});
it('should not render without techRadarApiRef', () => {
const errorApi = { post: () => {} };
expect(
withLogCollector(['error'], () => {
expect(() => {
render(
<ThemeProvider theme={lightTheme}>
<ApiProvider apis={ApiRegistry.from([[errorApiRef, errorApi]])}>
<RadarPage />
</ApiProvider>
</ThemeProvider>,
);
}).toThrow();
}).error[0],
).toMatch(
/^Error: Uncaught \[Error: No implementation available for apiRef{plugin.techradar}\]/,
);
});
});
+38 -28
View File
@@ -24,36 +24,46 @@ import {
HeaderLabel,
SupportButton,
pageTheme,
useApi,
} from '@backstage/core';
import RadarComponent from '../components/RadarComponent';
import { techRadarApiRef, TechRadarApi } from '../api';
import { TechRadarComponentProps } from '../api';
const RadarPage = (): JSX.Element => {
const techRadarApi = useApi<TechRadarApi>(techRadarApiRef);
return (
<Page theme={pageTheme.tool}>
<Header title={techRadarApi.title} subtitle={techRadarApi.subtitle}>
<HeaderLabel label="Owner" value="Spotify" />
<HeaderLabel label="Lifecycle" value="Beta" />
</Header>
<Content>
<ContentHeader title={techRadarApi.pageTitle}>
<SupportButton>
This is used for visualizing the official guidelines of different
areas of software development such as languages, frameworks,
infrastructure and processes.
</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="row">
<Grid item xs={12} sm={6} md={4}>
<RadarComponent {...techRadarApi} />
</Grid>
</Grid>
</Content>
</Page>
);
export type TechRadarPageProps = TechRadarComponentProps & {
title?: string;
subtitle?: string;
pageTitle?: string;
};
export default RadarPage;
export const RadarPage = ({
title,
subtitle,
pageTitle,
...props
}: TechRadarPageProps): JSX.Element => (
<Page theme={pageTheme.tool}>
<Header title={title} subtitle={subtitle}>
<HeaderLabel label="Owner" value="Spotify" />
<HeaderLabel label="Lifecycle" value="Beta" />
</Header>
<Content>
<ContentHeader title={pageTitle}>
<SupportButton>
This is used for visualizing the official guidelines of different
areas of software development such as languages, frameworks,
infrastructure and processes.
</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="row">
<Grid item xs={12} sm={6} md={4}>
<RadarComponent {...props} />
</Grid>
</Grid>
</Content>
</Page>
);
RadarPage.defaultProps = {
title: 'Tech Radar',
subtitle: 'Pick the recommended technologies for your projects',
pageTitle: 'Company Radar',
};
+2
View File
@@ -16,6 +16,8 @@
export { plugin } from './plugin';
export { RadarPage as Router } from './components/RadarPage';
/**
* The TypeScript API for configuring Tech Radar.
*/
-4
View File
@@ -15,11 +15,7 @@
*/
import { createPlugin } from '@backstage/core';
import RadarPage from './components/RadarPage';
export const plugin = createPlugin({
id: 'tech-radar',
register({ router }) {
router.registerRoute('/tech-radar', RadarPage);
},
});