From fd6b7ae04ccc2b47e82755499c3b6e39339fdfc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=8B=E6=B1=9F=E5=AE=9E=E9=AA=8C=E5=AE=A4?= Date: Mon, 26 Oct 2020 16:08:08 +0800 Subject: [PATCH] update webapp --- webapp/.env.development | 4 +- webapp/.env.test | 13 + webapp/.eslintrc.js | 24 +- webapp/CHANGELOG.md | 26 + webapp/LICENSE | 211 + webapp/README.md | 72 +- webapp/package.json | 7 +- webapp/src/App.vue | 4 +- webapp/src/api/preparation/annotation.js | 11 +- webapp/src/api/preparation/datalabel.js | 8 + webapp/src/api/preparation/dataset.js | 29 +- webapp/src/api/preparation/labelGroup.js | 89 + webapp/src/api/system/harbor.js | 2 +- webapp/src/api/system/pod.js | 50 + webapp/src/api/trainingImage/index.js | 22 +- webapp/src/api/trainingJob/job.js | 19 +- webapp/src/assets/styles/atomic.scss | 4 + webapp/src/assets/styles/common.scss | 19 +- webapp/src/assets/styles/element-ui.scss | 32 + webapp/src/assets/styles/index.scss | 22 +- webapp/src/assets/styles/variables.scss | 1 + webapp/src/components/BaseModal/index.js | 4 +- webapp/src/components/Crud/CD.operation.vue | 2 + webapp/src/components/Crud/RR.operation.vue | 2 + webapp/src/components/Drag/drag.vue | 133 + webapp/src/components/Drag/index.js | 19 + webapp/src/components/Exception/index.vue | 9 +- webapp/src/components/IconFont/index.js | 2 +- webapp/src/components/ImageGallery/index.vue | 14 +- .../src/components/InfoSelect/info-select.vue | 21 +- webapp/src/components/LabelPopover/index.vue | 217 - webapp/src/components/LogContainer/index.vue | 125 + .../Training/dataSourceSelector.vue | 243 + webapp/src/components/Training/jobForm.vue | 736 +- webapp/src/components/Training/paramPair.vue | 135 + .../src/components/Training/runParamForm.vue | 261 +- .../components/Training/saveModelDialog.vue | 8 +- webapp/src/components/UploadForm/form.js | 11 +- webapp/src/components/UploadForm/inline.vue | 2 +- .../src/components/UploadProgress/index.vue | 93 + webapp/src/components/svg/brush/Brush.js | 217 + .../src/components/svg/brush/BrushCorner.js | 227 + .../src/components/svg/brush/BrushHandle.js | 193 + .../components/svg/brush/BrushSelection.js | 55 + webapp/src/components/svg/brush/index.js | 19 + webapp/src/components/svg/group/index.js | 42 + webapp/src/components/svg/index.js | 18 + webapp/src/config/index.js | 6 +- webapp/src/hooks/brush/useBrush.js | 38 + webapp/src/hooks/zoom/useZoom.js | 5 + webapp/src/layout/BaseLayout.vue | 3 +- .../src/layout/components/AppMain/index.vue | 1 + .../src/layout/components/Feedback/index.vue | 6 +- webapp/src/store/modules/dataset.js | 13 + webapp/src/utils/base.js | 26 +- webapp/src/utils/event.js | 49 +- webapp/src/utils/request.js | 4 + webapp/src/utils/utils.js | 89 + webapp/src/utils/validate.js | 27 + webapp/src/views/algorithm/index.vue | 134 +- webapp/src/views/dashboard/dashboard.vue | 56 +- webapp/src/views/dataset/annotate/index.vue | 156 +- .../annotate/settingContainer/annotations.vue | 2 +- .../annotate/settingContainer/enhance.vue | 2 +- .../annotate/settingContainer/index.vue | 77 +- .../settingContainer/labelList/edit.vue | 135 + .../{labelList.vue => labelList/index.vue} | 79 +- .../annotate/settingContainer/selectLabel.vue | 16 +- .../dataset/annotate/thumbContainer/index.vue | 20 +- .../dataset/annotate/thumbContainer/list.vue | 12 +- .../workSpaceContainer/annotationId.js | 49 +- .../annotate/workSpaceContainer/bbox.js | 66 +- .../workSpaceContainer/bboxWrapper.js | 430 + .../annotate/workSpaceContainer/brushTip.js | 89 + .../annotate/workSpaceContainer/index.vue | 428 +- .../annotate/workSpaceContainer/score.js | 45 +- .../annotate/workSpaceContainer/tag.js | 47 +- webapp/src/views/dataset/classify.vue | 83 +- .../dataset/components/picInfoModal/index.vue | 2 +- webapp/src/views/dataset/list/action.js | 132 +- .../src/views/dataset/list/create-dataset.vue | 622 + .../src/views/dataset/list/edit-dataset.vue | 345 + .../src/views/dataset/list/import-dataset.vue | 137 +- webapp/src/views/dataset/list/index.vue | 1308 +- webapp/src/views/dataset/list/status.js | 11 +- .../views/dataset/list/upload-datafile.vue | 259 + webapp/src/views/dataset/style/list.scss | 262 + webapp/src/views/dataset/util.js | 93 +- .../development/components/CreateDialog.vue | 8 +- webapp/src/views/development/notebook.vue | 82 +- webapp/src/views/labelGroup/dynamicField.vue | 117 + webapp/src/views/labelGroup/index.vue | 359 + .../src/views/labelGroup/labelGroupAction.js | 89 + .../src/views/labelGroup/labelGroupForm.vue | 654 + .../views/model/components/addModelDialog.vue | 61 +- webapp/src/views/model/index.vue | 101 +- webapp/src/views/model/version.vue | 84 +- webapp/src/views/system/dict/dictDetail.vue | 15 +- webapp/src/views/system/user/index.vue | 71 +- webapp/src/views/trainingImage/index.vue | 177 +- webapp/src/views/trainingJob/add.vue | 58 +- .../trainingJob/components/jobDetail.vue | 237 +- .../trainingJob/components/jobDrawer.vue | 320 +- .../components/pathSelectDialog.vue | 206 + webapp/src/views/trainingJob/detail.vue | 142 +- webapp/src/views/trainingJob/index.vue | 62 +- webapp/src/views/trainingJob/jobList.vue | 7 +- webapp/src/views/trainingJob/jobParam.vue | 82 +- webapp/src/views/user/center.vue | 2 +- webapp/src/views/visual/Layout.vue | 250 +- .../Visual/exception/excepContainer.vue | 4 +- .../drawStatistic/statisticContainer.vue | 2 +- webapp/yarn.lock | 12802 ++++++++++++++++ 113 files changed, 22128 insertions(+), 2705 deletions(-) create mode 100644 webapp/.env.test create mode 100644 webapp/CHANGELOG.md create mode 100755 webapp/LICENSE create mode 100644 webapp/src/api/preparation/labelGroup.js create mode 100644 webapp/src/api/system/pod.js create mode 100644 webapp/src/components/Drag/drag.vue create mode 100644 webapp/src/components/Drag/index.js delete mode 100644 webapp/src/components/LabelPopover/index.vue create mode 100644 webapp/src/components/LogContainer/index.vue create mode 100644 webapp/src/components/Training/dataSourceSelector.vue create mode 100644 webapp/src/components/Training/paramPair.vue create mode 100644 webapp/src/components/UploadProgress/index.vue create mode 100644 webapp/src/components/svg/brush/Brush.js create mode 100644 webapp/src/components/svg/brush/BrushCorner.js create mode 100644 webapp/src/components/svg/brush/BrushHandle.js create mode 100644 webapp/src/components/svg/brush/BrushSelection.js create mode 100644 webapp/src/components/svg/brush/index.js create mode 100644 webapp/src/components/svg/group/index.js create mode 100644 webapp/src/components/svg/index.js create mode 100644 webapp/src/views/dataset/annotate/settingContainer/labelList/edit.vue rename webapp/src/views/dataset/annotate/settingContainer/{labelList.vue => labelList/index.vue} (53%) create mode 100644 webapp/src/views/dataset/annotate/workSpaceContainer/bboxWrapper.js create mode 100644 webapp/src/views/dataset/annotate/workSpaceContainer/brushTip.js create mode 100644 webapp/src/views/dataset/list/create-dataset.vue create mode 100644 webapp/src/views/dataset/list/edit-dataset.vue create mode 100644 webapp/src/views/dataset/list/upload-datafile.vue create mode 100644 webapp/src/views/dataset/style/list.scss create mode 100644 webapp/src/views/labelGroup/dynamicField.vue create mode 100644 webapp/src/views/labelGroup/index.vue create mode 100644 webapp/src/views/labelGroup/labelGroupAction.js create mode 100644 webapp/src/views/labelGroup/labelGroupForm.vue create mode 100644 webapp/src/views/trainingJob/components/pathSelectDialog.vue create mode 100644 webapp/yarn.lock diff --git a/webapp/.env.development b/webapp/.env.development index 9051227..702f9a5 100644 --- a/webapp/.env.development +++ b/webapp/.env.development @@ -7,7 +7,7 @@ VUE_APP_BASE_API = '' VUE_APP_DATA_API = '' # minio -VUE_APP_MINIO_API = '' +VUE_APP_MINIO_API = '' // 建议使用与当前 web 服务域名的域名 -# atlas +# atlas 模型炼知 VUE_APP_ATLAS_HOST = '' diff --git a/webapp/.env.test b/webapp/.env.test new file mode 100644 index 0000000..5fa6d82 --- /dev/null +++ b/webapp/.env.test @@ -0,0 +1,13 @@ +ENV = 'test' + +# 默认BASE URL +VUE_APP_BASE_API = '' + +# 数据管理 +VUE_APP_DATA_API = '' + +# minio +VUE_APP_MINIO_API = '' + +# atlas +VUE_APP_ATLAS_HOST = '' diff --git a/webapp/.eslintrc.js b/webapp/.eslintrc.js index 02908fc..cc4ae86 100644 --- a/webapp/.eslintrc.js +++ b/webapp/.eslintrc.js @@ -53,15 +53,19 @@ module.exports = { "vue/attribute-hyphenation": "off", "vue/comment-directive": "off", "vue/prop-name-casing": "off", - "vue/max-attributes-per-line": [ - 2, - { - singleline: 20, - multiline: { - max: 1, - allowFirstLine: false - } - } - ] + "vue/max-attributes-per-line": [2, { + singleline: 20, + multiline: { + max: 1, + allowFirstLine: false + }} + ], + "vue/html-indent": ["error", 2, { + "attribute": 1, + "baseIndent": 1, + "closeBracket": 0, + "alignAttributesVertically": true, + "ignores": [] + }] } }; diff --git a/webapp/CHANGELOG.md b/webapp/CHANGELOG.md new file mode 100644 index 0000000..c8cbda0 --- /dev/null +++ b/webapp/CHANGELOG.md @@ -0,0 +1,26 @@ +## 1.1.0 (2020-10-26) + +### Breaking Change + +- [数据管理] 导入数据集功能重构。系统提供标准数据集模板,用户按照规范导入数据集文件,实现数据集全功能兼容 +- [训练管理] 支持OneFlow、TensorFlow、Pytorch等主流框架的多机多卡模式分布式训练 +- [训练管理] 训练时支持将已有模型作为训练入参 +- [训练管理] 训练时支持区分训练数据集与验证数据集 +- [训练管理] 训练支持延时启动、定时停止功能 +- [训练管理] 训练日志、运行日志下载功能优化,避免大文件导致的浏览器卡死 + +### Features + +- [数据管理] 将标签和数据集拆分,引入「标签组」统一管理标签 +- [数据管理] 超大数据集操作流程优化。实现超大数据集(40w+文件)的全流程平滑操作 +- [数据管理] 数据集图片手动标注优化。支持标注像素级位置、大小调整,支持常见缩放、拖拽、平移等操作 +- [数据管理] 数据集状态逻辑优化,代码性能优化等 +- [训练管理] 断点续训功能、模型下载功能、模型保存功能支持通过目录树选择模型文件/文件夹 +- [训练管理] 文件上传增加进度条展示 +- [训练管理] 训练创建页,增加运行命令预览功能;训练详情页,增加算法在线编辑跳转功能 +- [训练管理] 镜像管理功能,镜像名称支持自定义;支持镜像的删除、修改等操作 +- [训练管理] 增加训练失败异常信息反馈 + +### Bug Fixs + +- [数据管理] 标注详情里面不同分辨率图片标注位置偏移 bug \ No newline at end of file diff --git a/webapp/LICENSE b/webapp/LICENSE new file mode 100755 index 0000000..cf57587 --- /dev/null +++ b/webapp/LICENSE @@ -0,0 +1,211 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + +Other dependencies and licenses: +---------------------------------------------------------------------------------------- + +Open Source Software Licensed Under the Apache License, Version 2.0: +The below software in this distribution may have been modified. +---------------------------------------------------------------------------------------- +1. EL-ADMIN +Copyright 2019-2020 Zheng Jie \ No newline at end of file diff --git a/webapp/README.md b/webapp/README.md index b31ee0e..b189f9e 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -1,10 +1,76 @@ -# 一站式开发平台-前端 +# 之江天枢-前端 + +**之江天枢一站式人工智能开源平台**(简称:**之江天枢**),包括海量数据处理、交互式模型构建(包含Notebook和模型可视化)、AI模型高效训练。多维度产品形态满足从开发者到大型企业的不同需求,将提升人工智能技术的研发效率、扩大算法模型的应用范围,进一步构建人工智能生态“朋友圈”。 + +## 特性 +* 一站式开发 +* 集成先进算法 +* 灵活易用 +* 性能优越 + +## 预览 +![概览](/public/dubhe_dashboard.png "概览") + +## 源码部署 + +### 1. 下载源码 + +``` bash +git clone https://codeup.teambition.com/zhejianglab/dubhe-web.git + +# 进入根目录 +cd dubhe-web + +``` +### 2. 配置 + +根据需要修改如下配置文件 +``` +config/index.js +settings.js +.env.production +``` + +### 3. 构建 + +``` bash +# 安装项目依赖 +npm install + +# 构建生产环境 +npm run build:prod +``` + +### 4. 部署 + +- 构建完成后会在根目录生成 dist 文件夹,并将该文件夹上传至服务器; +- 在服务器 nginx.conf 文件中添加如下配置; + +``` nginx +server { + listen 80; # 端口 + server_name localhost; # 域名/外网IP + + location / { + root /home/wwwroot/dubhe-web/dist; # dist 文件夹根目录 + index index.html; + try_files $uri $uri/ /index.html; + } +} + +``` + +- 保存 `nginx.conf` 并重启 Nginx 使之生效。 + ## 本地开发 ``` bash -# 进入前端项目根目录 -cd webapp +# 下载源码 +git clone https://codeup.teambition.com/zhejianglab/dubhe-web.git + +# 进入项目根目录 +cd dubhe-web # 安装依赖 npm install diff --git a/webapp/package.json b/webapp/package.json index c73678d..857480b 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "dubhe-web", - "version": "1.0.0", + "version": "1.1.0", "description": "之江天枢人工智能开源平台", "author": "zhejianglab", "keywords": [ @@ -13,6 +13,8 @@ "scripts": { "dev": "vue-cli-service serve --open", "build:prod": "vue-cli-service build", + "build:test": "vue-cli-service build --mode test", + "build:dev": "vue-cli-service build --mode development", "lint": "eslint --ext .js,.vue src", "fix": "eslint --fix --ext .js,.vue src", "lint:style": "stylelint src/**/*.{html,vue,css,sass,scss}", @@ -54,6 +56,7 @@ "filereader-stream": "^2.0.0", "jquery": "^3.5.1", "jquery-contextmenu": "^2.9.1", + "js-beautify": "^1.13.0", "js-cookie": "2.2.0", "jsencrypt": "^3.0.0-rc.1", "json2csv": "^5.0.1", @@ -72,7 +75,9 @@ "v-hotkey": "^0.8.0", "vee-validate": "^3.3.0", "vue": "2.6.10", + "vue-copy-to-clipboard": "^1.0.3", "vue-prism-component": "^1.2.0", + "vue-prism-editor": "^1.2.2", "vue-router": "^3.0.2", "vuex": "3.1.0" }, diff --git a/webapp/src/App.vue b/webapp/src/App.vue index a4fd21a..3cf15e0 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -16,7 +16,9 @@ diff --git a/webapp/src/api/preparation/annotation.js b/webapp/src/api/preparation/annotation.js index f064ac2..5edc191 100644 --- a/webapp/src/api/preparation/annotation.js +++ b/webapp/src/api/preparation/annotation.js @@ -16,9 +16,9 @@ import request from '@/utils/request'; -export function batchFinishAnnotation(data) { +export function batchFinishAnnotation(data, datasetId) { return request({ - url: 'api/data/datasets/files/annotations', + url: `api/data/datasets/files/${datasetId}/annotations`, method: 'post', data, }); @@ -33,6 +33,13 @@ export function delAnnotation(id) { }); } +export function track(id) { + return request({ + url: `api/data/datasets/files/annotations/auto/track/${id}`, + method: 'get', + }); +} + export function autoAnnotate(ids) { const data = { datasetIds: ids }; return request({ diff --git a/webapp/src/api/preparation/datalabel.js b/webapp/src/api/preparation/datalabel.js index 5840b25..390b051 100644 --- a/webapp/src/api/preparation/datalabel.js +++ b/webapp/src/api/preparation/datalabel.js @@ -31,6 +31,14 @@ export function createLabel(id, label) { }); } +export function editLabel(id, label) { + return request({ + url: `api/data/datasets/labels/${id}`, + method: 'put', + data: label, + }); +} + export function getAutoLabels() { return request({ url: 'api/data/datasets/labels/auto', diff --git a/webapp/src/api/preparation/dataset.js b/webapp/src/api/preparation/dataset.js index 0921a3d..02eca08 100644 --- a/webapp/src/api/preparation/dataset.js +++ b/webapp/src/api/preparation/dataset.js @@ -32,6 +32,15 @@ export function detail(id) { }); } +// 数据集状态(导入数据集轮询使用) +export function queryDatasetStatus(ids) { + return request({ + url: `/api/data/datasets/status`, + method: 'get', + params: { datasetIds: ids }, + }); +} + export function add(data) { return request({ url: 'api/data/datasets', @@ -57,6 +66,13 @@ export function editDataset(data) { }); } +export function topDataset(data) { + return request({ + url: `api/data/datasets/${data.id}/top`, + method: 'get', + }); +} + // 导入自定义数据集 export function addCustomDataset(data) { return request({ @@ -153,9 +169,9 @@ export function postDataEnhance(datasetId, types = []) { } // 指定原始文件,获取增强文件列表 -export function getEnhanceFileList(fileId) { +export function getEnhanceFileList(datasetId, fileId) { return request({ - url: `api/data/datasets/${fileId}/enhanceFileList`, + url: `api/data/datasets/${datasetId}/${fileId}/enhanceFileList`, }); } @@ -173,4 +189,13 @@ export function queryDatasetsCount() { }); } +// 查询数据集状态 +export function queryDatasetsProgress(params) { + return request({ + url: `/api/data/datasets/progress`, + method: 'get', + params, + }); +} + export default { list, add, del }; diff --git a/webapp/src/api/preparation/labelGroup.js b/webapp/src/api/preparation/labelGroup.js new file mode 100644 index 0000000..0a6e172 --- /dev/null +++ b/webapp/src/api/preparation/labelGroup.js @@ -0,0 +1,89 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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 request from '@/utils/request'; + +// 创建标签组 +export function add(data) { + return request({ + url: `/api/data/labelGroup`, + method: 'post', + data, + }); +} + +// 编辑标签组 +export function edit(data) { + return request({ + url: `/api/data/labelGroup/${data.id}`, + method: 'put', + data, + }); +} + +// 删除标签组 +export function del(ids) { + return request({ + url: `/api/data/labelGroup`, + method: 'delete', + data: {ids}, + }); +} + +// 复制标签组 +export function copy(data) { + return request({ + url: `/api/data/labelGroup/copy`, + method: 'post', + data, + }); +} + +// 标签组列表分页查询 +export function list(params) { + return request({ + url: `/api/data/labelGroup/query`, + method: 'get', + params, + }); +} + +// 标签组列表的简况查询 用于详情页选择标签组列举 +export function getLabelGroupList(params) { + return request({ + url: `/api/data/labelGroup/getList/${params}`, + method: 'get', + }); +} + +// 获取标签组详情 +export function getLabelGroupDetail(id) { + return request({ + url: `/api/data/labelGroup/${id}`, + method: 'get', + }); +} + +export function importLabelGroup(form) { + return request.post(`api/data/labelGroup/import`, form, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +} + +export default { list, add, del, edit }; + diff --git a/webapp/src/api/system/harbor.js b/webapp/src/api/system/harbor.js index 2b1a6ba..15fe6ce 100644 --- a/webapp/src/api/system/harbor.js +++ b/webapp/src/api/system/harbor.js @@ -18,7 +18,7 @@ import request from '@/utils/request'; export function harborProjectNames() { return request({ - url: `api/v1/ptImage/project`, + url: `api/v1/ptImage/imageNameList`, method: 'get', }); } diff --git a/webapp/src/api/system/pod.js b/webapp/src/api/system/pod.js new file mode 100644 index 0000000..969d5e8 --- /dev/null +++ b/webapp/src/api/system/pod.js @@ -0,0 +1,50 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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 request from '@/utils/request'; + +export function getPodLog(params) { + return request({ + url: 'api/v1/pod/log', + method: 'get', + params, + }); +} + +export function downloadPodLog(params) { + return request({ + url: 'api/v1/pod/log/download', + method: 'get', + params, + }); +} + +export function batchDownloadPodLog(data) { + return request({ + url: 'api/v1/pod/log/download', + method: 'post', + responseType: 'blob', + data, + }); +} + +export function countPodLogs(podVOList) { + return request({ + url: 'api/v1/pod/log/count', + method: 'post', + data: { podVOList }, + }); +} diff --git a/webapp/src/api/trainingImage/index.js b/webapp/src/api/trainingImage/index.js index 9e63f59..b04a497 100644 --- a/webapp/src/api/trainingImage/index.js +++ b/webapp/src/api/trainingImage/index.js @@ -32,11 +32,27 @@ export function add(data) { }); } -export function project() { +export function edit(data) { return request({ - url: 'api/v1/ptImage/project', + url: 'api/v1/ptImage', + method: 'put', + data, + }); +} + +export function del(ids) { + return request({ + url: 'api/v1/ptImage', + method: 'delete', + data: ids, + }); +} + +export function imageNameList() { + return request({ + url: 'api/v1/ptImage/imageNameList', method: 'get', }); } -export default { list, add }; +export default { list, add, edit }; diff --git a/webapp/src/api/trainingJob/job.js b/webapp/src/api/trainingJob/job.js index 7479b3d..08eaf95 100644 --- a/webapp/src/api/trainingJob/job.js +++ b/webapp/src/api/trainingJob/job.js @@ -72,17 +72,17 @@ export function getJobList(params) { }); } -export function getTrainLog(params) { +export function getJobDetail(jobId) { return request({ - url: `api/v1/trainLog`, + url: `api/v1/trainJob/jobDetail`, method: 'get', - params, + params: { id: jobId }, }); } -export function downloadTrainLog(params) { +export function getTrainLog(params) { return request({ - url: `api/v1/trainLog/download`, + url: `api/v1/trainLog`, method: 'get', params, }); @@ -109,4 +109,11 @@ export function getGarafanaInfo(jobId) { }); } -export default { list, add, edit, del, getGarafanaInfo }; +export function getPods(jobId) { + return request({ + url: `api/v1/trainLog/pod/${jobId}`, + method: 'get', + }); +} + +export default { list, add, edit, del }; diff --git a/webapp/src/assets/styles/atomic.scss b/webapp/src/assets/styles/atomic.scss index 5a42586..a475124 100644 --- a/webapp/src/assets/styles/atomic.scss +++ b/webapp/src/assets/styles/atomic.scss @@ -214,6 +214,10 @@ img.responsive { color: red; } +.success { + color: $successColor; +} + .g3 { color: #333; } diff --git a/webapp/src/assets/styles/common.scss b/webapp/src/assets/styles/common.scss index 59b2e25..014fd4f 100644 --- a/webapp/src/assets/styles/common.scss +++ b/webapp/src/assets/styles/common.scss @@ -126,7 +126,6 @@ } .text { - height: 19px; font-size: 14px; line-height: 19px; color: rgba(68, 68, 68, 1); @@ -143,18 +142,6 @@ } } } - - .fr { - margin-right: 5%; - } - - .iframe { - width: 90%; - height: 500px; - margin: 40px 5%; - overflow: auto; - border: #ccc solid 1px; - } } .eltabs-inlineblock.el-tabs { @@ -242,3 +229,9 @@ color: $primaryHoverColor; } } + +.tree-container { + height: 500px; + margin-top: 20px; + overflow-y: scroll; +} diff --git a/webapp/src/assets/styles/element-ui.scss b/webapp/src/assets/styles/element-ui.scss index 4bbdf20..ee18e90 100644 --- a/webapp/src/assets/styles/element-ui.scss +++ b/webapp/src/assets/styles/element-ui.scss @@ -44,6 +44,14 @@ } } +// el-radio border +.el-radio.is-bordered { + &.is-checked, + &:hover { + border-color: $primaryBorderColor; + } +} + // radio-button .el-radio-button__inner { padding: 8px 25px; @@ -298,3 +306,27 @@ .el-tooltip__popper { max-width: 50%; } + +.info-alert.is-light { + margin-bottom: 20px; + background-color: $primaryBg; + + .el-alert__content { + width: 100%; + } + + .el-alert__icon { + color: $primaryColor; + } + + .slot-content { + display: flex; + justify-content: space-between; + width: 100%; + color: #000; + + a { + color: $primaryColor; + } + } +} diff --git a/webapp/src/assets/styles/index.scss b/webapp/src/assets/styles/index.scss index dae421a..e38b528 100644 --- a/webapp/src/assets/styles/index.scss +++ b/webapp/src/assets/styles/index.scss @@ -69,7 +69,6 @@ ol { a, a:focus, a:hover { - color: inherit; text-decoration: none; cursor: pointer; } @@ -87,6 +86,10 @@ ol li { color: $infoColor; } +.fontBold { + font-weight: bold; +} + .primary-bg { background-color: $primaryColor; @@ -100,3 +103,20 @@ p.error-message { font-size: 12px; color: $red; } + +// 新手导引 +.v-tour { + div.v-step { + color: #2e4fde; + background: #d8dfff; + } + + button.v-step__button { + color: #2e4fde; + border-color: #2e4fde; + + &:hover { + background: #f3f7ff; + } + } +} diff --git a/webapp/src/assets/styles/variables.scss b/webapp/src/assets/styles/variables.scss index b005c2a..b5c5cc2 100644 --- a/webapp/src/assets/styles/variables.scss +++ b/webapp/src/assets/styles/variables.scss @@ -33,6 +33,7 @@ $imageBg: #f8f8f8; $borderColorBase: #ebeef5; $borderColorDark: #c0c4cc; $black: #001529; +$dark: #323232; // sidebar $menuBg: #f3f7ff; diff --git a/webapp/src/components/BaseModal/index.js b/webapp/src/components/BaseModal/index.js index 4a40255..5109388 100644 --- a/webapp/src/components/BaseModal/index.js +++ b/webapp/src/components/BaseModal/index.js @@ -97,10 +97,10 @@ const BaseModal = { return ( ); }; diff --git a/webapp/src/components/Crud/CD.operation.vue b/webapp/src/components/Crud/CD.operation.vue index 492fbd8..81fbd95 100644 --- a/webapp/src/components/Crud/CD.operation.vue +++ b/webapp/src/components/Crud/CD.operation.vue @@ -19,6 +19,7 @@ +import { reactive } from '@vue/composition-api'; +import { findAncestorSvg } from '@/utils'; + +export default { + name: 'Drag', + props: { + width: Number, + height: Number, + resetOnStart: { + type: Boolean, + default: false, + }, + onDragStart: Function, + onDragMove: Function, + onDragEnd: Function, + }, + setup(props) { + const { resetOnStart, onDragStart, onDragMove, onDragEnd } = props; + const state = reactive({ + x: undefined, + y: undefined, + dx: 0, + dy: 0, + isDragging: false, // 鼠标按下 + isMoving: true, // 鼠标移动 + }); + + function getPoint(event) { + // 容器尺寸 + const bound = findAncestorSvg(event).getBoundingClientRect(); + const { clientX, clientY } = event; + + return { + x: clientX - bound.left, + y: clientY - bound.top, + }; + } + + function dragStart(event) { + const point = getPoint(event); + const nextState = { + isDragging: true, + isMoving: false, + dx: resetOnStart ? 0 : state.dx, + dy: resetOnStart ? 0 : state.dy, + x: resetOnStart ? point.x : -state.dx + point.x, + y: resetOnStart ? point.y : -state.dy + point.y, + }; + Object.assign(state, nextState); + if (typeof onDragStart === 'function') onDragStart(nextState, event); + } + + function dragMove(event) { + if (!state.isDragging) return; + const point = getPoint(event); + // 避免无效移动 + if(Math.abs(point.x - state.x) < 2 && Math.abs(point.y - state.y) < 2) return; + const nextState = { + isDragging: true, + isMoving: true, + dx: point.x - state.x, + dy: point.y - state.y, + }; + Object.assign(state, nextState); + if (typeof onDragMove === 'function') onDragMove(state, event); + } + + function dragEnd(event) { + const nextState = { + isDragging: false, + isMoving: false, + }; + const prevState = { ...state }; + Object.assign(state, nextState); + // 传递 prevState + if (typeof onDragEnd === 'function') onDragEnd(state, event, { + prevState, + }); + } + + return { + state, + dragStart, + dragMove, + dragEnd, + }; + }, + + render() { + const children = this.$scopedSlots.default; + + return ( + + {this.state.isDragging && + ( + + )} + { typeof children === 'function' && ( + children({ + state: this.state, + dragStart: this.dragStart, + dragMove: this.dragMove, + dragEnd: this.dragEnd, + }) + ) } + + ); + }, +}; + diff --git a/webapp/src/components/Drag/index.js b/webapp/src/components/Drag/index.js new file mode 100644 index 0000000..010364e --- /dev/null +++ b/webapp/src/components/Drag/index.js @@ -0,0 +1,19 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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 Drag from './drag'; + +export default Drag; diff --git a/webapp/src/components/Exception/index.vue b/webapp/src/components/Exception/index.vue index 16000fd..ccbe530 100644 --- a/webapp/src/components/Exception/index.vue +++ b/webapp/src/components/Exception/index.vue @@ -38,19 +38,18 @@ export default { }; diff --git a/webapp/src/components/IconFont/index.js b/webapp/src/components/IconFont/index.js index dd71921..0070d1a 100644 --- a/webapp/src/components/IconFont/index.js +++ b/webapp/src/components/IconFont/index.js @@ -26,7 +26,7 @@ import create from './iconfont'; const IconFont = create({ - scriptUrl: '//at.alicdn.com/t/font_1756495_bycxbb6pz6s.js', + scriptUrl: '//at.alicdn.com/t/font_1756495_k4j524i5vng.js', extraIconProps: { class: 'svg-icon' }, }); diff --git a/webapp/src/components/ImageGallery/index.vue b/webapp/src/components/ImageGallery/index.vue index 47229c2..7023def 100644 --- a/webapp/src/components/ImageGallery/index.vue +++ b/webapp/src/components/ImageGallery/index.vue @@ -53,7 +53,7 @@ :class="rootClass + '__img'" @click="onClickImg(dataImage)" > - {{ imageLabelTag[dataImage.id]['text'] }} + {{ imageLabelTag[dataImage.id]['text'] }}
{{ basename(dataImage.url) }}
@@ -74,6 +74,7 @@ - - diff --git a/webapp/src/components/LogContainer/index.vue b/webapp/src/components/LogContainer/index.vue new file mode 100644 index 0000000..6534a34 --- /dev/null +++ b/webapp/src/components/LogContainer/index.vue @@ -0,0 +1,125 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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. +* ============================================================= +*/ + + + + diff --git a/webapp/src/components/Training/dataSourceSelector.vue b/webapp/src/components/Training/dataSourceSelector.vue new file mode 100644 index 0000000..dd68f69 --- /dev/null +++ b/webapp/src/components/Training/dataSourceSelector.vue @@ -0,0 +1,243 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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. +* ============================================================= +*/ + + + + diff --git a/webapp/src/components/Training/jobForm.vue b/webapp/src/components/Training/jobForm.vue index d1ce0e1..2542f0f 100644 --- a/webapp/src/components/Training/jobForm.vue +++ b/webapp/src/components/Training/jobForm.vue @@ -22,28 +22,36 @@ :model="form" :rules="rules" label-width="120px" - class="demo-ruleForm" - :style="`width: ${widthPercent}%;`" + :style="`width: ${widthPercent}%; margin-top: 20px;`" > - +
{{ form.jobName }}
- + - + -
+ @@ -198,26 +319,51 @@ {{ form.imageName }} - + + {{ form.modelName }} + + {{ form.dataSourceName }} + + {{ form.valDataSourceName }} + {{ form.runCommand }} - --{{ key }}={{ form.runParams[key] }} + --{{ key }}={{ form.runParams[key] }} + + + {{ form.trainType === 1 ? '是' : '否' }} + + + {{ form.resourcesPoolNode }} + + + {{ delayCreateDelete ? '是' : '否' }} + + + {{ form.delayCreateTime }} 小时 + + + {{ form.delayDeleteTime }} 小时 - {{ form.resourcesPoolType ? 'GPU' : 'CPU' }} + {{ form.resourcesPoolNode }} - {{ formSpecs && formSpecs.specsName }} + {{ formSpecs && formSpecs.label }} + + +
+ {{ preview }} +
- - 开始训练 - 清空 -
@@ -225,42 +371,79 @@ - diff --git a/webapp/src/components/Training/paramPair.vue b/webapp/src/components/Training/paramPair.vue new file mode 100644 index 0000000..0cd9511 --- /dev/null +++ b/webapp/src/components/Training/paramPair.vue @@ -0,0 +1,135 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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. +* ============================================================= +*/ + + + + + + diff --git a/webapp/src/components/Training/runParamForm.vue b/webapp/src/components/Training/runParamForm.vue index 0bb0e6a..2f08656 100644 --- a/webapp/src/components/Training/runParamForm.vue +++ b/webapp/src/components/Training/runParamForm.vue @@ -18,8 +18,8 @@
- key-value - arguments + key-value + arguments - -
- - - - - - - -
-
+
import { stringIsValidPythonVariable } from '@/utils'; +import ParamPair from './paramPair'; export default { name: 'RunParamForm', + components: { ParamPair }, props: { - id: { - type: [Number, String], - default: null, - }, runParamObj: { type: Object, - default: () => {}, + default: () => ({}), }, prop: { type: String, default: null, }, - input1Width: { - type: Number, - default: 150, - }, - input2Width: { - type: Number, - default: 150, - }, paramLabelWidth: { type: String, default: '100px', @@ -110,73 +83,34 @@ export default { }, }, data() { + const isInputEmpty = value => { + return value === '' || value === null; + }; + + const keyValidator = (rule, value, callback) => { + if (!isInputEmpty(value) && !stringIsValidPythonVariable(value)) { + callback(new Error('参数key必须是合法变量名')); + } else { + callback(); + } + }; + return { runParamsList: [], - errMsg: [], paramsMode: 1, paramsArguments: '', argErrorMsg: null, - validateKey: (callback, item, index) => { - // 先校验是不是都为空,若都为空则通过 - const isEmptyKey = this.isInputEmpty(item.key); - const isEmptyValue = this.isInputEmpty(item.value); - if (isEmptyKey && isEmptyValue) { - // 可能之前value有校验错误信息 - this.$refs[this.itemValueId(index)][0].form.clearValidate(this.itemValueId(index)); - callback(); - return; - } - // 再校验自己是不是合法的变量名 - if (!stringIsValidPythonVariable(item.key)) { - callback(new Error('参数key必须是合法变量名')); - return; - } - // 然后和value联合校验 - if (isEmptyKey) { - callback(new Error('请输入参数key')); - return; - } if (isEmptyValue) { - this.$refs.runParamForm.validateField(this.itemValueId(index)); - } else { - callback(); - - } - }, - validateValue: (callback, item, index) => { - // 先校验是不是都为空,若都为空则通过 - const isEmptyKey = this.isInputEmpty(item.key); - const isEmptyValue = this.isInputEmpty(item.value); - if (isEmptyKey && isEmptyValue) { - // 可能之前key有校验错误信息 - this.$refs[this.itemKeyId(index)][0].form.clearValidate(this.itemKeyId(index)); - callback(); - return; - } - // 输入框格式保证了其类似一定是字符串,只需不传空即可 - // 然后和key联合校验 - if (isEmptyValue) { - callback(); - return; - } if (isEmptyKey) { - this.$refs.runParamForm.validateField(this.itemKeyId(index)); - } else { - callback(); - - } - }, + // 整体校验规则:对 key 做 python 变量名有效性校验,对 value 不做任何校验 + keyRule: [{ + validator: keyValidator, + trigger: 'blur', + }], + paramId: 0, + paramRepeatWarning: null, + hasError: false, }; }, watch: { - id(newValue) { - if (newValue === null || isNaN(newValue)) { - /** - * newValue为null时的一种情况是与el-form组合使用的 - * crud组件触发了cancelCU方法,此时不需更新 - */ - return; - } - this.syncListData(); - }, runParamObj() { this.syncListData(); }, @@ -185,19 +119,12 @@ export default { this.syncListData(); }, methods: { - isInputEmpty(value) { - return value === '' || value === null; - }, - itemKeyId(index) { - return `runParamsList.${ index }.key`; - }, - itemValueId(index) { - return `runParamsList.${ index }.value`; - }, addP() { this.runParamsList.push({ key: '', value: '', + // eslint-disable-next-line no-plusplus + id: this.paramId++, }); }, removeP(i) { @@ -205,12 +132,17 @@ export default { this.updateRunParamObj(); }, syncListData() { - const rpObj = { ...this.runParamObj}; const list = []; - for (const formKey in rpObj) { - list.push({ - key: formKey, - value: typeof (rpObj[formKey]) === 'object' ? JSON.stringify(rpObj[formKey]) : rpObj[formKey], + for (const key in this.runParamObj) { + const objItem = this.runParamsList.find(p => p.key === key); + if (objItem) { + objItem.value = this.runParamObj[key]; + } + list.push(objItem || { + key, + value: this.runParamObj[key], + // eslint-disable-next-line no-plusplus + id: this.paramId++, }); } this.runParamsList = list; @@ -221,33 +153,59 @@ export default { this.convertPairsToArgs(); } }, - handleChange() { + handleChange(paramPair) { + // 当参数对的值改变时 key 为空,则把对于的 param 删除 + if (!paramPair.key) { + const paramIndex = this.runParamsList.findIndex(p => p.id === paramPair.id); + this.runParamsList.splice(paramIndex, 1); + } + if (!this.runParamsList.length) { + this.addP(); + } this.updateRunParamObj(); }, + // 提供修改参数的入口, 如果参数存在则可修改 + updateParam(key, value) { + const param = this.runParamsList.find(p => p.key === key); + if (param) { + param.value = value; + this.updateRunParamObj(); + } + }, updateRunParamObj() { const obj = {}; - this.runParamsList.forEach(d => { - if (d.key === '') return; - obj[d.key] = d.value; + const repeatedParams = new Set(); + this.runParamsList.forEach(param => { + // 当 key 为空或者已存在相同 key 时,不加入数值 + if (!param.key) { + return; + } + if (obj[param.key] !== undefined) { + repeatedParams.add(param.key); + return; + } + obj[param.key] = param.value; }); + if (repeatedParams.size) { + this.paramRepeatWarning && this.paramRepeatWarning.close(); + this.paramRepeatWarning = this.$message.warning(`参数 ${[...repeatedParams].join(', ')} 有重复, 将取用第一个值。`); + } this.$emit('updateRunParams', obj); }, - goValid() { + validate() { // 单独校验 let valid = true; - this.errMsg = []; - this.runParamsList.forEach((item, index) => { - if (this.isInputEmpty(item.key)) { - if (!this.isInputEmpty(item.value)) { - valid = false; - } - } else if (!stringIsValidPythonVariable(item.key)) { - valid = false; - this.$nextTick(() => { - this.errMsg[index] = '参数key必须是合法变量名'; - }); - } - }); + const validCallback = pairValid => { + valid = valid && pairValid; + }; + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < this.runParamsList.length; i++) { + this.paramsMode === 1 && this.$refs.paramPairs[i].validate(validCallback); + }; + + valid = valid && !this.hasError; + return valid; }, onParamsModeChange(value) { @@ -265,33 +223,41 @@ export default { const paramsList = this.paramsArguments.split(' '); const pairList = []; const re = /^--(.+)=(.*)$/; + this.hasError = false; + // 先使用正则进行匹配 paramsList.forEach(arg => { const group = re.exec(arg); if (group) { pairList.push({ key: group[1], value: group[2], + // eslint-disable-next-line no-plusplus + id: this.paramId++, }); } else if (arg) { this.$nextTick(() => { this.argErrorMsg = `参数'${arg}'不合法,请检查运行参数`; }); this.paramsMode = 2; - + this.hasError = true; } }); + if (this.hasError) return; + // 其次做参数名验证 pairList.forEach(pair => { if (!stringIsValidPythonVariable(pair.key)) { this.$nextTick(() => { this.argErrorMsg = `参数名'${pair.key}'不是合法参数,请检查运行参数`; }); this.paramsMode = 2; - + this.hasError = true; } }); + if (this.hasError) return; // 参数为空时增加一个空参数 if (!pairList.length) { - pairList.push({ key: '', value: '' }); + // eslint-disable-next-line no-plusplus + pairList.push({ key: '', value: '', id: this.paramId++ }); } this.runParamsList = pairList; this.updateRunParamObj(); @@ -308,13 +274,20 @@ export default { this.paramsArguments = args; }, reset() { - this.errMsg = []; this.argErrorMsg = null; this.paramsMode = 1; this.paramsArguments = ''; - this.runParamsList = [{ key: '', value: '' }]; - this.$refs.runParamForm.clearValidate(); + // eslint-disable-next-line no-plusplus + this.runParamsList = [{ key: '', value: '', id: this.paramId++ }]; }, }, }; + \ No newline at end of file diff --git a/webapp/src/components/Training/saveModelDialog.vue b/webapp/src/components/Training/saveModelDialog.vue index 8a5bb9c..9f2daec 100644 --- a/webapp/src/components/Training/saveModelDialog.vue +++ b/webapp/src/components/Training/saveModelDialog.vue @@ -33,7 +33,7 @@ - + 新建模型 @@ -210,6 +210,12 @@ export default { this.createAlgorithmUsage(value); } }, + formatVersion(item) { + if (item.versionNum) { + return `${item.name} (V${(Number(item.versionNum.substr(1)) + 1).toString().padStart(4, '0')})`; + } + return `${item.name} (V0001)`; + }, // op doSaveModel() { this.$refs.modelForm.validate(valid => { diff --git a/webapp/src/components/UploadForm/form.js b/webapp/src/components/UploadForm/form.js index e59ae52..65d45cb 100644 --- a/webapp/src/components/UploadForm/form.js +++ b/webapp/src/components/UploadForm/form.js @@ -43,16 +43,12 @@ export default { }, limit: { type: Number, - default: 1000, + default: 5000, }, showFileCount: { type: Boolean, default: true, }, - wordShow: { - type: Boolean, - default: true, - }, }, data() { return { @@ -130,6 +126,7 @@ export default { }, onRemove(file, fileList) { this.lenOfFileList = fileList.length; + this.$attrs['on-remove'] && this.$attrs['on-remove'](file, fileList); }, cancelUpload() { if (this.source) { @@ -163,7 +160,7 @@ export default { class='upload-field' limit={this.limit} multiple - list-type='picture' + list-type={this.lenOfFileList>100? 'text' : 'picture'} auto-upload={false} disabled={this.uploading} {...uploadProps} @@ -180,7 +177,7 @@ export default {
{ this.showFileCount && ( - this.wordShow ? 已选择{ this.lenOfFileList }张 : null + 已选择{ this.lenOfFileList }张 ) } diff --git a/webapp/src/components/UploadForm/inline.vue b/webapp/src/components/UploadForm/inline.vue index 6282309..29f3b20 100644 --- a/webapp/src/components/UploadForm/inline.vue +++ b/webapp/src/components/UploadForm/inline.vue @@ -69,7 +69,7 @@ export default { } state.uploading = true; - ctx.emit('uploadStart'); + ctx.emit('uploadStart', files); const uploadReqeust = request || minIOUpload; // 开始调用上传接口 return uploadReqeust({ ...props.params, fileList: renameFileList, transformFile }, callback) diff --git a/webapp/src/components/UploadProgress/index.vue b/webapp/src/components/UploadProgress/index.vue new file mode 100644 index 0000000..8bbe26e --- /dev/null +++ b/webapp/src/components/UploadProgress/index.vue @@ -0,0 +1,93 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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. +* ============================================================= +*/ + +// 仅支持line-upload上传文件,线性进度条 + + + + + \ No newline at end of file diff --git a/webapp/src/components/svg/brush/Brush.js b/webapp/src/components/svg/brush/Brush.js new file mode 100644 index 0000000..e5d7c2a --- /dev/null +++ b/webapp/src/components/svg/brush/Brush.js @@ -0,0 +1,217 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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 cx from 'classnames'; +import Vue from 'vue'; +import { reactive } from '@vue/composition-api'; + +import Drag from '@/components/Drag'; +import Group from '../group'; +import BrushSelection from './BrushSelection'; + +export default { + name: 'Brush', + components: { + Group, + }, + props: { + stageWidth: Number, + stageHeight: Number, + className: String, + onBrushStart: Function, + onBrushMove: Function, + onBrushEnd: Function, + transformZoom: Function, + left: { + type: Number, + default: 0, + }, + top: { + type: Number, + default: 0, + }, + brushSelectionStyle: { + type: Object, + default: () => ({ + fill: 'rgba(102, 181, 245, 0.1)', + stroke: 'rgba(102, 181, 245, 1)', + strokeWidth: 1, + }), + }, + }, + + setup(props){ + const { onBrushStart, onBrushMove, left, top, onChange, onBrushEnd, transformZoom } = props; + const state = reactive({ + start: { x: 0, y: 0 }, + end: { x: 0, y: 0 }, + extent: { x0: 0, x1: 0, y0: 0, y1: 0 }, + isBrushing: false, + }); + const getWidth = () => { + return Math.abs(state.extent.x1 - state.extent.x0); + }; + + const getHeight = () => { + return Math.abs(state.extent.y1 - state.extent.y0); + }; + + const getExtent = (start, end) => { + const x0 = Math.min(start.x, end.x); + const x1 = Math.max(start.x, end.x); + const y0 = Math.min(start.y, end.y); + const y1 = Math.max(start.y, end.y); + + return { + x0, + x1, + y0, + y1, + }; + }; + + const update = (updater, callback) => { + Object.assign(state, updater(state)); + Vue.nextTick(() => { + if(callback) { + callback(state); + } + if(onChange) { + onChange(state); + } + }); + }; + + const handleDragStart = (draw, event) => { + const start = transformZoom({ + x: draw.x + draw.dx - left, + y: draw.y + draw.dy - top, + }); + if (onBrushStart) { + onBrushStart(start, event); + } + + update(prevBrush => ({ + ...prevBrush, + start, + end: undefined, + extent: { + x0: -1, + x1: -1, + y0: -1, + y1: -1, + }, + isBrushing: true, + })); + }; + + const handleDragMove = (draw, event) => { + if (!draw.isDragging) return; + const end = transformZoom({ + x: draw.x + draw.dx - left, + y: draw.y + draw.dy - top, + }); + + update(prevBrush => { + const { start } = prevBrush; + const extent = getExtent(start, end); + return { + ...prevBrush, + end, + extent, + }; + }, (nextState) => { + // 回调 + typeof onBrushMove === 'function' && onBrushMove(nextState, event); + }); + }; + + + const handleDragEnd = (draw, event, options = {}) => { + update(prevBrush => ({ + ...prevBrush, + isBrushing: false, + }), state => onBrushEnd(state, event, options)); + }; + + return { + state, + getWidth, + getHeight, + update, + handleDragStart, + handleDragMove, + handleDragEnd, + getExtent, + }; + }, + + render(h) { + const { stageWidth, stageHeight, className, left, top, brushSelectionStyle } = this; + const { start, end, isBrushing } = this.state; + + const width = this.getWidth(); + const height = this.getHeight(); + + const dragProps = { + props: { + width: stageWidth, + height: stageHeight, + resetOnStart: true, + onDragStart: this.handleDragStart, + onDragMove: this.handleDragMove, + onDragEnd: this.handleDragEnd, + }, + }; + + return ( + + {/* overlay */} + + { + (drag) => ( + + ) + } + + {start && end && !!isBrushing && ( + + + + )} + + ); + }, +}; diff --git a/webapp/src/components/svg/brush/BrushCorner.js b/webapp/src/components/svg/brush/BrushCorner.js new file mode 100644 index 0000000..9aa3d01 --- /dev/null +++ b/webapp/src/components/svg/brush/BrushCorner.js @@ -0,0 +1,227 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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 Drag from '@/components/Drag'; +import { chroma } from '@/utils'; + +export default { + name: 'BrushCorner', + props: { + annotate: Object, + transformer: Object, + currentAnnotationId: String, + stageWidth: Number, + stageHeight: Number, + type: String, + scale: { + type: Number, + default: 1, + }, + x: Number, + y: Number, + width: Number, + height: Number, + handleBrushStart: Function, + updateBrush: Function, + updateBrushEnd: Function, + getZoom: Function, + }, + + setup(props) { + const { updateBrush, updateBrushEnd, type, scale, handleBrushStart, getZoom } = props; + + const handleDragStart = (drag, event) => { + // 开始拖拽是选中当前标注 + if(handleBrushStart) { + handleBrushStart(drag, event); + } + }; + + const handleDragMove = (drag) => { + if (!drag.isDragging) return; + const { zoom } = getZoom(); + updateBrush(prevBrush => { + const { start, end } = prevBrush; + let nextState = {}; + + let moveX = 0; + let moveY = 0; + + const _scale = scale * zoom; + + const xMax = Math.max(start.x, end.x); + const xMin = Math.min(start.x, end.x); + const yMax = Math.max(start.y, end.y); + const yMin = Math.min(start.y, end.y); + + switch (type) { + case 'topRight': + moveX = xMax + drag.dx / _scale; + moveY = yMin + drag.dy / _scale; + + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max(Math.min(moveX, start.x), prevBrush.bounds.x0), + x1: Math.min(Math.max(moveX, start.x), prevBrush.bounds.x1), + y0: Math.max(Math.min(moveY, end.y), prevBrush.bounds.y0), + y1: Math.min(Math.max(moveY, end.y), prevBrush.bounds.y1), + }, + }; + break; + case 'topLeft': + moveX = xMin + drag.dx / _scale; + moveY = yMin + drag.dy / _scale; + + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max(Math.min(moveX, end.x), prevBrush.bounds.x0), + x1: Math.min(Math.max(moveX, end.x), prevBrush.bounds.x1), + y0: Math.max(Math.min(moveY, end.y), prevBrush.bounds.y0), + y1: Math.min(Math.max(moveY, end.y), prevBrush.bounds.y1), + }, + }; + break; + case 'bottomLeft': + moveX = xMin + drag.dx / _scale; + moveY = yMax + drag.dy / _scale; + + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max(Math.min(moveX, end.x), prevBrush.bounds.x0), + x1: Math.min(Math.max(moveX, end.x), prevBrush.bounds.x1), + y0: Math.max(Math.min(moveY, start.y), prevBrush.bounds.y0), + y1: Math.min(Math.max(moveY, start.y), prevBrush.bounds.y1), + }, + }; + break; + case 'bottomRight': + moveX = xMax + drag.dx / _scale; + moveY = yMax + drag.dy / _scale; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max(Math.min(moveX, start.x), prevBrush.bounds.x0), + x1: Math.min(Math.max(moveX, start.x), prevBrush.bounds.x1), + y0: Math.max(Math.min(moveY, start.y), prevBrush.bounds.y0), + y1: Math.min(Math.max(moveY, start.y), prevBrush.bounds.y1), + }, + }; + break; + default: + break; + } + return nextState; + }); + }; + + const handleDragEnd = () => { + updateBrushEnd(prevBrush => { + const { start, end, extent } = { ...prevBrush }; + start.x = Math.min(extent.x0, extent.x1); + start.y = Math.min(extent.y0, extent.y0); + end.x = Math.max(extent.x0, extent.x1); + end.y = Math.max(extent.y0, extent.y1); + const nextBrush = { + ...prevBrush, + start, + end, + activeHandle: undefined, + isBrushing: false, + domain: { + x0: Math.min(start.x, end.x), + x1: Math.max(start.x, end.x), + y0: Math.min(start.y, end.y), + y1: Math.max(start.y, end.y), + }, + }; + return nextBrush; + }); + }; + + return { + handleDragStart, + handleDragMove, + handleDragEnd, + }; + }, + + render(h) { + const { annotate, transformer, currentAnnotationId, stageWidth, stageHeight, type, x, y, width, height } = this; + + const cursor = type === 'topLeft' || type === 'bottomRight' ? 'nwse-resize' : 'nesw-resize'; + + let transform = null; + if(annotate.id === transformer.id) { + transform = `translate(${transformer.dx}, ${transformer.dy})`; + } + + const { data = {} } = annotate; + const { color } = data; + const defaultFill = 'rgba(102, 181, 245, 0.1)'; + const bgColor = color || defaultFill; + const isActive = currentAnnotationId === annotate.id; + const colorAlpha = isActive ? 1 : 0; + const fillColor = chroma(bgColor).alpha(colorAlpha); + + const dragProps = { + props: { + width: stageWidth, + height: stageHeight, + resetOnStart: true, + onDragStart: this.handleDragStart, + onDragMove: this.handleDragMove, + onDragEnd: this.handleDragEnd, + }, + }; + + const style = { + cursor, + }; + + return ( + + { + (drag) => ( + + ) + } + + ); + }, +}; diff --git a/webapp/src/components/svg/brush/BrushHandle.js b/webapp/src/components/svg/brush/BrushHandle.js new file mode 100644 index 0000000..d12d456 --- /dev/null +++ b/webapp/src/components/svg/brush/BrushHandle.js @@ -0,0 +1,193 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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 Drag from '@/components/Drag'; + +export default { + name: 'BrushHandle', + props: { + stageWidth: Number, + stageHeight: Number, + type: String, + scale: { + type: Number, + default: 1, + }, + handle: { + type: Object, + default: () => ({ x: 0, y: 0, width: 0, height: 0 }), + }, + handleBrushStart: Function, + updateBrush: Function, + updateBrushEnd: Function, + getZoom: Function, + }, + + // todo: 鼠标离开画布没有释放 + setup(props) { + const { updateBrush, updateBrushEnd, type, scale, handleBrushStart, getZoom } = props; + + const handleDragStart = (drag, event) => { + // 开始拖拽是选中当前标注 + if(handleBrushStart) { + handleBrushStart(drag, event); + } + }; + + const handleDragMove = (drag) => { + if (!drag.isDragging) return; + const { zoom } = getZoom(); + updateBrush(prevBrush => { + const { start, end } = prevBrush; + let nextState = {}; + let move = 0; + const _scale = scale * zoom; + + const xMax = Math.max(start.x, end.x); + const xMin = Math.min(start.x, end.x); + const yMax = Math.max(start.y, end.y); + const yMin = Math.min(start.y, end.y); + + switch (type) { + case 'right': + move = xMax + drag.dx / _scale; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.max(Math.min(move, start.x), prevBrush.bounds.x0), + x1: Math.min(Math.max(move, start.x), prevBrush.bounds.x1), + }, + }; + break; + case 'left': + move = xMin + drag.dx / _scale; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + x0: Math.min(move, end.x), + x1: Math.max(move, end.x), + }, + }; + break; + case 'top': + move = yMin + drag.dy / _scale; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + y0: Math.min(move, end.y), + y1: Math.max(move, end.y), + }, + }; + break; + case 'bottom': + move = yMax + drag.dy / _scale; + nextState = { + ...prevBrush, + activeHandle: type, + extent: { + ...prevBrush.extent, + y0: Math.min(move, start.y), + y1: Math.max(move, start.y), + }, + }; + break; + default: + break; + } + return nextState; + }); + }; + + const handleDragEnd = () => { + updateBrushEnd(prevBrush => { + const { start, end, extent } = { ...prevBrush }; + start.x = Math.min(extent.x0, extent.x1); + start.y = Math.min(extent.y0, extent.y0); + end.x = Math.max(extent.x0, extent.x1); + end.y = Math.max(extent.y0, extent.y1); + const nextBrush = { + ...prevBrush, + start, + end, + activeHandle: undefined, + isBrushing: false, + domain: { + x0: Math.min(start.x, end.x), + x1: Math.max(start.x, end.x), + y0: Math.min(start.y, end.y), + y1: Math.max(start.y, end.y), + }, + }; + return nextBrush; + }); + }; + + return { + handleDragStart, + handleDragMove, + handleDragEnd, + }; + }, + + render(h) { + const { stageWidth, stageHeight, handle, type } = this; + const { x, y, width, height } = handle; + + const cursor = type === 'right' || type === 'left' ? 'ew-resize' : 'ns-resize'; + + const dragProps = { + props: { + width: stageWidth, + height: stageHeight, + resetOnStart: true, + onDragStart: this.handleDragStart, + onDragMove: this.handleDragMove, + onDragEnd: this.handleDragEnd, + }, + }; + + const style = { + cursor, + }; + + return ( + + { + (drag) => ( + + ) + } + + ); + }, +}; diff --git a/webapp/src/components/svg/brush/BrushSelection.js b/webapp/src/components/svg/brush/BrushSelection.js new file mode 100644 index 0000000..eeca76c --- /dev/null +++ b/webapp/src/components/svg/brush/BrushSelection.js @@ -0,0 +1,55 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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. +* ============================================================= +*/ + +export default { + name: 'BrushSelection', + props: { + stageWidth: Number, + stageHeight: Number, + width: Number, + height: Number, + updateBrush: Function, + brush: Object, + onBrushStart: Function, + onBrushEnd: Function, + disableDraggingSelection: { + type: Boolean, + default: false, + }, + selectionStyle: { + type: Object, + }, + }, + + render(h) { + const { width, height, brush, disableDraggingSelection, selectionStyle } = this; + + return ( + + ); + }, +}; diff --git a/webapp/src/components/svg/brush/index.js b/webapp/src/components/svg/brush/index.js new file mode 100644 index 0000000..57a5e31 --- /dev/null +++ b/webapp/src/components/svg/brush/index.js @@ -0,0 +1,19 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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. +* ============================================================= +*/ + +export { default as Brush } from './Brush'; +export { default as BrushHandle } from './BrushHandle'; +export { default as BrushCorner } from './BrushCorner'; diff --git a/webapp/src/components/svg/group/index.js b/webapp/src/components/svg/group/index.js new file mode 100644 index 0000000..e4037e4 --- /dev/null +++ b/webapp/src/components/svg/group/index.js @@ -0,0 +1,42 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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 cx from 'classnames'; + +export default { + name: 'Group', + functional: true, + render(h, context) { + const { props, children } = context; + const { + top = 0, + left = 0, + transform, + className, + ...otherProps + } = props; + + return ( + + {children} + + ); + }, +}; diff --git a/webapp/src/components/svg/index.js b/webapp/src/components/svg/index.js new file mode 100644 index 0000000..f2b4568 --- /dev/null +++ b/webapp/src/components/svg/index.js @@ -0,0 +1,18 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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. +* ============================================================= +*/ + +export { default as Group } from './group'; +export * from './brush'; diff --git a/webapp/src/config/index.js b/webapp/src/config/index.js index 406f934..6255f1c 100644 --- a/webapp/src/config/index.js +++ b/webapp/src/config/index.js @@ -18,7 +18,7 @@ module.exports = { minIO: { development: { config: { - endPoint: '10.5.26.234', + endPoint: '', // MinIO 服务地址 port: 9000, useSSL: false, }, @@ -26,7 +26,7 @@ module.exports = { }, test: { config: { - endPoint: '10.5.26.234', + endPoint: '', port: 9000, useSSL: false, }, @@ -34,7 +34,7 @@ module.exports = { }, production: { config: { - endPoint: '121.41.72.89', + endPoint: '', port: 9000, useSSL: false, }, diff --git a/webapp/src/hooks/brush/useBrush.js b/webapp/src/hooks/brush/useBrush.js index 25e45e4..567d1e8 100644 --- a/webapp/src/hooks/brush/useBrush.js +++ b/webapp/src/hooks/brush/useBrush.js @@ -21,26 +21,53 @@ function useBrush() { const state = reactive({ start: undefined, end: undefined, + extent: undefined, isBrushing: false, }); + function getExtent(start, end) { + const x0 = Math.min(start.x, end.x); + const x1 = Math.max(start.x, end.x); + const y0 = Math.min(start.y, end.y); + const y1 = Math.max(start.y, end.y); + + return { + x0, + x1, + y0, + y1, + }; + } + function onBrushStart({ x, y }) { Object.assign(state, { start: { x, y }, isBrushing: true, end: undefined, + extent: undefined, }); } function onBrushMove({ x, y }) { + const extent = getExtent(state.start, {x, y}); Object.assign(state, { end: { x, y }, + extent, }); } function onBrushEnd() { + const { extent } = state; Object.assign(state, { isBrushing: false, + start: { + x: extent.x0, + y: extent.y0, + }, + end: { + x: extent.x1, + y: extent.y1, + }, }); } @@ -48,15 +75,26 @@ function useBrush() { Object.assign(state, { start: undefined, end: undefined, + extent: undefined, isBrushing: false, }); } + function updateBrush(updater, callback) { + const newState = updater(state); + Object.assign(state, newState); + if(typeof callback === 'function') { + callback(state); + } + } + return ({ brush: state, + getExtent, onBrushStart, onBrushMove, onBrushEnd, + updateBrush, onBrushReset, }); } diff --git a/webapp/src/hooks/zoom/useZoom.js b/webapp/src/hooks/zoom/useZoom.js index 25356ec..e655ab8 100644 --- a/webapp/src/hooks/zoom/useZoom.js +++ b/webapp/src/hooks/zoom/useZoom.js @@ -66,8 +66,13 @@ function useZoom(initialZoom, wrapperRef, options = { updateZoom({ newZoom: 1, zoom: 1, zoomX: 0, zoomY: 0 }); } + function getZoom(){ + return state; + } + return ({ zoom: state, + getZoom, setZoom, zoomIn, zoomOut, diff --git a/webapp/src/layout/BaseLayout.vue b/webapp/src/layout/BaseLayout.vue index 7f6421d..9bfd700 100644 --- a/webapp/src/layout/BaseLayout.vue +++ b/webapp/src/layout/BaseLayout.vue @@ -91,7 +91,8 @@ export default { } else { // 不存在历史记录 // 或者新开 Tab - if (!window.history.length || window.history.length === 1) { + // chrome 新开tab页面历史记录为 2 + if (!window.history.length || window.history.length <= 2) { this.$router.push('/'); return; } diff --git a/webapp/src/layout/components/AppMain/index.vue b/webapp/src/layout/components/AppMain/index.vue index eb8ef3e..367ea94 100644 --- a/webapp/src/layout/components/AppMain/index.vue +++ b/webapp/src/layout/components/AppMain/index.vue @@ -59,6 +59,7 @@ export default { font-size: 0.7rem !important; color: #7a8b9a; letter-spacing: 0.8px; + pointer-events: none; background: none repeat scroll 0 0 white; border-top: 1px solid #e7eaec; } diff --git a/webapp/src/layout/components/Feedback/index.vue b/webapp/src/layout/components/Feedback/index.vue index b5b34e8..40fa179 100644 --- a/webapp/src/layout/components/Feedback/index.vue +++ b/webapp/src/layout/components/Feedback/index.vue @@ -33,15 +33,15 @@
钉钉交流群
-
+
diff --git a/webapp/src/store/modules/dataset.js b/webapp/src/store/modules/dataset.js index fc9505a..a45b239 100644 --- a/webapp/src/store/modules/dataset.js +++ b/webapp/src/store/modules/dataset.js @@ -20,6 +20,7 @@ const state = { activePanel: 0, + activePanelLabelGroup: 0, }; const mutations = { @@ -29,6 +30,12 @@ const mutations = { RESET_PANEL: (state) => { state.activePanel = 0; }, + TOGGLE_PANEL_LABEL_GROUP: (state, panel) => { + state.activePanelLabelGroup = panel; + }, + RESET_PANEL_LABEL_GROUP: (state) => { + state.activePanelLabelGroup = 0; + }, }; const actions = { @@ -38,6 +45,12 @@ const actions = { resetPanel({ commit }) { commit('RESET_PANEL'); }, + togglePanelLabelGroup({ commit }, panel) { + commit('TOGGLE_PANEL_LABEL_GROUP', panel); + }, + resetPanelLabelGroup({ commit }) { + commit('RESET_PANEL_LABEL_GROUP'); + }, }; export default { diff --git a/webapp/src/utils/base.js b/webapp/src/utils/base.js index d5730b1..adc8772 100644 --- a/webapp/src/utils/base.js +++ b/webapp/src/utils/base.js @@ -15,9 +15,17 @@ */ import { format, parseISO, isDate } from 'date-fns'; -import { isEqual, isPlainObject } from 'lodash'; +import { isEqual, isPlainObject, isNil, findIndex, findLastIndex } from 'lodash'; import { nanoid } from 'nanoid'; +const chroma = require('chroma-js'); + +export const duplicate = (arr, callback) => { + const index = findIndex(arr, callback); + const lastIndex = findLastIndex(arr, callback); + return index !== lastIndex; +}; + // 合并多个属性 export function mergeProps(...args) { const props = {}; @@ -144,3 +152,19 @@ export const identity = d => d; export const isEqualByProp = (arr1, arr2, prop) => { return isEqual(arr1.map(d => d[prop]), arr2.map(d => d[prop])); }; + +// 根据背景色深浅来设置颜色 +export const colorByLuminance = (color) => { + if(isNil(color) || color === '') { + return '#333'; + } + const colorMap = { + dark: '#333', + light: '#fff', + }; + const luminance = chroma(color).luminance(); + const theme = luminance < 0.5 ? 'light' : 'dark'; + return colorMap[theme]; +}; + +export { chroma }; diff --git a/webapp/src/utils/event.js b/webapp/src/utils/event.js index c89e9c4..a5c80cd 100644 --- a/webapp/src/utils/event.js +++ b/webapp/src/utils/event.js @@ -25,10 +25,10 @@ export const getCursorPosition = (el, event, options = {}) => { }; // 根据 d3-zoom 获取缩放后的相对位置 -export const getZoomPosition = (el, originPosition = []) => { +export const getZoomPosition = (el, {x, y}) => { const transform = zoomTransform(el); // const invertPosition = transform.invert(originPosition) - return [originPosition[0] / transform.k, originPosition[1] / transform.k]; + return { x: x / transform.k, y: y / transform.k }; // return invertPosition }; @@ -60,6 +60,22 @@ export const generateBbox = (brush) => { }; }; +// Bbox 转为 extent +export const bbox2Extent = bbox => ({ + x0: bbox.x, + y0: bbox.y, + x1: bbox.x + bbox.width, + y1: bbox.y + bbox.height, +}); + +// 将 extent 转为 bbox +export const extent2Bbox = extent => ({ + x: extent.x0, + y: extent.y0, + width: extent.x1 - extent.x0, + height: extent.y1 - extent.y0, +}); + // 解析bbox export const parseBbox = (bbox = []) => { if (!bbox.length) return null; @@ -97,3 +113,32 @@ export function getStyle(el, property) { .getPropertyValue(property) .replace('px', ''); } + +/** + * 向上找到原始 svg 元素 + * @param {[type]} node [节点] + * @param {[type]} event [事件对象] + */ +// eslint-disable-next-line +export const findAncestorSvg = (node, event) => { + // 检测是否有参数传入 + if (!node) return null; + + // 如果只有一个参数 + if (node.target) { + event = null; + // 当前元素的 svg 包裹元素 + node = node.target.ownerSVGElement; + } + // 向上一直遍历,直到找到 svg 元素 + while (node.ownerSVGElement) { + node = node.ownerSVGElement; + } + + return node; +}; + +export const raise = (arr, raiseIndex) => { + return ([...arr.slice(0, raiseIndex), ...arr.slice(raiseIndex + 1), arr[raiseIndex]]); +}; + diff --git a/webapp/src/utils/request.js b/webapp/src/utils/request.js index 440d52d..4c4c38e 100644 --- a/webapp/src/utils/request.js +++ b/webapp/src/utils/request.js @@ -71,6 +71,10 @@ service.interceptors.request.use( service.interceptors.response.use( response => { const res = response.data; + // 如果请求的返回类型是流,则直接返回 data + if (response.config.responseType === 'blob') { + return res; + } // if the custom code is not 200, it is judged as an error. if (res.code !== 200) { if (isWhiteList(response.config.url)) { diff --git a/webapp/src/utils/utils.js b/webapp/src/utils/utils.js index a29e555..fca1c16 100644 --- a/webapp/src/utils/utils.js +++ b/webapp/src/utils/utils.js @@ -18,6 +18,8 @@ * utils, 通用方法 */ +import { nanoid } from 'nanoid'; + /** * Parse the time to string * @param {(Object|string|number)} time @@ -252,5 +254,92 @@ export function stringIsValidPythonVariable(str) { } const pattern = /^[_a-zA-Z][_a-zA-Z0-9]*$/; return pattern.test(str); +} + + +const _toTree = (data) => { + const result = []; + if (!Array.isArray(data)) { + return result; + } + data.forEach(item => { + delete item.children; + }); + const map = {}; + data.forEach(item => { + map[item.id] = item; + }); + data.forEach(item => { + const parent = map[item.pid]; + if (parent) { + (parent.children || (parent.children = [])).push(item); + } else { + result.push(item); + } + }); + return result; +}; + +/** + * minio数据转成树形结构 + * @param {string} filepath minio的路径 + * @returns {list} [treeList, expandedKeys] 树形结构,和默认展开的元素 + */ +export const getTreeListFromFilepath = async (filepath) => { + // 1,获取minio的数据 + const tmp = await window.minioClient.listObjects(filepath); + if(!tmp || !tmp.length){ + return [[], []]; + } + const minioList = []; + for (const item of tmp) { + minioList.push(item.name.replace(filepath, "")); + } + // 2 转成平级数据 + const dataList = []; + const keyList = []; // 去重用 + for (const filename of minioList) { + const list = filename.split("/"); + list.forEach((item, index) => { + const p = { + pid: index === 0 ? 9999 : `${index - 1}_${list[index - 1]}`, + id: `${index}_${item}`, + name: item, + originPath: filepath + list.slice(0,index+1).join('/'), + isFile: index === list.length-1, + }; + const key = `${p.pid}_${p.id}`; + if (keyList.indexOf(key) === -1) { + keyList.push(key); + dataList.push(p); + } + }); + } + // 2.1 最外层单独封装一层 + const tmp2 = filepath.split('/'); + const wrapperNodeName = tmp2[tmp2.length-2]; + const wrapperNode = { + pid: 0, + id: 9999, + name: wrapperNodeName, + originPath: filepath, + isFile: false, + }; + dataList.push(wrapperNode); + // 3 转成树形结构 + const treeList = _toTree([].concat(dataList)); + // 4 显示默认展开的层级,默认二级 + const expandedKeys = []; + for (const item of treeList) { + expandedKeys.push(item.id); + for(const item2 of item.children){ + expandedKeys.push(item2.id); + } + } + // 返回数据 + return [treeList, expandedKeys]; +}; +export function getUniqueId() { + return parseTime(new Date(), '{y}{m}{d}{h}{i}{s}{S}') + nanoid(4); } diff --git a/webapp/src/utils/validate.js b/webapp/src/utils/validate.js index 890cf7b..118e87e 100644 --- a/webapp/src/utils/validate.js +++ b/webapp/src/utils/validate.js @@ -18,6 +18,7 @@ * validate,校验函数 */ +import { isPlainObject } from 'lodash'; import { ValidationProvider, ValidationObserver, extend } from 'vee-validate'; import { required } from 'vee-validate/dist/rules'; @@ -268,3 +269,29 @@ export function validateRunCommand(rule, value, callback) { callback(new Error('请输入正确的启动命令')); } } + +// 校验标签组基本方法 +export const validateLabelsUtil = (value) => { + if(!isPlainObject(value)) { + return '标签不能为空'; + } + if(!value.name) { + return '标签名称不能为空'; + } + if(!value.color) { + return '标签颜色不能为空'; + } + if(!/^#[0-9A-F]{6}$/i.test(value.color)) { + return '标签颜色格式不对'; + } + return ''; +}; + +export function validateLabel(rule, value, callback) { + const validateResult = validateLabelsUtil(value); + if(validateResult !== '') { + callback(new Error(validateResult)); + return; + } + callback(); +} diff --git a/webapp/src/views/algorithm/index.vue b/webapp/src/views/algorithm/index.vue index 7ec82a4..5e00ac8 100644 --- a/webapp/src/views/algorithm/index.vue +++ b/webapp/src/views/algorithm/index.vue @@ -21,6 +21,7 @@
- - + +
@@ -82,19 +84,19 @@ + + diff --git a/webapp/src/views/trainingJob/components/pathSelectDialog.vue b/webapp/src/views/trainingJob/components/pathSelectDialog.vue new file mode 100644 index 0000000..4c4b66a --- /dev/null +++ b/webapp/src/views/trainingJob/components/pathSelectDialog.vue @@ -0,0 +1,206 @@ +/** Copyright 2020 Zhejiang Lab. All Rights Reserved. +* +* Licensed 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. +* ============================================================= +*/ + + + + diff --git a/webapp/src/views/trainingJob/detail.vue b/webapp/src/views/trainingJob/detail.vue index 94c8abe..c453e0c 100644 --- a/webapp/src/views/trainingJob/detail.vue +++ b/webapp/src/views/trainingJob/detail.vue @@ -61,38 +61,56 @@ 停止 修改 可视化 - + 保存模型 断点续训 更多 + + 保存模型 + + @@ -103,6 +121,7 @@ >保存任务模板
@@ -129,11 +148,8 @@ @@ -142,15 +158,22 @@ ref="saveModel" type="training" /> + + - + @@ -162,38 +185,20 @@ import { mapGetters } from 'vuex'; import { debounce } from 'throttle-debounce'; import CRUD, { presenter, header, crud } from '@crud/crud'; -import { parseTime, Constant } from '@/utils'; -import crudJob, { getJobList, resumeTrain, stop as stopJob, del as deleteJob, edit as editJob } from '@/api/trainingJob/job'; +import { Constant } from '@/utils'; +import crudJob, { getJobList, stop as stopJob, del as deleteJob, edit as editJob } from '@/api/trainingJob/job'; import { add as addParams } from '@/api/trainingJob/params'; import BaseModal from '@/components/BaseModal'; import JobForm from '@/components/Training/jobForm'; import SaveModelDialog from '@/components/Training/saveModelDialog'; +import pathSelectDialog from './components/pathSelectDialog'; import jobDrawer from './components/jobDrawer'; import { trainingStatusMap as map } from './utils'; -const defaultJobForm = { - $_id: 0, - trainName: '', - paramName: '', - description: '', - algorithmSource: 1, - algorithmId: null, - imageTag: null, - imageName: null, - runCommand: null, - dataSourceName: null, - dataSourcePath: null, - outPath: '/home/result/', - logPath: '/home/log/', - resourcesPoolType: 0, - trainJobSpecsId: null, - runParams: {}, -}; - export default { name: 'JobDetail', dicts: ['job_status'], - components: { BaseModal, JobForm, jobDrawer, SaveModelDialog }, + components: { BaseModal, JobForm, jobDrawer, SaveModelDialog, pathSelectDialog }, cruds() { return CRUD({ crudMethod: { ...crudJob }, @@ -209,17 +214,16 @@ export default { data() { return { id: null, - form: { ...defaultJobForm}, // 任务版本编辑 tableList: [], // 任务版本列表 params: {}, map, + pathType: '', // 目录树选择类型 keepPool: true, showDialog: false, dialogTitle: '', dialogType: 'edit', // edit: 修改训练任务; saveParams: 保存任务参数 - drawer: false, + drawerVisible: false, reFresh: true, // job_form_id 没法重新渲染,先用refresh - jobDetail: null, // job 详情 modelList: [], submitLoading: false, resumeLoading: false, @@ -254,11 +258,12 @@ export default { this.keepPool = false; }, methods: { - parseTime, // handle 操作 onRowClick(row) { - this.jobDetail = row; - this.drawer = true; + this.drawerVisible = true; + this.$nextTick(() => { + this.$refs.jobDrawer.onOpen(row.id); + }); }, handleSortChange({ prop, order }) { const sortParams = { @@ -268,10 +273,8 @@ export default { this.params = Object.assign(this.params, sortParams); this.getJobList(); }, - handleDrawerOpen() { - setTimeout(() => { - this.$refs.jobDrawer.reset(); - }, 0); + handleDrawerClose() { + this.$refs.jobDrawer.onClose(); }, // 页面逻辑 async getJobList() { @@ -289,25 +292,9 @@ export default { this.getJobList(); }, async getForm(form) { - const { id, paramName } = form; - const params = { - description: form.description, - algorithmId: form.algorithmId, - dataSourceName: form.dataSourceName, - dataSourcePath: form.dataSourcePath, - outPath: form.outPath, - logPath: form.logPath, - runParams: form.runParams, - resourcesPoolType: form.resourcesPoolType, - trainJobSpecsId: form.trainJobSpecsId, - imageTag: form.imageTag, - imageName: form.imageName, - runCommand: form.runCommand, - }; this.submitLoading = true; if (this.dialogType === 'edit') { - params.id = id; - await editJob(params).finally(() => { + await editJob(form).finally(() => { this.submitLoading = false; }); this.refreshList(); @@ -316,8 +303,7 @@ export default { type: 'success', }); } else { - params.paramName = paramName; - await addParams(params).finally(() => { + await addParams(form).finally(() => { this.submitLoading = false; }); this.$message({ @@ -339,9 +325,11 @@ export default { if (dialogType === 'saveParams') { item.paramName = item.jobName; } - this.form = { ...item}; - this.form.$_id = new Date().getTime(); + item.valAlgorithmUsage = item.algorithmUsage; this.showDialog = true; + this.$nextTick(() => { + this.$refs.jobFormEdit.initForm(item); + }); this.dialogTitle = dialogType === 'edit' ? '修改任务' : '保存任务模板'; this.dialogType = dialogType; this.reFresh = true; @@ -357,6 +345,7 @@ export default { window.open(url, '_blank'); }, async goSaveModel(model) { + this.pathType = 'modelSelect'; const modelParams = { algorithmId: model.algorithmId, algorithmName: model.algorithmName, @@ -364,7 +353,13 @@ export default { algorithmUsage: model.algorithmUsage, modelAddress: model.outPath, }; - this.$refs.saveModel.show(modelParams); + this.$nextTick(() => { + this.$refs.pathSelect.show({ + resumePath: `${model.outPath}/`, + id: model.algorithmId, + params: modelParams, + }); + }); }, // op doEdit() { @@ -407,13 +402,22 @@ export default { } }); }, - async doResume(id) { - this.resumeLoading = true; - await resumeTrain({ id }).finally(() => { - this.resumeLoading = false; - this.refreshList(); + async doResume(item) { + this.pathType = 'jobResume'; + this.$nextTick(() => { + this.$refs.pathSelect.show({ + resumePath: `${item.outPath}/`, + id: item.id, + }); }); }, + chooseDone() { + this.refreshList(); + }, + chooseModel(selectPath, params) { + Object.assign(params, {modelAddress: selectPath}); + this.$refs.saveModel.show(params); + }, }, }; diff --git a/webapp/src/views/trainingJob/index.vue b/webapp/src/views/trainingJob/index.vue index 85b2739..5cd2410 100644 --- a/webapp/src/views/trainingJob/index.vue +++ b/webapp/src/views/trainingJob/index.vue @@ -21,85 +21,72 @@
- 创建训练任务 - + >创建训练任务 - - 重置 - - 搜索 + 重置 + 搜索
- - - + + + - +