Add EntityRefLinks that shows one or multiple entity ref links.

Add `EntityRefLinks` that shows one or multiple entity ref links.

Change the about card and catalog table to use `EntityRefLinks` due to the
nature of relations to support multiple relations per type.

This simplifies usage of relations and support to visualize edge-cases, e.g. duplicate data ingested into the catalog, like two ownerOf relationships for the same component.
This commit is contained in:
Oliver Sand
2021-01-25 17:37:55 +01:00
parent 2dbb61a418
commit 0b11823463
6 changed files with 155 additions and 62 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-catalog': patch
---
Add `EntityRefLinks` that shows one or multiple entity ref links.
Change the about card and catalog table to use `EntityRefLinks` due to the
nature of relations to support multiple relations per type.
@@ -21,7 +21,7 @@ import {
} from '@backstage/catalog-model';
import { Chip, Grid, makeStyles, Typography } from '@material-ui/core';
import React from 'react';
import { EntityRefLink } from '../EntityRefLink';
import { EntityRefLinks } from '../EntityRefLink';
import { getEntityRelations } from '../getEntityRelations';
import { AboutField } from './AboutField';
@@ -41,17 +41,17 @@ export const AboutContent = ({ entity }: Props) => {
const isDomain = entity.kind.toLowerCase() === 'domain';
const isResource = entity.kind.toLowerCase() === 'resource';
const isComponent = entity.kind.toLowerCase() === 'component';
const [partOfSystemRelation] = getEntityRelations(entity, RELATION_PART_OF, {
const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, {
kind: 'system',
});
const [partOfComponentRelation] = getEntityRelations(
const partOfComponentRelations = getEntityRelations(
entity,
RELATION_PART_OF,
{
kind: 'component',
},
);
const [partOfDomainRelation] = getEntityRelations(entity, RELATION_PART_OF, {
const partOfDomainRelations = getEntityRelations(entity, RELATION_PART_OF, {
kind: 'domain',
});
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
@@ -64,12 +64,7 @@ export const AboutContent = ({ entity }: Props) => {
</Typography>
</AboutField>
<AboutField label="Owner" gridSizes={{ xs: 12, sm: 6, lg: 4 }}>
{ownedByRelations.map((t, i) => (
<React.Fragment key={i}>
{i > 0 && ', '}
<EntityRefLink entityRef={t} defaultKind="group" />
</React.Fragment>
))}
<EntityRefLinks entityRefs={ownedByRelations} />
</AboutField>
{isSystem && (
<AboutField
@@ -77,12 +72,10 @@ export const AboutContent = ({ entity }: Props) => {
value="No Domain"
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
>
{partOfDomainRelation && (
<EntityRefLink
entityRef={partOfDomainRelation}
defaultKind="domain"
/>
)}
<EntityRefLinks
entityRefs={partOfDomainRelations}
defaultKind="domain"
/>
</AboutField>
)}
{!isSystem && !isDomain && (
@@ -91,22 +84,20 @@ export const AboutContent = ({ entity }: Props) => {
value="No System"
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
>
{partOfSystemRelation && (
<EntityRefLink
entityRef={partOfSystemRelation}
defaultKind="system"
/>
)}
<EntityRefLinks
entityRefs={partOfSystemRelations}
defaultKind="system"
/>
</AboutField>
)}
{isComponent && partOfComponentRelation && (
{isComponent && partOfComponentRelations.length > 0 && (
<AboutField
label="Parent Component"
value="No Parent Component"
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
>
<EntityRefLink
entityRef={partOfComponentRelation}
<EntityRefLinks
entityRefs={partOfComponentRelations}
defaultKind="component"
/>
</AboutField>
@@ -20,17 +20,19 @@ import {
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Table, TableColumn, TableProps } from '@backstage/core';
import { Chip, Link } from '@material-ui/core';
import { Chip } from '@material-ui/core';
import Edit from '@material-ui/icons/Edit';
import OpenInNew from '@material-ui/icons/OpenInNew';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
import { findLocationForEntityMeta } from '../../data/utils';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import { entityRoute, entityRouteParams } from '../../routes';
import { createEditLink } from '../createEditLink';
import { EntityRefLink, formatEntityRefTitle } from '../EntityRefLink';
import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
} from '../EntityRefLink';
import {
favouriteEntityIcon,
favouriteEntityTooltip,
@@ -40,7 +42,7 @@ import { getEntityRelations } from '../getEntityRelations';
type EntityRow = Entity & {
row: {
partOfSystemRelationTitle?: string;
partOfSystemRelation?: EntityName;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
@@ -52,43 +54,27 @@ const columns: TableColumn<EntityRow>[] = [
field: 'metadata.name',
highlight: true,
render: entity => (
<Link
component={RouterLink}
to={generatePath(entityRoute.path, {
...entityRouteParams(entity),
selectedTabId: 'overview',
})}
>
{entity.metadata.name}
</Link>
<EntityRefLink entityRef={entity}>{entity.metadata.name}</EntityRefLink>
),
},
{
title: 'System',
field: 'row.partOfSystemRelationTitle',
render: entity => (
<>
{entity.row.partOfSystemRelation && (
<EntityRefLink
entityRef={entity.row.partOfSystemRelation}
defaultKind="system"
/>
)}
</>
<EntityRefLinks
entityRefs={entity.row.partOfSystemRelations}
defaultKind="system"
/>
),
},
{
title: 'Owner',
field: 'row.ownedByRelationsTitle',
render: entity => (
<>
{entity.row.ownedByRelations.map((t, i) => (
<React.Fragment key={i}>
{i > 0 && ', '}
<EntityRefLink entityRef={t} defaultKind="group" />
</React.Fragment>
))}
</>
<EntityRefLinks
entityRefs={entity.row.ownedByRelations}
defaultKind="group"
/>
),
},
{
@@ -182,7 +168,7 @@ export const CatalogTable = ({
];
const rows = entities.map(e => {
const [partOfSystemRelation] = getEntityRelations(e, RELATION_PART_OF, {
const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, {
kind: 'system',
});
const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY);
@@ -194,12 +180,13 @@ export const CatalogTable = ({
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle: partOfSystemRelation
? formatEntityRefTitle(partOfSystemRelation, {
defaultKind: 'system',
})
: undefined,
partOfSystemRelation,
partOfSystemRelationTitle:
partOfSystemRelations.length > 0
? formatEntityRefTitle(partOfSystemRelations[0], {
defaultKind: 'system',
})
: undefined,
partOfSystemRelations,
},
};
});
@@ -0,0 +1,66 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router';
import { EntityRefLinks } from './EntityRefLinks';
describe('<EntityRefLinks />', () => {
it('renders a single link', () => {
const entityNames = [
{
kind: 'Component',
namespace: 'default',
name: 'software',
},
];
const { getByText } = render(<EntityRefLinks entityRefs={entityNames} />, {
wrapper: MemoryRouter,
});
expect(getByText('component:software')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
});
it('renders multiple links', () => {
const entityNames = [
{
kind: 'Component',
namespace: 'default',
name: 'software',
},
{
kind: 'API',
namespace: 'default',
name: 'interface',
},
];
const { getByText } = render(<EntityRefLinks entityRefs={entityNames} />, {
wrapper: MemoryRouter,
});
expect(getByText(',')).toBeInTheDocument();
expect(getByText('component:software')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
expect(getByText('api:interface')).toHaveAttribute(
'href',
'/catalog/default/api/interface',
);
});
});
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Spotify AB
*
* 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, EntityName } from '@backstage/catalog-model';
import React from 'react';
import { EntityRefLink } from './EntityRefLink';
type EntityRefLinksProps = {
entityRefs: (Entity | EntityName)[];
defaultKind?: string;
};
// TODO: Move into a shared helper package
export const EntityRefLinks = ({
entityRefs,
defaultKind,
}: EntityRefLinksProps) => {
return (
<>
{entityRefs.map((r, i) => (
<React.Fragment key={i}>
{i > 0 && ', '}
<EntityRefLink entityRef={r} defaultKind={defaultKind} />
</React.Fragment>
))}
</>
);
};
@@ -14,4 +14,5 @@
* limitations under the License.
*/
export { EntityRefLink } from './EntityRefLink';
export { EntityRefLinks } from './EntityRefLinks';
export { formatEntityRefTitle } from './format';