Use EntityTable everywhere

This commit is contained in:
Oliver Sand
2021-02-12 10:40:42 +01:00
parent 6141ec38fb
commit bd27885876
19 changed files with 237 additions and 616 deletions
+1 -1
View File
@@ -11,6 +11,6 @@ Introduce new cards to `@backstage/plugin-catalog` that can be added to entity p
- `EntityHasSubcomponentsCard` to display subcomponents of a subcomponent.
- In addition, `EntityHasApisCard` to display APIs of a system is added to `@backstage/plugin-api-docs`.
`@backstage/plugin-catalog-react` now provides `ComponentsTable` and `SystemsTable` to build own cards for components and systems.
`@backstage/plugin-catalog-react` now provides an `EntityTable` to build own cards for entities.
The styling of the tables and new cards was also applied to the existing `EntityConsumedApisCard`,
`EntityConsumingComponentsCard`, `EntityProvidedApisCard`, and `EntityProvidingComponentsCard`.
@@ -1,167 +0,0 @@
/*
* 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 {
ApiEntity,
EntityName,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Table, TableColumn } from '@backstage/core';
import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { makeStyles } from '@material-ui/core';
import React from 'react';
import { ApiTypeTitle } from '../ApiDefinitionCard';
type EntityRow = {
entity: ApiEntity;
resolved: {
name: string;
partOfSystemRelationTitle?: string;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
};
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'resolved.name',
highlight: true,
render: ({ entity }) => (
<EntityRefLink entityRef={entity} defaultKind="API" />
),
},
{
title: 'System',
field: 'resolved.partOfSystemRelationTitle',
render: ({ resolved }) => (
<EntityRefLinks
entityRefs={resolved.partOfSystemRelations}
defaultKind="system"
/>
),
},
{
title: 'Owner',
field: 'resolved.ownedByRelationsTitle',
render: ({ resolved }) => (
<EntityRefLinks
entityRefs={resolved.ownedByRelations}
defaultKind="group"
/>
),
},
{
title: 'Lifecycle',
field: 'entity.spec.lifecycle',
},
{
title: 'Type',
field: 'entity.spec.type',
render: ({ entity }) => <ApiTypeTitle apiEntity={entity} />,
},
{
title: 'Description',
field: 'entity.metadata.description',
width: 'auto',
},
];
type Props = {
title: string;
variant?: string;
entities: ApiEntity[];
emptyComponent?: JSX.Element;
};
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
export const ApisTable = ({
entities,
title,
emptyComponent,
variant = 'gridItem',
}: Props) => {
const classes = useStyles();
const tableStyle: React.CSSProperties = {
minWidth: '0',
width: '100%',
};
if (variant === 'gridItem') {
tableStyle.height = 'calc(100% - 10px)';
}
const rows = entities.map(entity => {
const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, {
kind: 'system',
});
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
return {
entity,
resolved: {
name: formatEntityRefTitle(entity, {
defaultKind: 'API',
}),
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle: partOfSystemRelations
.map(r =>
formatEntityRefTitle(r, {
defaultKind: 'system',
}),
)
.join(', '),
partOfSystemRelations,
},
};
});
return (
<Table<EntityRow>
columns={columns}
title={title}
style={tableStyle}
emptyComponent={
emptyComponent && <div className={classes.empty}>{emptyComponent}</div>
}
options={{
// TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
search: false,
paging: false,
actionsColumnIndex: -1,
padding: 'dense',
}}
data={rows}
/>
);
};
@@ -26,9 +26,13 @@ import {
Progress,
WarningPanel,
} from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import {
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
import { ApisTable } from './ApisTable';
import { apiEntityColumns } from './presets';
const ApisCard = ({
children,
@@ -74,7 +78,7 @@ export const ConsumedApisCard = ({ variant = 'gridItem' }: Props) => {
}
return (
<ApisTable
<EntityTable
title="Consumed APIs"
variant={variant}
emptyComponent={
@@ -85,6 +89,7 @@ export const ConsumedApisCard = ({ variant = 'gridItem' }: Props) => {
</Link>
</div>
}
columns={apiEntityColumns}
entities={entities as ApiEntity[]}
/>
);
@@ -22,9 +22,13 @@ import {
Progress,
WarningPanel,
} from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import {
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
import { ApisTable } from './ApisTable';
import { apiEntityColumns } from './presets';
const ApisCard = ({
children,
@@ -69,7 +73,7 @@ export const HasApisCard = ({ variant = 'gridItem' }: Props) => {
}
return (
<ApisTable
<EntityTable
title="APIs"
variant={variant}
emptyComponent={
@@ -80,6 +84,7 @@ export const HasApisCard = ({ variant = 'gridItem' }: Props) => {
</Link>
</div>
}
columns={apiEntityColumns}
entities={entities as ApiEntity[]}
/>
);
@@ -26,9 +26,13 @@ import {
Progress,
WarningPanel,
} from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import {
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
import { ApisTable } from './ApisTable';
import { apiEntityColumns } from './presets';
const ApisCard = ({
children,
@@ -74,7 +78,7 @@ export const ProvidedApisCard = ({ variant = 'gridItem' }: Props) => {
}
return (
<ApisTable
<EntityTable
title="Provided APIs"
variant={variant}
emptyComponent={
@@ -85,6 +89,7 @@ export const ProvidedApisCard = ({ variant = 'gridItem' }: Props) => {
</Link>
</div>
}
columns={apiEntityColumns}
entities={entities as ApiEntity[]}
/>
);
@@ -0,0 +1,42 @@
/*
* 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 { ApiEntity } from '@backstage/catalog-model';
import { TableColumn } from '@backstage/core';
import { EntityTable } from '@backstage/plugin-catalog-react';
import React from 'react';
import { ApiTypeTitle } from '../ApiDefinitionCard';
export function createSpecApiTypeColumn(): TableColumn<ApiEntity> {
return {
title: 'Type',
field: 'spec.type',
render: entity => <ApiTypeTitle apiEntity={entity} />,
};
}
// TODO: This could be moved to plugin-catalog-react if we wouldn't have a
// special createSpecApiTypeColumn. But this is requited to use ApiTypeTitle to
// resolve the display name of an entity. Is the display name really worth it?
export const apiEntityColumns: TableColumn<ApiEntity>[] = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'API' }),
EntityTable.columns.createSystemColumn(),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createSpecLifecycleColumn(),
createSpecApiTypeColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
@@ -27,11 +27,10 @@ import {
WarningPanel,
} from '@backstage/core';
import {
ComponentsTable,
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
const ComponentsCard = ({
@@ -78,7 +77,7 @@ export const ConsumingComponentsCard = ({ variant = 'gridItem' }: Props) => {
}
return (
<ComponentsTable
<EntityTable
title="Consumers"
variant={variant}
emptyComponent={
@@ -89,6 +88,7 @@ export const ConsumingComponentsCard = ({ variant = 'gridItem' }: Props) => {
</Link>
</div>
}
columns={EntityTable.componentEntityColumns}
entities={entities as ComponentEntity[]}
/>
);
@@ -27,7 +27,7 @@ import {
WarningPanel,
} from '@backstage/core';
import {
ComponentsTable,
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
@@ -77,7 +77,7 @@ export const ProvidingComponentsCard = ({ variant = 'gridItem' }: Props) => {
}
return (
<ComponentsTable
<EntityTable
title="Providers"
variant={variant}
emptyComponent={
@@ -88,6 +88,7 @@ export const ProvidingComponentsCard = ({ variant = 'gridItem' }: Props) => {
</Link>
</div>
}
columns={EntityTable.componentEntityColumns}
entities={entities as ComponentEntity[]}
/>
);
@@ -15,6 +15,7 @@
*/
import {
Entity,
RELATION_OWNED_BY,
RELATION_PART_OF,
SystemEntity,
@@ -22,70 +23,51 @@ import {
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { SystemsTable } from './SystemsTable';
import { EntityTable } from './EntityTable';
describe('<SystemsTable />', () => {
describe('<EntityTable />', () => {
it('shows empty table', async () => {
const { getByText } = await renderInTestApp(
<SystemsTable
title="My Systems"
<EntityTable
title="Entities"
entities={[]}
emptyComponent={<div>EMPTY</div>}
columns={[]}
/>,
);
expect(getByText('My Systems')).toBeInTheDocument();
expect(getByText('Entities')).toBeInTheDocument();
expect(getByText('EMPTY')).toBeInTheDocument();
});
it('shows systems', async () => {
const entities: SystemEntity[] = [
it('shows entities', async () => {
const entities: Entity[] = [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'System',
metadata: {
name: 'my-system',
namespace: 'my-namespace',
description: 'Some description',
name: 'my-entity',
},
spec: {
owner: 'owner-data',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'Domain',
name: 'my-domain',
namespace: 'my-namespace',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
spec: {},
},
];
const { getByText } = await renderInTestApp(
<SystemsTable
title="My Systems"
<EntityTable
title="Entities"
entities={entities}
emptyComponent={<div>EMPTY</div>}
columns={[
{
title: 'Name',
field: 'metadata.name',
},
]}
/>,
);
await waitFor(() => {
expect(getByText('My Systems')).toBeInTheDocument();
expect(getByText('my-namespace/my-system')).toBeInTheDocument();
expect(getByText('my-namespace/my-domain')).toBeInTheDocument();
expect(getByText('Test')).toBeInTheDocument();
expect(getByText('Some description')).toBeInTheDocument();
expect(getByText('my-entity')).toBeInTheDocument();
});
});
});
@@ -19,7 +19,7 @@ import { Table, TableColumn } from '@backstage/core';
import { makeStyles } from '@material-ui/core';
import React from 'react';
import * as columnFactories from './columns';
import { systemEntityColumns } from './presets';
import { componentEntityColumns, systemEntityColumns } from './presets';
type Props<T extends Entity> = {
title: string;
@@ -77,3 +77,5 @@ export function EntityTable<T extends Entity>({
EntityTable.columns = columnFactories;
EntityTable.systemEntityColumns = systemEntityColumns;
EntityTable.componentEntityColumns = componentEntityColumns;
@@ -16,6 +16,7 @@
import {
Entity,
EntityName,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
@@ -43,11 +44,17 @@ export function createEntityRefColumn<T extends Entity>({
title: 'Name',
highlight: true,
customFilterAndSearch(filter, entity) {
// TODO: We could implement this more efficiently, like searching over each field individually, but that migth confuse the user
// TODO: We could implement this more efficiently, like searching over
// each field that is displayed individually (kind, namespace, name).
// but that migth confuse the user as it will behave different than a
// simple text search.
// Another altnerative would be to cache the values. But writing them
// into the entity feels bad too.
return formatContent(entity).includes(filter);
},
customSort(entity1, entity2) {
// TODO: We could implement this more efficiently by comparing field by field.
// This has similar issues as above.
return formatContent(entity1).localeCompare(formatContent(entity2));
},
render: entity => (
@@ -56,16 +63,29 @@ export function createEntityRefColumn<T extends Entity>({
};
}
export function createOwnerColumn<T extends Entity>(): TableColumn<T> {
export function createEntityRelationColumn<T extends Entity>({
title,
relation,
defaultKind,
filter: entityFilter,
}: {
title: string;
relation: string;
defaultKind?: string;
filter?: { kind: string };
}): TableColumn<T> {
function getRelations(entity: T): EntityName[] {
return getEntityRelations(entity, relation, entityFilter);
}
function formatContent(entity: T): string {
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
return ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
return getRelations(entity)
.map(r => formatEntityRefTitle(r, { defaultKind }))
.join(', ');
}
return {
title: 'Owner',
title,
customFilterAndSearch(filter, entity) {
return formatContent(entity).includes(filter);
},
@@ -73,48 +93,44 @@ export function createOwnerColumn<T extends Entity>(): TableColumn<T> {
return formatContent(entity1).localeCompare(formatContent(entity2));
},
render: entity => {
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
return (
<EntityRefLinks entityRefs={ownedByRelations} defaultKind="group" />
<EntityRefLinks
entityRefs={getRelations(entity)}
defaultKind={defaultKind}
/>
);
},
};
}
export function createDomainColumn<T extends Entity>(): TableColumn<T> {
function formatContent(entity: T): string {
const partOfDomainRelations = getEntityRelations(entity, RELATION_PART_OF, {
kind: 'domain',
});
return partOfDomainRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'domain' }))
.join(', ');
}
export function createOwnerColumn<T extends Entity>(): TableColumn<T> {
return createEntityRelationColumn({
title: 'Owner',
relation: RELATION_OWNED_BY,
defaultKind: 'group',
});
}
return {
export function createDomainColumn<T extends Entity>(): TableColumn<T> {
return createEntityRelationColumn({
title: 'Domain',
customFilterAndSearch(filter, entity) {
return formatContent(entity).includes(filter);
relation: RELATION_PART_OF,
defaultKind: 'domain',
filter: {
kind: 'domain',
},
customSort(entity1, entity2) {
return formatContent(entity1).localeCompare(formatContent(entity2));
});
}
export function createSystemColumn<T extends Entity>(): TableColumn<T> {
return createEntityRelationColumn({
title: 'System',
relation: RELATION_PART_OF,
defaultKind: 'system',
filter: {
kind: 'system',
},
render: entity => {
const partOfDomainRelations = getEntityRelations(
entity,
RELATION_PART_OF,
{
kind: 'domain',
},
);
return (
<EntityRefLinks
entityRefs={partOfDomainRelations}
defaultKind="domain"
/>
);
},
};
});
}
export function createMetadataDescriptionColumn<
@@ -126,3 +142,17 @@ export function createMetadataDescriptionColumn<
width: 'auto',
};
}
export function createSpecLifecycleColumn<T extends Entity>(): TableColumn<T> {
return {
title: 'Lifecycle',
field: 'spec.lifecycle',
};
}
export function createSpecTypeColumn<T extends Entity>(): TableColumn<T> {
return {
title: 'Type',
field: 'spec.type',
};
}
@@ -18,26 +18,68 @@ import {
ComponentEntity,
RELATION_OWNED_BY,
RELATION_PART_OF,
SystemEntity,
} from '@backstage/catalog-model';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { ComponentsTable } from './ComponentsTable';
import { EntityTable } from './EntityTable';
import { componentEntityColumns, systemEntityColumns } from './presets';
describe('systemEntityColumns', () => {
it('shows systems', async () => {
const entities: SystemEntity[] = [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'System',
metadata: {
name: 'my-system',
namespace: 'my-namespace',
description: 'Some description',
},
spec: {
owner: 'owner-data',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'Domain',
name: 'my-domain',
namespace: 'my-namespace',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
},
];
describe('<ComponentsTable />', () => {
it('shows empty table', async () => {
const { getByText } = await renderInTestApp(
<ComponentsTable
title="My Components"
entities={[]}
<EntityTable
title="My Systems"
entities={entities}
emptyComponent={<div>EMPTY</div>}
columns={systemEntityColumns}
/>,
);
expect(getByText('My Components')).toBeInTheDocument();
expect(getByText('EMPTY')).toBeInTheDocument();
await waitFor(() => {
expect(getByText('my-namespace/my-system')).toBeInTheDocument();
expect(getByText('my-namespace/my-domain')).toBeInTheDocument();
expect(getByText('Test')).toBeInTheDocument();
expect(getByText('Some description')).toBeInTheDocument();
});
});
});
describe('componentEntityColumns', () => {
it('shows components', async () => {
const entities: ComponentEntity[] = [
{
@@ -75,15 +117,15 @@ describe('<ComponentsTable />', () => {
];
const { getByText } = await renderInTestApp(
<ComponentsTable
<EntityTable
title="My Components"
entities={entities}
emptyComponent={<div>EMPTY</div>}
columns={componentEntityColumns}
/>,
);
await waitFor(() => {
expect(getByText('My Components')).toBeInTheDocument();
expect(getByText('my-namespace/my-component')).toBeInTheDocument();
expect(getByText('my-namespace/my-system')).toBeInTheDocument();
expect(getByText('Test')).toBeInTheDocument();
@@ -14,18 +14,30 @@
* limitations under the License.
*/
import { SystemEntity } from '@backstage/catalog-model';
import { ComponentEntity, SystemEntity } from '@backstage/catalog-model';
import { TableColumn } from '@backstage/core';
import {
createDomainColumn,
createEntityRefColumn,
createMetadataDescriptionColumn,
createOwnerColumn,
createDomainColumn,
createSpecLifecycleColumn,
createSpecTypeColumn,
createSystemColumn,
} from './columns';
export const systemEntityColumns: TableColumn<SystemEntity>[] = [
createEntityRefColumn({ defaultKind: 'system' }),
createOwnerColumn(),
createDomainColumn(),
createOwnerColumn(),
createMetadataDescriptionColumn(),
];
export const componentEntityColumns: TableColumn<ComponentEntity>[] = [
createEntityRefColumn({ defaultKind: 'component' }),
createSystemColumn(),
createOwnerColumn(),
createSpecTypeColumn(),
createSpecLifecycleColumn(),
createMetadataDescriptionColumn(),
];
@@ -1,165 +0,0 @@
/*
* 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 {
ComponentEntity,
EntityName,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Table, TableColumn } from '@backstage/core';
import { makeStyles } from '@material-ui/core';
import React from 'react';
import { getEntityRelations } from '../../utils';
import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
} from '../EntityRefLink';
type EntityRow = {
entity: ComponentEntity;
resolved: {
name: string;
partOfSystemRelationTitle?: string;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
};
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'resolved.name',
highlight: true,
render: ({ entity }) => (
<EntityRefLink entityRef={entity} defaultKind="Component" />
),
},
{
title: 'System',
field: 'resolved.partOfSystemRelationTitle',
render: ({ resolved }) => (
<EntityRefLinks
entityRefs={resolved.partOfSystemRelations}
defaultKind="system"
/>
),
},
{
title: 'Owner',
field: 'resolved.ownedByRelationsTitle',
render: ({ resolved }) => (
<EntityRefLinks
entityRefs={resolved.ownedByRelations}
defaultKind="group"
/>
),
},
{
title: 'Lifecycle',
field: 'entity.spec.lifecycle',
},
{
title: 'Type',
field: 'entity.spec.type',
},
{
title: 'Description',
field: 'entity.metadata.description',
width: 'auto',
},
];
type Props = {
title: string;
variant?: string;
entities: ComponentEntity[];
emptyComponent?: JSX.Element;
};
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
export const ComponentsTable = ({
entities,
title,
emptyComponent,
variant = 'gridItem',
}: Props) => {
const classes = useStyles();
const tableStyle: React.CSSProperties = {
minWidth: '0',
width: '100%',
};
if (variant === 'gridItem') {
tableStyle.height = 'calc(100% - 10px)';
}
const rows = entities.map(entity => {
const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, {
kind: 'system',
});
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
return {
entity: entity,
resolved: {
name: formatEntityRefTitle(entity, {
defaultKind: 'Component',
}),
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle: partOfSystemRelations
.map(r =>
formatEntityRefTitle(r, {
defaultKind: 'system',
}),
)
.join(', '),
partOfSystemRelations,
},
};
});
return (
<Table<EntityRow>
columns={columns}
title={title}
style={tableStyle}
emptyComponent={
emptyComponent && <div className={classes.empty}>{emptyComponent}</div>
}
options={{
// TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
search: false,
paging: false,
actionsColumnIndex: -1,
padding: 'dense',
}}
data={rows}
/>
);
};
@@ -1,157 +0,0 @@
/*
* 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 {
EntityName,
RELATION_OWNED_BY,
RELATION_PART_OF,
SystemEntity,
} from '@backstage/catalog-model';
import { Table, TableColumn } from '@backstage/core';
import { makeStyles } from '@material-ui/core';
import React from 'react';
import { getEntityRelations } from '../../utils';
import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
} from '../EntityRefLink';
type EntityRow = {
entity: SystemEntity;
resolved: {
name: string;
partOfDomainRelationTitle?: string;
partOfDomainRelations: EntityName[];
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
};
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'resolved.name',
highlight: true,
render: ({ entity }) => (
<EntityRefLink entityRef={entity} defaultKind="System" />
),
},
{
title: 'Domain',
field: 'resolved.partOfDomainRelationTitle',
render: ({ resolved }) => (
<EntityRefLinks
entityRefs={resolved.partOfDomainRelations}
defaultKind="domain"
/>
),
},
{
title: 'Owner',
field: 'resolved.ownedByRelationsTitle',
render: ({ resolved }) => (
<EntityRefLinks
entityRefs={resolved.ownedByRelations}
defaultKind="group"
/>
),
},
{
title: 'Description',
field: 'entity.metadata.description',
width: 'auto',
},
];
type Props = {
title: string;
variant?: string;
entities: SystemEntity[];
emptyComponent?: JSX.Element;
};
const useStyles = makeStyles(theme => ({
empty: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
}));
export const SystemsTable = ({
entities,
title,
emptyComponent,
variant = 'gridItem',
}: Props) => {
const classes = useStyles();
const tableStyle: React.CSSProperties = {
minWidth: '0',
width: '100%',
};
if (variant === 'gridItem') {
tableStyle.height = 'calc(100% - 10px)';
}
const rows = entities.map(entity => {
const partOfDomainRelations = getEntityRelations(entity, RELATION_PART_OF, {
kind: 'domain',
});
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
return {
entity,
resolved: {
name: formatEntityRefTitle(entity, {
defaultKind: 'System',
}),
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfDomainRelationTitle: partOfDomainRelations
.map(r =>
formatEntityRefTitle(r, {
defaultKind: 'domain',
}),
)
.join(', '),
partOfDomainRelations,
},
};
});
return (
<Table<EntityRow>
columns={columns}
title={title}
style={tableStyle}
emptyComponent={
emptyComponent && <div className={classes.empty}>{emptyComponent}</div>
}
options={{
// TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
search: false,
paging: false,
actionsColumnIndex: -1,
padding: 'dense',
}}
data={rows}
/>
);
};
@@ -1,17 +0,0 @@
/*
* 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 { ComponentsTable } from './ComponentsTable';
export { SystemsTable } from './SystemsTable';
@@ -16,4 +16,3 @@
export * from './EntityProvider';
export * from './EntityRefLink';
export * from './EntityTable';
export * from './EntityTables';
@@ -23,7 +23,7 @@ import {
WarningPanel,
} from '@backstage/core';
import {
ComponentsTable,
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
@@ -72,7 +72,7 @@ export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => {
}
return (
<ComponentsTable
<EntityTable
title="Components"
variant={variant}
emptyComponent={
@@ -83,6 +83,7 @@ export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => {
</Link>
</div>
}
columns={EntityTable.componentEntityColumns}
entities={entities as ComponentEntity[]}
/>
);
@@ -23,7 +23,7 @@ import {
WarningPanel,
} from '@backstage/core';
import {
ComponentsTable,
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
@@ -72,7 +72,7 @@ export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => {
}
return (
<ComponentsTable
<EntityTable
title="Subcomponents"
variant={variant}
emptyComponent={
@@ -83,6 +83,7 @@ export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => {
</Link>
</div>
}
columns={EntityTable.componentEntityColumns}
entities={entities as ComponentEntity[]}
/>
);