/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import 'jquery-ui/ui/widgets/draggable' import 'jquery-ui/ui/widgets/droppable' import 'jquery-ui/ui/widgets/resizable' import _ from 'lodash' import i18n from '@/module/i18n' import { jsPlumb } from 'jsplumb' import DragZoom from './dragZoom' import store from '@/conf/home/store' import router from '@/conf/home/router' import { uuid, findComponentDownward } from '@/module/util/' import { tasksAll, rtTasksTpl, setSvgColor, saveTargetarr, rtTargetarrArr, computeScale } from './util' import multiDrag from './multiDrag' const JSP = function () { this.dag = {} this.selectedElement = {} this.config = { // Whether to drag isDrag: true, // Whether to allow connection isAttachment: false, // Whether to drag a new node isNewNodes: true, // Whether to support double-click node events isDblclick: true, // Whether to support right-click menu events isContextmenu: true, // Whether to allow click events isClick: false } } /** * dag init */ JSP.prototype.init = function ({ dag, instance, options }) { // Get the dag component instance this.dag = dag // Get jsplumb instance this.JspInstance = instance // Get JSP options this.options = options || {} // Register jsplumb connection type and configuration this.JspInstance.registerConnectionType('basic', { anchor: 'Continuous', connector: 'Bezier' // Line type }) // Initial configuration this.setConfig({ isDrag: !store.state.dag.isDetails, isAttachment: false, isNewNodes: !store.state.dag.isDetails, // Permissions.getAuth() === false ? false : !store.state.dag.isDetails, isDblclick: true, isContextmenu: true, isClick: false }) // Monitor line click this.JspInstance.bind('click', e => { // Untie event if (this.config.isClick) { this.connectClick(e) } else { findComponentDownward(this.dag.$root, 'dag-chart')._createLineLabel({ id: e._jsPlumb.overlays.label.canvas.id, sourceId: e.sourceId, targetId: e.targetId }) } }) // Drag and drop if (this.config.isNewNodes) { DragZoom.init() } // support multi drag multiDrag() } /** * set config attribute */ JSP.prototype.setConfig = function (o) { this.config = Object.assign(this.config, {}, o) } /** * Node binding event */ JSP.prototype.tasksEvent = function (selfId) { const tasks = $(`#${selfId}`) // Bind right event tasks.on('contextmenu', e => { this.tasksContextmenu(e) return false }) // Binding double click event tasks.find('.icos').bind('dblclick', e => { this.tasksDblclick(e) }) // Binding click event tasks.on('click', e => { this.tasksClick(e) }) } /** * Dag node drag and drop processing */ JSP.prototype.draggable = function () { if (this.config.isNewNodes) { let selfId const self = this $('.toolbar-btn .roundedRect').draggable({ scope: 'plant', helper: 'clone', containment: $('.dag-model'), stop: function (e, ui) { }, drag: function () { $('body').find('.tooltip.fade.top.in').remove() } }) $('#canvas').droppable({ scope: 'plant', drop: function (ev, ui) { let id = 'tasks-' + Math.ceil(Math.random() * 100000) // eslint-disable-line let scale = computeScale($(this)) scale = scale || 1 // Get mouse coordinates and after scale coordinate const left = parseInt(ui.offset.left - $(this).offset().left) / scale const top = parseInt(ui.offset.top - $(this).offset().top) / scale // Generate template node $('#canvas').append(rtTasksTpl({ id: id, name: id, x: left, y: top, isAttachment: self.config.isAttachment, taskType: findComponentDownward(self.dag.$root, 'dag-chart').dagBarId })) // Get the generated node const thisDom = jsPlumb.getSelector('.statemachine-demo .w') // Generating a connection node self.JspInstance.batch(() => { self.initNode(thisDom[thisDom.length - 1]) }) selfId = id self.tasksEvent(selfId) // Dom structure is not generated without pop-up form form if ($(`#${selfId}`).html()) { // dag event findComponentDownward(self.dag.$root, 'dag-chart')._createNodes({ id: selfId }) } } }) } } /** * Echo json processing and old data structure processing */ JSP.prototype.jsonHandle = function ({ largeJson, locations }) { _.map(largeJson, v => { // Generate template $('#canvas').append(rtTasksTpl({ id: v.id, name: v.name, x: locations[v.id].x, y: locations[v.id].y, targetarr: locations[v.id].targetarr, isAttachment: this.config.isAttachment, taskType: v.type, runFlag: v.runFlag, nodenumber: locations[v.id].nodenumber, successNode: v.conditionResult === undefined ? '' : v.conditionResult.successNode[0], failedNode: v.conditionResult === undefined ? '' : v.conditionResult.failedNode[0] })) // contextmenu event $(`#${v.id}`).on('contextmenu', e => { this.tasksContextmenu(e) return false }) // dblclick event $(`#${v.id}`).find('.icos').bind('dblclick', e => { this.tasksDblclick(e) }) // click event $(`#${v.id}`).bind('click', e => { this.tasksClick(e) }) }) } /** * Initialize a single node */ JSP.prototype.initNode = function (el) { // Whether to drag if (this.config.isDrag) { this.JspInstance.draggable(el, { containment: 'dag-container' }) } // Node attribute configuration this.JspInstance.makeSource(el, { filter: '.ep', anchor: 'Continuous', connectorStyle: { stroke: '#2d8cf0', strokeWidth: 2, outlineStroke: 'transparent', outlineWidth: 4 }, // This place is leaking // connectionType: "basic", extract: { action: 'the-action' }, maxConnections: -1 }) // Node connection property configuration this.JspInstance.makeTarget(el, { dropOptions: { hoverClass: 'dragHover' }, anchor: 'Continuous', allowLoopback: false // Forbid yourself to connect yourself }) this.JspInstance.fire('jsPlumbDemoNodeAdded', el) } /** * Node right click menu */ JSP.prototype.tasksContextmenu = function (event) { if (this.config.isContextmenu) { const routerName = router.history.current.name // state const isOne = routerName === 'projects-definition-details' && this.dag.releaseState !== 'NOT_RELEASE' // hide const isTwo = store.state.dag.isDetails const html = [ `${i18n.$t('Start')}`, `${i18n.$t('Edit')}`, `${i18n.$t('Copy')}`, `${i18n.$t('Delete')}` ] const operationHtml = () => { return html.splice(',') } const e = event const $id = e.currentTarget.id const $contextmenu = $('#contextmenu') const $name = $(`#${$id}`).find('.name-p').text() const $left = e.pageX + document.body.scrollLeft - 5 const $top = e.pageY + document.body.scrollTop - 5 $contextmenu.css({ left: $left, top: $top, visibility: 'visible' }) // Action bar $contextmenu.html('').append(operationHtml) if (isOne) { // start run $('#startRunning').on('click', () => { const name = store.state.dag.name const id = router.history.current.params.id store.dispatch('dag/getStartCheck', { processDefinitionId: id }).then(res => { this.dag.startRunning({ id: id, name: name }, $name, 'contextmenu') }) }) } if (!isTwo) { // edit node $('#editNodes').click(ev => { findComponentDownward(this.dag.$root, 'dag-chart')._createNodes({ id: $id, type: $(`#${$id}`).attr('data-tasks-type') }) }) // delete node $('#removeNodes').click(ev => { this.removeNodes($id) }) // copy node $('#copyNodes').click(res => { this.copyNodes($id) }) } } } /** * Node double click event */ JSP.prototype.tasksDblclick = function (e) { // Untie event if (this.config.isDblclick) { const id = $(e.currentTarget.offsetParent).attr('id') findComponentDownward(this.dag.$root, 'dag-chart')._createNodes({ id: id, type: $(`#${id}`).attr('data-tasks-type') }) } } /** * Node click event */ JSP.prototype.tasksClick = function (e) { let $id const self = this const $body = $('body') if (this.config.isClick) { const $connect = this.selectedElement.connect $('.w').removeClass('jtk-tasks-active') $(e.currentTarget).addClass('jtk-tasks-active') if ($connect) { setSvgColor($connect, '#2d8cf0') this.selectedElement.connect = null } this.selectedElement.id = $(e.currentTarget).attr('id') // Unbind copy and paste events $body.unbind('copy').unbind('paste') // Copy binding id $id = self.selectedElement.id $body.bind({ copy: function () { $id = self.selectedElement.id }, paste: function () { $id && self.copyNodes($id) } }) } } /** * Remove binding events * paste */ JSP.prototype.removePaste = function () { const $body = $('body') // Unbind copy and paste events $body.unbind('copy').unbind('paste') // Remove selected node parameters this.selectedElement.id = null // Remove node selection effect $('.w').removeClass('jtk-tasks-active') } /** * Line click event */ JSP.prototype.connectClick = function (e) { // Set svg color setSvgColor(e, '#0097e0') const $id = this.selectedElement.id if ($id) { $(`#${$id}`).removeClass('jtk-tasks-active') this.selectedElement.id = null } this.selectedElement.connect = e } /** * toolbarEvent * @param {Pointer} */ JSP.prototype.handleEventPointer = function (is) { this.setConfig({ isClick: is, isAttachment: false }) } /** * toolbarEvent * @param {Line} */ JSP.prototype.handleEventLine = function (is) { const wDom = $('.w') this.setConfig({ isAttachment: is }) is ? wDom.addClass('jtk-ep') : wDom.removeClass('jtk-ep') } /** * toolbarEvent * @param {Remove} */ JSP.prototype.handleEventRemove = function () { const $id = this.selectedElement.id || null const $connect = this.selectedElement.connect || null if ($id) { this.removeNodes(this.selectedElement.id) } else { this.removeConnect($connect) } // Monitor whether to edit DAG store.commit('dag/setIsEditDag', true) } /** * Delete node */ JSP.prototype.removeNodes = function ($id) { // Delete node processing(data-targetarr) _.map(tasksAll(), v => { const targetarr = v.targetarr.split(',') if (targetarr.length) { const newArr = _.filter(targetarr, v1 => v1 !== $id) $(`#${v.id}`).attr('data-targetarr', newArr.toString()) } }) // delete node this.JspInstance.remove($id) // delete dom $(`#${$id}`).remove() // callback onRemoveNodes event this.options && this.options.onRemoveNodes && this.options.onRemoveNodes($id) const connects = [] _.map(this.JspInstance.getConnections(), v => { connects.push({ endPointSourceId: v.sourceId, endPointTargetId: v.targetId, label: v._jsPlumb.overlays.label.canvas.innerText }) }) // Storage line dependence store.commit('dag/setConnects', connects) } /** * Delete connection */ JSP.prototype.removeConnect = function ($connect) { if (!$connect) { return } // Remove connections and remove node and node dependencies const targetId = $connect.targetId const sourceId = $connect.sourceId let targetarr = rtTargetarrArr(targetId) if (targetarr.length) { targetarr = _.filter(targetarr, v => v !== sourceId) $(`#${targetId}`).attr('data-targetarr', targetarr.toString()) } if ($(`#${sourceId}`).attr('data-tasks-type') === 'CONDITIONS') { $(`#${sourceId}`).attr('data-nodenumber', Number($(`#${sourceId}`).attr('data-nodenumber')) - 1) } this.JspInstance.deleteConnection($connect) this.selectedElement = {} } /** * Copy node */ JSP.prototype.copyNodes = function ($id) { let newNodeInfo = _.cloneDeep(_.find(store.state.dag.tasks, v => v.id === $id)) const newNodePors = store.state.dag.locations[$id] // Unstored nodes do not allow replication if (!newNodePors) { return } // Generate random id const newUuId = `${uuid() + uuid()}` const id = newNodeInfo.id.length > 8 ? newNodeInfo.id.substr(0, 7) : newNodeInfo.id const name = newNodeInfo.name.length > 8 ? newNodeInfo.name.substr(0, 7) : newNodeInfo.name // new id const newId = `${id || ''}-${newUuId}` // new name const newName = `${name || ''}-${newUuId}` // coordinate x const newX = newNodePors.x + 100 // coordinate y const newY = newNodePors.y + 40 // Generate template node $('#canvas').append(rtTasksTpl({ id: newId, name: newName, x: newX, y: newY, isAttachment: this.config.isAttachment, taskType: newNodeInfo.type })) // Get the generated node const thisDom = jsPlumb.getSelector('.statemachine-demo .w') // Copy node information newNodeInfo = Object.assign(newNodeInfo, { id: newId, name: newName }) // Add new node store.commit('dag/addTasks', newNodeInfo) // Add node location information store.commit('dag/addLocations', { [newId]: { name: newName, targetarr: '', nodenumber: 0, x: newX, y: newY } }) // Generating a connection node this.JspInstance.batch(() => { this.initNode(thisDom[thisDom.length - 1]) // Add events to nodes this.tasksEvent(newId) }) } /** * toolbarEvent * @param {Screen} */ JSP.prototype.handleEventScreen = function ({ item, is }) { let screenOpen = true if (is) { item.icon = 'el-icon-minus' screenOpen = true } else { item.icon = 'el-icon-full-screen' screenOpen = false } const $mainLayoutModel = $('.main-layout-model') if (screenOpen) { $mainLayoutModel.addClass('dag-screen') } else { $mainLayoutModel.removeClass('dag-screen') } } /** * save task * @param tasks * @param locations * @param connects */ JSP.prototype.saveStore = function () { return new Promise((resolve, reject) => { const connects = [] const locations = {} const tasks = [] const is = (id) => { return !!_.filter(tasksAll(), v => v.id === id).length } // task _.map(_.cloneDeep(store.state.dag.tasks), v => { if (is(v.id)) { const preTasks = [] const id = $(`#${v.id}`) const tar = id.attr('data-targetarr') const idDep = tar ? id.attr('data-targetarr').split(',') : [] if (idDep.length) { _.map(idDep, v1 => { preTasks.push($(`#${v1}`).find('.name-p').text()) }) } let tasksParam = _.assign(v, { preTasks: preTasks }) // Sub-workflow has no retries and interval if (v.type === 'SUB_PROCESS') { tasksParam = _.omit(tasksParam, ['maxRetryTimes', 'retryInterval']) } tasks.push(tasksParam) } }) console.log(store.state.dag.connects.length, this.JspInstance.getConnections().length) if (store.state.dag.connects.length === this.JspInstance.getConnections().length) { _.map(store.state.dag.connects, u => { connects.push({ endPointSourceId: u.endPointSourceId, endPointTargetId: u.endPointTargetId, label: u.label }) }) } else if (store.state.dag.connects.length > 0 && store.state.dag.connects.length < this.JspInstance.getConnections().length) { _.map(this.JspInstance.getConnections(), v => { connects.push({ endPointSourceId: v.sourceId, endPointTargetId: v.targetId, label: v._jsPlumb.overlays.label.canvas.innerText }) }) _.map(store.state.dag.connects, u => { _.map(connects, v => { if (u.label && u.endPointSourceId === v.endPointSourceId && u.endPointTargetId === v.endPointTargetId) { v.label = u.label } }) }) } else if (store.state.dag.connects.length === 0) { _.map(this.JspInstance.getConnections(), v => { connects.push({ endPointSourceId: v.sourceId, endPointTargetId: v.targetId, label: v._jsPlumb.overlays.label.canvas.innerText }) }) } else if (store.state.dag.connects.length > this.JspInstance.getConnections().length) { _.map(this.JspInstance.getConnections(), v => { connects.push({ endPointSourceId: v.sourceId, endPointTargetId: v.targetId, label: v._jsPlumb.overlays.label.canvas.innerText }) }) } _.map(tasksAll(), v => { locations[v.id] = { name: v.name, targetarr: v.targetarr, nodenumber: v.nodenumber, x: v.x, y: v.y } }) // Storage node store.commit('dag/setTasks', tasks) // Store coordinate information store.commit('dag/setLocations', locations) // Storage line dependence store.commit('dag/setConnects', connects) resolve({ connects: connects, tasks: tasks, locations: locations }) }) } /** * Event processing */ JSP.prototype.handleEvent = function () { this.JspInstance.bind('beforeDrop', function (info) { const sourceId = info.sourceId// 出 const targetId = info.targetId// 入 /** * Recursive search for nodes */ let recursiveVal const recursiveTargetarr = (arr, targetId) => { for (const i in arr) { if (arr[i] === targetId) { recursiveVal = targetId } else { recursiveTargetarr(rtTargetarrArr(arr[i]), targetId) } } return recursiveVal } // Connection to connected nodes is not allowed if (_.findIndex(rtTargetarrArr(targetId), v => v === sourceId) !== -1) { return false } // Recursive form to find if the target Targetarr has a sourceId if (recursiveTargetarr(rtTargetarrArr(sourceId), targetId)) { return false } if ($(`#${sourceId}`).attr('data-tasks-type') === 'CONDITIONS' && $(`#${sourceId}`).attr('data-nodenumber') === 2) { return false } else { $(`#${sourceId}`).attr('data-nodenumber', Number($(`#${sourceId}`).attr('data-nodenumber')) + 1) } // Storage node dependency information saveTargetarr(sourceId, targetId) // Monitor whether to edit DAG store.commit('dag/setIsEditDag', true) return true }) } /** * Backfill data processing */ JSP.prototype.jspBackfill = function ({ connects, locations, largeJson }) { // Backfill nodes this.jsonHandle({ largeJson: largeJson, locations: locations }) const wNodes = jsPlumb.getSelector('.statemachine-demo .w') // Backfill line this.JspInstance.batch(() => { for (let i = 0; i < wNodes.length; i++) { this.initNode(wNodes[i]) } _.map(connects, v => { let sourceId = v.endPointSourceId.split('-') let targetId = v.endPointTargetId.split('-') const labels = v.label if (sourceId.length === 4 && targetId.length === 4) { sourceId = `${sourceId[0]}-${sourceId[1]}-${sourceId[2]}` targetId = `${targetId[0]}-${targetId[1]}-${targetId[2]}` } else { sourceId = v.endPointSourceId targetId = v.endPointTargetId } if ($(`#${sourceId}`).attr('data-tasks-type') === 'CONDITIONS' && $(`#${sourceId}`).attr('data-successnode') === $(`#${targetId}`).find('.name-p').text()) { this.JspInstance.connect({ source: sourceId, target: targetId, type: 'basic', paintStyle: { strokeWidth: 2, stroke: '#4caf50' }, HoverPaintStyle: { stroke: '#ccc', strokeWidth: 3 }, overlays: [['Label', { label: labels }]] }) } else if ($(`#${sourceId}`).attr('data-tasks-type') === 'CONDITIONS' && $(`#${sourceId}`).attr('data-failednode') === $(`#${targetId}`).find('.name-p').text()) { this.JspInstance.connect({ source: sourceId, target: targetId, type: 'basic', paintStyle: { strokeWidth: 2, stroke: '#252d39' }, HoverPaintStyle: { stroke: '#ccc', strokeWidth: 3 }, overlays: [['Label', { label: labels }]] }) } else { this.JspInstance.connect({ source: sourceId, target: targetId, type: 'basic', paintStyle: { strokeWidth: 2, stroke: '#2d8cf0' }, HoverPaintStyle: { stroke: '#ccc', strokeWidth: 3 }, overlays: [['Label', { label: labels }]] }) } }) }) jsPlumb.fire('jsPlumbDemoLoaded', this.JspInstance) // Connection monitoring this.handleEvent() // Drag and drop new nodes this.draggable() } export default new JSP()