From 3a3bd61064ca511166688649dcb0cbbbad60649d Mon Sep 17 00:00:00 2001 From: Nut He <18328704+hetao92@users.noreply.github.com> Date: Sun, 29 Jan 2023 18:24:48 +0800 Subject: [PATCH] feat: update import logic (#435) * feat: update import logic * feat: update import page * mod: code review * mod: code review * mod: update style --- app/common.less | 4 +- .../CSVPreviewLink/index.module.less | 22 +- app/components/CSVPreviewLink/index.tsx | 35 +- app/components/Instruction/index.tsx | 2 +- app/config/locale/en-US.json | 46 ++- app/config/locale/zh-CN.json | 44 ++- app/interfaces/import.ts | 22 +- .../DisplayPanel/ExpandItem/index.module.less | 4 +- .../ConfigConfirmModal/index.module.less | 39 +++ .../index.tsx | 23 +- .../Import/TaskCreate/FileSelect/index.tsx | 4 +- .../PasswordInputModal/index.module.less | 10 - .../SchemaConfig/EdgeConfig/index.tsx | 109 ------- .../SchemaConfig/FileMapping/index.tsx | 190 +++++++++++ .../SchemaConfig/TagConfig/index.tsx | 110 ------- .../TaskCreate/SchemaConfig/index.module.less | 173 +++++++--- .../Import/TaskCreate/SchemaConfig/index.tsx | 137 ++++---- app/pages/Import/TaskCreate/index.module.less | 44 +++ app/pages/Import/TaskCreate/index.tsx | 261 ++++++++++----- .../TaskItem/LogModal/index.module.less | 2 +- .../SchemaVisualization/index.module.less | 2 +- .../Plugins/SketchShapes/index.module.less | 4 +- .../SketchConfigHeader/index.module.less | 2 +- .../SketchList/index.module.less | 2 +- app/pages/Welcome/index.module.less | 23 +- app/stores/global.ts | 12 + app/stores/import.ts | 301 +++++++++--------- app/utils/constant.ts | 7 + app/utils/function.ts | 9 + app/utils/import.ts | 255 ++++++++------- config/webpack.base.js | 2 +- server/api/studio/internal/service/import.go | 2 - server/api/studio/internal/types/types.go | 6 +- server/api/studio/restapi/import.api | 6 +- 34 files changed, 1157 insertions(+), 757 deletions(-) create mode 100644 app/pages/Import/TaskCreate/ConfigConfirmModal/index.module.less rename app/pages/Import/TaskCreate/{PasswordInputModal => ConfigConfirmModal}/index.tsx (50%) delete mode 100644 app/pages/Import/TaskCreate/PasswordInputModal/index.module.less delete mode 100644 app/pages/Import/TaskCreate/SchemaConfig/EdgeConfig/index.tsx create mode 100644 app/pages/Import/TaskCreate/SchemaConfig/FileMapping/index.tsx delete mode 100644 app/pages/Import/TaskCreate/SchemaConfig/TagConfig/index.tsx diff --git a/app/common.less b/app/common.less index fd3aec26..3af54e62 100644 --- a/app/common.less +++ b/app/common.less @@ -3,9 +3,9 @@ @containerWidth: 1180px; @darkGray: #8697B0; @red: #EB5757; -@blue: #2F80ED; +@blue: #0091FF; @lightGray: #E9EDEF; @lightBlue: #F3F6F9; @darkBlue: #465B7A; @lightBlack: #172F52; -@headerBlack: #2F3A4A; +@headerBlack: #2F3A4A; \ No newline at end of file diff --git a/app/components/CSVPreviewLink/index.module.less b/app/components/CSVPreviewLink/index.module.less index 28a3bb4f..ca06f7b2 100644 --- a/app/components/CSVPreviewLink/index.module.less +++ b/app/components/CSVPreviewLink/index.module.less @@ -6,15 +6,22 @@ max-width: 600px; padding: 16px 8px 50px; overflow: auto; - + .selectTitle { + margin-bottom: 0; + font-family: Roboto-Medium, sans-serif; + color: #212A39; + } table { td, th { text-align: center; } - .ant-table-tbody { - tr { - background-color: @lightBlue; + :global { + .ant-table-tbody { + tr { + background-color: @lightBlue; + color: #212A39; + } } } .limitWidth { @@ -37,18 +44,13 @@ } > .operation { - padding-bottom: 25px; text-align: center; position: absolute; - bottom: 0; + bottom: 15px; left: 50%; transform: translateX(-50%); } - .csvSelectIndex { - margin-right: 10px; - } - .anticon { font-size: 16px; } diff --git a/app/components/CSVPreviewLink/index.tsx b/app/components/CSVPreviewLink/index.tsx index 4952fa17..2520b9b5 100644 --- a/app/components/CSVPreviewLink/index.tsx +++ b/app/components/CSVPreviewLink/index.tsx @@ -9,42 +9,47 @@ import styles from './index.module.less'; interface IProps { file: any; children: any; - onMapping?: (index) => void; + onMapping?: (index: number) => void; btnType?: string selected?: boolean } const CSVPreviewLink = (props: IProps) => { - const { onMapping, file: { content }, children, btnType, selected } = props; + const { onMapping, file, children, btnType, selected } = props; const [visible, setVisible] = useState(false); const { intl } = useI18n(); const handleLinkClick = e => { e.stopPropagation(); setVisible(true); }; - - const handleMapping = index => { - onMapping && onMapping(index); + const handleMapping = (index: number, e: React.MouseEvent) => { + e.stopPropagation(); + onMapping?.(index); setVisible(false); }; - const columns = content.length - ? content[0].map((_, index) => { + const columns = file?.content?.length + ? file.content[0].map((header, index) => { const textIndex = index; + const _header = file?.withHeader ? header : `Column ${textIndex}`; return { title: onMapping ? ( + onClick={(e) => handleMapping(textIndex, e)} + >{_header} ) : ( - `Column ${textIndex}` + _header ), dataIndex: index, render: value => {value}, }; }) : []; + const handleOpen = (visible) => { + if(!file) return; + setVisible(visible); + }; return ( { open={visible} trigger="click" arrowPointAtCenter - onOpenChange={visible => setVisible(visible)} + onOpenChange={handleOpen} content={
+

{intl.get('import.selectCsvColumn')}

uuidv4()} />
{onMapping && ( - )}
} > - diff --git a/app/components/Instruction/index.tsx b/app/components/Instruction/index.tsx index 945c00cb..db0f69bf 100644 --- a/app/components/Instruction/index.tsx +++ b/app/components/Instruction/index.tsx @@ -4,7 +4,7 @@ import React from 'react'; import './index.less'; -const Instruction = (props: { description: string; onClick?: () => void }) => { +const Instruction = (props: { description: React.ReactNode; onClick?: () => void }) => { return ( div { + color: @darkBlue; + &:not(:last-child) { + margin-bottom: 10px; + } + } + } + .label { + color: #212A39; + font-weight: 700; + font-family: 'Roboto-Bold', sans-serif; + padding-right: 20px; + &::after { + content: ':'; + padding-right: 5px; + } + } + .btns { + margin: 30px 0 0; + display: flex; + justify-content: center; + :global(.ant-btn:not(:last-child)) { + margin-right: 20px; + } + } +} diff --git a/app/pages/Import/TaskCreate/PasswordInputModal/index.tsx b/app/pages/Import/TaskCreate/ConfigConfirmModal/index.tsx similarity index 50% rename from app/pages/Import/TaskCreate/PasswordInputModal/index.tsx rename to app/pages/Import/TaskCreate/ConfigConfirmModal/index.tsx index a03c89bd..81bb3ebb 100644 --- a/app/pages/Import/TaskCreate/PasswordInputModal/index.tsx +++ b/app/pages/Import/TaskCreate/ConfigConfirmModal/index.tsx @@ -1,14 +1,18 @@ +import { useStore } from '@app/stores'; import { useI18n } from '@vesoft-inc/i18n'; import { Button, Input, Modal } from 'antd'; +import { observer } from 'mobx-react-lite'; import React, { useState } from 'react'; import styles from './index.module.less'; interface IProps { visible: boolean; onConfirm: (password: string) => void - onCancel: () => void + onCancel: () => void; + needPwdConfirm?: boolean; } -const PasswordInputModal = (props: IProps) => { +const ConfigConfirmModal = (props: IProps) => { + const { dataImport: { tagConfig, edgeConfig } } = useStore(); const [password, setPassword] = useState(''); const { visible, onConfirm, onCancel } = props; const { intl } = useI18n(); @@ -22,14 +26,23 @@ const PasswordInputModal = (props: IProps) => { }; return ( handleCancel()} - className={styles.passwordModal} + className={styles.importConfirmModal} footer={false} > + {intl.get('import.configDisplay')} +
+ {tagConfig.map((config) => config.files.map((item, index) =>
- {intl.get('import.loadToTag', { file: item.file?.name, name: config.name })}
))} + {edgeConfig.map((config) => config.files.map((item, index) =>
- {intl.get('import.loadToEdge', { file: item.file?.name, name: config.name })}
))} +
+ {intl.get('import.enterPassword')} + {intl.get('configServer.password')} setPassword(e.target.value)} />
@@ -48,4 +61,4 @@ const PasswordInputModal = (props: IProps) => { ); }; -export default PasswordInputModal; +export default observer(ConfigConfirmModal); diff --git a/app/pages/Import/TaskCreate/FileSelect/index.tsx b/app/pages/Import/TaskCreate/FileSelect/index.tsx index ffd56d50..9cfad631 100644 --- a/app/pages/Import/TaskCreate/FileSelect/index.tsx +++ b/app/pages/Import/TaskCreate/FileSelect/index.tsx @@ -16,7 +16,7 @@ const FormItem = Form.Item; const FileSelect = (props: IProps) => { const { type } = props; - const { files, dataImport: { update, verticesConfig, edgesConfig } } = useStore(); + const { files, dataImport: { update, verticesConfig, edgeConfig } } = useStore(); const { fileList, getFiles } = files; const [visible, setVisible] = useState(false); const { intl } = useI18n(); @@ -33,7 +33,7 @@ const FileSelect = (props: IProps) => { }); } else { update({ - edgesConfig: [...edgesConfig, { + edgeConfig: [...edgeConfig, { name: uuidv4(), file, props: [], diff --git a/app/pages/Import/TaskCreate/PasswordInputModal/index.module.less b/app/pages/Import/TaskCreate/PasswordInputModal/index.module.less deleted file mode 100644 index 048b599a..00000000 --- a/app/pages/Import/TaskCreate/PasswordInputModal/index.module.less +++ /dev/null @@ -1,10 +0,0 @@ -.passwordModal { - .btns { - margin: 30px 0 0; - display: flex; - justify-content: center; - :global(.ant-btn:not(:last-child)) { - margin-right: 20px; - } - } -} diff --git a/app/pages/Import/TaskCreate/SchemaConfig/EdgeConfig/index.tsx b/app/pages/Import/TaskCreate/SchemaConfig/EdgeConfig/index.tsx deleted file mode 100644 index 315057cc..00000000 --- a/app/pages/Import/TaskCreate/SchemaConfig/EdgeConfig/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { Select, Table, Tooltip } from 'antd'; -import React from 'react'; -import { CloseOutlined } from '@ant-design/icons'; -import { observer } from 'mobx-react-lite'; -import { useStore } from '@app/stores'; -import CSVPreviewLink from '@app/components/CSVPreviewLink'; -import classNames from 'classnames'; -import { useI18n } from '@vesoft-inc/i18n'; -import styles from '../index.module.less'; - -const Option = Select.Option; - -interface IProps { - configIndex: number; - edge: any; -} -const EdgeConfig = (configProps: IProps) => { - const { configIndex, edge: { type, props, file } } = configProps; - const { dataImport, schema } = useStore(); - const { updateEdgeConfig, updateEdgePropMapping } = dataImport; - const { edgeTypes } = schema; - const { intl } = useI18n(); - const handleEdgeChange = (index: number, value: string) => { - updateEdgeConfig({ index, edgeType: value }); - }; - - const handleRemoveEdge = () => { - updateEdgePropMapping({ configIndex }); - }; - const handlePropChange = (index, field, value) => { - updateEdgePropMapping({ - configIndex, - propIndex: index, - field, - value - }); - }; - - const columns = [ - { - title: intl.get('import.prop'), - dataIndex: 'name', - ellipsis: { - showTitle: false, - }, - render: data => ( - - {data} - - ), - }, - { - title: intl.get('import.mapping'), - dataIndex: 'mapping', - render: (mappingIndex, prop, propIndex) => ( -
- {!prop.isDefault && prop.name !== 'rank' && ( - * - )} - - handlePropChange(propIndex, 'mapping', columnIndex) - } - file={file} - > - {mappingIndex === null ? intl.get('import.choose') : `Column ${mappingIndex}`} - -
- ), - }, - { - title: intl.get('common.type'), - dataIndex: 'type', - }, - ]; - - return ( -
-
-
- Edge Type - -
- -
- {type &&
} - - ); -}; - -export default observer(EdgeConfig); diff --git a/app/pages/Import/TaskCreate/SchemaConfig/FileMapping/index.tsx b/app/pages/Import/TaskCreate/SchemaConfig/FileMapping/index.tsx new file mode 100644 index 00000000..57277214 --- /dev/null +++ b/app/pages/Import/TaskCreate/SchemaConfig/FileMapping/index.tsx @@ -0,0 +1,190 @@ +import { Collapse, Input, Select, Table, Tooltip } from 'antd'; +import React, { useMemo } from 'react'; +import { observer } from 'mobx-react-lite'; +import cls from 'classnames'; +import { useStore } from '@app/stores'; +import { useI18n } from '@vesoft-inc/i18n'; +import CSVPreviewLink from '@app/components/CSVPreviewLink'; +import { CloseOutlined } from '@ant-design/icons'; +import Instruction from '@app/components/Instruction'; +import { ISchemaEnum } from '@app/interfaces/schema'; +import { IEdgeFileItem, ITagFileItem } from '@app/stores/import'; +import { IImportFile } from '@app/interfaces/import'; +import styles from '../index.module.less'; + +const Option = Select.Option; +const Panel = Collapse.Panel; + +interface IProps { + item: ITagFileItem | IEdgeFileItem + onRemove: (file: ITagFileItem | IEdgeFileItem) => void; + onReset: (item: ITagFileItem | IEdgeFileItem, file: IImportFile) => void; + type: ISchemaEnum +} + + +const VIDSetting = observer((props: { + data: ITagFileItem | IEdgeFileItem, + keyMap: { + idKey: string, + idFunction?: string, + idPrefix?: string, + label: string + } +}) => { + const { keyMap: { idKey, idFunction, idPrefix, label }, data } = props; + const { intl } = useI18n(); + return
+ + +
+ {intl.get(`import.${label}`)} + data.update({ [idKey]: index })} + file={data.file} + > + {!data[idKey] && data[idKey] !== 0 ? intl.get('import.selectCsvColumn') : `Column ${data[idKey]}`} + +
+ +
+ {intl.get('import.vidFunction')} + {intl.get('import.vidFunctionTip')} +
+
+ {intl.get('import.vidPrefix')} + {intl.get('import.vidPrefixTip')} +
+ } /> +
} key="default"> + {idFunction &&
+ {intl.get('import.vidFunction')} + +
} + {idPrefix &&
+ {intl.get('import.vidPrefix')} + data.update({ [idPrefix]: e.target.value })} /> +
} + + + ; +}); + +const idMap = { + [ISchemaEnum.Tag]: [{ + idKey: 'vidIndex', + idFunction: 'vidFunction', + idPrefix: 'vidPrefix', + label: 'vidColumn' + }], + [ISchemaEnum.Edge]: [{ + idKey: 'srcIdIndex', + idFunction: 'srcIdFunction', + label: 'srcVidColumn' + }, { + idKey: 'dstIdIndex', + idFunction: 'dstIdFunction', + label: 'dstVidColumn' + }], +}; + +const FileMapping = (props: IProps) => { + const { item, onRemove, type, onReset } = props; + const { files } = useStore(); + const { fileList, getFiles } = files; + const { file, props: mappingProps } = item; + const { intl } = useI18n(); + const handleFileChange = (value: string) => { + const file = fileList.find(item => item.name === value); + onReset(item, file); + }; + + const updateFilePropMapping = (index: number, value: number) => item.updatePropItem(index, { mapping: value }); + const columns = [ + { + title: intl.get('import.prop'), + dataIndex: 'name', + ellipsis: { + showTitle: false, + }, + render: (value, record) => { + return ( + +
+ {value} +
+
+ ); + }, + }, + { + title: intl.get('import.mapping'), + dataIndex: 'mapping', + render: (mappingIndex, _, propIndex) => ( + updateFilePropMapping(propIndex, columnIndex)} + file={file} + > + {!mappingIndex && mappingIndex !== 0 ? intl.get('import.choose') : `Column ${mappingIndex}`} + + ), + }, + { + title: intl.get('common.type'), + dataIndex: 'type', + }, + ]; + const handleGetFiles = () => { + if(fileList.length === 0) { + getFiles(); + } + }; + + const idConfig = useMemo(() => type === ISchemaEnum.Tag ? idMap[ISchemaEnum.Tag] : idMap[ISchemaEnum.Edge], [type]); + return ( +
+
+
+ {intl.get('import.dataSourceFile')} + +
+ onRemove(item)} /> +
+ {idConfig.map((idItem, index) => )} +
+ + ); +}; + +export default observer(FileMapping); diff --git a/app/pages/Import/TaskCreate/SchemaConfig/TagConfig/index.tsx b/app/pages/Import/TaskCreate/SchemaConfig/TagConfig/index.tsx deleted file mode 100644 index 791c62bc..00000000 --- a/app/pages/Import/TaskCreate/SchemaConfig/TagConfig/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Select, Table, Tooltip } from 'antd'; -import React from 'react'; -import { CloseOutlined } from '@ant-design/icons'; -import { observer } from 'mobx-react-lite'; -import { useStore } from '@app/stores'; -import CSVPreviewLink from '@app/components/CSVPreviewLink'; -import classNames from 'classnames'; -import { useI18n } from '@vesoft-inc/i18n'; -import styles from '../index.module.less'; - -const Option = Select.Option; - -interface IProps { - tag: any; - tagIndex: number; - configIndex: number; - file: any; -} -const VerticesConfig = (props: IProps) => { - const { tag, tagIndex, configIndex, file } = props; - const { dataImport, schema } = useStore(); - const { updateTagConfig, updateTagPropMapping } = dataImport; - const { tags } = schema; - const { intl } = useI18n(); - const handleTagChange = (configIndex: number, tagIndex: number, value: string) => { - updateTagConfig({ configIndex, tagIndex, tag: value }); - }; - - const handlePropChange = (index, field, value) => { - updateTagPropMapping({ - configIndex, - tagIndex, - propIndex: index, - field, - value - }); - }; - - const columns = [ - { - title: intl.get('import.prop'), - dataIndex: 'name', - ellipsis: { - showTitle: false, - }, - render: data => ( - - {data} - - ), - }, - { - title: intl.get('import.mapping'), - dataIndex: 'mapping', - render: (mappingIndex, prop, propIndex) => ( -
- {!prop.isDefault && *} - - handlePropChange(propIndex, 'mapping', columnIndex) - } - file={file} - > - {mappingIndex === null ? intl.get('import.choose') : `Column ${mappingIndex}`} - -
- ), - }, - { - title: intl.get('common.type'), - dataIndex: 'type', - }, - ]; - - const handleRemoveTag = () => { - updateTagPropMapping({ configIndex, tagIndex }); - }; - return ( -
-
-
- Tag - -
- -
- {tag.name &&
} - - ); -}; - -export default observer(VerticesConfig); diff --git a/app/pages/Import/TaskCreate/SchemaConfig/index.module.less b/app/pages/Import/TaskCreate/SchemaConfig/index.module.less index e7641619..10e9d5bc 100644 --- a/app/pages/Import/TaskCreate/SchemaConfig/index.module.less +++ b/app/pages/Import/TaskCreate/SchemaConfig/index.module.less @@ -12,58 +12,143 @@ color: @darkBlue; } - .configItem { - .idRow { - padding: 10px 15px; - background: #FFFFFF; - border: 1px solid @gray; - box-sizing: border-box; - border-radius: 3px; - margin-bottom: 10px; + .panelTitle { + display: flex; + align-items: center; + } + + .configTargetSelect { + min-width: 60px; + width: fit-content; + max-width: 60%; + :global { + .ant-select-selector { + padding-right: 20px; + } + .ant-select-selection-item, .ant-select-selection-placeholder { + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + white-space: nowrap; + display: inline-block; + padding: 0 36px; + background: @blue; + border-radius: 30px; + color: #FFFFFF; + font-family: Roboto-Bold, sans-serif; + } + .ant-select-arrow { + right: 0; + color: @blue; + } } - .btns { - text-align: center; + + &.noValue :global(.ant-select-arrow svg) { + color: @blue; } - } - .configContainer { - padding: 10px 15px; - margin-bottom: 15px; - background: #FFFFFF; - border: 1px solid @gray; - box-sizing: border-box; - border-radius: 3px; - .tagSelectRow { - display: flex; - justify-content: space-between; - align-items: center; - .left { - display: flex; - align-items: center; - max-width: 95%; - .tagSelect { - min-width: 60px; - :global { - .ant-select-selection-item { - font-family: Roboto-Bold, sans-serif; - } - .ant-select-selection-placeholder { - color: @blue; - } - } - - &.noValue :global(.ant-select-arrow svg) { - color: @blue; - } - } + &.edgeLabel :global(.ant-select-selection-item) { + &::before, &::after { + content: ''; + display: inline-block; + width: 12px; + height: 4px; + background: #FFFFFF; + position: absolute; + top: 50%; + transform: translateY(-50%); + } + &::before { + left: 15px; + } + &::after { + right: 15px; + // Overridden by antd by default + visibility: visible; } } - + } + .btns { + text-align: center; } .btnClose { cursor: pointer; } } -.label::after { + +.label { + white-space: nowrap; + font-family: 'Roboto-Medium', sans-serif; + color: @darkBlue; +} +.title::after, .label::after { content: ':'; padding-right: 5px; -} \ No newline at end of file +} +.required::before { + content: '*'; + padding-right: 3px; + color: @red; +} +.selectItems { + max-width: 400px; +} + +.fileMappingContainer { + padding: 15px; + background: #FFFFFF; + border: 1px solid @gray; + border-radius: 3px; + margin-bottom: 15px; + .row { + display: flex; + align-items: center; + margin-bottom: 15px; + border-bottom: 1px solid @gray; + } + .operation { + max-width: 100%; + } + .fileSelect { + :global { + .ant-select-selection-placeholder, .anticon, .ant-select-selection-item { + color: @blue; + } + } + } + .vidCollapse { + width: 100%; + + .rowItem { + width: 45%; + display: inline-block; + } + .functionSelect { + width: 60%; + background: @lightBlue; + color: @darkGray; + } + .prefixInput { + width: 60%; + background: @lightBlue; + color: @darkGray; + :global(.anticon) { + color: @darkBlue; + } + } + } + .propsMappingTable { + :global(.ant-table-row) { + background-color: @lightBlue; + } + } + .spaceBetween { + display: flex; + justify-content: space-between; + } + :global(.ant-select) { + width: fit-content; + min-width: 100px; + max-width: 80%; + } +} + diff --git a/app/pages/Import/TaskCreate/SchemaConfig/index.tsx b/app/pages/Import/TaskCreate/SchemaConfig/index.tsx index 19818f64..73d53ec6 100644 --- a/app/pages/Import/TaskCreate/SchemaConfig/index.tsx +++ b/app/pages/Import/TaskCreate/SchemaConfig/index.tsx @@ -1,83 +1,100 @@ -import { Button, Collapse } from 'antd'; -import React from 'react'; +import { Button, Collapse, Select } from 'antd'; +import React, { useCallback } from 'react'; import { CloseOutlined } from '@ant-design/icons'; import { observer } from 'mobx-react-lite'; import { useStore } from '@app/stores'; -import CSVPreviewLink from '@app/components/CSVPreviewLink'; import Icon from '@app/components/Icon'; import { useI18n } from '@vesoft-inc/i18n'; -import TagConfig from './TagConfig'; -import EdgeConfig from './EdgeConfig'; -const { Panel } = Collapse; +import cls from 'classnames'; +import { ISchemaEnum, ISchemaType } from '@app/interfaces/schema'; +import { ITagItem, IEdgeItem, ITagFileItem, IEdgeFileItem, TagFileItem, EdgeFileItem } from '@app/stores/import'; +import { IImportFile } from '@app/interfaces/import'; import styles from './index.module.less'; - +import FileMapping from './FileMapping'; +const { Panel } = Collapse; +const Option = Select.Option; interface IProps { - type: 'vertices' | 'edge' - data: any; - configIndex: number; + configItem: ITagItem | IEdgeItem } + +interface IHeaderProps { + type: ISchemaType; + value: string; + onSelect: (value: string) => void; +} + +const SelectMappingTargetHeader = observer((props: IHeaderProps) => { + const { value, type, onSelect } = props; + const { dataImport, schema } = useStore(); + const { tags, edgeTypes } = schema; + const { tagConfig, edgeConfig } = dataImport; + const { intl } = useI18n(); + const config = type === ISchemaEnum.Tag ? tagConfig : edgeConfig; + const targetList = type === ISchemaEnum.Tag ? tags : edgeTypes; + return
+ {intl.get(`common.${type}`)} + +
; +}); const SchemaConfig = (props: IProps) => { - const { type, data, configIndex } = props; + const { configItem } = props; const { dataImport } = useStore(); - const { verticesConfig, updateVerticesConfig, updateEdgeConfig } = dataImport; + const { name, files, type } = configItem; + const { deleteTagConfig, deleteEdgeConfig, updateConfigItemName } = dataImport; const { intl } = useI18n(); - const addTag = index => { - updateVerticesConfig({ - index, - key: 'tags', - value: [...verticesConfig[index].tags, { - name: '', - props: [] - }] - }); - }; - const handleRemove = (event, index: number) => { + const addFileSource = useCallback(() => { + const payload = { file: undefined, props: configItem.props }; + configItem.addFileItem(configItem.type === ISchemaEnum.Tag ? new TagFileItem(payload) : new EdgeFileItem(payload)); + }, [configItem]); + + const resetFileSource = useCallback((item: ITagFileItem | IEdgeFileItem, file: IImportFile) => { + const index = configItem.files.findIndex(f => f === item); + const payload = { file, props: [...configItem.props] }; + configItem.resetFileItem(index, configItem.type === ISchemaEnum.Tag ? new TagFileItem(payload) : new EdgeFileItem(payload)); + }, [configItem]); + + + const handleRemove = useCallback((event: React.MouseEvent) => { event.stopPropagation(); - if(type === 'vertices') { - updateVerticesConfig({ index }); - } else { - updateEdgeConfig({ index }); - } - }; + configItem.type === ISchemaEnum.Tag ? deleteTagConfig(configItem) : deleteEdgeConfig(configItem); + }, [configItem]); + + const changeMappingTarget = useCallback((name: string) => { + updateConfigItemName(configItem, name); + }, [configItem]); + + const clearFileSource = useCallback((item: ITagFileItem | IEdgeFileItem) => configItem.deleteFileItem(item), [configItem]); return ( - - {type} {configIndex + 1} - - {data.file.name} - - } key="default" extra={ handleRemove(e, configIndex)} />}> -
- {type === 'vertices' &&
- vertexID - - updateVerticesConfig({ - index: configIndex, - key: 'idMapping', - value: columnIndex - }) - } - file={data.file} - > - {data.idMapping === null ? 'Select CSV Index' : `Column ${data.idMapping}`} - -
} - {type === 'vertices' && data.tags.map((tag, tagIndex) => )} - {type === 'edge' && } - {type === 'vertices' &&
- -
} -
+ } key="default" extra={}> + {files.map((item: ITagFileItem | IEdgeFileItem, index) => )} + {!!name &&
+ +
}
); diff --git a/app/pages/Import/TaskCreate/index.module.less b/app/pages/Import/TaskCreate/index.module.less index a151d4ae..ab4a146b 100644 --- a/app/pages/Import/TaskCreate/index.module.less +++ b/app/pages/Import/TaskCreate/index.module.less @@ -1,4 +1,5 @@ @import '~@app/common.less'; + .importCreate { .createForm { padding: 32px 0 100px; @@ -26,4 +27,47 @@ margin-right: 20px; } } + .configContainer { + width: 100%; + } + .toggleConfigBtn { + margin: 20px 0; + cursor: pointer; + display: flex; + align-items: center; + color: @blue; + .toggleIcon svg{ + width: 25px; + height: 25px; + } + } + .addressCheckbox { + :global { + .ant-checkbox-group-item { + background: @gray; + border-radius: 3px; + color: @darkBlue; + padding: 11px 12px; + } + .ant-checkbox-disabled + span { + color: @darkBlue; + } + .ant-checkbox-disabled { + .ant-checkbox-inner { + background: rgba(134, 151, 176, 0.4); + border: 1px solid #8697B0 !important; + &::after { + border-color: #FFFFFF; + } + } + } + } + } + .currentHost { + color: #8697B0; + } + .label::after { + content: ':'; + padding-right: 5px; + } } \ No newline at end of file diff --git a/app/pages/Import/TaskCreate/index.tsx b/app/pages/Import/TaskCreate/index.tsx index 8d159a76..e64b4ca2 100644 --- a/app/pages/Import/TaskCreate/index.tsx +++ b/app/pages/Import/TaskCreate/index.tsx @@ -1,41 +1,56 @@ -import { Button, Col, Form, Input, Row, Select, message } from 'antd'; -import React, { useEffect, useMemo, useState } from 'react'; +import { Button, Checkbox, Col, Form, Input, Row, Select, message } from 'antd'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import Breadcrumb from '@app/components/Breadcrumb'; import { observer } from 'mobx-react-lite'; import { useStore } from '@app/stores'; import { trackPageView } from '@app/utils/stat'; import cls from 'classnames'; import { useHistory } from 'react-router-dom'; -import { POSITIVE_INTEGER_REGEX } from '@app/utils/constant'; +import { DEFAULT_IMPORT_CONFIG, POSITIVE_INTEGER_REGEX } from '@app/utils/constant'; import { useI18n } from '@vesoft-inc/i18n'; +import { ISchemaEnum, ISchemaType } from '@app/interfaces/schema'; +import Icon from '@app/components/Icon'; +import Instruction from '@app/components/Instruction'; +import { isEmpty } from '@app/utils/function'; import styles from './index.module.less'; -import PasswordInputModal from './PasswordInputModal'; +import ConfigConfirmModal from './ConfigConfirmModal'; import SchemaConfig from './SchemaConfig'; -import FileSelect from './FileSelect'; const Option = Select.Option; const formItemLayout = { - labelCol: { - span: 6, - }, wrapperCol: { - span: 11, + span: 15, }, }; interface IProps { needPwdConfirm?: boolean; } + + +const AddMappingBtn = (props: { type: ISchemaType }) => { + const { intl } = useI18n(); + const { type } = props; + const { dataImport: { addTagConfig, addEdgeConfig } } = useStore(); + const addMapping = useCallback(() => type === ISchemaEnum.Tag ? addTagConfig() : addEdgeConfig(), [type]); + return ; +}; + const TaskCreate = (props: IProps) => { - const { dataImport, schema, files } = useStore(); + const { dataImport, schema, files, global } = useStore(); const { intl, currentLocale } = useI18n(); - const { basicConfig, verticesConfig, edgesConfig, updateBasicConfig, importTask } = dataImport; + const { basicConfig, tagConfig, edgeConfig, updateBasicConfig, importTask } = dataImport; const { spaces, getSpaces, updateSpaceInfo, currentSpace } = schema; + const { getGraphAddress, _host } = global; const { getFiles } = files; - const { batchSize } = basicConfig; const [modalVisible, setVisible] = useState(false); const history = useHistory(); const { needPwdConfirm = true } = props; + const [address, setAddress] = useState([]); const [loading, setLoading] = useState(false); + const [showMoreConfig, setShowMoreConfig] = useState(false); const routes = useMemo(() => ([ { path: '/import/tasks', @@ -47,6 +62,17 @@ const TaskCreate = (props: IProps) => { }, ]), [currentLocale]); + useEffect(() => { + initTaskDir(); + getSpaces(); + getFiles(); + if(currentSpace) { + updateSpaceInfo(currentSpace); + } + trackPageView('/import/create'); + return () => clearConfig('all'); + }, []); + const checkConfig = () => { try { check(); @@ -69,76 +95,130 @@ const TaskCreate = (props: IProps) => { } }; - const check = () => { - verticesConfig.forEach(config => { - if(config.idMapping === null) { - message.error(`vertexId ${intl.get('import.indexNotEmpty')}`); + [...tagConfig, ...edgeConfig].forEach(config => { + const { type, name, files } = config; + const _type = type === ISchemaEnum.Tag ? 'tag' : 'edge'; + if(!name) { + message.error(intl.get(`import.${_type}Required`)); throw new Error(); } - if(config.tags.length === 0) { - message.error(`Tag Mapping ${intl.get('import.isEmpty')}`); + if(files.length === 0) { + message.error(intl.get(`import.${_type}FileRequired`)); throw new Error(); } - config.tags.forEach(tag => { - if (!tag.name) { - message.error(`Tag ${intl.get('import.isEmpty')}`); + files.forEach((file) => { + if(!file.file?.name) { + message.error(intl.get(`import.${_type}FileSelect`)); throw new Error(); } - tag.props.forEach(prop => { - if (prop.mapping === null && !prop.allowNull && !prop.isDefault) { + if(type === ISchemaEnum.Tag && isEmpty(file.vidIndex)) { + message.error(`vertexId ${intl.get('import.indexNotEmpty')}`); + throw new Error(); + } else if (type === ISchemaEnum.Edge) { + if(isEmpty(file.srcIdIndex)) { + message.error(`${intl.get('common.edge')} ${config.name} ${intl.get('common.src')} id ${intl.get('import.indexNotEmpty')}`); + throw new Error(); + } else if (isEmpty(file.dstIdIndex)) { + message.error(`${intl.get('common.edge')} ${config.name} ${intl.get('common.dst')} id ${intl.get('import.indexNotEmpty')}`); + throw new Error(); + } + } + file.props.forEach(prop => { + if (isEmpty(prop.mapping) && !prop.allowNull && !prop.isDefault) { message.error(`${prop.name} ${intl.get('import.indexNotEmpty')}`); throw new Error(); } }); }); }); - edgesConfig.forEach(edge => { - if (!edge.type) { - message.error(`edgeType ${intl.get('import.isEmpty')}`); + extraConfigs.forEach(config => { + const { key, label } = config; + if(basicConfig[key] && !basicConfig[key].match(POSITIVE_INTEGER_REGEX)) { + message.error(`${label}: ${intl.get('formRules.numberRequired')}`); throw new Error(); } - edge.props.forEach(prop => { - if (prop.mapping === null && !prop.allowNull && prop.name !== 'rank' && !prop.isDefault) { - message.error(`${prop.name} ${intl.get('import.indexNotEmpty')}`); - throw new Error(); - } - }); }); - if(batchSize && !batchSize.match(POSITIVE_INTEGER_REGEX)) { - message.error(`${intl.get('import.batchSize')} ${intl.get('formRules.numberRequired')}`); - throw new Error(); - } }; - const clearConfig = (type?: string) => { + const clearConfig = useCallback((type?: string) => { const params = { - verticesConfig: [], - edgesConfig: [] + tagConfig: [], + edgeConfig: [] } as any; if(type === 'all') { - params.basicConfig = { taskName: '' }; + params.basicConfig = { taskName: '', address: address.map(i => i.value) }; } dataImport.update(params); - }; - const handleSpaceChange = (space: string) => { + }, []); + const handleSpaceChange = useCallback((space: string) => { clearConfig(); updateSpaceInfo(space); - }; + }, []); - const initTaskDir = async () => { - updateBasicConfig('taskName', `task-${Date.now()}`); - }; - useEffect(() => { - initTaskDir(); - getSpaces(); - getFiles(); - if(currentSpace) { - updateSpaceInfo(currentSpace); - } - trackPageView('/import/create'); - return () => clearConfig('all'); + const initTaskDir = useCallback(async () => { + updateBasicConfig({ 'taskName': `task-${Date.now()}` }); + const graphs = await getGraphAddress(); + updateBasicConfig({ 'address': graphs }); + setAddress(graphs.map(item => ({ + label: <> + {item} + {item === _host ?  ({intl.get('import.currentHost')}) : null} + , + value: item, + disabled: item === _host + }))); }, []); + const extraConfigs = useMemo(() => [ + { + label: intl.get('import.concurrency'), + key: 'concurrency', + rules: [ + { + pattern: POSITIVE_INTEGER_REGEX, + message: intl.get('formRules.numberRequired'), + }, + ], + placeholder: DEFAULT_IMPORT_CONFIG.concurrency, + description: intl.get('import.concurrencyTip'), + }, + { + label: intl.get('import.batchSize'), + key: 'batchSize', + rules: [ + { + pattern: POSITIVE_INTEGER_REGEX, + message: intl.get('formRules.numberRequired'), + }, + ], + placeholder: DEFAULT_IMPORT_CONFIG.batchSize, + description: intl.get('import.batchSizeTip'), + }, + { + label: intl.get('import.retry'), + key: 'retry', + rules: [ + { + pattern: POSITIVE_INTEGER_REGEX, + message: intl.get('formRules.numberRequired'), + }, + ], + placeholder: DEFAULT_IMPORT_CONFIG.retry, + description: intl.get('import.retryTip'), + }, + { + label: intl.get('import.channelBufferSize'), + key: 'channelBufferSize', + rules: [ + { + pattern: POSITIVE_INTEGER_REGEX, + message: intl.get('formRules.numberRequired'), + }, + ], + placeholder: DEFAULT_IMPORT_CONFIG.channelBufferSize, + description: intl.get('import.channelBufferSizeTip'), + }, + ], [currentLocale]); return (
@@ -158,35 +238,69 @@ const TaskCreate = (props: IProps) => {
- updateBasicConfig('taskName', e.target.value)} /> - - - - - - - updateBasicConfig('batchSize', e.target.value)} /> + updateBasicConfig({ 'taskName': e.target.value })} /> + {showMoreConfig ? ( +
+ +
+ + {intl.get('import.graphAddress')} + + } rules={[{ + required: true, + }]}> + updateBasicConfig({ 'address': value })} + /> + + + + + {extraConfigs.map(item => ( + + + {item.label} + + } name={item.key} rules={item.rules}> + updateBasicConfig(item.key, e.target.value)} /> + + + ))} + + +
setShowMoreConfig(false)}> + + {intl.get('import.pickUpConfig')} +
+
+ + ) : +
setShowMoreConfig(true)}> + + {intl.get('import.expandMoreConfig')} +
+
}
- +
- - {verticesConfig.map((item, index) => )} + + {tagConfig.map((item) => )}
- - {edgesConfig.map((item, index) => )} + + {edgeConfig.map((item) => )}
@@ -196,10 +310,11 @@ const TaskCreate = (props: IProps) => {
- {needPwdConfirm && setVisible(false)} diff --git a/app/pages/Import/TaskList/TaskItem/LogModal/index.module.less b/app/pages/Import/TaskList/TaskItem/LogModal/index.module.less index 22fa1db8..5ba3769b 100644 --- a/app/pages/Import/TaskList/TaskItem/LogModal/index.module.less +++ b/app/pages/Import/TaskList/TaskItem/LogModal/index.module.less @@ -69,7 +69,7 @@ } } .ant-tabs-tab-active { - background-color: #0091FF; + background-color: @blue; color: #fff; .ant-tabs-tab-btn { color: #fff; diff --git a/app/pages/Schema/SchemaConfig/List/SchemaVisualization/index.module.less b/app/pages/Schema/SchemaConfig/List/SchemaVisualization/index.module.less index fecf3f06..87accf57 100644 --- a/app/pages/Schema/SchemaConfig/List/SchemaVisualization/index.module.less +++ b/app/pages/Schema/SchemaConfig/List/SchemaVisualization/index.module.less @@ -23,7 +23,7 @@ background: #DBEFFF; border-radius: 3px; padding: 13px; - color: #0091FF; + color: @blue; flex: 1; margin-left: 20px; } diff --git a/app/pages/SketchModeling/Plugins/SketchShapes/index.module.less b/app/pages/SketchModeling/Plugins/SketchShapes/index.module.less index 8670ebee..7cb927c6 100644 --- a/app/pages/SketchModeling/Plugins/SketchShapes/index.module.less +++ b/app/pages/SketchModeling/Plugins/SketchShapes/index.module.less @@ -69,7 +69,7 @@ } :global(.ve-line.active), :global(.ve-line:hover) { .edgeLabel { - color: #0091FF; + color: @blue; span { background-color: @lightWhite; } @@ -108,7 +108,7 @@ } .ve-line-label { text { - fill: #0091FF; + fill: @blue; } } // .ve-line-arrow{ diff --git a/app/pages/SketchModeling/SketchConfigHeader/index.module.less b/app/pages/SketchModeling/SketchConfigHeader/index.module.less index d51bf0cd..65a812a0 100644 --- a/app/pages/SketchModeling/SketchConfigHeader/index.module.less +++ b/app/pages/SketchModeling/SketchConfigHeader/index.module.less @@ -76,7 +76,7 @@ border-radius: 3px; margin-top: 15px; padding: 13px 10px; - color: #0091FF; + color: @blue; margin-bottom: 30px; } } diff --git a/app/pages/SketchModeling/SketchList/index.module.less b/app/pages/SketchModeling/SketchList/index.module.less index cf3297da..b26839e5 100644 --- a/app/pages/SketchModeling/SketchList/index.module.less +++ b/app/pages/SketchModeling/SketchList/index.module.less @@ -64,7 +64,7 @@ flex-direction: column; position: relative; &.active { - border: 4px solid #0091FF; + border: 4px solid @blue; } &:hover { .count { diff --git a/app/pages/Welcome/index.module.less b/app/pages/Welcome/index.module.less index 889677ab..a682464f 100644 --- a/app/pages/Welcome/index.module.less +++ b/app/pages/Welcome/index.module.less @@ -1,3 +1,4 @@ +@import '~@app/common.less'; .loadingWrapper { position: fixed; left: 0; @@ -27,15 +28,15 @@ font-size: 14px; padding: 0 7px; width: 100px; - background: #0091FF; - border-color: #0091FF; + background: @blue; + border-color: @blue; &:not(:last-child) { margin-right: 16px; } &.sub { background: #fff; - color: #0091FF; - border-color: #0091FF; + color: @blue; + border-color: @blue; &:global(.ant-btn-disabled) { color: rgba(0, 0, 0, 0.25); border-color: rgba(0, 0, 0, 0.25); @@ -43,7 +44,7 @@ } } .link:not(:global(.ant-btn-disabled)) { - color: #0091FF; + color: @blue; } .disabledAction { margin-right: 16px; @@ -84,7 +85,7 @@ li.slick-active button { width: 10px; - background-color: #0091ff; + background-color: @blue; } } } @@ -155,8 +156,8 @@ & :global(.anticon > svg) { width: 34px; height: 34px; - color: #0091ff; - fill: #0091ff; + color: @blue; + fill: @blue; } .title { position: relative; @@ -195,7 +196,7 @@ height: 30px; border-radius: 15px; background-color: #d5ddeb; - color: #465b7a; + color: @darkBlue; font-size: 20px; line-height: 30px; text-align: center; @@ -283,7 +284,7 @@ color: #4F4F4F; } :global(.ant-btn-block) { - background-color: #0091FF; + background-color: @blue; } } } @@ -322,7 +323,7 @@ justify-content: center; &.ant-tabs-tab-active { - background-color: #0091ff; + background-color: @blue; border-radius: 14px; .ant-tabs-tab-btn { diff --git a/app/stores/global.ts b/app/stores/global.ts index d29f7fa0..a7e1534c 100644 --- a/app/stores/global.ts +++ b/app/stores/global.ts @@ -6,6 +6,7 @@ import { getI18n } from '@vesoft-inc/i18n'; import { BrowserHistory } from 'history'; import service from '@app/config/service'; import ngqlRunner from '@app/utils/websocket'; +import { isValidIP } from '@app/utils/function'; import { getRootStore, resetStore } from '.'; const { intl } = getI18n(); @@ -116,6 +117,17 @@ export class GlobalStore { cookies.remove('nu'); return false; }; + + getGraphAddress = async () => { + const { code, data } = await service.execNGQL({ gql: 'show hosts graph;' }); + return code !== 0 ? [] : data.tables.reduce((acc, cur) => { + if (isValidIP(cur.Host)) { + acc.push(`${cur.Host}:${cur.Port}`); + } + return acc; + } + , []); + }; } const globalStore = new GlobalStore(); diff --git a/app/stores/import.ts b/app/stores/import.ts index ad34260d..c4cecde7 100644 --- a/app/stores/import.ts +++ b/app/stores/import.ts @@ -1,17 +1,14 @@ -import { action, makeObservable, observable, runInAction } from 'mobx'; +import { makeAutoObservable, observable, runInAction } from 'mobx'; +import { v4 as uuidv4 } from 'uuid'; import service from '@app/config/service'; -import { IBasicConfig, IEdgeConfig, ITaskItem, IVerticesConfig } from '@app/interfaces/import'; -import { configToJson } from '@app/utils/import'; +import { IBasicConfig, ITaskItem, IImportFile, IPropertyProps } from '@app/interfaces/import'; import { ISchemaEnum } from '@app/interfaces/schema'; +import { configToJson } from '@app/utils/import'; +import { isEmpty } from '@app/utils/function'; import { getRootStore } from '.'; const handlePropertyMap = (item, defaultValueFields) => { - let type = item.Type; - if(item.Type.startsWith('fixed_string')) { - type = 'string'; - } else if (item.Type.startsWith('int')) { - type = 'int'; - } + const type = item.Type.startsWith('fixed_string') ? 'string' : item.Type.startsWith('int') ? 'int' : item.Type; return { name: item.Field, type, @@ -20,23 +17,104 @@ const handlePropertyMap = (item, defaultValueFields) => { mapping: null, }; }; + +export class TagFileItem { + file: IImportFile; + props = observable.array([]); + vidIndex?: number; + vidFunction?: string; + vidPrefix?: string; + + constructor({ file, props }: { file?: IImportFile; props?: IPropertyProps[] }) { + makeAutoObservable(this); + file && (this.file = file); + props && this.props.replace(props); + } + + update = (payload: Partial) => { + Object.keys(payload).forEach(key => Object.prototype.hasOwnProperty.call(this, key) && (this[key] = payload[key])); + }; + + updatePropItem = (index: number, payload: Partial) => { + this.props.splice(index, 1, { + ...this.props[index], + ...payload, + }); + }; +} +export class EdgeFileItem { + file: IImportFile; + props = observable.array([]); + srcIdIndex?: number; + dstIdIndex?: number; + srcIdFunction?: string; + dstIdFunction?: string; + + constructor({ file, props }: { file?: IImportFile; props?: IPropertyProps[] }) { + makeAutoObservable(this); + file && (this.file = file); + props && this.props.replace(props); + } + update = (payload: Partial) => { + Object.keys(payload).forEach(key => Object.prototype.hasOwnProperty.call(this, key) && (this[key] = payload[key])); + }; + + updatePropItem = (index: number, payload: Partial) => { + this.props.splice(index, 1, { + ...this.props[index], + ...payload, + }); + }; +} +class ImportSchemaConfigItem { + _id = uuidv4(); + type: T; + name?: string; + props = observable.array([]); + files = observable.array([]); + + constructor({ name, type }: { type: T; name?: string }) { + makeAutoObservable(this); + this.type = type; + this.name = name; + } + + addFileItem = (item: F) => this.files.push(item); + + deleteFileItem = (fileItem: F) => this.files.remove(fileItem); + + resetFileItem = (index: number, item: F) => { + // this.files. + this.files.splice(index, 1, item); + }; + + resetConfigItem = (name: string, props: IPropertyProps[]) => { + this.name = name; + this.props.replace(props); + this.files.replace([]); + }; + + addProp = (item: IPropertyProps) => this.props.push(item); + + deleteProp = (prop: IPropertyProps) => this.props.remove(prop); + + updateProp = (prop: IPropertyProps, payload: Partial) => + Object.keys(payload).forEach((key) => (prop[key] = payload[key])); +} + +export type ITagItem = ImportSchemaConfigItem; +export type IEdgeItem = ImportSchemaConfigItem; +export type ITagFileItem = TagFileItem; +export type IEdgeFileItem = EdgeFileItem; export class ImportStore { taskList: ITaskItem[] = []; - verticesConfig: IVerticesConfig[] = []; - edgesConfig: IEdgeConfig[] = []; - - basicConfig: IBasicConfig = { taskName: '' }; + tagConfig = observable.array([], { deep: false }); + edgeConfig = observable.array([], { deep: false }); + basicConfig: IBasicConfig = { taskName: '', address: [] }; constructor() { - makeObservable(this, { + makeAutoObservable(this, { taskList: observable, - verticesConfig: observable, - edgesConfig: observable, basicConfig: observable, - - update: action, - updateVerticesConfig: action, - updateTagPropMapping: action, - updateBasicConfig: action, }); } @@ -44,7 +122,7 @@ export class ImportStore { return getRootStore(); } - update = (payload: Record) => { + update = (payload: Partial) => { Object.keys(payload).forEach(key => Object.prototype.hasOwnProperty.call(this, key) && (this[key] = payload[key])); }; @@ -73,14 +151,13 @@ export class ImportStore { _config = config; } else { const { currentSpace, spaceVidType } = this.rootStore.schema; - const { username, host } = this.rootStore.global; + const { username } = this.rootStore.global; _config = configToJson({ ...this.basicConfig, space: currentSpace, - verticesConfig: this.verticesConfig, - edgesConfig: this.edgesConfig, + tagConfig: this.tagConfig, + edgeConfig: this.edgeConfig, username, - host, password, spaceVidType }); @@ -133,14 +210,32 @@ export class ImportStore { return null; }; - updateTagConfig = async (payload: { - tag: string; - tagIndex: number; - configIndex: number; - }) => { + addTagConfig = () => this.tagConfig.unshift(new ImportSchemaConfigItem({ type: ISchemaEnum.Tag })); + deleteTagConfig = (item: ITagItem) => this.tagConfig.remove(item); + + addEdgeConfig = () => this.edgeConfig.unshift(new ImportSchemaConfigItem({ type: ISchemaEnum.Edge })); + deleteEdgeConfig = (item: IEdgeItem) => this.edgeConfig.remove(item); + + updateConfigItemName = async (item: ITagItem | IEdgeItem, name: string) => { + const props = item.type === ISchemaEnum.Tag ? await this.getTagProps(name) : await this.getEdgeProps(name); + runInAction(() => { + item.resetConfigItem(name, props); + }); + }; + + updateBasicConfig = (payload: { [K in T]?: Person[K] }) => { + Object.keys(payload).forEach(key => { + if(isEmpty(payload[key])) { + delete this.basicConfig[key]; + } else { + this.basicConfig[key] = payload[key]; + } + }); + }; + + getTagProps = async (tag: string) => { const { schema } = this.rootStore; const { getTagOrEdgeInfo, getTagOrEdgeDetail } = schema; - const { tag, tagIndex, configIndex } = payload; const { code, data } = await getTagOrEdgeInfo(ISchemaEnum.Tag, tag); const createTagGQL = await getTagOrEdgeDetail(ISchemaEnum.Tag, tag); const defaultValueFields: any[] = []; @@ -157,119 +252,41 @@ export class ImportStore { } }); } - if (code === 0) { - const props = data.tables.map(attr => handlePropertyMap(attr, defaultValueFields)); - runInAction(() => { - this.verticesConfig[configIndex].tags[tagIndex] = { - name: tag, - props - }; - }); - } + return code === 0 + ? data.tables.map(attr => handlePropertyMap(attr, defaultValueFields)) + : []; }; - - updateBasicConfig = (key: string, value: any) => { - this.basicConfig[key] = value; - }; - - updateEdgeConfig = async (payload: { edgeType?: string, index: number; }) => { - const { edgeType, index } = payload; - if(!edgeType) { - this.edgesConfig.splice(index, 1); - } else { - const { schema } = this.rootStore; - const { getTagOrEdgeInfo, getTagOrEdgeDetail, spaceVidType } = schema; - const { code, data } = await getTagOrEdgeInfo(ISchemaEnum.Edge, edgeType); - const createEdgeGQL = await getTagOrEdgeDetail(ISchemaEnum.Edge, edgeType); - const defaultValueFields: any[] = []; - if (createEdgeGQL) { - const fields = createEdgeGQL.split(/\n|\r\n/); - fields.forEach(field => { - const fieldArr = field.trim().split(/\s|\s+/); - if (field.includes('default') || fieldArr.includes('DEFAULT')) { - let defaultField = fieldArr[0]; - if (defaultField.includes('`')) { - defaultField = defaultField.replace(/`/g, ''); - } - defaultValueFields.push(defaultField); + + getEdgeProps = async (edgeType: string) => { + const { schema } = this.rootStore; + const { getTagOrEdgeInfo, getTagOrEdgeDetail } = schema; + const { code, data } = await getTagOrEdgeInfo(ISchemaEnum.Edge, edgeType); + const createEdgeGQL = await getTagOrEdgeDetail(ISchemaEnum.Edge, edgeType); + const defaultValueFields: any[] = []; + if (createEdgeGQL) { + const fields = createEdgeGQL.split(/\n|\r\n/); + fields.forEach(field => { + const fieldArr = field.trim().split(/\s|\s+/); + if (field.includes('default') || fieldArr.includes('DEFAULT')) { + let defaultField = fieldArr[0]; + if (defaultField.includes('`')) { + defaultField = defaultField.replace(/`/g, ''); } - }); - } - if (code === 0) { - const props = data.tables.map(item => handlePropertyMap(item, defaultValueFields)); - runInAction(() => { - this.edgesConfig[index].type = edgeType; - this.edgesConfig[index].props = [ - // each edge must have the three special prop srcId, dstId, rank,put them ahead - { - name: 'srcId', - type: spaceVidType === 'INT64' ? 'int' : 'string', - mapping: null, - }, - { - name: 'dstId', - type: spaceVidType === 'INT64' ? 'int' : 'string', - mapping: null, - }, - { - name: 'rank', - type: 'int', - mapping: null, - }, - ...props, - ]; - }); - } - } - }; - - updateVerticesConfig = (payload: { - index: number - key?: string, - value?: any - }) => { - const { index, key, value } = payload; - if(key) { - this.verticesConfig[index][key] = value; - } else { - this.verticesConfig.splice(index, 1); - } - }; - - updateTagPropMapping = (payload: { - configIndex: number, - tagIndex: number, - propIndex?: number, - field?: string, - value?: any - }) => { - const { configIndex, tagIndex, propIndex, field, value } = payload; - if(propIndex === undefined) { - const tags = this.verticesConfig[configIndex].tags; - tags.splice(tagIndex, 1); - } else { - const tags = this.verticesConfig[configIndex].tags; - const _tag = { ...tags[tagIndex] }; - _tag.props[propIndex][field!] = value; - tags.splice(tagIndex, 1, _tag); - } - }; - - updateEdgePropMapping = (payload: { - configIndex, - propIndex?, - field?, - value? - }) => { - const { configIndex, propIndex, field, value } = payload; - if(propIndex === undefined) { - this.edgesConfig[configIndex].type = ''; - this.edgesConfig[configIndex].props = []; - } else { - const _edge = { ...this.edgesConfig[configIndex] }; - _edge.props[propIndex][field] = value; - this.edgesConfig[configIndex] = _edge; + defaultValueFields.push(defaultField); + } + }); } + return code !== 0 + ? [] + : [ + ...data.tables.map(item => handlePropertyMap(item, defaultValueFields)), + { + name: 'rank', + type: 'int', + allowNull: true, + mapping: null, + }, + ]; }; } diff --git a/app/utils/constant.ts b/app/utils/constant.ts index 08b4c32f..12749963 100644 --- a/app/utils/constant.ts +++ b/app/utils/constant.ts @@ -214,3 +214,10 @@ export const MAX_COMMENT_BYTES = 256; export const POSITIVE_INTEGER_REGEX = /^[1-9]\d*$/g; export const DEFAULT_PARTITION_NUM = 10; + +export const DEFAULT_IMPORT_CONFIG = { + retry: 3, + concurrency: 10, + channelBufferSize: 128, + batchSize: 128 +}; \ No newline at end of file diff --git a/app/utils/function.ts b/app/utils/function.ts index 8a340dbb..c96c6901 100644 --- a/app/utils/function.ts +++ b/app/utils/function.ts @@ -53,3 +53,12 @@ export const getByteLength = (str: string) => { const utf8Encode = new TextEncoder(); return utf8Encode.encode(str).length; }; + +export const isValidIP = (ip: string) => { + const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){3}$/; + return reg.test(ip); +}; + +export const isEmpty = (value: any) => { + return !value && value !== 0; +}; \ No newline at end of file diff --git a/app/utils/import.ts b/app/utils/import.ts index 901ca810..db98e330 100644 --- a/app/utils/import.ts +++ b/app/utils/import.ts @@ -1,42 +1,54 @@ -import { message } from 'antd'; -import _ from 'lodash'; -import { getI18n } from '@vesoft-inc/i18n'; -import { handleEscape } from './function'; +import { IBasicConfig } from '@app/interfaces/import'; +import { IEdgeItem, ITagItem } from '@app/stores/import'; +import { handleEscape, isEmpty } from './function'; +import { DEFAULT_IMPORT_CONFIG } from './constant'; -export function configToJson(payload) { +interface IConfig extends IBasicConfig { + space: string; + tagConfig: ITagItem[]; + edgeConfig: IEdgeItem[]; + username: string; + password: string; + spaceVidType: string; +} + +export function configToJson(payload: IConfig) { const { space, username, password, - host, - verticesConfig, - edgesConfig, + tagConfig, + edgeConfig, spaceVidType, - batchSize + batchSize, + address, + concurrency, + retry, + channelBufferSize } = payload; - const vertexToJSON = vertexDataToJSON( - verticesConfig, + const vertexToJSON = tagDataToJSON( + tagConfig, spaceVidType, batchSize ); const edgeToJSON = edgeDataToJSON( - edgesConfig, + edgeConfig, spaceVidType, batchSize ); const files: any[] = [...vertexToJSON, ...edgeToJSON]; const configJson = { version: 'v2', - description: 'web console import', + description: 'studio import', clientSettings: { - retry: 3, - concurrency: 10, - channelBufferSize: 128, + retry: Number(retry ?? DEFAULT_IMPORT_CONFIG.retry), + concurrency: Number(concurrency ?? DEFAULT_IMPORT_CONFIG.concurrency), + channelBufferSize: Number(channelBufferSize ?? DEFAULT_IMPORT_CONFIG.channelBufferSize), space: handleEscape(space), connection: { user: username, password, - address: host, + address: address.join(', ') }, }, files, @@ -45,126 +57,116 @@ export function configToJson(payload) { } export function edgeDataToJSON( - config: any, + configs: IEdgeItem[], spaceVidType: string, batchSize?: string, ) { - const files = config.map(edge => { - const edgePorps: any[] = []; - _.sortBy(edge.props, t => t.mapping).forEach(prop => { - switch (prop.name) { - case 'rank': - if (prop.mapping !== null) { - edge.rank = { - index: prop.mapping, - }; - } - break; - case 'srcId': - edge.srcVID = { - index: indexJudge(prop.mapping, prop.name), - type: spaceVidType === 'INT64' ? 'int' : 'string', - }; - break; - case 'dstId': - edge.dstVID = { - index: indexJudge(prop.mapping, prop.name), - type: spaceVidType === 'INT64' ? 'int' : 'string', - }; - break; - default: - if (prop.mapping === null && (prop.allowNull || prop.isDefault)) { - break; - } - const _prop = { - name: handleEscape(prop.name), - type: prop.type, - index: indexJudge(prop.mapping, prop.name), - }; - edgePorps.push(_prop); - } - }); - const edgeConfig = { - path: edge.file.name, - batchSize: Number(batchSize) || 60, - type: 'csv', - csv: { - withHeader: false, - withLabel: false, - }, - schema: { - type: 'edge', - edge: { - name: handleEscape(edge.type), - srcVID: edge.srcVID, - dstVID: edge.dstVID, - rank: edge.rank, - withRanking: edge.rank?.index !== undefined, - props: edgePorps, + const result = configs.reduce((acc: any, cur) => { + const { name, files } = cur; + const _config = files.map(item => { + const { file, props, srcIdIndex, srcIdFunction, dstIdIndex, dstIdFunction } = item; + const vidType = spaceVidType === 'INT64' ? 'int' : 'string'; + // rank is the last prop + const rank = props[props.length - 1]; + const edgeProps = props.slice(0, -1).reduce((acc: any, cur) => { + if (isEmpty(cur.mapping) && (cur.allowNull || cur.isDefault)) { + return acc; + } + acc.push({ + name: handleEscape(cur.name), + type: cur.type, + index: cur.mapping, + }); + return acc; + }, []); + const edgeConfig = { + path: file.name, + batchSize: Number(batchSize) || DEFAULT_IMPORT_CONFIG.batchSize, + type: 'csv', + csv: { + withHeader: file.withHeader || false, + withLabel: false, + delimiter: file.delimiter }, - }, - }; - return edgeConfig; - }); - return files; + schema: { + type: 'edge', + edge: { + name: handleEscape(name), + srcVID: { + index: srcIdIndex, + function: srcIdFunction, + type: vidType, + }, + dstVID: { + index: dstIdIndex, + function: dstIdFunction, + type: vidType, + }, + rank: { index: rank.mapping }, + props: edgeProps, + }, + }, + }; + return edgeConfig; + }); + acc.push(..._config); + return acc; + }, []); + return result; } -export function vertexDataToJSON( - config: any, +export function tagDataToJSON( + configs: ITagItem[], spaceVidType: string, batchSize?: string ) { - const files = config.map(vertex => { - const tags = vertex.tags.map(tag => { - const props = tag.props - .sort((p1, p2) => p1.mapping - p2.mapping) - .map(prop => { - if (prop.mapping === null && (prop.allowNull || prop.isDefault)) { - return null; - } - return { - name: handleEscape(prop.name), - type: prop.type, - index: indexJudge(prop.mapping, prop.name), - }; + const result = configs.reduce((acc: any, cur) => { + const { name, files } = cur; + const _config = files.map(item => { + const { file, props, vidIndex, vidFunction, vidPrefix } = item; + const _props = props.reduce((acc: any, cur) => { + if (isEmpty(cur.mapping) && (cur.allowNull || cur.isDefault)) { + return acc; + } + acc.push({ + name: handleEscape(cur.name), + type: cur.type, + index: cur.mapping, }); - const _tag = { - name: handleEscape(tag.name), - props: props.filter(prop => prop), + return acc; + }, []); + + const tags = [{ + name: handleEscape(name), + props: _props.filter(prop => prop), + }]; + return { + path: file.name, + batchSize: Number(batchSize) || DEFAULT_IMPORT_CONFIG.batchSize, + type: 'csv', + csv: { + withHeader: file.withHeader || false, + withLabel: false, + delimiter: file.delimiter + }, + schema: { + type: 'vertex', + vertex: { + vid: { + index: vidIndex, + function: vidFunction, + type: spaceVidType === 'INT64' ? 'int' : 'string', + prefix: vidPrefix, + }, + tags, + }, + } }; - return _tag; }); - const vertexConfig: any = { - path: vertex.file.name, - batchSize: Number(batchSize) || 60, - type: 'csv', - csv: { - withHeader: false, - withLabel: false, - }, - schema: { - type: 'vertex', - vertex: { - vid: { - index: indexJudge(vertex.idMapping, 'vertexId'), - type: spaceVidType === 'INT64' ? 'int' : 'string', - }, - tags, - }, - }, - }; - return vertexConfig; - }); - return files; -} - -export function indexJudge(index: number | null, name: string) { - if (index === null) { - const { intl } = getI18n(); - message.error(`${name} ${intl.get('import.indexNotEmpty')}`); - throw new Error(); - } - return index; + acc.push(..._config); + return acc; + }, []); + return result; } export const exampleJson = { @@ -257,7 +259,6 @@ export const exampleJson = { 'type': 'edge', 'edge': { 'name': 'order', - 'withRanking': false, 'props': [ { 'name': 'order_id', @@ -294,13 +295,11 @@ export const exampleJson = { 'index': 1, 'function': null, 'type': 'string', - 'prefix': null }, 'dstVID': { 'index': 1, 'function': null, 'type': 'string', - 'prefix': null }, 'rank': null }, diff --git a/config/webpack.base.js b/config/webpack.base.js index fa7afa14..c0ebb3f8 100644 --- a/config/webpack.base.js +++ b/config/webpack.base.js @@ -42,7 +42,7 @@ const commonConfig = { lessOptions: { javascriptEnabled: true, modifyVars: { - 'primary-color': '#2F80ED', + 'primary-color': '#0091FF', 'menu-dark-bg': '#2F3A4A', 'table-header-bg': '#E9EDEF', 'table-header-color': '#465B7A', diff --git a/server/api/studio/internal/service/import.go b/server/api/studio/internal/service/import.go index af170b8f..496ba581 100644 --- a/server/api/studio/internal/service/import.go +++ b/server/api/studio/internal/service/import.go @@ -69,13 +69,11 @@ func (i *importService) CreateImportTask(req *types.CreateImportTaskRequest) (*t if err != nil { return nil, ecode.WithErrorMessage(ecode.ErrParam, err) } - conf := importconfig.YAMLConfig{} err = json.Unmarshal(jsons, &conf) if err != nil { return nil, ecode.WithErrorMessage(ecode.ErrInternalServer, err) } - if err = validClientParams(&conf); err != nil { err = importererrors.Wrap(importererrors.InvalidConfigPathOrFormat, err) zap.L().Warn("client params is wrong", zap.Error(err)) diff --git a/server/api/studio/internal/types/types.go b/server/api/studio/internal/types/types.go index a4ee0dad..b5c92851 100644 --- a/server/api/studio/internal/types/types.go +++ b/server/api/studio/internal/types/types.go @@ -90,7 +90,7 @@ type ImportTaskVID struct { type ImportTaskTagProp struct { Name *string `json:"name" validate:"required"` Type *string `json:"type" validate:"required"` - Index *int64 `json:"index" validate:"required"` + Index *int64 `json:"index, optional"` } type ImportTaskTag struct { @@ -111,13 +111,13 @@ type ImportTaskEdgeID struct { } type ImportTaskEdgeRank struct { - Index *int64 `json:"index"` + Index *int64 `json:"index, optional"` } type ImportTaskEdgeProp struct { Name *string `json:"name"` Type *string `json:"type"` - Index *int64 `json:"index"` + Index *int64 `json:"index, optional"` } type ImportTaskEdge struct { diff --git a/server/api/studio/restapi/import.api b/server/api/studio/restapi/import.api index e06b7497..cb87b7a8 100644 --- a/server/api/studio/restapi/import.api +++ b/server/api/studio/restapi/import.api @@ -42,7 +42,7 @@ type ( ImportTaskTagProp { Name *string `json:"name" validate:"required"` Type *string `json:"type" validate:"required"` - Index *int64 `json:"index" validate:"required"` + Index *int64 `json:"index, optional"` } ImportTaskTag { @@ -63,13 +63,13 @@ type ( } ImportTaskEdgeRank { - Index *int64 `json:"index"` + Index *int64 `json:"index, optional"` } ImportTaskEdgeProp { Name *string `json:"name"` Type *string `json:"type"` - Index *int64 `json:"index"` + Index *int64 `json:"index, optional"` } ImportTaskEdge {