diff --git a/.changeset/healthy-stingrays-shout.md b/.changeset/healthy-stingrays-shout.md
new file mode 100644
index 0000000000..c3ce59ce1f
--- /dev/null
+++ b/.changeset/healthy-stingrays-shout.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog': patch
+---
+
+SystemDiagramCard UI improvements
diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx
index baa8c667d2..337e9d12b6 100644
--- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx
+++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx
@@ -19,6 +19,7 @@ import {
catalogApiRef,
CatalogApi,
EntityProvider,
+ entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { Entity, RELATION_PART_OF } from '@backstage/catalog-model';
import { renderInTestApp } from '@backstage/test-utils';
@@ -59,13 +60,16 @@ describe('', () => {
,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
);
expect(queryByText(/System Diagram/)).toBeInTheDocument();
- expect(queryByText(/system:my-namespace2\/my-system2/)).toBeInTheDocument();
- expect(
- queryByText(/component:my-namespace\/my-entity/),
- ).not.toBeInTheDocument();
+ expect(queryByText(/my-namespace2\/my-system2/)).toBeInTheDocument();
+ expect(queryByText(/my-namespace\/my-entity/)).not.toBeInTheDocument();
});
it('shows related systems', async () => {
@@ -115,10 +119,15 @@ describe('', () => {
,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
);
expect(getByText('System Diagram')).toBeInTheDocument();
- expect(getByText('system:my-namespace/my-system')).toBeInTheDocument();
- expect(getByText('component:my-namespace/my-entity')).toBeInTheDocument();
+ expect(getByText('my-namespace/my-system')).toBeInTheDocument();
+ expect(getByText('my-namespace/my-entity')).toBeInTheDocument();
});
});
diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx
index 78288c46e9..5fd32a6298 100644
--- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx
+++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx
@@ -19,10 +19,13 @@ import {
RELATION_DEPENDS_ON,
RELATION_PROVIDES_API,
RELATION_PART_OF,
- serializeEntityRef,
+ stringifyEntityRef,
+ ENTITY_DEFAULT_NAMESPACE,
+ parseEntityRef,
} from '@backstage/catalog-model';
import {
catalogApiRef,
+ entityRouteRef,
getEntityRelations,
useEntity,
} from '@backstage/plugin-catalog-react';
@@ -33,26 +36,107 @@ import {
Progress,
useApi,
ResponseErrorPanel,
+ Link,
+ useRouteRef,
} from '@backstage/core';
-import { Box, Typography } from '@material-ui/core';
+import { Box, makeStyles, Typography } from '@material-ui/core';
import ZoomOutMap from '@material-ui/icons/ZoomOutMap';
import React from 'react';
import { useAsync } from 'react-use';
+import { BackstageTheme } from '@backstage/theme';
-function simplifiedEntityName(
+const useStyles = makeStyles((theme: BackstageTheme) => ({
+ domainNode: {
+ fill: theme.palette.primary.main,
+ stroke: theme.palette.border,
+ },
+ systemNode: {
+ fill: 'coral',
+ stroke: theme.palette.border,
+ },
+ componentNode: {
+ fill: 'yellowgreen',
+ stroke: theme.palette.border,
+ },
+ apiNode: {
+ fill: theme.palette.gold,
+ stroke: theme.palette.border,
+ },
+ resourceNode: {
+ fill: 'grey',
+ stroke: theme.palette.border,
+ },
+}));
+
+// Simplifies the diagram output by hiding the default namespace and kind
+function readableEntityName(
ref:
| Entity
| {
- kind?: string;
+ kind: string;
namespace?: string;
name: string;
},
): string {
- // Simplify the diagram output by hiding only the default namespace
- return serializeEntityRef(ref)
- .toString()
+ return stringifyEntityRef(ref)
.toLocaleLowerCase('en-US')
- .replace(':default/', ':');
+ .replace(`:${ENTITY_DEFAULT_NAMESPACE}/`, ':')
+ .split(':')[1];
+}
+
+function RenderNode(props: DependencyGraphTypes.RenderNodeProps) {
+ const classes = useStyles();
+ const catalogEntityRoute = useRouteRef(entityRouteRef);
+ const kind = props.node.kind || 'Component';
+ const ref = parseEntityRef(props.node.id);
+ let nodeClass = classes.componentNode;
+
+ switch (kind) {
+ case 'Domain':
+ nodeClass = classes.domainNode;
+ break;
+ case 'System':
+ nodeClass = classes.systemNode;
+ break;
+ case 'Component':
+ nodeClass = classes.componentNode;
+ break;
+ case 'API':
+ nodeClass = classes.apiNode;
+ break;
+ case 'Resource':
+ nodeClass = classes.resourceNode;
+ break;
+ default:
+ nodeClass = classes.componentNode;
+ }
+
+ return (
+
+
+
+
+ {props.node.name}
+
+
+
+
+ {props.node.kind}
+
+
+ );
}
/**
@@ -62,8 +146,8 @@ function simplifiedEntityName(
export function SystemDiagramCard() {
const { entity } = useEntity();
const currentSystemName = entity.metadata.name;
- const currentSystemNode = simplifiedEntityName(entity);
- const systemNodes = new Array<{ id: string }>();
+ const currentSystemNode = stringifyEntityRef(entity);
+ const systemNodes = new Array<{ id: string; kind: string; name: string }>();
const systemEdges = new Array<{ from: string; to: string; label: string }>();
const catalogApi = useApi(catalogApiRef);
@@ -71,7 +155,10 @@ export function SystemDiagramCard() {
return catalogApi.getEntities({
filter: {
kind: ['Component', 'API', 'Resource', 'System', 'Domain'],
- 'spec.system': currentSystemName,
+ 'spec.system': [
+ currentSystemName,
+ `${ENTITY_DEFAULT_NAMESPACE}/${currentSystemName}`,
+ ],
},
});
}, [catalogApi, currentSystemName]);
@@ -79,22 +166,26 @@ export function SystemDiagramCard() {
// pick out the system itself
systemNodes.push({
id: currentSystemNode,
+ kind: 'System',
+ name: readableEntityName(entity),
});
// 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',
+ kind: 'Domain',
});
catalogItemDomain.forEach(foundDomain =>
systemNodes.push({
- id: simplifiedEntityName(foundDomain),
+ id: stringifyEntityRef(foundDomain),
+ kind: foundDomain.kind,
+ name: readableEntityName(foundDomain),
}),
);
catalogItemDomain.forEach(foundDomain =>
systemEdges.push({
from: currentSystemNode,
- to: simplifiedEntityName(foundDomain),
+ to: stringifyEntityRef(foundDomain),
label: 'part of',
}),
);
@@ -102,7 +193,9 @@ export function SystemDiagramCard() {
if (catalogResponse && catalogResponse.items) {
for (const catalogItem of catalogResponse.items) {
systemNodes.push({
- id: simplifiedEntityName(catalogItem),
+ id: stringifyEntityRef(catalogItem),
+ kind: catalogItem.kind,
+ name: readableEntityName(catalogItem),
});
// Check relations of the entity assigned to this system to see
@@ -115,8 +208,8 @@ export function SystemDiagramCard() {
);
catalogItemRelations_partOf.forEach(foundRelation =>
systemEdges.push({
- from: simplifiedEntityName(catalogItem),
- to: simplifiedEntityName(foundRelation),
+ from: stringifyEntityRef(catalogItem),
+ to: stringifyEntityRef(foundRelation),
label: 'part of',
}),
);
@@ -127,9 +220,9 @@ export function SystemDiagramCard() {
);
catalogItemRelations_providesApi.forEach(foundRelation =>
systemEdges.push({
- from: simplifiedEntityName(catalogItem),
- to: simplifiedEntityName(foundRelation),
- label: 'provides API',
+ from: stringifyEntityRef(catalogItem),
+ to: stringifyEntityRef(foundRelation),
+ label: 'provides',
}),
);
@@ -139,8 +232,8 @@ export function SystemDiagramCard() {
);
catalogItemRelations_dependsOn.forEach(foundRelation =>
systemEdges.push({
- from: simplifiedEntityName(catalogItem),
- to: simplifiedEntityName(foundRelation),
+ from: stringifyEntityRef(catalogItem),
+ to: stringifyEntityRef(foundRelation),
label: 'depends on',
}),
);
@@ -160,6 +253,7 @@ export function SystemDiagramCard() {
edges={systemEdges}
nodeMargin={10}
direction={DependencyGraphTypes.Direction.BOTTOM_TOP}
+ renderNode={RenderNode}
/>