|
- import { useModal } from '@/hooks';
- import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
- import { to } from '@/utils/promise';
- import { SaveOutlined } from '@ant-design/icons';
- import { useEmotionCss } from '@ant-design/use-emotion-css';
- import G6 from '@antv/g6';
- import { Button, message } from 'antd';
- import { useEffect, useRef, useState } from 'react';
- import { useNavigate, useParams } from 'react-router-dom';
- import { Icon } from 'umi';
- import { s8 } from '../../../utils';
- import Styles from './editPipeline.less';
- import GlobalParamsDrawer from './globalParamsDrawer';
- import ModelMenus from './modelMenus';
- import Props from './props';
-
- let graph = null;
-
- const EditPipeline = () => {
- const propsRef = useRef();
- const navgite = useNavigate();
- // const [contextMenu,setContextMenu]=useState({})
- let contextMenu = {};
- const locationParams = useParams(); //新版本获取路由参数接口
- let sourceAnchorIdx, targetAnchorIdx;
- const pipelineContainer = useEmotionCss(() => {
- return {
- display: 'flex',
- backgroundColor: '#fff',
- height: '98vh',
- position: 'relative',
- };
- });
- const rightmenu = useEmotionCss(() => {
- return {
- position: 'absolute',
- width: '120px',
- height: '146px',
- left: '0px',
- top: '0px',
-
- color: '#333333',
- overflowY: 'auto',
- };
- });
- const rightmenuItem = useEmotionCss(() => {
- return {
- padding: '10px 20px',
- cursor: 'pointer',
- fontSize: '12px',
- };
- });
- const graphStyle = useEmotionCss(() => {
- return {
- width: '100%',
- backgroundSize: '100% 100%',
- backgroundImage: 'url(/assets/images/pipeline-canvas-back.png)',
- flex: 1,
- };
- });
- const graphRef = useRef();
- const paramsDrawerRef = useRef();
- const [paramsDrawerVisible, openParamsDrawer, closeParamsDrawer] = useModal(false);
- const [globalParam, setGlobalParam] = useState([]);
-
- const onDragEnd = (val) => {
- console.log(val, 'eee');
- const _x = val.x;
- const _y = val.y;
- const point = graph.getPointByClient(_x, _y);
- let model = {};
- // 元模型
- model = {
- ...val,
- x: point.x,
- y: point.y,
- id: val.component_name + '-' + s8(),
- isCluster: false,
- };
- console.log(graph, model);
-
- graph.addItem('node', model, true);
- console.log(graph);
- };
- const formChange = (val) => {
- if (graph) {
- const data = graph.save();
- const index = data.nodes.findIndex((item) => {
- return item.id === val.id;
- });
- data.nodes[index] = val;
- graph.changeData(data);
- graph.render();
- }
- };
- const savePipeline = async (val) => {
- const [res, error] = await to(paramsDrawerRef.current.getFieldsValue());
- if (error) {
- message.error('全局参数配置有误');
- return;
- }
- const data = graph.save();
- console.log(data);
- const params = {
- ...locationParams,
- dag: JSON.stringify(data),
- global_param: JSON.stringify(res.global_param),
- };
- saveWorkflow(params).then((ret) => {
- message.success('保存成功');
- closeParamsDrawer();
- setTimeout(() => {
- if (val) {
- navgite({ pathname: `/pipeline` });
- }
- }, 500);
- });
- };
- const handlerClick = (e) => {
- e.stopPropagation();
- console.log(propsRef, graph);
- // let cache = [];
- // let json_str = JSON.stringify(graph, function(key, value) {
- // if (typeof value === 'object' && value !== null) {
- // if (cache.indexOf(value) !== -1) {
- // return;
- // }
- // cache.push(value);
- // }
- // return value;
- // });
- // console.log(json_str);
- propsRef.current.showDrawer(e);
- };
- const getGraphData = (data) => {
- if (graph) {
- console.log(graph);
- graph.data(data);
- graph.render();
- } else {
- setTimeout(() => {
- getGraphData(data);
- }, 500);
- }
- };
-
- const processParallelEdgesOnAnchorPoint = (
- edges,
- offsetDiff = 15,
- multiEdgeType = 'cubic-vertical',
- singleEdgeType = undefined,
- loopEdgeType = undefined,
- ) => {
- const len = edges.length;
- const cod = offsetDiff * 2;
- const loopPosition = [
- 'top',
- 'top-right',
- 'right',
- 'bottom-right',
- 'bottom',
- 'bottom-left',
- 'left',
- 'top-left',
- ];
- const edgeMap = {};
- const tags = [];
- const reverses = {};
- for (let i = 0; i < len; i++) {
- const edge = edges[i];
- const { source, target, sourceAnchor, targetAnchor } = edge;
- const sourceTarget = `${source}|${sourceAnchor}-${target}|${targetAnchor}`;
-
- if (tags[i]) continue;
- if (!edgeMap[sourceTarget]) {
- edgeMap[sourceTarget] = [];
- }
- tags[i] = true;
- edgeMap[sourceTarget].push(edge);
- for (let j = 0; j < len; j++) {
- if (i === j) continue;
- const sedge = edges[j];
- const {
- source: src,
- target: dst,
- sourceAnchor: srcAnchor,
- targetAnchor: dstAnchor,
- } = sedge;
-
- // 两个节点之间共同的边
- // 第一条的source = 第二条的target
- // 第一条的target = 第二条的source
- if (!tags[j]) {
- if (
- source === dst &&
- sourceAnchor === dstAnchor &&
- target === src &&
- targetAnchor === srcAnchor
- ) {
- edgeMap[sourceTarget].push(sedge);
- tags[j] = true;
- reverses[
- `${src}|${srcAnchor}|${dst}|${dstAnchor}|${edgeMap[sourceTarget].length - 1}`
- ] = true;
- } else if (
- source === src &&
- sourceAnchor === srcAnchor &&
- target === dst &&
- targetAnchor === dstAnchor
- ) {
- edgeMap[sourceTarget].push(sedge);
- tags[j] = true;
- }
- }
- }
- }
-
- for (const key in edgeMap) {
- const arcEdges = edgeMap[key];
- const { length } = arcEdges;
- for (let k = 0; k < length; k++) {
- const current = arcEdges[k];
- if (current.source === current.target) {
- if (loopEdgeType) current.type = loopEdgeType;
- // 超过8条自环边,则需要重新处理
- current.loopCfg = {
- position: loopPosition[k % 8],
- dist: Math.floor(k / 8) * 20 + 50,
- };
- continue;
- }
- if (
- length === 1 &&
- singleEdgeType &&
- (current.source !== current.target || current.sourceAnchor !== current.targetAnchor)
- ) {
- current.type = singleEdgeType;
- continue;
- }
- current.type = multiEdgeType;
- const sign =
- (k % 2 === 0 ? 1 : -1) *
- (reverses[
- `${current.source}|${current.sourceAnchor}|${current.target}|${current.targetAnchor}|${k}`
- ]
- ? -1
- : 1);
- if (length % 2 === 1) {
- current.curveOffset = sign * Math.ceil(k / 2) * cod;
- } else {
- current.curveOffset = sign * (Math.floor(k / 2) * cod + offsetDiff);
- }
- }
- }
- return edges;
- };
- const cloneElement = (item) => {
- console.log(item);
- let data = graph.save();
- const nodeId = s8();
- console.log(item.getModel());
- data.nodes.push({
- ...item.getModel(),
- label: item.getModel().label + '-copy',
- x: item.getModel().x + 150,
- y: item.getModel().y,
- id: item.getModel().component_name + '-' + nodeId,
- });
- graph.changeData(data);
- };
- const getFirstWorkflow = (val) => {
- getWorkflowById(val).then((ret) => {
- if (ret && ret.data) {
- setGlobalParam(ret.data.global_param);
- }
- if (graph && ret.data && ret.data.dag) {
- getGraphData(JSON.parse(ret.data.dag));
- }
- // graph&&graph.data(JSON.parse(ret.dag))
- // graph.render()
- });
- };
- const handlerContextMenu = (e) => {
- e.stopPropagation();
- // this.menuType = e.item._cfg.type;
- };
- const initMenu = () => {
- // const selectedNodes = this.selectedNodes;
- contextMenu = new G6.Menu({
- getContent(evt) {
- return `
- <ul style="position: absolute;
- width: 100px;
- padding-left:0;
- display:flex;
- flex-direction: column;
- align-items:center;
- left: 0px;
- top: 0px;
- background-color: #ffffff;
- font-size: 14px;
- color: #333333;
- overflow-y: auto;">
-
- <li style="padding: 10px 20px;cursor: pointer;" code="clone">复制</li>
- <li style="padding: 10px 20px;cursor: pointer;" code="delete">删除</li>
- </ul>`;
- },
- handleMenuClick: (target, item) => {
- switch (target.getAttribute('code')) {
- case 'delete':
- graph.removeItem(item);
- break;
- case 'clone':
- cloneElement(item);
- break;
- default:
- break;
- }
- },
- // offsetX and offsetY include the padding of the parent container
- // 需要加上父级容器的 padding-left 16 与自身偏移量 10
- offsetX: 16 + 10,
- // 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10
- offsetY: 0,
- // the types of items that allow the menu show up
- // 在哪些类型的元素上响应
- itemTypes: ['node', 'edge'],
- });
-
- initGraph();
- };
- useEffect(() => {
- getFirstWorkflow(locationParams.id);
- initMenu();
-
- return () => {
- graph.off('node:mouseenter', (e) => {
- graph.setItemState(e.item, 'showAnchors', true);
- graph.setItemState(e.item, 'nodeSelected', true);
- });
- graph.off('node:mouseleave', (e) => {
- // this.graph.setItemState(e.item, 'showAnchors', false);
- graph.setItemState(e.item, 'nodeSelected', false);
- });
- graph.off('dblclick', handlerClick);
- };
- }, []);
- const initGraph = () => {
- const fittingString = (str, maxWidth, fontSize) => {
- const ellipsis = '...';
- const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0];
- let currentWidth = 0;
- let res = str;
- const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters
- str.split('').forEach((letter, i) => {
- if (currentWidth > maxWidth - ellipsisLength) return;
- if (pattern.test(letter)) {
- // Chinese charactors
- currentWidth += fontSize;
- } else {
- // get the width of single letter according to the fontSize
- currentWidth += G6.Util.getLetterWidth(letter, fontSize);
- }
- if (currentWidth > maxWidth - ellipsisLength) {
- res = `${str.substr(0, i)}${ellipsis}`;
- }
- });
- return res;
- };
- // 获取文本的长度
- const getTextSize = (str, maxWidth, fontSize) => {
- let width = G6.Util.getTextSize(str, fontSize)[0];
- return width > maxWidth ? maxWidth : width;
- };
- G6.registerNode(
- 'rect-node',
- {
- // draw anchor-point circles according to the anchorPoints in afterDraw
- getAnchorPoints(cfg) {
- return (
- cfg.anchorPoints || [
- // 上下各3,左右各1
- [0.5, 0],
- [0.5, 1],
- ]
- );
- },
- // draw(cfg, group) {
-
- // let rect=group.addShape('text', {
- // attrs: {
- // text: fittingString(cfg.label, 110, 15),
- // x: 90 - getTextSize(cfg.label, 110, 15),
- // y: 0,
- // fontSize: 10,
- // textAlign: 'center',
- // textBaseline: 'middle',
- // fill:'#000'
- // },
- // name: 'text-shape',
- // });
- // return rect;
- // },
- afterDraw(cfg, group) {
- // console.log(group, cfg, 12312);
- const image = group.addShape('image', {
- attrs: {
- x: -45,
- y: -10,
- width: 20,
- height: 20,
- img: cfg.img,
- cursor: 'pointer',
- },
- draggable: true,
- });
- if (cfg.label) {
- group.addShape('text', {
- attrs: {
- text: fittingString(cfg.label, 70, 10),
- x: -20,
- y: 0,
- fontSize: 10,
- textAlign: 'left',
- textBaseline: 'middle',
- fill: '#000',
- },
- name: 'text-shape',
- draggable: true,
- });
- }
- const bbox = group.getBBox();
- const anchorPoints = this.getAnchorPoints(cfg);
- // console.log(anchorPoints);
- anchorPoints.forEach((anchorPos, i) => {
- group.addShape('circle', {
- attrs: {
- r: 3,
- x: bbox.x + bbox.width * anchorPos[0],
- y: bbox.y + bbox.height * anchorPos[1],
- fill: '#fff',
- stroke: '#a4a4a5',
- },
- name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point')
- anchorPointIdx: i, // flag the idx of the anchor-point circle
- links: 0, // cache the number of edges connected to this shape
- visible: false, // invisible by default, shows up when links > 1 or the node is in showAnchors state
- });
- });
- return image;
- },
-
- // response the state changes and show/hide the link-point circles
- setState(name, value, item) {
- const anchorPoints = item
- .getContainer()
- .findAll((ele) => ele.get('name') === 'anchor-point');
- anchorPoints.forEach((point) => {
- if (value || point.get('links') > 0) point.show();
- else point.hide();
- });
- // }
- },
- },
- 'rect',
- );
- console.log(graphRef, 'graphRef');
- graph = new G6.Graph({
- container: graphRef.current,
- grid: true,
- width: graphRef.current.clientWidth || 500,
- height: graphRef.current.clientHeight || '100%',
- animate: false,
- groupByTypes: false,
- fitView: true,
- plugins: [contextMenu],
- enabledStack: true,
- modes: {
- default: [
- // config the shouldBegin for drag-node to avoid node moving while dragging on the anchor-point circles
- {
- type: 'drag-node',
- shouldBegin: (e) => {
- if (e.target.get('name') === 'anchor-point') return false;
- return true;
- },
- // shouldEnd: e => {
- // console.log(e);
- // return false;
- // },
- },
- // config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles
- {
- type: 'create-edge',
- // trigger: 'drag',
- shouldBegin: (e) => {
- // avoid beginning at other shapes on the node
- if (e.target && e.target.get('name') !== 'anchor-point') return false;
- sourceAnchorIdx = e.target.get('anchorPointIdx');
- e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle
- return true;
- },
- shouldEnd: (e) => {
- // avoid ending at other shapes on the node
- if (e.target && e.target.get('name') !== 'anchor-point') return false;
- if (e.target) {
- targetAnchorIdx = e.target.get('anchorPointIdx');
- e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle
- return true;
- }
- targetAnchorIdx = undefined;
- return true;
- },
- },
- 'drag-canvas',
- 'zoom-canvas',
- // 'brush-select',
- 'drag-combo',
- ],
- altSelect: [
- {
- type: 'brush-select',
- trigger: 'drag',
- },
- 'drag-node',
- ],
- },
-
- defaultNode: {
- type: 'rect-node',
- size: [110, 36],
-
- labelCfg: {
- style: {
- fill: 'transparent',
- fontSize: 0,
- boxShadow: '0px 0px 12px rgba(75, 84, 137, 0.05)',
- overflow: 'hidden',
- x: -20,
- y: 0,
- textAlign: 'left',
- textBaseline: 'middle',
- },
- },
- style: {
- fill: '#fff',
- stroke: '#fff',
- cursor: 'pointer',
- radius: 10,
- overflow: 'hidden',
- lineWidth: 0.5,
- },
- },
- nodeStateStyles: {
- nodeSelected: {
- fill: 'red',
- shadowColor: 'red',
- stroke: 'red',
- 'text-shape': {
- fill: 'red',
- stroke: 'red',
- },
- },
- },
- defaultEdge: {
- // type: 'quadratic',
- type: 'cubic-vertical',
-
- style: {
- endArrow: {
- // 设置终点箭头
- path: G6.Arrow.triangle(3, 3, 3), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
- d: 4.5,
- fill: '#CDD0DC',
- },
- cursor: 'pointer',
- lineWidth: 1,
- opacity: 1,
- stroke: '#CDD0DC',
- radius: 1,
- },
- nodeStateStyle: {
- hover: {
- opacity: 1,
- stroke: '#8fe8ff',
- },
- },
- labelCfg: {
- autoRotate: true,
- // refY: 10,
- style: {
- fontSize: 10,
- fill: '#FFF',
- },
- },
- },
- defaultCombo: {
- type: 'rect',
- fixCollapseSize: 70,
- style: {
- fill: '#00e0ff0d',
- stroke: '#00e0ff',
- lineDash: [5, 10],
- cursor: 'pointer',
- },
- },
- // linkCenter: true,
- fitView: false,
- fitViewPadding: [60, 60, 60, 80],
- });
- graph.on('dblclick', (e) => {
- console.log(e.item);
- if (e.item) {
- graph.setItemState(e.item, 'nodeClicked', true);
- handlerClick(e);
- }
- });
- graph.on('click', (e) => {
- console.log(e.item);
- });
- graph.on('aftercreateedge', (e) => {
- // update the sourceAnchor and targetAnchor for the newly added edge
- graph.updateItem(e.edge, {
- sourceAnchor: sourceAnchorIdx,
- targetAnchor: targetAnchorIdx,
- });
-
- // update the curveOffset for parallel edges
- const edges = graph.save().edges;
- processParallelEdgesOnAnchorPoint(edges);
- graph.getEdges().forEach((edge, i) => {
- graph.updateItem(edge, {
- curveOffset: edges[i].curveOffset,
- curvePosition: edges[i].curvePosition,
- });
- });
- });
- graph.on('node:mouseenter', (e) => {
- // this.graph.setItemState(e.item, 'showAnchors', true);
- graph.setItemState(e.item, 'nodeSelected', true);
- graph.updateItem(e.item, {
- // 节点的样式
- style: {
- stroke: '#1664ff',
- },
- });
- });
- graph.on('node:mouseleave', (e) => {
- // this.graph.setItemState(e.item, 'showAnchors', false);
- graph.setItemState(e.item, 'nodeSelected', false);
- graph.updateItem(e.item, {
- // 节点的样式
- style: {
- stroke: 'transparent',
- },
- });
- });
- graph.on('afterremoveitem', (e) => {
- if (e.item && e.item.source && e.item.target) {
- const sourceNode = graph.findById(e.item.source);
- const targetNode = graph.findById(e.item.target);
- const { sourceAnchor, targetAnchor } = e.item;
- if (sourceNode && !isNaN(sourceAnchor)) {
- const sourceAnchorShape = sourceNode
- .getContainer()
- .find(
- (ele) =>
- ele.get('name') === 'anchor-point' && ele.get('anchorPointIdx') === sourceAnchor,
- );
- sourceAnchorShape.set('links', sourceAnchorShape.get('links') - 1);
- }
- if (targetNode && !isNaN(targetAnchor)) {
- const targetAnchorShape = targetNode
- .getContainer()
- .find(
- (ele) =>
- ele.get('name') === 'anchor-point' && ele.get('anchorPointIdx') === targetAnchor,
- );
- targetAnchorShape.set('links', targetAnchorShape.get('links') - 1);
- }
- }
- });
-
- // after clicking on the first node, the edge is created, update the sourceAnchor
- graph.on('afteradditem', (e) => {
- if (e.item && e.item.getType() === 'edge') {
- graph.updateItem(e.item, {
- sourceAnchor: sourceAnchorIdx,
- });
- }
- });
- window.onresize = () => {
- if (!graph || graph.get('destroyed')) return;
- if (!graphRef.current || !graphRef.current.scrollWidth || !graphRef.current.scrollHeight)
- return;
- graph.changeSize(graphRef.current.scrollWidth, graphRef.current.scrollHeight - 20);
- };
- };
- return (
- <div className={pipelineContainer}>
- <ModelMenus onParDragEnd={onDragEnd}></ModelMenus>
- <div className={Styles.centerContainer}>
- <div className={Styles.buttonList}>
- <Button
- type="default"
- icon={<Icon icon="local:parameter" style={{ verticalAlign: '-2px' }} />}
- style={{ marginRight: '20px' }}
- onClick={openParamsDrawer}
- >
- 全局参数
- </Button>
- <Button
- type="primary"
- icon={<SaveOutlined />}
- style={{ marginRight: '20px' }}
- onClick={() => {
- savePipeline(false);
- }}
- >
- 保存
- </Button>
- <Button
- type="primary"
- icon={<SaveOutlined />}
- onClick={() => {
- savePipeline(true);
- }}
- >
- 保存并返回
- </Button>
- </div>
- <div className={graphStyle} ref={graphRef} id={Styles.graphStyle}></div>
- </div>
- <Props ref={propsRef} onParentChange={formChange}></Props>
- <GlobalParamsDrawer
- ref={paramsDrawerRef}
- open={paramsDrawerVisible}
- globalParam={globalParam}
- onClose={closeParamsDrawer}
- ></GlobalParamsDrawer>
- </div>
- );
- };
- export default EditPipeline;
|