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 3 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. 67
      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. 124
      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. 43
      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. 4
      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. 23
      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', grid_layout: 'Grid',
dagre_layout: 'Dagre', dagre_layout: 'Dagre',
rows: 'Rows', 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: '网格布局', grid_layout: '网格布局',
dagre_layout: '层次布局', dagre_layout: '层次布局',
rows: '行数', 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( export function queryProcessDefinitionByCode(
code: CodeReq, code: number,
processCode: CodeReq projectCode: number
): any { ): any {
return axios({ return axios({
url: `/projects/${code}/process-definition/${processCode}`, url: `/projects/${projectCode}/process-definition/${code}`,
method: 'get' method: 'get'
}) })
} }

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

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
export const ALL_TASK_TYPES: any = { export const TASK_TYPES_MAP = {
SHELL: { SHELL: {
alias: 'SHELL' alias: 'SHELL'
}, },
@ -65,3 +65,5 @@ export const ALL_TASK_TYPES: any = {
alias: 'WATERDROP' 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 { defineComponent, ref, inject } from 'vue'
import Styles from './dag.module.scss' import Styles from './dag.module.scss'
import type { PropType, Ref } from 'vue' import { useCanvasInit, useCellActive } from './dag-hooks'
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: ''
})
}
}
export default defineComponent({ export default defineComponent({
name: 'workflow-dag-canvas', name: 'workflow-dag-canvas',
props, emits: ['drop'],
setup(props, context) { setup(props, context) {
const readonly = inject('readonly', ref(false)) const readonly = inject('readonly', ref(false))
const graph = inject('graph', ref()) const graph = inject('graph', ref())
const route = useRoute()
const projectCode = route.params.projectCode as string
const { paper, minimap, container } = useCanvasInit({ readonly, graph }) const { paper, minimap, container } = useCanvasInit({ readonly, graph })
// Change the style on cell hover and select // Change the style on cell hover and select
useCellActive({ graph }) useCellActive({ graph })
const preventDefault = (e: DragEvent) => {
// Drop sidebar item in canvas e.preventDefault()
const { onDrop, onDragenter, onDragover, onDragleave } = useCanvasDrop({ }
readonly,
dragged: props.dragged,
graph,
container,
projectCode
})
return () => ( return () => (
<div <div
ref={container} ref={container}
class={Styles.canvas} class={Styles.canvas}
onDrop={onDrop} onDrop={(e) => {
onDragenter={onDragenter} context.emit('drop', e)
onDragover={onDragover} }}
onDragleave={onDragleave} onDragenter={preventDefault}
onDragover={preventDefault}
onDragleave={preventDefault}
> >
<div ref={paper} class={Styles.paper}></div> <div ref={paper} class={Styles.paper}></div>
<div ref={minimap} class={Styles.minimap}></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 { useCanvasInit } from './use-canvas-init'
import { useGraphOperations } from './use-graph-operations' import { useCellQuery } from './use-cell-query'
import { useCellActive } from './use-cell-active' import { useCellActive } from './use-cell-active'
import { useSidebarDrag } from './use-sidebar-drag' import { useCellUpdate } from './use-cell-update'
import { useCanvasDrop } from './use-canvas-drop'
import { useNodeSearch } from './use-node-search' import { useNodeSearch } from './use-node-search'
import { useGraphAutoLayout } from './use-graph-auto-layout' 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 { export {
useCanvasInit, useCanvasInit,
useGraphOperations, useCellQuery,
useCellActive, useCellActive,
useSidebarDrag,
useCanvasDrop,
useNodeSearch, 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. * limitations under the License.
*/ */
import type { PropType, Ref } from 'vue' import { defineComponent } from 'vue'
import type { Dragged } from './index' import { TASK_TYPES_MAP, TaskType } from '../../../task/constants/task-type'
import { defineComponent, ref, inject } from 'vue'
import { ALL_TASK_TYPES } from '../../../task/constants/task-type'
import { useSidebarDrag } from './dag-hooks'
import Styles from './dag.module.scss' 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({ export default defineComponent({
name: 'workflow-dag-sidebar', name: 'workflow-dag-sidebar',
props, emits: ['dragStart'],
setup(props) { setup(props, context) {
const readonly = inject('readonly', ref(false)) const allTaskTypes = Object.keys(TASK_TYPES_MAP).map((type) => ({
const dragged = props.dragged
const { onDragStart } = useSidebarDrag({
readonly,
dragged
})
const allTaskTypes = Object.keys(ALL_TASK_TYPES).map((type) => ({
type, type,
...ALL_TASK_TYPES[type] ...TASK_TYPES_MAP[type as TaskType]
})) }))
return () => ( return () => (
@ -54,7 +34,9 @@ export default defineComponent({
<div <div
class={Styles.draggable} class={Styles.draggable}
draggable='true' draggable='true'
onDragstart={(e) => onDragStart(e, task.type)} onDragstart={(e) => {
context.emit('dragStart', e, task.type)
}}
> >
<em <em
class={[ class={[

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

@ -15,19 +15,29 @@
* limitations under the License. * 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 { useI18n } from 'vue-i18n'
import Styles from './dag.module.scss' import Styles from './dag.module.scss'
import { NTooltip, NIcon, NButton, NSelect } from 'naive-ui' import { NTooltip, NIcon, NButton, NSelect, useMessage } from 'naive-ui'
import { import {
SearchOutlined, SearchOutlined,
DownloadOutlined, DownloadOutlined,
FullscreenOutlined, FullscreenOutlined,
FullscreenExitOutlined, FullscreenExitOutlined,
InfoCircleOutlined, InfoCircleOutlined,
FormatPainterOutlined FormatPainterOutlined,
CopyOutlined
} from '@vicons/antd' } from '@vicons/antd'
import { useNodeSearch } from './dag-hooks' import { useNodeSearch, useTextCopy, useCellQuery } from './dag-hooks'
import { DataUri } from '@antv/x6' import { DataUri } from '@antv/x6'
import { useFullscreen } from '@vueuse/core' import { useFullscreen } from '@vueuse/core'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -37,12 +47,19 @@ const props = {
layoutToggle: { layoutToggle: {
type: Function as PropType<(bool?: boolean) => void>, type: Function as PropType<(bool?: boolean) => void>,
default: () => {} 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({ export default defineComponent({
name: 'workflow-dag-toolbar', name: 'workflow-dag-toolbar',
props, props,
emits: ['versionToggle'],
setup(props, context) { setup(props, context) {
const { t } = useI18n() const { t } = useI18n()
@ -55,11 +72,11 @@ export default defineComponent({
* Node search and navigate * Node search and navigate
*/ */
const { const {
searchNode, navigateTo,
getAllNodes,
allNodes,
toggleSearchInput, toggleSearchInput,
searchInputVisible searchInputVisible,
reQueryNodes,
nodesDropdown
} = useNodeSearch({ graph }) } = useNodeSearch({ graph })
/** /**
@ -94,7 +111,7 @@ export default defineComponent({
* Open workflow version modal * Open workflow version modal
*/ */
const openVersionModal = () => { 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) router.go(-1)
} }
/**
* Copy workflow name
*/
const { copy } = useTextCopy()
return () => ( return () => (
<div <div
class={[ class={[
@ -118,7 +140,24 @@ export default defineComponent({
Styles[themeStore.darkTheme ? 'toolbar-dark' : 'toolbar-light'] 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']}> <div class={Styles['toolbar-right-part']}>
{/* Search node */} {/* Search node */}
<NTooltip <NTooltip
@ -150,9 +189,9 @@ export default defineComponent({
> >
<NSelect <NSelect
size='small' size='small'
options={allNodes.value} options={nodesDropdown.value}
onFocus={getAllNodes} onFocus={reQueryNodes}
onUpdateValue={searchNode} onUpdateValue={navigateTo}
filterable filterable
/> />
</div> </div>
@ -233,6 +272,7 @@ export default defineComponent({
}} }}
></NTooltip> ></NTooltip>
{/* Version info */} {/* Version info */}
{!!props.definition && (
<NTooltip <NTooltip
v-slots={{ v-slots={{
trigger: () => ( trigger: () => (
@ -255,6 +295,7 @@ export default defineComponent({
default: () => t('project.workflow.version_info') default: () => t('project.workflow.version_info')
}} }}
></NTooltip> ></NTooltip>
)}
{/* Save workflow */} {/* Save workflow */}
<NButton <NButton
class={Styles['toolbar-right-item']} 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; font-size: 14px;
} }
.copy-btn {
margin-left: 5px;
}
.draggable { .draggable {
display: flex; display: flex;
width: 100%; 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 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 DagToolbar from './dag-toolbar'
import DagCanvas from './dag-canvas' import DagCanvas from './dag-canvas'
import DagSidebar from './dag-sidebar' import DagSidebar from './dag-sidebar'
import Styles from './dag.module.scss' import Styles from './dag.module.scss'
import DagAutoLayoutModal from './dag-auto-layout-modal' 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 { useThemeStore } from '@/store/theme/theme'
import VersionModal from '../../definition/components/version-modal'
import { WorkflowDefinition } from './types'
import './x6-style.scss' import './x6-style.scss'
export interface Dragged { const props = {
x: number // If this prop is passed, it means from definition detail
y: number definition: {
type: string type: Object as PropType<WorkflowDefinition>,
default: undefined
},
readonly: {
type: Boolean as PropType<boolean>,
default: false
}
} }
export default defineComponent({ export default defineComponent({
name: 'workflow-dag', name: 'workflow-dag',
props,
emits: ['refresh'],
setup(props, context) { setup(props, context) {
const theme = useThemeStore() const theme = useThemeStore()
// Whether the graph can be operated // Whether the graph can be operated
const readonly = ref(false) provide('readonly', toRef(props, 'readonly'))
provide('readonly', readonly)
const graph = ref<Graph>() const graph = ref<Graph>()
provide('graph', graph) provide('graph', graph)
// The sidebar slots // Auto layout modal
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
const { const {
visible: layoutVisible, visible: layoutVisible,
toggle: layoutToggle, toggle: layoutToggle,
@ -67,6 +67,28 @@ export default defineComponent({
cancel cancel
} = useGraphAutoLayout({ graph }) } = 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 () => ( return () => (
<div <div
class={[ class={[
@ -74,10 +96,14 @@ export default defineComponent({
Styles[`dag-${theme.darkTheme ? 'dark' : 'light'}`] Styles[`dag-${theme.darkTheme ? 'dark' : 'light'}`]
]} ]}
> >
<DagToolbar v-slots={toolbarSlots} layoutToggle={layoutToggle} /> <DagToolbar
layoutToggle={layoutToggle}
definition={props.definition}
onVersionToggle={versionToggle}
/>
<div class={Styles.content}> <div class={Styles.content}>
<DagSidebar dragged={dragged} /> <DagSidebar onDragStart={onDragStart} />
<DagCanvas dragged={dragged} /> <DagCanvas onDrop={onDrop} />
</div> </div>
<DagAutoLayoutModal <DagAutoLayoutModal
visible={layoutVisible.value} visible={layoutVisible.value}
@ -86,6 +112,13 @@ export default defineComponent({
formValue={formValue} formValue={formValue}
formRef={formRef} formRef={formRef}
/> />
{!!props.definition && (
<VersionModal
v-model:row={props.definition.processDefinition}
v-model:show={versionModalShow.value}
onUpdateList={refreshDetail}
/>
)}
</div> </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 type { Node } from '@antv/x6'
import { ref, onMounted, Ref, onUnmounted } from 'vue' import { ref, onMounted, Ref } from 'vue'
import { Graph } from '@antv/x6' import { Graph } from '@antv/x6'
import { NODE, EDGE, X6_NODE_NAME, X6_EDGE_NAME } from './dag-config' import { NODE, EDGE, X6_NODE_NAME, X6_EDGE_NAME } from './dag-config'
import { debounce } from 'lodash' 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
}
}

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

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. * limitations under the License.
*/ */
import { ref } from 'vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { Graph } from '@antv/x6' import type { Graph } from '@antv/x6'
import type { Dragged } from './dag' import type { Dragged } from '.'
import { genTaskCodeList } from '@/service/modules/task-definition' import { genTaskCodeList } from '@/service/modules/task-definition'
import { useGraphOperations } from './dag-hooks' import { useCellUpdate } from './dag-hooks'
import { useRoute } from 'vue-router'
interface Options { interface Options {
readonly: Ref<boolean> readonly: Ref<boolean>
graph: Ref<Graph | undefined> 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) { export function useDagDragAndDrop(options: Options) {
const { readonly, graph, container, dragged, projectCode } = 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.stopPropagation()
e.preventDefault() e.preventDefault()
if (readonly.value) { if (readonly.value) {
return return
} }
if (dragged.value && graph.value && container.value && projectCode) { if (dragged.value && graph.value && projectCode) {
const { type, x: eX, y: eY } = dragged.value const { type, x: eX, y: eY } = dragged.value
const { x, y } = graph.value.clientToLocal(e.clientX, e.clientY) const { x, y } = graph.value.clientToLocal(e.clientX, e.clientY)
const genNums = 1 const genNums = 1
genTaskCodeList(genNums, Number(projectCode)).then((res) => { genTaskCodeList(genNums, projectCode).then((res) => {
const [code] = res const [code] = res
addNode(code + '', type, { x: x - eX, y: y - eY }) addNode(code + '', type, { x: x - eX, y: y - eY })
// openTaskConfigModel(code, type) // openTaskConfigModel(code, type)
@ -60,6 +81,7 @@ export function useCanvasDrop(options: Options) {
} }
return { return {
onDragStart,
onDrop, onDrop,
onDragenter: preventDefault, onDragenter: preventDefault,
onDragover: 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 {}
}

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

@ -17,7 +17,7 @@
import type { Graph } from '@antv/x6' import type { Graph } from '@antv/x6'
import { ref, Ref } from 'vue' import { ref, Ref } from 'vue'
import { useGraphOperations } from './dag-hooks' import { useCellQuery } from './dag-hooks'
interface Options { interface Options {
graph: Ref<Graph | undefined> graph: Ref<Graph | undefined>
@ -29,30 +29,43 @@ interface Options {
export function useNodeSearch(options: Options) { export function useNodeSearch(options: Options) {
const { graph } = options const { graph } = options
/**
* Search input visible control
*/
const searchInputVisible = ref(false) const searchInputVisible = ref(false)
const allNodes = ref<any>([])
const toggleSearchInput = () => { const toggleSearchInput = () => {
searchInputVisible.value = !searchInputVisible.value searchInputVisible.value = !searchInputVisible.value
} }
const { getNodes, navigateTo } = useGraphOperations({ graph })
const searchNode = (val: string) => { /**
navigateTo(val) * Search dropdown control
} */
const getAllNodes = () => { const { getNodes } = useCellQuery({ graph })
const nodes = getNodes() const nodesDropdown = ref<{ label: string; value: string }[]>([])
allNodes.value = nodes.map((node) => { const reQueryNodes = () => {
return { nodesDropdown.value = getNodes().map((node) => ({
label: node.name, label: node.name,
value: node.code 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 { return {
searchNode, navigateTo,
getAllNodes,
allNodes,
toggleSearchInput, 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. * limitations under the License.
*/ */
import type { Ref } from 'vue' import { useClipboard } from '@vueuse/core'
import type { Dragged } from './dag' import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
interface Options {
readonly: Ref<boolean>
dragged: Ref<Dragged>
}
/** /**
* Sidebar drag * Text copy with success message
*/ */
export function useSidebarDrag(options: Options) { export function useTextCopy() {
const { readonly, dragged } = options const { t } = useI18n()
const { copy } = useClipboard()
const onDragStart = (e: DragEvent, type: string) => { const message = useMessage()
if (readonly.value) { const copyText = (text: string) => {
e.preventDefault() copy(text).then((res) => {
return message.success(t('project.dag.copy_success'))
})
} }
dragged.value = {
x: e.offsetX,
y: e.offsetY,
type: type
}
}
return { return {
onDragStart copy: copyText
} }
} }

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

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

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

@ -25,11 +25,6 @@ export default defineComponent({
setup() { setup() {
const theme = useThemeStore() const theme = useThemeStore()
const slots = {
toolbarLeft: () => <span>left-operations</span>,
toolbarRight: () => <span>right-operations</span>
}
return () => ( return () => (
<div <div
class={[ class={[
@ -37,7 +32,7 @@ export default defineComponent({
theme.darkTheme ? Styles['dark'] : Styles['light'] theme.darkTheme ? Styles['dark'] : Styles['light']
]} ]}
> >
<Dag v-slots={slots} /> <Dag />
</div> </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. * 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({ export default defineComponent({
name: 'WorkflowDefinitionDetails', name: 'WorkflowDefinitionDetails',
setup() { 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 StartModal from './components/start-modal'
import TimingModal from './components/timing-modal' import TimingModal from './components/timing-modal'
import VersionModal from './components/version-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' import styles from './index.module.scss'
export default defineComponent({ export default defineComponent({
name: 'WorkflowDefinitionList', name: 'WorkflowDefinitionList',
setup() { setup() {
const router: Router = useRouter()
const route = useRoute()
const projectCode = Number(route.params.projectCode)
const { variables, getTableData } = useTable() const { variables, getTableData } = useTable()
const requestData = () => { const requestData = () => {
@ -61,6 +67,12 @@ export default defineComponent({
requestData() requestData()
} }
const createDefinition = () => {
router.push({
path: `/projects/${projectCode}/workflow/definitions/create`
})
}
onMounted(() => { onMounted(() => {
requestData() requestData()
}) })
@ -69,6 +81,7 @@ export default defineComponent({
requestData, requestData,
handleSearch, handleSearch,
handleUpdateList, handleUpdateList,
createDefinition,
handleChangePageSize, handleChangePageSize,
...toRefs(variables) ...toRefs(variables)
} }
@ -81,7 +94,7 @@ export default defineComponent({
<Card class={styles.card}> <Card class={styles.card}>
<div class={styles.header}> <div class={styles.header}>
<NSpace> <NSpace>
<NButton type='primary' /* TODO: Create workflow */> <NButton type='primary' onClick={this.createDefinition}>
{t('project.workflow.create_workflow')} {t('project.workflow.create_workflow')}
</NButton> </NButton>
<NButton strong secondary onClick={() => (this.showRef = true)}> <NButton strong secondary onClick={() => (this.showRef = true)}>

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