diff --git a/.changeset/sixty-humans-rescue.md b/.changeset/sixty-humans-rescue.md
new file mode 100644
index 0000000000..e6c9e78131
--- /dev/null
+++ b/.changeset/sixty-humans-rescue.md
@@ -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.
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index be5ae3e3bf..25630253d4 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -66,6 +66,8 @@ import {
isKind,
isOrphan,
hasLabels,
+ hasRelationWarnings,
+ EntityRelationWarning,
} from '@internal/plugin-catalog-customized';
import {
Direction,
@@ -328,6 +330,14 @@ const entityWarningContent = (
+
+
+
+
+
+
+
+
diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
index 54a05eea03..6722ea2d03 100644
--- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx
@@ -25,6 +25,8 @@ import {
isKind,
hasCatalogProcessingErrors,
isOrphan,
+ hasRelationWarnings,
+ EntityRelationWarning,
} from '@backstage/plugin-catalog';
import {
isGithubActionsAvailable,
@@ -101,6 +103,14 @@ const entityWarningContent = (
+
+
+
+
+
+
+
+
diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md
index f053c15c8a..358bb82b8a 100644
--- a/plugins/catalog/api-report.md
+++ b/plugins/catalog/api-report.md
@@ -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;
+
// @public (undocumented)
export interface HasResourcesCardProps {
// (undocumented)
diff --git a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.test.tsx b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.test.tsx
new file mode 100644
index 0000000000..41e5f7e3d5
--- /dev/null
+++ b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.test.tsx
@@ -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('', () => {
+ 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(
+
+
+
+
+ ,
+ );
+
+ 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(
+
+
+
+
+ ,
+ );
+
+ 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();
+ });
+});
diff --git a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx
new file mode 100644
index 0000000000..16384ac1d2
--- /dev/null
+++ b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ if (loading || !value || value.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+
+ This entity has relations to other entities, which can't be found in the
+ catalog.
+ Entities not found are: {value.join(', ')}
+
+ >
+ );
+}
diff --git a/plugins/catalog/src/components/EntityRelationWarning/index.ts b/plugins/catalog/src/components/EntityRelationWarning/index.ts
new file mode 100644
index 0000000000..1d88926f25
--- /dev/null
+++ b/plugins/catalog/src/components/EntityRelationWarning/index.ts
@@ -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';
diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts
index 42150dfd66..a691011c96 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -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';