From f2f89445f970ad17de8b21d754c073f0c0227f7f Mon Sep 17 00:00:00 2001 From: Tarcisio Ambrosio Wensing Date: Tue, 24 Aug 2021 12:54:43 -0300 Subject: [PATCH 1/5] 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. */ From 68a709789e4f49e595a619d8db6ebff12ec4663f Mon Sep 17 00:00:00 2001 From: Tarcisio Ambrosio Wensing Date: Tue, 24 Aug 2021 14:20:21 -0300 Subject: [PATCH 2/5] fix prettier Signed-off-by: Tarcisio Ambrosio Wensing --- packages/catalog-model/examples/acme/team-e-big-name-group.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ebd26fc860..3242e5a715 100644 --- a/packages/catalog-model/examples/acme/team-e-big-name-group.yaml +++ b/packages/catalog-model/examples/acme/team-e-big-name-group.yaml @@ -10,4 +10,4 @@ spec: 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 + children: [] From 17ea7f7802827349800f17af5d19825a02663abe Mon Sep 17 00:00:00 2001 From: Tarcisio Ambrosio Wensing Date: Tue, 24 Aug 2021 22:20:24 -0300 Subject: [PATCH 3/5] making it thousand times more simple and remove useless files Signed-off-by: Tarcisio Ambrosio Wensing --- .changeset/lemon-fishes-build.md | 1 - packages/catalog-model/examples/acme/org.yaml | 1 - .../examples/acme/team-e-big-name-group.yaml | 13 -- .../GroupsDiagram.test.tsx | 180 +----------------- .../GroupsExplorerContent/GroupsDiagram.tsx | 143 +++++--------- 5 files changed, 46 insertions(+), 292 deletions(-) delete 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 index 965fba2bb6..0b1ba4090c 100644 --- a/.changeset/lemon-fishes-build.md +++ b/.changeset/lemon-fishes-build.md @@ -1,5 +1,4 @@ --- -'@backstage/catalog-model': patch '@backstage/plugin-explore': patch --- diff --git a/packages/catalog-model/examples/acme/org.yaml b/packages/catalog-model/examples/acme/org.yaml index b98647aec5..9a0d690b57 100644 --- a/packages/catalog-model/examples/acme/org.yaml +++ b/packages/catalog-model/examples/acme/org.yaml @@ -30,4 +30,3 @@ 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 deleted file mode 100644 index 3242e5a715..0000000000 --- a/packages/catalog-model/examples/acme/team-e-big-name-group.yaml +++ /dev/null @@ -1,13 +0,0 @@ -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: [] diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx index bce7e88191..66f54900c9 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx @@ -66,184 +66,6 @@ describe('', () => { }, }, ); - expect(getByText('Group A', { selector: 'tspan' })).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(); + expect(getByText('Group A', { selector: 'div' })).toBeInTheDocument(); }); }); diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index 53a16f1114..19570a8802 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -50,15 +50,32 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ fill: 'yellowgreen', stroke: theme.palette.border, }, + centeredContent: { + padding: '10px', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: 'black', + }, + textWrapper: { + display: '-webkit-box', + WebkitBoxOrient: 'vertical', + WebkitLineClamp: 2, + overflow: 'hidden', + textOverflow: 'ellipsis', + textAlign: 'center', + fontWeight: 'bold', + fontSize: '20px', + }, })); -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; +const dimensions = { + nodeWidth: 180, + nodeHeight: 90, + nodeCornerRadius: 20, + nodeAligmentShift: 5, +}; function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { const classes = useStyles(); @@ -68,127 +85,57 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { return ( {props.node.name} - - {props.node.name} - +
+
{props.node.name}
+
+
); } const ref = parseEntityRef(props.node.id); - const nameChunks = splitNameInChunks(props.node.name); - const objs = prepareForDisplay(nameChunks); return ( {props.node.name} - - {objs.map(function (object: any, i: number) { - return ( - - {object.text} - - ); - })} - +
+
{props.node.name}
+
+
); } -/** - * 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. */ From 226d6d2f1ea41f344127c0fe1485ce4e810eb319 Mon Sep 17 00:00:00 2001 From: Tarcisio Ambrosio Wensing Date: Wed, 25 Aug 2021 09:07:31 -0300 Subject: [PATCH 4/5] remove wrong classname ref Signed-off-by: Tarcisio Ambrosio Wensing --- .../src/components/GroupsExplorerContent/GroupsDiagram.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index 19570a8802..c865920e7d 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -116,7 +116,6 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { {props.node.name} Date: Wed, 25 Aug 2021 12:10:38 -0300 Subject: [PATCH 5/5] fix missing selector in test Signed-off-by: Tarcisio Ambrosio Wensing --- .../GroupsExplorerContent/GroupsExplorerContent.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 7b3a73673e..52def47f7c 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -79,7 +79,9 @@ describe('', () => { ); await waitFor(() => { - expect(getByText('my-namespace/group-a')).toBeInTheDocument(); + expect( + getByText('my-namespace/group-a', { selector: 'div' }), + ).toBeInTheDocument(); }); }); @@ -93,7 +95,9 @@ describe('', () => { mountedRoutes, ); - await waitFor(() => expect(getByText('Our Teams')).toBeInTheDocument()); + await waitFor(() => + expect(getByText('Our Teams', { selector: 'h2' })).toBeInTheDocument(), + ); }); it('renders a friendly error if it cannot collect domains', async () => {