Merge pull request #5361 from jmgrimes/catalog-model-system-resources-and-component-dependencies-cards

Added system resources and component dependencies (components/resources) cards
This commit is contained in:
Fredrik Adelöw
2021-05-17 22:16:39 +02:00
committed by GitHub
19 changed files with 828 additions and 189 deletions
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/plugin-catalog': patch
---
- Added `RelatedEntitesCard` as a base implementation of displaying entities that are related to another entity.
- Added `HasResourcesCard` to display resources that are part of a system.
- Added `DependsOnComponentsCard` to display components that are dependencies of a component.
- Added `DependsOnResourcesCard` to display resources that are dependencies of a component.
- Refactored `HasComponentsCard` to use base `RelatedEntitiesCard`. Card remains backwards compatible.
- Refactored `HasSubcomponentsCard` to use base `RelatedEntitiesCard`. Card remains backwards compatible.
- Refactored `HasSystemsCard` to use base `RelatedEntitiesCard`. Card remains backwards compatible.
- Updated the example app to take advantage of these new components.
@@ -28,6 +28,8 @@ import {
import { EntityBadgesDialog } from '@backstage/plugin-badges';
import {
EntityAboutCard,
EntityDependsOnComponentsCard,
EntityDependsOnResourcesCard,
EntityHasComponentsCard,
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
@@ -37,6 +39,7 @@ import {
EntitySwitch,
isComponentType,
isKind,
EntityHasResourcesCard,
} from '@backstage/plugin-catalog';
import {
EntityCircleCIContent,
@@ -288,6 +291,17 @@ const serviceEntityPage = (
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
@@ -336,6 +350,17 @@ const websiteEntityPage = (
{errorsContent}
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
<EntityTechdocsContent />
</EntityLayout.Route>
@@ -466,6 +491,9 @@ const systemPage = (
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityHasResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/diagram" title="Diagram">
@@ -0,0 +1,121 @@
/*
* 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_DEPENDS_ON } 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 { DependsOnComponentsCard } from './DependsOnComponentsCard';
describe('<DependsOnComponentsCard />', () => {
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 dependencies', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'my-component',
namespace: 'my-namespace',
},
relations: [],
};
const { getByText } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependsOnComponentsCard />
</EntityProvider>
</Wrapper>,
);
expect(getByText('Components')).toBeInTheDocument();
expect(
getByText(/No component is a dependency of this component/i),
).toBeInTheDocument();
});
it('shows dependency components', 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_DEPENDS_ON,
},
],
};
catalogApi.getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
kind: 'Component',
metadata: {
namespace: 'my-namespace',
name: 'target-name',
},
spec: {},
},
],
});
const { getByText } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependsOnComponentsCard />
</EntityProvider>
</Wrapper>,
);
await waitFor(() => {
expect(getByText('Components')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,43 @@
/*
* 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_DEPENDS_ON } from '@backstage/catalog-model';
import React from 'react';
import {
asComponentEntities,
componentEntityColumns,
componentEntityHelpLink,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
type Props = {
variant?: 'gridItem';
};
export const DependsOnComponentsCard = ({ variant = 'gridItem' }: Props) => {
return (
<RelatedEntitiesCard
variant={variant}
title="Components"
entityKind="Component"
relationType={RELATION_DEPENDS_ON}
columns={componentEntityColumns}
emptyMessage="No component is a dependency of this component"
emptyHelpLink={componentEntityHelpLink}
asRenderableEntities={asComponentEntities}
/>
);
};
@@ -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 { DependsOnComponentsCard } from './DependsOnComponentsCard';
@@ -0,0 +1,121 @@
/*
* 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_DEPENDS_ON } 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 { DependsOnResourcesCard } from './DependsOnResourcesCard';
describe('<DependsOnResourcesCard />', () => {
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 dependencies', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'my-component',
namespace: 'my-namespace',
},
relations: [],
};
const { getByText } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependsOnResourcesCard />
</EntityProvider>
</Wrapper>,
);
expect(getByText('Resources')).toBeInTheDocument();
expect(
getByText(/No resource is a dependency of this component/i),
).toBeInTheDocument();
});
it('shows dependency resources', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'my-component',
namespace: 'my-namespace',
},
relations: [
{
target: {
kind: 'Resource',
namespace: 'my-namespace',
name: 'target-name',
},
type: RELATION_DEPENDS_ON,
},
],
};
catalogApi.getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
kind: 'Resource',
metadata: {
namespace: 'my-namespace',
name: 'target-name',
},
spec: {},
},
],
});
const { getByText } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<DependsOnResourcesCard />
</EntityProvider>
</Wrapper>,
);
await waitFor(() => {
expect(getByText('Resources')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,43 @@
/*
* 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_DEPENDS_ON } from '@backstage/catalog-model';
import React from 'react';
import {
asResourceEntities,
componentEntityHelpLink,
RelatedEntitiesCard,
resourceEntityColumns,
} from '../RelatedEntitiesCard';
type Props = {
variant?: 'gridItem';
};
export const DependsOnResourcesCard = ({ variant = 'gridItem' }: Props) => {
return (
<RelatedEntitiesCard
variant={variant}
title="Resources"
entityKind="Resource"
relationType={RELATION_DEPENDS_ON}
columns={resourceEntityColumns}
emptyMessage="No resource is a dependency of this component"
emptyHelpLink={componentEntityHelpLink}
asRenderableEntities={asResourceEntities}
/>
);
};
@@ -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 { DependsOnResourcesCard } from './DependsOnResourcesCard';
@@ -14,79 +14,30 @@
* limitations under the License.
*/
import { ComponentEntity, RELATION_HAS_PART } from '@backstage/catalog-model';
import {
CodeSnippet,
InfoCard,
Link,
Progress,
WarningPanel,
} from '@backstage/core';
import { Typography } from '@material-ui/core';
import {
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import { RELATION_HAS_PART } from '@backstage/catalog-model';
import React from 'react';
import {
asComponentEntities,
componentEntityColumns,
componentEntityHelpLink,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
type Props = {
variant?: 'gridItem';
};
const columns = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'component' }),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createSpecTypeColumn(),
EntityTable.columns.createSpecLifecycleColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: RELATION_HAS_PART,
kind: 'Component',
});
if (loading) {
return (
<InfoCard variant={variant} title="Components">
<Progress />
</InfoCard>
);
}
if (error || !entities) {
return (
<InfoCard variant={variant} title="Components">
<WarningPanel
severity="error"
title="Could not load components"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</InfoCard>
);
}
return (
<EntityTable
title="Components"
<RelatedEntitiesCard
variant={variant}
emptyContent={
<div style={{ textAlign: 'center' }}>
<Typography variant="body1">
No component is part of this system.
</Typography>
<Typography variant="body2">
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component">
Learn how to change this.
</Link>
</Typography>
</div>
}
columns={columns}
entities={entities as ComponentEntity[]}
title="Components"
entityKind="Component"
relationType={RELATION_HAS_PART}
columns={componentEntityColumns}
emptyMessage="No component is part of this system"
emptyHelpLink={componentEntityHelpLink}
asRenderableEntities={asComponentEntities}
/>
);
};
@@ -0,0 +1,121 @@
/*
* 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 { HasResourcesCard } from './HasResourcesCard';
describe('<HasResourcesCard />', () => {
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}>
<HasResourcesCard />
</EntityProvider>
</Wrapper>,
);
expect(getByText('Resources')).toBeInTheDocument();
expect(
getByText(/No resource is part of this system/i),
).toBeInTheDocument();
});
it('shows related resources', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'System',
metadata: {
name: 'my-system',
namespace: 'my-namespace',
},
relations: [
{
target: {
kind: 'Resource',
namespace: 'my-namespace',
name: 'target-name',
},
type: RELATION_HAS_PART,
},
],
};
catalogApi.getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
kind: 'Resource',
metadata: {
name: 'target-name',
namespace: 'my-namespace',
},
spec: {},
},
],
});
const { getByText } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<HasResourcesCard />
</EntityProvider>
</Wrapper>,
);
await waitFor(() => {
expect(getByText('Resources')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
});
});
});
@@ -0,0 +1,43 @@
/*
* 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 } from '@backstage/catalog-model';
import React from 'react';
import {
asResourceEntities,
RelatedEntitiesCard,
resourceEntityColumns,
resourceEntityHelpLink,
} from '../RelatedEntitiesCard';
type Props = {
variant?: 'gridItem';
};
export const HasResourcesCard = ({ variant = 'gridItem' }: Props) => {
return (
<RelatedEntitiesCard
variant={variant}
title="Resources"
entityKind="Resource"
relationType={RELATION_HAS_PART}
columns={resourceEntityColumns}
asRenderableEntities={asResourceEntities}
emptyMessage="No resource is part of this system"
emptyHelpLink={resourceEntityHelpLink}
/>
);
};
@@ -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 { HasResourcesCard } from './HasResourcesCard';
@@ -14,79 +14,29 @@
* limitations under the License.
*/
import { ComponentEntity, RELATION_HAS_PART } from '@backstage/catalog-model';
import {
CodeSnippet,
InfoCard,
Link,
Progress,
WarningPanel,
} from '@backstage/core';
import { Typography } from '@material-ui/core';
import {
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import { RELATION_HAS_PART } from '@backstage/catalog-model';
import React from 'react';
import {
asComponentEntities,
componentEntityColumns,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
type Props = {
variant?: 'gridItem';
};
const columns = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'component' }),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createSpecTypeColumn(),
EntityTable.columns.createSpecLifecycleColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: RELATION_HAS_PART,
kind: 'Component',
});
if (loading) {
return (
<InfoCard variant={variant} title="Subcomponents">
<Progress />
</InfoCard>
);
}
if (error || !entities) {
return (
<InfoCard variant={variant} title="Subcomponents">
<WarningPanel
severity="error"
title="Could not load subcomponents"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</InfoCard>
);
}
return (
<EntityTable
title="Subcomponents"
<RelatedEntitiesCard
variant={variant}
emptyContent={
<div style={{ textAlign: 'center' }}>
<Typography variant="body1">
No subcomponent is part of this component.
</Typography>
<Typography variant="body2">
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#specsubcomponentof-optional">
Learn how to change this.
</Link>
</Typography>
</div>
}
columns={columns}
entities={entities as ComponentEntity[]}
title="Subcomponents"
entityKind="Component"
relationType={RELATION_HAS_PART}
columns={componentEntityColumns}
asRenderableEntities={asComponentEntities}
emptyMessage="No subcomponent is part of this component"
emptyHelpLink="https://backstage.io/docs/features/software-catalog/descriptor-format#specsubcomponentof-optional"
/>
);
};
@@ -14,76 +14,30 @@
* limitations under the License.
*/
import { RELATION_HAS_PART, SystemEntity } from '@backstage/catalog-model';
import {
CodeSnippet,
InfoCard,
Link,
Progress,
WarningPanel,
} from '@backstage/core';
import { Typography } from '@material-ui/core';
import {
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import { RELATION_HAS_PART } from '@backstage/catalog-model';
import React from 'react';
import {
asSystemEntities,
RelatedEntitiesCard,
systemEntityColumns,
systemEntityHelpLink,
} from '../RelatedEntitiesCard';
type Props = {
variant?: 'gridItem';
};
const columns = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'system' }),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
export const HasSystemsCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: RELATION_HAS_PART,
});
if (loading) {
return (
<InfoCard variant={variant} title="Systems">
<Progress />
</InfoCard>
);
}
if (error || !entities) {
return (
<InfoCard variant={variant} title="Systems">
<WarningPanel
severity="error"
title="Could not load systems"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</InfoCard>
);
}
return (
<EntityTable
title="Systems"
<RelatedEntitiesCard
variant={variant}
emptyContent={
<div style={{ textAlign: 'center' }}>
<Typography variant="body1">
No system is part of this domain.
</Typography>
<Typography variant="body2">
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system">
Learn how to change this.
</Link>
</Typography>
</div>
}
columns={columns}
entities={entities as SystemEntity[]}
title="Systems"
entityKind="System"
relationType={RELATION_HAS_PART}
columns={systemEntityColumns}
asRenderableEntities={asSystemEntities}
emptyMessage="No system is part of this domain"
emptyHelpLink={systemEntityHelpLink}
/>
);
};
@@ -0,0 +1,92 @@
/*
* 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 } from '@backstage/catalog-model';
import {
InfoCard,
Link,
Progress,
ResponseErrorPanel,
TableColumn,
} from '@backstage/core';
import { Typography } from '@material-ui/core';
import {
EntityTable,
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import React from 'react';
type Props<T extends Entity> = {
variant?: 'gridItem';
title: string;
columns: TableColumn<T>[];
entityKind: string;
relationType: string;
emptyMessage: string;
emptyHelpLink: string;
asRenderableEntities: (entities: Entity[]) => T[];
};
export function RelatedEntitiesCard<T extends Entity>({
variant = 'gridItem',
title,
columns,
entityKind,
relationType,
emptyMessage,
emptyHelpLink,
asRenderableEntities,
}: Props<T>) {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: relationType,
kind: entityKind,
});
if (loading) {
return (
<InfoCard variant={variant} title={title}>
<Progress />
</InfoCard>
);
}
if (error) {
return (
<InfoCard variant={variant} title={title}>
<ResponseErrorPanel error={error} />
</InfoCard>
);
}
return (
<EntityTable
title={title}
variant={variant}
emptyContent={
<div style={{ textAlign: 'center' }}>
<Typography variant="body1">{emptyMessage}</Typography>
<Typography variant="body2">
<Link to={emptyHelpLink}>Learn how to change this.</Link>
</Typography>
</div>
}
columns={columns}
entities={asRenderableEntities(entities || [])}
/>
);
}
@@ -0,0 +1,18 @@
/*
* 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 { RelatedEntitiesCard } from './RelatedEntitiesCard';
export * from './presets';
@@ -0,0 +1,57 @@
/*
* Copyright 2021 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,
Entity,
ResourceEntity,
SystemEntity,
} from '@backstage/catalog-model';
import { TableColumn } from '@backstage/core';
import { EntityTable } from '@backstage/plugin-catalog-react';
export const componentEntityColumns: TableColumn<ComponentEntity>[] = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'component' }),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createSpecTypeColumn(),
EntityTable.columns.createSpecLifecycleColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
export const componentEntityHelpLink: string =
'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component';
export const asComponentEntities = (entities: Entity[]): ComponentEntity[] =>
entities as ComponentEntity[];
export const resourceEntityColumns: TableColumn<ResourceEntity>[] = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'resource' }),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createSpecTypeColumn(),
EntityTable.columns.createSpecLifecycleColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
export const resourceEntityHelpLink: string =
'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-resource';
export const asResourceEntities = (entities: Entity[]): ResourceEntity[] =>
entities as ResourceEntity[];
export const systemEntityColumns: TableColumn<SystemEntity>[] = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'system' }),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
export const systemEntityHelpLink: string =
'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system';
export const asSystemEntities = (entities: Entity[]): SystemEntity[] =>
entities as SystemEntity[];
+3
View File
@@ -26,7 +26,10 @@ export {
catalogPlugin,
catalogPlugin as plugin,
EntityAboutCard,
EntityDependsOnComponentsCard,
EntityDependsOnResourcesCard,
EntityHasComponentsCard,
EntityHasResourcesCard,
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
EntityLinksCard,
+31
View File
@@ -115,6 +115,37 @@ export const EntityHasSubcomponentsCard = catalogPlugin.provide(
}),
);
export const EntityHasResourcesCard = catalogPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/HasResourcesCard').then(m => m.HasResourcesCard),
},
}),
);
export const EntityDependsOnComponentsCard = catalogPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/DependsOnComponentsCard').then(
m => m.DependsOnComponentsCard,
),
},
}),
);
export const EntityDependsOnResourcesCard = catalogPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/DependsOnResourcesCard').then(
m => m.DependsOnResourcesCard,
),
},
}),
);
export const EntitySystemDiagramCard = catalogPlugin.provide(
createComponentExtension({
component: {