diff --git a/.changeset/proud-bottles-dream.md b/.changeset/proud-bottles-dream.md new file mode 100644 index 0000000000..d9c7259758 --- /dev/null +++ b/.changeset/proud-bottles-dream.md @@ -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 { + // 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()), +]; +``` diff --git a/plugins/tech-radar/README.md b/plugins/tech-radar/README.md index 814ab6cba5..35c510ddd3 100644 --- a/plugins/tech-radar/README.md +++ b/plugins/tech-radar/README.md @@ -60,7 +60,6 @@ export type TechRadarPageProps = TechRadarComponentProps & { export interface TechRadarPageProps { width: number; height: number; - getData?: () => Promise; 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` 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 { + const data = await fetch('https://mydata.json').then(res => res.json()); -; + // 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? diff --git a/plugins/tech-radar/src/api.ts b/plugins/tech-radar/src/api.ts index eca80420bf..6f2a1a5cb1 100644 --- a/plugins/tech-radar/src/api.ts +++ b/plugins/tech-radar/src/api.ts @@ -15,6 +15,17 @@ */ import { MovedState } from './utils/types'; +import { createApiRef } from '@backstage/core'; + +export const techRadarApiRef = createApiRef({ + 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; +} /** * Types related to the Radar's visualization. @@ -65,16 +76,5 @@ export interface TechRadarLoaderResponse { export interface TechRadarComponentProps { width: number; height: number; - getData?: () => Promise; svgProps?: object; } - -/** - * Set up the Radar as a Backstage plugin. - */ - -export interface TechRadarApi extends TechRadarComponentProps { - title?: string; - subtitle?: string; - pageTitle?: string; -} diff --git a/plugins/tech-radar/src/components/RadarComponent.test.tsx b/plugins/tech-radar/src/components/RadarComponent.test.tsx index 59cababbc9..4b740597b5 100644 --- a/plugins/tech-radar/src/components/RadarComponent.test.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.test.tsx @@ -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 { + return { + entries: [], + quadrants: [], + rings: [], + }; + } + } + + const mockClient = new MockClient(); + it('should render a progress bar', async () => { jest.useFakeTimers(); const errorApi = { post: () => {} }; const { getByTestId, queryByTestId } = render( - + { 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( - + diff --git a/plugins/tech-radar/src/components/RadarComponent.tsx b/plugins/tech-radar/src/components/RadarComponent.tsx index a6e033a08e..39746e48fe 100644 --- a/plugins/tech-radar/src/components/RadarComponent.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.tsx @@ -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(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; diff --git a/plugins/tech-radar/src/components/RadarPage.test.tsx b/plugins/tech-radar/src/components/RadarPage.test.tsx index 918015001b..b5169fadd9 100644 --- a/plugins/tech-radar/src/components/RadarPage.test.tsx +++ b/plugins/tech-radar/src/components/RadarPage.test.tsx @@ -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 { + 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( - + + + , ), ); @@ -72,7 +86,9 @@ describe('RadarPage', () => { const { getByText, getByTestId } = await renderInTestApp( - + + + , ); @@ -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( - + , diff --git a/plugins/tech-radar/src/plugin.ts b/plugins/tech-radar/src/plugin.ts index 63af1f390f..e128600094 100644 --- a/plugins/tech-radar/src/plugin.ts +++ b/plugins/tech-radar/src/plugin.ts @@ -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( diff --git a/plugins/tech-radar/src/sampleData.ts b/plugins/tech-radar/src/sample.ts similarity index 95% rename from plugins/tech-radar/src/sampleData.ts rename to plugins/tech-radar/src/sample.ts index 70b9303f39..1e924d85d0 100644 --- a/plugins/tech-radar/src/sampleData.ts +++ b/plugins/tech-radar/src/sample.ts @@ -19,6 +19,7 @@ import { RadarQuadrant, RadarEntry, TechRadarLoaderResponse, + TechRadarApi, } from './api'; const rings = new Array(); @@ -169,10 +170,14 @@ entries.push({ quadrant: 'infrastructure', }); -export default function getSampleData(): Promise { - return Promise.resolve({ - rings, - quadrants, - entries, - }); +export const mock: TechRadarLoaderResponse = { + entries, + quadrants, + rings, +}; + +export class SampleTechRadarApi implements TechRadarApi { + async load() { + return mock; + } }