This is an automated email from the ASF dual-hosted git repository. nicholasjiang pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/incubator-paimon-webui.git
The following commit(s) were added to refs/heads/main by this push: new ee45ba6 [Feature] Introduce CDC module (#93) ee45ba6 is described below commit ee45ba6f94ff7f09e502121828d6dc2fe4bbdc7a Author: labbomb <739955...@qq.com> AuthorDate: Tue Nov 7 13:08:55 2023 +0800 [Feature] Introduce CDC module (#93) --- .../src/components/dynamic-form/types.ts | 2 +- paimon-web-ui-new/src/components/modal/index.tsx | 6 +- .../src/form-lib/{source => cdc}/use-cdc-list.ts | 3 +- paimon-web-ui-new/src/form-lib/cdc/use-mysql.ts | 186 +++++++++++++++++++++ paimon-web-ui-new/src/form-lib/cdc/use-paimon.ts | 155 +++++++++++++++++ paimon-web-ui-new/src/form-lib/index.ts | 8 +- paimon-web-ui-new/src/locales/en/modules/cdc.ts | 15 ++ paimon-web-ui-new/src/locales/zh/modules/cdc.ts | 15 ++ .../src/{form-lib => store/cdc}/index.ts | 24 ++- .../dag/{dag-canvas.tsx => context-menu.tsx} | 40 +++-- .../src/views/cdc/components/dag/custom-node.tsx | 4 +- .../src/views/cdc/components/dag/dag-canvas.tsx | 77 ++++++++- .../src/views/cdc/components/dag/dag-slider.tsx | 10 +- .../src/views/cdc/components/dag/drawer.tsx | 176 +++++++++++++++++++ .../src/views/cdc/components/dag/index.module.scss | 13 +- .../src/views/cdc/components/dag/index.tsx | 39 ++++- .../views/cdc/components/dag/use-canvas-init.ts | 50 ++++-- .../src/views/cdc/components/list/index.tsx | 169 ++++++++++++++++++- paimon-web-ui-new/src/views/cdc/index.tsx | 31 +++- 19 files changed, 957 insertions(+), 66 deletions(-) diff --git a/paimon-web-ui-new/src/components/dynamic-form/types.ts b/paimon-web-ui-new/src/components/dynamic-form/types.ts index 6ee0c6a..56cd0e9 100644 --- a/paimon-web-ui-new/src/components/dynamic-form/types.ts +++ b/paimon-web-ui-new/src/components/dynamic-form/types.ts @@ -40,7 +40,7 @@ interface IJsonItemParams { value?: any props?: any options?: IOption[] | Ref<IOption[]> - span?: number + span?: number | Ref<number> children?: IJsonItem[] validate?: IFormItemRule slots?: object diff --git a/paimon-web-ui-new/src/components/modal/index.tsx b/paimon-web-ui-new/src/components/modal/index.tsx index 211c05f..e65d17f 100644 --- a/paimon-web-ui-new/src/components/modal/index.tsx +++ b/paimon-web-ui-new/src/components/modal/index.tsx @@ -52,7 +52,7 @@ export default defineComponent({ setup(props, { expose, emit }) { const { t } = useLocaleHooks() const formRef = ref() - expose({formRef}) + expose({ formRef }) const { elementsRef, rulesRef, model } = useTask({ data: props.row, @@ -60,7 +60,7 @@ export default defineComponent({ }) const handleConfirm = () => { - emit('confirm') + emit('confirm', model) } const handleCancel = () => { @@ -94,7 +94,7 @@ export default defineComponent({ {{ default: () => ( <Form - ref={this.formRef} + ref='formRef' meta={{ model: this.model, rules: this.rulesRef, diff --git a/paimon-web-ui-new/src/form-lib/source/use-cdc-list.ts b/paimon-web-ui-new/src/form-lib/cdc/use-cdc-list.ts similarity index 97% rename from paimon-web-ui-new/src/form-lib/source/use-cdc-list.ts rename to paimon-web-ui-new/src/form-lib/cdc/use-cdc-list.ts index 6129a2c..59abe7d 100644 --- a/paimon-web-ui-new/src/form-lib/source/use-cdc-list.ts +++ b/paimon-web-ui-new/src/form-lib/cdc/use-cdc-list.ts @@ -69,7 +69,8 @@ export function useCDCList(item:any) { type: 'radio', field: 'synchronizationType', name: t('cdc.synchronization_type'), - options: synchronizationTypeOptions + options: synchronizationTypeOptions, + value: 0, }, ] as IJsonItem[], model } diff --git a/paimon-web-ui-new/src/form-lib/cdc/use-mysql.ts b/paimon-web-ui-new/src/form-lib/cdc/use-mysql.ts new file mode 100644 index 0000000..d47eab5 --- /dev/null +++ b/paimon-web-ui-new/src/form-lib/cdc/use-mysql.ts @@ -0,0 +1,186 @@ +/* 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 { IJsonItem } from "@/components/dynamic-form/types" + +export function useMYSQL(item:any) { + const { t } = useLocaleHooks() + + const tabType = item.data.tabType + console.log('item', item.data) + const data = item.data + + const model = reactive({ + host: data.host || '', + port: data.port || '', + username: data.username || '', + password: data.password || '', + other_configs: data.other_configs || '', + database: data.database || '', + table_name: data.table_name || '', + type_mapping: data.type_mapping || '', + metadata_column: data.metadata_column || '', + computed_column: data.computed_column || '', + }) + + const TypeMappingOptions = [] as any + + return { + json: [ + { + type: 'input', + field: 'host', + name: t('cdc.host_name_and_ip_address'), + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'connection_information' ? 24 : 0), + validate: { + trigger: ['input', 'blur'], + required: true, + message: 'error', + validator: (validator: any, value: string) => { + if (!value) { + return new Error('error') + } + } + } + }, + { + type: 'input', + field: 'port', + name: t('cdc.port'), + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'connection_information' ? 24 : 0), + }, + { + type: 'input', + field: 'username', + name: t('cdc.username'), + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'connection_information' ? 24 : 0), + validate: { + trigger: ['input', 'blur'], + required: true, + message: 'error', + validator: (validator: any, value: string) => { + if (!value) { + return new Error('error') + } + } + } + }, + { + type: 'input', + field: 'password', + name: t('cdc.password'), + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'connection_information' ? 24 : 0), + validate: { + trigger: ['input', 'blur'], + required: true, + message: 'error', + validator: (validator: any, value: string) => { + if (!value) { + return new Error('error') + } + } + } + }, + { + type: 'input', + field: 'other_configs', + name: t('cdc.other_configs'), + span: computed(() => tabType.value === 'connection_information' ? 24 : 0), + props: { + placeholder: '', + type: 'textarea', + } + }, + { + type: 'input', + field: 'database', + name: t('cdc.database'), + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + validate: { + trigger: ['input', 'blur'], + required: true, + message: 'error', + validator: (validator: any, value: string) => { + if (!value) { + return new Error('error') + } + } + } + }, + { + type: 'input', + field: 'table_name', + name: t('cdc.table_name'), + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + validate: { + trigger: ['input', 'blur'], + required: true, + message: 'error', + validator: (validator: any, value: string) => { + if (!value) { + return new Error('error') + } + } + } + }, + { + type: 'select', + field: 'type_mapping', + name: t('cdc.type_mapping'), + options: TypeMappingOptions, + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + }, + { + type: 'input', + field: 'metadata_column', + name: t('cdc.metadata_column'), + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + props: { + placeholder: '' + } + }, + { + type: 'input', + field: 'computed_column', + name: t('cdc.computed_column'), + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + props: { + placeholder: '', + type: 'textarea', + } + }, + ] as IJsonItem[], model + } +} diff --git a/paimon-web-ui-new/src/form-lib/cdc/use-paimon.ts b/paimon-web-ui-new/src/form-lib/cdc/use-paimon.ts new file mode 100644 index 0000000..10e0daf --- /dev/null +++ b/paimon-web-ui-new/src/form-lib/cdc/use-paimon.ts @@ -0,0 +1,155 @@ +/* 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 { IJsonItem } from "@/components/dynamic-form/types" + +export function usePaimon(item:any) { + const { t } = useLocaleHooks() + + const tabType = item.data.tabType + console.log('item', item.data) + + const model = reactive({ + warehouse: '', + metastore: '', + url: '', + other_configs: '', + database: '', + table_name: '', + primary_key: '', + partition_column: '', + other_configs2: '', + }) + + const TypeMappingOptions = [] as any + + return { + json: [ + { + type: 'input', + field: 'warehouse', + name: 'Warehouse', + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'catalog_configuration' ? 24 : 0), + validate: { + trigger: ['input', 'blur'], + required: true, + message: 'error', + validator: (validator: any, value: string) => { + if (!value) { + return new Error('error') + } + } + } + }, + { + type: 'select', + field: 'metastore', + name: 'Metastore', + options: TypeMappingOptions, + span: computed(() => tabType.value === 'catalog_configuration' ? 24 : 0), + }, + { + type: 'input', + field: 'url', + name: 'Url', + span: computed(() => tabType.value === 'catalog_configuration' ? 24 : 0), + props: { + placeholder: '' + } + }, + { + type: 'input', + field: 'other_configs', + name: t('cdc.other_configs'), + span: computed(() => tabType.value === 'catalog_configuration' ? 24 : 0), + props: { + placeholder: '', + type: 'textarea', + } + }, + { + type: 'input', + field: 'database', + name: t('cdc.database'), + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + validate: { + trigger: ['input', 'blur'], + required: true, + message: 'error', + validator: (validator: any, value: string) => { + if (!value) { + return new Error('error') + } + } + } + }, + { + type: 'input', + field: 'table_name', + name: t('cdc.table_name'), + props: { + placeholder: '' + }, + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + validate: { + trigger: ['input', 'blur'], + required: true, + message: 'error', + validator: (validator: any, value: string) => { + if (!value) { + return new Error('error') + } + } + } + }, + { + type: 'input', + field: 'primary_key', + name: t('cdc.primary_key'), + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + props: { + placeholder: '' + } + }, + { + type: 'input', + field: 'partition_column', + name: t('cdc.partition_column'), + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + props: { + placeholder: '' + } + }, + { + type: 'input', + field: 'other_configs2', + name: t('cdc.other_configs'), + span: computed(() => tabType.value === 'synchronization_configuration' ? 24 : 0), + props: { + placeholder: '', + type: 'textarea', + } + }, + ] as IJsonItem[], model + } +} diff --git a/paimon-web-ui-new/src/form-lib/index.ts b/paimon-web-ui-new/src/form-lib/index.ts index 8d366d9..e505e13 100644 --- a/paimon-web-ui-new/src/form-lib/index.ts +++ b/paimon-web-ui-new/src/form-lib/index.ts @@ -15,8 +15,12 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { useCDCList } from "./source/use-cdc-list"; +import { useCDCList } from "./cdc/use-cdc-list"; +import { useMYSQL } from './cdc/use-mysql' +import { usePaimon } from "./cdc/use-paimon"; export default { - CDCLIST: useCDCList + CDCLIST: useCDCList, + MYSQL: useMYSQL, + PAIMON: usePaimon } diff --git a/paimon-web-ui-new/src/locales/en/modules/cdc.ts b/paimon-web-ui-new/src/locales/en/modules/cdc.ts index 5d51dcc..2ae4a73 100644 --- a/paimon-web-ui-new/src/locales/en/modules/cdc.ts +++ b/paimon-web-ui-new/src/locales/en/modules/cdc.ts @@ -34,4 +34,19 @@ export default { single_table_synchronization: 'Single Table Synchronization', whole_database_synchronization: 'Whole Database Synchronization', save: 'Save', + host_name_and_ip_address: 'Host Name/IP Address', + port: 'Port', + username: 'Username', + password: 'Password', + database: 'Database', + table_name: 'Table Name', + other_configs: 'Other Configs', + type_mapping: 'Type Mapping', + metadata_column: 'Metadata Column', + computed_column: 'Computed Column', + connection_information: 'Connection Information', + catalog_configuration: 'Catalog Configuration', + synchronization_configuration: 'Synchronization Configuration', + primary_key: 'Primary Key', + partition_column: 'Partition Column', } diff --git a/paimon-web-ui-new/src/locales/zh/modules/cdc.ts b/paimon-web-ui-new/src/locales/zh/modules/cdc.ts index bf7e738..c95dd5d 100644 --- a/paimon-web-ui-new/src/locales/zh/modules/cdc.ts +++ b/paimon-web-ui-new/src/locales/zh/modules/cdc.ts @@ -34,4 +34,19 @@ export default { single_table_synchronization: '单表同步', whole_database_synchronization: '整库同步', save: '保存', + host_name_and_ip_address: '主机名/IP地址', + port: '端口', + username: '用户名', + password: '密码', + database: '数据库', + table_name: '表名', + other_configs: '其他配置', + type_mapping: '类型映射', + metadata_column: '元数据列', + computed_column: '计算列', + connection_information: '连接信息', + catalog_configuration: 'Catalog配置', + synchronization_configuration: '同步配置', + primary_key: '主键', + partition_column: '分区列', } diff --git a/paimon-web-ui-new/src/form-lib/index.ts b/paimon-web-ui-new/src/store/cdc/index.ts similarity index 70% copy from paimon-web-ui-new/src/form-lib/index.ts copy to paimon-web-ui-new/src/store/cdc/index.ts index 8d366d9..42b5c57 100644 --- a/paimon-web-ui-new/src/form-lib/index.ts +++ b/paimon-web-ui-new/src/store/cdc/index.ts @@ -15,8 +15,24 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { useCDCList } from "./source/use-cdc-list"; - -export default { - CDCLIST: useCDCList +export interface CDCState { + model: object } + +export const useCDCStore = defineStore({ + id: 'cdc', + state: (): CDCState => ({ + model: {} + }), + persist: true, + getters: { + getModel(): any { + return this.model + } + }, + actions: { + setModel(model: object): void { + this.model = model + } + } +}) diff --git a/paimon-web-ui-new/src/views/cdc/components/dag/dag-canvas.tsx b/paimon-web-ui-new/src/views/cdc/components/dag/context-menu.tsx similarity index 63% copy from paimon-web-ui-new/src/views/cdc/components/dag/dag-canvas.tsx copy to paimon-web-ui-new/src/views/cdc/components/dag/context-menu.tsx index ede6799..0f1df6f 100644 --- a/paimon-web-ui-new/src/views/cdc/components/dag/dag-canvas.tsx +++ b/paimon-web-ui-new/src/views/cdc/components/dag/context-menu.tsx @@ -15,34 +15,40 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { useCanvasInit } from './use-canvas-init' +import { NButton } from 'naive-ui' import styles from './index.module.scss' -import DagSlider from './dag-slider' export default defineComponent({ - name: 'DagCanvasPage', - setup() { + name: 'ContextMenuTool', + emits: ['delete'], + props: { + x: { + type: Number, + default: 0 + }, + y: { + type: Number, + default: 0 + } + }, + setup(props, { emit }) { const { t } = useLocaleHooks() - - const { graph, dnd } = useCanvasInit() + const handleDelete = () => { + emit('delete') + } return { t, - graph, - dnd + handleDelete } }, render() { return ( - <div class={styles.dag}> - <div - class={styles['dag-container']} - id="dag-container" - /> - <DagSlider - graph={this.graph} - dnd={this.dnd} - /> + <div + class={styles['context-menu']} + style={{ left: `${this.x}px`, top: `${this.y}px` }} + > + <NButton type='primary' style={'width: 100%'} onClick={this.handleDelete}>{this.t('cdc.delete')}</NButton> </div> ) } diff --git a/paimon-web-ui-new/src/views/cdc/components/dag/custom-node.tsx b/paimon-web-ui-new/src/views/cdc/components/dag/custom-node.tsx index d90682c..118d84c 100644 --- a/paimon-web-ui-new/src/views/cdc/components/dag/custom-node.tsx +++ b/paimon-web-ui-new/src/views/cdc/components/dag/custom-node.tsx @@ -15,6 +15,7 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import { NButton } from 'naive-ui' import styles from './index.module.scss' export default defineComponent({ @@ -23,6 +24,7 @@ export default defineComponent({ const getNode = inject('getNode') as any const node = getNode() const data = node.data + const onMainMouseEnter = () => { const ports = node.getPorts() || [] ports.forEach((port: { id: any }) => { @@ -56,7 +58,7 @@ export default defineComponent({ onMouseenter={this.onMainMouseEnter} onMouseleave={this.onMainMouseLeave} > - {this.data.name} + <NButton text>{this.data.name}</NButton> </div> ) } diff --git a/paimon-web-ui-new/src/views/cdc/components/dag/dag-canvas.tsx b/paimon-web-ui-new/src/views/cdc/components/dag/dag-canvas.tsx index ede6799..b9ef7c3 100644 --- a/paimon-web-ui-new/src/views/cdc/components/dag/dag-canvas.tsx +++ b/paimon-web-ui-new/src/views/cdc/components/dag/dag-canvas.tsx @@ -18,18 +18,73 @@ under the License. */ import { useCanvasInit } from './use-canvas-init' import styles from './index.module.scss' import DagSlider from './dag-slider' +import Drawer from './drawer' +import ContextMenuTool from './context-menu' export default defineComponent({ name: 'DagCanvasPage', - setup() { + setup(props, { expose }) { const { t } = useLocaleHooks() const { graph, dnd } = useCanvasInit() + const nodeVariables = reactive({ + x: 0, + y: 0, + row: {} as any, + cell: {} as any, + showDrawer: false, + showContextMenu: false + }) + + onMounted(() => { + if (graph.value) { + graph.value.on('node:dblclick', ({ node }) => { + nodeVariables.showDrawer = true + nodeVariables.row = node.data + }) + graph.value.on('node:contextmenu', ({ e, node }) => { + nodeVariables.showContextMenu = true + nodeVariables.row = node.data + nodeVariables.x = e.clientX - 20 + nodeVariables.y = e.clientY - 178 + }) + graph.value.on('blank:click', () => { + nodeVariables.showContextMenu = false + }) + } + }) + + const handleNodeConfirm = (model: any) => { + if (graph.value) { + nodeVariables.cell = graph.value.getCellById(nodeVariables.row.name) + if (nodeVariables.cell) { + nodeVariables.cell.data = { + ...nodeVariables.cell.data, + ...model + } + } + } + nodeVariables.showDrawer = false + } + + const handleDelete = () => { + nodeVariables.showContextMenu = false + graph.value?.removeNode(nodeVariables.row.name) + } + + expose({ + graph, + dnd + }) + return { t, graph, - dnd + dnd, + handleNodeConfirm, + handleDelete, + ...toRefs(nodeVariables) } }, render() { @@ -43,6 +98,24 @@ export default defineComponent({ graph={this.graph} dnd={this.dnd} /> + { + this.showDrawer && + <Drawer + showDrawer={this.showDrawer} + formType={this.row.value || 'MYSQL'} + onConfirm={this.handleNodeConfirm} + onCancel={() => this.showDrawer = false} + row={this.row} + /> + } + { + this.showContextMenu && + <ContextMenuTool + onDelete={this.handleDelete} + x={this.x} + y={this.y} + /> + } </div> ) } diff --git a/paimon-web-ui-new/src/views/cdc/components/dag/dag-slider.tsx b/paimon-web-ui-new/src/views/cdc/components/dag/dag-slider.tsx index 982a474..9961d31 100644 --- a/paimon-web-ui-new/src/views/cdc/components/dag/dag-slider.tsx +++ b/paimon-web-ui-new/src/views/cdc/components/dag/dag-slider.tsx @@ -36,29 +36,29 @@ export default defineComponent({ sourceList: [ { name: 'MySQL', - value: 'mysql', + value: 'MYSQL', type: 'INPUT' }, { name: 'Kafka', - value: 'kafka', + value: 'KAFKA', type: 'INPUT' }, { name: 'MongoDB', - value: 'mongodb', + value: 'MONGODB', type: 'INPUT' }, { name: 'PostgreSQL', - value: 'postgresql', + value: 'POSTGRESQL', type: 'INPUT' } ], sinkList: [ { name: 'Paimon', - value: 'paimon', + value: 'PAIMON', type: 'OUTPUT' } ] diff --git a/paimon-web-ui-new/src/views/cdc/components/dag/drawer.tsx b/paimon-web-ui-new/src/views/cdc/components/dag/drawer.tsx new file mode 100644 index 0000000..f116261 --- /dev/null +++ b/paimon-web-ui-new/src/views/cdc/components/dag/drawer.tsx @@ -0,0 +1,176 @@ +/* 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 Form from "@/components/dynamic-form" +import { useTask } from "@/components/modal/use-task" + +const props = { + title: { + type: String as PropType<string>, + default: '' + }, + row: { + type: Object as PropType<any>, + default: () => {} + }, + showDrawer: { + type: Boolean as PropType<boolean>, + default: false + }, + autoFocus: { + type: Boolean as PropType<boolean>, + default: false + }, + closeable: { + type: Boolean as PropType<boolean>, + default: true + }, + formType: { + type: String as PropType<string>, + default: '' + }, +} + +export default defineComponent({ + name: 'DrawerPage', + props, + emits: ['confirm', 'cancel'], + setup(props, { expose, emit }) { + const { t } = useLocaleHooks() + const formRef = ref() + expose({formRef}) + + const chooseTab = ref('connection_information') + + const { elementsRef, rulesRef, model } = useTask({ + data: { + ...props.row, + tabType: chooseTab + }, + formType: props.formType + }) + + const handleConfirm = () => { + emit('confirm', model) + } + + const handleCancel = () => { + emit('cancel') + } + + + watch( + () => props.row.type, + (val) => { + if (val === 'INPUT') { + chooseTab.value = 'connection_information' + } else if (val === 'OUTPUT') { + chooseTab.value = 'catalog_configuration' + } + }, + { + immediate: true + } + ) + + return { + t, + handleConfirm, + handleCancel, + formRef, + elementsRef, + rulesRef, + model, + chooseTab + } + }, + render () { + return ( + <n-drawer + v-model:show={this.showDrawer} + mask-closable={false} + auto-focus={this.autoFocus} + default-width="502" + resizable + > + <n-drawer-content> + {{ + default: () => ( + <n-tabs type="line" v-model:value={this.chooseTab}> + { + this.row.type === 'INPUT' && + <n-tab-pane name="connection_information" tab={this.t('cdc.connection_information')}> + <Form + ref={this.formRef} + meta={{ + model: this.model, + rules: this.rulesRef, + elements: this.elementsRef, + }} + gridProps={{ + xGap: 10 + }} + /> + </n-tab-pane> + } + { + this.row.type === 'OUTPUT' && + <n-tab-pane name="catalog_configuration" tab={this.t('cdc.catalog_configuration')}> + <Form + ref={this.formRef} + meta={{ + model: this.model, + rules: this.rulesRef, + elements: this.elementsRef, + }} + gridProps={{ + xGap: 10 + }} + /> + </n-tab-pane> + } + <n-tab-pane name="synchronization_configuration" tab={this.t('cdc.synchronization_configuration')}> + <Form + ref={this.formRef} + meta={{ + model: this.model, + rules: this.rulesRef, + elements: this.elementsRef, + }} + gridProps={{ + xGap: 10 + }} + /> + </n-tab-pane> + </n-tabs> + ), + footer: () => ( + <n-space justify='end'> + <n-button onClick={this.handleCancel}> + {this.t('layout.cancel')} + </n-button> + <n-button type='primary' onClick={this.handleConfirm}> + {this.t('layout.confirm')} + </n-button> + </n-space> + ) + }} + </n-drawer-content> + </n-drawer> + ) + } +}) diff --git a/paimon-web-ui-new/src/views/cdc/components/dag/index.module.scss b/paimon-web-ui-new/src/views/cdc/components/dag/index.module.scss index d1dd006..77658b5 100644 --- a/paimon-web-ui-new/src/views/cdc/components/dag/index.module.scss +++ b/paimon-web-ui-new/src/views/cdc/components/dag/index.module.scss @@ -15,10 +15,14 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -.title { +.title-bar { width: 100%; display: flex; justify-content: space-between; + + .title { + cursor: pointer; + } } .dag { @@ -66,3 +70,10 @@ under the License. */ border-radius: 4px; box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06); } + + +.context-menu { + position: absolute; + width: 100px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); +} diff --git a/paimon-web-ui-new/src/views/cdc/components/dag/index.tsx b/paimon-web-ui-new/src/views/cdc/components/dag/index.tsx index 8ea45a0..073ec6b 100644 --- a/paimon-web-ui-new/src/views/cdc/components/dag/index.tsx +++ b/paimon-web-ui-new/src/views/cdc/components/dag/index.tsx @@ -18,14 +18,44 @@ under the License. */ import { Leaf, Save } from "@vicons/ionicons5" import styles from './index.module.scss'; import DagCanvas from "./dag-canvas"; +import { useCDCStore } from "@/store/cdc"; +import type { Router } from "vue-router"; export default defineComponent({ name: 'DagPage', setup() { const { t } = useLocaleHooks() + const CDCStore = useCDCStore() + const title = ref('') + onMounted(() => { + title.value = CDCStore.getModel.name + }) + + const dagRef = ref() as any + const handleSave = () => { + // console.log('dagRef', dagRef.value.graph.toJSON()) + router.push({ path: '/cdc_ingestion' }) + } + + const router: Router = useRouter() + const handleJump = () => { + router.push({ path: '/cdc_ingestion' }) + } + + onMounted(() => { + if (dagRef.value && dagRef.value.graph) { + dagRef.value.graph.fromJSON({ + cells: CDCStore.getModel.cells + }) + } + }) return { t, + title, + handleSave, + dagRef, + handleJump } }, render() { @@ -33,10 +63,12 @@ export default defineComponent({ <n-card> <n-space vertical size={24}> <n-card> - <div class={styles.title}> + <div class={styles['title-bar']}> <n-space align="center"> <n-icon component={Leaf} color="#2F7BEA" size="18" /> - <span>{this.t('cdc.synchronization_job_definition')}</span> + <span class={styles.title} onClick={this.handleJump}>{this.t('cdc.synchronization_job_definition')} { + this.title ? ` - ${this.title}` : '' + }</span> </n-space> <div class={styles.operation}> <n-space> @@ -44,6 +76,7 @@ export default defineComponent({ v-slots={{ trigger: () => ( <n-button + onClick={this.handleSave} v-slots={{ icon: () => <n-icon component={Save}></n-icon> }} @@ -57,7 +90,7 @@ export default defineComponent({ </div> </div> </n-card> - <DagCanvas></DagCanvas> + <DagCanvas ref='dagRef'></DagCanvas> </n-space> </n-card> ) diff --git a/paimon-web-ui-new/src/views/cdc/components/dag/use-canvas-init.ts b/paimon-web-ui-new/src/views/cdc/components/dag/use-canvas-init.ts index 59791dc..36526eb 100644 --- a/paimon-web-ui-new/src/views/cdc/components/dag/use-canvas-init.ts +++ b/paimon-web-ui-new/src/views/cdc/components/dag/use-canvas-init.ts @@ -21,29 +21,29 @@ import { register } from '@antv/x6-vue-shape' import CustomNode from './custom-node' import { EDGE, PORT } from './node-config' -register({ - shape: 'custom-node', - width: 150, - height: 40, - component: CustomNode, - ports: { - ...PORT - } -}) - export function useCanvasInit() { const graph = ref<Graph>() const dnd = ref<Dnd>() const graphInit = () => { + register({ + shape: 'custom-node', + width: 150, + height: 40, + component: CustomNode, + ports: { + ...PORT + } + }) + return new Graph({ container: document.getElementById('dag-container') || undefined, // background: { // color: '#F2F7FA', // }, autoResize: true, - panning: true, - mousewheel: true, + panning: false, + mousewheel: false, grid: { visible: true, type: 'dot', @@ -76,6 +76,32 @@ export function useCanvasInit() { anchor: { name: 'left', }, + validateConnection(data: any) { + const { sourceCell, targetCell, sourceView, targetView } = data + // Prevent loop edges + if (sourceView === targetView) { + return false + } + if ( + sourceCell && + targetCell && + sourceCell.isNode() && + targetCell.isNode() + ) { + const sourceData = sourceCell.getData() + // Prevent edges from being created from the output port + if (sourceData.type === 'OUTPUT') return false + // Prevent edges from being created from the input port to the input port + if (targetCell.getData().type === 'INPUT') return false + // Prevent multiple edges from being created between the same start node and end node + const edges = graph.value?.getConnectedEdges(targetCell) + if (edges!.length > 0) { + return false + } + + } + return true + } } }) } diff --git a/paimon-web-ui-new/src/views/cdc/components/list/index.tsx b/paimon-web-ui-new/src/views/cdc/components/list/index.tsx index 0d78f32..78deb86 100644 --- a/paimon-web-ui-new/src/views/cdc/components/list/index.tsx +++ b/paimon-web-ui-new/src/views/cdc/components/list/index.tsx @@ -17,6 +17,7 @@ under the License. */ import styles from './index.module.scss'; import TableAction from '@/components/table-action'; +import { useCDCStore } from '@/store/cdc'; import type { Router } from 'vue-router'; export default defineComponent({ @@ -64,20 +65,178 @@ export default defineComponent({ h(TableAction, { row, onHandleEdit: (row) => { - tableVariables.row = row + const CDCStore = useCDCStore() + CDCStore.setModel(row) router.push({ path: '/cdc_ingestion/dag' }) }, }) } ], data: [ - { name: 1, type: 'Single table synchronization', create_user: 'admin' }, - { name: 2, type: "Whole database synchronization", create_user: 'admin' }, + { + name: 1, + type: 'Single table synchronization', + create_user: 'admin', + cells: [ + { + "position": { + "x": 300, + "y": 40 + }, + "size": { + "width": 150, + "height": 40 + }, + "view": "vue-shape-view", + "shape": "custom-node", + "ports": { + "groups": { + "in": { + "position": "left", + "attrs": { + "circle": { + "r": 4, + "magnet": true, + "stroke": "transparent", + "strokeWidth": 1, + "fill": "transparent" + } + } + }, + "out": { + "position": { + "name": "right", + "args": { + "dx": 5 + } + }, + "attrs": { + "circle": { + "r": 4, + "magnet": true, + "stroke": "transparent", + "strokeWidth": 1, + "fill": "transparent" + } + } + } + }, + "items": [ + { + "id": "MySQL-out", + "group": "out", + "attrs": { + "circle": { + "fill": "transparent", + "stroke": "transparent" + } + } + } + ] + }, + "id": "MySQL", + "data": { + "name": "MySQL", + "value": "MYSQL", + "type": "INPUT", + "host": "1", + "port": "2", + "username": "3", + "password": "4", + "other_configs": "5", + "database": "", + "table_name": "", + "type_mapping": "", + "metadata_column": "", + "computed_column": "" + }, + "zIndex": 1 + }, + { + "position": { + "x": 640, + "y": 40 + }, + "size": { + "width": 150, + "height": 40 + }, + "view": "vue-shape-view", + "shape": "custom-node", + "ports": { + "groups": { + "in": { + "position": "left", + "attrs": { + "circle": { + "r": 4, + "magnet": true, + "stroke": "transparent", + "strokeWidth": 1, + "fill": "transparent" + } + } + }, + "out": { + "position": { + "name": "right", + "args": { + "dx": 5 + } + }, + "attrs": { + "circle": { + "r": 4, + "magnet": true, + "stroke": "transparent", + "strokeWidth": 1, + "fill": "transparent" + } + } + } + }, + "items": [ + { + "id": "Paimon-in", + "group": "in", + "attrs": { + "circle": { + "fill": "transparent", + "stroke": "transparent" + } + } + } + ] + }, + "id": "Paimon", + "data": { + "name": "Paimon", + "value": "PAIMON", + "type": "OUTPUT" + }, + "zIndex": 2 + }, + { + "shape": "dag-edge", + "connector": { + "name": "smooth" + }, + "id": "c3bec4f4-eea9-44ed-b98e-63605d619d50", + "zIndex": 3, + "source": { + "cell": "MySQL", + "port": "MySQL-out" + }, + "target": { + "cell": "Paimon" + } + } + ] + }, ], pagination: { pageSize: 10 - }, - row: {} + } }) return { diff --git a/paimon-web-ui-new/src/views/cdc/index.tsx b/paimon-web-ui-new/src/views/cdc/index.tsx index 548414e..bd21ba1 100644 --- a/paimon-web-ui-new/src/views/cdc/index.tsx +++ b/paimon-web-ui-new/src/views/cdc/index.tsx @@ -19,6 +19,8 @@ import Modal from '@/components/modal'; import List from './components/list'; import styles from './index.module.scss'; import { Leaf } from '@vicons/ionicons5'; +import { useCDCStore } from '@/store/cdc'; +import type { Router } from 'vue-router'; export default defineComponent({ name: 'CDCPage', @@ -31,15 +33,22 @@ export default defineComponent({ showModalRef.value = true } - const handleConfirm = () => { + const CDCStore = useCDCStore() + const router: Router = useRouter() + const CDCModalRef = ref() + const handleConfirm = async(model: any) => { + CDCStore.setModel(model) + await CDCModalRef.value.formRef.validate() showModalRef.value = false + router.push({ path: '/cdc_ingestion/dag' }) } return { t, showModalRef, handleOpenModal, - handleConfirm + handleConfirm, + CDCModalRef } }, render() { @@ -62,13 +71,17 @@ export default defineComponent({ </div> </n-card> <List></List> - <Modal - showModal={this.showModalRef} - title={this.t('cdc.create_synchronization_job')} - formType="CDCLIST" - onCancel={() => this.showModalRef = false} - onConfirm={this.handleConfirm} - /> + { + this.showModalRef && + <Modal + ref='CDCModalRef' + showModal={this.showModalRef} + title={this.t('cdc.create_synchronization_job')} + formType="CDCLIST" + onCancel={() => this.showModalRef = false} + onConfirm={this.handleConfirm} + /> + } </n-space> </n-card> </div>