handle big names in explore plugin

Signed-off-by: Tarcisio Ambrosio Wensing <tarcisio.wensing@midway.com.br>
This commit is contained in:
Tarcisio Ambrosio Wensing
2021-08-24 12:54:43 -03:00
parent 1d91d78490
commit f2f89445f9
5 changed files with 294 additions and 12 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/catalog-model': patch
'@backstage/plugin-explore': patch
---
Handle Group Big Names in explore plugin
@@ -30,3 +30,4 @@ spec:
- ./team-b-group.yaml
- ./team-c-group.yaml
- ./team-d-group.yaml
- ./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: []
@@ -33,7 +33,7 @@ describe('<GroupsDiagram />', () => {
});
});
it('shows groups', async () => {
it('show single group', async () => {
const catalogApi: Partial<CatalogApi> = {
getEntities: () =>
Promise.resolve({
@@ -66,7 +66,184 @@ describe('<GroupsDiagram />', () => {
},
},
);
expect(getByText('Group A', { selector: 'tspan' })).toBeInTheDocument();
});
expect(getByText('Group A')).toBeInTheDocument();
it('show multiple groups', async () => {
const catalogApi: Partial<CatalogApi> = {
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(
<ApiProvider apis={ApiRegistry.from([[catalogApiRef, catalogApi]])}>
<GroupsDiagram />
</ApiProvider>,
{
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<CatalogApi> = {
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(
<ApiProvider apis={ApiRegistry.from([[catalogApiRef, catalogApi]])}>
<GroupsDiagram />
</ApiProvider>,
{
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<CatalogApi> = {
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(
<ApiProvider apis={ApiRegistry.from([[catalogApiRef, catalogApi]])}>
<GroupsDiagram />
</ApiProvider>,
{
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();
});
});
@@ -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<any>) {
const classes = useStyles();
const catalogEntityRoute = useRouteRef(entityRouteRef);
@@ -60,14 +68,15 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps<any>) {
return (
<g>
<rect
width={180}
height={80}
rx={20}
width={NODE_WIDTH}
height={NODE_HEIGHT}
rx={NODE_CORNER_RADIUS}
className={classes.organizationNode}
/>
<title>{props.node.name}</title>
<text
x={90}
y={45}
x={NODE_WIDTH / 2}
y={NODE_HEIGHT / 2 + NODE_MIDDLE_ALIGNMENT_SHIFT}
textAnchor="middle"
alignmentBaseline="baseline"
style={{ fontWeight: 'bold' }}
@@ -79,10 +88,19 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps<any>) {
}
const ref = parseEntityRef(props.node.id);
const nameChunks = splitNameInChunks(props.node.name);
const objs = prepareForDisplay(nameChunks);
return (
<g>
<rect width={180} height={80} rx={20} className={classes.groupNode} />
<rect
width={NODE_WIDTH}
height={NODE_HEIGHT}
rx={NODE_CORNER_RADIUS}
className={classes.groupNode}
/>
<title>{props.node.name}</title>
<Link
to={catalogEntityRoute({
kind: ref.kind,
@@ -91,19 +109,86 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps<any>) {
})}
>
<text
x={90}
y={45}
x={NODE_WIDTH / 2}
y={NODE_HEIGHT / 2 + NODE_MIDDLE_ALIGNMENT_SHIFT}
textAnchor="middle"
alignmentBaseline="baseline"
style={{ fontWeight: 'bold' }}
style={{ fontWeight: 'bold', fontSize: TEXT_FONT_SIZE }}
>
{props.node.name}
{objs.map(function (object: any, i: number) {
return (
<tspan
key={i}
y={NODE_HEIGHT / 2 + NODE_MIDDLE_ALIGNMENT_SHIFT}
x="90"
textAnchor="middle"
dy={object.dy}
>
{object.text}
</tspan>
);
})}
</text>
</Link>
</g>
);
}
/**
* 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<string>, 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<any>, 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.
*/