Display systems in catalog table and make both owner and system link to the entity pages
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user