Browse Source

[Feature][UI Next] DAG backfill (#8325)

* [Feature][UI Next] Dag backfill

* [Feature][UI Next] Add license header
3.0.0/version-upgrade
wangyizhi 2 years ago committed by GitHub
parent
commit
480492db73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      dolphinscheduler-ui-next/src/locales/modules/en_US.ts
  2. 3
      dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
  3. 6
      dolphinscheduler-ui-next/src/service/modules/process-definition/index.ts
  4. 4
      dolphinscheduler-ui-next/src/views/projects/task/constants/task-type.ts
  5. 42
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-canvas.tsx
  6. 20
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts
  7. 36
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-sidebar.tsx
  8. 111
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-toolbar.tsx
  9. 4
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag.module.scss
  10. 83
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx
  11. 95
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/types.ts
  12. 2
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-canvas-init.ts
  13. 54
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-cell-query.ts
  14. 76
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-cell-update.ts
  15. 126
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-custom-cell-builder.ts
  16. 46
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-dag-drag-drop.ts
  17. 43
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-graph-backfill.ts
  18. 47
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-search.ts
  19. 36
      dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-text-copy.ts
  20. 6
      dolphinscheduler-ui-next/src/views/projects/workflow/definition/components/version-modal.tsx
  21. 7
      dolphinscheduler-ui-next/src/views/projects/workflow/definition/create/index.tsx
  22. 41
      dolphinscheduler-ui-next/src/views/projects/workflow/definition/detail/index.module.scss
  23. 37
      dolphinscheduler-ui-next/src/views/projects/workflow/definition/detail/index.tsx
  24. 15
      dolphinscheduler-ui-next/src/views/projects/workflow/definition/index.tsx
  25. 25
      dolphinscheduler-ui-next/src/views/projects/workflow/definition/use-table.tsx

3
dolphinscheduler-ui-next/src/locales/modules/en_US.ts

@ -495,7 +495,8 @@ const project = {
grid_layout: 'Grid',
dagre_layout: 'Dagre',
rows: 'Rows',
cols: 'Cols'
cols: 'Cols',
copy_success: 'Copy Success'
}
}

3
dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts

@ -494,7 +494,8 @@ const project = {
grid_layout: '网格布局',
dagre_layout: '层次布局',
rows: '行数',
cols: '列数'
cols: '列数',
copy_success: '复制成功'
}
}

6
dolphinscheduler-ui-next/src/service/modules/process-definition/index.ts

@ -148,11 +148,11 @@ export function verifyName(params: NameReq, code: CodeReq): any {
}
export function queryProcessDefinitionByCode(
code: CodeReq,
processCode: CodeReq
code: number,
projectCode: number
): any {
return axios({
url: `/projects/${code}/process-definition/${processCode}`,
url: `/projects/${projectCode}/process-definition/${code}`,
method: 'get'
})
}

4
dolphinscheduler-ui-next/src/views/projects/task/constants/task-type.ts

@ -15,7 +15,7 @@
* limitations under the License.
*/
export const ALL_TASK_TYPES: any = {
export const TASK_TYPES_MAP = {
SHELL: {
alias: 'SHELL'
},
@ -65,3 +65,5 @@ export const ALL_TASK_TYPES: any = {
alias: 'WATERDROP'
}
}
export type TaskType = keyof typeof TASK_TYPES_MAP

42
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-canvas.tsx

@ -17,53 +17,33 @@
import { defineComponent, ref, inject } from 'vue'
import Styles from './dag.module.scss'
import type { PropType, Ref } from 'vue'
import type { Dragged } from './index'
import { useCanvasInit, useCellActive, useCanvasDrop } from './dag-hooks'
import { useRoute } from 'vue-router'
const props = {
dragged: {
type: Object as PropType<Ref<Dragged>>,
default: ref({
x: 0,
y: 0,
type: ''
})
}
}
import { useCanvasInit, useCellActive } from './dag-hooks'
export default defineComponent({
name: 'workflow-dag-canvas',
props,
emits: ['drop'],
setup(props, context) {
const readonly = inject('readonly', ref(false))
const graph = inject('graph', ref())
const route = useRoute()
const projectCode = route.params.projectCode as string
const { paper, minimap, container } = useCanvasInit({ readonly, graph })
// Change the style on cell hover and select
useCellActive({ graph })
// Drop sidebar item in canvas
const { onDrop, onDragenter, onDragover, onDragleave } = useCanvasDrop({
readonly,
dragged: props.dragged,
graph,
container,
projectCode
})
const preventDefault = (e: DragEvent) => {
e.preventDefault()
}
return () => (
<div
ref={container}
class={Styles.canvas}
onDrop={onDrop}
onDragenter={onDragenter}
onDragover={onDragover}
onDragleave={onDragleave}
onDrop={(e) => {
context.emit('drop', e)
}}
onDragenter={preventDefault}
onDragover={preventDefault}
onDragleave={preventDefault}
>
<div ref={paper} class={Styles.paper}></div>
<div ref={minimap} class={Styles.minimap}></div>

20
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts

@ -16,19 +16,25 @@
*/
import { useCanvasInit } from './use-canvas-init'
import { useGraphOperations } from './use-graph-operations'
import { useCellQuery } from './use-cell-query'
import { useCellActive } from './use-cell-active'
import { useSidebarDrag } from './use-sidebar-drag'
import { useCanvasDrop } from './use-canvas-drop'
import { useCellUpdate } from './use-cell-update'
import { useNodeSearch } from './use-node-search'
import { useGraphAutoLayout } from './use-graph-auto-layout'
import { useTextCopy } from './use-text-copy'
import { useCustomCellBuilder } from './use-custom-cell-builder'
import { useGraphBackfill } from './use-graph-backfill'
import { useDagDragAndDrop } from './use-dag-drag-drop'
export {
useCanvasInit,
useGraphOperations,
useCellQuery,
useCellActive,
useSidebarDrag,
useCanvasDrop,
useNodeSearch,
useGraphAutoLayout
useGraphAutoLayout,
useTextCopy,
useCustomCellBuilder,
useGraphBackfill,
useCellUpdate,
useDagDragAndDrop
}

36
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-sidebar.tsx

@ -15,37 +15,17 @@
* limitations under the License.
*/
import type { PropType, Ref } from 'vue'
import type { Dragged } from './index'
import { defineComponent, ref, inject } from 'vue'
import { ALL_TASK_TYPES } from '../../../task/constants/task-type'
import { useSidebarDrag } from './dag-hooks'
import { defineComponent } from 'vue'
import { TASK_TYPES_MAP, TaskType } from '../../../task/constants/task-type'
import Styles from './dag.module.scss'
const props = {
dragged: {
type: Object as PropType<Ref<Dragged>>,
default: ref({
x: 0,
y: 0,
type: ''
})
}
}
export default defineComponent({
name: 'workflow-dag-sidebar',
props,
setup(props) {
const readonly = inject('readonly', ref(false))
const dragged = props.dragged
const { onDragStart } = useSidebarDrag({
readonly,
dragged
})
const allTaskTypes = Object.keys(ALL_TASK_TYPES).map((type) => ({
emits: ['dragStart'],
setup(props, context) {
const allTaskTypes = Object.keys(TASK_TYPES_MAP).map((type) => ({
type,
...ALL_TASK_TYPES[type]
...TASK_TYPES_MAP[type as TaskType]
}))
return () => (
@ -54,7 +34,9 @@ export default defineComponent({
<div
class={Styles.draggable}
draggable='true'
onDragstart={(e) => onDragStart(e, task.type)}
onDragstart={(e) => {
context.emit('dragStart', e, task.type)
}}
>
<em
class={[

111
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-toolbar.tsx

@ -15,19 +15,29 @@
* limitations under the License.
*/
import { defineComponent, ref, inject, PropType } from 'vue'
import {
defineComponent,
ref,
inject,
PropType,
onMounted,
watch,
computed
} from 'vue'
import type { Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Styles from './dag.module.scss'
import { NTooltip, NIcon, NButton, NSelect } from 'naive-ui'
import { NTooltip, NIcon, NButton, NSelect, useMessage } from 'naive-ui'
import {
SearchOutlined,
DownloadOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
InfoCircleOutlined,
FormatPainterOutlined
FormatPainterOutlined,
CopyOutlined
} from '@vicons/antd'
import { useNodeSearch } from './dag-hooks'
import { useNodeSearch, useTextCopy, useCellQuery } from './dag-hooks'
import { DataUri } from '@antv/x6'
import { useFullscreen } from '@vueuse/core'
import { useRouter } from 'vue-router'
@ -37,12 +47,19 @@ const props = {
layoutToggle: {
type: Function as PropType<(bool?: boolean) => void>,
default: () => {}
},
// If this prop is passed, it means from definition detail
definition: {
// The same as the structure responsed by the queryProcessDefinitionByCode api
type: Object as PropType<any>,
default: null
}
}
export default defineComponent({
name: 'workflow-dag-toolbar',
props,
emits: ['versionToggle'],
setup(props, context) {
const { t } = useI18n()
@ -55,11 +72,11 @@ export default defineComponent({
* Node search and navigate
*/
const {
searchNode,
getAllNodes,
allNodes,
navigateTo,
toggleSearchInput,
searchInputVisible
searchInputVisible,
reQueryNodes,
nodesDropdown
} = useNodeSearch({ graph })
/**
@ -94,7 +111,7 @@ export default defineComponent({
* Open workflow version modal
*/
const openVersionModal = () => {
//TODO, same as the version popup in the workflow list page
context.emit('versionToggle', true)
}
/**
@ -111,6 +128,11 @@ export default defineComponent({
router.go(-1)
}
/**
* Copy workflow name
*/
const { copy } = useTextCopy()
return () => (
<div
class={[
@ -118,7 +140,24 @@ export default defineComponent({
Styles[themeStore.darkTheme ? 'toolbar-dark' : 'toolbar-light']
]}
>
<span class={Styles['workflow-name']}>{t('project.dag.create')}</span>
<div>
<span class={Styles['workflow-name']}>
{props.definition?.processDefinition?.name ||
t('project.dag.create')}
</span>
{props.definition?.processDefinition?.name && (
<NButton
quaternary
circle
onClick={() => copy(props.definition?.processDefinition?.name)}
class={Styles['copy-btn']}
>
<NIcon>
<CopyOutlined />
</NIcon>
</NButton>
)}
</div>
<div class={Styles['toolbar-right-part']}>
{/* Search node */}
<NTooltip
@ -150,9 +189,9 @@ export default defineComponent({
>
<NSelect
size='small'
options={allNodes.value}
onFocus={getAllNodes}
onUpdateValue={searchNode}
options={nodesDropdown.value}
onFocus={reQueryNodes}
onUpdateValue={navigateTo}
filterable
/>
</div>
@ -233,28 +272,30 @@ export default defineComponent({
}}
></NTooltip>
{/* Version info */}
<NTooltip
v-slots={{
trigger: () => (
<NButton
class={Styles['toolbar-right-item']}
strong
secondary
circle
type='info'
onClick={openVersionModal}
v-slots={{
icon: () => (
<NIcon>
<InfoCircleOutlined />
</NIcon>
)
}}
/>
),
default: () => t('project.workflow.version_info')
}}
></NTooltip>
{!!props.definition && (
<NTooltip
v-slots={{
trigger: () => (
<NButton
class={Styles['toolbar-right-item']}
strong
secondary
circle
type='info'
onClick={openVersionModal}
v-slots={{
icon: () => (
<NIcon>
<InfoCircleOutlined />
</NIcon>
)
}}
/>
),
default: () => t('project.workflow.version_info')
}}
></NTooltip>
)}
{/* Save workflow */}
<NButton
class={Styles['toolbar-right-item']}

4
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag.module.scss

@ -76,6 +76,10 @@ $bgLight: #ffffff;
font-size: 14px;
}
.copy-btn {
margin-left: 5px;
}
.draggable {
display: flex;
width: 100%;

83
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx

@ -16,48 +16,48 @@
*/
import type { Graph } from '@antv/x6'
import { defineComponent, ref, provide } from 'vue'
import { defineComponent, ref, provide, PropType, toRef } from 'vue'
import DagToolbar from './dag-toolbar'
import DagCanvas from './dag-canvas'
import DagSidebar from './dag-sidebar'
import Styles from './dag.module.scss'
import DagAutoLayoutModal from './dag-auto-layout-modal'
import { useGraphAutoLayout } from './dag-hooks'
import {
useGraphAutoLayout,
useGraphBackfill,
useDagDragAndDrop
} from './dag-hooks'
import { useThemeStore } from '@/store/theme/theme'
import VersionModal from '../../definition/components/version-modal'
import { WorkflowDefinition } from './types'
import './x6-style.scss'
export interface Dragged {
x: number
y: number
type: string
const props = {
// If this prop is passed, it means from definition detail
definition: {
type: Object as PropType<WorkflowDefinition>,
default: undefined
},
readonly: {
type: Boolean as PropType<boolean>,
default: false
}
}
export default defineComponent({
name: 'workflow-dag',
props,
emits: ['refresh'],
setup(props, context) {
const theme = useThemeStore()
// Whether the graph can be operated
const readonly = ref(false)
provide('readonly', readonly)
provide('readonly', toRef(props, 'readonly'))
const graph = ref<Graph>()
provide('graph', graph)
// The sidebar slots
const toolbarSlots = {
left: context.slots.toolbarLeft,
right: context.slots.toolbarRight
}
// The element currently being dragged up
const dragged = ref<Dragged>({
x: 0,
y: 0,
type: ''
})
// Auto layout
// Auto layout modal
const {
visible: layoutVisible,
toggle: layoutToggle,
@ -67,6 +67,28 @@ export default defineComponent({
cancel
} = useGraphAutoLayout({ graph })
const { onDragStart, onDrop } = useDagDragAndDrop({
graph,
readonly: toRef(props, 'readonly')
})
// backfill
useGraphBackfill({ graph, definition: toRef(props, 'definition') })
// version modal
const versionModalShow = ref(false)
const versionToggle = (bool: boolean) => {
if (typeof bool === 'boolean') {
versionModalShow.value = bool
} else {
versionModalShow.value = !versionModalShow.value
}
}
const refreshDetail = () => {
context.emit('refresh')
versionModalShow.value = false
}
return () => (
<div
class={[
@ -74,10 +96,14 @@ export default defineComponent({
Styles[`dag-${theme.darkTheme ? 'dark' : 'light'}`]
]}
>
<DagToolbar v-slots={toolbarSlots} layoutToggle={layoutToggle} />
<DagToolbar
layoutToggle={layoutToggle}
definition={props.definition}
onVersionToggle={versionToggle}
/>
<div class={Styles.content}>
<DagSidebar dragged={dragged} />
<DagCanvas dragged={dragged} />
<DagSidebar onDragStart={onDragStart} />
<DagCanvas onDrop={onDrop} />
</div>
<DagAutoLayoutModal
visible={layoutVisible.value}
@ -86,6 +112,13 @@ export default defineComponent({
formValue={formValue}
formRef={formRef}
/>
{!!props.definition && (
<VersionModal
v-model:row={props.definition.processDefinition}
v-model:show={versionModalShow.value}
onUpdateList={refreshDetail}
/>
)}
</div>
)
}

95
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/types.ts

@ -0,0 +1,95 @@
/*
* 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.
*/
export interface ProcessDefinition {
id: number
code: number
name: string
version: number
releaseState: string
projectCode: number
description: string
globalParams: string
globalParamList: any[]
globalParamMap: any
createTime: string
updateTime: string
flag: string
userId: number
userName?: any
projectName?: any
locations: string
scheduleReleaseState?: any
timeout: number
tenantId: number
tenantCode: string
modifyBy?: any
warningGroupId: number
}
export interface ProcessTaskRelationList {
id: number
name: string
processDefinitionVersion: number
projectCode: any
processDefinitionCode: any
preTaskCode: number
preTaskVersion: number
postTaskCode: any
postTaskVersion: number
conditionType: string
conditionParams: any
createTime: string
updateTime: string
}
export interface TaskDefinitionList {
id: number
code: any
name: string
version: number
description: string
projectCode: any
userId: number
taskType: string
taskParams: any
taskParamList: any[]
taskParamMap: any
flag: string
taskPriority: string
userName?: any
projectName?: any
workerGroup: string
environmentCode: number
failRetryTimes: number
failRetryInterval: number
timeoutFlag: string
timeoutNotifyStrategy: string
timeout: number
delayTime: number
resourceIds: string
createTime: string
updateTime: string
modifyBy?: any
dependence: string
}
export interface WorkflowDefinition {
processDefinition: ProcessDefinition
processTaskRelationList: ProcessTaskRelationList[]
taskDefinitionList: TaskDefinitionList[]
}

2
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-canvas-init.ts

@ -16,7 +16,7 @@
*/
import type { Node } from '@antv/x6'
import { ref, onMounted, Ref, onUnmounted } from 'vue'
import { ref, onMounted, Ref } from 'vue'
import { Graph } from '@antv/x6'
import { NODE, EDGE, X6_NODE_NAME, X6_EDGE_NAME } from './dag-config'
import { debounce } from 'lodash'

54
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-cell-query.ts

@ -0,0 +1,54 @@
/*
* 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 type { Ref } from 'vue'
import type { Graph } from '@antv/x6'
import { TaskType } from '../../../task/constants/task-type'
interface Options {
graph: Ref<Graph | undefined>
}
/**
* Expose some cell-related query methods and refs
* @param {Options} options
*/
export function useCellQuery(options: Options) {
const { graph } = options
/**
* Get all nodes
*/
function getNodes() {
const nodes = graph.value?.getNodes()
if (!nodes) return []
return nodes.map((node) => {
const position = node.getPosition()
const data = node.getData()
return {
code: node.id,
position: position,
name: data.taskName as string,
type: data.taskType as TaskType
}
})
}
return {
getNodes
}
}

76
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-cell-update.ts

@ -0,0 +1,76 @@
/*
* 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 type { Ref } from 'vue'
import type { Graph } from '@antv/x6'
import type { TaskType } from '@/views/projects/task/constants/task-type'
import { TASK_TYPES_MAP } from '@/views/projects/task/constants/task-type'
import { useCustomCellBuilder } from './dag-hooks'
import type { Coordinate } from './use-custom-cell-builder'
import utils from '@/utils'
interface Options {
graph: Ref<Graph | undefined>
}
/**
* Expose some cell query
* @param {Options} options
*/
export function useCellUpdate(options: Options) {
const { graph } = options
const { buildNode } = useCustomCellBuilder()
/**
* Set node name by id
* @param {string} id
* @param {string} name
*/
function setNodeName(id: string, newName: string) {
const node = graph.value?.getCellById(id)
if (node) {
const truncation = utils.truncateText(newName, 18)
node.attr('title/text', truncation)
node.setData({ taskName: newName })
}
}
/**
* Add a node to the graph
* @param {string} id
* @param {string} taskType
* @param {Coordinate} coordinate Default is { x: 100, y: 100 }
*/
function addNode(
id: string,
type: string,
coordinate: Coordinate = { x: 100, y: 100 }
) {
if (!TASK_TYPES_MAP[type as TaskType]) {
console.warn(`taskType:${type} is invalid!`)
return
}
const node = buildNode(id, type, '', coordinate)
graph.value?.addNode(node)
}
return {
setNodeName,
addNode
}
}

126
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-graph-operations.ts → dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-custom-cell-builder.ts

@ -15,24 +15,27 @@
* limitations under the License.
*/
import type { Ref } from 'vue'
import type { Node, Graph, Edge } from '@antv/x6'
import type { Node, Edge } from '@antv/x6'
import { X6_NODE_NAME, X6_EDGE_NAME } from './dag-config'
import { ALL_TASK_TYPES } from '../../../task/constants/task-type'
import utils from '@/utils'
import { WorkflowDefinition } from './types'
interface Options {
graph: Ref<Graph | undefined>
}
type Coordinate = { x: number; y: number }
export type Coordinate = { x: number; y: number }
/**
* Expose some graph operation methods
* @param {Options} options
*/
export function useGraphOperations(options: Options) {
const { graph } = options
export function useCustomCellBuilder() {
/**
* Convert locationStr to JSON
* @param {string} locationStr
* @returns
*/
function parseLocationStr(locationStr: string) {
let locations = null
if (!locationStr) return locations
try {
locations = JSON.parse(locationStr)
} catch (error) {}
return Array.isArray(locations) ? locations : null
}
/**
* Build edge metadata
@ -40,7 +43,7 @@ export function useGraphOperations(options: Options) {
* @param {string} targetId
* @param {string} label
*/
function buildEdgeMetadata(
function buildEdge(
sourceId: string,
targetId: string,
label: string = ''
@ -63,7 +66,7 @@ export function useGraphOperations(options: Options) {
* @param {string} taskType
* @param {Coordinate} coordinate Default is { x: 100, y: 100 }
*/
function buildNodeMetadata(
function buildNode(
id: string,
type: string,
taskName: string,
@ -92,74 +95,43 @@ export function useGraphOperations(options: Options) {
}
/**
* Add a node to the graph
* @param {string} id
* @param {string} taskType
* @param {Coordinate} coordinate Default is { x: 100, y: 100 }
* Build graph JSON
* @param {WorkflowDefinition} definition
* @returns
*/
function addNode(
id: string,
type: string,
coordinate: Coordinate = { x: 100, y: 100 }
) {
if (!ALL_TASK_TYPES[type]) {
console.warn(`taskType:${type} is invalid!`)
return
}
const node = buildNodeMetadata(id, type, '', coordinate)
graph.value?.addNode(node)
}
function buildGraph(definition: WorkflowDefinition) {
const nodes: Node.Metadata[] = []
const edges: Edge.Metadata[] = []
/**
* Set node name by id
* @param {string} id
* @param {string} name
*/
function setNodeName(id: string, newName: string) {
const node = graph.value?.getCellById(id)
if (node) {
const truncation = utils.truncateText(newName, 18)
node.attr('title/text', truncation)
node.setData({ taskName: newName })
}
}
const locations =
parseLocationStr(definition.processDefinition.locations) || []
const tasks = definition.taskDefinitionList
const connects = definition.processTaskRelationList
/**
* Get nodes
*/
function getNodes() {
const nodes = graph.value?.getNodes()
if (!nodes) return []
return nodes.map((node) => {
const position = node.getPosition()
const data = node.getData()
return {
code: node.id,
position: position,
name: data.taskName,
type: data.taskType
}
tasks.forEach((task) => {
const location = locations.find((l) => l.taskCode === task.code) || {}
const node = buildNode(task.code, task.taskType, task.name, {
x: location.x,
y: location.y
})
nodes.push(node)
})
}
/**
* Navigate to cell
* @param {string} code
*/
function navigateTo(code: string) {
if (!graph.value) return
const cell = graph.value.getCellById(code)
graph.value.scrollToCell(cell, { animation: { duration: 600 } })
graph.value.cleanSelection()
graph.value.select(cell)
connects
.filter((r) => !!r.preTaskCode)
.forEach((c) => {
const edge = buildEdge(c.preTaskCode + '', c.postTaskCode, c.name)
edges.push(edge)
})
return {
nodes,
edges
}
}
return {
buildEdgeMetadata,
buildNodeMetadata,
addNode,
setNodeName,
getNodes,
navigateTo
buildNode,
buildEdge,
buildGraph
}
}

46
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-canvas-drop.ts → dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-dag-drag-drop.ts

@ -15,39 +15,60 @@
* limitations under the License.
*/
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { Graph } from '@antv/x6'
import type { Dragged } from './dag'
import type { Dragged } from '.'
import { genTaskCodeList } from '@/service/modules/task-definition'
import { useGraphOperations } from './dag-hooks'
import { useCellUpdate } from './dag-hooks'
import { useRoute } from 'vue-router'
interface Options {
readonly: Ref<boolean>
graph: Ref<Graph | undefined>
container: Ref<HTMLElement | undefined>
dragged: Ref<Dragged>
projectCode: string
}
/**
* Drop sidebar item in canvas
* Sidebar item drag && drop in canvas
*/
export function useCanvasDrop(options: Options) {
const { readonly, graph, container, dragged, projectCode } = options
export function useDagDragAndDrop(options: Options) {
const { readonly, graph } = options
const { addNode } = useGraphOperations({ graph })
const route = useRoute()
const projectCode = Number(route.params.projectCode)
const onDrop = (e: DragEvent) => {
const { addNode } = useCellUpdate({ graph })
// The element currently being dragged up
const dragged = ref<Dragged>({
x: 0,
y: 0,
type: ''
})
function onDragStart(e: DragEvent, type: string) {
if (readonly.value) {
e.preventDefault()
return
}
dragged.value = {
x: e.offsetX,
y: e.offsetY,
type: type
}
}
function onDrop(e: DragEvent) {
e.stopPropagation()
e.preventDefault()
if (readonly.value) {
return
}
if (dragged.value && graph.value && container.value && projectCode) {
if (dragged.value && graph.value && projectCode) {
const { type, x: eX, y: eY } = dragged.value
const { x, y } = graph.value.clientToLocal(e.clientX, e.clientY)
const genNums = 1
genTaskCodeList(genNums, Number(projectCode)).then((res) => {
genTaskCodeList(genNums, projectCode).then((res) => {
const [code] = res
addNode(code + '', type, { x: x - eX, y: y - eY })
// openTaskConfigModel(code, type)
@ -60,6 +81,7 @@ export function useCanvasDrop(options: Options) {
}
return {
onDragStart,
onDrop,
onDragenter: preventDefault,
onDragover: preventDefault,

43
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-graph-backfill.ts

@ -0,0 +1,43 @@
/*
* 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 { Ref, watch } from 'vue'
import { useCustomCellBuilder } from './dag-hooks'
import type { Graph } from '@antv/x6'
import { WorkflowDefinition } from './types'
interface Options {
graph: Ref<Graph | undefined>
definition: Ref<WorkflowDefinition | undefined>
}
/**
* Backfill workflow into graph
*/
export function useGraphBackfill(options: Options) {
const { graph, definition } = options
const { buildGraph } = useCustomCellBuilder()
watch([graph, definition], () => {
if (graph.value && definition.value) {
graph.value.fromJSON(buildGraph(definition.value))
}
})
return {}
}

47
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-search.ts

@ -17,7 +17,7 @@
import type { Graph } from '@antv/x6'
import { ref, Ref } from 'vue'
import { useGraphOperations } from './dag-hooks'
import { useCellQuery } from './dag-hooks'
interface Options {
graph: Ref<Graph | undefined>
@ -29,30 +29,43 @@ interface Options {
export function useNodeSearch(options: Options) {
const { graph } = options
/**
* Search input visible control
*/
const searchInputVisible = ref(false)
const allNodes = ref<any>([])
const toggleSearchInput = () => {
searchInputVisible.value = !searchInputVisible.value
}
const { getNodes, navigateTo } = useGraphOperations({ graph })
const searchNode = (val: string) => {
navigateTo(val)
/**
* Search dropdown control
*/
const { getNodes } = useCellQuery({ graph })
const nodesDropdown = ref<{ label: string; value: string }[]>([])
const reQueryNodes = () => {
nodesDropdown.value = getNodes().map((node) => ({
label: node.name,
value: node.code
}))
}
const getAllNodes = () => {
const nodes = getNodes()
allNodes.value = nodes.map((node) => {
return {
label: node.name,
value: node.code
}
})
/**
* Navigate to cell
* @param {string} code
*/
function navigateTo(code: string) {
if (!graph.value) return
const cell = graph.value.getCellById(code)
graph.value.scrollToCell(cell, { animation: { duration: 600 } })
graph.value.cleanSelection()
graph.value.select(cell)
}
return {
searchNode,
getAllNodes,
allNodes,
navigateTo,
toggleSearchInput,
searchInputVisible
searchInputVisible,
reQueryNodes,
nodesDropdown
}
}

36
dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-sidebar-drag.ts → dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-text-copy.ts

@ -15,33 +15,23 @@
* limitations under the License.
*/
import type { Ref } from 'vue'
import type { Dragged } from './dag'
interface Options {
readonly: Ref<boolean>
dragged: Ref<Dragged>
}
import { useClipboard } from '@vueuse/core'
import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
/**
* Sidebar drag
* Text copy with success message
*/
export function useSidebarDrag(options: Options) {
const { readonly, dragged } = options
const onDragStart = (e: DragEvent, type: string) => {
if (readonly.value) {
e.preventDefault()
return
}
dragged.value = {
x: e.offsetX,
y: e.offsetY,
type: type
}
export function useTextCopy() {
const { t } = useI18n()
const { copy } = useClipboard()
const message = useMessage()
const copyText = (text: string) => {
copy(text).then((res) => {
message.success(t('project.dag.copy_success'))
})
}
return {
onDragStart
copy: copyText
}
}

6
dolphinscheduler-ui-next/src/views/projects/workflow/definition/components/version-modal.tsx

@ -58,9 +58,11 @@ export default defineComponent({
}
watch(
() => props.row.code,
() => props.show,
() => {
getTableData(props.row)
if (props.show && props.row?.code) {
getTableData(props.row)
}
}
)

7
dolphinscheduler-ui-next/src/views/projects/workflow/definition/create/index.tsx

@ -25,11 +25,6 @@ export default defineComponent({
setup() {
const theme = useThemeStore()
const slots = {
toolbarLeft: () => <span>left-operations</span>,
toolbarRight: () => <span>right-operations</span>
}
return () => (
<div
class={[
@ -37,7 +32,7 @@ export default defineComponent({
theme.darkTheme ? Styles['dark'] : Styles['light']
]}
>
<Dag v-slots={slots} />
<Dag />
</div>
)
}

41
dolphinscheduler-ui-next/src/views/projects/workflow/definition/detail/index.module.scss

@ -0,0 +1,41 @@
/*
* 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.
*/
$borderDark: rgba(255, 255, 255, 0.09);
$borderLight: rgb(239, 239, 245);
$bgDark: rgb(24, 24, 28);
$bgLight: #ffffff;
.container {
width: 100%;
padding: 20px;
box-sizing: border-box;
height: calc(100vh - 100px);
overflow: hidden;
display: block;
}
.dark {
border: solid 1px $borderDark;
background-color: $bgDark;
}
.light {
border: solid 1px $borderLight;
background-color: $bgLight;
}

37
dolphinscheduler-ui-next/src/views/projects/workflow/definition/detail/index.tsx

@ -15,11 +15,44 @@
* limitations under the License.
*/
import { defineComponent } from 'vue'
import { defineComponent, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useThemeStore } from '@/store/theme/theme'
import Dag from '../../components/dag'
import { queryProcessDefinitionByCode } from '@/service/modules/process-definition'
import { WorkflowDefinition } from '../../components/dag/types'
import Styles from './index.module.scss'
export default defineComponent({
name: 'WorkflowDefinitionDetails',
setup() {
return () => <div>WorkflowDefinitionDetails</div>
const theme = useThemeStore()
const route = useRoute()
const projectCode = Number(route.params.projectCode)
const code = Number(route.params.code)
const definition = ref<WorkflowDefinition>()
const refresh = () => {
queryProcessDefinitionByCode(code, projectCode).then((res: any) => {
definition.value = res
})
}
onMounted(() => {
if (!code || !projectCode) return
refresh()
})
return () => (
<div
class={[
Styles.container,
theme.darkTheme ? Styles['dark'] : Styles['light']
]}
>
<Dag definition={definition.value} onRefresh={refresh} />
</div>
)
}
})

15
dolphinscheduler-ui-next/src/views/projects/workflow/definition/index.tsx

@ -32,11 +32,17 @@ import ImportModal from './components/import-modal'
import StartModal from './components/start-modal'
import TimingModal from './components/timing-modal'
import VersionModal from './components/version-modal'
import { useRouter, useRoute } from 'vue-router'
import type { Router } from 'vue-router'
import styles from './index.module.scss'
export default defineComponent({
name: 'WorkflowDefinitionList',
setup() {
const router: Router = useRouter()
const route = useRoute()
const projectCode = Number(route.params.projectCode)
const { variables, getTableData } = useTable()
const requestData = () => {
@ -61,6 +67,12 @@ export default defineComponent({
requestData()
}
const createDefinition = () => {
router.push({
path: `/projects/${projectCode}/workflow/definitions/create`
})
}
onMounted(() => {
requestData()
})
@ -69,6 +81,7 @@ export default defineComponent({
requestData,
handleSearch,
handleUpdateList,
createDefinition,
handleChangePageSize,
...toRefs(variables)
}
@ -81,7 +94,7 @@ export default defineComponent({
<Card class={styles.card}>
<div class={styles.header}>
<NSpace>
<NButton type='primary' /* TODO: Create workflow */>
<NButton type='primary' onClick={this.createDefinition}>
{t('project.workflow.create_workflow')}
</NButton>
<NButton strong secondary onClick={() => (this.showRef = true)}>

25
dolphinscheduler-ui-next/src/views/projects/workflow/definition/use-table.ts → dolphinscheduler-ui-next/src/views/projects/workflow/definition/use-table.tsx

@ -17,7 +17,7 @@
import { h, ref, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useRouter, RouterLink } from 'vue-router'
import type { Router } from 'vue-router'
import type { TableColumns } from 'naive-ui/es/data-table/src/interface'
import { useAsyncState } from '@vueuse/core'
@ -49,14 +49,21 @@ export function useTable() {
title: t('project.workflow.workflow_name'),
key: 'name',
width: 200,
render: (_row) =>
h(
NEllipsis,
{ style: 'max-width: 200px' },
{
default: () => _row.name
}
)
render: (_row) => (
<NEllipsis
style={{
maxWidth: '200px'
}}
>
<RouterLink
to={{
path: `/projects/${_row.projectCode}/workflow/definitions/${_row.code}`
}}
>
{_row.name}
</RouterLink>
</NEllipsis>
)
},
{
title: t('project.workflow.status'),
Loading…
Cancel
Save