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';