fix: implement missing related entities
Signed-off-by: David Weber <david.weber@w3tec.ch>
This commit is contained in:
@@ -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}>
|
||||
|
||||
+10
@@ -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}>
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user