added HasResourcesCard for system resources, and DependsOnComponentsCard/DependsOnResourcesCard for component dependencies

Signed-off-by: Jonah Grimes <jgrimes@appriss.com>
This commit is contained in:
Jonah Grimes
2021-04-16 09:26:50 -04:00
parent 0bfcc4323a
commit 536834515b
10 changed files with 713 additions and 0 deletions
@@ -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_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.getEntityByName.mockResolvedValue({
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,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 { ComponentEntity, RELATION_DEPENDS_ON } 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 React from 'react';
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 DependsOnComponentsCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: RELATION_DEPENDS_ON,
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"
variant={variant}
emptyContent={
<div style={{ textAlign: 'center' }}>
<Typography variant="body1">
No component is a dependency of this component.
</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[]}
/>
);
};
@@ -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,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_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.getEntityByName.mockResolvedValue({
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,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 { ComponentEntity, RELATION_DEPENDS_ON } 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 React from 'react';
type Props = {
variant?: 'gridItem';
};
const columns = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'resource' }),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createSpecTypeColumn(),
EntityTable.columns.createSpecLifecycleColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
export const DependsOnResourcesCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: RELATION_DEPENDS_ON,
kind: 'Resource',
});
if (loading) {
return (
<InfoCard variant={variant} title="Resources">
<Progress />
</InfoCard>
);
}
if (error || !entities) {
return (
<InfoCard variant={variant} title="Resources">
<WarningPanel
severity="error"
title="Could not load resources"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</InfoCard>
);
}
return (
<EntityTable
title="Resources"
variant={variant}
emptyContent={
<div style={{ textAlign: 'center' }}>
<Typography variant="body1">
No resource is a dependency of this component.
</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[]}
/>
);
};
@@ -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';
@@ -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 { HasResourcesCard } from './HasResourcesCard';
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}>
<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.getEntityByName.mockResolvedValue({
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,94 @@
/*
* 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 { ResourceEntity, 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 React from 'react';
type Props = {
variant?: 'gridItem';
};
const columns = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'resource' }),
EntityTable.columns.createOwnerColumn(),
EntityTable.columns.createSpecTypeColumn(),
EntityTable.columns.createSpecLifecycleColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
];
export const HasResourcesCard = ({
variant = 'gridItem',
}: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: RELATION_HAS_PART,
kind: 'Resource',
});
if (loading) {
return (
<InfoCard variant={variant} title="Resources">
<Progress />
</InfoCard>
);
}
if (error || !entities) {
return (
<InfoCard variant={variant} title="Resources">
<WarningPanel
severity="error"
title="Could not load resources"
message={<CodeSnippet text={`${error}`} language="text" />}
/>
</InfoCard>
);
}
return (
<EntityTable
title="Resources"
variant={variant}
emptyContent={
<div style={{ textAlign: 'center' }}>
<Typography variant="body1">
No resource is part of this system.
</Typography>
<Typography variant="body2">
<Link to="https://backstage.io/docs/features/software-catalog/descriptor-format#kind-resource">
Learn how to change this.
</Link>
</Typography>
</div>
}
columns={columns}
entities={entities as ResourceEntity[]}
/>
);
};
@@ -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';
+33
View File
@@ -115,6 +115,39 @@ 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: {