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 +2 -0
- web/jest.config.ts +33 -0
- web/package-lock.json +0 -0
- web/package.json +11 -1
- web/src/pages/flow/canvas/index.tsx +9 -33
- web/src/pages/flow/mock.tsx +36 -2
- web/src/pages/flow/utils.test.ts +30 -0
- web/src/pages/flow/utils.ts +70 -19
- web/tsconfig.json +1 -1
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
|
62 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
const nodes: Node[] = [];
|
7 |
-
|
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 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
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 |
}
|