balibabu commited on
Commit
dee7bd1
·
1 Parent(s): b61ed5f

feat: test buildNodesAndEdgesFromDSLComponents (#940)

Browse files

### What problem does this PR solve?
feat: test buildNodesAndEdgesFromDSLComponents #918

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

web/jest-setup.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ import '@testing-library/jest-dom';
2
+ import 'umi/test-setup';
web/jest.config.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Config, configUmiAlias, createConfig } from 'umi/test';
2
+
3
+ export default async () => {
4
+ return (await configUmiAlias({
5
+ ...createConfig({
6
+ target: 'browser',
7
+ jsTransformer: 'esbuild',
8
+ // config opts for esbuild , it will pass to esbuild directly
9
+ jsTransformerOpts: { jsx: 'automatic' },
10
+ }),
11
+ setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
12
+ collectCoverageFrom: [
13
+ '**/*.{ts,tsx,js,jsx}',
14
+ '!.umi/**',
15
+ '!.umi-test/**',
16
+ '!.umi-production/**',
17
+ '!.umirc.{js,ts}',
18
+ '!.umirc.*.{js,ts}',
19
+ '!jest.config.{js,ts}',
20
+ '!coverage/**',
21
+ '!dist/**',
22
+ '!config/**',
23
+ '!mock/**',
24
+ ],
25
+ // if you require some es-module npm package, please uncomment below line and insert your package name
26
+ // transformIgnorePatterns: ['node_modules/(?!.*(lodash-es|your-es-pkg-name)/)']
27
+ coverageThreshold: {
28
+ global: {
29
+ lines: 1,
30
+ },
31
+ },
32
+ })) as Config.InitialOptions;
33
+ };
web/package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
web/package.json CHANGED
@@ -7,7 +7,8 @@
7
  "postinstall": "umi setup",
8
  "lint": "umi lint --eslint-only",
9
  "setup": "umi setup",
10
- "start": "npm run dev"
 
11
  },
12
  "dependencies": {
13
  "@ant-design/icons": "^5.2.6",
@@ -18,6 +19,7 @@
18
  "antd": "^5.12.7",
19
  "axios": "^1.6.3",
20
  "classnames": "^2.5.1",
 
21
  "dayjs": "^1.11.10",
22
  "eventsource-parser": "^1.1.2",
23
  "i18next": "^23.7.16",
@@ -45,20 +47,28 @@
45
  },
46
  "devDependencies": {
47
  "@react-dev-inspector/umi4-plugin": "^2.0.1",
 
 
 
 
48
  "@types/lodash": "^4.14.202",
49
  "@types/react": "^18.0.33",
50
  "@types/react-copy-to-clipboard": "^5.0.7",
51
  "@types/react-dom": "^18.0.11",
52
  "@types/react-syntax-highlighter": "^15.5.11",
 
53
  "@types/uuid": "^9.0.8",
54
  "@types/webpack-env": "^1.18.4",
55
  "@umijs/lint": "^4.1.1",
56
  "@umijs/plugins": "^4.1.0",
57
  "cross-env": "^7.0.3",
 
 
58
  "prettier": "^3.2.4",
59
  "prettier-plugin-organize-imports": "^3.2.4",
60
  "prettier-plugin-packagejson": "^2.4.9",
61
  "react-dev-inspector": "^2.0.1",
 
62
  "typescript": "^5.0.3",
63
  "umi-plugin-icons": "^0.1.1"
64
  }
 
7
  "postinstall": "umi setup",
8
  "lint": "umi lint --eslint-only",
9
  "setup": "umi setup",
10
+ "start": "npm run dev",
11
+ "test": "jest --no-cache --coverage"
12
  },
13
  "dependencies": {
14
  "@ant-design/icons": "^5.2.6",
 
19
  "antd": "^5.12.7",
20
  "axios": "^1.6.3",
21
  "classnames": "^2.5.1",
22
+ "dagre": "^0.8.5",
23
  "dayjs": "^1.11.10",
24
  "eventsource-parser": "^1.1.2",
25
  "i18next": "^23.7.16",
 
47
  },
48
  "devDependencies": {
49
  "@react-dev-inspector/umi4-plugin": "^2.0.1",
50
+ "@testing-library/jest-dom": "^6.4.5",
51
+ "@testing-library/react": "^15.0.7",
52
+ "@types/dagre": "^0.7.52",
53
+ "@types/jest": "^29.5.12",
54
  "@types/lodash": "^4.14.202",
55
  "@types/react": "^18.0.33",
56
  "@types/react-copy-to-clipboard": "^5.0.7",
57
  "@types/react-dom": "^18.0.11",
58
  "@types/react-syntax-highlighter": "^15.5.11",
59
+ "@types/testing-library__jest-dom": "^6.0.0",
60
  "@types/uuid": "^9.0.8",
61
  "@types/webpack-env": "^1.18.4",
62
  "@umijs/lint": "^4.1.1",
63
  "@umijs/plugins": "^4.1.0",
64
  "cross-env": "^7.0.3",
65
+ "jest": "^29.7.0",
66
+ "jest-environment-jsdom": "^29.7.0",
67
  "prettier": "^3.2.4",
68
  "prettier-plugin-organize-imports": "^3.2.4",
69
  "prettier-plugin-packagejson": "^2.4.9",
70
  "react-dev-inspector": "^2.0.1",
71
+ "ts-node": "^10.9.2",
72
  "typescript": "^5.0.3",
73
  "umi-plugin-icons": "^0.1.1"
74
  }
web/src/pages/flow/canvas/index.tsx CHANGED
@@ -8,7 +8,6 @@ import ReactFlow, {
8
  OnConnect,
9
  OnEdgesChange,
10
  OnNodesChange,
11
- Position,
12
  addEdge,
13
  applyEdgeChanges,
14
  applyNodeChanges,
@@ -19,47 +18,24 @@ import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
19
 
20
  import FlowDrawer from '../flow-drawer';
21
  import { useHandleDrop, useShowDrawer } from '../hooks';
 
 
22
  import { TextUpdaterNode } from './node';
23
 
24
  const nodeTypes = { textUpdater: TextUpdaterNode };
25
 
26
- const initialNodes = [
27
- {
28
- sourcePosition: Position.Left,
29
- targetPosition: Position.Right,
30
- id: 'node-1',
31
- type: 'textUpdater',
32
- position: { x: 400, y: 100 },
33
- data: { label: 123 },
34
- },
35
- {
36
- sourcePosition: Position.Right,
37
- targetPosition: Position.Left,
38
- id: '1',
39
- data: { label: 'Hello' },
40
- position: { x: 0, y: 50 },
41
- type: 'input',
42
- },
43
- {
44
- sourcePosition: Position.Right,
45
- targetPosition: Position.Left,
46
- id: '2',
47
- data: { label: 'World' },
48
- position: { x: 200, y: 50 },
49
- },
50
- ];
51
-
52
- const initialEdges = [
53
- { id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
54
- ];
55
-
56
  interface IProps {
57
  sideWidth: number;
58
  }
59
 
60
  function FlowCanvas({ sideWidth }: IProps) {
61
- const [nodes, setNodes] = useState<Node[]>(initialNodes);
62
- const [edges, setEdges] = useState<Edge[]>(initialEdges);
 
 
 
 
 
63
  const { ref, menu, onNodeContextMenu, onPaneClick } =
64
  useHandleNodeContextMenu(sideWidth);
65
  const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
 
8
  OnConnect,
9
  OnEdgesChange,
10
  OnNodesChange,
 
11
  addEdge,
12
  applyEdgeChanges,
13
  applyNodeChanges,
 
18
 
19
  import FlowDrawer from '../flow-drawer';
20
  import { useHandleDrop, useShowDrawer } from '../hooks';
21
+ import { initialEdges, initialNodes } from '../mock';
22
+ import { getLayoutedElements } from '../utils';
23
  import { TextUpdaterNode } from './node';
24
 
25
  const nodeTypes = { textUpdater: TextUpdaterNode };
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  interface IProps {
28
  sideWidth: number;
29
  }
30
 
31
  function FlowCanvas({ sideWidth }: IProps) {
32
+ const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
33
+ initialNodes,
34
+ initialEdges,
35
+ 'LR',
36
+ );
37
+ const [nodes, setNodes] = useState<Node[]>(layoutedNodes);
38
+ const [edges, setEdges] = useState<Edge[]>(layoutedEdges);
39
  const { ref, menu, onNodeContextMenu, onPaneClick } =
40
  useHandleNodeContextMenu(sideWidth);
41
  const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
web/src/pages/flow/mock.tsx CHANGED
@@ -3,6 +3,7 @@ import {
3
  RocketOutlined,
4
  SendOutlined,
5
  } from '@ant-design/icons';
 
6
 
7
  export const componentList = [
8
  { name: 'Begin', icon: <SendOutlined />, description: '' },
@@ -10,6 +11,39 @@ export const componentList = [
10
  { name: 'Generate', icon: <MergeCellsOutlined />, description: '' },
11
  ];
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  export const dsl = {
14
  components: {
15
  begin: {
@@ -17,8 +51,8 @@ export const dsl = {
17
  component_name: 'Begin',
18
  params: {},
19
  },
20
- downstream: ['Answer:China'],
21
- upstream: [],
22
  },
23
  'Answer:China': {
24
  obj: {
 
3
  RocketOutlined,
4
  SendOutlined,
5
  } from '@ant-design/icons';
6
+ import { Position } from 'reactflow';
7
 
8
  export const componentList = [
9
  { name: 'Begin', icon: <SendOutlined />, description: '' },
 
11
  { name: 'Generate', icon: <MergeCellsOutlined />, description: '' },
12
  ];
13
 
14
+ export const initialNodes = [
15
+ {
16
+ sourcePosition: Position.Left,
17
+ targetPosition: Position.Right,
18
+ id: 'node-1',
19
+ type: 'textUpdater',
20
+ position: { x: 0, y: 0 },
21
+ // position: { x: 400, y: 100 },
22
+ data: { label: 123 },
23
+ },
24
+ {
25
+ sourcePosition: Position.Right,
26
+ targetPosition: Position.Left,
27
+ id: '1',
28
+ data: { label: 'Hello' },
29
+ position: { x: 0, y: 0 },
30
+ // position: { x: 0, y: 50 },
31
+ type: 'input',
32
+ },
33
+ {
34
+ sourcePosition: Position.Right,
35
+ targetPosition: Position.Left,
36
+ id: '2',
37
+ data: { label: 'World' },
38
+ position: { x: 0, y: 0 },
39
+ // position: { x: 200, y: 50 },
40
+ },
41
+ ];
42
+
43
+ export const initialEdges = [
44
+ { id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
45
+ ];
46
+
47
  export const dsl = {
48
  components: {
49
  begin: {
 
51
  component_name: 'Begin',
52
  params: {},
53
  },
54
+ downstream: ['Answer:China'], // other edge target is downstream, edge source is current node id
55
+ upstream: [], // edge source is upstream, edge target is current node id
56
  },
57
  'Answer:China': {
58
  obj: {
web/src/pages/flow/utils.test.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dsl } from './mock';
2
+ import { buildNodesAndEdgesFromDSLComponents } from './utils';
3
+
4
+ test('buildNodesAndEdgesFromDSLComponents', () => {
5
+ const { edges, nodes } = buildNodesAndEdgesFromDSLComponents(dsl.components);
6
+
7
+ expect(nodes.length).toEqual(4);
8
+ expect(edges.length).toEqual(4);
9
+
10
+ expect(edges).toEqual(
11
+ expect.arrayContaining([
12
+ expect.objectContaining({
13
+ source: 'begin',
14
+ target: 'Answer:China',
15
+ }),
16
+ expect.objectContaining({
17
+ source: 'Answer:China',
18
+ target: 'Retrieval:China',
19
+ }),
20
+ expect.objectContaining({
21
+ source: 'Retrieval:China',
22
+ target: 'Generate:China',
23
+ }),
24
+ expect.objectContaining({
25
+ source: 'Generate:China',
26
+ target: 'Answer:China',
27
+ }),
28
+ ]),
29
+ );
30
+ });
web/src/pages/flow/utils.ts CHANGED
@@ -1,10 +1,32 @@
1
  import { DSLComponents } from '@/interfaces/database/flow';
 
2
  import { Edge, Node, Position } from 'reactflow';
3
  import { v4 as uuidv4 } from 'uuid';
4
 
5
- export const buildNodesFromDSLComponents = (data: DSLComponents) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  const nodes: Node[] = [];
7
- const edges: Edge[] = [];
8
 
9
  Object.entries(data).forEach(([key, value]) => {
10
  const downstream = [...value.downstream];
@@ -23,22 +45,51 @@ export const buildNodesFromDSLComponents = (data: DSLComponents) => {
23
  targetPosition: Position.Right,
24
  });
25
 
26
- // intermediate node
27
- // The first and last nodes do not need to be considered
28
- if (upstream.length > 0 && downstream.length > 0) {
29
- for (let i = 0; i < upstream.length; i++) {
30
- const up = upstream[i];
31
- for (let j = 0; j < downstream.length; j++) {
32
- const down = downstream[j];
33
- edges.push({
34
- id: uuidv4(),
35
- label: '',
36
- type: 'step',
37
- source: up,
38
- target: down,
39
- });
40
- }
41
- }
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  });
 
 
44
  };
 
1
  import { DSLComponents } from '@/interfaces/database/flow';
2
+ import dagre from 'dagre';
3
  import { Edge, Node, Position } from 'reactflow';
4
  import { v4 as uuidv4 } from 'uuid';
5
 
6
+ const buildEdges = (
7
+ operatorIds: string[],
8
+ currentId: string,
9
+ allEdges: Edge[],
10
+ isUpstream = false,
11
+ ) => {
12
+ operatorIds.forEach((cur) => {
13
+ const source = isUpstream ? cur : currentId;
14
+ const target = isUpstream ? currentId : cur;
15
+ if (!allEdges.some((e) => e.source === source && e.target === target)) {
16
+ allEdges.push({
17
+ id: uuidv4(),
18
+ label: '',
19
+ type: 'step',
20
+ source: source,
21
+ target: target,
22
+ });
23
+ }
24
+ });
25
+ };
26
+
27
+ export const buildNodesAndEdgesFromDSLComponents = (data: DSLComponents) => {
28
  const nodes: Node[] = [];
29
+ let edges: Edge[] = [];
30
 
31
  Object.entries(data).forEach(([key, value]) => {
32
  const downstream = [...value.downstream];
 
45
  targetPosition: Position.Right,
46
  });
47
 
48
+ buildEdges(upstream, key, edges, true);
49
+ buildEdges(downstream, key, edges, false);
50
+ });
51
+
52
+ return { nodes, edges };
53
+ };
54
+
55
+ const dagreGraph = new dagre.graphlib.Graph();
56
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
57
+
58
+ const nodeWidth = 172;
59
+ const nodeHeight = 36;
60
+
61
+ export const getLayoutedElements = (
62
+ nodes: Node[],
63
+ edges: Edge[],
64
+ direction = 'TB',
65
+ ) => {
66
+ const isHorizontal = direction === 'LR';
67
+ dagreGraph.setGraph({ rankdir: direction });
68
+
69
+ nodes.forEach((node) => {
70
+ dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
71
+ });
72
+
73
+ edges.forEach((edge) => {
74
+ dagreGraph.setEdge(edge.source, edge.target);
75
+ });
76
+
77
+ dagre.layout(dagreGraph);
78
+
79
+ nodes.forEach((node) => {
80
+ const nodeWithPosition = dagreGraph.node(node.id);
81
+ node.targetPosition = isHorizontal ? Position.Left : Position.Top;
82
+ node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
83
+
84
+ // We are shifting the dagre node position (anchor=center center) to the top left
85
+ // so it matches the React Flow node anchor point (top left).
86
+ node.position = {
87
+ x: nodeWithPosition.x - nodeWidth / 2,
88
+ y: nodeWithPosition.y - nodeHeight / 2,
89
+ };
90
+
91
+ return node;
92
  });
93
+
94
+ return { nodes, edges };
95
  };
web/tsconfig.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "extends": "./src/.umi/tsconfig.json",
3
  "@@/*": [
4
- "src/.umi/*"
5
  ],
6
  }
 
1
  {
2
  "extends": "./src/.umi/tsconfig.json",
3
  "@@/*": [
4
+ "src/.umi/*",
5
  ],
6
  }