diff --git a/.changeset/healthy-comics-drive.md b/.changeset/healthy-comics-drive.md new file mode 100644 index 0000000000..4b532dc853 --- /dev/null +++ b/.changeset/healthy-comics-drive.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Display the owner, system, and domain as links to the entity pages in the about card. +Only display fields in the about card that are applicable to the entity kind. diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index b8d5d84316..178debb42a 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -14,15 +14,16 @@ * limitations under the License. */ -import React from 'react'; -import { Grid, Typography, Chip, makeStyles } from '@material-ui/core'; -import { AboutField } from './AboutField'; import { Entity, - ENTITY_DEFAULT_NAMESPACE, RELATION_OWNED_BY, - serializeEntityRef, + RELATION_PART_OF, } from '@backstage/catalog-model'; +import { Chip, Grid, makeStyles, Typography } from '@material-ui/core'; +import React from 'react'; +import { EntityRefLink } from '../EntityRefLink'; +import { getEntityRelations } from '../getEntityRelations'; +import { AboutField } from './AboutField'; const useStyles = makeStyles({ description: { @@ -36,6 +37,17 @@ type Props = { export const AboutContent = ({ entity }: Props) => { const classes = useStyles(); + const isSystem = entity.kind.toLowerCase() === 'system'; + const isDomain = entity.kind.toLowerCase() === 'domain'; + const isResource = entity.kind.toLowerCase() === 'resource'; + const [partOfSystemRelation] = getEntityRelations(entity, RELATION_PART_OF, { + kind: 'system', + }); + const [partOfDomainRelation] = getEntityRelations(entity, RELATION_PART_OF, { + kind: 'domain', + }); + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + return ( @@ -43,32 +55,56 @@ export const AboutContent = ({ entity }: Props) => { {entity?.metadata?.description || 'No description'} - r.type === RELATION_OWNED_BY) - .map(({ target: { kind, name, namespace } }) => - // TODO(Rugvip): we want to provide some utils for this - serializeEntityRef({ - kind, - name, - namespace: - namespace === ENTITY_DEFAULT_NAMESPACE ? undefined : namespace, - }), - ) - .join(', ')} - gridSizes={{ xs: 12, sm: 6, lg: 4 }} - /> - - + + {ownedByRelations.map((t, i) => ( + + {i > 0 && ', '} + + + ))} + + {isSystem && ( + + {partOfDomainRelation && ( + + )} + + )} + {!isSystem && !isDomain && ( + + {partOfSystemRelation && ( + + )} + + )} + {!isSystem && !isDomain && ( + + )} + {!isSystem && !isDomain && !isResource && ( + + )} ', () => { + it('renders link for entity in default namespace', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const { getByText } = render(, { + wrapper: MemoryRouter, + }); + + expect(getByText('component:software')).toBeInTheDocument(); + }); + + it('renders link for entity in other namespace', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + namespace: 'test', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const { getByText } = render(, { + wrapper: MemoryRouter, + }); + expect(getByText('component:test/software')).toBeInTheDocument(); + }); + + it('renders link for entity and hides default kind', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + namespace: 'test', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const { getByText } = render( + , + { + wrapper: MemoryRouter, + }, + ); + expect(getByText('test/software')).toBeInTheDocument(); + }); + + it('renders link for entity name in default namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'default', + name: 'software', + }; + const { getByText } = render(, { + wrapper: MemoryRouter, + }); + expect(getByText('component:software')).toBeInTheDocument(); + }); + + it('renders link for entity name in other namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'test', + name: 'software', + }; + const { getByText } = render(, { + wrapper: MemoryRouter, + }); + expect(getByText('component:test/software')).toBeInTheDocument(); + }); + + it('renders link for entity name and hides default kind', () => { + const entityName = { + kind: 'Component', + namespace: 'test', + name: 'software', + }; + const { getByText } = render( + , + { + wrapper: MemoryRouter, + }, + ); + expect(getByText('test/software')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog/src/components/EntityRefLink/EntityRefLink.tsx new file mode 100644 index 0000000000..a06ba69fbe --- /dev/null +++ b/plugins/catalog/src/components/EntityRefLink/EntityRefLink.tsx @@ -0,0 +1,80 @@ +/* + * 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, + ENTITY_DEFAULT_NAMESPACE, + serializeEntityRef, +} from '@backstage/catalog-model'; +import { Link } from '@material-ui/core'; +import React from 'react'; +import { generatePath } from 'react-router'; +import { Link as RouterLink } from 'react-router-dom'; +import { entityRoute } from '../../routes'; + +type EntityRefLinkProps = { + entityRef: Entity | EntityName; + defaultKind?: string; +}; + +// TODO: This component is private for now, as it should probably belong into +// some kind of helper module for the catalog plugin to avoid a dependency on +// the catalog plugin itself. +export const EntityRefLink = ({ + entityRef, + defaultKind, +}: EntityRefLinkProps) => { + let kind; + let namespace; + let name; + + if ('metadata' in entityRef) { + kind = entityRef.kind; + namespace = entityRef.metadata.namespace; + name = entityRef.metadata.name; + } else { + kind = entityRef.kind; + namespace = entityRef.namespace; + name = entityRef.name; + } + + if (namespace === ENTITY_DEFAULT_NAMESPACE) { + namespace = undefined; + } + + kind = kind.toLowerCase(); + + const title = `${serializeEntityRef({ + kind: defaultKind && defaultKind.toLowerCase() === kind ? undefined : kind, + name, + namespace, + })}`; + const routeParams = { + kind, + namespace: namespace?.toLowerCase() ?? ENTITY_DEFAULT_NAMESPACE, + name, + }; + + // TODO: Use useRouteRef here to generate the path + return ( + + {title} + + ); +}; diff --git a/plugins/catalog/src/components/EntityRefLink/index.ts b/plugins/catalog/src/components/EntityRefLink/index.ts new file mode 100644 index 0000000000..aa2c6641ef --- /dev/null +++ b/plugins/catalog/src/components/EntityRefLink/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export { EntityRefLink } from './EntityRefLink';