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: