Browse Source

[Improvement-14318][UI] migrate version 2.x workflow definition d3 tree view to version 3.x (#14382)

* [Improvement][UI] migrate version 2.x workflow definition d3 tree view to version 3.x

* remove unnessnary code

* fix code smells

* update lock file to fix front-end CI Build error

* update package.json
3.2.1-prepare
yeahhhz 1 year ago committed by GitHub
parent
commit
0880549440
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      dolphinscheduler-ui/package.json
  2. 3078
      dolphinscheduler-ui/pnpm-lock.yaml
  3. 1
      dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.module.scss
  4. 153
      dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx
  5. 52
      dolphinscheduler-ui/src/views/projects/workflow/definition/tree/use-d3-tree/index.scss
  6. 34
      dolphinscheduler-ui/src/views/projects/workflow/definition/tree/use-d3-tree/index.tsx
  7. 366
      dolphinscheduler-ui/src/views/projects/workflow/definition/tree/use-d3-tree/tree.ts
  8. 73
      dolphinscheduler-ui/src/views/projects/workflow/definition/tree/use-d3-tree/util.ts

1
dolphinscheduler-ui/package.json

@ -13,6 +13,7 @@
"@antv/x6": "^1.34.1", "@antv/x6": "^1.34.1",
"@vueuse/core": "^9.2.0", "@vueuse/core": "^9.2.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"d3": "7.8.5",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7", "date-fns-tz": "^1.3.7",
"echarts": "^5.3.3", "echarts": "^5.3.3",

3078
dolphinscheduler-ui/pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

1
dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.module.scss

@ -17,6 +17,7 @@
.content { .content {
width: 100%; width: 100%;
position: relative;
.card { .card {
margin-bottom: 8px; margin-bottom: 8px;

153
dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx

@ -17,18 +17,37 @@
import Card from '@/components/card' import Card from '@/components/card'
import { ArrowLeftOutlined } from '@vicons/antd' import { ArrowLeftOutlined } from '@vicons/antd'
import { NButton, NFormItem, NIcon, NSelect, NSpace, NImage } from 'naive-ui' import {
import { defineComponent, onMounted, Ref, ref, watch } from 'vue' NButton,
NFormItem,
NIcon,
NSelect,
NSpace,
NImage,
NTooltip
} from 'naive-ui'
import {
defineComponent,
onMounted,
Ref,
ref,
watch,
h,
toRefs,
reactive,
getCurrentInstance
} from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import styles from './index.module.scss' import styles from './index.module.scss'
import UseTree from '@/views/projects/workflow/definition/tree/use-tree' import UseD3Tree from '@/views/projects/workflow/definition/tree/use-d3-tree'
import Tree from '@/views/projects/workflow/definition/tree/use-d3-tree/tree'
import { IChartDataItem } from '@/components/chart/modules/types' import { IChartDataItem } from '@/components/chart/modules/types'
import { Router, useRouter } from 'vue-router' import { Router, useRouter } from 'vue-router'
import { viewTree } from '@/service/modules/process-definition' import { viewTree } from '@/service/modules/process-definition'
import { SelectMixedOption } from 'naive-ui/lib/select/src/interface' import { SelectMixedOption } from 'naive-ui/lib/select/src/interface'
import { find } from 'lodash' import { tasksState, uuid } from '@/common/common'
import { tasksState } from '@/common/common'
import type { ITaskTypeNodeOption } from './types' import type { ITaskTypeNodeOption } from './types'
import { cloneDeep, map } from 'lodash'
export default defineComponent({ export default defineComponent({
name: 'WorkflowDefinitionTree', name: 'WorkflowDefinitionTree',
@ -146,6 +165,18 @@ export default defineComponent({
} }
]) ])
const showTooltip = ref(false)
const tooltipText = ref('')
const tooltipProps = reactive({
x: 0,
y: 0
})
const changeTooltip = (options: any) => {
tooltipProps.x = options.x
tooltipProps.y = options.y - 20
}
const initTaskStateMap = () => { const initTaskStateMap = () => {
taskStateMap.value = Object.entries(tasksState(t)).map(([key, item]) => ({ taskStateMap.value = Object.entries(tasksState(t)).map(([key, item]) => ({
state: key, state: key,
@ -154,72 +185,39 @@ export default defineComponent({
})) }))
} }
const initChartData = (node: any, newNode: any) => { const currentInstance = getCurrentInstance()
newNode.children = []
node?.children.map((child: any) => {
const newChild = {}
initChartData(child, newChild)
newNode.children.push(newChild)
})
newNode.name = node.name const getWorkflowTreeData = async (limit: number) => {
newNode.value = node.name === 'DAG' ? 'DAG' : node?.type if (projectCode.value && definitionCode) {
const taskTypeNodeOption = find(taskTypeNodeOptions.value, { Tree.reset()
taskType: newNode.value
}) const res = await viewTree(projectCode.value, definitionCode.value, {
if (taskTypeNodeOption) { limit: limit
newNode.itemStyle = { color: taskTypeNodeOption.color }
if (newNode.name !== 'DAG') {
let taskState = null
if (
node.instances &&
node.instances.length > 0 &&
node.instances[0].state
) {
taskState = find(taskStateMap.value, {
state: node.instances[0].state
}) })
const treeData = cloneDeep(res)
if (!treeData?.children) return
const recursiveChildren = (children: any) => {
if (children.length) {
map(children, (v) => {
v.uuid = `${uuid('uuid_')}${uuid('') + uuid('')}`
if (v.children.length) {
recursiveChildren(v.children)
} }
newNode.label = { })
show: true,
formatter: [
`{name|${t('project.task.task_name')}:${newNode.name}}`,
`{type|${t('project.task.task_type')}:${
taskTypeNodeOption.taskType
}}`,
taskState
? `{state|${t('project.workflow.task_state')}: ${
taskState.value
}}`
: ''
].join('\n'),
rich: {
type: {
lineHeight: 20,
align: 'left'
},
name: {
lineHeight: 20,
align: 'left'
},
state: {
lineHeight: 20,
align: 'left',
color: taskState ? taskState.color : 'black'
}
}
}
}
} }
} }
const getWorkflowTreeData = async (limit: number) => { recursiveChildren(treeData.children)
if (projectCode.value && definitionCode) {
const res = await viewTree(projectCode.value, definitionCode.value, { Tree.init({
limit: limit data: cloneDeep(treeData),
limit: limit,
selfTree: currentInstance,
taskTypeNodeOptions: taskTypeNodeOptions.value,
tasksStateObj: tasksState(t)
}) })
chartData.value = [{ name: 'DAG', value: 'DAG' }]
initChartData(res, chartData.value[0])
} }
} }
@ -249,11 +247,15 @@ export default defineComponent({
chartData, chartData,
options, options,
onSelectChange, onSelectChange,
taskTypeNodeOptions taskTypeNodeOptions,
showTooltip,
tooltipText,
changeTooltip,
...toRefs(tooltipProps)
} }
}, },
render() { render() {
const { chartData, options, onSelectChange, taskTypeNodeOptions } = this const { options, onSelectChange, taskTypeNodeOptions } = this
const { t } = useI18n() const { t } = useI18n()
const router: Router = useRouter() const router: Router = useRouter()
@ -293,7 +295,24 @@ export default defineComponent({
</NButton> </NButton>
))} ))}
</NSpace> </NSpace>
<UseTree chartData={chartData} /> </Card>
{h(
NTooltip,
{
show: this.showTooltip,
placement: 'top',
x: this.x,
y: this.y,
duration: 10,
'show-arrow': false
},
{
default: () => <div innerHTML={this.tooltipText}></div>,
trigger: () => ''
}
)}
<Card>
<UseD3Tree />
</Card> </Card>
</div> </div>
) )

52
dolphinscheduler-ui/src/views/projects/workflow/definition/tree/use-d3-tree/index.scss

@ -0,0 +1,52 @@
/*
* 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.
*/
.tree-model {
width: calc(100%);
height: calc(100vh - 300px);
overflow-x: scroll;
.d3-tree {
padding-left: 30px;
.node {
text {
font: 11px sans-serif;
pointer-events: none;
}
}
rect {
cursor: pointer;
&.state {
stroke: #666;
shape-rendering: crispEdges;
}
}
path {
&.link{
fill: none;
stroke: #666;
stroke-width: 2px;
}
}
circle {
stroke: #666;
fill: #0097e0;
stroke-width: 1.5px;
cursor: pointer;
}
}
}

34
dolphinscheduler-ui/src/views/projects/workflow/definition/tree/use-d3-tree/index.tsx

@ -0,0 +1,34 @@
/*
* 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 { defineComponent } from 'vue'
import './index.scss'
const UseTree = defineComponent({
name: 'D3Tree',
render() {
return (
<div class='tree-model'>
<div class='d3-tree'>
<svg class='tree-svg' width='100%'></svg>
</div>
</div>
)
}
})
export default UseTree

366
dolphinscheduler-ui/src/views/projects/workflow/definition/tree/use-d3-tree/tree.ts

@ -0,0 +1,366 @@
/*
* 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.
*/
// @ts-nocheck
import * as d3 from 'd3'
import { rtInstancesTooltip, rtCountMethod } from './util'
// eslint-disable-next-line @typescript-eslint/no-this-alias
let self = this
const Tree = function () {
// eslint-disable-next-line @typescript-eslint/no-this-alias
self = this
this.selfTree = {}
this.tree = function () {}
// basic configuration
this.config = {
barHeight: 26,
axisHeight: 40,
squareSize: 10,
squarePading: 4,
taskNum: 25,
nodesMax: 0
}
// Margin configuration
this.config.margin = {
top: this.config.barHeight / 2 + this.config.axisHeight,
right: 0,
bottom: 0,
left: this.config.barHeight / 2
}
// width
this.config.margin.width =
960 - this.config.margin.left - this.config.margin.right
// bar width
this.config.barWidth = parseInt(this.config.margin.width * 0.9)
}
/**
* init
*/
Tree.prototype.init = function ({
data,
limit,
selfTree,
taskTypeNodeOptions,
tasksStateObj
}) {
return new Promise((resolve) => {
this.selfTree = selfTree
this.config.taskNum = limit
this.duration = 400
this.i = 0
this.tree = d3.tree().size([0, 46])
this.taskTypeNodeOptions = taskTypeNodeOptions
this.tasksStateObj = tasksStateObj
const root = d3.hierarchy(data)
const treeData = this.tree(root)
const tasks = treeData.descendants()
const links = treeData.links()
this.tasks = tasks
this.links = links
this.diagonal = d3
.linkHorizontal()
.x((d) => d.y)
.y((d) => d.x)
this.svg = d3
.select('.tree-svg')
.append('g')
.attr('class', 'level')
.attr(
'transform',
'translate(' +
this.config.margin.left +
',' +
this.config.margin.top +
')'
)
data.x0 = 0
data.y0 = 0
this.squareNum = tasks[tasks.length === 1 ? 0 : 1]?.data?.instances.length
// Calculate the maximum node length
this.config.nodesMax = rtCountMethod(data.children)
this.treeUpdate((this.root = data)).then(() => {
this.treeTooltip()
resolve()
})
})
}
/**
* tasks
*/
Tree.prototype.nodesClass = function (d) {
let sclass = 'node'
if (d.children === undefined && d._children === undefined) {
sclass += ' leaf'
} else {
sclass += ' parent'
if (d.children === undefined) {
sclass += ' collapsed'
} else {
sclass += ' expanded'
}
}
return sclass
}
/**
* tree Expand hidden
*/
Tree.prototype.treeToggles = function (e,clicked_d) { // eslint-disable-line
self.removeTooltip()
// eslint-disable-next-line quotes
d3.selectAll("[task_id='" + clicked_d.data.uuid + "']").each((d) => {
if (clicked_d !== d && d.children) {
// eslint-disable-line
d._children = d.children
d.children = null
self.treeUpdate(d)
}
})
if (clicked_d._children) {
clicked_d.children = clicked_d._children
clicked_d._children = null
} else {
clicked_d._children = clicked_d.children
clicked_d.children = null
}
self.treeUpdate(clicked_d)
}
/**
* update tree
*/
Tree.prototype.treeUpdate = function (source) {
const tasksStateObj = this.tasksStateObj
const tasksType = {}
this.taskTypeNodeOptions.map((v) => {
tasksType[v.taskType] = {
color: v.color
}
})
return new Promise((resolve) => {
const tasks = this.tasks
const height = Math.max(
500,
tasks.length * this.config.barHeight +
this.config.margin.top +
this.config.margin.bottom
)
d3.select('.tree-svg')
.transition()
.duration(this.duration)
.attr('height', height)
tasks.forEach((n, i) => {
n.x = i * this.config.barHeight
})
const task = this.svg.selectAll('g.node').data(tasks, (d) => {
return d.id || (d.id = ++this.i)
})
const nodeEnter = task
.enter()
.append('g')
.attr('class', this.nodesClass)
.attr('transform', () => 'translate(' + source.y0 + ',' + source.x0 + ')')
.style('opacity', 1e-6)
// Node circle
nodeEnter
.append('circle')
.attr('r', this.config.barHeight / 3)
.attr('class', 'task')
.attr('title', (d) => {
return d.data.type ? d.data.type : ''
})
.attr('height', this.config.barHeight)
.attr('width', (d) => this.config.barWidth - d.y)
.style('fill', (d) =>
d.data.type ? tasksType[d.data.type]?.color : '#fff'
)
.attr('task_id', (d) => {
return d.data.uuid
})
.on('click', this.treeToggles)
.on('mouseover', (e, d) => {
self.treeTooltip(d.data.type, e)
})
.on('mouseout', () => {
self.removeTooltip()
})
// Node text
nodeEnter
.append('text')
.attr('dy', 3.5)
.attr('dx', this.config.barHeight / 2)
.text((d) => {
return d.data.name
})
.style('fill', 'var(--n-title-text-color)')
const translateRatio =
this.config.nodesMax > 10 ? (this.config.nodesMax > 20 ? 10 : 30) : 60
// Right node information
nodeEnter
.append('g')
.attr('class', 'stateboxes')
.attr(
'transform',
(d) =>
'translate(' + (this.config.nodesMax * translateRatio - d.y) + ',0)'
)
.selectAll('rect')
.data((d) => d.data.instances)
.enter()
.append('rect')
.on('click', () => {
this.removeTooltip()
})
.attr('class', 'state')
.style(
'fill',
(d) => (d.state && tasksStateObj[d.state].color) || '#ffffff'
)
.attr('rx', (d) => (d.type ? 0 : 12))
.attr('ry', (d) => (d.type ? 0 : 12))
.style('shape-rendering', (d) => (d.type ? 'crispEdges' : 'auto'))
.attr(
'x',
(d, i) => i * (this.config.squareSize + this.config.squarePading)
)
.attr('y', -(this.config.squareSize / 2))
.attr('width', 10)
.attr('height', 10)
.on('mouseover', (e, d) => {
self.treeTooltip(rtInstancesTooltip(d, tasksStateObj), e)
})
.on('mouseout', () => {
self.removeTooltip()
})
// Convert nodes to their new location。
nodeEnter
.transition()
.duration(this.duration)
.attr('transform', (d) => 'translate(' + d.y + ',' + d.x + ')')
.style('opacity', 1)
// Node line
task
.transition()
.duration(this.duration)
.attr('class', this.nodesClass)
.attr('transform', (d) => 'translate(' + d.y + ',' + d.x + ')')
.style('opacity', 1)
// Convert the exit node to the new location of the parent node。
task
.exit()
.transition()
.duration(this.duration)
.attr('transform', () => 'translate(' + source.y + ',' + source.x + ')')
.style('opacity', 1e-6)
.remove()
// Update link
const link = this.svg
.selectAll('path.link')
.data(this.links, (d) => d.target.id)
// Enter any new links in the previous location of the parent node。
link
.enter()
.insert('path', 'g')
.attr('class', 'link')
.attr('d', () => {
const o = { x: source.x0, y: source.y0 }
return this.diagonal({ source: o, target: o })
})
.transition()
.duration(this.duration)
.attr('d', this.diagonal)
// Transition link
link.transition().duration(this.duration).attr('d', this.diagonal)
// Convert the exit node to the new location of the parent node
link
.exit()
.transition()
.duration(this.duration)
.attr('d', () => {
const o = { x: source.x, y: source.y }
return this.diagonal({ source: o, target: o })
})
.remove()
// Hide the old position for a transition.
tasks.forEach((d) => {
d.x0 = d.x
d.y0 = d.y
})
resolve()
})
}
/**
* reset
*/
Tree.prototype.reset = function () {
// $('.d3-tree .tree').html('')
d3.select('.d3-tree .tree-svg').html('')
}
/**
* toottip handle
*/
Tree.prototype.treeTooltip = function (str, e) {
if (!str) return
this.selfTree.proxy.showTooltip = true
this.selfTree.proxy.tooltipText = str
this.selfTree.proxy.changeTooltip(e)
}
/**
* Manually clear tooltip
*/
Tree.prototype.removeTooltip = function () {
this.selfTree.proxy.showTooltip = false
}
export default new Tree()

73
dolphinscheduler-ui/src/views/projects/workflow/definition/tree/use-d3-tree/util.ts

@ -0,0 +1,73 @@
/*
* 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.
*/
// @ts-nocheck
import { renderTableTime } from '@/common/common'
/**
* Node prompt dom
*/
const rtInstancesTooltip = (data, tasksStateObj) => {
let str = '<div style="text-align: left;word-break:break-all">'
str += `id : ${data.id}</br>`
str += `host : ${data.host}</br>`
str += `name : ${data.name}</br>`
str += `state : ${data.state ? tasksStateObj[data.state].desc : '-'}${
data.state
}</br>`
if (data.type) {
str += `type : ${data.type}</br>`
}
str += `startTime : ${
data.startTime ? renderTableTime(data.startTime) : '-'
}</br>`
str += `endTime : ${data.endTime ? renderTableTime(data.endTime) : '-'}</br>`
str += `duration : ${data.duration}</br>`
str += '</div>'
return str
}
/**
* Calculate the maximum node length
* Easy to calculate the width dynamically
*/
const rtCountMethod = (list: any) => {
const arr: any = []
function count(list: any, t: string) {
let toggle = false
list.forEach((v) => {
if (v.children && v.children.length > 0) {
if (!toggle) {
toggle = true
t += '*'
arr.push(t)
}
count(v.children, t)
}
})
}
count(list, '*')
let num = 6
arr.forEach((v) => {
if (v.length > num) {
num = v.length
}
})
return num
}
export { rtInstancesTooltip, rtCountMethod }
Loading…
Cancel
Save