From 654e8ac5afdc20874f97a3559081caf677b43dad Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Fri, 23 Dec 2022 13:11:49 -0500 Subject: [PATCH] Added api for adr plugin Switched to using an AdrApi and removed the fetchers for adr reading. Signed-off-by: Robert Bunning --- plugins/adr/src/api/AdrClient.ts | 66 +++++++++ plugins/adr/src/{hooks => api}/index.ts | 11 +- plugins/adr/src/api/types.ts | 74 ++++++++++ .../components/AdrReader/AdrReader.test.tsx | 129 ---------------- .../src/components/AdrReader/AdrReader.tsx | 17 +-- .../EntityAdrContent.test.tsx | 139 ------------------ .../EntityAdrContent/EntityAdrContent.tsx | 23 ++- plugins/adr/src/hooks/adrFileFetcher.ts | 99 ------------- plugins/adr/src/hooks/useOctokitRequest.ts | 59 -------- plugins/adr/src/index.ts | 4 +- plugins/adr/src/plugin.ts | 15 +- 11 files changed, 184 insertions(+), 452 deletions(-) create mode 100644 plugins/adr/src/api/AdrClient.ts rename plugins/adr/src/{hooks => api}/index.ts (73%) create mode 100644 plugins/adr/src/api/types.ts delete mode 100644 plugins/adr/src/components/AdrReader/AdrReader.test.tsx delete mode 100644 plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx delete mode 100644 plugins/adr/src/hooks/adrFileFetcher.ts delete mode 100644 plugins/adr/src/hooks/useOctokitRequest.ts diff --git a/plugins/adr/src/api/AdrClient.ts b/plugins/adr/src/api/AdrClient.ts new file mode 100644 index 0000000000..d79a327db3 --- /dev/null +++ b/plugins/adr/src/api/AdrClient.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { DiscoveryApi } from '@backstage/core-plugin-api'; +import { AdrApi, AdrListResult, AdrReadResult } from './types'; + +/** + * Options for creating an AdrClient. + * + * @public + */ +export interface AdrClientOptions { + discoveryApi: DiscoveryApi; +} + +const readEndpoint = 'file'; +const listEndpoint = 'list'; + +/** + * An implementation of the AdrApi that communicates with the ADR backend plugin. + * + * @public + */ +export class AdrClient implements AdrApi { + private readonly discoveryApi: DiscoveryApi; + + constructor(options: AdrClientOptions) { + this.discoveryApi = options.discoveryApi; + } + + private async fetchAdrApi(endpoint: string, fileUrl: string): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('adr'); + const targetUrl = `${baseUrl}/${endpoint}?url=${encodeURIComponent( + fileUrl, + )}`; + + const result = await fetch(targetUrl); + const data = await result.json(); + + if (!result.ok) { + throw new Error(data.error.message); + } + return data; + } + + async listAdrs(url: string): Promise { + return this.fetchAdrApi(listEndpoint, url); + } + + async readAdr(url: string): Promise { + return this.fetchAdrApi(readEndpoint, url); + } +} diff --git a/plugins/adr/src/hooks/index.ts b/plugins/adr/src/api/index.ts similarity index 73% rename from plugins/adr/src/hooks/index.ts rename to plugins/adr/src/api/index.ts index d5aa102fb8..095da3519d 100644 --- a/plugins/adr/src/hooks/index.ts +++ b/plugins/adr/src/api/index.ts @@ -13,4 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './useOctokitRequest'; + +export { AdrClient } from './AdrClient'; +export type { AdrClientOptions } from './AdrClient'; +export { adrApiRef } from './types'; +export type { + AdrApi, + AdrFileInfo, + AdrListResult, + AdrReadResult, +} from './types'; diff --git a/plugins/adr/src/api/types.ts b/plugins/adr/src/api/types.ts new file mode 100644 index 0000000000..e4632e171a --- /dev/null +++ b/plugins/adr/src/api/types.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { createApiRef } from '@backstage/core-plugin-api'; + +/** + * Contains information about an ADR file. + * + * @public + */ +export type AdrFileInfo = { + /** The file type. */ + type: string; + + /** The relative path of the ADR file. */ + path: string; + + /** The name of the ADR file. */ + name: string; +}; + +/** + * The result of listing ADRs. + * + * @public + */ +export type AdrListResult = { + data: AdrFileInfo[]; +}; + +/** + * The result of reading an ADR. + * + * @public + */ +export type AdrReadResult = { + /** The contents of the read ADR file. */ + data: string; +}; + +/** + * The API used by the adr plugin to list and read ADRs. + * + * @public + */ +export interface AdrApi { + /** Lists the ADRs at the provided url. */ + listAdrs(url: string): Promise; + + /** Reads the contents of the ADR at the provided url. */ + readAdr(url: string): Promise; +} + +/** + * ApiRef for the AdrApi. + * + * @public + */ +export const adrApiRef = createApiRef({ + id: 'plugin.adr.api', +}); diff --git a/plugins/adr/src/components/AdrReader/AdrReader.test.tsx b/plugins/adr/src/components/AdrReader/AdrReader.test.tsx deleted file mode 100644 index 1b1cdcf851..0000000000 --- a/plugins/adr/src/components/AdrReader/AdrReader.test.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { EntityLayout } from '@backstage/plugin-catalog'; -import { Entity, ANNOTATION_SOURCE_LOCATION } from '@backstage/catalog-model'; -import { ApiProvider } from '@backstage/core-app-api'; -import { CatalogApi } from '@backstage/catalog-client'; -import { - EntityProvider, - catalogApiRef, - starredEntitiesApiRef, - MockStarredEntitiesApi, -} from '@backstage/plugin-catalog-react'; -import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { permissionApiRef } from '@backstage/plugin-permission-react'; -import { - renderInTestApp, - TestApiRegistry, - MockPermissionApi, -} from '@backstage/test-utils'; -import { AdrReader } from './AdrReader'; -import { ScmIntegrationRegistry } from '@backstage/integration'; -import { ANNOTATION_ADR_LOCATION } from '@backstage/plugin-adr-common'; -import { - octokitAdrFileFetcher, - urlReaderAdrFileFetcher, -} from '../../hooks/adrFileFetcher'; - -const mockApis = TestApiRegistry.from( - [catalogApiRef, {} as CatalogApi], - [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], - [ - scmIntegrationsApiRef, - { - resolveUrl: options => `${options.url}`, - } as ScmIntegrationRegistry, - ], -); - -const mockEntity: Entity = { - kind: 'TestEntity', - metadata: { - name: 'Testing Entity 1', - annotations: { - [ANNOTATION_ADR_LOCATION]: 'testAdrFolder', - [ANNOTATION_SOURCE_LOCATION]: 'source:location', - }, - }, - apiVersion: '', -}; - -afterEach(() => { - jest.resetAllMocks(); -}); - -describe('AdrReader', () => { - it('Falls back to octokitAdrFileFetcher when adrFileFetcher is not specified', async () => { - const spyInstance = jest - .spyOn(octokitAdrFileFetcher, 'useReadAdrFileAtUrl') - .mockImplementation(() => { - return { data: '' }; - }); - - await renderInTestApp( - - - - - - - - - , - ); - - expect(spyInstance).toHaveBeenCalled(); - }); - - it('Uses an alternative AdrFileFetcher when provided', async () => { - const octokitSpyInstance = jest - .spyOn(octokitAdrFileFetcher, 'useReadAdrFileAtUrl') - .mockImplementation(() => { - return { - data: '', - }; - }); - - const urlReadersSpyInstance = jest - .spyOn(urlReaderAdrFileFetcher, 'useReadAdrFileAtUrl') - .mockImplementation(() => { - return { - data: '', - }; - }); - - await renderInTestApp( - - - - - - - - - , - ); - - expect(octokitSpyInstance).not.toHaveBeenCalled(); - expect(urlReadersSpyInstance).toHaveBeenCalled(); - }); -}); diff --git a/plugins/adr/src/components/AdrReader/AdrReader.tsx b/plugins/adr/src/components/AdrReader/AdrReader.tsx index 6f76295e8c..ad99ce3a5a 100644 --- a/plugins/adr/src/components/AdrReader/AdrReader.tsx +++ b/plugins/adr/src/components/AdrReader/AdrReader.tsx @@ -28,10 +28,8 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import { adrDecoratorFactories } from './decorators'; import { AdrContentDecorator } from './types'; -import { - AdrFileFetcher, - octokitAdrFileFetcher, -} from '../../hooks/adrFileFetcher'; +import { adrApiRef } from '../../api'; +import useAsync from 'react-use/lib/useAsync'; /** * Component to fetch and render an ADR. @@ -41,16 +39,17 @@ import { export const AdrReader = (props: { adr: string; decorators?: AdrContentDecorator[]; - adrFileFetcher?: AdrFileFetcher; }) => { - const { adr, decorators, adrFileFetcher } = props; + const { adr, decorators } = props; const { entity } = useEntity(); const scmIntegrations = useApi(scmIntegrationsApiRef); + const adrApi = useApi(adrApiRef); const adrLocationUrl = getAdrLocationUrl(entity, scmIntegrations); - const targetAdrFileFetcher = adrFileFetcher ?? octokitAdrFileFetcher; - const { value, loading, error } = targetAdrFileFetcher.useReadAdrFileAtUrl( - `${adrLocationUrl.replace(/\/$/, '')}/${adr}`, + const url = `${adrLocationUrl.replace(/\/$/, '')}/${adr}`; + const { value, loading, error } = useAsync( + async () => adrApi.readAdr(url), + [url], ); const adrContent = useMemo(() => { diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx deleted file mode 100644 index ca6397e736..0000000000 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { EntityLayout } from '@backstage/plugin-catalog'; -import { Entity, ANNOTATION_SOURCE_LOCATION } from '@backstage/catalog-model'; -import { ApiProvider } from '@backstage/core-app-api'; -import { CatalogApi } from '@backstage/catalog-client'; -import { - EntityProvider, - catalogApiRef, - starredEntitiesApiRef, - MockStarredEntitiesApi, -} from '@backstage/plugin-catalog-react'; -import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { permissionApiRef } from '@backstage/plugin-permission-react'; -import { - renderInTestApp, - TestApiRegistry, - MockPermissionApi, -} from '@backstage/test-utils'; -import { ScmIntegrationRegistry } from '@backstage/integration'; -import { ANNOTATION_ADR_LOCATION } from '@backstage/plugin-adr-common'; -import { - octokitAdrFileFetcher, - urlReaderAdrFileFetcher, -} from '../../hooks/adrFileFetcher'; -import { EntityAdrContent } from './EntityAdrContent'; -import { rootRouteRef } from '../../routes'; - -const mockApis = TestApiRegistry.from( - [catalogApiRef, {} as CatalogApi], - [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], - [ - scmIntegrationsApiRef, - { - resolveUrl: options => `${options.url}`, - } as ScmIntegrationRegistry, - ], -); - -const mockEntity: Entity = { - kind: 'TestEntity', - metadata: { - name: 'Testing Entity 1', - annotations: { - [ANNOTATION_ADR_LOCATION]: 'testAdrFolder', - [ANNOTATION_SOURCE_LOCATION]: 'source:location', - }, - }, - apiVersion: '', -}; - -afterEach(() => { - jest.resetAllMocks(); -}); - -describe('EntityAdrContent', () => { - it('Falls back to octokitAdrFileFetcher when adrFileFetcher is not specified', async () => { - const getAdrFilesSpyInstance = jest - .spyOn(octokitAdrFileFetcher, 'useGetAdrFilesAtUrl') - .mockImplementation(() => { - return { - data: [], - }; - }); - - await renderInTestApp( - - - - - - - - - , - { - mountedRoutes: { - '/adr': rootRouteRef, - }, - }, - ); - - expect(getAdrFilesSpyInstance).toHaveBeenCalled(); - }); - - it('Uses an alternative AdrFileFetcher when provided', async () => { - const octokitGetAdrFilesSpyInstance = jest - .spyOn(octokitAdrFileFetcher, 'useGetAdrFilesAtUrl') - .mockImplementation(() => { - return { - data: [], - }; - }); - - const urlReadersGetAdrFilesSpyInstance = jest - .spyOn(urlReaderAdrFileFetcher, 'useGetAdrFilesAtUrl') - .mockImplementation(() => { - return { - data: [], - }; - }); - - await renderInTestApp( - - - - - - - - - , - { - mountedRoutes: { - '/adr': rootRouteRef, - }, - }, - ); - - expect(octokitGetAdrFilesSpyInstance).not.toHaveBeenCalled(); - expect(urlReadersGetAdrFilesSpyInstance).toHaveBeenCalled(); - }); -}); diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index 1a75856ab1..a698cffd26 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -46,10 +46,8 @@ import { import { rootRouteRef } from '../../routes'; import { AdrContentDecorator, AdrReader } from '../AdrReader'; -import { - AdrFileFetcher, - octokitAdrFileFetcher, -} from '../../hooks/adrFileFetcher'; +import { adrApiRef } from '../../api'; +import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles((theme: Theme) => ({ adrMenu: { @@ -64,20 +62,21 @@ const useStyles = makeStyles((theme: Theme) => ({ export const EntityAdrContent = (props: { contentDecorators?: AdrContentDecorator[]; filePathFilterFn?: AdrFilePathFilterFn; - adrFileFetcher?: AdrFileFetcher; }) => { - const { contentDecorators, filePathFilterFn, adrFileFetcher } = props; + const { contentDecorators, filePathFilterFn } = props; const classes = useStyles(); const { entity } = useEntity(); const rootLink = useRouteRef(rootRouteRef); const [adrList, setAdrList] = useState([]); const [searchParams, setSearchParams] = useSearchParams(); const scmIntegrations = useApi(scmIntegrationsApiRef); + const adrApi = useApi(adrApiRef); const entityHasAdrs = isAdrAvailable(entity); - const targetAdrFileFetcher = adrFileFetcher ?? octokitAdrFileFetcher; - const { value, loading, error } = targetAdrFileFetcher.useGetAdrFilesAtUrl( - getAdrLocationUrl(entity, scmIntegrations), + const url = getAdrLocationUrl(entity, scmIntegrations); + const { value, loading, error } = useAsync( + async () => adrApi.listAdrs(url), + [url], ); const selectedAdr = @@ -147,11 +146,7 @@ export const EntityAdrContent = (props: { - + ) : ( diff --git a/plugins/adr/src/hooks/adrFileFetcher.ts b/plugins/adr/src/hooks/adrFileFetcher.ts deleted file mode 100644 index b2c198336b..0000000000 --- a/plugins/adr/src/hooks/adrFileFetcher.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { - discoveryApiRef, - useApi, - DiscoveryApi, -} from '@backstage/core-plugin-api'; -import useAsync from 'react-use/lib/useAsync'; -import { useOctokitRequest } from './useOctokitRequest'; - -const useAdrApi = ( - endpoint: string, - fileUrl: string, - discoveryApi: DiscoveryApi, -) => { - return async () => { - const baseUrl = await discoveryApi.getBaseUrl('adr'); - const targetUrl = `${baseUrl}/${endpoint}?url=${encodeURIComponent( - fileUrl, - )}`; - - const result = await fetch(targetUrl); - const data = await result.json(); - - if (!result.ok) { - throw data; - } - return data; - }; -}; - -/** - * Represents something that is capable of fetching a listing of adr files at a provided url - * and fetching the contents of an adr file at a provided url. - * - * @public - */ -export interface AdrFileFetcher { - /** - * A hook to get a listing of adr files that exist at the provided url - * - * @param url - The url to get files from - */ - useGetAdrFilesAtUrl: (url: string) => any; - - /** - * A hook to get the contents of the adr file at the provided url - * - * @param url - The url of the adr file - */ - useReadAdrFileAtUrl: (url: string) => any; -} - -const getAdrFilesEndpoint = 'list'; -const readAdrFileEndpoint = 'file'; - -/** - * An AdrFileFetcher that uses UrlReaders to fetch adr files - * - * @public - */ -export const urlReaderAdrFileFetcher: AdrFileFetcher = { - useGetAdrFilesAtUrl(url: string) { - const discoveryApi = useApi(discoveryApiRef); - return useAsync(useAdrApi(getAdrFilesEndpoint, url, discoveryApi), [ - url, - ]); - }, - useReadAdrFileAtUrl(url: string) { - const discoveryApi = useApi(discoveryApiRef); - return useAsync(useAdrApi(readAdrFileEndpoint, url, discoveryApi), [ - url, - ]); - }, -}; - -/** - * An AdrFileFetcher that uses the useOctokitRequest hook for fetching adr files - * - * @public - */ -export const octokitAdrFileFetcher: AdrFileFetcher = { - useGetAdrFilesAtUrl: (url: string) => useOctokitRequest(url), - useReadAdrFileAtUrl: (url: string) => useOctokitRequest(url), -}; diff --git a/plugins/adr/src/hooks/useOctokitRequest.ts b/plugins/adr/src/hooks/useOctokitRequest.ts deleted file mode 100644 index f160651169..0000000000 --- a/plugins/adr/src/hooks/useOctokitRequest.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 parseGitUrl from 'git-url-parse'; -import useAsync from 'react-use/lib/useAsync'; -import { Octokit } from 'octokit'; -import { useApi } from '@backstage/core-plugin-api'; -import { - scmAuthApiRef, - scmIntegrationsApiRef, -} from '@backstage/integration-react'; - -/** - * Hook for triggering authenticated Octokit requests against the GitHub Content API - * @public - */ -export const useOctokitRequest = (request: string): any => { - const authApi = useApi(scmAuthApiRef); - const scmIntegrations = useApi(scmIntegrationsApiRef); - - const { owner, name, ref, filepath } = parseGitUrl(request); - const path = filepath.replace(/^\//, ''); - const baseUrl = scmIntegrations.github.byUrl(request)?.config.apiBaseUrl; - - return useAsync(async () => { - const { token } = await authApi.getCredentials({ - url: request, - additionalScope: { - customScopes: { - github: ['repo'], - }, - }, - }); - const octokit = new Octokit({ - auth: token, - baseUrl, - }); - - return octokit.request( - `GET /repos/${owner}/${name}/contents/${path}?ref=${ref}`, - { - headers: { Accept: 'application/vnd.github.v3.raw' }, - }, - ); - }, [request]); -}; diff --git a/plugins/adr/src/index.ts b/plugins/adr/src/index.ts index 1b8406330f..54cf26d85e 100644 --- a/plugins/adr/src/index.ts +++ b/plugins/adr/src/index.ts @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** * ADR frontend plugin * * @packageDocumentation */ + +export { adrApiRef, AdrClient } from './api'; export { isAdrAvailable } from '@backstage/plugin-adr-common'; export * from './components/AdrReader'; export { adrPlugin, EntityAdrContent } from './plugin'; export * from './search'; -export * from './hooks/adrFileFetcher'; diff --git a/plugins/adr/src/plugin.ts b/plugins/adr/src/plugin.ts index 03437b1f0b..f23df826cd 100644 --- a/plugins/adr/src/plugin.ts +++ b/plugins/adr/src/plugin.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { adrApiRef, AdrClient } from './api'; import { + createApiFactory, createPlugin, createRoutableExtension, + discoveryApiRef, } from '@backstage/core-plugin-api'; - import { rootRouteRef } from './routes'; /** @@ -27,6 +29,17 @@ import { rootRouteRef } from './routes'; */ export const adrPlugin = createPlugin({ id: 'adr', + apis: [ + createApiFactory({ + api: adrApiRef, + deps: { + discoveryApi: discoveryApiRef, + }, + factory({ discoveryApi }) { + return new AdrClient({ discoveryApi }); + }, + }), + ], routes: { root: rootRouteRef, },