Introduce new cards to @backstage/plugin-catalog that can be added to entity pages
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Introduce new cards to `@backstage/plugin-catalog` that can be added to entity pages:
|
||||
|
||||
- `EntityHasSystemsCard` to display systems of a domain.
|
||||
- `EntityHasComponentsCard` to display components of a system.
|
||||
- `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 `and` to build own cards for components and systems.
|
||||
The styling of the tables and new cards was also applied to the existing `EntityConsumedApisCard`,
|
||||
`EntityConsumingComponentsCard`, `EntityProvidedApisCard`, and `EntityProvidingComponentsCard`.
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
import {
|
||||
ApiEntity,
|
||||
DomainEntity,
|
||||
Entity,
|
||||
GroupEntity,
|
||||
SystemEntity,
|
||||
UserEntity,
|
||||
} from '@backstage/catalog-model';
|
||||
import { EmptyState } from '@backstage/core';
|
||||
@@ -25,11 +27,15 @@ import {
|
||||
ApiDefinitionCard,
|
||||
ConsumedApisCard,
|
||||
ConsumingComponentsCard,
|
||||
EntityHasApisCard,
|
||||
ProvidedApisCard,
|
||||
ProvidingComponentsCard,
|
||||
} from '@backstage/plugin-api-docs';
|
||||
import {
|
||||
AboutCard,
|
||||
EntityHasComponentsCard,
|
||||
EntityHasSubcomponentsCard,
|
||||
EntityHasSystemsCard,
|
||||
EntityLinksCard,
|
||||
EntityPageLayout,
|
||||
} from '@backstage/plugin-catalog';
|
||||
@@ -206,6 +212,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
<PullRequestsStatsCard entity={entity} />
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item md={6}>
|
||||
<EntityHasSubcomponentsCard variant="gridItem" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -425,6 +434,51 @@ const GroupEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item md={6}>
|
||||
<AboutCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<EntityHasComponentsCard variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<EntityHasApisCard variant="gridItem" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const SystemEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/*"
|
||||
title="Overview"
|
||||
element={<SystemOverviewContent entity={entity as SystemEntity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item md={6}>
|
||||
<AboutCard entity={entity} variant="gridItem" />
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<EntityHasSystemsCard variant="gridItem" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const DomainEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
<EntityPageLayout>
|
||||
<EntityPageLayout.Content
|
||||
path="/*"
|
||||
title="Overview"
|
||||
element={<DomainOverviewContent entity={entity as DomainEntity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
|
||||
export const EntityPage = () => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
@@ -437,6 +491,10 @@ export const EntityPage = () => {
|
||||
return <GroupEntityPage entity={entity} />;
|
||||
case 'user':
|
||||
return <UserEntityPage entity={entity} />;
|
||||
case 'system':
|
||||
return <SystemEntityPage entity={entity} />;
|
||||
case 'domain':
|
||||
return <DomainEntityPage entity={entity} />;
|
||||
default:
|
||||
return <DefaultEntityPage entity={entity} />;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,4 +24,5 @@ export {
|
||||
EntityConsumingComponentsCard,
|
||||
EntityProvidedApisCard,
|
||||
EntityProvidingComponentsCard,
|
||||
EntityHasApisCard,
|
||||
} from './plugin';
|
||||
|
||||
@@ -106,3 +106,11 @@ export const EntityProvidingComponentsCard = apiDocsPlugin.provide(
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityHasApisCard = apiDocsPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () => import('./components/ApisCards').then(m => m.HasApisCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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,
|
||||
RELATION_OWNED_BY,
|
||||
RELATION_PART_OF,
|
||||
} from '@backstage/catalog-model';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { ComponentsTable } from './ComponentsTable';
|
||||
|
||||
describe('<ComponentsTable />', () => {
|
||||
it('shows empty table', async () => {
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ComponentsTable
|
||||
title="My Components"
|
||||
entities={[]}
|
||||
emptyComponent={<div>EMPTY</div>}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByText('My Components')).toBeInTheDocument();
|
||||
expect(getByText('EMPTY')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows components', async () => {
|
||||
const entities: ComponentEntity[] = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'my-component',
|
||||
namespace: 'my-namespace',
|
||||
description: 'Some description',
|
||||
},
|
||||
spec: {
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
owner: 'owner-data',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
target: {
|
||||
kind: 'System',
|
||||
name: 'my-system',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: {
|
||||
kind: 'Group',
|
||||
name: 'Test',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ComponentsTable
|
||||
title="My Components"
|
||||
entities={entities}
|
||||
emptyComponent={<div>EMPTY</div>}
|
||||
/>,
|
||||
);
|
||||
|
||||
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();
|
||||
expect(getByText('production')).toBeInTheDocument();
|
||||
expect(getByText('service')).toBeInTheDocument();
|
||||
expect(getByText('Some description')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
+48
-37
@@ -21,17 +21,19 @@ import {
|
||||
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,
|
||||
getEntityRelations,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
} from '../EntityRefLink';
|
||||
|
||||
type EntityRow = {
|
||||
entity: ComponentEntity;
|
||||
resolved: {
|
||||
name: string;
|
||||
partOfSystemRelationTitle?: string;
|
||||
partOfSystemRelations: EntityName[];
|
||||
ownedByRelationsTitle?: string;
|
||||
@@ -42,10 +44,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="Component" />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -86,15 +88,25 @@ const columns: TableColumn<EntityRow>[] = [
|
||||
type Props = {
|
||||
title: string;
|
||||
variant?: string;
|
||||
entities: (ComponentEntity | undefined)[];
|
||||
entities: ComponentEntity[];
|
||||
emptyComponent?: JSX.Element;
|
||||
};
|
||||
|
||||
// TODO: In theory this could also be systems!
|
||||
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%',
|
||||
@@ -104,43 +116,42 @@ export const ComponentsTable = ({
|
||||
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,
|
||||
},
|
||||
};
|
||||
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,
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 {
|
||||
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 { SystemsTable } from './SystemsTable';
|
||||
|
||||
describe('<SystemsTable />', () => {
|
||||
it('shows empty table', async () => {
|
||||
const { getByText } = await renderInTestApp(
|
||||
<SystemsTable
|
||||
title="My Systems"
|
||||
entities={[]}
|
||||
emptyComponent={<div>EMPTY</div>}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByText('My Systems')).toBeInTheDocument();
|
||||
expect(getByText('EMPTY')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<SystemsTable
|
||||
title="My Systems"
|
||||
entities={entities}
|
||||
emptyComponent={<div>EMPTY</div>}
|
||||
/>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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';
|
||||
@@ -13,5 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './EntityRefLink';
|
||||
export * from './EntityProvider';
|
||||
export * from './EntityRefLink';
|
||||
export * from './EntityTables';
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 { HasComponentsCard } from './HasComponentsCard';
|
||||
|
||||
describe('<HasComponentsCard />', () => {
|
||||
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);
|
||||
|
||||
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}>
|
||||
<HasComponentsCard />
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(getByText('Components')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(/No component is part of this system/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows related components', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'System',
|
||||
metadata: {
|
||||
name: 'my-system',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Component',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<HasComponentsCard />
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Components')).toBeInTheDocument();
|
||||
expect(getByText(/target-name/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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, RELATION_HAS_PART } from '@backstage/catalog-model';
|
||||
import {
|
||||
CodeSnippet,
|
||||
InfoCard,
|
||||
Link,
|
||||
Progress,
|
||||
WarningPanel,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
ComponentsTable,
|
||||
useEntity,
|
||||
useRelatedEntities,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
const ComponentsCard = ({
|
||||
children,
|
||||
variant = 'gridItem',
|
||||
}: PropsWithChildren<{ variant?: string }>) => {
|
||||
return (
|
||||
<InfoCard variant={variant} title="Components">
|
||||
{children}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => {
|
||||
const { entity } = useEntity();
|
||||
const { entities, loading, error } = useRelatedEntities(entity, {
|
||||
type: RELATION_HAS_PART,
|
||||
kind: 'Component',
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ComponentsCard variant={variant}>
|
||||
<Progress />
|
||||
</ComponentsCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !entities) {
|
||||
return (
|
||||
<ComponentsCard variant={variant}>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
title="Could not load components"
|
||||
message={<CodeSnippet text={`${error}`} language="text" />}
|
||||
/>
|
||||
</ComponentsCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentsTable
|
||||
title="Components"
|
||||
variant={variant}
|
||||
emptyComponent={
|
||||
<div>
|
||||
No component is part of this system.{' '}
|
||||
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component">
|
||||
Learn how to add components.
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
entities={entities as ComponentEntity[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { HasComponentsCard } from './HasComponentsCard';
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 { HasSubcomponentsCard } from './HasSubcomponentsCard';
|
||||
|
||||
describe('<HasSubcomponentsCard />', () => {
|
||||
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);
|
||||
|
||||
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: 'Component',
|
||||
metadata: {
|
||||
name: 'my-components',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
relations: [],
|
||||
};
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<HasSubcomponentsCard />
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(getByText('Subcomponents')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(/No subcomponent is part of this component/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows related subcomponents', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'my-component',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Component',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<HasSubcomponentsCard />
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Subcomponents')).toBeInTheDocument();
|
||||
expect(getByText(/target-name/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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, RELATION_HAS_PART } from '@backstage/catalog-model';
|
||||
import {
|
||||
CodeSnippet,
|
||||
InfoCard,
|
||||
Link,
|
||||
Progress,
|
||||
WarningPanel,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
ComponentsTable,
|
||||
useEntity,
|
||||
useRelatedEntities,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
const SubcomponentsCard = ({
|
||||
children,
|
||||
variant = 'gridItem',
|
||||
}: PropsWithChildren<{ variant?: string }>) => {
|
||||
return (
|
||||
<InfoCard variant={variant} title="Subcomponents">
|
||||
{children}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => {
|
||||
const { entity } = useEntity();
|
||||
const { entities, loading, error } = useRelatedEntities(entity, {
|
||||
type: RELATION_HAS_PART,
|
||||
kind: 'Component',
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SubcomponentsCard variant={variant}>
|
||||
<Progress />
|
||||
</SubcomponentsCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !entities) {
|
||||
return (
|
||||
<SubcomponentsCard variant={variant}>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
title="Could not load subcomponents"
|
||||
message={<CodeSnippet text={`${error}`} language="text" />}
|
||||
/>
|
||||
</SubcomponentsCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentsTable
|
||||
title="Subcomponents"
|
||||
variant={variant}
|
||||
emptyComponent={
|
||||
<div>
|
||||
No subcomponent is part of this component.{' '}
|
||||
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#specsubcomponentof-optional">
|
||||
Learn how to add subcomponents.
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
entities={entities as ComponentEntity[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { HasSubcomponentsCard } from './HasSubcomponentsCard';
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 { HasSystemsCard } from './HasSystemsCard';
|
||||
|
||||
describe('<HasSystemsCard />', () => {
|
||||
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);
|
||||
|
||||
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: 'Domain',
|
||||
metadata: {
|
||||
name: 'my-domain',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
relations: [],
|
||||
};
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<HasSystemsCard />
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(getByText('Systems')).toBeInTheDocument();
|
||||
expect(getByText(/No system is part of this domain/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows related systems', async () => {
|
||||
const entity: Entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Domain',
|
||||
metadata: {
|
||||
name: 'my-domain',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'System',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
};
|
||||
catalogApi.getEntityByName.mockResolvedValue({
|
||||
apiVersion: 'v1',
|
||||
kind: 'System',
|
||||
metadata: {
|
||||
name: 'target-name',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
spec: {},
|
||||
});
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<Wrapper>
|
||||
<EntityProvider entity={entity}>
|
||||
<HasSystemsCard />
|
||||
</EntityProvider>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Systems')).toBeInTheDocument();
|
||||
expect(getByText(/target-name/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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 { RELATION_HAS_PART, SystemEntity } from '@backstage/catalog-model';
|
||||
import {
|
||||
CodeSnippet,
|
||||
InfoCard,
|
||||
Link,
|
||||
Progress,
|
||||
WarningPanel,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
SystemsTable,
|
||||
useEntity,
|
||||
useRelatedEntities,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
const SystemsCard = ({
|
||||
children,
|
||||
variant = 'gridItem',
|
||||
}: PropsWithChildren<{ variant?: string }>) => {
|
||||
return (
|
||||
<InfoCard variant={variant} title="Systems">
|
||||
{children}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
export const HasSystemsCard = ({ variant = 'gridItem' }: Props) => {
|
||||
const { entity } = useEntity();
|
||||
const { entities, loading, error } = useRelatedEntities(entity, {
|
||||
type: RELATION_HAS_PART,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SystemsCard variant={variant}>
|
||||
<Progress />
|
||||
</SystemsCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !entities) {
|
||||
return (
|
||||
<SystemsCard variant={variant}>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
title="Could not load systems"
|
||||
message={<CodeSnippet text={`${error}`} language="text" />}
|
||||
/>
|
||||
</SystemsCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SystemsTable
|
||||
title="Systems"
|
||||
variant={variant}
|
||||
emptyComponent={
|
||||
<div>
|
||||
No system is part of this domain.{' '}
|
||||
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system">
|
||||
Learn how to add systems.
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
entities={entities as SystemEntity[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { HasSystemsCard } from './HasSystemsCard';
|
||||
@@ -20,10 +20,14 @@ export { EntityPageLayout } from './components/EntityPageLayout';
|
||||
export * from './components/EntitySwitch';
|
||||
export { Router } from './components/Router';
|
||||
export {
|
||||
CatalogEntityPage,
|
||||
CatalogIndexPage,
|
||||
catalogPlugin,
|
||||
catalogPlugin as plugin,
|
||||
CatalogIndexPage,
|
||||
CatalogEntityPage,
|
||||
EntityAboutCard,
|
||||
EntityLinksCard,
|
||||
EntityHasComponentsCard,
|
||||
EntityHasSubcomponentsCard,
|
||||
EntityHasSystemsCard,
|
||||
EntityLinksCard
|
||||
} from './plugin';
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import {
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
discoveryApiRef,
|
||||
createComponentExtension,
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
@@ -60,9 +60,7 @@ export const CatalogIndexPage = catalogPlugin.provide(
|
||||
export const CatalogEntityPage = catalogPlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () =>
|
||||
import('./components/CatalogEntityPage/CatalogEntityPage').then(
|
||||
m => m.CatalogEntityPage,
|
||||
),
|
||||
import('./components/CatalogEntityPage').then(m => m.CatalogEntityPage),
|
||||
mountPoint: entityRouteRef,
|
||||
}),
|
||||
);
|
||||
@@ -83,3 +81,32 @@ export const EntityLinksCard = catalogPlugin.provide(
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityHasSystemsCard = catalogPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/HasSystemsCard').then(m => m.HasSystemsCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityHasComponentsCard = catalogPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/ComponentsCard').then(m => m.HasComponentsCard),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityHasSubcomponentsCard = catalogPlugin.provide(
|
||||
createComponentExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
import('./components/HasSubcomponentsCard').then(
|
||||
m => m.HasSubcomponentsCard,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user