feat(ADR): implement ADR plugin
Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-adr': minor
|
||||
'@backstage/plugin-adr-backend': minor
|
||||
'@backstage/plugin-adr-common': minor
|
||||
---
|
||||
|
||||
Implement ADR plugin
|
||||
@@ -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'
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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,
|
||||
...
|
||||
})
|
||||
```
|
||||
@@ -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
|
||||
/// <reference types="node" />
|
||||
|
||||
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<AdrDocument>;
|
||||
|
||||
// @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<AdrDocument>;
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
options: AdrCollatorFactoryOptions,
|
||||
): DefaultAdrCollatorFactory;
|
||||
// (undocumented)
|
||||
getCollator(): Promise<Readable>;
|
||||
// (undocumented)
|
||||
readonly type: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type MadrParserOptions = {
|
||||
locationTemplate?: string;
|
||||
dateFormat?: string;
|
||||
};
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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<AdrDocument> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, string>,
|
||||
): 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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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<AdrDocument>;
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,3 @@
|
||||
# ADR Common
|
||||
|
||||
Common types and functionalities for the ADR plugin.
|
||||
@@ -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;
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -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 = (
|
||||
<EntityLayout>
|
||||
{/* other tabs... */}
|
||||
<EntityLayout.Route if={isAdrAvailable} path="/adrs" title="ADRs">
|
||||
<EntityAdrContent />
|
||||
</EntityLayout.Route>
|
||||
</EntityLayout>
|
||||
```
|
||||
|
||||
2. Add `backstage.io/adr-location` annotation to your `catalog-info.yaml`:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
annotations:
|
||||
backstage.io/adr-location: <RELATIVE_PATH_TO_ADR_FILES_DIR>
|
||||
```
|
||||
|
||||
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 (
|
||||
<AdrSearchResultListItem
|
||||
key={document.location}
|
||||
result={document}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
## 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);
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
<EntityAdrContent filePathFilterFn={myCustomFilterFn} />
|
||||
```
|
||||
|
||||
### 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` => `<ABSOLUTE_ADR_DIR_URL>/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) };
|
||||
};
|
||||
|
||||
...
|
||||
|
||||
<EntityAdrContent contentDecorators={[
|
||||
AdrReader.decorators.createRewriteRelativeLinksDecorator(),
|
||||
AdrReader.decorators.createRewriteRelativeEmbedsDecorator(),
|
||||
myCustomDecorator,
|
||||
]}
|
||||
/>
|
||||
```
|
||||
@@ -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
|
||||
/// <reference types="react" />
|
||||
|
||||
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<undefined>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
|
||||
// @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 };
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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 (
|
||||
<InfoCard>
|
||||
{loading && <Progress />}
|
||||
|
||||
{!loading && error && (
|
||||
<WarningPanel title="Failed to fetch ADR" message={error?.message} />
|
||||
)}
|
||||
|
||||
{!loading && !error && value?.data && (
|
||||
<MarkdownContent content={adrContent} linkTarget="_blank" />
|
||||
)}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
AdrReader.decorators = adrDecoratorFactories;
|
||||
@@ -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,
|
||||
``,
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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 };
|
||||
@@ -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<string[]>([]);
|
||||
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 (
|
||||
<Content>
|
||||
<ContentHeader title="Architecture Decision Records">
|
||||
<SupportButton />
|
||||
</ContentHeader>
|
||||
|
||||
{!entityHasAdrs && (
|
||||
<MissingAnnotationEmptyState annotation={ANNOTATION_ADR_LOCATION} />
|
||||
)}
|
||||
|
||||
{loading && <Progress />}
|
||||
|
||||
{entityHasAdrs && !loading && error && (
|
||||
<WarningPanel title="Failed to fetch ADRs" message={error?.message} />
|
||||
)}
|
||||
|
||||
{entityHasAdrs &&
|
||||
!loading &&
|
||||
!error &&
|
||||
(adrList.length ? (
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={3}>
|
||||
<List className={classes.adrMenu} dense>
|
||||
{adrList.map((adr, idx) => (
|
||||
<ListItem
|
||||
key={idx}
|
||||
button
|
||||
component={Link}
|
||||
to={`${rootLink()}?record=${adr}`}
|
||||
selected={selectedAdr === adr}
|
||||
>
|
||||
<ListItemText
|
||||
secondaryTypographyProps={{ noWrap: true }}
|
||||
secondary={adr.replace(/\.md$/, '')}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<AdrReader adr={selectedAdr} decorators={contentDecorators} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography>No ADRs found</Typography>
|
||||
))}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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<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]);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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 (
|
||||
<Link to={result.location}>
|
||||
<ListItem alignItems="flex-start" className={classes.flexContainer}>
|
||||
<ListItemText
|
||||
className={classes.itemText}
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={result.title}
|
||||
secondary={
|
||||
<TextTruncate
|
||||
line={lineClamp}
|
||||
truncateText="…"
|
||||
text={result.text}
|
||||
element="span"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
{result.status && (
|
||||
<Chip label={`Status: ${result.status}`} size="small" />
|
||||
)}
|
||||
{result.date && <Chip label={`Date: ${result.date}`} size="small" />}
|
||||
</Box>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user