From fc1d240190490a48a8cc44c69078701e09263564 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Mon, 19 Dec 2022 08:20:20 -0500 Subject: [PATCH 01/23] Add UrlReader support for adr plugin The adr-backend plugin has been expanded with api endpoints that use url readers to read adr docs. This allows the adr frontend plugin to work with sites other than github, such as Azure DevOps. Created the concept of an AdrFileFetcher, which is used for retrieving adr file listings and content. EntityAdrContent and AdrReader have an optional prop to specify an override of the AdrFileFetcher to be used. By default, it uses the octokit fetcher for the octokit service. Signed-off-by: Robert Bunning --- packages/backend/package.json | 1 + packages/backend/src/index.ts | 5 +- packages/backend/src/plugins/adr.ts | 25 +++++++ plugins/adr-backend/package.json | 2 + plugins/adr-backend/src/index.ts | 1 + plugins/adr-backend/src/service/index.ts | 17 +++++ plugins/adr-backend/src/service/router.ts | 61 ++++++++++++++++ plugins/adr/package.json | 1 + .../src/components/AdrReader/AdrReader.tsx | 11 ++- .../EntityAdrContent/EntityAdrContent.tsx | 17 +++-- plugins/adr/src/hooks/adrFileFetcher.ts | 69 +++++++++++++++++++ yarn.lock | 10 ++- 12 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 packages/backend/src/plugins/adr.ts create mode 100644 plugins/adr-backend/src/service/index.ts create mode 100644 plugins/adr-backend/src/service/router.ts create mode 100644 plugins/adr/src/hooks/adrFileFetcher.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 781eff31a3..47c188e7cc 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -32,6 +32,7 @@ "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-adr-backend": "workspace:^", "@backstage/plugin-app-backend": "workspace:^", "@backstage/plugin-auth-backend": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 371ce40358..a721c443d6 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -61,6 +61,7 @@ import badges from './plugins/badges'; import jenkins from './plugins/jenkins'; import permission from './plugins/permission'; import playlist from './plugins/playlist'; +import adr from './plugins/adr'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; @@ -139,6 +140,7 @@ async function main() { const appEnv = useHotMemoize(module, () => createEnv('app')); const badgesEnv = useHotMemoize(module, () => createEnv('badges')); const jenkinsEnv = useHotMemoize(module, () => createEnv('jenkins')); + const adrEnv = useHotMemoize(module, () => createEnv('adr')); const techInsightsEnv = useHotMemoize(module, () => createEnv('tech-insights'), ); @@ -175,6 +177,7 @@ async function main() { apiRouter.use('/permission', await permission(permissionEnv)); apiRouter.use('/playlist', await playlist(playlistEnv)); apiRouter.use('/explore', await explore(exploreEnv)); + apiRouter.use('/adr', await adr(adrEnv)); apiRouter.use(notFoundHandler()); const service = createServiceBuilder(module) diff --git a/packages/backend/src/plugins/adr.ts b/packages/backend/src/plugins/adr.ts new file mode 100644 index 0000000000..dceb4d0c69 --- /dev/null +++ b/packages/backend/src/plugins/adr.ts @@ -0,0 +1,25 @@ +/* + * 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 { createRouter } from '@backstage/plugin-adr-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter(env.reader); +} diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index 66886415f3..84ec7ec5d1 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -36,6 +36,8 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-adr-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", + "express": "^4.18.2", + "express-promise-router": "^4.1.1", "luxon": "^3.0.0", "marked": "^4.0.14", "node-fetch": "^2.6.5", diff --git a/plugins/adr-backend/src/index.ts b/plugins/adr-backend/src/index.ts index 86e07943df..5cc8f50b72 100644 --- a/plugins/adr-backend/src/index.ts +++ b/plugins/adr-backend/src/index.ts @@ -21,3 +21,4 @@ */ export * from './search'; +export * from './service'; diff --git a/plugins/adr-backend/src/service/index.ts b/plugins/adr-backend/src/service/index.ts new file mode 100644 index 0000000000..434446cf3f --- /dev/null +++ b/plugins/adr-backend/src/service/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { createRouter } from './router'; diff --git a/plugins/adr-backend/src/service/router.ts b/plugins/adr-backend/src/service/router.ts new file mode 100644 index 0000000000..c70f7763e7 --- /dev/null +++ b/plugins/adr-backend/src/service/router.ts @@ -0,0 +1,61 @@ +/* + * 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 { UrlReader } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; + +export async function createRouter(reader: UrlReader): Promise { + const router = Router(); + router.use(express.json()); + + router.get('/getAdrFilesAtUrl', async (req, res) => { + const urlToProcess = req.query.url as string; + if (!urlToProcess) { + res.statusCode = 400; + res.json({ message: 'No URL provided' }); + return; + } + + const treeGetResponse = await reader.readTree(urlToProcess); + const files = await treeGetResponse.files(); + const fileData = files.map(file => { + return { + type: 'file', + name: file.path.substring(file.path.lastIndexOf('/') + 1), + path: file.path, + }; + }); + + res.json({ data: fileData }); + }); + + router.get('/readAdrFileAtUrl', async (req, res) => { + const urlToProcess = req.query.url as string; + if (!urlToProcess) { + res.statusCode = 400; + res.json({ message: 'No URL provided' }); + return; + } + + const fileGetResponse = await reader.readUrl(urlToProcess); + const fileBuffer = await fileGetResponse.buffer(); + + res.json({ data: fileBuffer.toString() }); + }); + + return router; +} diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 21e7ddd3ac..b28fdeb99c 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -28,6 +28,7 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-adr-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", "@backstage/theme": "workspace:^", diff --git a/plugins/adr/src/components/AdrReader/AdrReader.tsx b/plugins/adr/src/components/AdrReader/AdrReader.tsx index 971dae0949..6f76295e8c 100644 --- a/plugins/adr/src/components/AdrReader/AdrReader.tsx +++ b/plugins/adr/src/components/AdrReader/AdrReader.tsx @@ -26,9 +26,12 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { getAdrLocationUrl } from '@backstage/plugin-adr-common'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { useOctokitRequest } from '../../hooks'; import { adrDecoratorFactories } from './decorators'; import { AdrContentDecorator } from './types'; +import { + AdrFileFetcher, + octokitAdrFileFetcher, +} from '../../hooks/adrFileFetcher'; /** * Component to fetch and render an ADR. @@ -38,13 +41,15 @@ import { AdrContentDecorator } from './types'; export const AdrReader = (props: { adr: string; decorators?: AdrContentDecorator[]; + adrFileFetcher?: AdrFileFetcher; }) => { - const { adr, decorators } = props; + const { adr, decorators, adrFileFetcher } = props; const { entity } = useEntity(); const scmIntegrations = useApi(scmIntegrationsApiRef); const adrLocationUrl = getAdrLocationUrl(entity, scmIntegrations); - const { value, loading, error } = useOctokitRequest( + const targetAdrFileFetcher = adrFileFetcher ?? octokitAdrFileFetcher; + const { value, loading, error } = targetAdrFileFetcher.useReadAdrFileAtUrl( `${adrLocationUrl.replace(/\/$/, '')}/${adr}`, ); diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index 37344cecca..1a75856ab1 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -44,9 +44,12 @@ import { Typography, } from '@material-ui/core'; -import { useOctokitRequest } from '../../hooks'; import { rootRouteRef } from '../../routes'; import { AdrContentDecorator, AdrReader } from '../AdrReader'; +import { + AdrFileFetcher, + octokitAdrFileFetcher, +} from '../../hooks/adrFileFetcher'; const useStyles = makeStyles((theme: Theme) => ({ adrMenu: { @@ -61,8 +64,9 @@ const useStyles = makeStyles((theme: Theme) => ({ export const EntityAdrContent = (props: { contentDecorators?: AdrContentDecorator[]; filePathFilterFn?: AdrFilePathFilterFn; + adrFileFetcher?: AdrFileFetcher; }) => { - const { contentDecorators, filePathFilterFn } = props; + const { contentDecorators, filePathFilterFn, adrFileFetcher } = props; const classes = useStyles(); const { entity } = useEntity(); const rootLink = useRouteRef(rootRouteRef); @@ -71,7 +75,8 @@ export const EntityAdrContent = (props: { const scmIntegrations = useApi(scmIntegrationsApiRef); const entityHasAdrs = isAdrAvailable(entity); - const { value, loading, error } = useOctokitRequest( + const targetAdrFileFetcher = adrFileFetcher ?? octokitAdrFileFetcher; + const { value, loading, error } = targetAdrFileFetcher.useGetAdrFilesAtUrl( getAdrLocationUrl(entity, scmIntegrations), ); @@ -142,7 +147,11 @@ export const EntityAdrContent = (props: { - + ) : ( diff --git a/plugins/adr/src/hooks/adrFileFetcher.ts b/plugins/adr/src/hooks/adrFileFetcher.ts new file mode 100644 index 0000000000..2ddbe00434 --- /dev/null +++ b/plugins/adr/src/hooks/adrFileFetcher.ts @@ -0,0 +1,69 @@ +/* + * 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 } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/plugin-permission-common'; +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; + }; +}; + +export interface AdrFileFetcher { + useGetAdrFilesAtUrl: (url: string) => any; + useReadAdrFileAtUrl: (url: string) => any; +} + +const getAdrFilesEndpoint = 'getAdrFilesAtUrl'; +const readAdrFileEndpoint = 'readAdrFileAtUrl'; + +export const urlReaderAdrFileFetcher: AdrFileFetcher = { + useGetAdrFilesAtUrl: function (url: string) { + const discoveryApi = useApi(discoveryApiRef); + return useAsync(useAdrApi(getAdrFilesEndpoint, url, discoveryApi), [ + url, + ]); + }, + useReadAdrFileAtUrl: function (url: string) { + const discoveryApi = useApi(discoveryApiRef); + return useAsync(useAdrApi(readAdrFileEndpoint, url, discoveryApi), [ + url, + ]); + }, +}; + +export const octokitAdrFileFetcher: AdrFileFetcher = { + useGetAdrFilesAtUrl: (url: string) => useOctokitRequest(url), + useReadAdrFileAtUrl: (url: string) => useOctokitRequest(url), +}; diff --git a/yarn.lock b/yarn.lock index c9d87f09b8..3f38b22fc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4247,7 +4247,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-adr-backend@workspace:plugins/adr-backend": +"@backstage/plugin-adr-backend@workspace:^, @backstage/plugin-adr-backend@workspace:plugins/adr-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-adr-backend@workspace:plugins/adr-backend" dependencies: @@ -4262,6 +4262,8 @@ __metadata: "@backstage/plugin-search-common": "workspace:^" "@types/marked": ^4.0.0 "@types/supertest": ^2.0.8 + express: ^4.18.2 + express-promise-router: ^4.1.1 luxon: ^3.0.0 marked: ^4.0.14 msw: ^0.49.0 @@ -4296,6 +4298,7 @@ __metadata: "@backstage/integration-react": "workspace:^" "@backstage/plugin-adr-common": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -22303,6 +22306,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/plugin-adr-backend": "workspace:^" "@backstage/plugin-app-backend": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" @@ -22501,7 +22505,7 @@ __metadata: languageName: node linkType: hard -"express-promise-router@npm:^4.1.0": +"express-promise-router@npm:^4.1.0, express-promise-router@npm:^4.1.1": version: 4.1.1 resolution: "express-promise-router@npm:4.1.1" dependencies: @@ -22543,7 +22547,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1": +"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.1, express@npm:^4.18.2": version: 4.18.2 resolution: "express@npm:4.18.2" dependencies: From 99fea85ddf6c9287bad2f4b0d8767ab1c5e76156 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Mon, 19 Dec 2022 11:24:03 -0500 Subject: [PATCH 02/23] Updated documentation Added comments to exports. Updated READMEs for the adr-backend and adr plugins. Signed-off-by: Robert Bunning --- plugins/adr-backend/README.md | 2 ++ plugins/adr-backend/src/service/router.ts | 1 + plugins/adr/README.md | 25 ++++++++++++++++++++- plugins/adr/src/hooks/adrFileFetcher.ts | 27 +++++++++++++++++++++++ plugins/adr/src/index.ts | 1 + 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/plugins/adr-backend/README.md b/plugins/adr-backend/README.md index 79b5fe13ed..ab81c34c82 100644 --- a/plugins/adr-backend/README.md +++ b/plugins/adr-backend/README.md @@ -4,6 +4,8 @@ This ADR backend plugin is primarily responsible for the following: - Provides a `DefaultAdrCollatorFactory`, which can be used in the search backend to index ADR documents associated with entities to your Backstage Search. +- Provides endpoints that use UrlReaders for getting a ADR documents (used in the [ADR frontend plugin](../adr/README.md)). + ## Indexing ADR documents for search Before you are able to start indexing ADR documents to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). diff --git a/plugins/adr-backend/src/service/router.ts b/plugins/adr-backend/src/service/router.ts index c70f7763e7..1b896e2805 100644 --- a/plugins/adr-backend/src/service/router.ts +++ b/plugins/adr-backend/src/service/router.ts @@ -18,6 +18,7 @@ import { UrlReader } from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; +/** @public */ export async function createRouter(reader: UrlReader): Promise { const router = Router(); router.use(express.json()); diff --git a/plugins/adr/README.md b/plugins/adr/README.md index 2888ff2f07..2d3c2be9a4 100644 --- a/plugins/adr/README.md +++ b/plugins/adr/README.md @@ -4,7 +4,7 @@ Welcome to the ADR plugin! This plugin allows you to browse ADRs associated with your entities as well as a way to discover ADRs across others entities via Backstage Search. Use this to learn from the past experience of other projects to guide your own architecture decisions. -NOTE: This plugin currently only supports entities/ADRs registered via GitHub integration. +NOTE: By default, this plugin only supports entities/ADRs registered via GitHub integration. To get ADRs from other sites, see the [Using ADR plugin with sites other than GitHub](#using-adr-plugin-with-sites-other-than-github) section. ## Setup @@ -77,6 +77,29 @@ case 'adr': ); ``` +## Using ADR plugin with sites other than GitHub + +By default, the ADR plugin will only be able to retrieve ADRs through GitHub. If you would like to use it with other sites (for instance, AzureDevops): + +1. Make sure the [ADR backend plugin](../adr-backend/README.md) is installed. +2. [Configure an integration](https://backstage.io/docs/integrations/) for the site you would like to pull ADRs from. +3. Set the adrFileFetcher property on EntityAdrContent to urlReadersAdrFileFetcher: + +```jsx +// In packages/app/src/components/catalog/EntityPage.tsx +import { EntityAdrContent, isAdrAvailable, urlReaderAdrFileFetcher } from '@backstage/plugin-adr'; + +... + +const serviceEntityPage = ( + + {/* other tabs... */} + + + + +``` + ## Custom ADR formats By default, this plugin will parse ADRs according to the format specified by the [Markdown Architecture Decision Record (MADR)](https://adr.github.io/madr/) template. If your ADRs are written using a different format, you can apply the following customizations to correctly identify and parse your documents: diff --git a/plugins/adr/src/hooks/adrFileFetcher.ts b/plugins/adr/src/hooks/adrFileFetcher.ts index 2ddbe00434..c0402df628 100644 --- a/plugins/adr/src/hooks/adrFileFetcher.ts +++ b/plugins/adr/src/hooks/adrFileFetcher.ts @@ -40,14 +40,36 @@ const useAdrApi = ( }; }; +/** + * 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 = 'getAdrFilesAtUrl'; const readAdrFileEndpoint = 'readAdrFileAtUrl'; +/** + * An AdrFileFetcher that uses UrlReaders to fetch adr files + * + * @public + */ export const urlReaderAdrFileFetcher: AdrFileFetcher = { useGetAdrFilesAtUrl: function (url: string) { const discoveryApi = useApi(discoveryApiRef); @@ -63,6 +85,11 @@ export const urlReaderAdrFileFetcher: AdrFileFetcher = { }, }; +/** + * 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/index.ts b/plugins/adr/src/index.ts index 84012ccb7a..1b8406330f 100644 --- a/plugins/adr/src/index.ts +++ b/plugins/adr/src/index.ts @@ -22,3 +22,4 @@ export { isAdrAvailable } from '@backstage/plugin-adr-common'; export * from './components/AdrReader'; export { adrPlugin, EntityAdrContent } from './plugin'; export * from './search'; +export * from './hooks/adrFileFetcher'; From e4469d0ec1dcf75b52533e92bf79eda0e7ef79db Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Mon, 19 Dec 2022 11:54:31 -0500 Subject: [PATCH 03/23] Generated changeset Signed-off-by: Robert Bunning --- .changeset/dull-taxis-carry.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/dull-taxis-carry.md diff --git a/.changeset/dull-taxis-carry.md b/.changeset/dull-taxis-carry.md new file mode 100644 index 0000000000..60800c40eb --- /dev/null +++ b/.changeset/dull-taxis-carry.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-adr': patch +'@backstage/plugin-adr-backend': patch +--- + +The adr plugin can now work with sites other than GitHub. Expanded the adr backend plugin to provide endpoints to facilitate this and changed EntityAdrContent and AdrReader to take an optional property, adrFileFetcher, to allow switching between the new implementation and the octokit one. By default, the octokit version is used. From fd19425ebe470e7ac270b72e2e07163946ffe792 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Tue, 20 Dec 2022 14:58:57 -0500 Subject: [PATCH 04/23] Added tests Signed-off-by: Robert Bunning --- .../adr-backend/src/service/router.test.ts | 206 ++++++++++++++++++ plugins/adr/package.json | 4 + .../components/AdrReader/AdrReader.test.tsx | 129 +++++++++++ .../EntityAdrContent.test.tsx | 139 ++++++++++++ yarn.lock | 4 + 5 files changed, 482 insertions(+) create mode 100644 plugins/adr-backend/src/service/router.test.ts create mode 100644 plugins/adr/src/components/AdrReader/AdrReader.test.tsx create mode 100644 plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts new file mode 100644 index 0000000000..7c42e6eba7 --- /dev/null +++ b/plugins/adr-backend/src/service/router.test.ts @@ -0,0 +1,206 @@ +/* + * 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 { + ReadTreeResponse, + ReadTreeResponseFile, + ReadUrlResponse, + SearchResponse, + UrlReader, +} from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { createRouter } from './router'; + +const makeBufferFromString = (string: string) => async () => + Buffer.from(string); + +const testingUrlFakeFileTree: ReadTreeResponseFile[] = [ + { + path: 'folder/testFile001.txt', + content: makeBufferFromString('folder/testFile001.txt content'), + }, + { + path: 'testFile001.txt', + content: makeBufferFromString('testFile002.txt content'), + }, + { + path: 'testFile002.txt', + content: makeBufferFromString('testFile001.txt content'), + }, +]; + +const makeFileContent = async (fileContent: string) => { + const result: ReadUrlResponse = { + buffer: makeBufferFromString(fileContent), + }; + return result; +}; + +const testFileOneContent = 'testFileOne content'; +const testFileTwoContent = 'testFileTwo content'; +const genericFileContent = 'file content'; + +const mockUrlReader: UrlReader = { + read: function (): Promise { + throw new Error('read not implemented.'); + }, + readUrl: function (url: string): Promise { + switch (url) { + case 'testFileOne': + return makeFileContent(testFileOneContent); + case 'testFileTwo': + return makeFileContent(testFileTwoContent); + default: + return makeFileContent(genericFileContent); + } + }, + readTree: function (): Promise { + const result: ReadTreeResponse = { + files: async () => testingUrlFakeFileTree, + archive: function (): Promise { + throw new Error('Function not implemented.'); + }, + dir: function (): Promise { + throw new Error('Function not implemented.'); + }, + etag: '', + }; + + const resultPromise = async () => result; + return resultPromise(); + }, + search: function (): Promise { + throw new Error('search not implemented.'); + }, +}; + +describe('createRouter', () => { + let app: express.Express; + + beforeEach(async () => { + jest.resetAllMocks(); + + const router = await createRouter(mockUrlReader); + app = express().use(router); + }); + + describe('GET /getAdrFilesAtUrl', () => { + it('returns bad request (400) when no url is provided', async () => { + const urlNotSpecifiedRequest = await request(app).get( + '/getAdrFilesAtUrl', + ); + const urlNotSpecifiedStatus = urlNotSpecifiedRequest.status; + const urlNotSpecifiedMessage = urlNotSpecifiedRequest.body.message; + + const urlNotFilledRequest = await request(app).get( + '/getAdrFilesAtUrl?url=', + ); + const urlNotFilledStatus = urlNotFilledRequest.status; + const urlNotFilledMessage = urlNotFilledRequest.body.message; + + const expectedStatusCode = 400; + const expectedErrorMessage = 'No URL provided'; + + expect(urlNotSpecifiedStatus).toBe(expectedStatusCode); + expect(urlNotSpecifiedMessage).toBe(expectedErrorMessage); + + expect(urlNotFilledStatus).toBe(expectedStatusCode); + expect(urlNotFilledMessage).toBe(expectedErrorMessage); + }); + + it('returns the correct listing when reading a url', async () => { + const result = await request(app).get('/getAdrFilesAtUrl?url=testing'); + const { status, body, error } = result; + + const expectedStatusCode = 200; + const expectedBody = { + data: [ + { + type: 'file', + name: 'testFile001.txt', + path: 'folder/testFile001.txt', + }, + { + type: 'file', + name: 'testFile001.txt', + path: 'testFile001.txt', + }, + { + type: 'file', + name: 'testFile002.txt', + path: 'testFile002.txt', + }, + ], + }; + + expect(error).toBeFalsy(); + expect(status).toBe(expectedStatusCode); + expect(body).toEqual(expectedBody); + }); + }); + + describe('GET /readAdrFileAtUrl', () => { + it('returns bad request (400) when no url is provided', async () => { + const urlNotSpecifiedRequest = await request(app).get( + '/readAdrFileAtUrl', + ); + const urlNotSpecifiedStatus = urlNotSpecifiedRequest.status; + const urlNotSpecifiedMessage = urlNotSpecifiedRequest.body.message; + + const urlNotFilledRequest = await request(app).get( + '/readAdrFileAtUrl?url=', + ); + const urlNotFilledStatus = urlNotFilledRequest.status; + const urlNotFilledMessage = urlNotFilledRequest.body.message; + + const expectedStatusCode = 400; + const expectedErrorMessage = 'No URL provided'; + + expect(urlNotSpecifiedStatus).toBe(expectedStatusCode); + expect(urlNotSpecifiedMessage).toBe(expectedErrorMessage); + + expect(urlNotFilledStatus).toBe(expectedStatusCode); + expect(urlNotFilledMessage).toBe(expectedErrorMessage); + }); + + it('returns the correct file contents when reading a url', async () => { + const fileOneResponse = await request(app).get( + '/readAdrFileAtUrl?url=testFileOne', + ); + const fileOneStatus = fileOneResponse.status; + const fileOneBody = fileOneResponse.body; + const fileOneError = fileOneResponse.error; + + const fileTwoResponse = await request(app).get( + '/readAdrFileAtUrl?url=testFileTwo', + ); + const fileTwoStatus = fileTwoResponse.status; + const fileTwoBody = fileTwoResponse.body; + const fileTwoError = fileTwoResponse.error; + + const expectedStatusCode = 200; + + expect(fileOneError).toBeFalsy(); + expect(fileOneStatus).toBe(expectedStatusCode); + expect(fileOneBody.data).toBe(testFileOneContent); + + expect(fileTwoError).toBeFalsy(); + expect(fileTwoStatus).toBe(expectedStatusCode); + expect(fileTwoBody.data).toBe(testFileTwoContent); + }); + }); +}); diff --git a/plugins/adr/package.json b/plugins/adr/package.json index b28fdeb99c..dda581c5cd 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -46,9 +46,13 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { + "@backstage/catalog-client": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/integration": "workspace:^", + "@backstage/plugin-catalog": "workspace:^", + "@backstage/plugin-permission-react": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", diff --git a/plugins/adr/src/components/AdrReader/AdrReader.test.tsx b/plugins/adr/src/components/AdrReader/AdrReader.test.tsx new file mode 100644 index 0000000000..1b1cdcf851 --- /dev/null +++ b/plugins/adr/src/components/AdrReader/AdrReader.test.tsx @@ -0,0 +1,129 @@ +/* + * 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/EntityAdrContent/EntityAdrContent.test.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx new file mode 100644 index 0000000000..ca6397e736 --- /dev/null +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.test.tsx @@ -0,0 +1,139 @@ +/* + * 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/yarn.lock b/yarn.lock index 3f38b22fc1..eb0e5f3476 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4289,16 +4289,20 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-adr@workspace:plugins/adr" dependencies: + "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/integration": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-adr-common": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^" From f50f2cf89147e64e5643d60d5a1b8b7da485a7e6 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Tue, 20 Dec 2022 15:13:23 -0500 Subject: [PATCH 05/23] Fixed spelling mistake in adr's readme Signed-off-by: Robert Bunning --- plugins/adr/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/adr/README.md b/plugins/adr/README.md index 2d3c2be9a4..2478622560 100644 --- a/plugins/adr/README.md +++ b/plugins/adr/README.md @@ -83,7 +83,7 @@ By default, the ADR plugin will only be able to retrieve ADRs through GitHub. If 1. Make sure the [ADR backend plugin](../adr-backend/README.md) is installed. 2. [Configure an integration](https://backstage.io/docs/integrations/) for the site you would like to pull ADRs from. -3. Set the adrFileFetcher property on EntityAdrContent to urlReadersAdrFileFetcher: +3. Set the adrFileFetcher property on EntityAdrContent to urlReaderAdrFileFetcher: ```jsx // In packages/app/src/components/catalog/EntityPage.tsx From 502aea62e1da13362cdb27cee748a93db6eafc59 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Wed, 21 Dec 2022 10:49:26 -0500 Subject: [PATCH 06/23] Generated api reports Signed-off-by: Robert Bunning --- plugins/adr-backend/api-report.md | 4 ++++ plugins/adr/api-report.md | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/plugins/adr-backend/api-report.md b/plugins/adr-backend/api-report.md index 5a671e9c8f..24e54e8eb5 100644 --- a/plugins/adr-backend/api-report.md +++ b/plugins/adr-backend/api-report.md @@ -11,6 +11,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { Entity } from '@backstage/catalog-model'; +import express from 'express'; import { Logger } from 'winston'; import { PluginCacheManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -44,6 +45,9 @@ export type AdrParserContext = { // @public export const createMadrParser: (options?: MadrParserOptions) => AdrParser; +// @public (undocumented) +export function createRouter(reader: UrlReader): Promise; + // @public export class DefaultAdrCollatorFactory implements DocumentCollatorFactory { // (undocumented) diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md index ecec0c0dd5..99d3e537a3 100644 --- a/plugins/adr/api-report.md +++ b/plugins/adr/api-report.md @@ -20,6 +20,14 @@ export type AdrContentDecorator = (adrInfo: { content: string; }; +// @public +export interface AdrFileFetcher { + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen + useGetAdrFilesAtUrl: (url: string) => any; + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen + useReadAdrFileAtUrl: (url: string) => any; +} + // @public export const adrPlugin: BackstagePlugin< { @@ -31,7 +39,11 @@ export const adrPlugin: BackstagePlugin< // @public export const AdrReader: { - (props: { adr: string; decorators?: AdrContentDecorator[] }): JSX.Element; + (props: { + adr: string; + decorators?: AdrContentDecorator[]; + adrFileFetcher?: AdrFileFetcher; + }): JSX.Element; decorators: Readonly<{ createRewriteRelativeLinksDecorator(): AdrContentDecorator; createRewriteRelativeEmbedsDecorator(): AdrContentDecorator; @@ -50,7 +62,14 @@ export function AdrSearchResultListItem(props: { export const EntityAdrContent: (props: { contentDecorators?: AdrContentDecorator[] | undefined; filePathFilterFn?: AdrFilePathFilterFn | undefined; + adrFileFetcher?: AdrFileFetcher | undefined; }) => JSX.Element; export { isAdrAvailable }; + +// @public +export const octokitAdrFileFetcher: AdrFileFetcher; + +// @public +export const urlReaderAdrFileFetcher: AdrFileFetcher; ``` From 4b08032f84a7d9a0700396828f2392c27b71f367 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Wed, 21 Dec 2022 11:04:29 -0500 Subject: [PATCH 07/23] Addressed linter warnings Signed-off-by: Robert Bunning --- plugins/adr-backend/src/service/router.test.ts | 12 ++++++------ plugins/adr/src/hooks/adrFileFetcher.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts index 7c42e6eba7..b58543dbf0 100644 --- a/plugins/adr-backend/src/service/router.test.ts +++ b/plugins/adr-backend/src/service/router.test.ts @@ -55,10 +55,10 @@ const testFileTwoContent = 'testFileTwo content'; const genericFileContent = 'file content'; const mockUrlReader: UrlReader = { - read: function (): Promise { + read() { throw new Error('read not implemented.'); }, - readUrl: function (url: string): Promise { + readUrl(url: string) { switch (url) { case 'testFileOne': return makeFileContent(testFileOneContent); @@ -68,13 +68,13 @@ const mockUrlReader: UrlReader = { return makeFileContent(genericFileContent); } }, - readTree: function (): Promise { + readTree() { const result: ReadTreeResponse = { files: async () => testingUrlFakeFileTree, - archive: function (): Promise { + archive() { throw new Error('Function not implemented.'); }, - dir: function (): Promise { + dir() { throw new Error('Function not implemented.'); }, etag: '', @@ -83,7 +83,7 @@ const mockUrlReader: UrlReader = { const resultPromise = async () => result; return resultPromise(); }, - search: function (): Promise { + search() { throw new Error('search not implemented.'); }, }; diff --git a/plugins/adr/src/hooks/adrFileFetcher.ts b/plugins/adr/src/hooks/adrFileFetcher.ts index c0402df628..a9dec1105e 100644 --- a/plugins/adr/src/hooks/adrFileFetcher.ts +++ b/plugins/adr/src/hooks/adrFileFetcher.ts @@ -71,13 +71,13 @@ const readAdrFileEndpoint = 'readAdrFileAtUrl'; * @public */ export const urlReaderAdrFileFetcher: AdrFileFetcher = { - useGetAdrFilesAtUrl: function (url: string) { + useGetAdrFilesAtUrl(url: string) { const discoveryApi = useApi(discoveryApiRef); return useAsync(useAdrApi(getAdrFilesEndpoint, url, discoveryApi), [ url, ]); }, - useReadAdrFileAtUrl: function (url: string) { + useReadAdrFileAtUrl(url: string) { const discoveryApi = useApi(discoveryApiRef); return useAsync(useAdrApi(readAdrFileEndpoint, url, discoveryApi), [ url, From 722713a64b960343d8d05070dde7f42a7c7f3d2c Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Wed, 21 Dec 2022 11:12:59 -0500 Subject: [PATCH 08/23] Capitalized ADR in changeset Signed-off-by: Robert Bunning --- .changeset/dull-taxis-carry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dull-taxis-carry.md b/.changeset/dull-taxis-carry.md index 60800c40eb..6f2901bac2 100644 --- a/.changeset/dull-taxis-carry.md +++ b/.changeset/dull-taxis-carry.md @@ -3,4 +3,4 @@ '@backstage/plugin-adr-backend': patch --- -The adr plugin can now work with sites other than GitHub. Expanded the adr backend plugin to provide endpoints to facilitate this and changed EntityAdrContent and AdrReader to take an optional property, adrFileFetcher, to allow switching between the new implementation and the octokit one. By default, the octokit version is used. +The ADR plugin can now work with sites other than GitHub. Expanded the ADR backend plugin to provide endpoints to facilitate this and changed EntityAdrContent and AdrReader to take an optional property, adrFileFetcher, to allow switching between the new implementation and the octokit one. By default, the octokit version is used. From 2a90eb36681a16712cf8881afc90683b94befd50 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Wed, 21 Dec 2022 11:44:10 -0500 Subject: [PATCH 09/23] Remove unused SearchResponse import in test Signed-off-by: Robert Bunning --- plugins/adr-backend/src/service/router.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts index b58543dbf0..6cfccb0a40 100644 --- a/plugins/adr-backend/src/service/router.test.ts +++ b/plugins/adr-backend/src/service/router.test.ts @@ -18,7 +18,6 @@ import { ReadTreeResponse, ReadTreeResponseFile, ReadUrlResponse, - SearchResponse, UrlReader, } from '@backstage/backend-common'; import express from 'express'; From 39b54c6dd033eeffd157b32bee1e734bfb7d779d Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Wed, 21 Dec 2022 12:44:16 -0500 Subject: [PATCH 10/23] Fixed missing hyphen in param tag Signed-off-by: Robert Bunning --- plugins/adr/api-report.md | 2 -- plugins/adr/src/hooks/adrFileFetcher.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md index 99d3e537a3..2a49e89b2d 100644 --- a/plugins/adr/api-report.md +++ b/plugins/adr/api-report.md @@ -22,9 +22,7 @@ export type AdrContentDecorator = (adrInfo: { // @public export interface AdrFileFetcher { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen useGetAdrFilesAtUrl: (url: string) => any; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen useReadAdrFileAtUrl: (url: string) => any; } diff --git a/plugins/adr/src/hooks/adrFileFetcher.ts b/plugins/adr/src/hooks/adrFileFetcher.ts index a9dec1105e..fa690556e6 100644 --- a/plugins/adr/src/hooks/adrFileFetcher.ts +++ b/plugins/adr/src/hooks/adrFileFetcher.ts @@ -50,14 +50,14 @@ 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 + * @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 + * @param url - The url of the adr file */ useReadAdrFileAtUrl: (url: string) => any; } From 0b1c9e4d651ed7267849dd08c565e82fbbc26912 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Wed, 21 Dec 2022 13:28:46 -0500 Subject: [PATCH 11/23] Added express types to adr-backend plugin Signed-off-by: Robert Bunning --- plugins/adr-backend/package.json | 1 + yarn.lock | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index 90996ce7cf..3b1f19ec01 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -36,6 +36,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-adr-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", + "@types/express": "^4.17.15", "express": "^4.18.2", "express-promise-router": "^4.1.1", "luxon": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 1091eb58f7..d18b10cb4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4109,6 +4109,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-adr-common": "workspace:^" "@backstage/plugin-search-common": "workspace:^" + "@types/express": ^4.17.15 "@types/marked": ^4.0.0 "@types/supertest": ^2.0.8 express: ^4.18.2 @@ -14015,7 +14016,7 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:4.17.31, @types/express-serve-static-core@npm:^4.17.18, @types/express-serve-static-core@npm:^4.17.5": +"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:4.17.31, @types/express-serve-static-core@npm:^4.17.18, @types/express-serve-static-core@npm:^4.17.31, @types/express-serve-static-core@npm:^4.17.5": version: 4.17.31 resolution: "@types/express-serve-static-core@npm:4.17.31" dependencies: @@ -14045,7 +14046,19 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:4.17.14, @types/express@npm:^4.17.13, @types/express@npm:^4.17.6": +"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.15, @types/express@npm:^4.17.6": + version: 4.17.15 + resolution: "@types/express@npm:4.17.15" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^4.17.31 + "@types/qs": "*" + "@types/serve-static": "*" + checksum: b4acd8a836d4f6409cdf79b12d6e660485249b62500cccd61e7997d2f520093edf77d7f8498ca79d64a112c6434b6de5ca48039b8fde2c881679eced7e96979b + languageName: node + linkType: hard + +"@types/express@npm:4.17.14": version: 4.17.14 resolution: "@types/express@npm:4.17.14" dependencies: From 5b6a1920cd2da8948c4285a4ccdbacf5484a7864 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Fri, 23 Dec 2022 09:24:53 -0500 Subject: [PATCH 12/23] Changed endpoint names Signed-off-by: Robert Bunning --- .../adr-backend/src/service/router.test.ts | 25 +++++++++---------- plugins/adr-backend/src/service/router.ts | 4 +-- plugins/adr/src/hooks/adrFileFetcher.ts | 4 +-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts index 6cfccb0a40..76b0f5552d 100644 --- a/plugins/adr-backend/src/service/router.test.ts +++ b/plugins/adr-backend/src/service/router.test.ts @@ -24,6 +24,9 @@ import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; +const listEndpointName = '/list'; +const fileEndpointName = '/file'; + const makeBufferFromString = (string: string) => async () => Buffer.from(string); @@ -97,16 +100,14 @@ describe('createRouter', () => { app = express().use(router); }); - describe('GET /getAdrFilesAtUrl', () => { + describe(`GET ${listEndpointName}`, () => { it('returns bad request (400) when no url is provided', async () => { - const urlNotSpecifiedRequest = await request(app).get( - '/getAdrFilesAtUrl', - ); + const urlNotSpecifiedRequest = await request(app).get(listEndpointName); const urlNotSpecifiedStatus = urlNotSpecifiedRequest.status; const urlNotSpecifiedMessage = urlNotSpecifiedRequest.body.message; const urlNotFilledRequest = await request(app).get( - '/getAdrFilesAtUrl?url=', + `${listEndpointName}?url=`, ); const urlNotFilledStatus = urlNotFilledRequest.status; const urlNotFilledMessage = urlNotFilledRequest.body.message; @@ -122,7 +123,7 @@ describe('createRouter', () => { }); it('returns the correct listing when reading a url', async () => { - const result = await request(app).get('/getAdrFilesAtUrl?url=testing'); + const result = await request(app).get(`${listEndpointName}?url=testing`); const { status, body, error } = result; const expectedStatusCode = 200; @@ -152,16 +153,14 @@ describe('createRouter', () => { }); }); - describe('GET /readAdrFileAtUrl', () => { + describe(`GET ${fileEndpointName}`, () => { it('returns bad request (400) when no url is provided', async () => { - const urlNotSpecifiedRequest = await request(app).get( - '/readAdrFileAtUrl', - ); + const urlNotSpecifiedRequest = await request(app).get(fileEndpointName); const urlNotSpecifiedStatus = urlNotSpecifiedRequest.status; const urlNotSpecifiedMessage = urlNotSpecifiedRequest.body.message; const urlNotFilledRequest = await request(app).get( - '/readAdrFileAtUrl?url=', + `${fileEndpointName}?url=`, ); const urlNotFilledStatus = urlNotFilledRequest.status; const urlNotFilledMessage = urlNotFilledRequest.body.message; @@ -178,14 +177,14 @@ describe('createRouter', () => { it('returns the correct file contents when reading a url', async () => { const fileOneResponse = await request(app).get( - '/readAdrFileAtUrl?url=testFileOne', + `${fileEndpointName}?url=testFileOne`, ); const fileOneStatus = fileOneResponse.status; const fileOneBody = fileOneResponse.body; const fileOneError = fileOneResponse.error; const fileTwoResponse = await request(app).get( - '/readAdrFileAtUrl?url=testFileTwo', + `${fileEndpointName}?url=testFileTwo`, ); const fileTwoStatus = fileTwoResponse.status; const fileTwoBody = fileTwoResponse.body; diff --git a/plugins/adr-backend/src/service/router.ts b/plugins/adr-backend/src/service/router.ts index 1b896e2805..26bf381596 100644 --- a/plugins/adr-backend/src/service/router.ts +++ b/plugins/adr-backend/src/service/router.ts @@ -23,7 +23,7 @@ export async function createRouter(reader: UrlReader): Promise { const router = Router(); router.use(express.json()); - router.get('/getAdrFilesAtUrl', async (req, res) => { + router.get('/list', async (req, res) => { const urlToProcess = req.query.url as string; if (!urlToProcess) { res.statusCode = 400; @@ -44,7 +44,7 @@ export async function createRouter(reader: UrlReader): Promise { res.json({ data: fileData }); }); - router.get('/readAdrFileAtUrl', async (req, res) => { + router.get('/file', async (req, res) => { const urlToProcess = req.query.url as string; if (!urlToProcess) { res.statusCode = 400; diff --git a/plugins/adr/src/hooks/adrFileFetcher.ts b/plugins/adr/src/hooks/adrFileFetcher.ts index fa690556e6..d5e7c97f04 100644 --- a/plugins/adr/src/hooks/adrFileFetcher.ts +++ b/plugins/adr/src/hooks/adrFileFetcher.ts @@ -62,8 +62,8 @@ export interface AdrFileFetcher { useReadAdrFileAtUrl: (url: string) => any; } -const getAdrFilesEndpoint = 'getAdrFilesAtUrl'; -const readAdrFileEndpoint = 'readAdrFileAtUrl'; +const getAdrFilesEndpoint = 'list'; +const readAdrFileEndpoint = 'file'; /** * An AdrFileFetcher that uses UrlReaders to fetch adr files From 30519deddc9d976883309e99aa0236e0f0b6cfa4 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Fri, 23 Dec 2022 09:40:21 -0500 Subject: [PATCH 13/23] Updated documentation Changed the wording in the changeset. Fixed a grammar mistake in the backend plugin's readme. Signed-off-by: Robert Bunning --- .changeset/dull-taxis-carry.md | 2 +- plugins/adr-backend/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/dull-taxis-carry.md b/.changeset/dull-taxis-carry.md index 6f2901bac2..3cc4bbdde3 100644 --- a/.changeset/dull-taxis-carry.md +++ b/.changeset/dull-taxis-carry.md @@ -3,4 +3,4 @@ '@backstage/plugin-adr-backend': patch --- -The ADR plugin can now work with sites other than GitHub. Expanded the ADR backend plugin to provide endpoints to facilitate this and changed EntityAdrContent and AdrReader to take an optional property, adrFileFetcher, to allow switching between the new implementation and the octokit one. By default, the octokit version is used. +The ADR plugin can now work with sites other than GitHub. Expanded the ADR backend plugin to provide endpoints to facilitate this. diff --git a/plugins/adr-backend/README.md b/plugins/adr-backend/README.md index ab81c34c82..83c87530ff 100644 --- a/plugins/adr-backend/README.md +++ b/plugins/adr-backend/README.md @@ -4,7 +4,7 @@ This ADR backend plugin is primarily responsible for the following: - Provides a `DefaultAdrCollatorFactory`, which can be used in the search backend to index ADR documents associated with entities to your Backstage Search. -- Provides endpoints that use UrlReaders for getting a ADR documents (used in the [ADR frontend plugin](../adr/README.md)). +- Provides endpoints that use UrlReaders for getting ADR documents (used in the [ADR frontend plugin](../adr/README.md)). ## Indexing ADR documents for search From b267b6c04535d8b8cc37df158ba43f88011009a9 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Fri, 23 Dec 2022 10:07:05 -0500 Subject: [PATCH 14/23] Adjusted dependencies Changed @types imports to be dev dependencies. Changed import of DiscoveryApi to remove reference to @backstage/plugin-permission-common Signed-off-by: Robert Bunning --- plugins/adr-backend/package.json | 2 +- plugins/adr/package.json | 1 - plugins/adr/src/hooks/adrFileFetcher.ts | 7 +++++-- yarn.lock | 1 - 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index 3b1f19ec01..82af1cbc0b 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -36,7 +36,6 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-adr-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", - "@types/express": "^4.17.15", "express": "^4.18.2", "express-promise-router": "^4.1.1", "luxon": "^3.0.0", @@ -47,6 +46,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@types/express": "^4.17.15", "@types/marked": "^4.0.0", "@types/supertest": "^2.0.8", "msw": "^0.49.0", diff --git a/plugins/adr/package.json b/plugins/adr/package.json index dd705e7ad2..06d1f90273 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -28,7 +28,6 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-adr-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", - "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", "@backstage/theme": "workspace:^", diff --git a/plugins/adr/src/hooks/adrFileFetcher.ts b/plugins/adr/src/hooks/adrFileFetcher.ts index d5e7c97f04..b2c198336b 100644 --- a/plugins/adr/src/hooks/adrFileFetcher.ts +++ b/plugins/adr/src/hooks/adrFileFetcher.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; -import { DiscoveryApi } from '@backstage/plugin-permission-common'; +import { + discoveryApiRef, + useApi, + DiscoveryApi, +} from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; import { useOctokitRequest } from './useOctokitRequest'; diff --git a/yarn.lock b/yarn.lock index 7cf10f6d49..4543e82276 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4153,7 +4153,6 @@ __metadata: "@backstage/plugin-adr-common": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" - "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" From 654e8ac5afdc20874f97a3559081caf677b43dad Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Fri, 23 Dec 2022 13:11:49 -0500 Subject: [PATCH 15/23] 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, }, From 42ecf54e483fda7f47ba0ca92586b8c017d421b2 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Fri, 23 Dec 2022 13:32:18 -0500 Subject: [PATCH 16/23] Export api types Signed-off-by: Robert Bunning --- plugins/adr/src/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/adr/src/index.ts b/plugins/adr/src/index.ts index 54cf26d85e..2ec76b0fd6 100644 --- a/plugins/adr/src/index.ts +++ b/plugins/adr/src/index.ts @@ -21,6 +21,13 @@ */ export { adrApiRef, AdrClient } from './api'; +export type { + AdrApi, + AdrClientOptions, + AdrFileInfo, + AdrListResult, + AdrReadResult, +} from './api'; export { isAdrAvailable } from '@backstage/plugin-adr-common'; export * from './components/AdrReader'; export { adrPlugin, EntityAdrContent } from './plugin'; From 54baecbd1a56d00de487cb11d828c8a9fd30bf5c Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Fri, 23 Dec 2022 16:26:33 -0500 Subject: [PATCH 17/23] Implement caching via response etags for adr backend Signed-off-by: Robert Bunning --- packages/backend/src/plugins/adr.ts | 6 +- .../adr-backend/src/service/router.test.ts | 30 +++++- plugins/adr-backend/src/service/router.ts | 96 ++++++++++++++++--- plugins/adr/src/api/AdrClient.ts | 2 +- 4 files changed, 116 insertions(+), 18 deletions(-) diff --git a/packages/backend/src/plugins/adr.ts b/packages/backend/src/plugins/adr.ts index dceb4d0c69..6fa0d72969 100644 --- a/packages/backend/src/plugins/adr.ts +++ b/packages/backend/src/plugins/adr.ts @@ -21,5 +21,9 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin( env: PluginEnvironment, ): Promise { - return await createRouter(env.reader); + return await createRouter({ + reader: env.reader, + cacheClient: env.cache.getClient(), + logger: env.logger, + }); } diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts index 76b0f5552d..74955fcc6a 100644 --- a/plugins/adr-backend/src/service/router.test.ts +++ b/plugins/adr-backend/src/service/router.test.ts @@ -15,6 +15,7 @@ */ import { + CacheClient, ReadTreeResponse, ReadTreeResponseFile, ReadUrlResponse, @@ -23,6 +24,7 @@ import { import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; +import { Logger } from 'winston'; const listEndpointName = '/list'; const fileEndpointName = '/file'; @@ -90,13 +92,39 @@ const mockUrlReader: UrlReader = { }, }; +class MockCacheClient implements CacheClient { + private itemRegistry: { [key: string]: any }; + + constructor() { + this.itemRegistry = {}; + } + + async get(key: string) { + return this.itemRegistry[key]; + } + + async set(key: string, value: any) { + this.itemRegistry[key] = value; + } + + async delete(key: string) { + delete this.itemRegistry[key]; + } +} + describe('createRouter', () => { let app: express.Express; beforeEach(async () => { jest.resetAllMocks(); - const router = await createRouter(mockUrlReader); + const router = await createRouter({ + reader: mockUrlReader, + cacheClient: new MockCacheClient(), + logger: { + error: (message: any) => message, + } as Logger, + }); app = express().use(router); }); diff --git a/plugins/adr-backend/src/service/router.ts b/plugins/adr-backend/src/service/router.ts index 26bf381596..9c21b03d04 100644 --- a/plugins/adr-backend/src/service/router.ts +++ b/plugins/adr-backend/src/service/router.ts @@ -14,12 +14,24 @@ * limitations under the License. */ -import { UrlReader } from '@backstage/backend-common'; +import { CacheClient, UrlReader } from '@backstage/backend-common'; +import { NotModifiedError, stringifyError } from '@backstage/errors'; +import { Logger } from 'winston'; import express from 'express'; import Router from 'express-promise-router'; +export type AdrRouterOptions = { + reader: UrlReader; + cacheClient: CacheClient; + logger: Logger; +}; + /** @public */ -export async function createRouter(reader: UrlReader): Promise { +export async function createRouter( + options: AdrRouterOptions, +): Promise { + const { reader, cacheClient, logger } = options; + const router = Router(); router.use(express.json()); @@ -31,17 +43,46 @@ export async function createRouter(reader: UrlReader): Promise { return; } - const treeGetResponse = await reader.readTree(urlToProcess); - const files = await treeGetResponse.files(); - const fileData = files.map(file => { - return { - type: 'file', - name: file.path.substring(file.path.lastIndexOf('/') + 1), - path: file.path, - }; - }); + const cachedTree = (await cacheClient.get(urlToProcess)) as { + data: { + type: string; + name: string; + path: string; + }[]; + etag: string; + }; + const cachedData = cachedTree?.data; - res.json({ data: fileData }); + try { + const treeGetResponse = await reader.readTree(urlToProcess, { + etag: cachedTree?.etag, + }); + const files = await treeGetResponse.files(); + const data = files.map(file => { + return { + type: 'file', + name: file.path.substring(file.path.lastIndexOf('/') + 1), + path: file.path, + }; + }); + + await cacheClient.set(urlToProcess, { + data, + etag: treeGetResponse.etag, + }); + + res.json({ data }); + } catch (error: any) { + if (cachedData && error.name === NotModifiedError.name) { + res.json({ data: cachedData }); + return; + } + + const message = stringifyError(error); + logger.error(`Unable to fetch ADRs from ${urlToProcess}: ${message}`); + res.statusCode = 500; + res.json({ message }); + } }); router.get('/file', async (req, res) => { @@ -52,10 +93,35 @@ export async function createRouter(reader: UrlReader): Promise { return; } - const fileGetResponse = await reader.readUrl(urlToProcess); - const fileBuffer = await fileGetResponse.buffer(); + const cachedFileContent = (await cacheClient.get(urlToProcess)) as { + data: string; + etag: string; + }; - res.json({ data: fileBuffer.toString() }); + try { + const fileGetResponse = await reader.readUrl(urlToProcess, { + etag: cachedFileContent?.etag, + }); + const fileBuffer = await fileGetResponse.buffer(); + const data = fileBuffer.toString(); + + await cacheClient.set(urlToProcess, { + data, + etag: fileGetResponse.etag, + }); + + res.json({ data }); + } catch (error) { + if (cachedFileContent && error.name === NotModifiedError.name) { + res.json({ data: cachedFileContent.data }); + return; + } + + const message = stringifyError(error); + logger.error(`Unable to fetch ADRs from ${urlToProcess}: ${message}`); + res.statusCode = 500; + res.json({ message }); + } }); return router; diff --git a/plugins/adr/src/api/AdrClient.ts b/plugins/adr/src/api/AdrClient.ts index d79a327db3..da1adc04e4 100644 --- a/plugins/adr/src/api/AdrClient.ts +++ b/plugins/adr/src/api/AdrClient.ts @@ -51,7 +51,7 @@ export class AdrClient implements AdrApi { const data = await result.json(); if (!result.ok) { - throw new Error(data.error.message); + throw new Error(`${data.message}`); } return data; } From 0d514d6e1077397ef2dbb5d5e53203e3a8fa27c2 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Tue, 27 Dec 2022 09:13:14 -0500 Subject: [PATCH 18/23] Regenerate api reports Signed-off-by: Robert Bunning --- plugins/adr-backend/api-report.md | 12 ++++- plugins/adr-backend/src/service/index.ts | 1 + plugins/adr-backend/src/service/router.ts | 1 + plugins/adr/api-report.md | 58 ++++++++++++++++------- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/plugins/adr-backend/api-report.md b/plugins/adr-backend/api-report.md index 24e54e8eb5..d5645ad3a7 100644 --- a/plugins/adr-backend/api-report.md +++ b/plugins/adr-backend/api-report.md @@ -7,6 +7,7 @@ import { AdrDocument } from '@backstage/plugin-adr-common'; import { AdrFilePathFilterFn } from '@backstage/plugin-adr-common'; +import { CacheClient } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; @@ -42,11 +43,20 @@ export type AdrParserContext = { path: string; }; +// @public (undocumented) +export type AdrRouterOptions = { + reader: UrlReader; + cacheClient: CacheClient; + logger: Logger; +}; + // @public export const createMadrParser: (options?: MadrParserOptions) => AdrParser; // @public (undocumented) -export function createRouter(reader: UrlReader): Promise; +export function createRouter( + options: AdrRouterOptions, +): Promise; // @public export class DefaultAdrCollatorFactory implements DocumentCollatorFactory { diff --git a/plugins/adr-backend/src/service/index.ts b/plugins/adr-backend/src/service/index.ts index 434446cf3f..3736acb188 100644 --- a/plugins/adr-backend/src/service/index.ts +++ b/plugins/adr-backend/src/service/index.ts @@ -15,3 +15,4 @@ */ export { createRouter } from './router'; +export type { AdrRouterOptions } from './router'; diff --git a/plugins/adr-backend/src/service/router.ts b/plugins/adr-backend/src/service/router.ts index 9c21b03d04..e6603ab346 100644 --- a/plugins/adr-backend/src/service/router.ts +++ b/plugins/adr-backend/src/service/router.ts @@ -20,6 +20,7 @@ import { Logger } from 'winston'; import express from 'express'; import Router from 'express-promise-router'; +/** @public */ export type AdrRouterOptions = { reader: UrlReader; cacheClient: CacheClient; diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md index 2a49e89b2d..d6480cd090 100644 --- a/plugins/adr/api-report.md +++ b/plugins/adr/api-report.md @@ -7,11 +7,37 @@ import { AdrDocument } from '@backstage/plugin-adr-common'; import { AdrFilePathFilterFn } from '@backstage/plugin-adr-common'; +import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; import { isAdrAvailable } from '@backstage/plugin-adr-common'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +// @public +export interface AdrApi { + listAdrs(url: string): Promise; + readAdr(url: string): Promise; +} + +// @public +export const adrApiRef: ApiRef; + +// @public +export class AdrClient implements AdrApi { + constructor(options: AdrClientOptions); + // (undocumented) + listAdrs(url: string): Promise; + // (undocumented) + readAdr(url: string): Promise; +} + +// @public +export interface AdrClientOptions { + // (undocumented) + discoveryApi: DiscoveryApi; +} + // @public export type AdrContentDecorator = (adrInfo: { baseUrl: string; @@ -21,10 +47,16 @@ export type AdrContentDecorator = (adrInfo: { }; // @public -export interface AdrFileFetcher { - useGetAdrFilesAtUrl: (url: string) => any; - useReadAdrFileAtUrl: (url: string) => any; -} +export type AdrFileInfo = { + type: string; + path: string; + name: string; +}; + +// @public +export type AdrListResult = { + data: AdrFileInfo[]; +}; // @public export const adrPlugin: BackstagePlugin< @@ -37,17 +69,18 @@ export const adrPlugin: BackstagePlugin< // @public export const AdrReader: { - (props: { - adr: string; - decorators?: AdrContentDecorator[]; - adrFileFetcher?: AdrFileFetcher; - }): JSX.Element; + (props: { adr: string; decorators?: AdrContentDecorator[] }): JSX.Element; decorators: Readonly<{ createRewriteRelativeLinksDecorator(): AdrContentDecorator; createRewriteRelativeEmbedsDecorator(): AdrContentDecorator; }>; }; +// @public +export type AdrReadResult = { + data: string; +}; + // @public export function AdrSearchResultListItem(props: { lineClamp?: number; @@ -60,14 +93,7 @@ export function AdrSearchResultListItem(props: { export const EntityAdrContent: (props: { contentDecorators?: AdrContentDecorator[] | undefined; filePathFilterFn?: AdrFilePathFilterFn | undefined; - adrFileFetcher?: AdrFileFetcher | undefined; }) => JSX.Element; export { isAdrAvailable }; - -// @public -export const octokitAdrFileFetcher: AdrFileFetcher; - -// @public -export const urlReaderAdrFileFetcher: AdrFileFetcher; ``` From 83dc6e47853bce9b4bd8acbb9ccec4fcdb297e08 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Tue, 27 Dec 2022 10:18:20 -0500 Subject: [PATCH 19/23] Update version bump Changed plugin-adr's bump to minor and added breaking changes warning Signed-off-by: Robert Bunning --- .changeset/dull-taxis-carry.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/dull-taxis-carry.md b/.changeset/dull-taxis-carry.md index 3cc4bbdde3..1466f1246a 100644 --- a/.changeset/dull-taxis-carry.md +++ b/.changeset/dull-taxis-carry.md @@ -1,6 +1,8 @@ --- -'@backstage/plugin-adr': patch +'@backstage/plugin-adr': minor '@backstage/plugin-adr-backend': patch --- The ADR plugin can now work with sites other than GitHub. Expanded the ADR backend plugin to provide endpoints to facilitate this. + +**BREAKING** The ADR plugin now uses UrlReaders. You will have to [configure integrations](https://backstage.io/docs/integrations/index#configuration) for all sites you want to get ADRs from. If you would like to create your own implementation that has different behavior, you can override the AdrApi [just like you can with other apis.](https://backstage.io/docs/api/utility-apis#app-apis) The previously used Octokit implementation has been completely removed. From 015e76cbfd389ff2e0ff2d6c7b7c2f22784cf907 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Tue, 27 Dec 2022 10:37:40 -0500 Subject: [PATCH 20/23] Updated adr plugin's readme Signed-off-by: Robert Bunning --- plugins/adr/README.md | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/plugins/adr/README.md b/plugins/adr/README.md index 2478622560..dde270af11 100644 --- a/plugins/adr/README.md +++ b/plugins/adr/README.md @@ -4,17 +4,19 @@ Welcome to the ADR plugin! This plugin allows you to browse ADRs associated with your entities as well as a way to discover ADRs across others entities via Backstage Search. Use this to learn from the past experience of other projects to guide your own architecture decisions. -NOTE: By default, this plugin only supports entities/ADRs registered via GitHub integration. To get ADRs from other sites, see the [Using ADR plugin with sites other than GitHub](#using-adr-plugin-with-sites-other-than-github) section. - ## Setup -Install this plugin: +1. Install this plugin: ```bash # From your Backstage root directory yarn --cwd packages/app add @backstage/plugin-adr ``` +2. Make sure the [ADR backend plugin](../adr-backend/README.md) is installed. + +3. [Configure integrations](https://backstage.io/docs/integrations/) for all sites you would like to pull ADRs from. + ### Entity Pages 1. Add the plugin as a tab to your Entity pages: @@ -77,29 +79,6 @@ case 'adr': ); ``` -## Using ADR plugin with sites other than GitHub - -By default, the ADR plugin will only be able to retrieve ADRs through GitHub. If you would like to use it with other sites (for instance, AzureDevops): - -1. Make sure the [ADR backend plugin](../adr-backend/README.md) is installed. -2. [Configure an integration](https://backstage.io/docs/integrations/) for the site you would like to pull ADRs from. -3. Set the adrFileFetcher property on EntityAdrContent to urlReaderAdrFileFetcher: - -```jsx -// In packages/app/src/components/catalog/EntityPage.tsx -import { EntityAdrContent, isAdrAvailable, urlReaderAdrFileFetcher } from '@backstage/plugin-adr'; - -... - -const serviceEntityPage = ( - - {/* other tabs... */} - - - - -``` - ## Custom ADR formats By default, this plugin will parse ADRs according to the format specified by the [Markdown Architecture Decision Record (MADR)](https://adr.github.io/madr/) template. If your ADRs are written using a different format, you can apply the following customizations to correctly identify and parse your documents: From ebe652df00acd011cffec3f101cd021638e6fce5 Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Wed, 28 Dec 2022 13:24:42 -0500 Subject: [PATCH 21/23] Update dependencies Signed-off-by: Robert Bunning --- plugins/adr-backend/package.json | 2 +- plugins/adr/package.json | 3 --- yarn.lock | 3 --- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index cbafc29c6d..2203cfaf5c 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -36,6 +36,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-adr-common": "workspace:^", "@backstage/plugin-search-common": "workspace:^", + "@types/express": "^4.17.15", "express": "^4.18.2", "express-promise-router": "^4.1.1", "luxon": "^3.0.0", @@ -46,7 +47,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@types/express": "^4.17.15", "@types/marked": "^4.0.0", "@types/supertest": "^2.0.8", "msw": "^0.49.0", diff --git a/plugins/adr/package.json b/plugins/adr/package.json index dc3a8d46eb..2bd06c4891 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -49,9 +49,6 @@ "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", - "@backstage/integration": "workspace:^", - "@backstage/plugin-catalog": "workspace:^", - "@backstage/plugin-permission-react": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", diff --git a/yarn.lock b/yarn.lock index 31bd5f6cb7..2c6c8a4878 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4243,12 +4243,9 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" - "@backstage/integration": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-adr-common": "workspace:^" - "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" - "@backstage/plugin-permission-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^" From 888a53b9b7c2cbde0b74b9b294179fdca73d4eba Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Thu, 29 Dec 2022 08:57:17 -0500 Subject: [PATCH 22/23] Remove unneeded dependencies Signed-off-by: Robert Bunning --- plugins/adr/package.json | 3 --- yarn.lock | 3 --- 2 files changed, 6 deletions(-) diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 2bd06c4891..016e779bf7 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -34,8 +34,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "git-url-parse": "^13.0.0", - "octokit": "^2.0.0", "react-markdown": "^8.0.0", "react-use": "^17.2.4", "remark-gfm": "^3.0.1" @@ -45,7 +43,6 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "devDependencies": { - "@backstage/catalog-client": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 9fe2584a3b..76a82ba929 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4236,7 +4236,6 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-adr@workspace:plugins/adr" dependencies: - "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" @@ -4259,9 +4258,7 @@ __metadata: "@types/git-url-parse": ^9.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - git-url-parse: ^13.0.0 msw: ^0.49.0 - octokit: ^2.0.0 react-markdown: ^8.0.0 react-use: ^17.2.4 remark-gfm: ^3.0.1 From 30caac0eb35c4903ae58d2391724d408e31e008b Mon Sep 17 00:00:00 2001 From: Robert Bunning Date: Tue, 3 Jan 2023 11:51:10 -0500 Subject: [PATCH 23/23] Remove unneeded read method in mocked UrlReader Signed-off-by: Robert Bunning --- plugins/adr-backend/src/service/router.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts index 74955fcc6a..fbbd6e8532 100644 --- a/plugins/adr-backend/src/service/router.test.ts +++ b/plugins/adr-backend/src/service/router.test.ts @@ -59,9 +59,6 @@ const testFileTwoContent = 'testFileTwo content'; const genericFileContent = 'file content'; const mockUrlReader: UrlReader = { - read() { - throw new Error('read not implemented.'); - }, readUrl(url: string) { switch (url) { case 'testFileOne':