You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by ji...@apache.org on 2021/12/03 05:40:15 UTC
[dolphinscheduler] branch 2.0.1-prepare updated: [2.0.1-cherrypick][Improvement][UI] DAG page interaction optimization (#7137)
This is an automated email from the ASF dual-hosted git repository.
jinyleechina pushed a commit to branch 2.0.1-prepare
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git
The following commit(s) were added to refs/heads/2.0.1-prepare by this push:
new b2143c3 [2.0.1-cherrypick][Improvement][UI] DAG page interaction optimization (#7137)
b2143c3 is described below
commit b2143c31e68e656ced1c89a613608289bfc0ba21
Author: wangyizhi <wa...@cmss.chinamobile.com>
AuthorDate: Fri Dec 3 13:40:09 2021 +0800
[2.0.1-cherrypick][Improvement][UI] DAG page interaction optimization (#7137)
* [Feature-6918][UI] DAG page interaction optimization (#6919)
* [Feature] Add grid layout and optimize DAG style
* [Improvement] Add process definition status label and code optimization
* [Feature] add DAG node search bar
* [Feature] Add DAG scale bar
* [Improvement] Open pre-tasks settings for all tasks
* [Fix] Fix ut
* [Fix] Replace /deep/ with ::v-deep
* [Fix] Fix code smell
* [Improvement-7092][UI] Optimize DAG to adapt to situations without locations (#7102)
---
.../conf/home/pages/dag/_source/canvas/canvas.scss | 29 +-
.../conf/home/pages/dag/_source/canvas/canvas.vue | 325 +++++++++------------
.../pages/dag/_source/canvas/layoutConfigModal.vue | 115 ++++++++
.../home/pages/dag/_source/canvas/taskbar.scss | 4 +
.../conf/home/pages/dag/_source/canvas/taskbar.vue | 9 +
.../home/pages/dag/_source/canvas/toolbar.scss | 14 +
.../conf/home/pages/dag/_source/canvas/toolbar.vue | 101 +++++--
.../home/pages/dag/_source/canvas/x6-helper.js | 163 ++++++-----
.../pages/dag/_source/canvas/x6-style-mixin.js | 145 +++++++++
.../home/pages/dag/_source/canvas/x6-style.scss | 21 +-
.../src/js/conf/home/pages/dag/_source/dag.vue | 10 +-
.../home/pages/dag/_source/formModel/formModel.vue | 1 -
.../src/js/conf/home/store/dag/actions.js | 17 +-
13 files changed, 657 insertions(+), 297 deletions(-)
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.scss b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.scss
index bcf6a36..ea15323 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.scss
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.scss
@@ -34,14 +34,35 @@
.minimap {
position: absolute;
- width: 300px;
- height: 200px;
- right: 10px;
- bottom: 10px;
+ right: 0px;
+ bottom: 0px;
border: dashed 1px #e4e4e4;
z-index: 9;
}
+ .scale-slider{
+ position: absolute;
+ height: 140px;
+ width: 70px;
+ right: 0px;
+ bottom: 140px;
+ z-index: 9;
+ display: flex;
+ justify-content: center;
+
+ ::v-deep .el-slider__runway{
+ background-color: #fff;
+ }
+
+ .scale-title{
+ position: absolute;
+ top: -30px;
+ left: 22px;
+ font-size: 12px;
+ color: #666;
+ }
+ }
+
.context-menu{
position: absolute;
left: 100px;
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.vue b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.vue
index 2e4bc44..67c56a6 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.vue
@@ -27,8 +27,21 @@
>
<div ref="paper" class="paper"></div>
<div ref="minimap" class="minimap"></div>
+ <div class="scale-slider">
+ <span class="scale-title">{{$t('dagScale')}}</span>
+ <el-slider
+ v-model="scale"
+ vertical
+ :max="2"
+ :min="0.2"
+ :step="0.2"
+ :marks="SCALE_MARKS"
+ @input='scaleChange'
+ />
+ </div>
<context-menu ref="contextMenu" />
</div>
+ <layout-config-modal ref="layoutModal" @submit="format" />
</div>
</template>
@@ -37,23 +50,25 @@
import { Graph, DataUri } from '@antv/x6'
import dagTaskbar from './taskbar.vue'
import contextMenu from './contextMenu.vue'
+ import layoutConfigModal, { LAYOUT_TYPE, DEFAULT_LAYOUT_CONFIG } from './layoutConfigModal.vue'
import {
- NODE_PROPS,
- EDGE_PROPS,
- PORT_PROPS,
+ NODE,
+ EDGE,
X6_NODE_NAME,
- X6_PORT_OUT_NAME,
- X6_PORT_IN_NAME,
X6_EDGE_NAME,
- NODE_HIGHLIGHT_PROPS,
- PORT_HIGHLIGHT_PROPS,
- EDGE_HIGHLIGHT_PROPS,
NODE_STATUS_MARKUP
} from './x6-helper'
- import { DagreLayout } from '@antv/layout'
+ import { DagreLayout, GridLayout } from '@antv/layout'
import { tasksType, tasksState } from '../config'
import { mapActions, mapMutations, mapState } from 'vuex'
import nodeStatus from './nodeStatus'
+ import x6StyleMixin from './x6-style-mixin'
+
+ const SCALE_MARKS = {
+ 0.2: '0.2',
+ 1: '1',
+ 2: '2'
+ }
export default {
name: 'dag-canvas',
@@ -71,7 +86,10 @@
x: 0,
y: 0,
type: ''
- }
+ },
+ // The canvas scale
+ scale: 1,
+ SCALE_MARKS
}
},
provide () {
@@ -79,10 +97,12 @@
dagCanvas: this
}
},
+ mixins: [x6StyleMixin],
inject: ['dagChart'],
components: {
dagTaskbar,
- contextMenu
+ contextMenu,
+ layoutConfigModal
},
computed: {
...mapState('dag', ['tasks'])
@@ -118,6 +138,14 @@
movable: true,
showNodeSelectionBox: false
},
+ scaling: {
+ min: 0.2,
+ max: 2
+ },
+ mousewheel: {
+ enabled: true,
+ modifiers: ['ctrl', 'meta']
+ },
scroller: true,
grid: {
size: 10,
@@ -126,7 +154,10 @@
snapline: true,
minimap: {
enabled: true,
- container: minimap
+ container: minimap,
+ scalable: false,
+ width: 200,
+ height: 120
},
interacting: {
edgeLabelMovable: false,
@@ -134,9 +165,6 @@
magnetConnectable: !!editable
},
connecting: {
- snap: {
- radius: 30
- },
// Whether multiple edges can be created between the same start node and end
allowMulti: false,
// Whether a point is allowed to connect to a blank position on the canvas
@@ -148,32 +176,14 @@
// Whether edges are allowed to link to nodes
allowNode: true,
// Whether to allow edge links to ports
- allowPort: true,
+ allowPort: false,
// Whether all available ports or nodes are highlighted when you drag the edge
highlight: true,
createEdge () {
return graph.createEdge({ shape: X6_EDGE_NAME })
},
- validateMagnet ({ magnet }) {
- return magnet.getAttribute('port-group') !== X6_PORT_IN_NAME
- },
validateConnection (data) {
- const { sourceCell, targetCell, sourceMagnet, targetMagnet } = data
- // Connections can only be created from the output link post
- if (
- !sourceMagnet ||
- sourceMagnet.getAttribute('port-group') !== X6_PORT_OUT_NAME
- ) {
- return false
- }
-
- // Can only be connected to the input link post
- if (
- !targetMagnet ||
- targetMagnet.getAttribute('port-group') !== X6_PORT_IN_NAME
- ) {
- return false
- }
+ const { sourceCell, targetCell } = data
if (
sourceCell &&
@@ -214,6 +224,7 @@
}
}
}))
+
this.registerX6Shape()
this.bindGraphEvent()
this.originalScrollPosition = graph.getScrollbarPosition()
@@ -224,37 +235,17 @@
registerX6Shape () {
Graph.unregisterNode(X6_NODE_NAME)
Graph.unregisterEdge(X6_EDGE_NAME)
- Graph.registerNode(X6_NODE_NAME, { ...NODE_PROPS })
- Graph.registerEdge(X6_EDGE_NAME, { ...EDGE_PROPS })
+ Graph.registerNode(X6_NODE_NAME, { ...NODE })
+ Graph.registerEdge(X6_EDGE_NAME, { ...EDGE })
},
/**
* Bind grap event
*/
bindGraphEvent () {
- // nodes and edges hover
- this.graph.on('cell:mouseenter', (data) => {
- const { cell, e } = data
- const isStatusIcon = (tagName) =>
- tagName &&
- (tagName.toLocaleLowerCase() === 'em' ||
- tagName.toLocaleLowerCase() === 'body')
- if (!isStatusIcon(e.target.tagName)) {
- this.setHighlight(cell)
- }
- })
- this.graph.on('cell:mouseleave', ({ cell }) => {
- if (!this.graph.isSelected(cell)) {
- this.resetHighlight(cell)
- }
- })
- // select
- this.graph.on('cell:selected', ({ cell }) => {
- this.setHighlight(cell)
- })
- this.graph.on('cell:unselected', ({ cell }) => {
- if (!this.graph.isSelected(cell)) {
- this.resetHighlight(cell)
- }
+ this.bindStyleEvent(this.graph)
+ // update scale bar
+ this.graph.on('scale', ({ sx }) => {
+ this.scale = sx
})
// right click
this.graph.on('node:contextmenu', ({ x, y, cell }) => {
@@ -279,6 +270,13 @@
label: labelName
})
})
+ // Make sure the edge starts with node, not port
+ this.graph.on('edge:connected', ({ isNew, edge }) => {
+ if (isNew) {
+ const sourceNode = edge.getSourceNode()
+ edge.setSource(sourceNode)
+ }
+ })
},
/**
* @param {Edge|string} edge
@@ -297,9 +295,6 @@
setEdgeLabel (id, label) {
const edge = this.graph.getCellById(id)
edge.setLabels(label)
- if (this.graph.isSelected(edge)) {
- this.setEdgeHighlight(edge)
- }
},
/**
* @param {number} limit
@@ -349,94 +344,6 @@
}
},
/**
- * Set node highlight
- * @param {Node} node
- */
- setNodeHighlight (node) {
- const url = require(`../images/task-icos/${node.data.taskType.toLocaleLowerCase()}_hover.png`)
- node.setAttrs(NODE_HIGHLIGHT_PROPS.attrs)
- node.setAttrByPath('image/xlink:href', url)
- node.setPortProp(
- X6_PORT_OUT_NAME,
- 'attrs',
- PORT_HIGHLIGHT_PROPS[X6_PORT_OUT_NAME].attrs
- )
- },
- /**
- * Reset node style
- * @param {Node} node
- */
- resetNodeStyle (node) {
- const url = require(`../images/task-icos/${node.data.taskType.toLocaleLowerCase()}.png`)
- node.setAttrs(NODE_PROPS.attrs)
- node.setAttrByPath('image/xlink:href', url)
- node.setPortProp(
- X6_PORT_OUT_NAME,
- 'attrs',
- PORT_PROPS.groups[X6_PORT_OUT_NAME].attrs
- )
- },
- /**
- * Set edge highlight
- * @param {Edge} edge
- */
- setEdgeHighlight (edge) {
- const labelName = this.getEdgeLabelName(edge)
- edge.setAttrs(EDGE_HIGHLIGHT_PROPS.attrs)
- edge.setLabels([
- _.merge(
- {
- attrs: _.cloneDeep(EDGE_HIGHLIGHT_PROPS.defaultLabel.attrs)
- },
- {
- attrs: { label: { text: labelName } }
- }
- )
- ])
- },
- /**
- * Reset edge style
- * @param {Edge} edge
- */
- resetEdgeStyle (edge) {
- const labelName = this.getEdgeLabelName(edge)
- edge.setAttrs(EDGE_PROPS.attrs)
- edge.setLabels([
- {
- ..._.merge(
- {
- attrs: _.cloneDeep(EDGE_PROPS.defaultLabel.attrs)
- },
- {
- attrs: { label: { text: labelName } }
- }
- )
- }
- ])
- },
- /**
- * Set cell highlight
- * @param {Cell} cell
- */
- setHighlight (cell) {
- if (cell.isEdge()) {
- this.setEdgeHighlight(cell)
- } else if (cell.isNode()) {
- this.setNodeHighlight(cell)
- }
- },
- /**
- * Reset cell highlight
- * @param {Cell} cell
- */
- resetHighlight (cell) {
- if (cell.isEdge()) {
- this.resetEdgeStyle(cell)
- } else if (cell.isNode()) {
- this.resetNodeStyle(cell)
- }
- },
- /**
* Convert the graph to JSON
* @return {{cells:Cell[]}}
*/
@@ -512,38 +419,70 @@
}
)
},
+ showLayoutModal () {
+ const layoutModal = this.$refs.layoutModal
+ if (layoutModal) {
+ layoutModal.show()
+ }
+ },
/**
* format
* @desc Auto layout use @antv/layout
*/
- format () {
- const dagreLayout = new DagreLayout({
- type: 'dagre',
- rankdir: 'LR',
- align: 'UL',
- // Calculate the node spacing based on the edge label length
- ranksepFunc: (d) => {
- const edges = this.graph.getOutgoingEdges(d.id)
- let max = 0
- if (edges && edges.length > 0) {
- edges.forEach((edge) => {
- const edgeView = this.graph.findViewByCell(edge)
- const labelWidth = +edgeView.findAttr(
- 'width',
- _.get(edgeView, ['labelSelectors', '0', 'body'], null)
- )
- max = Math.max(max, labelWidth)
- })
- }
- return 50 + max
- },
- nodesep: 50,
- controlPoints: true
- })
+ format (layoutConfig) {
+ if (!layoutConfig) {
+ layoutConfig = DEFAULT_LAYOUT_CONFIG
+ }
+ this.graph.cleanSelection()
+
+ let layoutFunc = null
+ if (layoutConfig.type === LAYOUT_TYPE.DAGRE) {
+ layoutFunc = new DagreLayout({
+ type: LAYOUT_TYPE.DAGRE,
+ rankdir: 'LR',
+ align: 'UL',
+ // Calculate the node spacing based on the edge label length
+ ranksepFunc: (d) => {
+ const edges = this.graph.getOutgoingEdges(d.id)
+ let max = 0
+ if (edges && edges.length > 0) {
+ edges.forEach((edge) => {
+ const edgeView = this.graph.findViewByCell(edge)
+ const labelWidth = +edgeView.findAttr(
+ 'width',
+ _.get(edgeView, ['labelSelectors', '0', 'body'], null)
+ )
+ max = Math.max(max, labelWidth)
+ })
+ }
+ return layoutConfig.ranksep + max
+ },
+ nodesep: layoutConfig.nodesep,
+ controlPoints: true
+ })
+ } else if (layoutConfig.type === LAYOUT_TYPE.GRID) {
+ layoutFunc = new GridLayout({
+ type: LAYOUT_TYPE.GRID,
+ preventOverlap: true,
+ preventOverlapPadding: layoutConfig.padding,
+ sortBy: '_index',
+ rows: layoutConfig.rows || undefined,
+ cols: layoutConfig.cols || undefined,
+ nodeSize: 220
+ })
+ }
const json = this.toJSON()
- const nodes = json.cells.filter((cell) => cell.shape === X6_NODE_NAME)
+ const nodes = json.cells
+ .filter((cell) => cell.shape === X6_NODE_NAME)
+ .map((item) => {
+ return {
+ ...item,
+ // sort by code aesc
+ _index: -item.id
+ }
+ })
const edges = json.cells.filter((cell) => cell.shape === X6_EDGE_NAME)
- const newModel = dagreLayout.layout({
+ const newModel = layoutFunc.layout({
nodes: nodes,
edges: edges
})
@@ -606,12 +545,10 @@
return {
shape: X6_EDGE_NAME,
source: {
- cell: sourceId,
- port: X6_PORT_OUT_NAME
+ cell: sourceId
},
target: {
- cell: targetId,
- port: X6_PORT_IN_NAME
+ cell: targetId
},
labels: label ? [label] : undefined
}
@@ -688,7 +625,7 @@
if (node) {
// Destroy the previous dom
node.removeMarkup()
- node.setMarkup(NODE_PROPS.markup.concat(NODE_STATUS_MARKUP))
+ node.setMarkup(NODE.markup.concat(NODE_STATUS_MARKUP))
const nodeView = this.graph.findViewByCell(node)
const el = nodeView.find('div')[0]
nodeStatus({
@@ -828,6 +765,28 @@
const edge = this.genEdgeJSON(code, postCode)
this.graph.addEdge(edge)
})
+ },
+ /**
+ * Navigate to cell
+ * @param {string} taskName
+ */
+ navigateTo (taskName) {
+ const nodes = this.getNodes()
+ nodes.forEach((node) => {
+ if (node.data.taskName === taskName) {
+ const id = node.id
+ const cell = this.graph.getCellById(id)
+ this.graph.scrollToCell(cell, { animation: { duration: 600 } })
+ this.graph.cleanSelection()
+ this.graph.select(cell)
+ }
+ })
+ },
+ /**
+ * Canvas scale
+ */
+ scaleChange (val) {
+ this.graph.zoomTo(val)
}
}
}
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/layoutConfigModal.vue b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/layoutConfigModal.vue
new file mode 100644
index 0000000..d13bd68
--- /dev/null
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/layoutConfigModal.vue
@@ -0,0 +1,115 @@
+/*
+ * 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.
+ */
+<template>
+ <el-dialog
+ :title="$t('Format DAG')"
+ :visible.sync="visible"
+ width="500px"
+ class="dag-layout-modal"
+ :append-to-body="true"
+ >
+ <el-form
+ ref="form"
+ :model="form"
+ label-width="100px"
+ class="dag-layout-form"
+ >
+ <el-form-item :label="$t('layoutType')">
+ <el-radio-group v-model="form.type">
+ <el-radio label="grid">{{ $t("gridLayout") }}</el-radio>
+ <el-radio label="dagre">{{ $t("dagreLayout") }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item :label="$t('rows')" v-if="form.type === LAYOUT_TYPE.GRID">
+ <el-input-number
+ v-model="form.rows"
+ :min="0"
+ size="small"
+ ></el-input-number>
+ </el-form-item>
+ <el-form-item :label="$t('cols')" v-if="form.type === LAYOUT_TYPE.GRID">
+ <el-input-number
+ v-model="form.cols"
+ :min="0"
+ size="small"
+ ></el-input-number>
+ </el-form-item>
+ </el-form>
+ <span slot="footer" class="dialog-footer">
+ <el-button size="small" @click="close">{{ $t("Cancel") }}</el-button>
+ <el-button size="small" type="primary" @click="submit">{{
+ $t("Confirm")
+ }}</el-button>
+ </span>
+ </el-dialog>
+</template>
+<script>
+ export const LAYOUT_TYPE = {
+ GRID: 'grid',
+ DAGRE: 'dagre'
+ }
+
+ export const DEFAULT_LAYOUT_CONFIG = {
+ cols: 0,
+ nodesep: 50,
+ padding: 50,
+ ranksep: 50,
+ rows: 0,
+ type: LAYOUT_TYPE.DAGRE
+ }
+
+ export default {
+ data () {
+ return {
+ visible: false,
+ form: { ...DEFAULT_LAYOUT_CONFIG },
+ LAYOUT_TYPE
+ }
+ },
+ methods: {
+ show () {
+ this.visible = true
+ },
+ close () {
+ this.visible = false
+ },
+ submit () {
+ this.$emit('submit', this.form)
+ this.close()
+ }
+ }
+ }
+</script>
+<style lang="scss" scoped>
+.dag-layout-modal {
+ ::v-deep .el-dialog__header {
+ border-bottom: solid 1px #d4d4d4;
+ }
+
+ ::v-deep .dag-layout-form {
+ margin-top: 20px;
+ }
+
+ ::v-deep .el-radio {
+ margin-bottom: 0;
+ }
+
+ .el-form-item {
+ margin-bottom: 10px;
+ }
+}
+</style>
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.scss b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.scss
index 9f1c6ee..aec283f 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.scss
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.scss
@@ -174,6 +174,10 @@
}
}
}
+
+ &.disabled{
+ cursor: default
+ }
}
}
}
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue
index 0ffc40c..7c5b665 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue
@@ -24,6 +24,9 @@
<draggable-box
:key="taskType.name"
@onDragstart="(e) => $emit('on-drag-start', e, taskType)"
+ :class="{
+ disabled: isDetails
+ }"
>
<div class="task-item">
<em :class="`icos-${taskType.name.toLocaleLowerCase()}`"></em>
@@ -38,6 +41,7 @@
<script>
import draggableBox from './draggableBox.vue'
import { tasksType } from '../config.js'
+ import { mapState } from 'vuex'
export default {
name: 'dag-taskbar',
@@ -55,6 +59,11 @@
return {
tasksTypeList
}
+ },
+ computed: {
+ ...mapState('dag', [
+ 'isDetails'
+ ])
}
}
</script>
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.scss b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.scss
index 03578f3..155083c 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.scss
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.scss
@@ -110,4 +110,18 @@
}
}
}
+
+ .process-online-tag{
+ margin-left: 10px;
+ }
+
+ .search-box{
+ width: 0;
+ overflow: hidden;
+ transition: all 0.5s;
+
+ &.visible{
+ width: 200px;
+ }
+ }
}
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue
index 4071fd9..d8f57ad 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue
@@ -23,76 +23,111 @@
:content="$t('Copy name')"
placement="bottom"
>
- <i class="el-icon-copy-document" @click="copyName"></i>
+ <em class="el-icon-copy-document" @click="copyName"></em>
</el-tooltip>
<textarea ref="textarea" cols="30" rows="10" class="transparent"></textarea>
<div class="toolbar-left">
+ <el-tag
+ class="process-online-tag"
+ size="small"
+ v-if="dagChart.type === 'definition' && releaseState === 'ONLINE'"
+ >{{ $t("processOnline") }}</el-tag
+ >
<el-tooltip
:content="$t('View variables')"
placement="bottom"
class="toolbar-operation"
>
- <i
+ <em
class="custom-ico view-variables"
v-if="$route.name === 'projects-instance-details'"
@click="toggleVariableView"
- ></i>
+ ></em>
</el-tooltip>
<el-tooltip
:content="$t('Startup parameter')"
placement="bottom"
class="toolbar-operation"
>
- <i
+ <em
class="custom-ico startup-parameters"
v-if="$route.name === 'projects-instance-details'"
@click="toggleParamView"
- ></i>
+ ></em>
</el-tooltip>
</div>
<div class="toolbar-right">
<el-tooltip
class="toolbar-operation"
+ :content="$t('searchNode')"
+ placement="bottom"
+ v-if="!searchInputVisible"
+ >
+ <em
+ class="el-icon-search"
+ @click="showSearchInput"
+ ></em>
+ </el-tooltip>
+ <div
+ :class="{
+ 'search-box': true,
+ 'visible': searchInputVisible
+ }"
+ >
+ <el-input
+ v-model="searchText"
+ placeholder=""
+ prefix-icon="el-icon-search"
+ size="mini"
+ @keyup.enter.native="onSearch"
+ clearable
+ @blur="searchInputBlur"
+ ref="searchInput"
+ ></el-input>
+ </div>
+ <el-tooltip
+ class="toolbar-operation"
:content="$t('Delete selected lines or nodes')"
placement="bottom"
v-if="!isDetails"
>
- <i class="el-icon-delete" @click="removeCells"></i>
+ <em class="el-icon-delete" @click="removeCells"></em>
</el-tooltip>
<el-tooltip
class="toolbar-operation"
:content="$t('Download')"
placement="bottom"
>
- <i class="el-icon-download" @click="downloadPNG"></i>
+ <em class="el-icon-download" @click="downloadPNG"></em>
</el-tooltip>
<el-tooltip
class="toolbar-operation"
- :content="$t('Full Screen')"
+ :content="$t('Refresh DAG status')"
placement="bottom"
+ v-if="dagChart.type === 'instance'"
>
- <i
- :class="[
- 'custom-ico',
- dagChart.fullScreen ? 'full-screen-close' : 'full-screen-open',
- ]"
- @click="toggleFullScreen"
- ></i>
+ <em class="el-icon-refresh" @click="refreshTaskStatus"></em>
</el-tooltip>
<el-tooltip
class="toolbar-operation"
- :content="$t('Refresh DAG status')"
+ :content="$t('Format DAG')"
placement="bottom"
- v-if="dagChart.type === 'instance'"
+ v-if="!isDetails"
>
- <i class="el-icon-refresh" @click="refreshTaskStatus"></i>
+ <em class="custom-ico graph-format" @click="chartFormat"></em>
</el-tooltip>
<el-tooltip
class="toolbar-operation last"
- :content="$t('Format DAG')"
+ :content="$t('Full Screen')"
placement="bottom"
>
- <i class="custom-ico graph-format" @click="chartFormat"></i>
+ <em
+ :class="[
+ 'custom-ico',
+ dagChart.fullScreen ? 'full-screen-close' : 'full-screen-open',
+ ]"
+ @click="toggleFullScreen"
+ ></em>
</el-tooltip>
<el-button
class="toolbar-el-btn"
@@ -101,7 +136,7 @@
v-if="dagChart.type === 'definition'"
@click="showVersions"
icon="el-icon-info"
- >{{$t('Version Info')}}</el-button
+ >{{ $t("Version Info") }}</el-button
>
<el-button
class="toolbar-el-btn"
@@ -125,7 +160,6 @@
type="primary"
icon="el-icon-switch-button"
size="mini"
- v-if="type === 'instance' || 'definition'"
@click="returnToListPage"
>
{{ $t("Close") }}
@@ -143,15 +177,28 @@
inject: ['dagChart'],
data () {
return {
- canvasRef: null
+ canvasRef: null,
+ searchText: '',
+ searchInputVisible: false
}
},
computed: {
- ...mapState('dag', [
- 'isDetails'
- ])
+ ...mapState('dag', ['isDetails', 'releaseState'])
},
methods: {
+ onSearch () {
+ const canvas = this.getDagCanvasRef()
+ canvas.navigateTo(this.searchText)
+ },
+ showSearchInput () {
+ this.searchInputVisible = true
+ this.$refs.searchInput.focus()
+ },
+ searchInputBlur () {
+ if (!this.searchText) {
+ this.searchInputVisible = false
+ }
+ },
getDagCanvasRef () {
if (this.canvasRef) {
return this.canvasRef
@@ -200,7 +247,7 @@
},
chartFormat () {
const canvas = this.getDagCanvasRef()
- canvas.format()
+ canvas.showLayoutModal()
},
refreshTaskStatus () {
this.dagChart.refreshTaskStatus()
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-helper.js b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-helper.js
index 96a48eb..1be067f 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-helper.js
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-helper.js
@@ -17,16 +17,17 @@
export const X6_NODE_NAME = 'dag-task'
export const X6_EDGE_NAME = 'dag-edge'
export const X6_PORT_OUT_NAME = 'dag-port-out'
-export const X6_PORT_IN_NAME = 'dag-port-in'
-const EDGE = '#999999'
-const BG_BLUE = 'rgba(40, 143, 255, 0.1)'
+const EDGE_COLOR = '#999999'
+const BG_BLUE = '#DFE9F7'
const BG_WHITE = '#FFFFFF'
-const NODE_BORDER = '#e4e4e4'
-const TITLE = '#333'
+const NODE_BORDER = '#CCCCCC'
+const TITLE = '#333333'
const STROKE_BLUE = '#288FFF'
+const NODE_SHADOW = 'drop-shadow(3px 3px 4px rgba(0, 0, 0, 0.2))'
+const EDGE_SHADOW = 'drop-shadow(3px 3px 2px rgba(0, 0, 0, 0.2))'
-export const PORT_PROPS = {
+export const PORT = {
groups: {
[X6_PORT_OUT_NAME]: {
position: {
@@ -62,14 +63,14 @@ export const PORT_PROPS = {
},
'plus-text': {
fontSize: 12,
- fill: EDGE,
+ fill: NODE_BORDER,
text: '+',
textAnchor: 'middle',
x: 0,
y: 3
},
'circle-outer': {
- stroke: EDGE,
+ stroke: NODE_BORDER,
strokeWidth: 1,
r: 6,
fill: BG_WHITE
@@ -79,57 +80,42 @@ export const PORT_PROPS = {
fill: 'transparent'
}
}
- },
- [X6_PORT_IN_NAME]: {
- position: {
- name: 'absolute',
- args: {
- x: 0,
- y: 24
- }
- },
- markup: [
- {
- tagName: 'g',
- selector: 'body',
- className: 'in-port-body',
- children: [{
- tagName: 'circle',
- selector: 'circle',
- className: 'circle'
- }]
- }
- ],
+ }
+ }
+}
+
+export const PORT_HOVER = {
+ groups: {
+ [X6_PORT_OUT_NAME]: {
attrs: {
- body: {
- magnet: true
+ 'circle-outer': {
+ stroke: STROKE_BLUE,
+ fill: BG_BLUE,
+ r: 8
},
- circle: {
- r: 4,
- strokeWidth: 0,
- fill: 'transparent'
+ 'circle-inner': {
+ fill: STROKE_BLUE,
+ r: 6
}
}
}
}
}
-export const PORT_HIGHLIGHT_PROPS = {
- [X6_PORT_OUT_NAME]: {
- attrs: {
- 'circle-outer': {
- stroke: STROKE_BLUE,
- fill: BG_BLUE
- },
- 'plus-text': {
- fill: STROKE_BLUE
- },
- 'circle-inner': {
- fill: STROKE_BLUE
+export const PORT_SELECTED = {
+ groups: {
+ [X6_PORT_OUT_NAME]: {
+ attrs: {
+ 'plus-text': {
+ fill: STROKE_BLUE
+ },
+ 'circle-outer': {
+ stroke: STROKE_BLUE,
+ fill: BG_WHITE
+ }
}
}
- },
- [X6_PORT_IN_NAME]: {}
+ }
}
export const NODE_STATUS_MARKUP = [{
@@ -148,13 +134,14 @@ export const NODE_STATUS_MARKUP = [{
]
}]
-export const NODE_PROPS = {
+export const NODE = {
width: 220,
height: 48,
markup: [
{
tagName: 'rect',
- selector: 'body'
+ selector: 'body',
+ className: 'dag-task-body'
},
{
tagName: 'image',
@@ -174,7 +161,9 @@ export const NODE_PROPS = {
pointerEvents: 'visiblePainted',
fill: BG_WHITE,
stroke: NODE_BORDER,
- strokeWidth: 1
+ strokeWidth: 1,
+ strokeDasharray: 'none',
+ filter: 'none'
},
image: {
width: 30,
@@ -199,21 +188,17 @@ export const NODE_PROPS = {
}
},
ports: {
- ...PORT_PROPS,
+ ...PORT,
items: [
{
id: X6_PORT_OUT_NAME,
group: X6_PORT_OUT_NAME
- },
- {
- id: X6_PORT_IN_NAME,
- group: X6_PORT_IN_NAME
}
]
}
}
-export const NODE_HIGHLIGHT_PROPS = {
+export const NODE_HOVER = {
attrs: {
body: {
fill: BG_BLUE,
@@ -226,28 +211,42 @@ export const NODE_HIGHLIGHT_PROPS = {
}
}
-export const EDGE_PROPS = {
+export const NODE_SELECTED = {
+ attrs: {
+ body: {
+ filter: NODE_SHADOW,
+ fill: BG_WHITE,
+ stroke: STROKE_BLUE,
+ strokeDasharray: '5,2',
+ strokeWidth: '1.5'
+ },
+ title: {
+ fill: STROKE_BLUE
+ }
+ }
+}
+
+export const EDGE = {
attrs: {
line: {
- stroke: EDGE,
- strokeWidth: 0.8,
+ stroke: EDGE_COLOR,
+ strokeWidth: 1,
targetMarker: {
tagName: 'path',
- fill: EDGE,
+ fill: EDGE_COLOR,
strokeWidth: 0,
d: 'M 6 -3 0 0 6 3 Z'
- }
+ },
+ filter: 'none'
}
},
connector: {
name: 'rounded'
},
router: {
- name: 'er',
+ name: 'manhattan',
args: {
- offset: 20,
- min: 20,
- direction: 'L'
+ endDirections: ['top', 'bottom', 'left']
}
},
defaultLabel: {
@@ -263,7 +262,7 @@ export const EDGE_PROPS = {
],
attrs: {
label: {
- fill: EDGE,
+ fill: EDGE_COLOR,
fontSize: 14,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
@@ -272,7 +271,7 @@ export const EDGE_PROPS = {
body: {
ref: 'label',
fill: BG_WHITE,
- stroke: EDGE,
+ stroke: EDGE_COLOR,
strokeWidth: 1,
rx: 4,
ry: 4,
@@ -292,7 +291,7 @@ export const EDGE_PROPS = {
}
}
-export const EDGE_HIGHLIGHT_PROPS = {
+export const EDGE_HOVER = {
attrs: {
line: {
stroke: STROKE_BLUE,
@@ -313,3 +312,27 @@ export const EDGE_HIGHLIGHT_PROPS = {
}
}
}
+
+export const EDGE_SELECTED = {
+ attrs: {
+ line: {
+ stroke: STROKE_BLUE,
+ targetMarker: {
+ fill: STROKE_BLUE
+ },
+ strokeWidth: 2,
+ filter: EDGE_SHADOW
+ }
+ },
+ defaultLabel: {
+ attrs: {
+ label: {
+ fill: STROKE_BLUE
+ },
+ body: {
+ fill: BG_WHITE,
+ stroke: STROKE_BLUE
+ }
+ }
+ }
+}
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style-mixin.js b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style-mixin.js
new file mode 100644
index 0000000..d5f9f7b
--- /dev/null
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style-mixin.js
@@ -0,0 +1,145 @@
+/*
+ * 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 {
+ NODE,
+ EDGE,
+ PORT,
+ NODE_HOVER,
+ PORT_HOVER,
+ EDGE_HOVER,
+ PORT_SELECTED,
+ NODE_SELECTED,
+ EDGE_SELECTED,
+ X6_PORT_OUT_NAME
+} from './x6-helper'
+import _ from 'lodash'
+
+export default {
+ data () {
+ return {
+ hoverCell: null
+ }
+ },
+ methods: {
+ bindStyleEvent (graph) {
+ // nodes and edges hover
+ graph.on('cell:mouseenter', (data) => {
+ const { cell, e } = data
+ const isStatusIcon = (tagName) =>
+ tagName &&
+ (tagName.toLocaleLowerCase() === 'em' ||
+ tagName.toLocaleLowerCase() === 'body')
+ if (!isStatusIcon(e.target.tagName)) {
+ this.hoverCell = cell
+ this.updateCellStyle(cell, graph)
+ }
+ })
+ graph.on('cell:mouseleave', ({ cell }) => {
+ this.hoverCell = null
+ this.updateCellStyle(cell, graph)
+ })
+ // select
+ graph.on('cell:selected', ({ cell }) => {
+ this.updateCellStyle(cell, graph)
+ })
+ graph.on('cell:unselected', ({ cell }) => {
+ this.updateCellStyle(cell, graph)
+ })
+ },
+ updateCellStyle (cell, graph) {
+ if (cell.isEdge()) {
+ this.setEdgeStyle(cell, graph)
+ } else if (cell.isNode()) {
+ this.setNodeStyle(cell, graph)
+ }
+ },
+ /**
+ * Set node style
+ * @param {Node} node
+ * @param {Graph} graph
+ */
+ setNodeStyle (node, graph) {
+ const isHover = node === this.hoverCell
+ const isSelected = graph.isSelected(node)
+ const portHover = _.cloneDeep(PORT_HOVER.groups[X6_PORT_OUT_NAME].attrs)
+ const portSelected = _.cloneDeep(PORT_SELECTED.groups[X6_PORT_OUT_NAME].attrs)
+ const portDefault = _.cloneDeep(PORT.groups[X6_PORT_OUT_NAME].attrs)
+ const nodeHover = _.merge(_.cloneDeep(NODE.attrs), NODE_HOVER.attrs)
+ const nodeSelected = _.merge(_.cloneDeep(NODE.attrs), NODE_SELECTED.attrs)
+
+ let img = null
+ let nodeAttrs = null
+ let portAttrs = null
+
+ if (isHover || isSelected) {
+ img = require(`../images/task-icos/${node.data.taskType.toLocaleLowerCase()}_hover.png`)
+ if (isHover) {
+ nodeAttrs = nodeHover
+ portAttrs = _.merge(portDefault, portHover)
+ } else {
+ nodeAttrs = nodeSelected
+ portAttrs = _.merge(portDefault, portSelected)
+ }
+ } else {
+ img = require(`../images/task-icos/${node.data.taskType.toLocaleLowerCase()}.png`)
+ nodeAttrs = NODE.attrs
+ portAttrs = portDefault
+ }
+ node.setAttrByPath('image/xlink:href', img)
+ node.setAttrs(nodeAttrs)
+ node.setPortProp(
+ X6_PORT_OUT_NAME,
+ 'attrs',
+ portAttrs
+ )
+ },
+ /**
+ * Set edge style
+ * @param {Edge} edge
+ * @param {Graph} graph
+ */
+ setEdgeStyle (edge, graph) {
+ const isHover = edge === this.hoverCell
+ const isSelected = graph.isSelected(edge)
+ const labelName = this.getEdgeLabelName ? this.getEdgeLabelName(edge) : ''
+ let edgeProps = null
+
+ if (isHover) {
+ edgeProps = _.merge(_.cloneDeep(EDGE), EDGE_HOVER)
+ } else if (isSelected) {
+ edgeProps = _.merge(_.cloneDeep(EDGE), EDGE_SELECTED)
+ } else {
+ edgeProps = _.cloneDeep(EDGE)
+ }
+
+ edge.setAttrs(edgeProps.attrs)
+ edge.setLabels([
+ {
+ ..._.merge(
+ {
+ attrs: _.cloneDeep(edgeProps.defaultLabel.attrs)
+ },
+ {
+ attrs: { label: { text: labelName } }
+ }
+ )
+ }
+ ])
+ }
+ }
+}
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style.scss b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style.scss
index b86b51a..bc56d51 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style.scss
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style.scss
@@ -14,16 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-$STROKE_BLUE: #288FFF;
-$BG_WHITE: #FFFFFF;
+$STROKE_BLUE: #288fff;
+$BG_WHITE: #ffffff;
-.x6-node[data-shape="dag-task"]{
- .in-port-body{
- &.adsorbed,&.available{
- .circle {
- stroke: $STROKE_BLUE;
- stroke-width: 4;
- fill: $BG_WHITE;
+.x6-node[data-shape="dag-task"] {
+ &.available {
+ .dag-task-body {
+ stroke: $STROKE_BLUE;
+ stroke-width: 1;
+ stroke-dasharray: 5, 2;
+ }
+ &.adsorbed {
+ .dag-task-body {
+ stroke-width: 3;
}
}
}
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue
index 77946e7..1489020 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue
@@ -60,6 +60,10 @@
</el-dialog>
<edge-edit-model ref="edgeEditModel" />
<el-drawer :visible.sync="versionDrawer" size="" :with-header="false">
+ <!-- fix the bug that Element-ui(2.13.2) auto focus on the first input -->
+ <div style="width: 0px; height: 0px; overflow: hidden">
+ <el-input type="text" />
+ </div>
<m-versions
:versionData="versionData"
:isInstance="type === 'instance'"
@@ -187,7 +191,6 @@
},
beforeDestroy () {
this.resetParams()
-
clearInterval(this.statusTimer)
window.removeEventListener('resize', this.resizeDebounceFunc)
},
@@ -400,6 +403,7 @@
buildGraphJSON (tasks, locations, connects) {
const nodes = []
const edges = []
+ if (!locations) { locations = [] }
tasks.forEach((task) => {
const location = locations.find((l) => l.taskCode === task.code) || {}
const node = this.$refs.canvas.genNodeJSON(
@@ -484,6 +488,10 @@
const connects = this.connects
const json = this.buildGraphJSON(tasks, locations, connects)
this.$refs.canvas.fromJSON(json)
+ // Auto format
+ if (!locations) {
+ this.$refs.canvas.format()
+ }
},
/**
* Return to the previous process
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
index fcafefc..0552a4d 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
@@ -398,7 +398,6 @@
<!-- Pre-tasks in workflow -->
<m-pre-tasks
ref="preTasks"
- v-if="['SHELL', 'SUB_PROCESS'].indexOf(nodeData.taskType) > -1"
:code="code"
/>
</div>
diff --git a/dolphinscheduler-ui/src/js/conf/home/store/dag/actions.js b/dolphinscheduler-ui/src/js/conf/home/store/dag/actions.js
index a78df48..fda65d9 100644
--- a/dolphinscheduler-ui/src/js/conf/home/store/dag/actions.js
+++ b/dolphinscheduler-ui/src/js/conf/home/store/dag/actions.js
@@ -18,6 +18,16 @@
import _ from 'lodash'
import io from '@/module/io'
+// Avoid passing in illegal values when users directly call third-party interfaces
+const convertLocations = (locationStr) => {
+ let locations = null
+ if (!locationStr) return locations
+ try {
+ locations = JSON.parse(locationStr)
+ } catch (error) {}
+ return Array.isArray(locations) ? locations : null
+}
+
export default {
/**
* Task status acquisition
@@ -133,12 +143,14 @@ export default {
state.version = res.data.processDefinition.version
// name
state.name = res.data.processDefinition.name
+ // releaseState
+ state.releaseState = res.data.processDefinition.releaseState
// description
state.description = res.data.processDefinition.description
// taskRelationJson
state.connects = res.data.processTaskRelationList
// locations
- state.locations = JSON.parse(res.data.processDefinition.locations)
+ state.locations = convertLocations(res.data.processDefinition.locations)
// global params
state.globalParams = res.data.processDefinition.globalParamList
// timeout
@@ -164,6 +176,7 @@ export default {
'timeout',
'environmentCode'
]))
+
resolve(res.data)
}).catch(res => {
reject(res)
@@ -235,7 +248,7 @@ export default {
// connects
state.connects = processTaskRelationList
// locations
- state.locations = JSON.parse(processDefinition.locations)
+ state.locations = convertLocations(processDefinition.locations)
// global params
state.globalParams = processDefinition.globalParamList
// timeout