feat(ADR): implement ADR plugin
Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user