Merge pull request #698 from spotify/bil/tech-radar-as-a-react-component

plugin(tech-radar): make configurable as a UI component or Backstage plugin
This commit is contained in:
Bilawal Hameed
2020-05-06 20:59:24 +02:00
committed by GitHub
9 changed files with 361 additions and 173 deletions
+9 -6
View File
@@ -30,11 +30,7 @@ import {
LighthouseRestApi,
} from '@backstage/plugin-lighthouse';
import {
techRadarApiRef,
TechRadar,
loadSampleData,
} from '@backstage/plugin-tech-radar';
import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar';
const builder = ApiRegistry.builder();
@@ -47,6 +43,13 @@ builder.add(errorApiRef, errorApiForwarder);
builder.add(featureFlagsApiRef, new FeatureFlags());
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
builder.add(techRadarApiRef, new TechRadar(1800, 800, loadSampleData));
builder.add(
techRadarApiRef,
new TechRadar({
width: 1500,
height: 800,
}),
);
export default builder.build() as ApiHolder;
+95 -69
View File
@@ -1,89 +1,120 @@
# @backstage/plugin-tech-radar
<img src="docs/screenshot.png" width="700" alt="Screenshot of Tech Radar plugin" />
<img src="docs/screenshot.png" alt="Screenshot of Tech Radar plugin" />
The Backstage integration for the Tech Radar based on [Zalando's Tech Radar](https://opensource.zalando.com/tech-radar/) open sourced on [GitHub](https://github.com/zalando/tech-radar). This is used at [Spotify](https://spotify.github.io) for visualizing the official guidelines of different areas of software development such as languages, frameworks, infrastructure and processes.
## Purpose
Zalando explains it very well on their website:
Zalando has a fantastic description [on their website](https://opensource.zalando.com/tech-radar/):
> The Tech Radar is a tool to inspire and support engineering teams at Zalando to pick the best technologies for new projects; it provides a platform to share knowledge and experience in technologies, to reflect on technology decisions and continuously evolve our technology landscape. Based on the pioneering work of ThoughtWorks, our Tech Radar sets out the changes in technologies that are interesting in software development — changes that we think our engineering teams should pay attention to and consider using in their projects.
It serves well for teams and companies of all sizes that want to have alignment and wish to visualize it. It scales well for companies who have dozens of different technologies in place.
It serves and scales well for teams and companies of all sizes that want to have alignment across dozens of technologies and visualize it in a simple way.
## Getting Started
In your installation, add the dependency to your Backstage installation:
The Tech Radar can be used in two ways:
- **Simple (Recommended)** - This gives you an out-of-the-box Tech Radar experience. It lives on the `/tech-radar` URL of your Backstage installation, and you can set a variety of configuration directly in your `apis.ts`.
- **Advanced** - This gives you the React UI component directly. It enables you to insert the Radar on your own layout or page for a more customized feel.
### Install
For either simple or advanced installations, you'll need to add the dependency using Yarn:
```sh
yarn add @backstage/plugin-tech-radar
```
In your `apis.ts` set up the "out of the box" implementation for Tech Radar:
### Simple 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,
loadSampleData,
} from '@backstage/plugin-tech-radar';
const builder = ApiRegistry.builder();
builder.add(techRadarApiRef, new TechRadar(1400, 800, loadSampleData));
builder.add(techRadarApiRef, new TechRadar({
width: 1400,
height: 800
));
export default builder.build() as ApiHolder;
```
It will then be available on your Backstage installation over at <http://localhost:3000/tech-radar>
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.
## Configuration
The implementation for the TechRadar class is:
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 TechRadarAdditionalOptions {
title?: string;
subtitle?: string;
export interface TechRadarComponentProps {
width: number;
height: number;
getData?: () => Promise<TechRadarLoaderResponse>;
svgProps?: object;
}
export interface TechRadarLoaderResponse {
quadrants: RadarQuadrant[];
rings: RadarRing[];
entries: RadarEntry[];
export interface TechRadarApi extends TechRadarComponentProps {
title?: string;
subtitle?: string;
}
export interface TechRadarApi {
width: number;
height: number;
load: () => Promise<TechRadarLoaderResponse>;
additionalOpts: TechRadarAdditionalOptions;
}
// Constructor signature for the `TechRadar` class
// constructor(
// public width: number,
// public height: number,
// public load: () => Promise<TechRadarLoaderResponse>,
// public additionalOpts: TechRadarAdditionalOptions = {},
// )
```
The source code is available in [api.ts](src/api.ts).
You can see the API directly over at [src/api.ts](./src/api.ts).
## Code Samples
### Advanced Configuration
### Set up Tech Radar with sample data
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.
See example above.
In your Backstage app, run the following command:
### Set up Tech Radar with hard-coded values
```sh
yarn create-plugin
```
In your plugin, in any React component you'd like to import the Tech Radar, do the following:
```tsx
import { TechRadarComponent } from '@backstage/plugin-tech-radar';
function MyCustomRadar() {
return <TechRadarComponent width={1400} height={800} />;
}
```
If you'd like to configure it more, see the `TechRadarComponentProps` TypeScript interface for options:
```ts
const hardCodedData = () =>
export interface TechRadarComponentProps {
width: number;
height: number;
getData?: () => Promise<TechRadarLoaderResponse>;
svgProps?: object;
}
```
You can see the API directly over at [src/api.ts](./src/api.ts).
## Frequently Asked Questions
### Who created the Tech Radar?
[ThoughtWorks](https://thoughtworks.com/radar) created the Tech Radar concept, and [Zalando created the visualization](https://opensource.zalando.com/tech-radar/) that we use at Spotify and in this plugin.
### 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).
Here's an example:
```tsx
const getHardCodedData = () =>
Promise.resolve({
quadrants: [{ id: 'infrastructure', name: 'Infrastructure' }],
rings: [{ id: 'use', name: 'USE', color: '#93c47d' }],
@@ -100,48 +131,43 @@ const hardCodedData = () =>
],
});
builder.add(techRadarApiRef, new TechRadar(1400, 800, hardCodedData));
// Simple
builder.add(techRadarApiRef, new TechRadar({
width: 1400,
height: 800,
getData: getHardCodedData
));
// Advanced
<TechRadarComponent width={1400} height={800} getData={getHardCodedData} />
```
### Set up Tech Radar with an API call
```ts
const apiRetrievedData = async () => {
const response = await fetch('http://example.com/tech-radar-values.json');
const json = await response.json();
return json as TechRadarLoaderResponse;
};
builder.add(techRadarApiRef, new TechRadar(1400, 800, apiRetrievedData));
```
### Use a custom title and subtitle
### 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(1400, 800, loadSampleData, {
title: 'My Company Tech Radar',
subtitle: 'Learn about what technologies we use at My Company.',
}),
);
```
### Use custom props
Great for testing through adding a `data-testid` for being able to test with `@testing-library/react`
```ts
builder.add(
techRadarApiRef,
new TechRadar(1400, 800, loadSampleData, {
new TechRadar({
width: 1400,
height: 800,
svgProps: {
// for the main <svg> tag of the visualization
'data-testid': 'tech-radar-svg',
},
}),
);
// Advanced
<TechRadarComponent
width={1400}
height={800}
svgProps={{
'data-testid': 'tech-radar-svg',
}}
/>;
// Then, in your tests...
// const { getByTestId } = render(...);
// expect(getByTestId('tech-radar-svg')).toBeInTheDocument();
+45 -20
View File
@@ -16,6 +16,10 @@
import { ApiRef } from '@backstage/core';
/**
* Types related to the Radar's visualization.
*/
export interface RadarRing {
id: string;
name: string;
@@ -37,42 +41,63 @@ export interface RadarEntry {
url: string;
}
/**
* Types related to data collection for the Radar.
*/
export interface TechRadarLoaderResponse {
quadrants: RadarQuadrant[];
rings: RadarRing[];
entries: RadarEntry[];
}
export interface TechRadarAdditionalOptions {
title?: string;
subtitle?: string;
/**
* Set up the Radar as a Backstage component.
*/
export interface TechRadarComponentProps {
width: number;
height: number;
getData?: () => Promise<TechRadarLoaderResponse>;
svgProps?: object;
}
export interface TechRadarApi {
width: number;
height: number;
load: () => Promise<TechRadarLoaderResponse>;
additionalOpts: TechRadarAdditionalOptions;
/**
* Set up the Radar as a Backstage plugin.
*/
export interface TechRadarApi extends TechRadarComponentProps {
title?: string;
subtitle?: string;
}
export const techRadarApiRef = new ApiRef<TechRadarApi>({
id: 'plugin.techradar',
description: 'Used by the Tech Radar to render the diagram',
description: 'Used by the Tech Radar to render the visualization',
});
export class TechRadar implements TechRadarApi {
private defaultAdditionalOpts: Partial<TechRadarAdditionalOptions> = {
title: 'Tech Radar',
subtitle: 'Welcome to the Tech Radar!',
};
// 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'];
constructor(
public width: number,
public height: number,
public load: () => Promise<TechRadarLoaderResponse>,
public additionalOpts: TechRadarAdditionalOptions = {},
) {
this.additionalOpts = { ...this.defaultAdditionalOpts, ...additionalOpts };
constructor(overrideOptions: TechRadarApi) {
const defaultOptions: Partial<TechRadarApi> = {
title: 'Tech Radar',
subtitle: 'Welcome to the Tech 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;
}
}
@@ -0,0 +1,102 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { 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 RadarComponent from './RadarComponent';
describe('RadarComponent', () => {
beforeAll(() => {
GetBBoxPolyfill.create(0, 0, 1000, 500);
});
afterAll(() => {
GetBBoxPolyfill.remove();
});
it('should render a progress bar', async () => {
const errorApi = { post: () => {} };
const { getByTestId, queryByTestId } = render(
<ThemeProvider theme={lightTheme}>
<ApiProvider apis={ApiRegistry.from([[errorApiRef, errorApi]])}>
<RadarComponent
width={1200}
height={800}
svgProps={{ 'data-testid': 'tech-radar-svg' }}
/>
</ApiProvider>
</ThemeProvider>,
);
expect(getByTestId('progress')).toBeInTheDocument();
await waitForElement(() => queryByTestId('tech-radar-svg'));
});
it('should call the errorApi if load fails', async () => {
const errorApi = { post: jest.fn() };
const techRadarLoadFail = () =>
Promise.reject(new Error('404 Page Not Found'));
const { queryByTestId } = render(
<ThemeProvider theme={lightTheme}>
<ApiProvider apis={ApiRegistry.from([[errorApiRef, errorApi]])}>
<RadarComponent
width={1200}
height={800}
getData={techRadarLoadFail}
svgProps={{ 'data-testid': 'tech-radar-svg' }}
/>
</ApiProvider>
</ThemeProvider>,
);
await waitForElement(() => !queryByTestId('progress'));
expect(errorApi.post).toHaveBeenCalledTimes(1);
expect(errorApi.post).toHaveBeenCalledWith(new Error('404 Page Not Found'));
expect(queryByTestId('tech-radar-svg')).not.toBeInTheDocument();
});
it('should not render without errorApiRef', () => {
expect(
withLogCollector(['error'], () => {
expect(() => {
render(
<ThemeProvider theme={lightTheme}>
<ApiProvider apis={ApiRegistry.from([])}>
<RadarComponent
width={1200}
height={800}
svgProps={{ 'data-testid': 'tech-radar-svg' }}
/>
</ApiProvider>
</ThemeProvider>,
);
}).toThrow();
}).error[0],
).toMatch(
/^Error: Uncaught \[Error: No implementation available for apiRef{core.error}\]/,
);
});
});
@@ -0,0 +1,85 @@
/*
* Copyright 2020 Spotify AB
*
* 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, { useEffect, useState, FC } from 'react';
import { Progress, useApi, errorApiRef, ErrorApi } from '@backstage/core';
import Radar from '../components/Radar';
import { TechRadarComponentProps, TechRadarLoaderResponse } from '../api';
import getSampleData from '../sampleData';
const useTechRadarLoader = (props: TechRadarComponentProps) => {
const [state, setState] = useState<{
loading: boolean;
error?: Error;
data?: TechRadarLoaderResponse;
}>({
loading: true,
error: undefined,
data: undefined,
});
useEffect(() => {
if (!props.getData) {
return;
}
props
.getData()
.then((payload: TechRadarLoaderResponse) => {
setState({ loading: false, error: undefined, data: payload });
})
.catch((err: Error) => {
setState({
loading: false,
error: err,
data: undefined,
});
});
}, []);
return state;
};
const RadarComponent: FC<TechRadarComponentProps> = props => {
const errorApi = useApi<ErrorApi>(errorApiRef);
const { loading, error, data } = useTechRadarLoader(props);
useEffect(() => {
if (error) {
errorApi.post(error);
}
}, [error && error.message]);
return (
<>
{loading && <Progress />}
{!loading && !error && (
<Radar
{...props}
rings={data!.rings}
quadrants={data!.quadrants}
entries={data!.entries}
/>
)}
</>
);
};
RadarComponent.defaultProps = {
getData: getSampleData,
};
export default RadarComponent;
@@ -22,7 +22,7 @@ import { ApiRegistry, ApiProvider, errorApiRef } from '@backstage/core';
import { withLogCollector } from '@backstage/test-utils-core';
import GetBBoxPolyfill from '../utils/polyfills/getBBox';
import { techRadarApiRef, TechRadar, loadSampleData } from '../index';
import { techRadarApiRef, TechRadar } from '../index';
import RadarPage from './RadarPage';
describe('RadarPage', () => {
@@ -36,7 +36,9 @@ describe('RadarPage', () => {
it('should render a progress bar', async () => {
const errorApi = { post: () => {} };
const techRadarApi = new TechRadar(1200, 800, loadSampleData, {
const techRadarApi = new TechRadar({
width: 1200,
height: 800,
svgProps: { 'data-testid': 'tech-radar-svg' },
});
@@ -60,7 +62,9 @@ describe('RadarPage', () => {
it('should render a header with a svg', async () => {
const errorApi = { post: () => {} };
const techRadarApi = new TechRadar(1200, 800, loadSampleData, {
const techRadarApi = new TechRadar({
width: 1200,
height: 800,
svgProps: { 'data-testid': 'tech-radar-svg' },
});
@@ -87,7 +91,10 @@ describe('RadarPage', () => {
const errorApi = { post: jest.fn() };
const techRadarLoadFail = () =>
Promise.reject(new Error('404 Page Not Found'));
const techRadarApi = new TechRadar(1200, 800, techRadarLoadFail, {
const techRadarApi = new TechRadar({
width: 1200,
height: 800,
getData: techRadarLoadFail,
svgProps: { 'data-testid': 'tech-radar-svg' },
});
@@ -112,7 +119,10 @@ describe('RadarPage', () => {
});
it('should not render without errorApiRef', () => {
const techRadarApi = new TechRadar(1200, 800, loadSampleData);
const techRadarApi = new TechRadar({
width: 1200,
height: 800,
});
expect(
withLogCollector(['error'], () => {
@@ -14,81 +14,22 @@
* limitations under the License.
*/
import React, { useEffect, useState, FC } from 'react';
import React, { FC } from 'react';
import { Grid } from '@material-ui/core';
import {
Progress,
Page,
Header,
Content,
pageTheme,
useApi,
errorApiRef,
ErrorApi,
} from '@backstage/core';
import Radar from '../components/Radar';
import { techRadarApiRef, TechRadarApi, TechRadarLoaderResponse } from '../api';
const useTechRadarLoader = (techRadarApi: TechRadarApi) => {
const [state, setState] = useState<{
loading: boolean;
error?: Error;
data?: TechRadarLoaderResponse;
}>({
loading: true,
error: undefined,
data: undefined,
});
useEffect(() => {
techRadarApi
.load()
.then((payload: TechRadarLoaderResponse) => {
setState({ loading: false, error: undefined, data: payload });
})
.catch((err: Error) => {
setState({
loading: false,
error: err,
data: undefined,
});
});
}, []);
return state;
};
import { Page, Header, Content, pageTheme, useApi } from '@backstage/core';
import RadarComponent from '../components/RadarComponent';
import { techRadarApiRef, TechRadarApi } from '../api';
const RadarPage: FC<{}> = () => {
const errorApi = useApi<ErrorApi>(errorApiRef);
const techRadarApi = useApi<TechRadarApi>(techRadarApiRef);
const { loading, error, data } = useTechRadarLoader(techRadarApi);
useEffect(() => {
if (error) {
errorApi.post(error);
}
}, [error && error.message]);
return (
<Page theme={pageTheme.home}>
<Header
title={techRadarApi.additionalOpts.title}
subtitle={techRadarApi.additionalOpts.subtitle}
/>
<Header title={techRadarApi.title} subtitle={techRadarApi.subtitle} />
<Content>
<Grid container spacing={3} direction="row">
<Grid item xs={12} sm={6} md={4}>
{loading && <Progress />}
{!loading && !error && (
<Radar
width={techRadarApi.width}
height={techRadarApi.height}
svgProps={techRadarApi.additionalOpts.svgProps}
rings={data!.rings}
quadrants={data!.quadrants}
entries={data!.entries}
/>
)}
<RadarComponent {...techRadarApi} />
</Grid>
</Grid>
</Content>
+3 -7
View File
@@ -17,15 +17,11 @@
export { plugin } from './plugin';
/**
* The API for configuring the Tech Radar in a Backstage deployment.
* The TypeScript API for configuring Tech Radar.
*/
export * from './api';
/**
* Load sample data for Backstage users to get setup quickly.
*
* @example
* import { techRadarApiRef, TechRadar, loadSampleData } from '@backstage/plugin-tech-radar';
* builder.add(techRadarApiRef, new TechRadar(800, 500, loadSampleData));
* The React component for more advanced use cases.
*/
export { default as loadSampleData } from './sampleData';
export { default as TechRadarComponent } from './components/RadarComponent';
+1 -1
View File
@@ -98,7 +98,7 @@ entries.push({
quadrant: 'infrastructure',
});
export default function loadSampleData(): Promise<TechRadarLoaderResponse> {
export default function getSampleData(): Promise<TechRadarLoaderResponse> {
return Promise.resolve({
rings,
quadrants,