feat(ADR): implement ADR plugin

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2022-04-13 14:39:17 -04:00
parent 1cb9cec16b
commit e73075a301
37 changed files with 1679 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+126
View File
@@ -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,
]}
/>
```
+64
View File
@@ -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 };
```
+62
View File
@@ -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,
`![$1](${baseUrl}/$2$3$4)`,
),
});
},
});
@@ -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';
+16
View File
@@ -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]);
};
+24
View File
@@ -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';
+22
View File
@@ -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();
});
});
+46
View File
@@ -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,
}),
);
+24
View File
@@ -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>
);
};
+16
View File
@@ -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';
+17
View File
@@ -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';