diff --git a/app/config/locale/en-US.json b/app/config/locale/en-US.json index 2956fa42..fb3e8789 100644 --- a/app/config/locale/en-US.json +++ b/app/config/locale/en-US.json @@ -19,7 +19,6 @@ "confirm": "Confirm", "import": "Import", "ask": "Are you sure to proceed?", - "output": "Export CSV File", "openInExplore": "Open In Explorer", "schema": "Schema", "create": "Create", @@ -77,7 +76,7 @@ "username": "Username", "password": "Password", "success": "succeed", - "clear": "Clear Connection", + "clear": "Log out", "title": "Connect to Nebula Graph" }, "formRules": { @@ -85,6 +84,7 @@ "usernameRequired": "Username Required", "passwordRequired": "Password Required", "positiveIntegerRequired": "Please enter a non-negative integer", + "nameValidate": "The name must start with a letter, and it only supports English letters, numbers and underscores", "nameRequired": "Please enter the name", "numberRequired": "Please enter a positive integer", "replicaLimit": "Replica factor must not exceed the number of your current online machines({number})", @@ -130,7 +130,6 @@ "fileSize": "Size", "fileTitle": "Select Files", "bindDatasource": "Bind Datasource", - "confirm": "Confirm", "endImport": "Stop Import", "prop": "Prop", "mapping": "CSV Index", @@ -154,10 +153,15 @@ "addTag": "Add Tag", "config": "Task Config", "parseFailed": "File parsing failed", - "uploadTemplate": "Click or drag the yaml configuration file to this area to upload", - "uploadTemplateTip": "Please keep only the file name (retain the file extension) for all file paths in the template, such as logPath: config.csv", - "fileUploadRequired": "Make sure all csv data files are uploaded before uploading the configuration", - "reUpload": "Re-upload" + "uploadTemplate": "Drag & drop the YAML configuration file to this area", + "uploadBoxTip": "The YAML configuration file is used to describe information about the files to be imported, the Nebula Graph server, and more. ", + "fileUploadRequired": "1. Please make sure all CSV data files are uploaded before import the YAML file. If not, please go to ", + "fileUploadRequired2": " first.", + "exampleDownload": "2. An example for the configuration file: ", + "uploadTemplateTip": "3. 3. Configure the Yaml file: please keep only the file name (retain the file extension) for all file paths (path, failDataPath, logPath) in the template, e.g. logPath: config.csv", + "reUpload": "Re-upload", + "fileNotExist": "{name} file does not exist!", + "importYaml": "Import the YAML file" }, "schema": { "spaceList": "Graph Space List", @@ -215,11 +219,13 @@ "deleteSpace": "Delete Graph Space", "cloneSpace": "Clone Graph Space", "length": "Length", - "selectVidTypeTip": "Please select the type" + "selectVidTypeTip": "Please select the type", + "csvDownload": "Export CSV File", + "pngDownload": "Export PNG File" }, "menu": { "use": "Use Manual", - "release": "New Version", + "release": "Release Note", "forum": "Help Forum", "nGql": "nGQL" }, diff --git a/app/config/locale/zh-CN.json b/app/config/locale/zh-CN.json index 170ed0f2..2bfe14b1 100644 --- a/app/config/locale/zh-CN.json +++ b/app/config/locale/zh-CN.json @@ -19,7 +19,6 @@ "confirm": "确认", "import": "导入", "ask": "确定进行当前操作?", - "output":"导出CSV文件", "openInExplore": "导入图探索", "schema": "Schema", "create": "创建", @@ -77,7 +76,7 @@ "username": "用户名", "password": "密码", "success": "配置成功", - "clear": "清除连接", + "clear": "登出", "title": "配置数据库" }, "formRules": { @@ -85,6 +84,7 @@ "usernameRequired": "请填写用户名", "passwordRequired": "请填写密码", "positiveIntegerRequired": "请输入一个非负整数", + "nameValidate": "命名必须以字母开头,且只支持输入英文字母、数字以及下划线_", "nameRequired": "请输入名称", "numberRequired": "请输入正整数", "replicaLimit": "副本数量不得超过你当前 online 机器数量({number})", @@ -130,7 +130,6 @@ "fileSize": "大小", "fileTitle": "文件列表", "bindDatasource": "绑定数据源", - "confirm": "确认", "endImport": "终止导入", "prop": "属性", "mapping": "对应列标", @@ -154,10 +153,15 @@ "addTag": "添加 Tag", "config": "任务配置", "parseFailed": "文件解析失败", - "uploadTemplate": "点击或拖动yaml配置文件到该区域上传", - "uploadTemplateTip": "模板中所有文件路径请仅保留文件名(保留文件扩展名),比如 logPath: config.csv", - "fileUploadRequired": "上传配置前请确保所有 csv 数据文件已上传", - "reUpload": "重新上传" + "uploadTemplate": "将 YAML 配置文件拖放到该区域", + "uploadBoxTip": "The YAML configuration file is used to describe information about the files to be imported, the Nebula Graph server, and more. ", + "fileUploadRequired": "1. 请确保在导入 YAML 文件之前上传所有 CSV 数据文件。 如果没有,请先前往", + "fileUploadRequired2": "数据文件", + "exampleDownload": "2. 配置文件示例:", + "uploadTemplateTip": "3.配置Yaml文件:模板中所有文件路径(path、failDataPath、logPath)请只保留文件名(保留文件扩展名),例如: 日志路径:config.csv", + "reUpload": "重新上传", + "fileNotExist": "文件 {name} 不存在", + "importYaml": "导入 YAML 文件" }, "schema": { "spaceList": "图空间列表", @@ -215,11 +219,13 @@ "deleteSpace": "删除图空间", "cloneSpace": "克隆图空间", "length": "长度", - "selectVidTypeTip": "选择 Vid 类型" + "selectVidTypeTip": "选择 Vid 类型", + "csvDownload": "导出 CSV", + "pngDownload": " 导出 PNG" }, "menu": { "use": "使用手册", - "release": "新发布", + "release": "更新日志", "forum": "求助论坛", "nGql": "nGQL" }, diff --git a/app/config/rules.ts b/app/config/rules.ts index baa83422..0d0ad482 100644 --- a/app/config/rules.ts +++ b/app/config/rules.ts @@ -1,4 +1,4 @@ -import { POSITIVE_INTEGER_REGEX } from '@app/utils/constant'; +import { NAME_REGEX, POSITIVE_INTEGER_REGEX } from '@app/utils/constant'; import intl from 'react-intl-universal'; export const hostRulesFn = () => [ @@ -22,12 +22,22 @@ export const passwordRulesFn = () => [ }, ]; -export const nameRulesFn = () => [ - { - required: true, - message: intl.get('formRules.nameRequired'), - }, -]; +export const nameRulesFn = () => { + const version = sessionStorage.getItem('nebulaVersion'); + const nameRequired = [ + { + required: true, + message: intl.get('formRules.nameRequired'), + }, + ]; + const nameValidate = [ + { + pattern: NAME_REGEX, + message: intl.get('formRules.nameValidate'), + }, + ]; + return version?.startsWith('v2') ? [...nameRequired, ...nameValidate] : nameRequired; +}; export const numberRulesFn = () => [ { diff --git a/app/pages/Console/ExportModal.tsx b/app/pages/Console/ExportModal.tsx index 115678d9..92acd8e1 100644 --- a/app/pages/Console/ExportModal.tsx +++ b/app/pages/Console/ExportModal.tsx @@ -84,7 +84,7 @@ const ExportModal = (props: IProps) => { - + {({ getFieldValue }) => { const type = getFieldValue('type'); return type === 'vertex' ? <> @@ -133,7 +133,7 @@ const ExportModal = (props: IProps) => { ; }} - + + + } + > + + {visible ? setVisible(false)} @@ -293,7 +330,7 @@ const OutputBox = (props: IProps) => { } key="graph" > - + )} {code !== 0 && ( diff --git a/app/pages/Console/index.less b/app/pages/Console/index.less index 0747e6fb..300bbfd9 100644 --- a/app/pages/Console/index.less +++ b/app/pages/Console/index.less @@ -82,6 +82,7 @@ border: 1px solid @gray; border-top: none; cursor: pointer; + opacity: 0.7; svg { fill: @darkBlue; } diff --git a/app/pages/Console/index.tsx b/app/pages/Console/index.tsx index ec735c65..12e0060e 100644 --- a/app/pages/Console/index.tsx +++ b/app/pages/Console/index.tsx @@ -39,7 +39,7 @@ const Console = (props: IProps) => { const { onExplorer } = props; const { spaces, getSpaces, switchSpace, currentSpace } = schema; const { runGQL, currentGQL, results, runGQLLoading, getParams, update, paramsMap } = console; - const { username, host } = global; + const { username, host, nebulaVersion } = global; const [isUpDown, setUpDown] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [modalData, setModalData] = useState(null); @@ -154,7 +154,7 @@ const Console = (props: IProps) => {
- + {nebulaVersion?.startsWith('v3') && } update({ currentGQL: value })} diff --git a/app/pages/Doc/index.tsx b/app/pages/Doc/index.tsx index 60158192..6546f0f4 100644 --- a/app/pages/Doc/index.tsx +++ b/app/pages/Doc/index.tsx @@ -20,7 +20,7 @@ const MODULES = [ icon: 'icon-studio-nav-import', title: 'import.importData', tip: 'doc.importIntro', - link: '/import' + link: '/import/files' }, { icon: 'icon-studio-nav-console', diff --git a/app/pages/Import/FileUpload/index.tsx b/app/pages/Import/FileUpload/index.tsx index cd86c5b6..55610c6b 100644 --- a/app/pages/Import/FileUpload/index.tsx +++ b/app/pages/Import/FileUpload/index.tsx @@ -13,7 +13,7 @@ import './index.less'; const FileUpload = () => { const { files } = useStore(); - const { fileList, uploadDir, asyncDeleteFile, asyncGetFiles, asyncUploadFile, asyncGetUploadDir } = files; + const { fileList, uploadDir, deleteFile, getFiles, uploadFile, getUploadDir } = files; const [loading, setLoading] = useState(false); const transformFile = async file => { file.path = `${uploadDir}/${file.name}`; @@ -30,14 +30,14 @@ const FileUpload = () => { 'content-type': 'multipart/form-data', }, }; - await asyncUploadFile({ data, config }).then(_ => { - asyncGetFiles(); + await uploadFile({ data, config }).then(_ => { + getFiles(); }); setLoading(false); }; useEffect(() => { - asyncGetFiles(); - asyncGetUploadDir(); + getFiles(); + getUploadDir(); trackPageView('/import/files'); }, []); const columns = [ @@ -67,7 +67,7 @@ const FileUpload = () => { asyncDeleteFile(index)} + onConfirm={() => deleteFile(index)} title={intl.get('common.ask')} okText={intl.get('common.ok')} cancelText={intl.get('common.cancel')} diff --git a/app/pages/Import/TaskCreate/FileSelect/index.tsx b/app/pages/Import/TaskCreate/FileSelect/index.tsx index d9ecd754..36c651cb 100644 --- a/app/pages/Import/TaskCreate/FileSelect/index.tsx +++ b/app/pages/Import/TaskCreate/FileSelect/index.tsx @@ -17,7 +17,7 @@ const FormItem = Form.Item; const FileSelect = (props: IProps) => { const { type } = props; const { files, dataImport: { update, verticesConfig, edgesConfig } } = useStore(); - const { fileList, asyncGetFiles } = files; + const { fileList, getFiles } = files; const [visible, setVisible] = useState(false); const onFinish = (value) => { const file = fileList.filter(item => item.name === value.name)[0]; @@ -43,7 +43,7 @@ const FileSelect = (props: IProps) => { setVisible(false); }; useEffect(() => { - asyncGetFiles(); + getFiles(); }, []); return ( { } diff --git a/app/pages/Import/TaskList/TaskItem/LogModal/index.less b/app/pages/Import/TaskList/TaskItem/LogModal/index.less index 4a3d09f7..66bf2260 100644 --- a/app/pages/Import/TaskList/TaskItem/LogModal/index.less +++ b/app/pages/Import/TaskList/TaskItem/LogModal/index.less @@ -33,7 +33,7 @@ height: 100%; overflow: auto; padding: 10px 20px 120px; - font-size: 18px; + font-size: 12px; text-align: left; background: #333; color: #fff; @@ -55,6 +55,9 @@ color: #fff; } } + .ant-tabs-nav-list { + background-color: @lightGray; + } } .ant-tabs-content-holder > .ant-tabs-content { display: none diff --git a/app/pages/Import/TaskList/TaskItem/index.less b/app/pages/Import/TaskList/TaskItem/index.less index be8d1eff..18175080 100644 --- a/app/pages/Import/TaskList/TaskItem/index.less +++ b/app/pages/Import/TaskList/TaskItem/index.less @@ -25,6 +25,7 @@ display: flex; justify-content: space-between; margin-bottom: 12px; + padding-right: calc(2em + 8px); .task-name { font-family: Roboto-Bold, serif; font-style: normal; diff --git a/app/pages/Import/TaskList/TemplateModal/index.less b/app/pages/Import/TaskList/TemplateModal/index.less index 2269e8f2..e61d1c15 100644 --- a/app/pages/Import/TaskList/TemplateModal/index.less +++ b/app/pages/Import/TaskList/TemplateModal/index.less @@ -1,25 +1,63 @@ @import '~@app/common.less'; .config-import-modal { .ant-modal-body { - width: 100%; - padding-top: 50px; + width:750px; + margin: 0 auto; .tip { - background: @lightBlue; - font-size: 16px; - padding: 10px; + font-size: 14px; + color: #828282; + } + .btn-example-download { + padding-left: 0; } } .dragger-template { margin-top: 30px; - .btn-add-file svg{ - width: 30px; - height: 30px; + background: #FFFFFF; + border: 1px dashed #000000; + .ant-upload-btn { + padding: 27px 30px; + & > .ant-upload-drag-container { + display: flex; + } + .btn-add-file svg{ + width: 95px; + height: 95px; + fill: @darkBlue; + } + .drag-tip { + display: flex; + flex-direction: column; + margin-left: 28px; + align-items: flex-start; + text-align: left; + } + .ant-upload-text { + font-weight: 400; + font-size: 18px; + } + .ant-upload-hint { + font-weight: 400; + font-size: 14px; + color: #828282; + } } } .ant-form { - text-align: right; - button:not(:last-child) { - margin-right: 10px; + text-align: center; + .config-area { + color: @darkBlue; + max-height: 360px !important; + overflow: auto; + &[disabled] { + background-color: #fff; + } + } + button { + width: 140px; + &:not(:last-child) { + margin-right: 10px; + } } } } \ No newline at end of file diff --git a/app/pages/Import/TaskList/TemplateModal/index.tsx b/app/pages/Import/TaskList/TemplateModal/index.tsx index 3bc2d75b..5505fdd8 100644 --- a/app/pages/Import/TaskList/TemplateModal/index.tsx +++ b/app/pages/Import/TaskList/TemplateModal/index.tsx @@ -3,11 +3,14 @@ import _ from 'lodash'; import React, { useEffect, useState } from 'react'; import intl from 'react-intl-universal'; import Icon from '@app/components/Icon'; +import { Link } from 'react-router-dom'; import './index.less'; import { observer } from 'mobx-react-lite'; import { useStore } from '@app/stores'; import { readFileContent } from '@app/utils/file'; import yaml from 'js-yaml'; +import json2yaml from 'json2yaml'; +import { exampleJson } from '@app/utils/import'; const { Dragger } = Upload; const { TextArea } = Input; @@ -22,33 +25,44 @@ const TemplateModal = (props: IProps) => { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [config, setConfig] = useState(''); - const { dataImport: { getTaskDir, taskDir, importTask }, files: { uploadDir, asyncGetUploadDir } } = useStore(); + const { dataImport: { getTaskDir, taskDir, importTask }, files: { uploadDir, getUploadDir, getFiles, fileList } } = useStore(); useEffect(() => { if(!uploadDir) { - asyncGetUploadDir(); + getUploadDir(); } getTaskDir(); + getFiles(); }, []); const handleFileImport = async ({ file }) => { await setLoading(true); + const files = fileList.map(item => item.name); const content = await readFileContent(file); const parseContent = yaml.load(content); - if(typeof parseContent === 'object') { - const _taskDir = taskDir.endsWith('/') ? taskDir : taskDir + '/'; - const _uploadDir = uploadDir.endsWith('/') ? uploadDir : uploadDir + '/'; - parseContent.logPath = _taskDir + parseContent.logPath; - parseContent.files.forEach(file => { - file.path = _uploadDir + file.path; - file.failDataPath = _taskDir + `err/${file.failDataPath}`; - }); - setConfig(JSON.stringify(parseContent, null, 2)); - form.setFieldsValue({ - content: JSON.stringify(parseContent, null, 2), - }); - } else { - return message.warning(intl.get('import.parseFailed')); + try { + if(typeof parseContent === 'object') { + const _taskDir = taskDir.endsWith('/') ? taskDir : taskDir + '/'; + const _uploadDir = uploadDir.endsWith('/') ? uploadDir : uploadDir + '/'; + parseContent.logPath = `${_taskDir}/import.log`; + parseContent.files?.forEach(file => { + if(!files.includes(file.path)) { + message.error(intl.get('import.fileNotExist', { name: file.path })); + throw new Error(); + } + file.path = _uploadDir + file.path; + file.failDataPath = _taskDir + `err/${file.failDataPath || 'err.log'}`; + }); + setConfig(JSON.stringify(parseContent, null, 2)); + form.setFieldsValue({ + content: JSON.stringify(parseContent, null, 2), + }); + } else { + return message.warning(intl.get('import.parseFailed')); + } + } catch (err) { + console.log('err', err); + } finally { + setLoading(false); } - await setLoading(false); }; const handleImport = async (values) => { @@ -59,39 +73,62 @@ const TemplateModal = (props: IProps) => { } onClose(); }; + + const handleTemplateDownload = () => { + const ymlStr = '### Please refer to the repo (https://github.com/vesoft-inc/nebula-importer) for configuration details' + json2yaml.stringify(exampleJson); + const blob = new Blob([ymlStr]); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = `template.yaml`; + link.click(); + }; return ( {!config ? -

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

+

+ {intl.get('import.fileUploadRequired')} + {intl.get('import.uploadFile')} + {intl.get('import.fileUploadRequired2')} +

+

+ {intl.get('import.exampleDownload')} + +

+

+ {intl.get('import.uploadTemplateTip')} +

false} onChange={handleFileImport} showUploadList={false} accept=".yaml, .yml">
-

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

-

- {intl.get('import.uploadTemplateTip')} -

+
+

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

+

+ {intl.get('import.uploadBoxTip')} +

+
:
- - - - -