Display systems in catalog table and make both owner and system link to the entity pages

This commit is contained in:
Oliver Sand
2021-01-18 15:08:51 +01:00
parent ba59fb6e1d
commit f04db53d73
7 changed files with 287 additions and 25 deletions
@@ -13,7 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import {
Entity,
EntityName,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Table, TableColumn, TableProps } from '@backstage/core';
import { Chip, Link } from '@material-ui/core';
import Edit from '@material-ui/icons/Edit';
@@ -22,20 +27,31 @@ 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 { createEditLink } from '../createEditLink';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import { entityRoute, entityRouteParams } from '../../routes';
import { createEditLink } from '../createEditLink';
import { EntityRefLink, formatEntityRefTitle } from '../EntityRefLink';
import {
favouriteEntityIcon,
favouriteEntityTooltip,
} from '../FavouriteEntity/FavouriteEntity';
import { getEntityRelations } from '../getEntityRelations';
const columns: TableColumn<Entity>[] = [
type EntityRow = Entity & {
row: {
partOfSystemRelationTitle?: string;
partOfSystemRelation?: EntityName;
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
};
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'metadata.name',
highlight: true,
render: (entity: any) => (
render: entity => (
<Link
component={RouterLink}
to={generatePath(entityRoute.path, {
@@ -47,9 +63,33 @@ const columns: TableColumn<Entity>[] = [
</Link>
),
},
{
title: 'System',
field: 'row.partOfSystemRelationTitle',
render: entity => (
<>
{entity.row.partOfSystemRelation && (
<EntityRefLink
entityRef={entity.row.partOfSystemRelation}
defaultKind="system"
/>
)}
</>
),
},
{
title: 'Owner',
field: 'spec.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>
))}
</>
),
},
{
title: 'Lifecycle',
@@ -65,7 +105,7 @@ const columns: TableColumn<Entity>[] = [
cellStyle: {
padding: '0px 16px 0px 20px',
},
render: (entity: Entity) => (
render: entity => (
<>
{entity.metadata.tags &&
entity.metadata.tags.map(t => (
@@ -141,8 +181,31 @@ export const CatalogTable = ({
},
];
const rows = entities.map(e => {
const [partOfSystemRelation] = getEntityRelations(e, RELATION_PART_OF, {
kind: 'system',
});
const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY);
return {
...e,
row: {
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle: partOfSystemRelation
? formatEntityRefTitle(partOfSystemRelation, {
defaultKind: 'system',
})
: undefined,
partOfSystemRelation,
},
};
});
return (
<Table<Entity>
<Table<EntityRow>
isLoading={loading}
columns={columns}
options={{
@@ -155,7 +218,7 @@ export const CatalogTable = ({
pageSizeOptions: [20, 50, 100],
}}
title={`${titlePreamble} (${(entities && entities.length) || 0})`}
data={entities}
data={rows}
actions={actions}
/>
);
@@ -37,7 +37,10 @@ describe('<EntityRefLink />', () => {
wrapper: MemoryRouter,
});
expect(getByText('component:software')).toBeInTheDocument();
expect(getByText('component:software')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
});
it('renders link for entity in other namespace', () => {
@@ -57,7 +60,10 @@ describe('<EntityRefLink />', () => {
const { getByText } = render(<EntityRefLink entityRef={entity} />, {
wrapper: MemoryRouter,
});
expect(getByText('component:test/software')).toBeInTheDocument();
expect(getByText('component:test/software')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
});
it('renders link for entity and hides default kind', () => {
@@ -80,7 +86,10 @@ describe('<EntityRefLink />', () => {
wrapper: MemoryRouter,
},
);
expect(getByText('test/software')).toBeInTheDocument();
expect(getByText('test/software')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
});
it('renders link for entity name in default namespace', () => {
@@ -92,7 +101,10 @@ describe('<EntityRefLink />', () => {
const { getByText } = render(<EntityRefLink entityRef={entityName} />, {
wrapper: MemoryRouter,
});
expect(getByText('component:software')).toBeInTheDocument();
expect(getByText('component:software')).toHaveAttribute(
'href',
'/catalog/default/component/software',
);
});
it('renders link for entity name in other namespace', () => {
@@ -104,7 +116,10 @@ describe('<EntityRefLink />', () => {
const { getByText } = render(<EntityRefLink entityRef={entityName} />, {
wrapper: MemoryRouter,
});
expect(getByText('component:test/software')).toBeInTheDocument();
expect(getByText('component:test/software')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
});
it('renders link for entity name and hides default kind', () => {
@@ -119,6 +134,29 @@ describe('<EntityRefLink />', () => {
wrapper: MemoryRouter,
},
);
expect(getByText('test/software')).toBeInTheDocument();
expect(getByText('test/software')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
});
it('renders link with custom children', () => {
const entityName = {
kind: 'Component',
namespace: 'test',
name: 'software',
};
const { getByText } = render(
<EntityRefLink entityRef={entityName} defaultKind="component">
Custom Children
</EntityRefLink>,
{
wrapper: MemoryRouter,
},
);
expect(getByText('Custom Children')).toHaveAttribute(
'href',
'/catalog/test/component/software',
);
});
});
@@ -17,17 +17,18 @@ 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';
import { formatEntityRefTitle } from './format';
type EntityRefLinkProps = {
entityRef: Entity | EntityName;
defaultKind?: string;
children?: React.ReactNode;
};
// TODO: This component is private for now, as it should probably belong into
@@ -36,6 +37,7 @@ type EntityRefLinkProps = {
export const EntityRefLink = ({
entityRef,
defaultKind,
children,
}: EntityRefLinkProps) => {
let kind;
let namespace;
@@ -51,17 +53,8 @@ export const EntityRefLink = ({
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,
@@ -74,7 +67,8 @@ export const EntityRefLink = ({
component={RouterLink}
to={generatePath(`/catalog/${entityRoute.path}`, routeParams)}
>
{title}
{children}
{!children && formatEntityRefTitle(entityRef, { defaultKind })}
</Link>
);
};
@@ -0,0 +1,106 @@
/*
* 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 { formatEntityRefTitle } from './format';
describe('formatEntityRefTitle', () => {
it('formats entity in default namespace', () => {
const entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
},
spec: {
owner: 'guest',
type: 'service',
lifecycle: 'production',
},
};
const title = formatEntityRefTitle(entity);
expect(title).toEqual('component:software');
});
it('formats entity in other namespace', () => {
const entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
namespace: 'test',
},
spec: {
owner: 'guest',
type: 'service',
lifecycle: 'production',
},
};
const title = formatEntityRefTitle(entity);
expect(title).toEqual('component:test/software');
});
it('formats entity and hides default kind', () => {
const entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
namespace: 'test',
},
spec: {
owner: 'guest',
type: 'service',
lifecycle: 'production',
},
};
const title = formatEntityRefTitle(entity, { defaultKind: 'Component' });
expect(title).toEqual('test/software');
});
it('formats entity name in default namespace', () => {
const entityName = {
kind: 'Component',
namespace: 'default',
name: 'software',
};
const title = formatEntityRefTitle(entityName);
expect(title).toEqual('component:software');
});
it('formats entity name in other namespace', () => {
const entityName = {
kind: 'Component',
namespace: 'test',
name: 'software',
};
const title = formatEntityRefTitle(entityName);
expect(title).toEqual('component:test/software');
});
it('renders link for entity name and hides default kind', () => {
const entityName = {
kind: 'Component',
namespace: 'test',
name: 'software',
};
const title = formatEntityRefTitle(entityName, {
defaultKind: 'component',
});
expect(title).toEqual('test/software');
});
});
@@ -0,0 +1,54 @@
/*
* 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';
export function formatEntityRefTitle(
entityRef: Entity | EntityName,
opts?: { defaultKind?: string },
) {
const defaultKind = opts?.defaultKind;
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();
return `${serializeEntityRef({
kind: defaultKind && defaultKind.toLowerCase() === kind ? undefined : kind,
name,
namespace,
})}`;
}
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { EntityRefLink } from './EntityRefLink';
export { formatEntityRefTitle } from './format';