From e73075a30159042ec0632217af508536958519e7 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Wed, 13 Apr 2022 14:39:17 -0400 Subject: [PATCH] feat(ADR): implement ADR plugin Signed-off-by: Phil Kuang --- .changeset/chatty-forks-bathe.md | 7 + microsite/data/plugins/adr.yaml | 9 + microsite/static/img/adr-logo.png | Bin 0 -> 21171 bytes plugins/adr-backend/.eslintrc.js | 1 + plugins/adr-backend/README.md | 47 ++++ plugins/adr-backend/api-report.md | 66 ++++++ plugins/adr-backend/package.json | 56 +++++ plugins/adr-backend/src/index.ts | 23 ++ .../src/search/DefaultAdrCollatorFactory.ts | 212 ++++++++++++++++++ .../src/search/createMadrParser.ts | 112 +++++++++ plugins/adr-backend/src/search/index.ts | 19 ++ plugins/adr-backend/src/search/types.ts | 43 ++++ plugins/adr-common/.eslintrc.js | 1 + plugins/adr-common/README.md | 3 + plugins/adr-common/api-report.md | 36 +++ plugins/adr-common/package.json | 43 ++++ plugins/adr-common/src/index.ts | 95 ++++++++ plugins/adr/.eslintrc.js | 1 + plugins/adr/README.md | 126 +++++++++++ plugins/adr/api-report.md | 64 ++++++ plugins/adr/package.json | 62 +++++ .../src/components/AdrReader/AdrReader.tsx | 83 +++++++ .../src/components/AdrReader/decorators.ts | 47 ++++ plugins/adr/src/components/AdrReader/index.ts | 17 ++ plugins/adr/src/components/AdrReader/types.ts | 25 +++ .../EntityAdrContent/EntityAdrContent.tsx | 155 +++++++++++++ .../src/components/EntityAdrContent/index.ts | 16 ++ plugins/adr/src/hooks/index.ts | 16 ++ plugins/adr/src/hooks/useOctokitRequest.ts | 59 +++++ plugins/adr/src/index.ts | 24 ++ plugins/adr/src/plugin.test.ts | 22 ++ plugins/adr/src/plugin.ts | 46 ++++ plugins/adr/src/routes.ts | 24 ++ .../src/search/AdrSearchResultListItem.tsx | 80 +++++++ plugins/adr/src/search/index.ts | 16 ++ plugins/adr/src/setupTests.ts | 17 ++ yarn.lock | 7 +- 37 files changed, 1679 insertions(+), 1 deletion(-) create mode 100644 .changeset/chatty-forks-bathe.md create mode 100644 microsite/data/plugins/adr.yaml create mode 100644 microsite/static/img/adr-logo.png create mode 100644 plugins/adr-backend/.eslintrc.js create mode 100644 plugins/adr-backend/README.md create mode 100644 plugins/adr-backend/api-report.md create mode 100644 plugins/adr-backend/package.json create mode 100644 plugins/adr-backend/src/index.ts create mode 100644 plugins/adr-backend/src/search/DefaultAdrCollatorFactory.ts create mode 100644 plugins/adr-backend/src/search/createMadrParser.ts create mode 100644 plugins/adr-backend/src/search/index.ts create mode 100644 plugins/adr-backend/src/search/types.ts create mode 100644 plugins/adr-common/.eslintrc.js create mode 100644 plugins/adr-common/README.md create mode 100644 plugins/adr-common/api-report.md create mode 100644 plugins/adr-common/package.json create mode 100644 plugins/adr-common/src/index.ts create mode 100644 plugins/adr/.eslintrc.js create mode 100644 plugins/adr/README.md create mode 100644 plugins/adr/api-report.md create mode 100644 plugins/adr/package.json create mode 100644 plugins/adr/src/components/AdrReader/AdrReader.tsx create mode 100644 plugins/adr/src/components/AdrReader/decorators.ts create mode 100644 plugins/adr/src/components/AdrReader/index.ts create mode 100644 plugins/adr/src/components/AdrReader/types.ts create mode 100644 plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx create mode 100644 plugins/adr/src/components/EntityAdrContent/index.ts create mode 100644 plugins/adr/src/hooks/index.ts create mode 100644 plugins/adr/src/hooks/useOctokitRequest.ts create mode 100644 plugins/adr/src/index.ts create mode 100644 plugins/adr/src/plugin.test.ts create mode 100644 plugins/adr/src/plugin.ts create mode 100644 plugins/adr/src/routes.ts create mode 100644 plugins/adr/src/search/AdrSearchResultListItem.tsx create mode 100644 plugins/adr/src/search/index.ts create mode 100644 plugins/adr/src/setupTests.ts diff --git a/.changeset/chatty-forks-bathe.md b/.changeset/chatty-forks-bathe.md new file mode 100644 index 0000000000..e6d3816cc6 --- /dev/null +++ b/.changeset/chatty-forks-bathe.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-adr': minor +'@backstage/plugin-adr-backend': minor +'@backstage/plugin-adr-common': minor +--- + +Implement ADR plugin diff --git a/microsite/data/plugins/adr.yaml b/microsite/data/plugins/adr.yaml new file mode 100644 index 0000000000..f180c80e36 --- /dev/null +++ b/microsite/data/plugins/adr.yaml @@ -0,0 +1,9 @@ +--- +title: Architecture Decision Records +author: Phil Kuang +authorUrl: https://github.com/kuangp +category: Discovery +description: Browse your project's ADRs. +documentation: https://github.com/backstage/backstage/tree/master/plugins/adr +iconUrl: img/adr-logo.png +npmPackageName: '@backstage/plugin-adr' diff --git a/microsite/static/img/adr-logo.png b/microsite/static/img/adr-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a71c54074d2651af5a6949159876893fecd825a5 GIT binary patch literal 21171 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 IM6N<$g2x(CPXGV_ literal 0 HcmV?d00001 diff --git a/plugins/adr-backend/.eslintrc.js b/plugins/adr-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/adr-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/adr-backend/README.md b/plugins/adr-backend/README.md new file mode 100644 index 0000000000..79b5fe13ed --- /dev/null +++ b/plugins/adr-backend/README.md @@ -0,0 +1,47 @@ +# ADR Backend + +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. + +## 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). + +When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, install this plugin and add the following code snippet to add the `DefaultAdrCollatorFactory`. Also make sure to set up the frontend [ADR plugin](../adr/README.md) so search results can be routed correctly. + +```bash +# From your Backstage root directory +yarn --cwd packages/backend add @backstage/plugin-adr-backend +``` + +```ts +import { DefaultAdrCollatorFactory } from '@backstage/plugin-adr-backend'; + +... + +indexBuilder.addCollator({ + schedule, + factory: DefaultAdrCollatorFactory.fromConfig({ + cache: env.cache, + config: env.config, + discovery: env.discovery, + logger: env.logger, + reader: env.reader, + tokenManager: env.tokenManager, + }), +}); +``` + +### Parsing custom ADR document formats + +By default, the `DefaultAdrCollatorFactory` will parse and index documents that follow the [MADR](https://adr.github.io/madr/) standard file name and template format. If you use a different ADR format and file name convention, you can configure `DefaultAdrCollatorFactory` with custom `adrFilePathFilterFn` and `parser` options (see type definitions for details): + +```ts +DefaultAdrCollatorFactory.fromConfig({ + ... + parser: myCustomAdrParser, + adrFilePathFilterFn: myCustomAdrFilePathFilter, + ... +}) +``` diff --git a/plugins/adr-backend/api-report.md b/plugins/adr-backend/api-report.md new file mode 100644 index 0000000000..5a671e9c8f --- /dev/null +++ b/plugins/adr-backend/api-report.md @@ -0,0 +1,66 @@ +## API Report File for "@backstage/plugin-adr-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { AdrDocument } from '@backstage/plugin-adr-common'; +import { AdrFilePathFilterFn } from '@backstage/plugin-adr-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 { Logger } from 'winston'; +import { PluginCacheManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Readable } from 'stream'; +import { TokenManager } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; + +// @public +export type AdrCollatorFactoryOptions = { + adrFilePathFilterFn?: AdrFilePathFilterFn; + cache: PluginCacheManager; + config: Config; + catalogClient?: CatalogApi; + discovery: PluginEndpointDiscovery; + logger: Logger; + parser?: AdrParser; + reader: UrlReader; + tokenManager: TokenManager; +}; + +// @public +export type AdrParser = (ctx: AdrParserContext) => Promise; + +// @public +export type AdrParserContext = { + entity: Entity; + content: string; + path: string; +}; + +// @public +export const createMadrParser: (options?: MadrParserOptions) => AdrParser; + +// @public +export class DefaultAdrCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + execute(): AsyncGenerator; + // (undocumented) + static fromConfig( + options: AdrCollatorFactoryOptions, + ): DefaultAdrCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; +} + +// @public +export type MadrParserOptions = { + locationTemplate?: string; + dateFormat?: string; +}; +``` diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json new file mode 100644 index 0000000000..e26d27ccee --- /dev/null +++ b/plugins/adr-backend/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-adr-backend", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/adr-backend" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "^0.13.3-next.0", + "@backstage/catalog-client": "^1.0.1", + "@backstage/catalog-model": "^1.0.1", + "@backstage/config": "^1.0.0", + "@backstage/errors": "^1.0.0", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-adr-common": "^0.0.0", + "@backstage/plugin-search-common": "^0.3.3", + "luxon": "^2.0.2", + "marked": "^4.0.14", + "winston": "^3.2.1", + "node-fetch": "^2.6.5", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.1-next.0", + "@types/marked": "^4.0.0", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.3", + "msw": "^0.35.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/adr-backend/src/index.ts b/plugins/adr-backend/src/index.ts new file mode 100644 index 0000000000..86e07943df --- /dev/null +++ b/plugins/adr-backend/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * ADR backend plugin + * + * @packageDocumentation + */ + +export * from './search'; diff --git a/plugins/adr-backend/src/search/DefaultAdrCollatorFactory.ts b/plugins/adr-backend/src/search/DefaultAdrCollatorFactory.ts new file mode 100644 index 0000000000..e0c90af767 --- /dev/null +++ b/plugins/adr-backend/src/search/DefaultAdrCollatorFactory.ts @@ -0,0 +1,212 @@ +/* + * 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 { Readable } from 'stream'; +import { Logger } from 'winston'; +import { + CacheClient, + PluginCacheManager, + PluginEndpointDiscovery, + TokenManager, + UrlReader, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + CATALOG_FILTER_EXISTS, +} from '@backstage/catalog-client'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { NotModifiedError } from '@backstage/errors'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; +import { + AdrDocument, + AdrFilePathFilterFn, + ANNOTATION_ADR_LOCATION, + getAdrLocationUrl, + madrFilePathFilter, +} from '@backstage/plugin-adr-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; + +import { createMadrParser } from './createMadrParser'; +import { AdrParser } from './types'; + +/** + * Options to configure the AdrCollatorFactory + * @public + */ +export type AdrCollatorFactoryOptions = { + /** + * Function used to filter ADR file paths. + * Defaults to filtering paths following MADR filename format. + */ + adrFilePathFilterFn?: AdrFilePathFilterFn; + /** + * Plugin cache manager + */ + cache: PluginCacheManager; + /** + * App Config + */ + config: Config; + /** + * Catalog API client. Defaults to CatalogClient. + */ + catalogClient?: CatalogApi; + /** + * Plugin Endpoint Discovery client + */ + discovery: PluginEndpointDiscovery; + /** + * Logger + */ + logger: Logger; + /** + * ADR content parser. Defaults to built in MADR parser. + */ + parser?: AdrParser; + /** + * URL Reader + */ + reader: UrlReader; + /** + * Token Manager + */ + tokenManager: TokenManager; +}; + +/** + * Default collator to index ADR documents for Backstage search. + * @public + */ +export class DefaultAdrCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'adr'; + private readonly adrFilePathFilterFn: AdrFilePathFilterFn; + private readonly cacheClient: CacheClient; + private readonly catalogClient: CatalogApi; + private readonly logger: Logger; + private readonly parser: AdrParser; + private readonly reader: UrlReader; + private readonly scmIntegrations: ScmIntegrationRegistry; + private readonly tokenManager: TokenManager; + + private constructor(options: AdrCollatorFactoryOptions) { + this.adrFilePathFilterFn = + options.adrFilePathFilterFn ?? madrFilePathFilter; + this.cacheClient = options.cache.getClient(); + this.catalogClient = + options.catalogClient ?? + new CatalogClient({ discoveryApi: options.discovery }); + this.logger = options.logger; + this.parser = options.parser ?? createMadrParser(); + this.reader = options.reader; + this.scmIntegrations = ScmIntegrations.fromConfig(options.config); + this.tokenManager = options.tokenManager; + } + + static fromConfig(options: AdrCollatorFactoryOptions) { + return new DefaultAdrCollatorFactory(options); + } + + async getCollator() { + return Readable.from(this.execute()); + } + + async *execute(): AsyncGenerator { + const { token } = await this.tokenManager.getToken(); + const entities = ( + await this.catalogClient.getEntities( + { + filter: { + [`metadata.annotations.${ANNOTATION_ADR_LOCATION}`]: + CATALOG_FILTER_EXISTS, + }, + fields: [ + 'kind', + 'metadata.annotations', + 'metadata.name', + 'metadata.namespace', + ], + }, + { token }, + ) + ).items; + + for (const ent of entities) { + let adrsUrl: string; + try { + adrsUrl = getAdrLocationUrl(ent, this.scmIntegrations); + } catch (e: any) { + this.logger.error( + `Unable to get ADR location URL for ${stringifyEntityRef(ent)}: ${ + e.message + }`, + ); + continue; + } + + const cacheItem = (await this.cacheClient.get(adrsUrl)) as { + adrFiles: { + path: string; + content: string; + }[]; + etag: string; + }; + + let adrFiles = cacheItem?.adrFiles; + try { + const tree = await this.reader.readTree(adrsUrl, { + etag: cacheItem?.etag, + filter: filePath => this.adrFilePathFilterFn(filePath), + }); + + adrFiles = await Promise.all( + ( + await tree.files() + ).map(async f => ({ + path: f.path, + content: (await f.content()).toString('utf8'), + })), + ); + + await this.cacheClient.set(adrsUrl, { adrFiles, etag: tree.etag }); + } catch (error: any) { + // Ignore error if we're able to use cached response + if (!cacheItem || error.name !== NotModifiedError.name) { + throw error; + } + } + + for (const { content, path } of adrFiles) { + try { + const adrDoc = await this.parser({ + entity: ent, + path, + content, + }); + yield adrDoc; + } catch (e: any) { + this.logger.error(`Unable to parse ADR ${path}: ${e.message}`); + } + } + + this.logger.info(`Indexed ${adrFiles.length} ADRs from ${adrsUrl}`); + } + } +} diff --git a/plugins/adr-backend/src/search/createMadrParser.ts b/plugins/adr-backend/src/search/createMadrParser.ts new file mode 100644 index 0000000000..9efd49754a --- /dev/null +++ b/plugins/adr-backend/src/search/createMadrParser.ts @@ -0,0 +1,112 @@ +/* + * 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 { DateTime } from 'luxon'; +import { marked } from 'marked'; +import { MADR_DATE_FORMAT } from '@backstage/plugin-adr-common'; + +import { AdrParser } from './types'; + +const applyArgsToFormat = ( + format: string, + args: Record, +): string => { + let formatted = format; + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + return formatted.toLowerCase(); +}; + +const DEFAULT_LOCATION_TEMPLATE = + '/catalog/:namespace/:kind/:name/adrs?record=:record'; + +/** + * + * Options for the default MADR content parser + * @public + */ +export type MadrParserOptions = { + /** + * Location template for the route of the frontend plugin + * Defaults to '/catalog/:namespace/:kind/:name/adrs?record=:record' + */ + locationTemplate?: string; + /** + * luxon DateTime format string to parse ADR dates with. + * Defaults to 'yyyy-MM-dd' + */ + dateFormat?: string; +}; + +/** + * Default content parser for ADRs following the MADR template (https://adr.github.io/madr/) + * @public + */ +export const createMadrParser = ( + options: MadrParserOptions = {}, +): AdrParser => { + const locationTemplate = + options.locationTemplate ?? DEFAULT_LOCATION_TEMPLATE; + const dateFormat = options.dateFormat ?? MADR_DATE_FORMAT; + + return async ({ entity, content, path }) => { + const tokens = marked.lexer(content); + if (!tokens.length) { + throw new Error('ADR has no content'); + } + + // First h1 header should contain ADR title + const adrTitle = ( + tokens.find( + t => t.type === 'heading' && t.depth === 1, + ) as marked.Tokens.Heading + )?.text; + + // First list should contain status & date metadata (if defined) + const listTokens = ( + tokens.find(t => t.type === 'list') as marked.Tokens.List + )?.items; + const adrStatus = listTokens + .find(t => /^status:/i.test(t.text)) + ?.text.replace(/^status:/i, '') + .trim() + .toLocaleLowerCase('en-US'); + const adrDateTime = DateTime.fromFormat( + listTokens + .find(t => /^date:/i.test(t.text)) + ?.text.replace(/^date:/i, '') + .trim() ?? '', + dateFormat, + ); + const adrDate = adrDateTime.isValid + ? adrDateTime.toFormat(MADR_DATE_FORMAT) + : undefined; + + return { + title: adrTitle ?? path.replace(/\.md$/, ''), + text: content, + status: adrStatus, + date: adrDate, + location: applyArgsToFormat(locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + record: path, + }), + }; + }; +}; diff --git a/plugins/adr-backend/src/search/index.ts b/plugins/adr-backend/src/search/index.ts new file mode 100644 index 0000000000..33f14631ec --- /dev/null +++ b/plugins/adr-backend/src/search/index.ts @@ -0,0 +1,19 @@ +/* + * 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 * from './DefaultAdrCollatorFactory'; +export * from './createMadrParser'; +export * from './types'; diff --git a/plugins/adr-backend/src/search/types.ts b/plugins/adr-backend/src/search/types.ts new file mode 100644 index 0000000000..8ac47d5c8b --- /dev/null +++ b/plugins/adr-backend/src/search/types.ts @@ -0,0 +1,43 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { AdrDocument } from '@backstage/plugin-adr-common'; + +/** + * Context passed to a AdrParser. + * @public + */ +export type AdrParserContext = { + /** + * The entity associated with the ADR. + */ + entity: Entity; + /** + * The ADR content string. + */ + content: string; + /** + * The ADR file path. + */ + path: string; +}; + +/** + * ADR parser function type. + * @public + */ +export type AdrParser = (ctx: AdrParserContext) => Promise; diff --git a/plugins/adr-common/.eslintrc.js b/plugins/adr-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/adr-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/adr-common/README.md b/plugins/adr-common/README.md new file mode 100644 index 0000000000..38b5b7433c --- /dev/null +++ b/plugins/adr-common/README.md @@ -0,0 +1,3 @@ +# ADR Common + +Common types and functionalities for the ADR plugin. diff --git a/plugins/adr-common/api-report.md b/plugins/adr-common/api-report.md new file mode 100644 index 0000000000..290d984181 --- /dev/null +++ b/plugins/adr-common/api-report.md @@ -0,0 +1,36 @@ +## API Report File for "@backstage/plugin-adr-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Entity } from '@backstage/catalog-model'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +// @public +export interface AdrDocument extends IndexableDocument { + date?: string; + status?: string; +} + +// @public +export type AdrFilePathFilterFn = (path: string) => boolean; + +// @public +export const ANNOTATION_ADR_LOCATION = 'backstage.io/adr-location'; + +// @public +export const getAdrLocationUrl: ( + entity: Entity, + scmIntegration: ScmIntegrationRegistry, +) => string; + +// @public +export const isAdrAvailable: (entity: Entity) => boolean; + +// @public +export const MADR_DATE_FORMAT = 'yyyy-MM-dd'; + +// @public +export const madrFilePathFilter: AdrFilePathFilterFn; +``` diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json new file mode 100644 index 0000000000..41e28423dc --- /dev/null +++ b/plugins/adr-common/package.json @@ -0,0 +1,43 @@ +{ + "name": "@backstage/plugin-adr-common", + "description": "Common functionalities for the adr plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/adr-common" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "^1.0.1", + "@backstage/integration": "^1.2.0-next.0", + "@backstage/plugin-search-common": "^0.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.17.1-next.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/adr-common/src/index.ts b/plugins/adr-common/src/index.ts new file mode 100644 index 0000000000..94b6b788c8 --- /dev/null +++ b/plugins/adr-common/src/index.ts @@ -0,0 +1,95 @@ +/* + * 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. + */ +/** + * Common types and functionalities for the ADR plugin. + * @packageDocumentation + */ +import { Entity, getEntitySourceLocation } from '@backstage/catalog-model'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +/** + * ADR plugin annotation. + * @public + */ +export const ANNOTATION_ADR_LOCATION = 'backstage.io/adr-location'; + +/** + * Standard luxon DateTime format string for MADR dates. + * @public + */ +export const MADR_DATE_FORMAT = 'yyyy-MM-dd'; + +/** + * Utility function to get the value of an entity ADR annotation. + * @public + */ +const getAdrLocationDir = (entity: Entity) => + entity.metadata.annotations?.[ANNOTATION_ADR_LOCATION]?.trim(); + +/** + * Utility function to determine if the given entity has ADRs. + * @public + */ +export const isAdrAvailable = (entity: Entity) => + Boolean(getAdrLocationDir(entity)); + +/** + * Utility function to extract the ADR location URL from an entity based off + * its ADR annotation and relative to the entity source location. + * @public + */ +export const getAdrLocationUrl = ( + entity: Entity, + scmIntegration: ScmIntegrationRegistry, +) => { + if (!isAdrAvailable(entity)) { + throw new Error(`Missing ADR annotation: ${ANNOTATION_ADR_LOCATION}`); + } + + return scmIntegration.resolveUrl({ + url: getAdrLocationDir(entity)!, + base: getEntitySourceLocation(entity).target, + }); +}; + +/** + * File path filter function type for ADR filenames + * @public + */ +export type AdrFilePathFilterFn = (path: string) => boolean; + +/** + * File path filter for MADR filename formats + * @public + */ +export const madrFilePathFilter: AdrFilePathFilterFn = (path: string) => + /^\d{4}-.+\.md$/.test(path); + +/** + * ADR indexable document interface + * @public + */ +export interface AdrDocument extends IndexableDocument { + /** + * ADR status label + */ + status?: string; + /** + * ADR date + */ + date?: string; +} diff --git a/plugins/adr/.eslintrc.js b/plugins/adr/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/adr/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/adr/README.md b/plugins/adr/README.md new file mode 100644 index 0000000000..2888ff2f07 --- /dev/null +++ b/plugins/adr/README.md @@ -0,0 +1,126 @@ +# Architecture Decision Records (ADR) Plugin + +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: + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-adr +``` + +### Entity Pages + +1. Add the plugin as a tab to your Entity pages: + +```jsx +// In packages/app/src/components/catalog/EntityPage.tsx +import { EntityAdrContent, isAdrAvailable } from '@backstage/plugin-adr'; + +... + +const serviceEntityPage = ( + + {/* other tabs... */} + + + + +``` + +2. Add `backstage.io/adr-location` annotation to your `catalog-info.yaml`: + +```yaml +metadata: + annotations: + backstage.io/adr-location: +``` + +The value for `backstage.io/adr-location` should be a path relative to your `catalog-info.yaml` file or a absolute URL to the directory which contains your ADR markdown files. + +For example, if you have the following directory structure, you would set `backstage.io/adr-location: docs/adrs`: + +``` +repo-root/ + README.md + src/ + catalog-info.yaml + docs/ + adrs/ + 0001-use-adrs.md + 0002-use-cloud.md +``` + +### Search + +First, make sure to setup Backstage Search with the [ADR backend plugin](../adr-backend/README.md). +Afterwards, add the following code snippet to use `AdrSearchResultListItem` when the type of the search results is `adr`: + +```tsx +// In packages/app/src/components/search/SearchPage.tsx +import { AdrSearchResultListItem } from '@backstage/plugin-adr'; + +... + +case 'adr': + return ( + + ); +``` + +## 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: + +### Custom Filename/Path Format + +In order to ensure the plugin fetches the correct ADR files (e.g. ignoring your template file), you can pass in an optional `filePathFilterFn` parameter to `EntityAdrContent` which will be called with each file path relative to the ADR location specified by `backstage.io/adr-location`. For example, the follow custom filter function will ignore the ADR template file and include files with a specific naming convention including those under a specified sub-directory: + +```tsx +const myCustomFilterFn: AdrFilePathFilterFn = (path: string): boolean => { + if (path === '0000-adr-template.md') { + return false; + } + // Match all files following the pattern NNNN-title-with-dashes.md including those under decided-adrs/ + return /^(decided-adrs\/)?\d{4}-.+\.md$/.test(path); +} + +... + + +``` + +### Custom Content Decorators + +Your ADR Markdown content will typically be rendered in the UI as is with the exception of relative links/embeds being rewritten as absolute URLs so they can be linked correctly (e.g. `./my-diagram.png` => `/my-diagram.png`). Depending on your ADR format, you may want to apply additional transformations to the content (e.g. parsing/ignoring front matter). You can do so by passing in a list of custom content decorators for the optional `contentDecorators` parameter. Note that passing in this parameter will override the default decorators. If you want to include the default ones, make sure to add them as well: + +```tsx +import { + AdrReader, + ... +} from '@backstage/plugin-adr'; + +... + +const myCustomDecorator: AdrContentDecorator = ({ content }) => { + return { content: applyCustomContentTransformation(content) }; +}; + +... + + +``` diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md new file mode 100644 index 0000000000..7d49689fc2 --- /dev/null +++ b/plugins/adr/api-report.md @@ -0,0 +1,64 @@ +## API Report File for "@backstage/plugin-adr" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { AdrDocument } from '@backstage/plugin-adr-common'; +import { AdrFilePathFilterFn } from '@backstage/plugin-adr-common'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { isAdrAvailable } from '@backstage/plugin-adr-common'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public +export type AdrContentDecorator = (adrInfo: { + baseUrl: string; + content: string; +}) => { + content: string; +}; + +// @public +export const adrPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// @public +export const AdrReader: { + ({ + adr, + decorators, + }: { + adr: string; + decorators?: AdrContentDecorator[] | undefined; + }): JSX.Element; + decorators: Readonly<{ + createRewriteRelativeLinksDecorator(): AdrContentDecorator; + createRewriteRelativeEmbedsDecorator(): AdrContentDecorator; + }>; +}; + +// @public +export const AdrSearchResultListItem: ({ + lineClamp, + result, +}: { + lineClamp?: number | undefined; + result: AdrDocument; +}) => JSX.Element; + +// @public +export const EntityAdrContent: ({ + contentDecorators, + filePathFilterFn, +}: { + contentDecorators?: AdrContentDecorator[] | undefined; + filePathFilterFn?: AdrFilePathFilterFn | undefined; +}) => JSX.Element; + +export { isAdrAvailable }; +``` diff --git a/plugins/adr/package.json b/plugins/adr/package.json new file mode 100644 index 0000000000..0f3b220ce5 --- /dev/null +++ b/plugins/adr/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage/plugin-adr", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.9.3", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-adr-common": "^0.0.0", + "@backstage/plugin-catalog-react": "^1.1.0-next.0", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "git-url-parse": "^11.6.0", + "octokit": "^1.7.1", + "react-markdown": "^8.0.0", + "react-router-dom": "6.0.0-beta.0", + "react-text-truncate": "^0.18.0", + "react-use": "^17.2.4", + "remark-gfm": "^3.0.1" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.1-next.0", + "@backstage/core-app-api": "^1.0.1", + "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/test-utils": "^1.0.2-next.0", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/git-url-parse": "^9.0.0", + "@types/jest": "*", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.35.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/adr/src/components/AdrReader/AdrReader.tsx b/plugins/adr/src/components/AdrReader/AdrReader.tsx new file mode 100644 index 0000000000..ae7dc6c271 --- /dev/null +++ b/plugins/adr/src/components/AdrReader/AdrReader.tsx @@ -0,0 +1,83 @@ +/* + * 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, { useMemo } from 'react'; +import { + InfoCard, + MarkdownContent, + Progress, + WarningPanel, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +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'; + +/** + * Component to fetch and render an ADR. + * @public + */ +export const AdrReader = ({ + adr, + decorators, +}: { + adr: string; + decorators?: AdrContentDecorator[]; +}) => { + const { entity } = useEntity(); + const scmIntegrations = useApi(scmIntegrationsApiRef); + const adrLocationUrl = getAdrLocationUrl(entity, scmIntegrations); + + const { value, loading, error } = useOctokitRequest( + `${adrLocationUrl}/${adr}`, + ); + + const adrContent = useMemo(() => { + if (!value?.data) { + return ''; + } + const adrDecorators = decorators ?? [ + adrDecoratorFactories.createRewriteRelativeLinksDecorator(), + adrDecoratorFactories.createRewriteRelativeEmbedsDecorator(), + ]; + + return adrDecorators.reduce( + (content, decorator) => + decorator({ baseUrl: adrLocationUrl, content }).content, + value.data, + ); + }, [adrLocationUrl, decorators, value]); + + return ( + + {loading && } + + {!loading && error && ( + + )} + + {!loading && !error && value?.data && ( + + )} + + ); +}; + +AdrReader.decorators = adrDecoratorFactories; diff --git a/plugins/adr/src/components/AdrReader/decorators.ts b/plugins/adr/src/components/AdrReader/decorators.ts new file mode 100644 index 0000000000..f24bb48a00 --- /dev/null +++ b/plugins/adr/src/components/AdrReader/decorators.ts @@ -0,0 +1,47 @@ +/* + * 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 { AdrContentDecorator } from './types'; + +/** + * + * Factory for creating default ADR content decorators. The adrDecoratorFactories + * symbol is not directly exported, but through the AdrReader.decorators field. + * @public + */ +export const adrDecoratorFactories = Object.freeze({ + /** + * Rewrites relative Markdown links as absolute links. + */ + createRewriteRelativeLinksDecorator(): AdrContentDecorator { + return ({ baseUrl, content }) => ({ + content: content.replace( + /\[([^\[\]]*)\]\((?!https?:\/\/)(.*?)(\.md)\)/gim, + `[$1](${baseUrl}/$2$3)`, + ), + }); + }, + /** + * Rewrites relative Markdown embeds using absolute URLs. + */ + createRewriteRelativeEmbedsDecorator(): AdrContentDecorator { + return ({ baseUrl, content }) => ({ + content: content.replace( + /!\[([^\[\]]*)\]\((?!https?:\/\/)(.*?)(\.png|\.jpg|\.jpeg|\.gif|\.webp)(.*)\)/gim, + `![$1](${baseUrl}/$2$3$4)`, + ), + }); + }, +}); diff --git a/plugins/adr/src/components/AdrReader/index.ts b/plugins/adr/src/components/AdrReader/index.ts new file mode 100644 index 0000000000..8482351016 --- /dev/null +++ b/plugins/adr/src/components/AdrReader/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 * from './AdrReader'; +export * from './types'; diff --git a/plugins/adr/src/components/AdrReader/types.ts b/plugins/adr/src/components/AdrReader/types.ts new file mode 100644 index 0000000000..f12906c00b --- /dev/null +++ b/plugins/adr/src/components/AdrReader/types.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. + */ + +/** + * ADR content decorator function type. Decorators are responsible for + * performing any necessary transformations on the ADR content before rendering. + * @public + */ +export type AdrContentDecorator = (adrInfo: { + baseUrl: string; + content: string; +}) => { content: string }; diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx new file mode 100644 index 0000000000..bd8e4be3cf --- /dev/null +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -0,0 +1,155 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { + Content, + ContentHeader, + MissingAnnotationEmptyState, + Progress, + SupportButton, + WarningPanel, +} from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { + AdrFilePathFilterFn, + ANNOTATION_ADR_LOCATION, + getAdrLocationUrl, + isAdrAvailable, + madrFilePathFilter, +} from '@backstage/plugin-adr-common'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + Grid, + List, + ListItem, + ListItemText, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; + +import { useOctokitRequest } from '../../hooks'; +import { rootRouteRef } from '../../routes'; +import { AdrContentDecorator, AdrReader } from '../AdrReader'; + +const useStyles = makeStyles((theme: Theme) => ({ + adrMenu: { + backgroundColor: theme.palette.background.paper, + }, +})); + +/** + * Component for browsing ADRs on an entity page. + * @public + */ +export const EntityAdrContent = ({ + contentDecorators, + filePathFilterFn, +}: { + contentDecorators?: AdrContentDecorator[]; + filePathFilterFn?: AdrFilePathFilterFn; +}) => { + const classes = useStyles(); + const { entity } = useEntity(); + const rootLink = useRouteRef(rootRouteRef); + const [adrList, setAdrList] = useState([]); + const [searchParams, setSearchParams] = useSearchParams(); + const scmIntegrations = useApi(scmIntegrationsApiRef); + const entityHasAdrs = isAdrAvailable(entity); + + const { value, loading, error } = useOctokitRequest( + getAdrLocationUrl(entity, scmIntegrations), + ); + + const selectedAdr = + adrList.find(adr => adr === searchParams.get('record')) ?? ''; + useEffect(() => { + if (adrList.length && !selectedAdr) { + searchParams.set('record', adrList[0]); + setSearchParams(searchParams, { replace: true }); + } + }); + + useEffect(() => { + if (!value?.data) { + return; + } + + const adrs = value.data + .filter( + (item: { type: string; name: string }) => + item.type === 'file' && + (filePathFilterFn + ? filePathFilterFn(item.name) + : madrFilePathFilter(item.name)), + ) + .map(({ name }: { name: string }) => name); + + setAdrList(adrs); + }, [filePathFilterFn, value]); + + return ( + + + + + + {!entityHasAdrs && ( + + )} + + {loading && } + + {entityHasAdrs && !loading && error && ( + + )} + + {entityHasAdrs && + !loading && + !error && + (adrList.length ? ( + + + + {adrList.map((adr, idx) => ( + + + + ))} + + + + + + + ) : ( + No ADRs found + ))} + + ); +}; diff --git a/plugins/adr/src/components/EntityAdrContent/index.ts b/plugins/adr/src/components/EntityAdrContent/index.ts new file mode 100644 index 0000000000..4552d4e87d --- /dev/null +++ b/plugins/adr/src/components/EntityAdrContent/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { EntityAdrContent } from './EntityAdrContent'; diff --git a/plugins/adr/src/hooks/index.ts b/plugins/adr/src/hooks/index.ts new file mode 100644 index 0000000000..d5aa102fb8 --- /dev/null +++ b/plugins/adr/src/hooks/index.ts @@ -0,0 +1,16 @@ +/* + * 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 * from './useOctokitRequest'; diff --git a/plugins/adr/src/hooks/useOctokitRequest.ts b/plugins/adr/src/hooks/useOctokitRequest.ts new file mode 100644 index 0000000000..f160651169 --- /dev/null +++ b/plugins/adr/src/hooks/useOctokitRequest.ts @@ -0,0 +1,59 @@ +/* + * 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 new file mode 100644 index 0000000000..84012ccb7a --- /dev/null +++ b/plugins/adr/src/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +/** + * ADR frontend plugin + * + * @packageDocumentation + */ +export { isAdrAvailable } from '@backstage/plugin-adr-common'; +export * from './components/AdrReader'; +export { adrPlugin, EntityAdrContent } from './plugin'; +export * from './search'; diff --git a/plugins/adr/src/plugin.test.ts b/plugins/adr/src/plugin.test.ts new file mode 100644 index 0000000000..e311bf98e6 --- /dev/null +++ b/plugins/adr/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { adrPlugin } from './plugin'; + +describe('adr', () => { + it('should export plugin', () => { + expect(adrPlugin).toBeDefined(); + }); +}); diff --git a/plugins/adr/src/plugin.ts b/plugins/adr/src/plugin.ts new file mode 100644 index 0000000000..03437b1f0b --- /dev/null +++ b/plugins/adr/src/plugin.ts @@ -0,0 +1,46 @@ +/* + * 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 { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +/** + * The Backstage plugin that holds ADR specific components + * @public + */ +export const adrPlugin = createPlugin({ + id: 'adr', + routes: { + root: rootRouteRef, + }, +}); + +/** + * An extension for browsing ADRs on an entity page. + * @public + */ +export const EntityAdrContent = adrPlugin.provide( + createRoutableExtension({ + name: 'EntityAdrContent', + component: () => + import('./components/EntityAdrContent').then(m => m.EntityAdrContent), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/adr/src/routes.ts b/plugins/adr/src/routes.ts new file mode 100644 index 0000000000..0ec5c71294 --- /dev/null +++ b/plugins/adr/src/routes.ts @@ -0,0 +1,24 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +/** + * Root route ref for the ADR plugin + * @public + */ +export const rootRouteRef = createRouteRef({ + id: 'adr', +}); diff --git a/plugins/adr/src/search/AdrSearchResultListItem.tsx b/plugins/adr/src/search/AdrSearchResultListItem.tsx new file mode 100644 index 0000000000..8b89629af6 --- /dev/null +++ b/plugins/adr/src/search/AdrSearchResultListItem.tsx @@ -0,0 +1,80 @@ +/* + * 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 TextTruncate from 'react-text-truncate'; +import { + Box, + Chip, + Divider, + ListItem, + ListItemText, + makeStyles, +} from '@material-ui/core'; +import { Link } from '@backstage/core-components'; +import { AdrDocument } from '@backstage/plugin-adr-common'; + +const useStyles = makeStyles({ + flexContainer: { + flexWrap: 'wrap', + }, + itemText: { + width: '100%', + wordBreak: 'break-all', + marginBottom: '1rem', + }, +}); + +/** + * A component to display a ADR search result + * @public + */ +export const AdrSearchResultListItem = ({ + lineClamp = 5, + result, +}: { + lineClamp?: number; + result: AdrDocument; +}) => { + const classes = useStyles(); + + return ( + + + + } + /> + + {result.status && ( + + )} + {result.date && } + + + + + ); +}; diff --git a/plugins/adr/src/search/index.ts b/plugins/adr/src/search/index.ts new file mode 100644 index 0000000000..519d8cb91f --- /dev/null +++ b/plugins/adr/src/search/index.ts @@ -0,0 +1,16 @@ +/* + * 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 * from './AdrSearchResultListItem'; diff --git a/plugins/adr/src/setupTests.ts b/plugins/adr/src/setupTests.ts new file mode 100644 index 0000000000..9bb3e72355 --- /dev/null +++ b/plugins/adr/src/setupTests.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. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index 2754e61e43..6ce1b2240e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6301,6 +6301,11 @@ resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.1.tgz#e34763178b46232e4c5f079f1706e18692415519" integrity sha512-nAPUltOT28fal2eDZz8yyzNhBjHw1NEymFBP7Q9iCShqpflWPybxHbD7pw/46jQmT+HXOy1QN5hNTms8MOTlOQ== +"@types/marked@^4.0.0": + version "4.0.3" + resolved "https://registry.npmjs.org/@types/marked/-/marked-4.0.3.tgz#2098f4a77adaba9ce881c9e0b6baf29116e5acc4" + integrity sha512-HnMWQkLJEf/PnxZIfbm0yGJRRZYYMhb++O9M36UCTA9z53uPvVoSlAwJr3XOpDEryb7Hwl1qAx/MV6YIW1RXxg== + "@types/mdast@^3.0.0", "@types/mdast@^3.0.3": version "3.0.10" resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -18467,7 +18472,7 @@ node-fetch@2.6.1: resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-fetch@2.6.7, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: +node-fetch@2.6.7, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==