diff --git a/.changeset/lemon-fishes-build.md b/.changeset/lemon-fishes-build.md
new file mode 100644
index 0000000000..965fba2bb6
--- /dev/null
+++ b/.changeset/lemon-fishes-build.md
@@ -0,0 +1,6 @@
+---
+'@backstage/catalog-model': patch
+'@backstage/plugin-explore': patch
+---
+
+Handle Group Big Names in explore plugin
diff --git a/packages/catalog-model/examples/acme/org.yaml b/packages/catalog-model/examples/acme/org.yaml
index 9a0d690b57..b98647aec5 100644
--- a/packages/catalog-model/examples/acme/org.yaml
+++ b/packages/catalog-model/examples/acme/org.yaml
@@ -30,3 +30,4 @@ spec:
- ./team-b-group.yaml
- ./team-c-group.yaml
- ./team-d-group.yaml
+ - ./team-e-big-name-group.yaml
diff --git a/packages/catalog-model/examples/acme/team-e-big-name-group.yaml b/packages/catalog-model/examples/acme/team-e-big-name-group.yaml
new file mode 100644
index 0000000000..ebd26fc860
--- /dev/null
+++ b/packages/catalog-model/examples/acme/team-e-big-name-group.yaml
@@ -0,0 +1,13 @@
+apiVersion: backstage.io/v1alpha1
+kind: Group
+metadata:
+ name: team-e
+ description: Team E With A Very Very Very Very Big Name Size
+spec:
+ type: team
+ profile:
+ displayName: Team E With A Very Very Very Very Big Name Size
+ email: team-e@example.com
+ picture: https://avatars.dicebear.com/api/identicon/team-e@example.com.svg?background=%23fff&margin=25
+ parent: boxoffice
+ children: []
\ No newline at end of file
diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx
index 3b9b523b02..bce7e88191 100644
--- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx
+++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx
@@ -33,7 +33,7 @@ describe('', () => {
});
});
- it('shows groups', async () => {
+ it('show single group', async () => {
const catalogApi: Partial = {
getEntities: () =>
Promise.resolve({
@@ -66,7 +66,184 @@ describe('', () => {
},
},
);
+ expect(getByText('Group A', { selector: 'tspan' })).toBeInTheDocument();
+ });
- expect(getByText('Group A')).toBeInTheDocument();
+ it('show multiple groups', async () => {
+ const catalogApi: Partial = {
+ getEntities: () =>
+ Promise.resolve({
+ items: [
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'group-a',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ profile: {
+ displayName: 'Group A',
+ },
+ type: 'organization',
+ },
+ },
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'group-b',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ profile: {
+ displayName: 'Group B',
+ },
+ type: 'organization',
+ },
+ },
+ ] as Entity[],
+ }),
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+ expect(getByText('Group A', { selector: 'tspan' })).toBeInTheDocument();
+ expect(getByText('Group B', { selector: 'tspan' })).toBeInTheDocument();
+ });
+
+ it('show single group with big name/displayName', async () => {
+ const catalogApi: Partial = {
+ getEntities: () =>
+ Promise.resolve({
+ items: [
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'group-a',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ profile: {
+ displayName:
+ 'Group A Big Name1 Name2 Name3 Name4 Name5 Name6',
+ },
+ type: 'organization',
+ },
+ },
+ ] as Entity[],
+ }),
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+
+ expect(
+ getByText('Group A Big Name1 Name2 Name3 Name4 Name5 Name6', {
+ selector: 'title',
+ }),
+ ).toBeInTheDocument();
+ expect(getByText('Group A Big', { selector: 'tspan' })).toBeInTheDocument();
+ expect(
+ getByText('Name1 Name2 Name3', { selector: 'tspan' }),
+ ).toBeInTheDocument();
+ expect(
+ getByText('Name4 Name5 Name6...', { selector: 'tspan' }),
+ ).toBeInTheDocument();
+ });
+
+ it('show multiple groups and one with big name', async () => {
+ const catalogApi: Partial = {
+ getEntities: () =>
+ Promise.resolve({
+ items: [
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'group-a',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ profile: {
+ displayName: 'Group A',
+ },
+ type: 'organization',
+ },
+ },
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'group-b',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ profile: {
+ displayName: 'Group B',
+ },
+ type: 'organization',
+ },
+ },
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'group-c',
+ namespace: 'my-namespace',
+ },
+ spec: {
+ profile: {
+ displayName:
+ 'Group C Big Name1 Name2 Name3 Name4 Name5 Name6',
+ },
+ type: 'organization',
+ },
+ },
+ ] as Entity[],
+ }),
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+ ,
+ {
+ mountedRoutes: {
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ );
+ expect(getByText('Group A', { selector: 'tspan' })).toBeInTheDocument();
+ expect(getByText('Group B', { selector: 'tspan' })).toBeInTheDocument();
+ expect(
+ getByText('Group C Big Name1 Name2 Name3 Name4 Name5 Name6', {
+ selector: 'title',
+ }),
+ ).toBeInTheDocument();
+ expect(getByText('Group C Big', { selector: 'tspan' })).toBeInTheDocument();
+ expect(
+ getByText('Name1 Name2 Name3', { selector: 'tspan' }),
+ ).toBeInTheDocument();
+ expect(
+ getByText('Name4 Name5 Name6...', { selector: 'tspan' }),
+ ).toBeInTheDocument();
});
});
diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx
index 71492c3520..53a16f1114 100644
--- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx
+++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx
@@ -52,6 +52,14 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({
},
}));
+const TEXT_FONT_SIZE: number = 15;
+const NODE_WIDTH: number = 180;
+const NODE_HEIGHT: number = 90;
+const NODE_CORNER_RADIUS: number = 20;
+const NODE_MIDDLE_ALIGNMENT_SHIFT: number = 5;
+const NODE_MAX_WORDS_PER_ROW: number = 3;
+const NODE_MAX_LINES: number = 3;
+
function RenderNode(props: DependencyGraphTypes.RenderNodeProps) {
const classes = useStyles();
const catalogEntityRoute = useRouteRef(entityRouteRef);
@@ -60,14 +68,15 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps) {
return (
+ {props.node.name}
) {
}
const ref = parseEntityRef(props.node.id);
+ const nameChunks = splitNameInChunks(props.node.name);
+ const objs = prepareForDisplay(nameChunks);
return (
-
+
+ {props.node.name}
+
) {
})}
>
- {props.node.name}
+ {objs.map(function (object: any, i: number) {
+ return (
+
+ {object.text}
+
+ );
+ })}
);
}
+/**
+ * Join chunks based on max words per line and add dy position shifting
+ * @param chunkedArray
+ * @returns
+ */
+function prepareForDisplay(chunkedArray: any): any {
+ return chunkedArray.map((val: Array, pos: number) => {
+ const text = val.join(' ');
+ // If array length == 1, dy should be 0 since there is no need to divide the node into blocks, we just need to centralize it
+ const dy = chunkedArray.length === 1 ? 0 : getDy(pos) - 30;
+ return { dy: dy, text: text };
+ });
+}
+
+/**
+ * text svg dy shifting based on array pos and in the blocks inside the node
+ * @param i text position on the array
+ */
+function getDy(i: number) {
+ const blocksSize = NODE_HEIGHT / NODE_MAX_LINES;
+ const position = blocksSize * i;
+ return position;
+}
+
+/**
+ * Create Chunked name based on NODE_MAX_LINES and NODE_MAX_WORDS_PER_ROW
+ * @param name from props.node.name
+ * @returns
+ */
+function splitNameInChunks(name: string) {
+ const array = name.split(' ');
+ const formated = array
+ .slice(0, NODE_MAX_LINES * NODE_MAX_WORDS_PER_ROW)
+ .map((it, pos) =>
+ pos + 1 === NODE_MAX_LINES * NODE_MAX_WORDS_PER_ROW ? `${it}...` : it,
+ );
+ const chunked = chunkArray(formated, NODE_MAX_WORDS_PER_ROW);
+ return chunked;
+}
+
+/**
+ * Returns an array with arrays of the given size.
+ *
+ * @param arr {Array} Array to split
+ * @param size {Integer} Size of each group
+ */
+function chunkArray(arr: Array, size: number) {
+ const results = [];
+
+ while (arr.length) {
+ results.push(arr.splice(0, size));
+ }
+ return results;
+}
+
/**
* Dynamically generates a diagram of groups registered in the catalog.
*/