From f2f89445f970ad17de8b21d754c073f0c0227f7f Mon Sep 17 00:00:00 2001 From: Tarcisio Ambrosio Wensing Date: Tue, 24 Aug 2021 12:54:43 -0300 Subject: [PATCH] handle big names in explore plugin Signed-off-by: Tarcisio Ambrosio Wensing --- .changeset/lemon-fishes-build.md | 6 + packages/catalog-model/examples/acme/org.yaml | 1 + .../examples/acme/team-e-big-name-group.yaml | 13 ++ .../GroupsDiagram.test.tsx | 181 +++++++++++++++++- .../GroupsExplorerContent/GroupsDiagram.tsx | 105 +++++++++- 5 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 .changeset/lemon-fishes-build.md create mode 100644 packages/catalog-model/examples/acme/team-e-big-name-group.yaml 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. */