Merge pull request #15348 from Robunnin/adr-plugin-adjustments
Allow ADR plugin to use sites other than GitHub
This commit is contained in:
@@ -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.
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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,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,
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user