You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by so...@apache.org on 2022/02/22 07:10:51 UTC

[dolphinscheduler] branch dev updated: [Feature][UI Next] Add dag menu (#8481)

This is an automated email from the ASF dual-hosted git repository.

songjian pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 66241fd  [Feature][UI Next] Add dag menu (#8481)
66241fd is described below

commit 66241fd5c27e7463707ff94ed55d3fce350a3f49
Author: Devosend <de...@gmail.com>
AuthorDate: Tue Feb 22 15:10:45 2022 +0800

    [Feature][UI Next] Add dag menu (#8481)
    
    * add dag menu
    
    * add dag menu click event
    
    * workflow online edit not allowed
---
 .../src/locales/modules/en_US.ts                   |   6 +-
 .../src/locales/modules/zh_CN.ts                   |   6 +-
 dolphinscheduler-ui-next/src/utils/common.ts       |  17 +++
 .../projects/workflow/components/dag/dag-config.ts |   7 +-
 .../workflow/components/dag/dag-context-menu.tsx   | 163 +++++++++++++++++++++
 .../projects/workflow/components/dag/dag-hooks.ts  |   4 +-
 .../projects/workflow/components/dag/index.tsx     |  38 ++++-
 .../dag/{dag-hooks.ts => menu.module.scss}         |  47 +++---
 .../workflow/components/dag/use-canvas-init.ts     |   3 +
 .../workflow/components/dag/use-node-menu.ts       |  75 ++++++++++
 .../workflow/components/dag/use-task-edit.ts       |  43 +++++-
 11 files changed, 376 insertions(+), 33 deletions(-)

diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
index 6c91e5c..7597e4a 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
@@ -684,7 +684,11 @@ const project = {
     sql_input_placeholder: 'Please enter non-query sql.',
     sql_empty_tips: 'The sql can not be empty.',
     procedure_method: 'SQL Statement',
-    procedure_method_tips: 'Please enter the procedure script'
+    procedure_method_tips: 'Please enter the procedure script',
+    start: 'Start',
+    edit: 'Edit',
+    copy: 'Copy',
+    delete: 'Delete'
   }
 }
 
diff --git a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
index a503c76..10b5a39 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
@@ -676,7 +676,11 @@ const project = {
     sql_input_placeholder: '请输入非查询SQL语句',
     sql_empty_tips: '语句不能为空',
     procedure_method: 'SQL语句',
-    procedure_method_tips: '请输入存储脚本'
+    procedure_method_tips: '请输入存储脚本',
+    start: '运行',
+    edit: '编辑',
+    copy: '复制节点',
+    delete: '删除'
   }
 }
 
diff --git a/dolphinscheduler-ui-next/src/utils/common.ts b/dolphinscheduler-ui-next/src/utils/common.ts
index 68766c9..23ca932 100644
--- a/dolphinscheduler-ui-next/src/utils/common.ts
+++ b/dolphinscheduler-ui-next/src/utils/common.ts
@@ -314,3 +314,20 @@ export const tasksState = (t: any): ITaskState => ({
     isSpin: false
   }
 })
+
+/**
+ * A simple uuid generator, support prefix and template pattern.
+ *
+ * @example
+ *
+ *  uuid('v-') // -> v-xxx
+ *  uuid('v-ani-%{s}-translate')  // -> v-ani-xxx
+ */
+export function uuid(prefix: string) {
+  const id = Math.floor(Math.random() * 10000).toString(36)
+  return prefix
+    ? ~prefix.indexOf('%{s}')
+      ? prefix.replace(/%\{s\}/g, id)
+      : prefix + id
+    : id
+}
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-config.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-config.ts
index 5f73e2f..b84adea 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-config.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-config.ts
@@ -200,7 +200,12 @@ export const NODE = {
         group: X6_PORT_OUT_NAME
       }
     ]
-  }
+  },
+  tools: [
+    {
+      name: 'contextmenu'
+    }
+  ]
 }
 
 export const NODE_HOVER = {
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-context-menu.tsx b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-context-menu.tsx
new file mode 100644
index 0000000..0681d54
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-context-menu.tsx
@@ -0,0 +1,163 @@
+/*
+ * 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 { genTaskCodeList } from '@/service/modules/task-definition'
+import type { Cell } from '@antv/x6'
+import {
+  defineComponent,
+  onMounted,
+  PropType,
+  inject,
+  ref,
+  computed
+} from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useRoute } from 'vue-router'
+import styles from './menu.module.scss'
+import { uuid } from '@/utils/common'
+
+const props = {
+  cell: {
+    type: Object as PropType<Cell>,
+    require: true
+  },
+  visible: {
+    type: Boolean as PropType<boolean>,
+    default: true
+  },
+  left: {
+    type: Number as PropType<number>,
+    default: 0
+  },
+  top: {
+    type: Number as PropType<number>,
+    default: 0
+  },
+  releaseState: {
+    type: String as PropType<string>,
+    default: 'OFFLINE'
+  }
+}
+
+export default defineComponent({
+  name: 'dag-context-menu',
+  props,
+  emits: ['hide', 'start', 'edit', 'copyTask', 'removeTasks'],
+  setup(props, ctx) {
+    const graph = inject('graph', ref())
+    const route = useRoute()
+    const projectCode = Number(route.params.projectCode)
+
+    const startAvailable = computed(
+      () =>
+        route.name === 'workflow-definition-detail' &&
+        props.releaseState !== 'NOT_RELEASE'
+    )
+
+    const hide = () => {
+      ctx.emit('hide', false)
+    }
+
+    const startRunning = () => {
+      ctx.emit('start')
+    }
+
+    const handleEdit = () => {
+      ctx.emit('edit', Number(props.cell?.id))
+    }
+
+    const handleCopy = () => {
+      const genNums = 1
+      const type = props.cell?.data.taskType
+      const taskName = uuid(props.cell?.data.taskName + '_')
+      const targetCode = Number(props.cell?.id)
+
+      genTaskCodeList(genNums, projectCode).then((res) => {
+        const [code] = res
+        ctx.emit('copyTask', taskName, code, targetCode, type, {
+          x: props.left + 100,
+          y: props.top + 100
+        })
+      })
+    }
+
+    const handleDelete = () => {
+      graph.value?.removeCell(props.cell)
+      ctx.emit('removeTasks', [Number(props.cell?.id)])
+    }
+
+    onMounted(() => {
+      document.addEventListener('click', () => {
+        hide()
+      })
+    })
+
+    return {
+      startAvailable,
+      startRunning,
+      handleEdit,
+      handleCopy,
+      handleDelete
+    }
+  },
+  render() {
+    const { t } = useI18n()
+
+    return (
+      this.visible && (
+        <div
+          class={styles['dag-context-menu']}
+          style={{ left: `${this.left}px`, top: `${this.top}px` }}
+        >
+          <div
+            class={`${styles['menu-item']} ${
+              !this.startAvailable ? styles['disabled'] : ''
+            } `}
+            onClick={this.startRunning}
+          >
+            {t('project.node.start')}
+          </div>
+          <div
+            class={`${styles['menu-item']} ${
+              this.releaseState === 'ONLINE' ? styles['disabled'] : ''
+            } `}
+            onClick={this.handleEdit}
+          >
+            {t('project.node.edit')}
+          </div>
+          <div
+            class={`${styles['menu-item']} ${
+              this.releaseState === 'ONLINE' ? styles['disabled'] : ''
+            } `}
+            onClick={this.handleCopy}
+          >
+            {t('project.node.copy')}
+          </div>
+          <div
+            class={`${styles['menu-item']} ${
+              this.releaseState === 'ONLINE' ? styles['disabled'] : ''
+            } `}
+            onClick={this.handleDelete}
+          >
+            {t('project.node.delete')}
+          </div>
+          {/* TODO: view log */}
+        </div>
+      )
+    )
+  }
+})
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts
index 3c69127..55b6e42 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts
@@ -26,6 +26,7 @@ import { useCustomCellBuilder } from './use-custom-cell-builder'
 import { useGraphBackfill } from './use-graph-backfill'
 import { useDagDragAndDrop } from './use-dag-drag-drop'
 import { useTaskEdit } from './use-task-edit'
+import { useNodeMenu } from './use-node-menu'
 
 export {
   useCanvasInit,
@@ -38,5 +39,6 @@ export {
   useGraphBackfill,
   useCellUpdate,
   useDagDragAndDrop,
-  useTaskEdit
+  useTaskEdit,
+  useNodeMenu
 }
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx
index 9fbbc30..e43076d 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx
+++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/index.tsx
@@ -27,13 +27,16 @@ import {
   useGraphBackfill,
   useDagDragAndDrop,
   useTaskEdit,
-  useBusinessMapper
+  useBusinessMapper,
+  useNodeMenu
 } from './dag-hooks'
 import { useThemeStore } from '@/store/theme/theme'
 import VersionModal from '../../definition/components/version-modal'
 import { WorkflowDefinition } from './types'
 import DagSaveModal from './dag-save-modal'
 import TaskModal from '@/views/projects/task/components/node/detail-modal'
+import StartModal from '@/views/projects/workflow/definition/components/start-modal'
+import ContextMenuItem from './dag-context-menu'
 import './x6-style.scss'
 
 const props = {
@@ -82,10 +85,25 @@ export default defineComponent({
       currTask,
       taskCancel,
       appendTask,
+      editTask,
+      copyTask,
       taskDefinitions,
       removeTasks
     } = useTaskEdit({ graph, definition: toRef(props, 'definition') })
 
+    // Right click cell
+    const {
+      menuCell,
+      pageX,
+      pageY,
+      menuVisible,
+      startModalShow,
+      menuHide,
+      menuStart
+    } = useNodeMenu({
+      graph
+    })
+
     const { onDragStart, onDrop } = useDagDragAndDrop({
       graph,
       readonly: toRef(props, 'readonly'),
@@ -177,6 +195,24 @@ export default defineComponent({
           onSubmit={taskConfirm}
           onCancel={taskCancel}
         />
+        <ContextMenuItem
+          cell={menuCell.value}
+          visible={menuVisible.value}
+          left={pageX.value}
+          top={pageY.value}
+          releaseState={props.definition?.processDefinition.releaseState}
+          onHide={menuHide}
+          onStart={menuStart}
+          onEdit={editTask}
+          onCopyTask={copyTask}
+          onRemoveTasks={removeTasks}
+        />
+        {!!props.definition && (
+          <StartModal
+            v-model:row={props.definition.processDefinition}
+            v-model:show={startModalShow.value}
+          />
+        )}
       </div>
     )
   }
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/menu.module.scss
similarity index 50%
copy from dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts
copy to dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/menu.module.scss
index 3c69127..b4d5ce1 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/dag-hooks.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/menu.module.scss
@@ -15,28 +15,29 @@
  * limitations under the License.
  */
 
-import { useCanvasInit } from './use-canvas-init'
-import { useBusinessMapper } from './use-business-mapper'
-import { useCellActive } from './use-cell-active'
-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'
-import { useTaskEdit } from './use-task-edit'
+ .dag-context-menu{
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100px;
+  background-color: #ffffff;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.12);
 
-export {
-  useCanvasInit,
-  useBusinessMapper,
-  useCellActive,
-  useNodeSearch,
-  useGraphAutoLayout,
-  useTextCopy,
-  useCustomCellBuilder,
-  useGraphBackfill,
-  useCellUpdate,
-  useDagDragAndDrop,
-  useTaskEdit
+  .menu-item{
+    padding: 5px 10px;
+    border-bottom: solid 1px #f2f3f7;
+    cursor: pointer;
+    color: rgb(89, 89, 89);
+    font-size: 12px;
+
+    &:hover:not(.disabled){
+      color: #262626;
+      background-color: #f5f5f5; 
+    }
+
+    &.disabled{
+      cursor: not-allowed;
+      color: rgba(89, 89, 89, .4);
+    }
+  }
 }
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-canvas-init.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-canvas-init.ts
index ebba1d8..6282acf 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-canvas-init.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-canvas-init.ts
@@ -21,6 +21,7 @@ import { Graph } from '@antv/x6'
 import { NODE, EDGE, X6_NODE_NAME, X6_EDGE_NAME } from './dag-config'
 import { debounce } from 'lodash'
 import { useResizeObserver } from '@vueuse/core'
+import ContextMenuTool from './dag-context-menu'
 
 interface Options {
   readonly: Ref<boolean>
@@ -45,6 +46,8 @@ export function useCanvasInit(options: Options) {
    * Graph Init, bind graph to the dom
    */
   function graphInit() {
+    Graph.registerNodeTool('contextmenu', ContextMenuTool, true)
+
     return new Graph({
       container: paper.value,
       selecting: {
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-menu.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-menu.ts
new file mode 100644
index 0000000..df66c3e
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-node-menu.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { onMounted, ref } from 'vue'
+import type { Graph, Cell } from '@antv/x6'
+
+interface Options {
+  graph: Ref<Graph | undefined>
+}
+
+/**
+ * Get position of the right-clicked Cell.
+ */
+export function useNodeMenu(options: Options) {
+  const { graph } = options
+  const startModalShow = ref(false)
+  const menuVisible = ref(false)
+  const pageX = ref()
+  const pageY = ref()
+  const menuCell = ref<Cell>()
+
+  const menuHide = () => {
+    menuVisible.value = false
+
+    // unlock scroller
+    graph.value?.unlockScroller()
+  }
+
+  const menuStart = () => {
+    startModalShow.value = true
+  }
+
+  onMounted(() => {
+    if (graph.value) {
+      // contextmenu
+      graph.value.on('node:contextmenu', ({ cell, x, y }) => {
+        menuCell.value = cell
+        const data = graph.value?.localToPage(x, y)
+        pageX.value = data?.x
+        pageY.value = data?.y
+
+        // show menu
+        menuVisible.value = true
+
+        // lock scroller
+        graph.value?.lockScroller()
+      })
+    }
+  })
+
+  return {
+    pageX,
+    pageY,
+    startModalShow,
+    menuVisible,
+    menuCell,
+    menuHide,
+    menuStart
+  }
+}
diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-task-edit.ts b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-task-edit.ts
index d4f91c0..d2bc42e 100644
--- a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-task-edit.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/use-task-edit.ts
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 
+import _ from 'lodash'
 import { ref, onMounted, watch } from 'vue'
 import type { Ref } from 'vue'
 import type { Graph } from '@antv/x6'
@@ -61,6 +62,28 @@ export function useTaskEdit(options: Options) {
   }
 
   /**
+   * Copy a task
+   */
+  function copyTask(
+    name: string,
+    code: number,
+    targetCode: number,
+    type: TaskType,
+    coordinate: Coordinate
+  ) {
+    addNode(code + '', type, name, coordinate)
+    const definition = taskDefinitions.value.find((t) => t.code === targetCode)
+
+    const newDefinition = {
+      ...definition,
+      code,
+      name
+    } as NodeData
+
+    taskDefinitions.value.push(newDefinition)
+  }
+
+  /**
    * Remove task
    * @param {number} code
    */
@@ -76,6 +99,18 @@ export function useTaskEdit(options: Options) {
   }
 
   /**
+   * Edit task
+   * @param {number} code
+   */
+  function editTask(code: number) {
+    const definition = taskDefinitions.value.find((t) => t.code === code)
+    if (definition) {
+      currTask.value = definition
+    }
+    taskModalVisible.value = true
+  }
+
+  /**
    * The confirm event in task config modal
    * @param formRef
    * @param from
@@ -108,11 +143,7 @@ export function useTaskEdit(options: Options) {
     if (graph.value) {
       graph.value.on('cell:dblclick', ({ cell }) => {
         const code = Number(cell.id)
-        const definition = taskDefinitions.value.find((t) => t.code === code)
-        if (definition) {
-          currTask.value = definition
-        }
-        taskModalVisible.value = true
+        editTask(code)
       })
     }
   })
@@ -127,6 +158,8 @@ export function useTaskEdit(options: Options) {
     taskConfirm,
     taskCancel,
     appendTask,
+    editTask,
+    copyTask,
     taskDefinitions,
     removeTasks
   }