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:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user