Introduce new cards to @backstage/plugin-catalog that can be added to entity pages

This commit is contained in:
Oliver Sand
2021-02-04 17:37:10 +01:00
parent 96f378d108
commit 0af242b6d8
33 changed files with 1569 additions and 295 deletions
@@ -27,12 +27,14 @@ import {
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;
@@ -43,10 +45,10 @@ type EntityRow = {
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'entity.metadata.name',
field: 'resolved.name',
highlight: true,
render: ({ entity }) => (
<EntityRefLink entityRef={entity}>{entity.metadata.name}</EntityRefLink>
<EntityRefLink entityRef={entity} defaultKind="API" />
),
},
{
@@ -88,10 +90,25 @@ const columns: TableColumn<EntityRow>[] = [
type Props = {
title: string;
variant?: string;
entities: (ApiEntity | undefined)[];
entities: ApiEntity[];
emptyComponent?: JSX.Element;
};
export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => {
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%',
@@ -101,43 +118,42 @@ export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => {
tableStyle.height = 'calc(100% - 10px)';
}
const rows = entities
// TODO: For now we skip all APIs that we can't find without a warning!
.filter(e => e !== undefined)
.map(entity => {
const partOfSystemRelations = getEntityRelations(
entity,
RELATION_PART_OF,
{
kind: 'system',
},
);
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
return {
entity: entity as ApiEntity,
resolved: {
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle: partOfSystemRelations
.map(r =>
formatEntityRefTitle(r, {
defaultKind: 'system',
}),
)
.join(', '),
partOfSystemRelations,
},
};
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,
@@ -14,12 +14,7 @@
* limitations under the License.
*/
import {
Entity,
RELATION_CONSUMES_API,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Entity, RELATION_CONSUMES_API } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
@@ -79,7 +74,7 @@ describe('<ConsumedApisCard />', () => {
);
expect(getByText(/Consumed APIs/i)).toBeInTheDocument();
expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument();
expect(getByText(/No Component consumes this API/i)).toBeInTheDocument();
});
it('shows consumed APIs', async () => {
@@ -108,34 +103,7 @@ describe('<ConsumedApisCard />', () => {
name: 'target-name',
namespace: 'my-namespace',
},
spec: {
type: 'openapi',
lifecycle: 'production',
definition: '...',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'System',
name: 'MySystem',
namespace: 'default',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
});
apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
type: 'openapi',
title: 'OpenAPI',
component: () => <div />,
spec: {},
});
const { getByText } = await renderInTestApp(
@@ -147,12 +115,8 @@ describe('<ConsumedApisCard />', () => {
);
await waitFor(() => {
expect(getByText(/Consumed APIs/i)).toBeInTheDocument();
expect(getByText('Consumed APIs')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(getByText(/OpenAPI/)).toBeInTheDocument();
expect(getByText(/Test/i)).toBeInTheDocument();
expect(getByText(/MySystem/i)).toBeInTheDocument();
expect(getByText(/production/i)).toBeInTheDocument();
});
});
});
@@ -19,10 +19,15 @@ import {
Entity,
RELATION_CONSUMES_API,
} from '@backstage/catalog-model';
import { EmptyState, InfoCard, Progress } from '@backstage/core';
import {
CodeSnippet,
InfoCard,
Link,
Progress,
WarningPanel,
} from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
import { MissingConsumesApisEmptyState } from '../EmptyState';
import { ApisTable } from './ApisTable';
const ApisCard = ({
@@ -56,31 +61,31 @@ export const ConsumedApisCard = ({ variant = 'gridItem' }: Props) => {
);
}
if (error) {
if (error || !entities) {
return (
<ApisCard variant={variant}>
<EmptyState
missing="info"
title="No information to display"
description="There was an error while loading the consumed APIs."
<WarningPanel
severity="error"
title="Could not load APIs"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</ApisCard>
);
}
if (!entities || entities.length === 0) {
return (
<ApisCard variant={variant}>
<MissingConsumesApisEmptyState />
</ApisCard>
);
}
return (
<ApisTable
title="Consumed APIs"
variant={variant}
entities={entities as (ApiEntity | undefined)[]}
emptyComponent={
<div>
No Component consumes this API.{' '}
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional">
Learn how to consume APIs.
</Link>
</div>
}
entities={entities as ApiEntity[]}
/>
);
};
@@ -0,0 +1,122 @@
/*
* 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, RELATION_HAS_PART } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
catalogApiRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { ApiDocsConfig, apiDocsConfigRef } from '../../config';
import { HasApisCard } from './HasApisCard';
describe('<HasApisCard />', () => {
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
getApiDefinitionWidget: jest.fn(),
} as any;
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi).with(
apiDocsConfigRef,
apiDocsConfig,
);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
);
});
afterEach(() => jest.resetAllMocks());
it('shows empty list if no relations', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'System',
metadata: {
name: 'my-system',
namespace: 'my-namespace',
},
relations: [],
};
const { getByText } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<HasApisCard />
</EntityProvider>
</Wrapper>,
);
expect(getByText('APIs')).toBeInTheDocument();
expect(getByText(/No API is part of this system/i)).toBeInTheDocument();
});
it('shows related APIs', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'System',
metadata: {
name: 'my-system',
namespace: 'my-namespace',
},
relations: [
{
target: {
kind: 'API',
namespace: 'my-namespace',
name: 'target-name',
},
type: RELATION_HAS_PART,
},
],
};
catalogApi.getEntityByName.mockResolvedValue({
apiVersion: 'v1',
kind: 'API',
metadata: {
name: 'target-name',
namespace: 'my-namespace',
},
spec: {},
});
const { getByText } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<HasApisCard />
</EntityProvider>
</Wrapper>,
);
await waitFor(() => {
expect(getByText('APIs')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,86 @@
/*
* 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, RELATION_HAS_PART } from '@backstage/catalog-model';
import {
CodeSnippet,
InfoCard,
Link,
Progress,
WarningPanel,
} from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
import { ApisTable } from './ApisTable';
const ApisCard = ({
children,
variant = 'gridItem',
}: PropsWithChildren<{ variant?: string }>) => {
return (
<InfoCard variant={variant} title="APIs">
{children}
</InfoCard>
);
};
type Props = {
variant?: string;
};
export const HasApisCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: RELATION_HAS_PART,
kind: 'API',
});
if (loading) {
return (
<ApisCard variant={variant}>
<Progress />
</ApisCard>
);
}
if (error || !entities) {
return (
<ApisCard variant={variant}>
<WarningPanel
severity="error"
title="Could not load APIs"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</ApisCard>
);
}
return (
<ApisTable
title="APIs"
variant={variant}
emptyComponent={
<div>
No API is part of this system.{' '}
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api">
Learn how to add APIs.
</Link>
</div>
}
entities={entities as ApiEntity[]}
/>
);
};
@@ -14,12 +14,7 @@
* limitations under the License.
*/
import {
Entity,
RELATION_OWNED_BY,
RELATION_PART_OF,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
@@ -79,7 +74,7 @@ describe('<ProvidedApisCard />', () => {
);
expect(getByText(/Provided APIs/i)).toBeInTheDocument();
expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument();
expect(getByText(/No Component provides this API/i)).toBeInTheDocument();
});
it('shows consumed APIs', async () => {
@@ -108,34 +103,7 @@ describe('<ProvidedApisCard />', () => {
name: 'target-name',
namespace: 'my-namespace',
},
spec: {
type: 'openapi',
lifecycle: 'production',
definition: '...',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'System',
name: 'MySystem',
namespace: 'default',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
});
apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
type: 'openapi',
title: 'OpenAPI',
component: () => <div />,
spec: {},
});
const { getByText } = await renderInTestApp(
@@ -149,10 +117,6 @@ describe('<ProvidedApisCard />', () => {
await waitFor(() => {
expect(getByText(/Provided APIs/i)).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(getByText(/OpenAPI/)).toBeInTheDocument();
expect(getByText(/MySystem/)).toBeInTheDocument();
expect(getByText(/Test/i)).toBeInTheDocument();
expect(getByText(/production/i)).toBeInTheDocument();
});
});
});
@@ -19,10 +19,15 @@ import {
Entity,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { EmptyState, InfoCard, Progress } from '@backstage/core';
import {
CodeSnippet,
InfoCard,
Link,
Progress,
WarningPanel,
} from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
import { MissingProvidesApisEmptyState } from '../EmptyState';
import { ApisTable } from './ApisTable';
const ApisCard = ({
@@ -56,31 +61,31 @@ export const ProvidedApisCard = ({ variant = 'gridItem' }: Props) => {
);
}
if (error) {
if (error || !entities) {
return (
<ApisCard variant={variant}>
<EmptyState
missing="info"
title="No information to display"
description="There was an error while loading the provided APIs."
<WarningPanel
severity="error"
title="Could not load APIs"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</ApisCard>
);
}
if (!entities || entities.length === 0) {
return (
<ApisCard variant={variant}>
<MissingProvidesApisEmptyState />
</ApisCard>
);
}
return (
<ApisTable
title="Provided APIs"
variant={variant}
entities={entities as (ApiEntity | undefined)[]}
emptyComponent={
<div>
No Component provides this API.{' '}
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional">
Learn how to provide APIs.
</Link>
</div>
}
entities={entities as ApiEntity[]}
/>
);
};
@@ -15,4 +15,5 @@
*/
export { ConsumedApisCard } from './ConsumedApisCard';
export { HasApisCard } from './HasApisCard';
export { ProvidedApisCard } from './ProvidedApisCard';
@@ -1,154 +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 {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import React from 'react';
type EntityRow = {
entity: ComponentEntity;
resolved: {
partOfSystemRelationTitle?: string;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
};
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'entity.metadata.name',
highlight: true,
render: ({ entity }) => (
<EntityRefLink entityRef={entity}>{entity.metadata.name}</EntityRefLink>
),
},
{
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 | undefined)[];
};
// TODO: In theory this could also be systems!
export const ComponentsTable = ({
entities,
title,
variant = 'gridItem',
}: Props) => {
const tableStyle: React.CSSProperties = {
minWidth: '0',
width: '100%',
};
if (variant === 'gridItem') {
tableStyle.height = 'calc(100% - 10px)';
}
const rows = entities
// TODO: For now we skip all Components that we can't find without a warning!
.filter(e => e !== undefined)
.map(entity => {
const partOfSystemRelations = getEntityRelations(
entity,
RELATION_PART_OF,
{
kind: 'system',
},
);
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
return {
entity: entity as ComponentEntity,
resolved: {
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}
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}
/>
);
};
@@ -14,12 +14,7 @@
* limitations under the License.
*/
import {
Entity,
RELATION_API_CONSUMED_BY,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Entity, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
@@ -77,8 +72,8 @@ describe('<ConsumingComponentsCard />', () => {
</Wrapper>,
);
expect(getByText(/Consumers/i)).toBeInTheDocument();
expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument();
expect(getByText('Consumers')).toBeInTheDocument();
expect(getByText(/No component consumes this API/i)).toBeInTheDocument();
});
it('shows consuming components', async () => {
@@ -113,28 +108,7 @@ describe('<ConsumingComponentsCard />', () => {
name: 'target-name',
namespace: 'my-namespace',
},
spec: {
type: 'service',
lifecycle: 'production',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'System',
name: 'MySystem',
namespace: 'default',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
spec: {},
});
const { getByText } = await renderInTestApp(
@@ -146,11 +120,8 @@ describe('<ConsumingComponentsCard />', () => {
);
await waitFor(() => {
expect(getByText(/Consumers/i)).toBeInTheDocument();
expect(getByText('Consumers')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(getByText(/Test/i)).toBeInTheDocument();
expect(getByText(/MySystem/i)).toBeInTheDocument();
expect(getByText(/production/i)).toBeInTheDocument();
});
});
});
@@ -19,11 +19,20 @@ import {
Entity,
RELATION_API_CONSUMED_BY,
} from '@backstage/catalog-model';
import { EmptyState, InfoCard, Progress } from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import {
CodeSnippet,
InfoCard,
Link,
Progress,
WarningPanel,
} from '@backstage/core';
import {
ComponentsTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
import { MissingConsumesApisEmptyState } from '../EmptyState';
import { ComponentsTable } from './ComponentsTable';
const ComponentsCard = ({
children,
@@ -56,31 +65,31 @@ export const ConsumingComponentsCard = ({ variant = 'gridItem' }: Props) => {
);
}
if (error) {
if (error || !entities) {
return (
<ComponentsCard variant={variant}>
<EmptyState
missing="info"
title="No information to display"
description="There was an error while loading the consumers."
<WarningPanel
severity="error"
title="Could not load components"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</ComponentsCard>
);
}
if (!entities || entities.length === 0) {
return (
<ComponentsCard variant={variant}>
<MissingConsumesApisEmptyState />
</ComponentsCard>
);
}
return (
<ComponentsTable
title="Consumers"
variant={variant}
entities={entities as (ComponentEntity | undefined)[]}
emptyComponent={
<div>
No component consumes this API.{' '}
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional">
Learn how to consume APIs.
</Link>
</div>
}
entities={entities as ComponentEntity[]}
/>
);
};
@@ -14,12 +14,7 @@
* limitations under the License.
*/
import {
Entity,
RELATION_API_PROVIDED_BY,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Entity, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
@@ -77,8 +72,8 @@ describe('<ProvidingComponentsCard />', () => {
</Wrapper>,
);
expect(getByText(/Providers/i)).toBeInTheDocument();
expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument();
expect(getByText('Providers')).toBeInTheDocument();
expect(getByText(/No component provides this API/i)).toBeInTheDocument();
});
it('shows providing components', async () => {
@@ -113,28 +108,7 @@ describe('<ProvidingComponentsCard />', () => {
name: 'target-name',
namespace: 'my-namespace',
},
spec: {
type: 'service',
lifecycle: 'production',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'System',
name: 'MySystem',
namespace: 'default',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
spec: {},
});
const { getByText } = await renderInTestApp(
@@ -146,11 +120,8 @@ describe('<ProvidingComponentsCard />', () => {
);
await waitFor(() => {
expect(getByText(/Providers/i)).toBeInTheDocument();
expect(getByText('Providers')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(getByText(/Test/i)).toBeInTheDocument();
expect(getByText(/MySystem/i)).toBeInTheDocument();
expect(getByText(/production/i)).toBeInTheDocument();
});
});
});
@@ -19,11 +19,19 @@ import {
Entity,
RELATION_API_PROVIDED_BY,
} from '@backstage/catalog-model';
import { EmptyState, InfoCard, Progress } from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import {
CodeSnippet,
InfoCard,
Link,
Progress,
WarningPanel,
} from '@backstage/core';
import {
ComponentsTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
import { MissingProvidesApisEmptyState } from '../EmptyState';
import { ComponentsTable } from './ComponentsTable';
const ComponentsCard = ({
children,
@@ -56,31 +64,31 @@ export const ProvidingComponentsCard = ({ variant = 'gridItem' }: Props) => {
);
}
if (error) {
if (error || !entities) {
return (
<ComponentsCard variant={variant}>
<EmptyState
missing="info"
title="No information to display"
description="There was an error while loading the providers."
<WarningPanel
severity="error"
title="Could not load components"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</ComponentsCard>
);
}
if (!entities || entities.length === 0) {
return (
<ComponentsCard variant={variant}>
<MissingProvidesApisEmptyState />
</ComponentsCard>
);
}
return (
<ComponentsTable
title="Providers"
variant={variant}
entities={entities as (ComponentEntity | undefined)[]}
emptyComponent={
<div>
No component provides this API.{' '}
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional">
Learn how to provide APIs.
</Link>
</div>
}
entities={entities as ComponentEntity[]}
/>
);
};
+1
View File
@@ -24,4 +24,5 @@ export {
EntityConsumingComponentsCard,
EntityProvidedApisCard,
EntityProvidingComponentsCard,
EntityHasApisCard,
} from './plugin';
+8
View File
@@ -106,3 +106,11 @@ export const EntityProvidingComponentsCard = apiDocsPlugin.provide(
},
}),
);
export const EntityHasApisCard = apiDocsPlugin.provide(
createComponentExtension({
component: {
lazy: () => import('./components/ApisCards').then(m => m.HasApisCard),
},
}),
);