balibabu commited on
Commit
e3322d7
·
1 Parent(s): a24094f

feat: add FlowHeader and delete edge (#959)

Browse files

### What problem does this PR solve?
feat: add FlowHeader and delete edge #918

### Type of change

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

web/package-lock.json CHANGED
@@ -16,6 +16,7 @@
16
  "classnames": "^2.5.1",
17
  "dagre": "^0.8.5",
18
  "dayjs": "^1.11.10",
 
19
  "eventsource-parser": "^1.1.2",
20
  "i18next": "^23.7.16",
21
  "i18next-browser-languagedetector": "^8.0.0",
@@ -11110,6 +11111,11 @@
11110
  "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.767.tgz",
11111
  "integrity": "sha512-nzzHfmQqBss7CE3apQHkHjXW77+8w3ubGCIoEijKCJebPufREaFETgGXWTkh32t259F3Kcq+R8MZdFdOJROgYw=="
11112
  },
 
 
 
 
 
11113
  "node_modules/elliptic": {
11114
  "version": "6.5.5",
11115
  "resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.5.5.tgz",
 
16
  "classnames": "^2.5.1",
17
  "dagre": "^0.8.5",
18
  "dayjs": "^1.11.10",
19
+ "elkjs": "^0.9.3",
20
  "eventsource-parser": "^1.1.2",
21
  "i18next": "^23.7.16",
22
  "i18next-browser-languagedetector": "^8.0.0",
 
11111
  "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.767.tgz",
11112
  "integrity": "sha512-nzzHfmQqBss7CE3apQHkHjXW77+8w3ubGCIoEijKCJebPufREaFETgGXWTkh32t259F3Kcq+R8MZdFdOJROgYw=="
11113
  },
11114
+ "node_modules/elkjs": {
11115
+ "version": "0.9.3",
11116
+ "resolved": "https://registry.npmmirror.com/elkjs/-/elkjs-0.9.3.tgz",
11117
+ "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
11118
+ },
11119
  "node_modules/elliptic": {
11120
  "version": "6.5.5",
11121
  "resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.5.5.tgz",
web/package.json CHANGED
@@ -21,6 +21,7 @@
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",
26
  "i18next-browser-languagedetector": "^8.0.0",
 
21
  "classnames": "^2.5.1",
22
  "dagre": "^0.8.5",
23
  "dayjs": "^1.11.10",
24
+ "elkjs": "^0.9.3",
25
  "eventsource-parser": "^1.1.2",
26
  "i18next": "^23.7.16",
27
  "i18next-browser-languagedetector": "^8.0.0",
web/src/locales/zh-traditional.ts CHANGED
@@ -24,7 +24,7 @@ export default {
24
  copied: '複製成功',
25
  comingSoon: '即將推出',
26
  download: '下載',
27
- close: '关闭',
28
  preview: '預覽',
29
  },
30
  login: {
 
24
  copied: '複製成功',
25
  comingSoon: '即將推出',
26
  download: '下載',
27
+ close: '關閉',
28
  preview: '預覽',
29
  },
30
  login: {
web/src/pages/flow/canvas/context-menu/index.tsx CHANGED
@@ -84,9 +84,6 @@ export const useHandleNodeContextMenu = (sideWidth: number) => {
84
  // event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
85
  // });
86
 
87
- console.info('clientX:', event.clientX);
88
- console.info('clientY:', event.clientY);
89
-
90
  setMenu({
91
  id: node.id,
92
  top: event.clientY - 72,
 
84
  // event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
85
  // });
86
 
 
 
 
87
  setMenu({
88
  id: node.id,
89
  top: event.clientY - 72,
web/src/pages/flow/canvas/index.tsx CHANGED
@@ -17,9 +17,13 @@ import 'reactflow/dist/style.css';
17
  import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
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 };
@@ -29,13 +33,11 @@ interface IProps {
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();
@@ -60,6 +62,8 @@ function FlowCanvas({ sideWidth }: IProps) {
60
 
61
  const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
62
 
 
 
63
  useEffect(() => {
64
  console.info('nodes:', nodes);
65
  console.info('edges:', edges);
@@ -82,6 +86,7 @@ function FlowCanvas({ sideWidth }: IProps) {
82
  onDragOver={onDragOver}
83
  onNodeClick={onNodeClick}
84
  onInit={setReactFlowInstance}
 
85
  >
86
  <Background />
87
  <Controls />
 
17
  import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
18
 
19
  import FlowDrawer from '../flow-drawer';
20
+ import {
21
+ useHandleDrop,
22
+ useHandleKeyUp,
23
+ useHandleSelectionChange,
24
+ useShowDrawer,
25
+ } from '../hooks';
26
+ import { dsl } from '../mock';
27
  import { TextUpdaterNode } from './node';
28
 
29
  const nodeTypes = { textUpdater: TextUpdaterNode };
 
33
  }
34
 
35
  function FlowCanvas({ sideWidth }: IProps) {
36
+ const [nodes, setNodes] = useState<Node[]>(dsl.graph.nodes);
37
+ const [edges, setEdges] = useState<Edge[]>(dsl.graph.edges);
38
+
39
+ const { selectedEdges, selectedNodes } = useHandleSelectionChange();
40
+
 
 
41
  const { ref, menu, onNodeContextMenu, onPaneClick } =
42
  useHandleNodeContextMenu(sideWidth);
43
  const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
 
62
 
63
  const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
64
 
65
+ const { handleKeyUp } = useHandleKeyUp(selectedEdges, selectedNodes);
66
+
67
  useEffect(() => {
68
  console.info('nodes:', nodes);
69
  console.info('edges:', edges);
 
86
  onDragOver={onDragOver}
87
  onNodeClick={onNodeClick}
88
  onInit={setReactFlowInstance}
89
+ onKeyUp={handleKeyUp}
90
  >
91
  <Background />
92
  <Controls />
web/src/pages/flow/elk-hooks.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useLayoutEffect } from 'react';
2
+ import { getLayoutedElements } from './elk-utils';
3
+
4
+ export const elkOptions = {
5
+ 'elk.algorithm': 'layered',
6
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '100',
7
+ 'elk.spacing.nodeNode': '80',
8
+ };
9
+
10
+ export const useLayoutGraph = (
11
+ initialNodes,
12
+ initialEdges,
13
+ setNodes,
14
+ setEdges,
15
+ ) => {
16
+ const onLayout = useCallback(({ direction, useInitialNodes = false }) => {
17
+ const opts = { 'elk.direction': direction, ...elkOptions };
18
+ const ns = initialNodes;
19
+ const es = initialEdges;
20
+
21
+ getLayoutedElements(ns, es, opts).then(
22
+ ({ nodes: layoutedNodes, edges: layoutedEdges }) => {
23
+ setNodes(layoutedNodes);
24
+ setEdges(layoutedEdges);
25
+
26
+ // window.requestAnimationFrame(() => fitView());
27
+ },
28
+ );
29
+ }, []);
30
+
31
+ // Calculate the initial layout on mount.
32
+ useLayoutEffect(() => {
33
+ onLayout({ direction: 'RIGHT', useInitialNodes: true });
34
+ }, [onLayout]);
35
+ };
web/src/pages/flow/elk-utils.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ELK from 'elkjs/lib/elk.bundled.js';
2
+ import { Edge, Node } from 'reactflow';
3
+
4
+ const elk = new ELK();
5
+
6
+ export const getLayoutedElements = (
7
+ nodes: Node[],
8
+ edges: Edge[],
9
+ options = {},
10
+ ) => {
11
+ const isHorizontal = options?.['elk.direction'] === 'RIGHT';
12
+ const graph = {
13
+ id: 'root',
14
+ layoutOptions: options,
15
+ children: nodes.map((node) => ({
16
+ ...node,
17
+ // Adjust the target and source handle positions based on the layout
18
+ // direction.
19
+ targetPosition: isHorizontal ? 'left' : 'top',
20
+ sourcePosition: isHorizontal ? 'right' : 'bottom',
21
+
22
+ // Hardcode a width and height for elk to use when layouting.
23
+ width: 150,
24
+ height: 50,
25
+ })),
26
+ edges: edges,
27
+ };
28
+
29
+ return elk
30
+ .layout(graph)
31
+ .then((layoutedGraph) => ({
32
+ nodes: layoutedGraph.children.map((node) => ({
33
+ ...node,
34
+ // React Flow expects a position property on the node instead of `x`
35
+ // and `y` fields.
36
+ position: { x: node.x, y: node.y },
37
+ })),
38
+
39
+ edges: layoutedGraph.edges,
40
+ }))
41
+ .catch(console.error);
42
+ };
web/src/pages/flow/header/index.less ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .flowHeader {
2
+ padding: 20px;
3
+ }
web/src/pages/flow/header/index.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button, Flex } from 'antd';
2
+
3
+ import { useSaveGraph } from '../hooks';
4
+ import styles from './index.less';
5
+
6
+ const FlowHeader = () => {
7
+ const { saveGraph } = useSaveGraph();
8
+
9
+ return (
10
+ <Flex
11
+ align="center"
12
+ justify="end"
13
+ gap={'large'}
14
+ className={styles.flowHeader}
15
+ >
16
+ <Button>
17
+ <b>Debug</b>
18
+ </Button>
19
+ <Button type="primary" onClick={saveGraph}>
20
+ <b>Save</b>
21
+ </Button>
22
+ </Flex>
23
+ );
24
+ };
25
+
26
+ export default FlowHeader;
web/src/pages/flow/hooks.ts CHANGED
@@ -1,6 +1,18 @@
1
  import { useSetModalState } from '@/hooks/commonHooks';
2
- import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
3
- import { Node, Position, ReactFlowInstance } from 'reactflow';
 
 
 
 
 
 
 
 
 
 
 
 
4
  import { v4 as uuidv4 } from 'uuid';
5
 
6
  export const useHandleDrag = () => {
@@ -75,3 +87,52 @@ export const useShowDrawer = () => {
75
  showDrawer,
76
  };
77
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { useSetModalState } from '@/hooks/commonHooks';
2
+ import React, {
3
+ Dispatch,
4
+ KeyboardEventHandler,
5
+ SetStateAction,
6
+ useCallback,
7
+ useState,
8
+ } from 'react';
9
+ import {
10
+ Node,
11
+ Position,
12
+ ReactFlowInstance,
13
+ useOnSelectionChange,
14
+ useReactFlow,
15
+ } from 'reactflow';
16
  import { v4 as uuidv4 } from 'uuid';
17
 
18
  export const useHandleDrag = () => {
 
87
  showDrawer,
88
  };
89
  };
90
+
91
+ export const useHandleSelectionChange = () => {
92
+ const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
93
+ const [selectedEdges, setSelectedEdges] = useState<string[]>([]);
94
+
95
+ useOnSelectionChange({
96
+ onChange: ({ nodes, edges }) => {
97
+ setSelectedNodes(nodes.map((node) => node.id));
98
+ setSelectedEdges(edges.map((edge) => edge.id));
99
+ },
100
+ });
101
+
102
+ return { selectedEdges, selectedNodes };
103
+ };
104
+
105
+ export const useDeleteEdge = (selectedEdges: string[]) => {
106
+ const { setEdges } = useReactFlow();
107
+
108
+ const deleteEdge = useCallback(() => {
109
+ setEdges((edges) =>
110
+ edges.filter((edge) => selectedEdges.every((x) => x !== edge.id)),
111
+ );
112
+ }, [setEdges, selectedEdges]);
113
+
114
+ return deleteEdge;
115
+ };
116
+
117
+ export const useHandleKeyUp = (
118
+ selectedEdges: string[],
119
+ selectedNodes: string[],
120
+ ) => {
121
+ const deleteEdge = useDeleteEdge(selectedEdges);
122
+ const handleKeyUp: KeyboardEventHandler = useCallback(
123
+ (e) => {
124
+ if (e.code === 'Delete') {
125
+ deleteEdge();
126
+ }
127
+ },
128
+ [deleteEdge],
129
+ );
130
+
131
+ return { handleKeyUp };
132
+ };
133
+
134
+ export const useSaveGraph = () => {
135
+ const saveGraph = useCallback(() => {}, []);
136
+
137
+ return { saveGraph };
138
+ };
web/src/pages/flow/index.tsx CHANGED
@@ -3,6 +3,7 @@ import { useState } from 'react';
3
  import { ReactFlowProvider } from 'reactflow';
4
  import FlowCanvas from './canvas';
5
  import Sider from './flow-sider';
 
6
 
7
  const { Content } = Layout;
8
 
@@ -14,6 +15,7 @@ function RagFlow() {
14
  <ReactFlowProvider>
15
  <Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
16
  <Layout>
 
17
  <Content style={{ margin: '0 16px' }}>
18
  <FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
19
  </Content>
 
3
  import { ReactFlowProvider } from 'reactflow';
4
  import FlowCanvas from './canvas';
5
  import Sider from './flow-sider';
6
+ import FlowHeader from './header';
7
 
8
  const { Content } = Layout;
9
 
 
15
  <ReactFlowProvider>
16
  <Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
17
  <Layout>
18
+ <FlowHeader></FlowHeader>
19
  <Content style={{ margin: '0 16px' }}>
20
  <FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
21
  </Content>
web/src/pages/flow/mock.tsx CHANGED
@@ -45,6 +45,100 @@ export const initialEdges = [
45
  ];
46
 
47
  export const dsl = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  components: {
49
  begin: {
50
  obj: {
 
45
  ];
46
 
47
  export const dsl = {
48
+ graph: {
49
+ nodes: [
50
+ {
51
+ id: 'begin',
52
+ type: 'textUpdater',
53
+ position: {
54
+ x: 50,
55
+ y: 200,
56
+ },
57
+ data: {
58
+ label: 'Begin',
59
+ },
60
+ sourcePosition: 'left',
61
+ targetPosition: 'right',
62
+ },
63
+ {
64
+ id: 'Answer:China',
65
+ type: 'textUpdater',
66
+ position: {
67
+ x: 150,
68
+ y: 200,
69
+ },
70
+ data: {
71
+ label: 'Answer',
72
+ },
73
+ sourcePosition: 'left',
74
+ targetPosition: 'right',
75
+ },
76
+ {
77
+ id: 'Retrieval:China',
78
+ type: 'textUpdater',
79
+ position: {
80
+ x: 250,
81
+ y: 200,
82
+ },
83
+ data: {
84
+ label: 'Retrieval',
85
+ },
86
+ sourcePosition: 'left',
87
+ targetPosition: 'right',
88
+ },
89
+ {
90
+ id: 'Generate:China',
91
+ type: 'textUpdater',
92
+ position: {
93
+ x: 100,
94
+ y: 100,
95
+ },
96
+ data: {
97
+ label: 'Generate',
98
+ },
99
+ sourcePosition: 'left',
100
+ targetPosition: 'right',
101
+ },
102
+ ],
103
+ edges: [
104
+ {
105
+ id: '7facb53d-65c9-43b3-ac55-339c445d3891',
106
+ label: '',
107
+ source: 'begin',
108
+ target: 'Answer:China',
109
+ markerEnd: {
110
+ type: 'arrow',
111
+ },
112
+ },
113
+ {
114
+ id: '7ac83631-502d-410f-a6e7-bec6866a5e99',
115
+ label: '',
116
+ source: 'Generate:China',
117
+ target: 'Answer:China',
118
+ markerEnd: {
119
+ type: 'arrow',
120
+ },
121
+ },
122
+ {
123
+ id: '0aaab297-5779-43ed-9281-2c4d3741566f',
124
+ label: '',
125
+ source: 'Answer:China',
126
+ target: 'Retrieval:China',
127
+ markerEnd: {
128
+ type: 'arrow',
129
+ },
130
+ },
131
+ {
132
+ id: '3477f9f3-0a7d-400e-af96-a11ea7673183',
133
+ label: '',
134
+ source: 'Retrieval:China',
135
+ target: 'Generate:China',
136
+ markerEnd: {
137
+ type: 'arrow',
138
+ },
139
+ },
140
+ ],
141
+ },
142
  components: {
143
  begin: {
144
  obj: {
web/src/pages/flow/utils.ts CHANGED
@@ -1,6 +1,6 @@
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 = (
@@ -16,9 +16,12 @@ const buildEdges = (
16
  allEdges.push({
17
  id: uuidv4(),
18
  label: '',
19
- type: 'step',
20
  source: source,
21
  target: target,
 
 
 
22
  });
23
  }
24
  });
 
1
  import { DSLComponents } from '@/interfaces/database/flow';
2
  import dagre from 'dagre';
3
+ import { Edge, MarkerType, Node, Position } from 'reactflow';
4
  import { v4 as uuidv4 } from 'uuid';
5
 
6
  const buildEdges = (
 
16
  allEdges.push({
17
  id: uuidv4(),
18
  label: '',
19
+ // type: 'step',
20
  source: source,
21
  target: target,
22
+ markerEnd: {
23
+ type: MarkerType.Arrow,
24
+ },
25
  });
26
  }
27
  });