diff --git a/.changeset/forty-jobs-occur.md b/.changeset/forty-jobs-occur.md new file mode 100644 index 0000000000..b1620a7535 --- /dev/null +++ b/.changeset/forty-jobs-occur.md @@ -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. diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index b2d029c927..82652b3986 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -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) => { - {ownedByRelations.map((t, i) => ( - - {i > 0 && ', '} - - - ))} + {isSystem && ( { value="No Domain" gridSizes={{ xs: 12, sm: 6, lg: 4 }} > - {partOfDomainRelation && ( - - )} + )} {!isSystem && !isDomain && ( @@ -91,22 +84,20 @@ export const AboutContent = ({ entity }: Props) => { value="No System" gridSizes={{ xs: 12, sm: 6, lg: 4 }} > - {partOfSystemRelation && ( - - )} + )} - {isComponent && partOfComponentRelation && ( + {isComponent && partOfComponentRelations.length > 0 && ( - diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 8741cb6157..18c61c2a04 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -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[] = [ field: 'metadata.name', highlight: true, render: entity => ( - - {entity.metadata.name} - + {entity.metadata.name} ), }, { title: 'System', field: 'row.partOfSystemRelationTitle', render: entity => ( - <> - {entity.row.partOfSystemRelation && ( - - )} - + ), }, { title: 'Owner', field: 'row.ownedByRelationsTitle', render: entity => ( - <> - {entity.row.ownedByRelations.map((t, i) => ( - - {i > 0 && ', '} - - - ))} - + ), }, { @@ -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, }, }; }); diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx b/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx new file mode 100644 index 0000000000..9bc26b0954 --- /dev/null +++ b/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.test.tsx @@ -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('', () => { + it('renders a single link', () => { + const entityNames = [ + { + kind: 'Component', + namespace: 'default', + name: 'software', + }, + ]; + const { getByText } = render(, { + 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(, { + 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', + ); + }); +}); diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx b/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx new file mode 100644 index 0000000000..3c8beaec6f --- /dev/null +++ b/plugins/catalog/src/components/EntityRefLink/EntityRefLinks.tsx @@ -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) => ( + + {i > 0 && ', '} + + + ))} + + ); +}; diff --git a/plugins/catalog/src/components/EntityRefLink/index.ts b/plugins/catalog/src/components/EntityRefLink/index.ts index 76a7da38c5..9e6e440514 100644 --- a/plugins/catalog/src/components/EntityRefLink/index.ts +++ b/plugins/catalog/src/components/EntityRefLink/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ export { EntityRefLink } from './EntityRefLink'; +export { EntityRefLinks } from './EntityRefLinks'; export { formatEntityRefTitle } from './format';