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. 322
      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. 163
      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. 21
      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 {
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;

322
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 } 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
@ -348,94 +343,6 @@
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
* @return {{cells:Cell[]}}
@ -512,38 +419,67 @@
}
)
},
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) {
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 +542,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 +622,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 +762,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)
}
}
}

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
: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>

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')"
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()

163
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
}
}
}
}

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

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

5
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)
},

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

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

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

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

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

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

Loading…
Cancel
Save