Browse Source

[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
3.0.0/version-upgrade
wangyizhi 3 years ago committed by GitHub
parent
commit
f2d242c7e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.scss
  2. 284
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.vue
  3. 113
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/layoutConfigModal.vue
  4. 4
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.scss
  5. 9
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue
  6. 14
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.scss
  7. 101
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue
  8. 149
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-helper.js
  9. 145
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style-mixin.js
  10. 17
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/x6-style.scss
  11. 5
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue
  12. 2
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
  13. 4
      dolphinscheduler-ui/src/js/conf/home/store/dag/actions.js
  14. 10
      dolphinscheduler-ui/src/js/module/i18n/locale/en_US.js
  15. 10
      dolphinscheduler-ui/src/js/module/i18n/locale/zh_CN.js

29
dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.scss

@ -34,14 +34,35 @@
.minimap { .minimap {
position: absolute; position: absolute;
width: 300px; right: 0px;
height: 200px; bottom: 0px;
right: 10px;
bottom: 10px;
border: dashed 1px #e4e4e4; border: dashed 1px #e4e4e4;
z-index: 9; 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{ .context-menu{
position: absolute; position: absolute;
left: 100px; left: 100px;

284
dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/canvas.vue

@ -27,8 +27,21 @@
> >
<div ref="paper" class="paper"></div> <div ref="paper" class="paper"></div>
<div ref="minimap" class="minimap"></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" /> <context-menu ref="contextMenu" />
</div> </div>
<layout-config-modal ref="layoutModal" @submit="format" />
</div> </div>
</template> </template>
@ -37,23 +50,25 @@
import { Graph, DataUri } from '@antv/x6' import { Graph, DataUri } from '@antv/x6'
import dagTaskbar from './taskbar.vue' import dagTaskbar from './taskbar.vue'
import contextMenu from './contextMenu.vue' import contextMenu from './contextMenu.vue'
import layoutConfigModal, { LAYOUT_TYPE } from './layoutConfigModal.vue'
import { import {
NODE_PROPS, NODE,
EDGE_PROPS, EDGE,
PORT_PROPS,
X6_NODE_NAME, X6_NODE_NAME,
X6_PORT_OUT_NAME,
X6_PORT_IN_NAME,
X6_EDGE_NAME, X6_EDGE_NAME,
NODE_HIGHLIGHT_PROPS,
PORT_HIGHLIGHT_PROPS,
EDGE_HIGHLIGHT_PROPS,
NODE_STATUS_MARKUP NODE_STATUS_MARKUP
} from './x6-helper' } from './x6-helper'
import { DagreLayout } from '@antv/layout' import { DagreLayout, GridLayout } from '@antv/layout'
import { tasksType, tasksState } from '../config' import { tasksType, tasksState } from '../config'
import { mapActions, mapMutations, mapState } from 'vuex' import { mapActions, mapMutations, mapState } from 'vuex'
import nodeStatus from './nodeStatus' import nodeStatus from './nodeStatus'
import x6StyleMixin from './x6-style-mixin'
const SCALE_MARKS = {
0.2: '0.2',
1: '1',
2: '2'
}
export default { export default {
name: 'dag-canvas', name: 'dag-canvas',
@ -71,7 +86,10 @@
x: 0, x: 0,
y: 0, y: 0,
type: '' type: ''
} },
// The canvas scale
scale: 1,
SCALE_MARKS
} }
}, },
provide () { provide () {
@ -79,10 +97,12 @@
dagCanvas: this dagCanvas: this
} }
}, },
mixins: [x6StyleMixin],
inject: ['dagChart'], inject: ['dagChart'],
components: { components: {
dagTaskbar, dagTaskbar,
contextMenu contextMenu,
layoutConfigModal
}, },
computed: { computed: {
...mapState('dag', ['tasks']) ...mapState('dag', ['tasks'])
@ -118,6 +138,14 @@
movable: true, movable: true,
showNodeSelectionBox: false showNodeSelectionBox: false
}, },
scaling: {
min: 0.2,
max: 2
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta']
},
scroller: true, scroller: true,
grid: { grid: {
size: 10, size: 10,
@ -126,7 +154,10 @@
snapline: true, snapline: true,
minimap: { minimap: {
enabled: true, enabled: true,
container: minimap container: minimap,
scalable: false,
width: 200,
height: 120
}, },
interacting: { interacting: {
edgeLabelMovable: false, edgeLabelMovable: false,
@ -134,9 +165,6 @@
magnetConnectable: !!editable magnetConnectable: !!editable
}, },
connecting: { connecting: {
snap: {
radius: 30
},
// Whether multiple edges can be created between the same start node and end // Whether multiple edges can be created between the same start node and end
allowMulti: false, allowMulti: false,
// Whether a point is allowed to connect to a blank position on the canvas // 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 // Whether edges are allowed to link to nodes
allowNode: true, allowNode: true,
// Whether to allow edge links to ports // Whether to allow edge links to ports
allowPort: true, allowPort: false,
// Whether all available ports or nodes are highlighted when you drag the edge // Whether all available ports or nodes are highlighted when you drag the edge
highlight: true, highlight: true,
createEdge () { createEdge () {
return graph.createEdge({ shape: X6_EDGE_NAME }) return graph.createEdge({ shape: X6_EDGE_NAME })
}, },
validateMagnet ({ magnet }) {
return magnet.getAttribute('port-group') !== X6_PORT_IN_NAME
},
validateConnection (data) { validateConnection (data) {
const { sourceCell, targetCell, sourceMagnet, targetMagnet } = data const { sourceCell, targetCell } = 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
}
if ( if (
sourceCell && sourceCell &&
@ -214,6 +224,7 @@
} }
} }
})) }))
this.registerX6Shape() this.registerX6Shape()
this.bindGraphEvent() this.bindGraphEvent()
this.originalScrollPosition = graph.getScrollbarPosition() this.originalScrollPosition = graph.getScrollbarPosition()
@ -224,37 +235,17 @@
registerX6Shape () { registerX6Shape () {
Graph.unregisterNode(X6_NODE_NAME) Graph.unregisterNode(X6_NODE_NAME)
Graph.unregisterEdge(X6_EDGE_NAME) Graph.unregisterEdge(X6_EDGE_NAME)
Graph.registerNode(X6_NODE_NAME, { ...NODE_PROPS }) Graph.registerNode(X6_NODE_NAME, { ...NODE })
Graph.registerEdge(X6_EDGE_NAME, { ...EDGE_PROPS }) Graph.registerEdge(X6_EDGE_NAME, { ...EDGE })
}, },
/** /**
* Bind grap event * Bind grap event
*/ */
bindGraphEvent () { bindGraphEvent () {
// nodes and edges hover this.bindStyleEvent(this.graph)
this.graph.on('cell:mouseenter', (data) => { // update scale bar
const { cell, e } = data this.graph.on('scale', ({ sx }) => {
const isStatusIcon = (tagName) => this.scale = sx
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)
}
}) })
// right click // right click
this.graph.on('node:contextmenu', ({ x, y, cell }) => { this.graph.on('node:contextmenu', ({ x, y, cell }) => {
@ -279,6 +270,13 @@
label: labelName 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 * @param {Edge|string} edge
@ -297,9 +295,6 @@
setEdgeLabel (id, label) { setEdgeLabel (id, label) {
const edge = this.graph.getCellById(id) const edge = this.graph.getCellById(id)
edge.setLabels(label) edge.setLabels(label)
if (this.graph.isSelected(edge)) {
this.setEdgeHighlight(edge)
}
}, },
/** /**
* @param {number} limit * @param {number} limit
@ -348,94 +343,6 @@
node.setData({ taskName: name }) node.setData({ taskName: name })
} }
}, },
/**
* 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 * Convert the graph to JSON
* @return {{cells:Cell[]}} * @return {{cells:Cell[]}}
@ -512,13 +419,23 @@
} }
) )
}, },
showLayoutModal () {
const layoutModal = this.$refs.layoutModal
if (layoutModal) {
layoutModal.show()
}
},
/** /**
* format * format
* @desc Auto layout use @antv/layout * @desc Auto layout use @antv/layout
*/ */
format () { format (layoutConfig) {
const dagreLayout = new DagreLayout({ this.graph.cleanSelection()
type: 'dagre',
let layoutFunc = null
if (layoutConfig.type === LAYOUT_TYPE.DAGRE) {
layoutFunc = new DagreLayout({
type: LAYOUT_TYPE.DAGRE,
rankdir: 'LR', rankdir: 'LR',
align: 'UL', align: 'UL',
// Calculate the node spacing based on the edge label length // Calculate the node spacing based on the edge label length
@ -535,15 +452,34 @@
max = Math.max(max, labelWidth) max = Math.max(max, labelWidth)
}) })
} }
return 50 + max return layoutConfig.ranksep + max
}, },
nodesep: 50, nodesep: layoutConfig.nodesep,
controlPoints: true 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 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 edges = json.cells.filter((cell) => cell.shape === X6_EDGE_NAME)
const newModel = dagreLayout.layout({ const newModel = layoutFunc.layout({
nodes: nodes, nodes: nodes,
edges: edges edges: edges
}) })
@ -606,12 +542,10 @@
return { return {
shape: X6_EDGE_NAME, shape: X6_EDGE_NAME,
source: { source: {
cell: sourceId, cell: sourceId
port: X6_PORT_OUT_NAME
}, },
target: { target: {
cell: targetId, cell: targetId
port: X6_PORT_IN_NAME
}, },
labels: label ? [label] : undefined labels: label ? [label] : undefined
} }
@ -688,7 +622,7 @@
if (node) { if (node) {
// Destroy the previous dom // Destroy the previous dom
node.removeMarkup() 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 nodeView = this.graph.findViewByCell(node)
const el = nodeView.find('div')[0] const el = nodeView.find('div')[0]
nodeStatus({ nodeStatus({
@ -828,6 +762,28 @@
const edge = this.genEdgeJSON(code, postCode) const edge = this.genEdgeJSON(code, postCode)
this.graph.addEdge(edge) 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)
} }
} }
} }

113
dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/layoutConfigModal.vue

@ -0,0 +1,113 @@
/*
* 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 default {
data () {
return {
visible: false,
form: {
type: LAYOUT_TYPE.DAGRE,
rows: 0,
cols: 0,
padding: 50,
ranksep: 50,
nodesep: 50
},
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>

4
dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.scss

@ -174,6 +174,10 @@
} }
} }
} }
&.disabled{
cursor: default
}
} }
} }
} }

9
dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue

@ -24,6 +24,9 @@
<draggable-box <draggable-box
:key="taskType.name" :key="taskType.name"
@onDragstart="(e) => $emit('on-drag-start', e, taskType)" @onDragstart="(e) => $emit('on-drag-start', e, taskType)"
:class="{
disabled: isDetails
}"
> >
<div class="task-item"> <div class="task-item">
<em :class="`icos-${taskType.name.toLocaleLowerCase()}`"></em> <em :class="`icos-${taskType.name.toLocaleLowerCase()}`"></em>
@ -38,6 +41,7 @@
<script> <script>
import draggableBox from './draggableBox.vue' import draggableBox from './draggableBox.vue'
import { tasksType } from '../config.js' import { tasksType } from '../config.js'
import { mapState } from 'vuex'
export default { export default {
name: 'dag-taskbar', name: 'dag-taskbar',
@ -55,6 +59,11 @@
return { return {
tasksTypeList tasksTypeList
} }
},
computed: {
...mapState('dag', [
'isDetails'
])
} }
} }
</script> </script>

14
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;
}
}
} }

101
dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue

@ -23,76 +23,111 @@
:content="$t('Copy name')" :content="$t('Copy name')"
placement="bottom" placement="bottom"
> >
<i class="el-icon-copy-document" @click="copyName"></i> <em class="el-icon-copy-document" @click="copyName"></em>
</el-tooltip> </el-tooltip>
<textarea ref="textarea" cols="30" rows="10" class="transparent"></textarea> <textarea ref="textarea" cols="30" rows="10" class="transparent"></textarea>
<div class="toolbar-left"> <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 <el-tooltip
:content="$t('View variables')" :content="$t('View variables')"
placement="bottom" placement="bottom"
class="toolbar-operation" class="toolbar-operation"
> >
<i <em
class="custom-ico view-variables" class="custom-ico view-variables"
v-if="$route.name === 'projects-instance-details'" v-if="$route.name === 'projects-instance-details'"
@click="toggleVariableView" @click="toggleVariableView"
></i> ></em>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
:content="$t('Startup parameter')" :content="$t('Startup parameter')"
placement="bottom" placement="bottom"
class="toolbar-operation" class="toolbar-operation"
> >
<i <em
class="custom-ico startup-parameters" class="custom-ico startup-parameters"
v-if="$route.name === 'projects-instance-details'" v-if="$route.name === 'projects-instance-details'"
@click="toggleParamView" @click="toggleParamView"
></i> ></em>
</el-tooltip> </el-tooltip>
</div> </div>
<div class="toolbar-right"> <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 <el-tooltip
class="toolbar-operation" class="toolbar-operation"
:content="$t('Delete selected lines or nodes')" :content="$t('Delete selected lines or nodes')"
placement="bottom" placement="bottom"
v-if="!isDetails" v-if="!isDetails"
> >
<i class="el-icon-delete" @click="removeCells"></i> <em class="el-icon-delete" @click="removeCells"></em>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
class="toolbar-operation" class="toolbar-operation"
:content="$t('Download')" :content="$t('Download')"
placement="bottom" placement="bottom"
> >
<i class="el-icon-download" @click="downloadPNG"></i> <em class="el-icon-download" @click="downloadPNG"></em>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
class="toolbar-operation" class="toolbar-operation"
:content="$t('Full Screen')" :content="$t('Refresh DAG status')"
placement="bottom" placement="bottom"
v-if="dagChart.type === 'instance'"
> >
<i <em class="el-icon-refresh" @click="refreshTaskStatus"></em>
:class="[
'custom-ico',
dagChart.fullScreen ? 'full-screen-close' : 'full-screen-open',
]"
@click="toggleFullScreen"
></i>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
class="toolbar-operation" class="toolbar-operation"
:content="$t('Refresh DAG status')" :content="$t('Format DAG')"
placement="bottom" 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>
<el-tooltip <el-tooltip
class="toolbar-operation last" class="toolbar-operation last"
:content="$t('Format DAG')" :content="$t('Full Screen')"
placement="bottom" 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-tooltip>
<el-button <el-button
class="toolbar-el-btn" class="toolbar-el-btn"
@ -101,7 +136,7 @@
v-if="dagChart.type === 'definition'" v-if="dagChart.type === 'definition'"
@click="showVersions" @click="showVersions"
icon="el-icon-info" icon="el-icon-info"
>{{$t('Version Info')}}</el-button >{{ $t("Version Info") }}</el-button
> >
<el-button <el-button
class="toolbar-el-btn" class="toolbar-el-btn"
@ -125,7 +160,6 @@
type="primary" type="primary"
icon="el-icon-switch-button" icon="el-icon-switch-button"
size="mini" size="mini"
v-if="type === 'instance' || 'definition'"
@click="returnToListPage" @click="returnToListPage"
> >
{{ $t("Close") }} {{ $t("Close") }}
@ -143,15 +177,28 @@
inject: ['dagChart'], inject: ['dagChart'],
data () { data () {
return { return {
canvasRef: null canvasRef: null,
searchText: '',
searchInputVisible: false
} }
}, },
computed: { computed: {
...mapState('dag', [ ...mapState('dag', ['isDetails', 'releaseState'])
'isDetails'
])
}, },
methods: { 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 () { getDagCanvasRef () {
if (this.canvasRef) { if (this.canvasRef) {
return this.canvasRef return this.canvasRef
@ -200,7 +247,7 @@
}, },
chartFormat () { chartFormat () {
const canvas = this.getDagCanvasRef() const canvas = this.getDagCanvasRef()
canvas.format() canvas.showLayoutModal()
}, },
refreshTaskStatus () { refreshTaskStatus () {
this.dagChart.refreshTaskStatus() this.dagChart.refreshTaskStatus()

149
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_NODE_NAME = 'dag-task'
export const X6_EDGE_NAME = 'dag-edge' export const X6_EDGE_NAME = 'dag-edge'
export const X6_PORT_OUT_NAME = 'dag-port-out' export const X6_PORT_OUT_NAME = 'dag-port-out'
export const X6_PORT_IN_NAME = 'dag-port-in'
const EDGE = '#999999' const EDGE_COLOR = '#999999'
const BG_BLUE = 'rgba(40, 143, 255, 0.1)' const BG_BLUE = '#DFE9F7'
const BG_WHITE = '#FFFFFF' const BG_WHITE = '#FFFFFF'
const NODE_BORDER = '#e4e4e4' const NODE_BORDER = '#CCCCCC'
const TITLE = '#333' const TITLE = '#333333'
const STROKE_BLUE = '#288FFF' 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: { groups: {
[X6_PORT_OUT_NAME]: { [X6_PORT_OUT_NAME]: {
position: { position: {
@ -62,14 +63,14 @@ export const PORT_PROPS = {
}, },
'plus-text': { 'plus-text': {
fontSize: 12, fontSize: 12,
fill: EDGE, fill: NODE_BORDER,
text: '+', text: '+',
textAnchor: 'middle', textAnchor: 'middle',
x: 0, x: 0,
y: 3 y: 3
}, },
'circle-outer': { 'circle-outer': {
stroke: EDGE, stroke: NODE_BORDER,
strokeWidth: 1, strokeWidth: 1,
r: 6, r: 6,
fill: BG_WHITE fill: BG_WHITE
@ -79,57 +80,42 @@ export const PORT_PROPS = {
fill: 'transparent' 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: { attrs: {
body: { 'circle-outer': {
magnet: true stroke: STROKE_BLUE,
fill: BG_BLUE,
r: 8
}, },
circle: { 'circle-inner': {
r: 4, fill: STROKE_BLUE,
strokeWidth: 0, r: 6
fill: 'transparent'
} }
} }
} }
} }
} }
export const PORT_HIGHLIGHT_PROPS = { export const PORT_SELECTED = {
groups: {
[X6_PORT_OUT_NAME]: { [X6_PORT_OUT_NAME]: {
attrs: { attrs: {
'circle-outer': {
stroke: STROKE_BLUE,
fill: BG_BLUE
},
'plus-text': { 'plus-text': {
fill: STROKE_BLUE fill: STROKE_BLUE
}, },
'circle-inner': { 'circle-outer': {
fill: STROKE_BLUE stroke: STROKE_BLUE,
fill: BG_WHITE
}
}
} }
} }
},
[X6_PORT_IN_NAME]: {}
} }
export const NODE_STATUS_MARKUP = [{ export const NODE_STATUS_MARKUP = [{
@ -148,13 +134,14 @@ export const NODE_STATUS_MARKUP = [{
] ]
}] }]
export const NODE_PROPS = { export const NODE = {
width: 220, width: 220,
height: 48, height: 48,
markup: [ markup: [
{ {
tagName: 'rect', tagName: 'rect',
selector: 'body' selector: 'body',
className: 'dag-task-body'
}, },
{ {
tagName: 'image', tagName: 'image',
@ -174,7 +161,9 @@ export const NODE_PROPS = {
pointerEvents: 'visiblePainted', pointerEvents: 'visiblePainted',
fill: BG_WHITE, fill: BG_WHITE,
stroke: NODE_BORDER, stroke: NODE_BORDER,
strokeWidth: 1 strokeWidth: 1,
strokeDasharray: 'none',
filter: 'none'
}, },
image: { image: {
width: 30, width: 30,
@ -199,21 +188,17 @@ export const NODE_PROPS = {
} }
}, },
ports: { ports: {
...PORT_PROPS, ...PORT,
items: [ items: [
{ {
id: X6_PORT_OUT_NAME, id: X6_PORT_OUT_NAME,
group: 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: { attrs: {
body: { body: {
fill: BG_BLUE, 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: { attrs: {
line: { line: {
stroke: EDGE, stroke: EDGE_COLOR,
strokeWidth: 0.8, strokeWidth: 1,
targetMarker: { targetMarker: {
tagName: 'path', tagName: 'path',
fill: EDGE, fill: EDGE_COLOR,
strokeWidth: 0, strokeWidth: 0,
d: 'M 6 -3 0 0 6 3 Z' d: 'M 6 -3 0 0 6 3 Z'
} },
filter: 'none'
} }
}, },
connector: { connector: {
name: 'rounded' name: 'rounded'
}, },
router: { router: {
name: 'er', name: 'manhattan',
args: { args: {
offset: 20, endDirections: ['top', 'bottom', 'left']
min: 20,
direction: 'L'
} }
}, },
defaultLabel: { defaultLabel: {
@ -263,7 +262,7 @@ export const EDGE_PROPS = {
], ],
attrs: { attrs: {
label: { label: {
fill: EDGE, fill: EDGE_COLOR,
fontSize: 14, fontSize: 14,
textAnchor: 'middle', textAnchor: 'middle',
textVerticalAnchor: 'middle', textVerticalAnchor: 'middle',
@ -272,7 +271,7 @@ export const EDGE_PROPS = {
body: { body: {
ref: 'label', ref: 'label',
fill: BG_WHITE, fill: BG_WHITE,
stroke: EDGE, stroke: EDGE_COLOR,
strokeWidth: 1, strokeWidth: 1,
rx: 4, rx: 4,
ry: 4, ry: 4,
@ -292,7 +291,7 @@ export const EDGE_PROPS = {
} }
} }
export const EDGE_HIGHLIGHT_PROPS = { export const EDGE_HOVER = {
attrs: { attrs: {
line: { line: {
stroke: STROKE_BLUE, 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
}
}
}
}

145
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 } }
}
)
}
])
}
}
}

17
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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
$STROKE_BLUE: #288FFF; $STROKE_BLUE: #288fff;
$BG_WHITE: #FFFFFF; $BG_WHITE: #ffffff;
.x6-node[data-shape="dag-task"] { .x6-node[data-shape="dag-task"] {
.in-port-body{ &.available {
&.adsorbed,&.available{ .dag-task-body {
.circle {
stroke: $STROKE_BLUE; stroke: $STROKE_BLUE;
stroke-width: 4; stroke-width: 1;
fill: $BG_WHITE; stroke-dasharray: 5, 2;
}
&.adsorbed {
.dag-task-body {
stroke-width: 3;
} }
} }
} }

5
dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue

@ -60,6 +60,10 @@
</el-dialog> </el-dialog>
<edge-edit-model ref="edgeEditModel" /> <edge-edit-model ref="edgeEditModel" />
<el-drawer :visible.sync="versionDrawer" size="" :with-header="false"> <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 <m-versions
:versionData="versionData" :versionData="versionData"
:isInstance="type === 'instance'" :isInstance="type === 'instance'"
@ -187,7 +191,6 @@
}, },
beforeDestroy () { beforeDestroy () {
this.resetParams() this.resetParams()
clearInterval(this.statusTimer) clearInterval(this.statusTimer)
window.removeEventListener('resize', this.resizeDebounceFunc) window.removeEventListener('resize', this.resizeDebounceFunc)
}, },

2
dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue

@ -420,7 +420,7 @@
<!-- Pre-tasks in workflow --> <!-- Pre-tasks in workflow -->
<m-pre-tasks <m-pre-tasks
ref="preTasks" ref="preTasks"
v-if="['SHELL', 'SUB_PROCESS'].indexOf(nodeData.taskType) > -1 && !fromTaskDefinition" v-if="!fromTaskDefinition"
:code="code" :code="code"
/> />
</div> </div>

4
dolphinscheduler-ui/src/js/conf/home/store/dag/actions.js

@ -133,6 +133,8 @@ export default {
state.version = res.data.processDefinition.version state.version = res.data.processDefinition.version
// name // name
state.name = res.data.processDefinition.name state.name = res.data.processDefinition.name
// releaseState
state.releaseState = res.data.processDefinition.releaseState
// description // description
state.description = res.data.processDefinition.description state.description = res.data.processDefinition.description
// taskRelationJson // taskRelationJson
@ -145,7 +147,6 @@ export default {
state.timeout = res.data.processDefinition.timeout state.timeout = res.data.processDefinition.timeout
// executionType // executionType
state.executionType = res.data.processDefinition.executionType state.executionType = res.data.processDefinition.executionType
// tenantId
// tenantCode // tenantCode
state.tenantCode = res.data.processDefinition.tenantCode || 'default' state.tenantCode = res.data.processDefinition.tenantCode || 'default'
// tasks info // tasks info
@ -167,6 +168,7 @@ export default {
'timeout', 'timeout',
'environmentCode' 'environmentCode'
])) ]))
resolve(res.data) resolve(res.data)
}).catch(res => { }).catch(res => {
reject(res) reject(res)

10
dolphinscheduler-ui/src/js/module/i18n/locale/en_US.js

@ -748,5 +748,13 @@ export default {
users: 'Users', users: 'Users',
Username: 'Username', Username: 'Username',
showType: 'Show Type', showType: 'Show Type',
'Please select a task type (required)': 'Please select a task type (required)' 'Please select a task type (required)': 'Please select a task type (required)',
layoutType: 'Layout Type',
gridLayout: 'Grid',
dagreLayout: 'Dagre',
rows: 'Rows',
cols: 'Cols',
processOnline: 'Online',
searchNode: 'Search Node',
dagScale: 'Scale'
} }

10
dolphinscheduler-ui/src/js/module/i18n/locale/zh_CN.js

@ -749,5 +749,13 @@ export default {
users: '群员', users: '群员',
Username: '用户名', Username: '用户名',
showType: '内容展示类型', showType: '内容展示类型',
'Please select a task type (required)': '请选择任务类型(必选)' 'Please select a task type (required)': '请选择任务类型(必选)',
layoutType: '布局类型',
gridLayout: '网格布局',
dagreLayout: '层次布局',
rows: '行数',
cols: '列数',
processOnline: '已上线',
searchNode: '搜索节点',
dagScale: '缩放'
} }

Loading…
Cancel
Save