Skip to content

Commit 8c72cb5

Browse files
authored
chore/COMPASS-9767 Add editable interactions stress test story (#144)
1 parent 3c91663 commit 8c72cb5

File tree

4 files changed

+170
-44
lines changed

4 files changed

+170
-44
lines changed

src/components/diagram.stories.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EMPLOYEES_TO_EMPLOYEES_EDGE, ORDERS_TO_EMPLOYEES_EDGE } from '@/mocks/d
66
import { DiagramStressTestDecorator } from '@/mocks/decorators/diagram-stress-test.decorator';
77
import { DiagramConnectableDecorator } from '@/mocks/decorators/diagram-connectable.decorator';
88
import { DiagramEditableInteractionsDecorator } from '@/mocks/decorators/diagram-editable-interactions.decorator';
9+
import { DiagramEditableStressTestDecorator } from '@/mocks/decorators/diagram-editable-stress-test.decorator';
910

1011
const diagram: Meta<typeof Diagram> = {
1112
title: 'Diagram',
@@ -91,3 +92,13 @@ export const DiagramStressTest: Story = {
9192
nodes: [],
9293
},
9394
};
95+
96+
export const DiagramEditableStressTest: Story = {
97+
decorators: [DiagramEditableStressTestDecorator],
98+
args: {
99+
title: 'MongoDB Diagram',
100+
isDarkMode: true,
101+
edges: [],
102+
nodes: [],
103+
},
104+
};

src/mocks/decorators/diagram-editable-interactions.decorator.tsx

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useState, MouseEvent as ReactMouseEvent } from 'react';
1+
import { useCallback, useEffect, useRef, useState, MouseEvent as ReactMouseEvent } from 'react';
22
import { Decorator } from '@storybook/react';
33

44
import { DiagramProps, FieldId, NodeField, NodeProps } from '@/types';
@@ -49,8 +49,51 @@ function addFieldToNode(existingFields: NodeField[], parentFieldPath: string[])
4949
return fields;
5050
}
5151

52-
export const DiagramEditableInteractionsDecorator: Decorator<DiagramProps> = (Story, context) => {
53-
const [nodes, setNodes] = useState<NodeProps[]>(context.args.nodes);
52+
let idAccumulator: string[];
53+
let lastDepth = 0;
54+
// Used to build a string array id based on field depth.
55+
function idFromDepthAccumulator(name: string, depth?: number) {
56+
if (!depth) {
57+
idAccumulator = [name];
58+
} else if (depth > lastDepth) {
59+
idAccumulator.push(name);
60+
} else if (depth === lastDepth) {
61+
idAccumulator[idAccumulator.length - 1] = name;
62+
} else {
63+
idAccumulator = idAccumulator.slice(0, depth);
64+
idAccumulator[depth] = name;
65+
}
66+
lastDepth = depth ?? 0;
67+
return [...idAccumulator];
68+
}
69+
function editableNodesFromNodes(nodes: NodeProps[]): NodeProps[] {
70+
return nodes.map(node => ({
71+
...node,
72+
type: 'collection',
73+
fields: node.fields.map(field => ({
74+
...field,
75+
selectable: true,
76+
id: idFromDepthAccumulator(field.name, field.depth),
77+
})),
78+
}));
79+
}
80+
81+
export const useEditableNodes = (initialNodes: NodeProps[]) => {
82+
const [nodes, setNodes] = useState<NodeProps[]>([]);
83+
84+
const hasInitialized = useRef(false);
85+
useEffect(() => {
86+
if (hasInitialized.current) {
87+
return;
88+
}
89+
90+
if (!initialNodes || initialNodes.length === 0) {
91+
return;
92+
}
93+
94+
hasInitialized.current = true;
95+
setNodes(editableNodesFromNodes(initialNodes));
96+
}, [initialNodes]);
5497

5598
const onFieldClick = useCallback(
5699
(
@@ -61,18 +104,32 @@ export const DiagramEditableInteractionsDecorator: Decorator<DiagramProps> = (St
61104
},
62105
) => {
63106
setNodes(nodes =>
64-
nodes.map(node => ({
65-
...node,
66-
fields: node.fields.map(field => ({
67-
...field,
68-
selected:
107+
nodes.map(node => {
108+
let nodeFieldDidChange = false;
109+
const fields = node.fields.map(field => {
110+
const selected =
69111
params.nodeId === node.id &&
70112
!!field.id &&
71113
typeof field.id !== 'string' &&
72114
typeof params.id !== 'string' &&
73-
stringArrayCompare(params.id, field.id),
74-
})),
75-
})),
115+
stringArrayCompare(params.id, field.id);
116+
if (field.selected !== selected) {
117+
nodeFieldDidChange = true;
118+
}
119+
return {
120+
...field,
121+
selected,
122+
};
123+
});
124+
125+
if (!nodeFieldDidChange) {
126+
return node;
127+
}
128+
return {
129+
...node,
130+
fields,
131+
};
132+
}),
76133
);
77134
},
78135
[],
@@ -107,14 +164,17 @@ export const DiagramEditableInteractionsDecorator: Decorator<DiagramProps> = (St
107164
[],
108165
);
109166

167+
return { nodes, onFieldClick, onAddFieldToNodeClick, onAddFieldToObjectFieldClick };
168+
};
169+
170+
export const DiagramEditableInteractionsDecorator: Decorator<DiagramProps> = (Story, context) => {
171+
const editableArgs = useEditableNodes(context.args.nodes || []);
172+
110173
return Story({
111174
...context,
112175
args: {
113176
...context.args,
114-
nodes,
115-
onFieldClick,
116-
onAddFieldToNodeClick,
117-
onAddFieldToObjectFieldClick,
177+
...editableArgs,
118178
},
119179
});
120180
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Decorator } from '@storybook/react';
2+
3+
import { DiagramProps } from '@/types';
4+
import { useEditableNodes } from '@/mocks/decorators/diagram-editable-interactions.decorator';
5+
import { useStressTestNodesAndEdges } from '@/mocks/decorators/diagram-stress-test.decorator';
6+
7+
export const DiagramEditableStressTestDecorator: Decorator<DiagramProps> = (Story, context) => {
8+
const { nodes: initialNodes, edges } = useStressTestNodesAndEdges(100);
9+
const editableArgs = useEditableNodes(initialNodes);
10+
11+
return Story({
12+
...context,
13+
args: {
14+
...context.args,
15+
...editableArgs,
16+
edges: editableArgs.nodes.length > 0 ? edges : [],
17+
},
18+
});
19+
};

src/mocks/decorators/diagram-stress-test.decorator.tsx

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,45 +17,81 @@ const names = [
1717
'api_keys',
1818
];
1919

20-
export const DiagramStressTestDecorator: Decorator<DiagramProps> = (Story, context) => {
20+
const types = ['string', 'number', 'boolean', 'date', 'object', 'array'];
21+
22+
let previousWasObject = false;
23+
let previousDepth = 0;
24+
function getRandomTypeAndDepth(i: number) {
25+
if (i === 0) {
26+
previousWasObject = false;
27+
previousDepth = 0;
28+
}
29+
const type = types[Math.floor(Math.random() * types.length)];
30+
31+
const depth = previousWasObject
32+
? Math.random() > 0.25
33+
? previousDepth + 1
34+
: previousDepth
35+
: previousDepth > 0 && Math.random() > 0.2
36+
? previousDepth
37+
: 0;
38+
39+
previousWasObject = type === 'object';
40+
41+
previousDepth = depth || 0;
42+
43+
return {
44+
type,
45+
depth,
46+
};
47+
}
48+
49+
const generateNodes = (count: number): NodeProps[] => {
50+
return Array.from(Array(count).keys()).map((nodeIndex: number) => ({
51+
id: `node_${nodeIndex}`,
52+
type: 'table',
53+
position: {
54+
x: 0,
55+
y: 0,
56+
},
57+
title: names[Math.floor(Math.random() * names.length)],
58+
fields: Array.from(Array(1 + (nodeIndex % 9)).keys()).map((fieldIndex: number) => ({
59+
name: `${names[Math.floor(Math.random() * names.length)]}-${fieldIndex}`,
60+
...getRandomTypeAndDepth(fieldIndex),
61+
})),
62+
}));
63+
};
64+
65+
export const useStressTestNodesAndEdges = (nodeCount: number) => {
2166
const [nodes, setNodes] = useState<NodeProps[]>([]);
2267
const [edges, setEdges] = useState<EdgeProps[]>([]);
2368

2469
useEffect(() => {
25-
const nodes = generateNodes(100);
26-
const edges = generateEdges(nodes);
70+
const newNodes = generateNodes(nodeCount);
71+
const newEdges: EdgeProps[] = newNodes.map(node => ({
72+
id: `edge_${node.id}`,
73+
source: newNodes[Math.floor(Math.random() * newNodes.length)].id,
74+
target: newNodes[Math.floor(Math.random() * newNodes.length)].id,
75+
markerStart: 'many',
76+
markerEnd: 'one',
77+
}));
2778

28-
applyLayout<NodeProps, EdgeProps>(nodes, edges, 'STAR').then(result => {
79+
let applyUpdate = true;
80+
applyLayout<NodeProps, EdgeProps>(newNodes, newEdges, 'STAR').then(result => {
81+
if (!applyUpdate) return;
2982
setNodes(result.nodes);
3083
setEdges(result.edges);
3184
});
32-
}, []);
85+
return () => {
86+
applyUpdate = false;
87+
};
88+
}, [nodeCount]);
3389

34-
const generateEdges = (nodes: NodeProps[]): EdgeProps[] => {
35-
return nodes.map(node => ({
36-
id: `edge_${node.id}`,
37-
source: nodes[Math.floor(Math.random() * nodes.length)].id,
38-
target: nodes[Math.floor(Math.random() * nodes.length)].id,
39-
markerStart: 'many',
40-
markerEnd: 'one',
41-
}));
42-
};
90+
return { nodes, edges };
91+
};
4392

44-
const generateNodes = (count: number): NodeProps[] => {
45-
return Array.from(Array(count).keys()).map(i => ({
46-
id: `node_${i}`,
47-
type: 'table',
48-
position: {
49-
x: 0,
50-
y: 0,
51-
},
52-
title: names[Math.floor(Math.random() * names.length)],
53-
fields: Array.from(Array(1 + (i % 9)).keys()).map(_ => ({
54-
name: names[Math.floor(Math.random() * names.length)],
55-
type: 'varchar',
56-
})),
57-
}));
58-
};
93+
export const DiagramStressTestDecorator: Decorator<DiagramProps> = (Story, context) => {
94+
const { nodes, edges } = useStressTestNodesAndEdges(100);
5995

6096
return Story({
6197
...context,

0 commit comments

Comments
 (0)