[Feature][UI Next] Add dag menu (#8481)

* add dag menu

* add dag menu click event

* workflow online edit not allowed
This commit is contained in:
Devosend 2022-02-22 15:10:45 +08:00 committed by GitHub
parent 2a844dcc67
commit 66241fd5c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 395 additions and 10 deletions

View File

@ -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'
}
}

View File

@ -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: '删除'
}
}

View File

@ -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
}

View File

@ -200,7 +200,12 @@ export const NODE = {
group: X6_PORT_OUT_NAME
}
]
}
},
tools: [
{
name: 'contextmenu'
}
]
}
export const NODE_HOVER = {

View File

@ -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>
)
)
}
})

View File

@ -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
}

View File

@ -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>
)
}

View File

@ -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);
}
}
}

View File

@ -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: {

View File

@ -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
}
}

View File

@ -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
}