diff --git a/.changeset/perfect-coats-train.md b/.changeset/perfect-coats-train.md
new file mode 100644
index 0000000000..9357f51370
--- /dev/null
+++ b/.changeset/perfect-coats-train.md
@@ -0,0 +1,26 @@
+---
+'@backstage/plugin-catalog': patch
+---
+
+Adds a new `EntitySystemDiagramCard` component to visually map all elements in a system.
+
+To use this new component with the legacy composability pattern, you can add a new tab with the component on to the System Entity Page in your `packages/app/src/components/catalog/EntityPage.tsx` file.
+
+For example,
+
+```diff
+ const SystemEntityPage = ({ entity }: { entity: Entity }) => (
+
+ }
+ />
++ }
++ />
+
+ );
+```
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index 0f9cc98f4b..47bb9ef09f 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -39,6 +39,7 @@ import {
EntityHasSystemsCard,
EntityLinksCard,
EntityPageLayout,
+ EntitySystemDiagramCard,
} from '@backstage/plugin-catalog';
import { EntityProvider, useEntity } from '@backstage/plugin-catalog-react';
import {
@@ -501,6 +502,11 @@ const SystemEntityPage = ({ entity }: { entity: Entity }) => (
title="Overview"
element={}
/>
+ }
+ />
);
diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx
new file mode 100644
index 0000000000..baa8c667d2
--- /dev/null
+++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * 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 { ApiProvider, ApiRegistry } from '@backstage/core';
+import {
+ catalogApiRef,
+ CatalogApi,
+ EntityProvider,
+} from '@backstage/plugin-catalog-react';
+import { Entity, RELATION_PART_OF } from '@backstage/catalog-model';
+import { renderInTestApp } from '@backstage/test-utils';
+import React from 'react';
+import { SystemDiagramCard } from './SystemDiagramCard';
+
+describe('', () => {
+ beforeAll(() => {
+ Object.defineProperty(window.SVGElement.prototype, 'getBBox', {
+ value: () => ({ width: 100, height: 100 }),
+ configurable: true,
+ });
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const catalogApi: Partial = {
+ getEntities: () =>
+ Promise.resolve({
+ items: [] as Entity[],
+ }),
+ };
+
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'System',
+ metadata: {
+ name: 'my-system2',
+ namespace: 'my-namespace2',
+ },
+ relations: [],
+ };
+
+ const { queryByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(queryByText(/System Diagram/)).toBeInTheDocument();
+ expect(queryByText(/system:my-namespace2\/my-system2/)).toBeInTheDocument();
+ expect(
+ queryByText(/component:my-namespace\/my-entity/),
+ ).not.toBeInTheDocument();
+ });
+
+ it('shows related systems', async () => {
+ const catalogApi: Partial = {
+ getEntities: () =>
+ Promise.resolve({
+ items: [
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-entity',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ owner: 'not-tools@example.com',
+ type: 'service',
+ system: 'my-system',
+ },
+ },
+ ] as Entity[],
+ }),
+ };
+
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'System',
+ metadata: {
+ name: 'my-system',
+ namespace: 'my-namespace',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'Domain',
+ namespace: 'my-namespace',
+ name: 'my-domain',
+ },
+ type: RELATION_PART_OF,
+ },
+ ],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(getByText('System Diagram')).toBeInTheDocument();
+ expect(getByText('system:my-namespace/my-system')).toBeInTheDocument();
+ expect(getByText('component:my-namespace/my-entity')).toBeInTheDocument();
+ });
+});
diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx
new file mode 100644
index 0000000000..df20e910aa
--- /dev/null
+++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx
@@ -0,0 +1,162 @@
+/*
+ * 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 {
+ Entity,
+ RELATION_PROVIDES_API,
+ RELATION_PART_OF,
+ serializeEntityRef,
+} from '@backstage/catalog-model';
+import {
+ catalogApiRef,
+ getEntityRelations,
+ useEntity,
+} from '@backstage/plugin-catalog-react';
+import {
+ DependencyGraph,
+ DependencyGraphTypes,
+ InfoCard,
+ Progress,
+ useApi,
+ ResponseErrorPanel,
+} from '@backstage/core';
+import { Box, Typography } from '@material-ui/core';
+import ZoomOutMap from '@material-ui/icons/ZoomOutMap';
+import React from 'react';
+import { useAsync } from 'react-use';
+
+function simplifiedEntityName(
+ ref:
+ | Entity
+ | {
+ kind?: string;
+ namespace?: string;
+ name: string;
+ },
+): string {
+ // Simplify the diagram output by hiding only the default namespace
+ return serializeEntityRef(ref)
+ .toString()
+ .toLocaleLowerCase('en-US')
+ .replace(':default/', ':');
+}
+
+/**
+ * Dynamically generates a diagram of a system, its assigned entities,
+ * and relationships of those entities.
+ */
+export function SystemDiagramCard() {
+ const { entity } = useEntity();
+ const currentSystemName = entity.metadata.name;
+ const currentSystemNode = simplifiedEntityName(entity);
+ const systemNodes = new Array<{ id: string }>();
+ const systemEdges = new Array<{ from: string; to: string; label: string }>();
+
+ const catalogApi = useApi(catalogApiRef);
+ const { loading, error, value: catalogResponse } = useAsync(() => {
+ return catalogApi.getEntities({
+ filter: {
+ kind: ['Component', 'API', 'Resource', 'System', 'Domain'],
+ 'spec.system': currentSystemName,
+ },
+ });
+ }, [catalogApi, currentSystemName]);
+
+ // pick out the system itself
+ systemNodes.push({
+ id: currentSystemNode,
+ });
+
+ // check if the system has an assigned domain
+ // even if the domain object doesn't exist in the catalog, display it in the map
+ const catalogItemDomain = getEntityRelations(entity, RELATION_PART_OF, {
+ kind: 'domain',
+ });
+ catalogItemDomain.forEach(foundDomain =>
+ systemNodes.push({
+ id: simplifiedEntityName(foundDomain),
+ }),
+ );
+ catalogItemDomain.forEach(foundDomain =>
+ systemEdges.push({
+ from: currentSystemNode,
+ to: simplifiedEntityName(foundDomain),
+ label: 'part of',
+ }),
+ );
+
+ if (catalogResponse && catalogResponse.items) {
+ for (const catalogItem of catalogResponse.items) {
+ systemNodes.push({
+ id: simplifiedEntityName(catalogItem),
+ });
+
+ // Check relations of the entity assigned to this system to see
+ // if it relates to other entities.
+ // Note those relations may, or may not, be explicitly
+ // assigned to the system.
+ const catalogItemRelations_partOf = getEntityRelations(
+ catalogItem,
+ RELATION_PART_OF,
+ );
+ catalogItemRelations_partOf.forEach(foundRelation =>
+ systemEdges.push({
+ from: simplifiedEntityName(catalogItem),
+ to: simplifiedEntityName(foundRelation),
+ label: 'part of',
+ }),
+ );
+
+ const catalogItemRelations_providesApi = getEntityRelations(
+ catalogItem,
+ RELATION_PROVIDES_API,
+ );
+
+ catalogItemRelations_providesApi.forEach(foundRelation =>
+ systemEdges.push({
+ from: simplifiedEntityName(catalogItem),
+ to: simplifiedEntityName(foundRelation),
+ label: 'provides API',
+ }),
+ );
+ }
+ }
+
+ if (loading) {
+ return ;
+ } else if (error) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ Use pinch & zoom
+ to move around the diagram.
+
+
+ );
+}
diff --git a/plugins/catalog/src/components/SystemDiagramCard/index.ts b/plugins/catalog/src/components/SystemDiagramCard/index.ts
new file mode 100644
index 0000000000..6d86b31b68
--- /dev/null
+++ b/plugins/catalog/src/components/SystemDiagramCard/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+export { SystemDiagramCard } from './SystemDiagramCard';
diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts
index 800c5c4693..7ab7b95dd7 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -29,4 +29,5 @@ export {
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
EntityLinksCard,
+ EntitySystemDiagramCard,
} from './plugin';
diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts
index 19dc778280..20c7a865db 100644
--- a/plugins/catalog/src/plugin.ts
+++ b/plugins/catalog/src/plugin.ts
@@ -114,3 +114,12 @@ export const EntityHasSubcomponentsCard = catalogPlugin.provide(
},
}),
);
+
+export const EntitySystemDiagramCard = catalogPlugin.provide(
+ createComponentExtension({
+ component: {
+ lazy: () =>
+ import('./components/SystemDiagramCard').then(m => m.SystemDiagramCard),
+ },
+ }),
+);