fix: implement missing related entities

Signed-off-by: David Weber <david.weber@w3tec.ch>
This commit is contained in:
David Weber
2023-06-26 23:11:42 +02:00
parent 2a1caca3d6
commit 294b1629de
8 changed files with 339 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/create-app': patch
'@backstage/plugin-catalog': patch
---
Display a warning alert if relations are defined, which don't exist in the catalog.
@@ -66,6 +66,8 @@ import {
isKind,
isOrphan,
hasLabels,
hasRelationWarnings,
EntityRelationWarning,
} from '@internal/plugin-catalog-customized';
import {
Direction,
@@ -328,6 +330,14 @@ const entityWarningContent = (
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasRelationWarnings}>
<Grid item xs={12}>
<EntityRelationWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
@@ -25,6 +25,8 @@ import {
isKind,
hasCatalogProcessingErrors,
isOrphan,
hasRelationWarnings,
EntityRelationWarning,
} from '@backstage/plugin-catalog';
import {
isGithubActionsAvailable,
@@ -101,6 +103,14 @@ const entityWarningContent = (
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasRelationWarnings}>
<Grid item xs={12}>
<EntityRelationWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
+11
View File
@@ -377,6 +377,9 @@ export interface EntityPredicates {
// @public
export function EntityProcessingErrorsPanel(): JSX.Element | null;
// @public
export function EntityRelationWarning(): JSX.Element | null;
// @public (undocumented)
export const EntitySwitch: {
(props: EntitySwitchProps): JSX.Element;
@@ -429,6 +432,14 @@ export interface HasComponentsCardProps {
// @public
export function hasLabels(entity: Entity): boolean;
// @public
export function hasRelationWarnings(
entity: Entity,
context: {
apis: ApiHolder;
},
): Promise<boolean>;
// @public (undocumented)
export interface HasResourcesCardProps {
// (undocumented)
@@ -0,0 +1,176 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { ApiProvider } from '@backstage/core-app-api';
import {
CatalogApi,
catalogApiRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import React from 'react';
import {
EntityRelationWarning,
hasRelationWarnings,
} from './EntityRelationWarning';
describe('<EntityRelationWarning />', () => {
const getEntitiesByRefs: jest.MockedFunction<
CatalogApi['getEntitiesByRefs']
> = jest.fn();
const apis = TestApiRegistry.from([catalogApiRef, { getEntitiesByRefs }]);
const entityExisting: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'existing',
},
};
it('renders EntityRelationWarning if the entity has missing relations', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
},
relations: [
{
type: 'dependsOn',
targetRef: 'component:default/missing',
},
{
type: 'dependsOn',
targetRef: 'component:default/existing',
},
],
};
getEntitiesByRefs.mockResolvedValue({
items: [undefined, entityExisting],
});
await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<EntityRelationWarning />
</EntityProvider>
</ApiProvider>,
);
expect(
screen.getByText(content =>
content.includes(
"This entity has relations to other entities, which can't be found in the catalog.",
),
),
).toBeInTheDocument();
expect(
screen.getByText(content =>
content.includes('Entities not found are: component:default/missing'),
),
).toBeInTheDocument();
});
it("doesn't render EntityRelationWarning if the entity has no missing relations", async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
},
relations: [
{
type: 'dependsOn',
targetRef: 'component:default/existing',
},
],
};
getEntitiesByRefs.mockResolvedValue({
items: [entityExisting],
});
await renderInTestApp(
<ApiProvider apis={apis}>
<EntityProvider entity={entity}>
<EntityRelationWarning />
</EntityProvider>
</ApiProvider>,
);
expect(
screen.queryByText(content =>
content.includes(
"This entity has relations to other entities, which can't be found in the catalog.",
),
),
).not.toBeInTheDocument();
});
it('returns hasRelationWarnings truthy if the entity has missing relations', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
},
relations: [
{
type: 'dependsOn',
targetRef: 'component:default/missing',
},
{
type: 'dependsOn',
targetRef: 'component:default/existing',
},
],
};
getEntitiesByRefs.mockResolvedValue({
items: [undefined, entityExisting],
});
const hasWarnings = await hasRelationWarnings(entity, { apis });
expect(hasWarnings).toBeTruthy();
});
it('returns hasRelationWarnings falsy if the entity has no missing relations', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'software',
},
relations: [
{
type: 'dependsOn',
targetRef: 'component:default/existing',
},
],
};
getEntitiesByRefs.mockResolvedValue({
items: [entityExisting],
});
const hasWarnings = await hasRelationWarnings(entity, { apis });
expect(hasWarnings).toBeFalsy();
});
});
@@ -0,0 +1,105 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 {
CatalogApi,
catalogApiRef,
useEntity,
} from '@backstage/plugin-catalog-react';
import { Alert } from '@material-ui/lab';
import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import { Box } from '@material-ui/core';
import { ResponseErrorPanel } from '@backstage/core-components';
import { useApi, ApiHolder } from '@backstage/core-plugin-api';
async function getRelationWarnings(entity: Entity, catalogApi: CatalogApi) {
const entityRefRelations = entity.relations?.map(
relation => relation.targetRef,
);
if (
!entityRefRelations ||
entityRefRelations?.length < 1 ||
entityRefRelations.length > 1000
) {
return [];
}
const relatedEntities = await catalogApi.getEntitiesByRefs({
entityRefs: entityRefRelations,
fields: ['kind', 'metadata.name', 'metadata.namespace'],
});
return entityRefRelations.filter(
(_, index) => relatedEntities.items[index] === undefined,
);
}
/**
* Returns true if the given entity has relations to other entities, which
* don't exist in the catalog
*
* @public
*/
export async function hasRelationWarnings(
entity: Entity,
context: { apis: ApiHolder },
) {
const catalogApi = context.apis.get(catalogApiRef);
if (!catalogApi) {
throw new Error(`No implementation available for ${catalogApiRef}`);
}
const relatedEntitiesMissing = await getRelationWarnings(entity, catalogApi);
return relatedEntitiesMissing.length > 0;
}
/**
* Displays a warning alert if the entity has relations to other entities, which
* don't exist in the catalog
*
* @public
*/
export function EntityRelationWarning() {
const { entity } = useEntity();
const catalogApi = useApi(catalogApiRef);
const { loading, error, value } = useAsync(async () => {
return getRelationWarnings(entity, catalogApi);
}, [entity, catalogApi]);
if (error) {
return (
<Box mb={1}>
<ResponseErrorPanel error={error} />
</Box>
);
}
if (loading || !value || value.length === 0) {
return null;
}
return (
<>
<Alert severity="warning">
This entity has relations to other entities, which can't be found in the
catalog. <br />
Entities not found are: {value.join(', ')}
</Alert>
</>
);
}
@@ -0,0 +1,20 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 {
EntityRelationWarning,
hasRelationWarnings,
} from './EntityRelationWarning';
+1
View File
@@ -32,6 +32,7 @@ export * from './components/CatalogKindHeader';
export * from './components/CatalogTable';
export * from './components/EntityLayout';
export * from './components/EntityOrphanWarning';
export * from './components/EntityRelationWarning';
export * from './components/EntityProcessingErrorsPanel';
export * from './components/EntitySwitch';
export * from './components/FilteredEntityLayout';