|
- import KFIcon from '@/components/KFIcon';
- import { useStateRef, useVisible } from '@/hooks';
- import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
- import themes from '@/styles/theme.less';
- import { fittingString } from '@/utils';
- import { to } from '@/utils/promise';
- import G6 from '@antv/g6';
- import { App, Button } from 'antd';
- import { useEffect, useRef } from 'react';
- import { useNavigate, useParams } from 'react-router-dom';
- import { s8 } from '../../../utils';
- import GlobalParamsDrawer from '../components/GlobalParamsDrawer';
- import ModelMenu from '../components/ModelMenu';
- import styles from './index.less';
- import Props from './props';
- import { findAllParentNodes } from './utils';
-
- let graph = null;
-
- const EditPipeline = () => {
- const navigate = useNavigate();
- const locationParams = useParams(); //新版本获取路由参数接口
- const graphRef = useRef();
- const paramsDrawerRef = useRef();
- const propsRef = useRef();
- const [paramsDrawerOpen, openParamsDrawer, closeParamsDrawer] = useVisible(false);
- const [globalParam, setGlobalParam, globalParamRef] = useStateRef([]);
- const { message } = App.useApp();
- let sourceAnchorIdx, targetAnchorIdx, dropAnchorIdx;
- let dragSourceNode;
-
- useEffect(() => {
- initGraph();
- getFirstWorkflow(locationParams.id);
-
- const changeSize = () => {
- if (!graph || graph.get('destroyed')) return;
- if (!graphRef.current) return;
- graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
- graph.fitView();
- };
-
- window.addEventListener('resize', changeSize);
- return () => {
- window.removeEventListener('resize', changeSize);
- };
- }, []);
-
- // 拖拽结束,添加新节点
- const onDragEnd = (val) => {
- const { x, y } = val;
- const point = graph.getPointByClient(x, y);
- // 元模型
- const model = {
- ...val,
- x: point.x,
- y: point.y,
- id: val.component_name + '-' + s8(),
- isCluster: false,
- formError: true,
- };
- // console.log('model', model);
- graph.addItem('node', model, false);
- };
-
- // 节点数据发生变化
- const handleFormChange = (val) => {
- if (graph) {
- const data = graph.save();
- const index = data.nodes.findIndex((item) => {
- return item.id === val.id;
- });
- data.nodes[index] = val;
- const zoom = graph.getZoom();
- // 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置
- const lastPoint = graph.getCanvasByPoint(0, 0);
- graph.changeData(data);
- graph.render();
- graph.zoomTo(zoom);
- // 获取重新渲染之后点(0, 0)在画布的位置
- const newPoint = graph.getCanvasByPoint(0, 0);
- // 移动画布相对位移;
- graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y);
- }
- };
-
- // 保存
- const savePipeline = async (val) => {
- const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields());
- if (globalParamError) {
- message.error('全局参数配置有误');
- openParamsDrawer();
- return;
- }
- closeParamsDrawer();
-
- const [propsRes, propsError] = await to(propsRef.current.validateFields());
- if (propsError) {
- message.error('节点必填项必须配置');
- return;
- }
- propsRef.current.close();
-
- setTimeout(() => {
- const data = graph.save();
- // console.log(data);
- const errorNode = data.nodes.find((item) => item.formError === true);
- if (errorNode) {
- message.error(`【${errorNode.label}】节点必填项必须配置`);
- const graphNode = graph.findById(errorNode.id);
- if (graphNode) {
- openNodeDrawer(graphNode, true);
- }
- return;
- }
- const params = {
- ...locationParams,
- dag: JSON.stringify(data),
- global_param: JSON.stringify(globalParamRes.global_param),
- };
- saveWorkflow(params).then((ret) => {
- message.success('保存成功');
- setTimeout(() => {
- if (val) {
- navigate({ pathname: `/pipeline/template` });
- }
- }, 500);
- });
- }, 500);
- };
-
- // 渲染数据
- const getGraphData = (data) => {
- if (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;
- }
- }
- }
- }
-
- // eslint-disable-next-line
- 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 hasEdge = (source, target) => {
- const neighbors = source.getNeighbors();
- for (const node of neighbors) {
- // 新建边的时候,获取的 neighbors 的数据有问题,不全是 INode 类型,可能没有 getID 方法
- if (node.getID?.() === target.getID?.()) return true;
- }
- return false;
- };
-
- // 复制节点
- const cloneElement = (item) => {
- let data = graph.save();
- const nodeId = s8();
- 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 = async (val) => {
- const [res] = await to(getWorkflowById(val));
- if (res && res.data) {
- const { global_param, dag } = res.data;
- setGlobalParam(global_param || []);
- if (dag) {
- getGraphData(JSON.parse(dag));
- }
- }
- };
-
- // 打开节点抽屉
- const openNodeDrawer = (node, validate = false) => {
- // 获取所有的上游节点
- const parentNodes = findAllParentNodes(graph, node);
- // 如果没有打开过全局参数抽屉,获取不到全局参数
- const globalParams =
- paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
- // 打开节点编辑抽屉
- propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate);
- };
-
- // 初始化图
- const initGraph = () => {
- const contextMenu = initMenu();
- G6.registerNode(
- 'rect-node',
- {
- // draw anchor-point circles according to the anchorPoints in afterDraw
- getAnchorPoints(cfg) {
- return (
- cfg.anchorPoints || [
- // 四个,上下左右
- [0.5, 0],
- [0.5, 1],
- [0, 0.5],
- [1, 0.5],
- ]
- );
- },
- afterDraw(cfg, group) {
- 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',
- cursor: 'pointer',
- },
- name: 'text-shape',
- draggable: true,
- });
- }
- if (cfg.formError) {
- group.addShape('image', {
- attrs: {
- x: 43,
- y: -24,
- width: 18,
- height: 18,
- img: require('@/assets/img/pipeline-warning.png'),
- cursor: 'pointer',
- },
- draggable: false,
- capture: false,
- });
- }
- const bbox = group.getBBox();
- if (cfg.formError) {
- bbox.y += 6;
- bbox.width -= 6;
- bbox.height -= 6;
- }
- const anchorPoints = this.getAnchorPoints(cfg);
- 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',
- cursor: 'crosshair',
- lineWidth: 1,
- },
- 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
- draggable: true,
- });
- });
- },
-
- // response the state changes and show/hide the link-point circles
- setState(name, value, item) {
- const group = item.getContainer();
- const shape = group.get('children')[0];
- const anchorPoints = group.findAll((item) => item.get('name') === 'anchor-point');
- if (name === 'hover') {
- if (value) {
- shape.attr('stroke', themes['primaryColor']);
- anchorPoints.forEach((point) => {
- point.show();
- });
- } else {
- shape.attr('stroke', 'transparent');
- anchorPoints.forEach((point) => {
- point.hide();
- });
- }
- } else if (name === 'drag') {
- if (sourceAnchorIdx !== null && sourceAnchorIdx !== undefined) {
- const anchorPoint = anchorPoints[sourceAnchorIdx];
- anchorPoint.attr('stroke', value ? themes['primaryColor'] : '#a4a4a5');
- anchorPoint.attr('lineWidth', value ? 2 : 1);
- }
- } else if (name === 'drop') {
- if (dropAnchorIdx !== null && dropAnchorIdx !== undefined) {
- const anchorPoint = anchorPoints[dropAnchorIdx];
- anchorPoint.attr('stroke', value ? themes['primaryColor'] : '#a4a4a5');
- anchorPoint.attr('lineWidth', value ? 2 : 1);
- }
- }
- },
- },
- 'rect',
- );
-
- graph = new G6.Graph({
- container: graphRef.current,
- width: graphRef.current.clientWidth || 500,
- height: graphRef.current.clientHeight || '100%',
- animate: false,
- groupByTypes: true,
- fitView: true,
- plugins: [contextMenu],
- enabledStack: false,
- fitView: true,
- minZoom: 0.5,
- maxZoom: 5,
- fitViewPadding: 300,
- 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;
- },
- },
- // 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
- dragSourceNode = e.item;
- return true;
- },
- shouldEnd: (e) => {
- // avoid ending at other shapes on the node
- if (e.target && e.target.get('name') !== 'anchor-point') return false;
- if (!dragSourceNode || !e.item) return false;
- // 不允许连接自己
- if (dragSourceNode.getID() === e.item.getID()) return false;
- // 两个节点不允许多条边
- if (hasEdge(dragSourceNode, e.item)) 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',
- ],
- },
- 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: 'transparent',
- cursor: 'pointer',
- radius: 8,
- shadowColor: 'rgba(75, 84, 137, 0.4)',
- shadowBlur: 6,
- shadowOffsetX: 0,
- shadowOffsetY: 0,
- overflow: 'hidden',
- lineWidth: 0.5,
- },
- },
- defaultEdge: {
- // type: 'cubic-vertical',
- style: {
- endArrow: {
- // 设置终点箭头
- path: G6.Arrow.triangle(3, 3, 3), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
- d: 4.5,
- fill: '#CDD0DC',
- },
- cursor: 'pointer',
- lineWidth: 1,
- lineAppendWidth: 4,
- opacity: 1,
- stroke: '#CDD0DC',
- radius: 1,
- },
- labelCfg: {
- autoRotate: true,
- style: {
- fontSize: 10,
- fill: '#FFF',
- },
- },
- },
- });
-
- // 修改历史数据样式问题
- graph.node((node) => {
- return {
- style: {
- stroke: 'transparent',
- radius: 8,
- },
- };
- });
-
- // 绑定事件
- bindEvents();
- };
-
- // 绑定事件
- const bindEvents = () => {
- graph.on('node:click', (e) => {
- if (e.target.get('name') !== 'anchor-point' && e.item) {
- openNodeDrawer(e.item);
- }
- });
- graph.on('aftercreateedge', (e) => {
- // update the sourceAnchor and targetAnchor for the newly added edge
- graph.updateItem(e.edge, {
- sourceAnchor: sourceAnchorIdx,
- targetAnchor: targetAnchorIdx,
- type:
- targetAnchorIdx === 0 || targetAnchorIdx === 1 ? 'cubic-vertical' : 'cubic-horizontal',
- });
- });
- // 删除边时,修改 anchor-point 的 links 值
- graph.on('afterremoveitem', (e) => {
- if (e.item && e.item.source && e.item.target) {
- const { source, target, sourceAnchor, targetAnchor } = e.item;
- const sourceNode = graph.findById(source);
- const targetNode = graph.findById(target);
- 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 drag on the first node, the edge is created, update the sourceAnchor
- graph.on('afteradditem', (e) => {
- const sourceAnchor = e.item.getModel().sourceAnchor;
- if (e.item && e.item.getType() === 'edge' && !sourceAnchor) {
- graph.updateItem(e.item, {
- sourceAnchor: sourceAnchorIdx,
- });
- }
- });
- graph.on('node:mouseenter', (e) => {
- graph.setItemState(e.item, 'hover', true);
- });
- graph.on('node:mouseleave', (e) => {
- graph.setItemState(e.item, 'hover', false);
- });
- graph.on('node:dragstart', (e) => {
- graph.setItemState(e.item, 'hover', true);
- graph.setItemState(e.item, 'drag', true);
- });
- graph.on('node:dragend', (e) => {
- graph.setItemState(e.item, 'hover', false);
- graph.setItemState(e.item, 'drag', false);
- });
- graph.on('node:dragenter', (e) => {
- if (e.item?.getID() === dragSourceNode?.getID()) return;
- graph.setItemState(e.item, 'hover', true);
- if (e.target.get('name') === 'anchor-point') {
- dropAnchorIdx = e.target.get('anchorPointIdx');
- graph.setItemState(e.item, 'drop', true);
- } else {
- graph.setItemState(e.item, 'drop', false);
- }
- });
- graph.on('node:dragleave', (e) => {
- if (e.item?.getID() === dragSourceNode?.getID()) return;
- graph.setItemState(e.item, 'hover', false);
- graph.setItemState(e.item, 'drop', false);
- dropAnchorIdx = undefined;
- });
- graph.on('node:drop', (e) => {
- graph.setItemState(e.item, 'hover', false);
- graph.setItemState(e.item, 'drop', false);
- dropAnchorIdx = undefined;
- });
- };
-
- // 上下文菜单
- const initMenu = () => {
- const contextMenu = new G6.Menu({
- className: 'pipeline-context-menu',
- getContent(evt) {
- const type = evt.item.getType();
- const cloneDisplay = type === 'node' ? 'flex' : 'none';
- return `
- <div>
- <div class="pipeline-context-menu__item" style="display: ${cloneDisplay}" id="clone">
- <svg class="pipeline-context-menu__item__icon" id="clone-svg">
- <use xlink:href="#icon-fuzhi1" />
- </svg>
- 复制
- </div>
- <div class="pipeline-context-menu__item" id="delete">
- <svg class="pipeline-context-menu__item__icon" id="delete-svg">
- <use xlink:href="#icon-shanchu1" />
- </svg>
- 删除
- </div>
- </div>`;
- },
- handleMenuClick: (target, item) => {
- const id = target.id;
- if (id.startsWith('clone')) {
- cloneElement(item);
- } else if (id.startsWith('delete')) {
- graph.removeItem(item);
- }
- },
- // 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'],
- });
-
- return contextMenu;
- };
-
- return (
- <div className={styles['pipeline-container']}>
- <ModelMenu onComponentDragEnd={onDragEnd}></ModelMenu>
- <div className={styles['pipeline-container__workflow']}>
- <div className={styles['pipeline-container__workflow__top']}>
- <Button
- type="default"
- icon={<KFIcon type="icon-quanjucanshu" />}
- style={{ marginRight: '20px' }}
- onClick={openParamsDrawer}
- >
- 全局参数
- </Button>
- <Button
- type="primary"
- icon={<KFIcon type="icon-baocun" />}
- style={{ marginRight: '20px' }}
- onClick={() => {
- savePipeline(false);
- }}
- >
- 保存
- </Button>
- <Button
- type="primary"
- style={{
- border: '1px solid',
- borderColor: '#1664ff',
- background: '#fff',
- color: '#1664ff',
- }}
- icon={<KFIcon type="icon-baocunbingfanhui" />}
- onClick={() => {
- savePipeline(true);
- }}
- >
- 保存并返回
- </Button>
- </div>
- <div className={styles['pipeline-container__workflow__graph']} ref={graphRef}></div>
- </div>
- <Props ref={propsRef} onFormChange={handleFormChange}></Props>
- <GlobalParamsDrawer
- ref={paramsDrawerRef}
- open={paramsDrawerOpen}
- globalParam={globalParam}
- onClose={closeParamsDrawer}
- ></GlobalParamsDrawer>
- </div>
- );
- };
- export default EditPipeline;
|