Merge pull request #5891 from backstage/feat/reimplement-tech-rader

Reworking the Tech Radar plugin to provide easier API to fetch your own data
This commit is contained in:
Ben Lambert
2021-06-03 12:00:09 +02:00
committed by GitHub
8 changed files with 157 additions and 78 deletions
+32
View File
@@ -0,0 +1,32 @@
---
'@backstage/plugin-tech-radar': minor
---
Migrating the Tech Radar to support using `ApiRefs` to load custom data.
If you had a `getData` function, you'll now need to encapsulate that logic in a class that can override the `techRadarApiRef`.
```ts
// app/src/lib/MyClient.ts
import {
TechRadarApi,
TechRadarLoaderResponse,
} from '@backstage/plugin-tech-radar';
class MyOwnClient implements TechRadarApi {
async load(): Promise<TechRadarLoaderResponse> {
// here's where you would put you logic to load the response that was previously passed into getData
}
}
// app/src/apis.ts
import { MyOwnClient } from './lib/MyClient';
import { techRadarApiRef } from '@backstage/plugin-tech-radar';
export const apis: AnyApiFactory[] = [
/*
...
*/
createApiFactory(techRadarApiRef, new MyOwnClient()),
];
```
+26 -28
View File
@@ -60,7 +60,6 @@ export type TechRadarPageProps = TechRadarComponentProps & {
export interface TechRadarPageProps {
width: number;
height: number;
getData?: () => Promise<TechRadarLoaderResponse>;
svgProps?: object;
}
```
@@ -73,36 +72,35 @@ export interface TechRadarPageProps {
### How do I load in my own data?
It's simple, you can pass through a `getData` prop which expects a `Promise<TechRadarLoaderResponse>` signature.
The `TechRadar` plugin uses the `techRadarApiRef` to get a client which implements the `TechRadarApi` interface. The default sample one is located [here](https://github.com/backstage/backstage/blob/master/plugins/tech-radar/src/sample.ts). To load your own data, you'll need to provide a class that implements the `TechRadarApi` and override the `techRadarApiRef` in the `app/src/apis.ts`.
Here's an example:
```ts
// app/src/lib/MyClient.ts
import {
TechRadarApi,
TechRadarLoaderResponse,
} from '@backstage/plugin-tech-radar';
```tsx
const getHardCodedData = () =>
Promise.resolve({
quadrants: [{ id: 'infrastructure', name: 'Infrastructure' }],
rings: [{ id: 'use', name: 'USE', color: '#93c47d' }],
entries: [
{
url: '#',
key: 'github-actions',
id: 'github-actions',
title: 'GitHub Actions',
quadrant: 'infrastructure',
timeline: [
{
moved: 0,
ringId: 'use',
date: new Date('2020-08-06'),
description:
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat',
},
],
},
],
});
class MyOwnClient implements TechRadarApi {
async load(): Promise<TechRadarLoaderResponse> {
const data = await fetch('https://mydata.json').then(res => res.json());
<TechRadarComponent width={1400} height={800} getData={getHardCodedData} />;
// maybe you'll need to do some data transformation here to make it look like TechRadarLoaderResponse
return data;
}
}
// app/src/apis.ts
import { MyOwnClient } from './lib/MyClient';
import { techRadarApiRef } from '@backstage/plugin-tech-radar';
export const apis: AnyApiFactory[] = [
/*
...
*/
createApiFactory(techRadarApiRef, new MyOwnClient()),
];
```
### How do I write tests?
+11 -11
View File
@@ -15,6 +15,17 @@
*/
import { MovedState } from './utils/types';
import { createApiRef } from '@backstage/core';
export const techRadarApiRef = createApiRef<TechRadarApi>({
id: 'plugin.techradar.service',
description: 'Used to populate data in the TechRadar plugin',
});
export interface TechRadarApi {
// Loads the TechRadar response data to pass through to the TechRadar component
load: () => Promise<TechRadarLoaderResponse>;
}
/**
* Types related to the Radar's visualization.
@@ -65,16 +76,5 @@ export interface TechRadarLoaderResponse {
export interface TechRadarComponentProps {
width: number;
height: number;
getData?: () => Promise<TechRadarLoaderResponse>;
svgProps?: object;
}
/**
* Set up the Radar as a Backstage plugin.
*/
export interface TechRadarApi extends TechRadarComponentProps {
title?: string;
subtitle?: string;
pageTitle?: string;
}
@@ -24,6 +24,7 @@ import { withLogCollector } from '@backstage/test-utils';
import GetBBoxPolyfill from '../utils/polyfills/getBBox';
import RadarComponent from './RadarComponent';
import { TechRadarLoaderResponse, techRadarApiRef, TechRadarApi } from '../api';
describe('RadarComponent', () => {
beforeAll(() => {
@@ -34,13 +35,30 @@ describe('RadarComponent', () => {
GetBBoxPolyfill.remove();
});
class MockClient implements TechRadarApi {
async load(): Promise<TechRadarLoaderResponse> {
return {
entries: [],
quadrants: [],
rings: [],
};
}
}
const mockClient = new MockClient();
it('should render a progress bar', async () => {
jest.useFakeTimers();
const errorApi = { post: () => {} };
const { getByTestId, queryByTestId } = render(
<ThemeProvider theme={lightTheme}>
<ApiProvider apis={ApiRegistry.from([[errorApiRef, errorApi]])}>
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[techRadarApiRef, mockClient],
])}
>
<RadarComponent
width={1200}
height={800}
@@ -61,16 +79,21 @@ describe('RadarComponent', () => {
it('should call the errorApi if load fails', async () => {
const errorApi = { post: jest.fn() };
const techRadarLoadFail = () =>
Promise.reject(new Error('404 Page Not Found'));
jest
.spyOn(mockClient, 'load')
.mockRejectedValue(new Error('404 Page Not Found'));
const { queryByTestId } = render(
<ThemeProvider theme={lightTheme}>
<ApiProvider apis={ApiRegistry.from([[errorApiRef, errorApi]])}>
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[techRadarApiRef, mockClient],
])}
>
<RadarComponent
width={1200}
height={800}
getData={techRadarLoadFail}
svgProps={{ 'data-testid': 'tech-radar-svg' }}
/>
</ApiProvider>
@@ -15,38 +15,35 @@
*/
import React, { useEffect } from 'react';
import { Progress, useApi, errorApiRef, ErrorApi } from '@backstage/core';
import { Progress, useApi, errorApiRef } from '@backstage/core';
import { useAsync } from 'react-use';
import Radar from '../components/Radar';
import { TechRadarComponentProps, TechRadarLoaderResponse } from '../api';
import getSampleData from '../sampleData';
import {
techRadarApiRef,
TechRadarComponentProps,
TechRadarLoaderResponse,
} from '../api';
import { Entry } from '../utils/types';
const useTechRadarLoader = (props: TechRadarComponentProps) => {
const errorApi = useApi<ErrorApi>(errorApiRef);
const useTechRadarLoader = () => {
const errorApi = useApi(errorApiRef);
const techRadarApi = useApi(techRadarApiRef);
const { getData } = props;
const state = useAsync(async () => {
if (getData) {
const response: TechRadarLoaderResponse = await getData();
return response;
}
return undefined;
}, [getData, errorApi]);
const { error, value, loading } = useAsync(async () => techRadarApi.load(), [
techRadarApi,
]);
useEffect(() => {
const { error } = state;
if (error) {
errorApi.post(error);
}
}, [errorApi, state]);
}, [error, errorApi]);
return state;
return { loading, value, error };
};
const RadarComponent = (props: TechRadarComponentProps): JSX.Element => {
const { loading, error, value: data } = useTechRadarLoader(props);
const { loading, error, value: data } = useTechRadarLoader();
const mapToEntries = (
loaderResponse: TechRadarLoaderResponse | undefined,
@@ -89,8 +86,4 @@ const RadarComponent = (props: TechRadarComponentProps): JSX.Element => {
);
};
RadarComponent.defaultProps = {
getData: getSampleData,
};
export default RadarComponent;
@@ -27,6 +27,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import GetBBoxPolyfill from '../utils/polyfills/getBBox';
import { RadarPage } from './RadarPage';
import { TechRadarLoaderResponse, techRadarApiRef, TechRadarApi } from '../api';
describe('RadarPage', () => {
beforeAll(() => {
@@ -36,6 +37,17 @@ describe('RadarPage', () => {
afterAll(() => {
GetBBoxPolyfill.remove();
});
class MockClient implements TechRadarApi {
async load(): Promise<TechRadarLoaderResponse> {
return {
entries: [],
quadrants: [],
rings: [],
};
}
}
const mockClient = new MockClient();
it('should render a progress bar', async () => {
jest.useFakeTimers();
@@ -49,7 +61,9 @@ describe('RadarPage', () => {
const { getByTestId, queryByTestId } = render(
wrapInTestApp(
<ThemeProvider theme={lightTheme}>
<RadarPage {...techRadarProps} />
<ApiProvider apis={ApiRegistry.from([[techRadarApiRef, mockClient]])}>
<RadarPage {...techRadarProps} />
</ApiProvider>
</ThemeProvider>,
),
);
@@ -72,7 +86,9 @@ describe('RadarPage', () => {
const { getByText, getByTestId } = await renderInTestApp(
<ThemeProvider theme={lightTheme}>
<RadarPage {...techRadarProps} />
<ApiProvider apis={ApiRegistry.from([[techRadarApiRef, mockClient]])}>
<RadarPage {...techRadarProps} />
</ApiProvider>
</ThemeProvider>,
);
@@ -86,18 +102,25 @@ describe('RadarPage', () => {
it('should call the errorApi if load fails', async () => {
const errorApi = new MockErrorApi({ collect: true });
const techRadarLoadFail = () =>
Promise.reject(new Error('404 Page Not Found'));
jest
.spyOn(mockClient, 'load')
.mockRejectedValue(new Error('404 Page Not Found'));
const techRadarProps = {
width: 1200,
height: 800,
getData: techRadarLoadFail,
svgProps: { 'data-testid': 'tech-radar-svg' },
};
const { queryByTestId } = await renderInTestApp(
<ThemeProvider theme={lightTheme}>
<ApiProvider apis={ApiRegistry.with(errorApiRef, errorApi)}>
<ApiProvider
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[techRadarApiRef, mockClient],
])}
>
<RadarPage {...techRadarProps} />
</ApiProvider>
</ThemeProvider>,
+5
View File
@@ -18,8 +18,12 @@ import {
createPlugin,
createRouteRef,
createRoutableExtension,
createApiFactory,
} from '@backstage/core';
import { techRadarApiRef } from './api';
import { SampleTechRadarApi } from './sample';
const rootRouteRef = createRouteRef({
title: 'Tech Radar',
});
@@ -29,6 +33,7 @@ export const techRadarPlugin = createPlugin({
routes: {
root: rootRouteRef,
},
apis: [createApiFactory(techRadarApiRef, new SampleTechRadarApi())],
});
export const TechRadarPage = techRadarPlugin.provide(
@@ -19,6 +19,7 @@ import {
RadarQuadrant,
RadarEntry,
TechRadarLoaderResponse,
TechRadarApi,
} from './api';
const rings = new Array<RadarRing>();
@@ -169,10 +170,14 @@ entries.push({
quadrant: 'infrastructure',
});
export default function getSampleData(): Promise<TechRadarLoaderResponse> {
return Promise.resolve({
rings,
quadrants,
entries,
});
export const mock: TechRadarLoaderResponse = {
entries,
quadrants,
rings,
};
export class SampleTechRadarApi implements TechRadarApi {
async load() {
return mock;
}
}