diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts index 6c91e5cb4f..7597e4a30f 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 a503c76830..10b5a39627 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 68766c9407..23ca932ca6 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 5f73e2f1b0..b84adeae46 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 0000000000..0681d54993 --- /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, + require: true + }, + visible: { + type: Boolean as PropType, + default: true + }, + left: { + type: Number as PropType, + default: 0 + }, + top: { + type: Number as PropType, + default: 0 + }, + releaseState: { + type: String as PropType, + 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 && ( +
+
+ {t('project.node.start')} +
+
+ {t('project.node.edit')} +
+
+ {t('project.node.copy')} +
+
+ {t('project.node.delete')} +
+ {/* TODO: view log */} +
+ ) + ) + } +}) 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 3c691275d3..55b6e42ffc 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 9fbbc30ab2..e43076db06 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} /> + + {!!props.definition && ( + + )} ) } diff --git a/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/menu.module.scss b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/menu.module.scss new file mode 100644 index 0000000000..b4d5ce196d --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/projects/workflow/components/dag/menu.module.scss @@ -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. + */ + + .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); + + .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 ebba1d8b2f..6282acfaed 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 @@ -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 0000000000..df66c3e396 --- /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 +} + +/** + * 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() + + 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 d4f91c09f2..d2bc42ebac 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' @@ -60,6 +61,28 @@ export function useTaskEdit(options: Options) { openTaskModal({ code, taskType: type, name: '' }) } + /** + * 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 @@ -75,6 +98,18 @@ export function useTaskEdit(options: Options) { taskModalVisible.value = true } + /** + * 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 @@ -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 }