Procházet zdrojové kódy

Merge branch 'yili' of http://gogsb.soaringnova.com/ylproj/jupyterlab into yili

Leo před 2 roky
rodič
revize
bfce422a2f

+ 1 - 0
packages/yili-dag/package.json

@@ -44,6 +44,7 @@
     "@jupyterlab/settingregistry": "^3.4.3",
     "@jupyterlab/ui-components": "^3.4.3",
     "@lumino/signaling": "^1.10.0",
+    "@mui/material": "^5.8.1",
     "antd": "^4.22.8",
     "axios": "^0.27.2",
     "canvas": "^2.6.1",

+ 68 - 0
packages/yili-dag/src/ContextMenu.tsx

@@ -0,0 +1,68 @@
+import React, { useState, useEffect } from 'react';
+import Menu from '@mui/material/Menu';
+import MenuItem from '@mui/material/MenuItem';
+
+interface IProps {
+  contextData: any;
+  onSetContextData: (contextData: any) => void;
+}
+
+const ContextMenu: React.FC<IProps> = props => {
+  const { contextData, onSetContextData } = props;
+
+  const [contextMenu, setContextMenu] = useState<{
+    mouseX: number;
+    mouseY: number;
+  } | null>(null);
+
+  useEffect(() => {
+    setContextMenu(contextData);
+  }, [contextData]);
+
+  // 关闭右键菜单
+  const handleClose = () => {
+    setContextMenu(null);
+  };
+
+  // 执行此处
+  const handleRun = () => {
+    setContextMenu(null);
+    handleSetcontextData;
+  };
+  // 执行到此处
+  const handleRunEndHere = () => {
+    setContextMenu(null);
+    handleSetcontextData;
+  };
+  // 从此处开始执行
+  const handleRunBeginHere = () => {
+    setContextMenu(null);
+    handleSetcontextData;
+  };
+  const handleSetcontextData = () => {
+    console.log(onSetContextData);
+    onSetContextData(null);
+  };
+
+  return (
+    <Menu
+      open={contextMenu !== null}
+      onClose={handleClose}
+      anchorReference="anchorPosition"
+      anchorPosition={
+        contextMenu !== null
+          ? {
+              top: contextMenu.mouseY,
+              left: contextMenu.mouseX
+            }
+          : undefined
+      }
+    >
+      <MenuItem onClick={handleRun}>执行</MenuItem>
+      <MenuItem onClick={handleRunEndHere}>执行到此处</MenuItem>
+      <MenuItem onClick={handleRunBeginHere}>从此处开始执行</MenuItem>
+    </Menu>
+  );
+};
+
+export default ContextMenu;

+ 268 - 221
packages/yili-dag/src/Dag.tsx

@@ -1,128 +1,127 @@
-import React from 'react'
-import { Graph, Addon, Path, Cell, Model, Node } from '@antv/x6'
-import { Drawer } from 'antd'
-import AlgoNode from './AlgoNode'
-import ToolBar from './ToolBar'
-import DatasourceNodeInfo from './DatasourceNodeInfo'
-import ScriptNodeInfo from './ScriptNodeInfo'
+import React from 'react';
+import { Graph, Addon, Path, Cell, Model, Node } from '@antv/x6';
+import { Drawer } from 'antd';
+import AlgoNode from './AlgoNode';
+import ToolBar from './ToolBar';
+import DatasourceNodeInfo from './DatasourceNodeInfo';
+import ScriptNodeInfo from './ScriptNodeInfo';
+import ContextMenuView from './ContextMenu';
 
 // 侧边栏UI组件
-const { Stencil } = Addon
+const { Stencil } = Addon;
 
 // 节点状态接口
 interface NodeStatus {
-  id: string
-  status: 'default' | 'success' | 'failed' | 'running'
-  type: 'script' | 'datasource'
-  label?: string
+  id: string;
+  status: 'default' | 'success' | 'failed' | 'running';
+  type: 'script' | 'datasource';
+  label?: string;
 }
 
 // 数据源节点
 const dataSourceNodes = [
   {
-    "id": "0",
-    "shape": "dag-node",
-    "height": 80,
-    "width": 180,
-    "data": {
-      "label": "DataSource",
-      "status": "default",
-      "type": 'datasource'
+    id: '0',
+    shape: 'dag-node',
+    height: 80,
+    width: 180,
+    data: {
+      label: 'DataSource',
+      status: 'default',
+      type: 'datasource'
     },
-    "ports": {
-      "items": [
-        {"id": 'bottomPort', "group": 'bottom'},
-      ]
+    ports: {
+      items: [{ id: 'bottomPort', group: 'bottom' }]
     }
   }
-]
+];
 
 // 数据处理节点
 const dataHandleNodes = [
   {
-    "id": "1",
-    "shape": "dag-node",
-    "data": {
-      "label": "数据处理节点",
-      "status": "default",
-      "type": 'script'
+    id: '1',
+    shape: 'dag-node',
+    data: {
+      label: '数据处理节点',
+      status: 'default',
+      type: 'script'
     },
-    "ports": {
-      "items": [
-        {"id": 'topPort', "group": 'top'},
-        {"id": 'bottomPort', "group": 'bottom'},
+    ports: {
+      items: [
+        { id: 'topPort', group: 'top' },
+        { id: 'bottomPort', group: 'bottom' }
       ]
     }
   }
-]
+];
 
 // 其它节点
 const otherScriptNodes = [
   {
-    "id": "2",
-    "shape": "dag-node",
-    "data": {
-      "label": "其它节点",
-      "status": "default",
-      "type": 'script'
+    id: '2',
+    shape: 'dag-node',
+    data: {
+      label: '其它节点',
+      status: 'default',
+      type: 'script'
     },
-    "ports": {
-      "items": [
-        {"id": 'topPort', "group": 'top'},
-        {"id": 'bottomPort', "group": 'bottom'},
+    ports: {
+      items: [
+        { id: 'topPort', group: 'top' },
+        { id: 'bottomPort', group: 'bottom' }
       ]
     }
-  },
-]
+  }
+];
 
 // 用户自定义脚本节点
 const customScriptNodes = [
   {
-    "id": "5",
-    "shape": "dag-node",
-    "data": {
-      "label": "SQL",
-      "status": "default",
-      "type": 'script'
+    id: '5',
+    shape: 'dag-node',
+    data: {
+      label: 'SQL',
+      status: 'default',
+      type: 'script'
     },
-    "ports": {
-      "items": [
-        {"id": 'topPort', "group": 'top'},
-        {"id": 'bottomPort', "group": 'bottom'},
+    ports: {
+      items: [
+        { id: 'topPort', group: 'top' },
+        { id: 'bottomPort', group: 'bottom' }
       ]
     }
   },
   {
-    "id": "6",
-    "shape": "dag-node",
-    "data": {
-      "label": "PySpark",
-      "status": "default",
-      "type": 'script'
+    id: '6',
+    shape: 'dag-node',
+    data: {
+      label: 'PySpark',
+      status: 'default',
+      type: 'script'
     },
-    "ports": {
-      "items": [
-        {"id": 'topPort', "group": 'top'},
-        {"id": 'bottomPort', "group": 'bottom'},
+    ports: {
+      items: [
+        { id: 'topPort', group: 'top' },
+        { id: 'bottomPort', group: 'bottom' }
       ]
     }
   },
   {
-    "id": "7",
-    "shape": "dag-node",
-    "data": {
-      "label": "Python",
-      "status": "default",
-      "type": 'script'
+    id: '7',
+    shape: 'dag-node',
+    data: {
+      label: 'Python',
+      status: 'default',
+      type: 'script'
     },
-    "ports": {
-      "items": [
-        {"id": 'topPort', "group": 'top'},
-        {"id": 'bottomPort', "group": 'bottom'},
+    ports: {
+      items: [
+        { id: 'topPort', group: 'top' },
+        { id: 'bottomPort', group: 'bottom' }
       ]
     }
-  },
-]
+  }
+];
 
 // 注册算子节点
 Graph.registerNode(
@@ -142,9 +141,9 @@ Graph.registerNode(
               magnet: true,
               stroke: '#C2C8D5',
               strokeWidth: 1,
-              fill: '#fff',
-            },
-          },
+              fill: '#fff'
+            }
+          }
         },
         bottom: {
           position: 'bottom',
@@ -154,15 +153,15 @@ Graph.registerNode(
               magnet: true,
               stroke: '#C2C8D5',
               strokeWidth: 1,
-              fill: '#fff',
-            },
-          },
-        },
-      },
-    },
+              fill: '#fff'
+            }
+          }
+        }
+      }
+    }
   },
-  true,
-)
+  true
+);
 
 // 注册边
 Graph.registerEdge(
@@ -173,61 +172,76 @@ Graph.registerEdge(
       line: {
         stroke: '#C2C8D5',
         strokeWidth: 1,
-        targetMarker: 'block',
-      },
-    },
+        targetMarker: 'block'
+      }
+    }
   },
-  true,
-)
+  true
+);
 
 // 注册连接
 Graph.registerConnector(
   'algo-connector',
   (s, e) => {
-    const offset = 4
-    const deltaY = Math.abs(e.y - s.y)
-    const control = Math.floor((deltaY / 3) * 2)
+    const offset = 4;
+    const deltaY = Math.abs(e.y - s.y);
+    const control = Math.floor((deltaY / 3) * 2);
 
-    const v1 = { x: s.x, y: s.y + offset + control }
-    const v2 = { x: e.x, y: e.y - offset - control }
+    const v1 = { x: s.x, y: s.y + offset + control };
+    const v2 = { x: e.x, y: e.y - offset - control };
 
     return Path.normalize(
       `M ${s.x} ${s.y}
        L ${s.x} ${s.y + offset}
        C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
        L ${e.x} ${e.y}
-      `,
-    )
+      `
+    );
   },
-  true,
-)
+  true
+);
 
 // 存储边
-const dagEdges: Array<any> = []
+const dagEdges: Array<any> = [];
 export default class Dag extends React.Component<any, any> {
   // 容器DIV
-  private container: any
+  private container: any;
   // 侧边栏UI容器
-  private stencilContainer: any
+  private stencilContainer: any;
   // 构造函数
   constructor(props: any) {
     super(props);
     this.state = {
       nodeInfoVisible: false,
       dagGraph: null,
-      selectedNodeData: {}
+      selectedNodeData: {},
+      contextMenu: null
     };
   }
-  
+
   // 展示node节点信息
   showNodeInfo = () => {
-    this.setState({nodeInfoVisible: true})
-  }
+    this.setState({ nodeInfoVisible: true });
+  };
 
   // 隐藏node节点信息
   hideNodeInfo = () => {
-    this.setState({nodeInfoVisible: false})
-  }
+    this.setState({ nodeInfoVisible: false });
+  };
+
+  // 右键菜单
+  handleContextMenu = (event: React.MouseEvent) => {
+    event.preventDefault();
+    this.setState({
+      contextMenu:
+        this.state.contextMenu === null
+          ? {
+              mouseX: event.clientX + 2,
+              mouseY: event.clientY - 6
+            }
+          : null
+    });
+  };
 
   // 挂载
   componentDidMount(): void {
@@ -243,7 +257,7 @@ export default class Dag extends React.Component<any, any> {
       // 拖动
       panning: {
         enabled: true,
-        eventTypes: ['leftMouseDown', 'mouseWheel'],
+        eventTypes: ['leftMouseDown', 'mouseWheel']
       },
       // 放大缩小
       mousewheel: {
@@ -251,7 +265,7 @@ export default class Dag extends React.Component<any, any> {
         modifiers: 'ctrl',
         factor: 1.1,
         maxScale: 1.5,
-        minScale: 0.5,
+        minScale: 0.5
       },
       // 交互触发高亮
       highlighting: {
@@ -261,10 +275,10 @@ export default class Dag extends React.Component<any, any> {
             attrs: {
               fill: '#fff',
               stroke: '#31d0c6',
-              strokeWidth: 4,
-            },
-          },
-        },
+              strokeWidth: 4
+            }
+          }
+        }
       },
       // 连接
       connecting: {
@@ -276,27 +290,30 @@ export default class Dag extends React.Component<any, any> {
         connectionPoint: 'anchor',
         anchor: 'center',
         // 验证
-        validateMagnet({ magnet }) {          
-          return magnet.getAttribute('port-group') !== 'top'
+        validateMagnet({ magnet }) {
+          return magnet.getAttribute('port-group') !== 'top';
         },
-        validateConnection({targetPort, sourceCell, targetCell}) {
-          const jugeEdge = dagEdges.find((item: any) => (item?.source === sourceCell?.id) && (item?.target === targetCell?.id))
+        validateConnection({ targetPort, sourceCell, targetCell }) {
+          const jugeEdge = dagEdges.find(
+            (item: any) =>
+              item?.source === sourceCell?.id && item?.target === targetCell?.id
+          );
           if (jugeEdge) {
-            return false
+            return false;
           }
-          return targetPort === 'topPort'
+          return targetPort === 'topPort';
         },
         createEdge() {
           return graph.createEdge({
             shape: 'dag-edge',
             attrs: {
               line: {
-                strokeDasharray: '5 5',
-              },
+                strokeDasharray: '5 5'
+              }
             },
-            zIndex: -1,
-          })
-        },
+            zIndex: -1
+          });
+        }
       },
       // 选择
       selecting: {
@@ -305,98 +322,103 @@ export default class Dag extends React.Component<any, any> {
         rubberEdge: true,
         rubberNode: true,
         modifiers: 'shift',
-        rubberband: true,
+        rubberband: true
       },
       // 快捷键
       keyboard: {
         enabled: true,
-        global: true,
-      },
-    })
+        global: true
+      }
+    });
 
     // 删除节点
     graph.bindKey(['delete', 'backspace'], () => {
       graph.getSelectedCells().forEach((item: any) => {
-        item.remove()
-      })
-    })
+        item.remove();
+      });
+    });
 
     // 监听边连接
     graph.on('edge:connected', ({ edge }) => {
-      
-      dagEdges.push({source: edge.getSourceCell()?.id, target: edge.getTargetCell()?.id})
-      const targetNodeData = edge.getTargetCell()?.data
-      targetNodeData.inputNumber += 1
+      dagEdges.push({
+        source: edge.getSourceCell()?.id,
+        target: edge.getTargetCell()?.id
+      });
+      const targetNodeData = edge.getTargetCell()?.data;
+      targetNodeData.inputNumber += 1;
       edge.attr({
         line: {
-          strokeDasharray: '',
-        },
-      })
-    })
+          strokeDasharray: ''
+        }
+      });
+    });
+
+    graph.on('edge:removed', ({ edge }) => {
+      const sourceNodeId = (edge as any).store.data.source.cell;
+      const targetNodeId = (edge as any).store.data.target.cell;
 
-    graph.on('edge:removed', ({edge}) => {
-      const sourceNodeId = (edge as any).store.data.source.cell
-      const targetNodeId = (edge as any).store.data.target.cell
-      
       const index = dagEdges.indexOf({
         source: sourceNodeId,
         target: targetNodeId
-      })
+      });
+
+      const targetNode = this.state.dagGraph.getCellById(targetNodeId);
 
-      const targetNode = this.state.dagGraph.getCellById(targetNodeId)
-      
       if (targetNode) {
-        targetNode.data.inputNumber -= 1
+        targetNode.data.inputNumber -= 1;
       }
 
-      dagEdges.splice(index, 1)
-
-    })
+      dagEdges.splice(index, 1);
+    });
 
     // 监听状态改变
     graph.on('node:change:data', ({ node }) => {
-      const edges = graph.getIncomingEdges(node)
-      const { status } = node.getData() as NodeStatus
-      edges?.forEach((edge) => {
+      const edges = graph.getIncomingEdges(node);
+      const { status } = node.getData() as NodeStatus;
+      edges?.forEach(edge => {
         if (status === 'running') {
-          edge.attr('line/strokeDasharray', 5)
-          edge.attr('line/style/animation', 'running-line 30s infinite linear')
+          edge.attr('line/strokeDasharray', 5);
+          edge.attr('line/style/animation', 'running-line 30s infinite linear');
         } else {
-          edge.attr('line/strokeDasharray', '')
-          edge.attr('line/style/animation', '')
+          edge.attr('line/strokeDasharray', '');
+          edge.attr('line/style/animation', '');
         }
-      })
-    })
+      });
+    });
 
     // 监听节点被双击
-    graph.on('node:dblclick', (args: { 
-      cell: Cell
-      node: Node 
-      options: Model.SetOptions 
-    }) => {
-      const nodeData = args.cell.data
-      this.setState({selectedNodeData: nodeData})
-      this.showNodeInfo()
-    })
-
-    const text = this.props.context.current
+    graph.on(
+      'node:dblclick',
+      (args: { cell: Cell; node: Node; options: Model.SetOptions }) => {
+        const nodeData = args.cell.data;
+        this.setState({ selectedNodeData: nodeData });
+        this.showNodeInfo();
+      }
+    );
+
+    //监听右键
+    graph.on('node:contextmenu', ({ node, e }) => {
+      this.handleContextMenu(e);
+    });
+
+    const text = this.props.context.current;
     text.ready.then(() => {
       const dagJson: any = text.model.toJSON();
       if (dagJson?.graph) {
-        graph.fromJSON(dagJson.graph)
+        graph.fromJSON(dagJson.graph);
       }
     });
 
     // 内容居中
-    graph.centerContent()
-    
+    graph.centerContent();
+
     // 创建侧边栏
     const stencil = new Stencil({
       title: '算子检索',
       target: graph,
       // 搜索
       search(cell, keyword) {
-        return cell.data.label.indexOf(keyword) !== -1
+        return cell.data.label.indexOf(keyword) !== -1;
       },
       placeholder: '搜索算子',
       notFoundText: '未找到相关算子',
@@ -433,44 +455,55 @@ export default class Dag extends React.Component<any, any> {
       ],
       // 拖拽事件 处理节点数据
       getDropNode(draggingNode: Node): Node {
-        console.log(123);
-        
-        const newNode = draggingNode.clone()
-        initNodeData(newNode)
-        return newNode
-      },
-    })
+
+        const newNode = draggingNode.clone();
+        initNodeData(newNode);
+        return newNode;
+      }
+    });
 
     // 添加到侧边栏容器内
-    this.stencilContainer.appendChild(stencil.container)
+    this.stencilContainer.appendChild(stencil.container);
 
     // 将可拖拽项加载到侧边栏中
-    stencil.load(dataSourceNodes, 'dataSource')
-    stencil.load(dataHandleNodes, 'dataHandle')
-    stencil.load(otherScriptNodes, 'otherScript')
-    stencil.load(customScriptNodes, 'customScript')
-    stencil.resizeGroup('dataSource', { width: 200, height: dataSourceNodes.length * 120 })
-    stencil.resizeGroup('dataHandle', { width: 200, height: dataHandleNodes.length * 60 })
-    stencil.resizeGroup('otherScript', { width: 200, height: otherScriptNodes.length * 60 })
-    stencil.resizeGroup('customScript', { width: 200, height: customScriptNodes.length * 60 })    
-    
+    stencil.load(dataSourceNodes, 'dataSource');
+    stencil.load(dataHandleNodes, 'dataHandle');
+    stencil.load(otherScriptNodes, 'otherScript');
+    stencil.load(customScriptNodes, 'customScript');
+    stencil.resizeGroup('dataSource', {
+      width: 200,
+      height: dataSourceNodes.length * 120
+    });
+    stencil.resizeGroup('dataHandle', {
+      width: 200,
+      height: dataHandleNodes.length * 60
+    });
+    stencil.resizeGroup('otherScript', {
+      width: 200,
+      height: otherScriptNodes.length * 60
+    });
+    stencil.resizeGroup('customScript', {
+      width: 200,
+      height: customScriptNodes.length * 60
+    });
+
     // 设置图
-    this.setState({dagGraph: graph})
+    this.setState({ dagGraph: graph });
   }
 
   // 卸载
   componentWillUnmount(): void {
-    this.state.dagGraph.dispose()
+    this.state.dagGraph.dispose();
   }
   // 容器
   refContainer = (container: HTMLDivElement) => {
-    this.container = container
-  }
+    this.container = container;
+  };
 
   // 侧边栏
   refStencil = (container: HTMLDivElement) => {
-    this.stencilContainer = container
-  }
+    this.stencilContainer = container;
+  };
 
   // 最后输出模板
   render() {
@@ -479,8 +512,14 @@ export default class Dag extends React.Component<any, any> {
         {/* 侧边栏 */}
         <div className="app-stencil" ref={this.refStencil} />
         {/* 容器 */}
-        <div className='app-content'>
+        <div className="app-content">
           <div className="app-graph" ref={this.refContainer} />
+          <ContextMenuView
+            contextData={this.state.contextMenu}
+            onSetContextData={contextData =>
+              this.setState({ contextMenu: contextData })
+            }
+          />
           {/* 节点信息 */}
           <Drawer
             title={this.state.selectedNodeData?.label || 'NodeTitle'}
@@ -493,34 +532,42 @@ export default class Dag extends React.Component<any, any> {
             style={{ position: 'absolute' }}
           >
             {/* 数据源节点 */}
-            {this.state.selectedNodeData?.type === 'datasource' && <DatasourceNodeInfo nodeInfo={this.state.selectedNodeData}/>}
+            {this.state.selectedNodeData?.type === 'datasource' && (
+              <DatasourceNodeInfo nodeInfo={this.state.selectedNodeData} />
+            )}
             {/* 脚本节点 */}
-            {this.state.selectedNodeData?.type === 'script' && <ScriptNodeInfo nodeInfo={this.state.selectedNodeData}/>}
+            {this.state.selectedNodeData?.type === 'script' && (
+              <ScriptNodeInfo nodeInfo={this.state.selectedNodeData} />
+            )}
           </Drawer>
           {/* 工具栏 */}
-          <ToolBar className="app-tool" graph={this.state.dagGraph} saveGraph={this.props.saveGraph}/>
+          <ToolBar
+            className="app-tool"
+            graph={this.state.dagGraph}
+            saveGraph={this.props.saveGraph}
+          />
         </div>
       </div>
-    )
+    );
   }
 }
 
 // 初始化节点数据
 function initNodeData(newNode: Node<Node.Properties>) {
   // 节点id
-  newNode.data.id = newNode.id
-  switch(newNode.data.type) {
+  newNode.data.id = newNode.id;
+  switch (newNode.data.type) {
     case 'datasource':
-      newNode.data.inputSource = undefined
-      newNode.data.dataTable = undefined
-      break
+      newNode.data.inputSource = undefined;
+      newNode.data.dataTable = undefined;
+      break;
     case 'script':
-      newNode.data.paramText = undefined
-      newNode.data.scriptText = undefined
-      newNode.data.outputData = undefined
-      newNode.data.inputNumber = 0
-      newNode.data.packageData = undefined
-      break
+      newNode.data.paramText = undefined;
+      newNode.data.scriptText = undefined;
+      newNode.data.outputData = undefined;
+      newNode.data.inputNumber = 0;
+      newNode.data.packageData = undefined;
+      break;
     default:
   }
-}
+}