feat: test buildNodesAndEdgesFromDSLComponents (#940)

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

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-05-27 19:35:14 +08:00 committed by GitHub
parent 571aaaff22
commit d9bc093df1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 4844 additions and 188 deletions

2
web/jest-setup.ts Normal file
View File

@ -0,0 +1,2 @@
import '@testing-library/jest-dom';
import 'umi/test-setup';

33
web/jest.config.ts Normal file
View File

@ -0,0 +1,33 @@
import { Config, configUmiAlias, createConfig } from 'umi/test';
export default async () => {
return (await configUmiAlias({
...createConfig({
target: 'browser',
jsTransformer: 'esbuild',
// config opts for esbuild , it will pass to esbuild directly
jsTransformerOpts: { jsx: 'automatic' },
}),
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
collectCoverageFrom: [
'**/*.{ts,tsx,js,jsx}',
'!.umi/**',
'!.umi-test/**',
'!.umi-production/**',
'!.umirc.{js,ts}',
'!.umirc.*.{js,ts}',
'!jest.config.{js,ts}',
'!coverage/**',
'!dist/**',
'!config/**',
'!mock/**',
],
// if you require some es-module npm package, please uncomment below line and insert your package name
// transformIgnorePatterns: ['node_modules/(?!.*(lodash-es|your-es-pkg-name)/)']
coverageThreshold: {
global: {
lines: 1,
},
},
})) as Config.InitialOptions;
};

4784
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,8 @@
"postinstall": "umi setup", "postinstall": "umi setup",
"lint": "umi lint --eslint-only", "lint": "umi lint --eslint-only",
"setup": "umi setup", "setup": "umi setup",
"start": "npm run dev" "start": "npm run dev",
"test": "jest --no-cache --coverage"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
@ -18,6 +19,7 @@
"antd": "^5.12.7", "antd": "^5.12.7",
"axios": "^1.6.3", "axios": "^1.6.3",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"dagre": "^0.8.5",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"eventsource-parser": "^1.1.2", "eventsource-parser": "^1.1.2",
"i18next": "^23.7.16", "i18next": "^23.7.16",
@ -45,20 +47,28 @@
}, },
"devDependencies": { "devDependencies": {
"@react-dev-inspector/umi4-plugin": "^2.0.1", "@react-dev-inspector/umi4-plugin": "^2.0.1",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/dagre": "^0.7.52",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202", "@types/lodash": "^4.14.202",
"@types/react": "^18.0.33", "@types/react": "^18.0.33",
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@types/testing-library__jest-dom": "^6.0.0",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/webpack-env": "^1.18.4", "@types/webpack-env": "^1.18.4",
"@umijs/lint": "^4.1.1", "@umijs/lint": "^4.1.1",
"@umijs/plugins": "^4.1.0", "@umijs/plugins": "^4.1.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-packagejson": "^2.4.9", "prettier-plugin-packagejson": "^2.4.9",
"react-dev-inspector": "^2.0.1", "react-dev-inspector": "^2.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.0.3", "typescript": "^5.0.3",
"umi-plugin-icons": "^0.1.1" "umi-plugin-icons": "^0.1.1"
} }

View File

@ -8,7 +8,6 @@ import ReactFlow, {
OnConnect, OnConnect,
OnEdgesChange, OnEdgesChange,
OnNodesChange, OnNodesChange,
Position,
addEdge, addEdge,
applyEdgeChanges, applyEdgeChanges,
applyNodeChanges, applyNodeChanges,
@ -19,47 +18,24 @@ import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
import FlowDrawer from '../flow-drawer'; import FlowDrawer from '../flow-drawer';
import { useHandleDrop, useShowDrawer } from '../hooks'; import { useHandleDrop, useShowDrawer } from '../hooks';
import { initialEdges, initialNodes } from '../mock';
import { getLayoutedElements } from '../utils';
import { TextUpdaterNode } from './node'; import { TextUpdaterNode } from './node';
const nodeTypes = { textUpdater: TextUpdaterNode }; const nodeTypes = { textUpdater: TextUpdaterNode };
const initialNodes = [
{
sourcePosition: Position.Left,
targetPosition: Position.Right,
id: 'node-1',
type: 'textUpdater',
position: { x: 400, y: 100 },
data: { label: 123 },
},
{
sourcePosition: Position.Right,
targetPosition: Position.Left,
id: '1',
data: { label: 'Hello' },
position: { x: 0, y: 50 },
type: 'input',
},
{
sourcePosition: Position.Right,
targetPosition: Position.Left,
id: '2',
data: { label: 'World' },
position: { x: 200, y: 50 },
},
];
const initialEdges = [
{ id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
];
interface IProps { interface IProps {
sideWidth: number; sideWidth: number;
} }
function FlowCanvas({ sideWidth }: IProps) { function FlowCanvas({ sideWidth }: IProps) {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
const [edges, setEdges] = useState<Edge[]>(initialEdges); initialNodes,
initialEdges,
'LR',
);
const [nodes, setNodes] = useState<Node[]>(layoutedNodes);
const [edges, setEdges] = useState<Edge[]>(layoutedEdges);
const { ref, menu, onNodeContextMenu, onPaneClick } = const { ref, menu, onNodeContextMenu, onPaneClick } =
useHandleNodeContextMenu(sideWidth); useHandleNodeContextMenu(sideWidth);
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer(); const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();

View File

@ -3,6 +3,7 @@ import {
RocketOutlined, RocketOutlined,
SendOutlined, SendOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Position } from 'reactflow';
export const componentList = [ export const componentList = [
{ name: 'Begin', icon: <SendOutlined />, description: '' }, { name: 'Begin', icon: <SendOutlined />, description: '' },
@ -10,6 +11,39 @@ export const componentList = [
{ name: 'Generate', icon: <MergeCellsOutlined />, description: '' }, { name: 'Generate', icon: <MergeCellsOutlined />, description: '' },
]; ];
export const initialNodes = [
{
sourcePosition: Position.Left,
targetPosition: Position.Right,
id: 'node-1',
type: 'textUpdater',
position: { x: 0, y: 0 },
// position: { x: 400, y: 100 },
data: { label: 123 },
},
{
sourcePosition: Position.Right,
targetPosition: Position.Left,
id: '1',
data: { label: 'Hello' },
position: { x: 0, y: 0 },
// position: { x: 0, y: 50 },
type: 'input',
},
{
sourcePosition: Position.Right,
targetPosition: Position.Left,
id: '2',
data: { label: 'World' },
position: { x: 0, y: 0 },
// position: { x: 200, y: 50 },
},
];
export const initialEdges = [
{ id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
];
export const dsl = { export const dsl = {
components: { components: {
begin: { begin: {
@ -17,8 +51,8 @@ export const dsl = {
component_name: 'Begin', component_name: 'Begin',
params: {}, params: {},
}, },
downstream: ['Answer:China'], downstream: ['Answer:China'], // other edge target is downstream, edge source is current node id
upstream: [], upstream: [], // edge source is upstream, edge target is current node id
}, },
'Answer:China': { 'Answer:China': {
obj: { obj: {

View File

@ -0,0 +1,30 @@
import { dsl } from './mock';
import { buildNodesAndEdgesFromDSLComponents } from './utils';
test('buildNodesAndEdgesFromDSLComponents', () => {
const { edges, nodes } = buildNodesAndEdgesFromDSLComponents(dsl.components);
expect(nodes.length).toEqual(4);
expect(edges.length).toEqual(4);
expect(edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
source: 'begin',
target: 'Answer:China',
}),
expect.objectContaining({
source: 'Answer:China',
target: 'Retrieval:China',
}),
expect.objectContaining({
source: 'Retrieval:China',
target: 'Generate:China',
}),
expect.objectContaining({
source: 'Generate:China',
target: 'Answer:China',
}),
]),
);
});

View File

@ -1,10 +1,32 @@
import { DSLComponents } from '@/interfaces/database/flow'; import { DSLComponents } from '@/interfaces/database/flow';
import dagre from 'dagre';
import { Edge, Node, Position } from 'reactflow'; import { Edge, Node, Position } from 'reactflow';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
export const buildNodesFromDSLComponents = (data: DSLComponents) => { const buildEdges = (
operatorIds: string[],
currentId: string,
allEdges: Edge[],
isUpstream = false,
) => {
operatorIds.forEach((cur) => {
const source = isUpstream ? cur : currentId;
const target = isUpstream ? currentId : cur;
if (!allEdges.some((e) => e.source === source && e.target === target)) {
allEdges.push({
id: uuidv4(),
label: '',
type: 'step',
source: source,
target: target,
});
}
});
};
export const buildNodesAndEdgesFromDSLComponents = (data: DSLComponents) => {
const nodes: Node[] = []; const nodes: Node[] = [];
const edges: Edge[] = []; let edges: Edge[] = [];
Object.entries(data).forEach(([key, value]) => { Object.entries(data).forEach(([key, value]) => {
const downstream = [...value.downstream]; const downstream = [...value.downstream];
@ -23,22 +45,51 @@ export const buildNodesFromDSLComponents = (data: DSLComponents) => {
targetPosition: Position.Right, targetPosition: Position.Right,
}); });
// intermediate node buildEdges(upstream, key, edges, true);
// The first and last nodes do not need to be considered buildEdges(downstream, key, edges, false);
if (upstream.length > 0 && downstream.length > 0) {
for (let i = 0; i < upstream.length; i++) {
const up = upstream[i];
for (let j = 0; j < downstream.length; j++) {
const down = downstream[j];
edges.push({
id: uuidv4(),
label: '',
type: 'step',
source: up,
target: down,
});
}
}
}
}); });
return { nodes, edges };
};
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 172;
const nodeHeight = 36;
export const getLayoutedElements = (
nodes: Node[],
edges: Edge[],
direction = 'TB',
) => {
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
};
return node;
});
return { nodes, edges };
}; };

View File

@ -1,6 +1,6 @@
{ {
"extends": "./src/.umi/tsconfig.json", "extends": "./src/.umi/tsconfig.json",
"@@/*": [ "@@/*": [
"src/.umi/*" "src/.umi/*",
], ],
} }