Merge pull request #15348 from Robunnin/adr-plugin-adjustments

Allow ADR plugin to use sites other than GitHub
This commit is contained in:
Johan Haals
2023-01-04 11:08:45 +01:00
committed by GitHub
23 changed files with 695 additions and 80 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@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.
+1
View File
@@ -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:^",
+4 -1
View File
@@ -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)
+29
View File
@@ -0,0 +1,29 @@
/*
* 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<Router> {
return await createRouter({
reader: env.reader,
cacheClient: env.cache.getClient(),
logger: env.logger,
});
}
+2
View File
@@ -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 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).
+14
View File
@@ -7,10 +7,12 @@
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';
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';
@@ -41,9 +43,21 @@ 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(
options: AdrRouterOptions,
): Promise<express.Router>;
// @public
export class DefaultAdrCollatorFactory implements DocumentCollatorFactory {
// (undocumented)
+3
View File
@@ -36,6 +36,9 @@
"@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",
"marked": "^4.0.14",
"node-fetch": "^2.6.5",
+1
View File
@@ -21,3 +21,4 @@
*/
export * from './search';
export * from './service';
@@ -13,4 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './useOctokitRequest';
export { createRouter } from './router';
export type { AdrRouterOptions } from './router';
@@ -0,0 +1,229 @@
/*
* 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 {
CacheClient,
ReadTreeResponse,
ReadTreeResponseFile,
ReadUrlResponse,
UrlReader,
} from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
import { Logger } from 'winston';
const listEndpointName = '/list';
const fileEndpointName = '/file';
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 = {
readUrl(url: string) {
switch (url) {
case 'testFileOne':
return makeFileContent(testFileOneContent);
case 'testFileTwo':
return makeFileContent(testFileTwoContent);
default:
return makeFileContent(genericFileContent);
}
},
readTree() {
const result: ReadTreeResponse = {
files: async () => testingUrlFakeFileTree,
archive() {
throw new Error('Function not implemented.');
},
dir() {
throw new Error('Function not implemented.');
},
etag: '',
};
const resultPromise = async () => result;
return resultPromise();
},
search() {
throw new Error('search not implemented.');
},
};
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({
reader: mockUrlReader,
cacheClient: new MockCacheClient(),
logger: {
error: (message: any) => message,
} as Logger,
});
app = express().use(router);
});
describe(`GET ${listEndpointName}`, () => {
it('returns bad request (400) when no url is provided', async () => {
const urlNotSpecifiedRequest = await request(app).get(listEndpointName);
const urlNotSpecifiedStatus = urlNotSpecifiedRequest.status;
const urlNotSpecifiedMessage = urlNotSpecifiedRequest.body.message;
const urlNotFilledRequest = await request(app).get(
`${listEndpointName}?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(`${listEndpointName}?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 ${fileEndpointName}`, () => {
it('returns bad request (400) when no url is provided', async () => {
const urlNotSpecifiedRequest = await request(app).get(fileEndpointName);
const urlNotSpecifiedStatus = urlNotSpecifiedRequest.status;
const urlNotSpecifiedMessage = urlNotSpecifiedRequest.body.message;
const urlNotFilledRequest = await request(app).get(
`${fileEndpointName}?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(
`${fileEndpointName}?url=testFileOne`,
);
const fileOneStatus = fileOneResponse.status;
const fileOneBody = fileOneResponse.body;
const fileOneError = fileOneResponse.error;
const fileTwoResponse = await request(app).get(
`${fileEndpointName}?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);
});
});
});
+129
View File
@@ -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 { 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';
/** @public */
export type AdrRouterOptions = {
reader: UrlReader;
cacheClient: CacheClient;
logger: Logger;
};
/** @public */
export async function createRouter(
options: AdrRouterOptions,
): Promise<express.Router> {
const { reader, cacheClient, logger } = options;
const router = Router();
router.use(express.json());
router.get('/list', async (req, res) => {
const urlToProcess = req.query.url as string;
if (!urlToProcess) {
res.statusCode = 400;
res.json({ message: 'No URL provided' });
return;
}
const cachedTree = (await cacheClient.get(urlToProcess)) as {
data: {
type: string;
name: string;
path: string;
}[];
etag: string;
};
const cachedData = cachedTree?.data;
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) => {
const urlToProcess = req.query.url as string;
if (!urlToProcess) {
res.statusCode = 400;
res.json({ message: 'No URL provided' });
return;
}
const cachedFileContent = (await cacheClient.get(urlToProcess)) as {
data: string;
etag: string;
};
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;
}
+5 -3
View File
@@ -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: This plugin currently only supports entities/ADRs registered via GitHub integration.
## 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:
+43
View File
@@ -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<AdrListResult>;
readAdr(url: string): Promise<AdrReadResult>;
}
// @public
export const adrApiRef: ApiRef<AdrApi>;
// @public
export class AdrClient implements AdrApi {
constructor(options: AdrClientOptions);
// (undocumented)
listAdrs(url: string): Promise<AdrListResult>;
// (undocumented)
readAdr(url: string): Promise<AdrReadResult>;
}
// @public
export interface AdrClientOptions {
// (undocumented)
discoveryApi: DiscoveryApi;
}
// @public
export type AdrContentDecorator = (adrInfo: {
baseUrl: string;
@@ -20,6 +46,18 @@ export type AdrContentDecorator = (adrInfo: {
content: string;
};
// @public
export type AdrFileInfo = {
type: string;
path: string;
name: string;
};
// @public
export type AdrListResult = {
data: AdrFileInfo[];
};
// @public
export const adrPlugin: BackstagePlugin<
{
@@ -38,6 +76,11 @@ export const AdrReader: {
}>;
};
// @public
export type AdrReadResult = {
data: string;
};
// @public
export function AdrSearchResultListItem(props: {
lineClamp?: number;
-2
View File
@@ -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"
+66
View File
@@ -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<T>(endpoint: string, fileUrl: string): Promise<T> {
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.message}`);
}
return data;
}
async listAdrs(url: string): Promise<AdrListResult> {
return this.fetchAdrApi<AdrListResult>(listEndpoint, url);
}
async readAdr(url: string): Promise<AdrReadResult> {
return this.fetchAdrApi<AdrReadResult>(readEndpoint, url);
}
}
+25
View File
@@ -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.
*/
export { AdrClient } from './AdrClient';
export type { AdrClientOptions } from './AdrClient';
export { adrApiRef } from './types';
export type {
AdrApi,
AdrFileInfo,
AdrListResult,
AdrReadResult,
} from './types';
+74
View File
@@ -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<AdrListResult>;
/** Reads the contents of the ADR at the provided url. */
readAdr(url: string): Promise<AdrReadResult>;
}
/**
* ApiRef for the AdrApi.
*
* @public
*/
export const adrApiRef = createApiRef<AdrApi>({
id: 'plugin.adr.api',
});
@@ -26,9 +26,10 @@ 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 { adrApiRef } from '../../api';
import useAsync from 'react-use/lib/useAsync';
/**
* Component to fetch and render an ADR.
@@ -42,10 +43,13 @@ export const AdrReader = (props: {
const { adr, decorators } = props;
const { entity } = useEntity();
const scmIntegrations = useApi(scmIntegrationsApiRef);
const adrApi = useApi(adrApiRef);
const adrLocationUrl = getAdrLocationUrl(entity, scmIntegrations);
const { value, loading, error } = useOctokitRequest(
`${adrLocationUrl.replace(/\/$/, '')}/${adr}`,
const url = `${adrLocationUrl.replace(/\/$/, '')}/${adr}`;
const { value, loading, error } = useAsync(
async () => adrApi.readAdr(url),
[url],
);
const adrContent = useMemo(() => {
@@ -44,9 +44,10 @@ import {
Typography,
} from '@material-ui/core';
import { useOctokitRequest } from '../../hooks';
import { rootRouteRef } from '../../routes';
import { AdrContentDecorator, AdrReader } from '../AdrReader';
import { adrApiRef } from '../../api';
import useAsync from 'react-use/lib/useAsync';
const useStyles = makeStyles((theme: Theme) => ({
adrMenu: {
@@ -69,10 +70,13 @@ export const EntityAdrContent = (props: {
const [adrList, setAdrList] = useState<string[]>([]);
const [searchParams, setSearchParams] = useSearchParams();
const scmIntegrations = useApi(scmIntegrationsApiRef);
const adrApi = useApi(adrApiRef);
const entityHasAdrs = isAdrAvailable(entity);
const { value, loading, error } = useOctokitRequest(
getAdrLocationUrl(entity, scmIntegrations),
const url = getAdrLocationUrl(entity, scmIntegrations);
const { value, loading, error } = useAsync(
async () => adrApi.listAdrs(url),
[url],
);
const selectedAdr =
@@ -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<any>(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]);
};
+10
View File
@@ -13,11 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* ADR frontend plugin
*
* @packageDocumentation
*/
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';
+14 -1
View File
@@ -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,
},
+21 -7
View File
@@ -4238,7 +4238,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:
@@ -4251,8 +4251,11 @@ __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
express-promise-router: ^4.1.1
luxon: ^3.0.0
marked: ^4.0.14
msw: ^0.49.0
@@ -4300,9 +4303,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
@@ -14412,7 +14413,7 @@ __metadata:
languageName: node
linkType: hard
"@types/express-serve-static-core@npm:*, @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.18, @types/express-serve-static-core@npm:^4.17.31, @types/express-serve-static-core@npm:^4.17.5":
version: 4.17.32
resolution: "@types/express-serve-static-core@npm:4.17.32"
dependencies:
@@ -14453,7 +14454,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:
@@ -22437,6 +22450,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:^"
@@ -22635,7 +22649,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:
@@ -22677,7 +22691,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: