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:
@@ -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()),
|
||||
];
|
||||
```
|
||||
@@ -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?
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user