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
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog': patch
---
Display systems in catalog table and make both owner and system link to the entity pages.
The owner field is now taken from the relations of the entity instead of its spec.
@@ -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';