| @@ -1,3 +1,9 @@ | |||
| ## 2.1.0 (2021-12-22) | |||
| ### Breaking Change | |||
| - [自动机器学习] 新增自动机器学习模块 | |||
| ## 2.0.0 (2021-08-30) | |||
| ### Breaking Change | |||
| @@ -8,5 +8,5 @@ if (process.env.NODE_ENV === "production") { | |||
| } | |||
| module.exports = { | |||
| plugins, | |||
| presets: ["@vue/app"], | |||
| presets: [["@vue/app",{ useBuiltIns: "entry" }]], | |||
| }; | |||
| @@ -1,6 +1,6 @@ | |||
| { | |||
| "name": "dubhe-web", | |||
| "version": "2.0.0", | |||
| "version": "2.1.0", | |||
| "description": "之江天枢人工智能开源平台", | |||
| "author": "zhejianglab", | |||
| "keywords": [ | |||
| @@ -41,6 +41,8 @@ | |||
| "url": "git@codeup.teambition.com:zhejianglab/dubhe-web.git" | |||
| }, | |||
| "dependencies": { | |||
| "@antv/g2plot": "^2.3.17", | |||
| "@opd/g2plot-vue": "3.1.12", | |||
| "@riophae/vue-treeselect": "0.1.0", | |||
| "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", | |||
| "@vue/composition-api": "^1.0.0-rc.1", | |||
| @@ -51,6 +53,8 @@ | |||
| "chroma-js": "^2.1.0", | |||
| "classnames": "^2.2.6", | |||
| "clipboard": "^2.0.6", | |||
| "codemirror": "^5.60.0", | |||
| "core-js": "^3.9.1", | |||
| "d3": "^5.16.0", | |||
| "d3-selection": "^1.4.1", | |||
| "d3-zoom": "^1.8.3", | |||
| @@ -68,6 +72,8 @@ | |||
| "jquery-contextmenu": "^2.9.1", | |||
| "js-beautify": "^1.13.0", | |||
| "js-cookie": "2.2.0", | |||
| "js-yaml": "^4.0.0", | |||
| "jschardet": "^2.2.1", | |||
| "jsencrypt": "^3.0.0-rc.1", | |||
| "json2csv": "^5.0.1", | |||
| "lodash": "^4.17.15", | |||
| @@ -87,6 +93,7 @@ | |||
| "screenfull": "^5.0.2", | |||
| "stream-to-array": "^2.3.0", | |||
| "streamsaver": "^2.0.4", | |||
| "url-join": "^4.0.1", | |||
| "v-click-outside": "^3.0.1", | |||
| "v-hotkey": "^0.8.0", | |||
| "vee-validate": "^3.3.0", | |||
| @@ -0,0 +1,292 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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'; | |||
| import { API_MODULE_NAME } from '@/config'; | |||
| export function list(params) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| // 创建/保存实验 | |||
| export function createExperiment(data) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment`, | |||
| method: 'post', | |||
| data, | |||
| }); | |||
| } | |||
| // 编辑实验 | |||
| export function editExperiment(data) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment`, | |||
| method: 'put', | |||
| data, | |||
| }); | |||
| } | |||
| // 查询实验详情的概览 | |||
| export function expDetailOverview(experimentId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 查询实验详情 | |||
| export function expDetail(experimentId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/info`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 查询阶段概览 | |||
| export function expStageInfo(experimentId, stageOrder) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 查询阶段实验参数 | |||
| export function expStageParam(experimentId, stageOrder) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/param`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 查询阶段运行中参数 | |||
| export function expStageRuntimeParam(experimentId, stageOrder) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/runtime/param`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 修改阶段运行参数之trial并发数 | |||
| export function updateConcurrentNum(experimentId, stageOrder, trialConcurrentNum) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/update/ConcurrentNum`, | |||
| method: 'put', | |||
| data: { experimentId, stageOrder, trialConcurrentNum }, | |||
| }); | |||
| } | |||
| // 修改阶段运行参数之trial最大值 | |||
| export function updateMaxTrialNum(experimentId, stageOrder, maxTrialNum) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/update/MaxTrialNum`, | |||
| method: 'put', | |||
| data: { experimentId, stageOrder, maxTrialNum }, | |||
| }); | |||
| } | |||
| // 修改阶段运行参数之最大运行时间 | |||
| export function updateMaxExecDuration( | |||
| experimentId, | |||
| stageOrder, | |||
| maxExecDuration, | |||
| maxExecDurationUnit | |||
| ) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/update/MaxExecDuration`, | |||
| method: 'put', | |||
| data: { experimentId, stageOrder, maxExecDuration, maxExecDurationUnit }, | |||
| }); | |||
| } | |||
| // 查询阶段trial精度最高5条 | |||
| export function expStageTrialRep(experimentId, stageOrder) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/trial/rep`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 查询实验配置信息 | |||
| export function expYaml(experimentId, stageOrder) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/yaml`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 修改实验配置yaml | |||
| export function updateExpYaml(experimentId, stageOrder, yaml) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/update/yaml`, | |||
| method: 'put', | |||
| data: { yaml, experimentId, stageOrder }, | |||
| }); | |||
| } | |||
| // 查询阶段trialsList列表 | |||
| export function expStageTrialList({ experimentId, stageOrder, current = 1, size = 1, ...args }) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/trial/${experimentId}/${stageOrder}/list`, | |||
| method: 'get', | |||
| params: { | |||
| experimentId, | |||
| stageOrder, | |||
| current, | |||
| size, | |||
| ...args, | |||
| }, | |||
| }); | |||
| } | |||
| // 查询阶段运行标准输出数据 | |||
| export function expStageAccuracy(experimentId, stageOrder) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/best/accuracy`, | |||
| method: 'get', | |||
| params: { | |||
| experimentId, | |||
| stageOrder, | |||
| }, | |||
| }); | |||
| } | |||
| // 查询多trial图数据 | |||
| export function expStageTrialData(experimentId, stageOrder, params) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/trialData`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| // 查询阶段运行中间值 | |||
| export function expStageIntermediate(experimentId, stageOrder, trialIds = null) { | |||
| const params = { | |||
| experimentId, | |||
| stageOrder, | |||
| trialIds, | |||
| }; | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/intermediate/accuracy`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| export function expStageRuntime(experimentId, stageOrder) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/runTime`, | |||
| method: 'get', | |||
| params: { | |||
| experimentId, | |||
| stageOrder, | |||
| }, | |||
| }); | |||
| } | |||
| // 启动实验 | |||
| export function startExp(experimentId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/start`, | |||
| method: 'put', | |||
| }); | |||
| } | |||
| // 暂停实验 | |||
| export function pauseExp(experimentId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/pause`, | |||
| method: 'put', | |||
| }); | |||
| } | |||
| // 删除实验 | |||
| export function deleteExp(experimentId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}`, | |||
| method: 'delete', | |||
| }); | |||
| } | |||
| // 查询searchspace内容 | |||
| export function getSearchSpace(experimentId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/searchSpace`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 查询best selected space内容 | |||
| export function getSelectedSpace(experimentId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/bestSelectedSpace`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 查询实验config | |||
| export function getExpConfig(experimentId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/config`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 查询实验总日志 | |||
| export function getExpLog(params) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/${params.experimentId}/logs`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| // 查询trial日志详情 | |||
| export function trialLog(trialId, startLine = 1, lines = 50) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/trial/trialLog`, | |||
| method: 'get', | |||
| params: { | |||
| trialId, | |||
| startLine, | |||
| lines, | |||
| }, | |||
| }); | |||
| } | |||
| /** | |||
| * /api/ {version} /tadl /experiment/{experimentId}/{stageOrder}/ {trialId} /model | |||
| */ | |||
| // 下载模型 | |||
| export function downloadModel(experimentId, stageOrder, trialId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/trial/${trialId}/model`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| // 获取资源配置 | |||
| export function getResources(params) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/experiment/resource`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| @@ -0,0 +1,106 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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'; | |||
| import { API_MODULE_NAME } from '@/config'; | |||
| // 算法解压 | |||
| export function unpackZip(params) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/unzip`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| export function parseYamlParams(params) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/yaml`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| export function getStrategyList(params) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/query`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| export function getVersionList(id) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/${id}/list`, | |||
| method: 'get', | |||
| }); | |||
| } | |||
| export function uploadStrategy(data) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/upload`, | |||
| method: 'post', | |||
| data, | |||
| }); | |||
| } | |||
| export function updateStrategy(data) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/update`, | |||
| method: 'post', | |||
| data, | |||
| }); | |||
| } | |||
| export function getNextVersion(algorithmId) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/${algorithmId} /next/version`, | |||
| method: 'get', | |||
| params: { algorithmId }, | |||
| }); | |||
| } | |||
| export function versionRelease(data) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/push/version`, | |||
| method: 'post', | |||
| data, | |||
| }); | |||
| } | |||
| export function shiftVersion(data) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/version/switch`, | |||
| method: 'put', | |||
| data, | |||
| }); | |||
| } | |||
| export function checkStrategy(params, id) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm/${id}/query`, | |||
| method: 'get', | |||
| params, | |||
| }); | |||
| } | |||
| export function deleteVersion(data) { | |||
| return request({ | |||
| url: `/${API_MODULE_NAME.TADL}/algorithm`, | |||
| method: 'delete', | |||
| data, | |||
| }); | |||
| } | |||
| @@ -180,6 +180,10 @@ | |||
| margin-left: 4px; | |||
| } | |||
| .mr-4 { | |||
| margin-right: 4px; | |||
| } | |||
| .ml-10 { | |||
| margin-left: 10px; | |||
| } | |||
| @@ -204,6 +208,10 @@ | |||
| margin-left: auto; | |||
| } | |||
| .mb-0 { | |||
| margin-bottom: 0; | |||
| } | |||
| .mb-50 { | |||
| margin-bottom: 50px; | |||
| } | |||
| @@ -250,6 +258,10 @@ | |||
| margin-bottom: auto; | |||
| } | |||
| .w-80 { | |||
| width: 80px; | |||
| } | |||
| .w-100 { | |||
| width: 100px; | |||
| } | |||
| @@ -411,3 +423,7 @@ img.responsive { | |||
| width: 100vw; | |||
| height: 100vh; | |||
| } | |||
| .multiple-lines { | |||
| @include multiple-lines; | |||
| } | |||
| @@ -264,6 +264,10 @@ pre { | |||
| color: $infoColor; | |||
| } | |||
| .CodeMirror-lint-tooltip { | |||
| z-index: 10000 !important; | |||
| } | |||
| .app-result-content { | |||
| padding: 24px 40px; | |||
| margin-top: 24px; | |||
| @@ -339,3 +339,29 @@ | |||
| } | |||
| } | |||
| } | |||
| .el-tooltip__popper.is-light { | |||
| border: none; | |||
| box-shadow: rgba(0, 0, 0, 0.15) 0 2px 8px 0; | |||
| .popper__arrow { | |||
| border: none; | |||
| } | |||
| } | |||
| .el-tabs-large .el-tabs__nav .el-tabs__item { | |||
| font-size: 16px; | |||
| } | |||
| .el-card__footer { | |||
| padding-top: 20px; | |||
| margin-top: 8px; | |||
| border-top: 1px solid #f0f0f0; | |||
| } | |||
| .el-form-item-explain { | |||
| min-height: 24px; | |||
| font-size: 14px; | |||
| line-height: 1.5715; | |||
| color: rgba(0, 0, 0, 0.45); | |||
| } | |||
| @@ -21,6 +21,7 @@ | |||
| @import 'element-ui'; | |||
| @import 'sidebar'; | |||
| @import 'common'; | |||
| @import url('//at.alicdn.com/t/font_1756495_ohftzv0cq9c.css'); | |||
| @media screen and (max-width: 768px) { | |||
| .mb-dn { | |||
| @@ -93,6 +94,11 @@ ol li { | |||
| color: $primaryColor; | |||
| } | |||
| .disabled { | |||
| color: $disableColor; | |||
| cursor: not-allowed; | |||
| } | |||
| .infoColor { | |||
| color: $infoColor; | |||
| } | |||
| @@ -43,6 +43,13 @@ | |||
| white-space: nowrap; | |||
| } | |||
| @mixin multiple-lines { | |||
| display: -webkit-box; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| -webkit-box-orient: vertical; | |||
| } | |||
| @mixin relative { | |||
| position: relative; | |||
| width: 100%; | |||
| @@ -1,18 +1,10 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-table ref="table" v-loading="loading" :data="data" v-bind="attrs" v-on="$listeners"> | |||
| @@ -68,6 +60,7 @@ | |||
| @click.native="() => runFunc(moreOp.func, scope.row)" | |||
| > | |||
| <el-button | |||
| v-click-once="moreOp.clickOnceTime || 1000" | |||
| type="text" | |||
| :disabled="getOperationStatus('disabled', 'disableFunc', moreOp, scope.row)" | |||
| > | |||
| @@ -81,6 +74,7 @@ | |||
| v-else-if="!getOperationStatus('hide', 'hideFunc', operation, scope.row)" | |||
| :id="operation.label + scope.$index" | |||
| :key="operation.key" | |||
| v-click-once="operation.clickOnceTime || 1000" | |||
| type="text" | |||
| :disabled="getOperationStatus('disabled', 'disableFunc', operation, scope.row)" | |||
| @click.stop="() => runFunc(operation.func, scope.row)" | |||
| @@ -94,6 +88,13 @@ | |||
| <el-tag v-else-if="column.type === 'tag'" v-bind="getTagAttrs(column, scope.row)">{{ | |||
| getContent(column, scope.row) | |||
| }}</el-tag> | |||
| <!-- link 列 --> | |||
| <el-link | |||
| v-else-if="column.type === 'link'" | |||
| type="primary" | |||
| @click="runFunc(column.func, scope.row)" | |||
| >{{ getContent(column, scope.row) }}</el-link | |||
| > | |||
| <!-- 其他展示列 --> | |||
| <span v-else> | |||
| {{ getContent(column, scope.row) }} | |||
| @@ -107,7 +108,7 @@ | |||
| <script> | |||
| import { computed, ref } from '@vue/composition-api'; | |||
| import { parseTime, noop, runFunc } from '@/utils'; | |||
| import { parseTime, noop, runFunc, restProps } from '@/utils'; | |||
| import DropdownHeader from '@/components/DropdownHeader'; | |||
| const defaultColunmDefinition = { | |||
| @@ -232,7 +233,7 @@ export default { | |||
| if (typeof column.tagAttrFunc === 'function') { | |||
| return column.tagAttrFunc(row[column.prop], row); | |||
| } | |||
| const tagAttr = column.tagAttr || {}; | |||
| const tagAttr = restProps(column.tagAttr || {}, row); | |||
| const tagMap = column.tagMap || {}; | |||
| return { | |||
| type: tagMap[row[column.prop]], | |||
| @@ -270,6 +271,7 @@ export default { | |||
| // Utils 方法 | |||
| parseTime, | |||
| runFunc, | |||
| restProps, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,42 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-tooltip v-bind="mergedAttrs"> | |||
| <i class="primary f18 vm" :class="[icon]" /> | |||
| </el-tooltip> | |||
| </template> | |||
| <script> | |||
| import { computed } from '@vue/composition-api'; | |||
| const defaultAttr = { | |||
| effect: 'dark', | |||
| placement: 'top', | |||
| }; | |||
| export default { | |||
| name: 'BaseTooltip', | |||
| props: { | |||
| icon: { | |||
| type: String, | |||
| default: 'el-icon-warning-outline', | |||
| }, | |||
| }, | |||
| setup(props, ctx) { | |||
| const mergedAttrs = computed(() => ({ | |||
| ...defaultAttr, | |||
| ...ctx.attrs, | |||
| })); | |||
| return { | |||
| mergedAttrs, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,66 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="description-items"> | |||
| <table> | |||
| <tbody> | |||
| <tr v-for="(row, index) in columns" :key="index" class="descriptions-row"> | |||
| <DescriptionItem | |||
| v-for="col in row" | |||
| :key="col[labelBy]" | |||
| class="description-item" | |||
| :col="col" | |||
| v-bind="attrs" | |||
| > | |||
| <template v-for="(_, name) in $slots" v-slot:[name]> | |||
| <slot :name="name" /> | |||
| </template> | |||
| </DescriptionItem> | |||
| </tr> | |||
| </tbody> | |||
| </table> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { computed } from '@vue/composition-api'; | |||
| import DescriptionItem from './item'; | |||
| export default { | |||
| name: 'Description', | |||
| components: { | |||
| DescriptionItem, | |||
| }, | |||
| props: { | |||
| columns: { | |||
| type: Array, | |||
| default: () => [], | |||
| }, | |||
| labelBy: String, | |||
| }, | |||
| setup(props, ctx) { | |||
| const attrs = computed(() => ctx.attrs); | |||
| return { | |||
| attrs, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .descriptions-row { | |||
| line-height: 1.5; | |||
| } | |||
| .description-items { | |||
| table { | |||
| width: 100%; | |||
| table-layout: fixed; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,50 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <td :colspan="col.span || 1" class="description-item"> | |||
| <span class="description-item-label">{{ col[labelBy] }}:</span> | |||
| <span v-if="state.content" class="description-item-content">{{ state.content }}</span> | |||
| <slot v-else :name="col[labelBy]"></slot> | |||
| </td> | |||
| </template> | |||
| <script> | |||
| import { reactive, watch } from '@vue/composition-api'; | |||
| export default { | |||
| name: 'DescriptionItem', | |||
| props: { | |||
| col: Object, | |||
| data: Object, | |||
| contentBy: { | |||
| type: String, | |||
| default: 'content', | |||
| }, | |||
| labelBy: { | |||
| type: String, | |||
| default: 'label', | |||
| }, | |||
| }, | |||
| setup(props) { | |||
| const state = reactive({ | |||
| content: props.col[props.contentBy] || null, | |||
| }); | |||
| watch( | |||
| () => props.col, | |||
| (next) => { | |||
| state.content = next[props.contentBy]; | |||
| } | |||
| ); | |||
| return { | |||
| state, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -26,7 +26,7 @@ | |||
| import create from './iconfont'; | |||
| const IconFont = create({ | |||
| scriptUrl: '//at.alicdn.com/t/font_1756495_hq281r3cld4.js', | |||
| scriptUrl: '//at.alicdn.com/t/font_1756495_ohftzv0cq9c.js', | |||
| extraIconProps: { class: 'svg-icon' }, | |||
| }); | |||
| @@ -1,18 +1,10 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <ValidationObserver ref="observerRef"> | |||
| @@ -24,7 +16,7 @@ | |||
| :title="props.title" | |||
| @show="onShow" | |||
| > | |||
| <ValidationProvider v-slot="{ errors }" :rules="rules" :name="label"> | |||
| <ValidationProvider ref="providerRef" v-slot="{ errors }" :rules="rules" :name="label"> | |||
| <el-input | |||
| ref="inputRef" | |||
| v-model.trim="state.value" | |||
| @@ -32,20 +24,24 @@ | |||
| placeholder="" | |||
| :type="inputType" | |||
| @keyup.enter.native="handleOk" | |||
| /> | |||
| > | |||
| <template v-for="(_, name) in $slots" v-slot:[name]> | |||
| <slot :name="name" /> | |||
| </template> | |||
| </el-input> | |||
| <p class="error-message" style="margin-top: 4px;">{{ errors[0] }}</p> | |||
| </ValidationProvider> | |||
| <div class="tc" style="margin-top: 8px;"> | |||
| <el-button @click="handleCancel">取消</el-button> | |||
| <el-button type="primary" @click="handleOk">确定</el-button> | |||
| </div> | |||
| <i slot="reference" class="el-icon-edit primary cp dib" /> | |||
| <i slot="reference" :class="triggerClass" /> | |||
| </el-popover> | |||
| </ValidationObserver> | |||
| </template> | |||
| <script> | |||
| import Vue from 'vue'; | |||
| import { reactive, ref, watch } from '@vue/composition-api'; | |||
| import { reactive, ref, watch, computed, nextTick } from '@vue/composition-api'; | |||
| import cx from 'classnames'; | |||
| export default { | |||
| name: 'Edit', | |||
| @@ -72,11 +68,20 @@ export default { | |||
| type: String, | |||
| default: '名称', // 错误展示字段 | |||
| }, | |||
| disabled: { | |||
| type: Boolean, | |||
| default: false, | |||
| }, | |||
| // 修改前校验 | |||
| beforeChange: { | |||
| type: Function, | |||
| }, | |||
| }, | |||
| setup(props, ctx) { | |||
| const { valueBy } = props; | |||
| const { valueBy, beforeChange } = props; | |||
| const observerRef = ref(null); | |||
| const inputRef = ref(null); | |||
| const providerRef = ref(null); | |||
| const state = reactive({ | |||
| visible: false, | |||
| @@ -97,24 +102,40 @@ export default { | |||
| if (!success) { | |||
| return; | |||
| } | |||
| // 判断是否发生过变更 | |||
| if (String(state.value) !== String(props.row[valueBy])) { | |||
| ctx.emit('handleOk', state.value, props.row); | |||
| if (typeof beforeChange === 'function') { | |||
| beforeChange(state.value, props.row, providerRef, { valueBy }) | |||
| .then(() => { | |||
| // TODO: 判断是否发生过变更 | |||
| ctx.emit('handleOk', state.value, props.row, { valueBy }); | |||
| handleCancel(); | |||
| }) | |||
| .catch((err) => { | |||
| console.error(err); | |||
| }); | |||
| } else { | |||
| ctx.emit('handleOk', state.value, props.row, { valueBy }); | |||
| handleCancel(); | |||
| } | |||
| handleCancel(); | |||
| }); | |||
| }; | |||
| const onShow = () => { | |||
| // onShow 的时候重置 | |||
| state.value = props.row[valueBy]; | |||
| Vue.nextTick(() => { | |||
| nextTick(() => { | |||
| const input = | |||
| (inputRef && inputRef.value.$refs.input) || (inputRef && inputRef.value.$refs.textarea); | |||
| input && input.focus(); | |||
| }); | |||
| }; | |||
| const triggerClass = computed(() => | |||
| cx('el-icon-edit primary cp dib', { | |||
| disabled: !!props.disabled, | |||
| pen: !!props.disabled, | |||
| }) | |||
| ); | |||
| watch( | |||
| () => props.row, | |||
| (next) => { | |||
| @@ -129,8 +150,10 @@ export default { | |||
| state, | |||
| inputRef, | |||
| observerRef, | |||
| providerRef, | |||
| handleOk, | |||
| handleCancel, | |||
| triggerClass, | |||
| onShow, | |||
| }; | |||
| }, | |||
| @@ -1,18 +1,10 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="pro-table-header flex py-4"> | |||
| @@ -38,6 +30,7 @@ | |||
| >{{ deleteTitle }}</el-button | |||
| > | |||
| </slot> | |||
| <i v-if="loading" class="el-icon-loading" /> | |||
| </span> | |||
| <span class="header-right ml-auto"> | |||
| <slot name="right"> | |||
| @@ -102,6 +95,11 @@ export default { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 是否展示 loading 图标 | |||
| loading: { | |||
| type: Boolean, | |||
| default: false, | |||
| }, | |||
| }, | |||
| setup(props, { emit }) { | |||
| // 点击创建按钮,抛出创建事件 | |||
| @@ -1,18 +1,10 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="pro-table-container"> | |||
| @@ -25,6 +17,7 @@ | |||
| :delete-title="deleteTitle" | |||
| :form-items="mergedFormItems" | |||
| :form-model="state.queryFormModel" | |||
| :loading="headerLoading" | |||
| @create="onCreate" | |||
| @delete="onDelete" | |||
| > | |||
| @@ -64,7 +57,7 @@ | |||
| </slot> | |||
| <BaseTable | |||
| ref="table" | |||
| v-loading="state.loading" | |||
| v-loading="tableLoading" | |||
| v-bind="tableAttrs" | |||
| :columns="mergedColumns" | |||
| :data="state.data" | |||
| @@ -77,7 +70,7 @@ | |||
| </template> | |||
| </BaseTable> | |||
| <el-pagination | |||
| v-if="showPagination" | |||
| v-if="pageShow" | |||
| v-bind="mergedPageAttrs" | |||
| :style="`text-align:${pageAlign}; margin-top: 8px;`" | |||
| @size-change="onSizeChange" | |||
| @@ -182,6 +175,10 @@ export default { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 查询之前的回调方法,如果返回 false 则停止请求 | |||
| beforeListFn: Function, | |||
| // 查询之后的回调方法,入参为当前查询结果 | |||
| afterListFn: Function, | |||
| // 删除数据方法 | |||
| delRequest: Function, | |||
| // 调用默认删除接口时用于获取 ID 字段 | |||
| @@ -189,6 +186,16 @@ export default { | |||
| type: String, | |||
| default: 'id', | |||
| }, | |||
| // 区分在表格上展示 loading 还是在头部展示 loading。table - 表格; header - 头部。 | |||
| loadingType: { | |||
| type: String, | |||
| default: 'table', | |||
| }, | |||
| // 是否在渲染之后立刻请求数据 | |||
| refreshImmediate: { | |||
| type: Boolean, | |||
| default: true, | |||
| }, | |||
| }, | |||
| setup(props, ctx) { | |||
| const { formItems, paginationAttrs, deleteDisabled, columns } = toRefs(props); | |||
| @@ -201,7 +208,7 @@ export default { | |||
| data: [], // 表格数据 | |||
| selectedRows: [], // 表格多选行 | |||
| loading: false, // 表格 loading 状态 | |||
| queryObj: {}, // 其他查询对象 | |||
| paginationVisible: false, // 需要在请求之后展示分页,避免分页页码提前设置之后无法正确展示 | |||
| }); | |||
| // 搜索 | |||
| @@ -254,6 +261,10 @@ export default { | |||
| // 数据请求 | |||
| const refresh = async (queryObj) => { | |||
| if (typeof listRequest === 'function') { | |||
| if (typeof props.beforeListFn === 'function') { | |||
| const res = props.beforeListFn(); | |||
| if (res === false) return; | |||
| } | |||
| state.loading = true; | |||
| const { currentPage, pageSize } = pagination; | |||
| // 清除空的查询参数 | |||
| @@ -268,7 +279,6 @@ export default { | |||
| size: pageSize, | |||
| sort: sortInfo.sort || undefined, | |||
| order: sortInfo.order || undefined, | |||
| ...state.queryObj, | |||
| ...props.listOptions, | |||
| ...queryObj, | |||
| }).finally(() => { | |||
| @@ -281,6 +291,10 @@ export default { | |||
| } | |||
| setPagination(page); | |||
| state.data = result; | |||
| state.paginationVisible = true; | |||
| if (typeof props.afterListFn === 'function') { | |||
| props.afterListFn(result); | |||
| } | |||
| } | |||
| }; | |||
| // 数据查询 | |||
| @@ -319,6 +333,7 @@ export default { | |||
| const onSelectionChange = (selections) => { | |||
| state.selectedRows = selections; | |||
| }; | |||
| const pageShow = computed(() => props.showPagination && state.paginationVisible); | |||
| // 列定义预处理 | |||
| const mergedColumns = computed(() => { | |||
| @@ -326,7 +341,7 @@ export default { | |||
| // 为下拉表头绑定默认查询方法 | |||
| if (column.dropdownList && typeof column.func !== 'function') { | |||
| column.func = (value) => { | |||
| state.queryObj[column.prop] = value; | |||
| state.queryFormModel[column.prop] = value; | |||
| query(); | |||
| }; | |||
| } | |||
| @@ -384,8 +399,18 @@ export default { | |||
| refresh(); | |||
| }; | |||
| const tableLoading = computed(() => { | |||
| return state.loading && props.loadingType === 'table'; | |||
| }); | |||
| const headerLoading = computed(() => { | |||
| return state.loading && props.loadingType === 'header'; | |||
| }); | |||
| // 渲染后调用一次查询 | |||
| onMounted(query); | |||
| if (props.refreshImmediate) { | |||
| onMounted(query); | |||
| } | |||
| return { | |||
| state, | |||
| @@ -401,19 +426,24 @@ export default { | |||
| refresh, | |||
| query, | |||
| setQuery, | |||
| resetQuery, | |||
| setSort, | |||
| sortInfo, | |||
| onSizeChange, | |||
| pagination, | |||
| setPagination, | |||
| onPageChange, | |||
| onSortChange, | |||
| onSelectionChange, | |||
| pageShow, | |||
| mergedPageAttrs, | |||
| mergedColumns, | |||
| mergedFormItems, | |||
| slotLeft, | |||
| tableLoading, | |||
| headerLoading, | |||
| }; | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,43 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="el-statistic"> | |||
| <div v-if="title" class="el-statistic-title">{{ title }}</div> | |||
| <div class="el-statistic-content"> | |||
| <span class="el-statistic-content-value">{{ value }}</span> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'Statistic', | |||
| props: { | |||
| title: String, | |||
| value: [String, Number], | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .el-statistic-title { | |||
| margin-bottom: 4px; | |||
| color: rgba(0, 0, 0, 0.45); | |||
| font-size: 14px; | |||
| } | |||
| .el-statistic-content { | |||
| color: rgba(0, 0, 0, 0.85); | |||
| font-size: 24px; | |||
| } | |||
| .el-statistic-content-value { | |||
| display: inline-block; | |||
| direction: ltr; | |||
| } | |||
| </style> | |||
| @@ -1,18 +1,10 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <!--训练管理页面-保存模型Dialog--> | |||
| @@ -165,6 +157,7 @@ const defaultModelForm = { | |||
| const typeMap = { | |||
| training: 1, | |||
| optimize: 2, | |||
| tadl: 4, | |||
| }; | |||
| export default { | |||
| @@ -0,0 +1,133 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="yaml-editor"> | |||
| <textarea ref="textarea" /> | |||
| <div class="tips"> | |||
| <p v-if="readOnly">当前编辑器为只读状态</p> | |||
| <p>tips: 编辑代码时请注意代码格式. 比如缩进及没用的换行, 以免影响提交数据</p> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { onMounted, ref, watch } from '@vue/composition-api'; | |||
| import { Message } from 'element-ui'; | |||
| import CodeMirror from 'codemirror'; | |||
| import 'codemirror/addon/lint/lint.css'; | |||
| import 'codemirror/lib/codemirror.css'; | |||
| import 'codemirror/theme/monokai.css'; | |||
| import 'codemirror/mode/yaml/yaml'; | |||
| import 'codemirror/addon/lint/lint'; | |||
| import 'codemirror/addon/lint/yaml-lint'; | |||
| window.jsyaml = require('js-yaml'); | |||
| const yaml = require('js-yaml'); | |||
| export default { | |||
| name: 'YamlEditor', | |||
| props: { | |||
| value: { | |||
| type: String, | |||
| required: true, | |||
| }, | |||
| readOnly: { | |||
| type: Boolean, | |||
| default: false, | |||
| }, | |||
| }, | |||
| setup(props, ctx) { | |||
| const yamlEditor = ref(null); | |||
| const textarea = ref(null); | |||
| const getValue = () => { | |||
| return yamlEditor.value.getValue(); | |||
| }; | |||
| const setValue = () => { | |||
| yamlEditor.value.setOption('readOnly', props.readOnly); | |||
| yamlEditor.value.setValue(props.value); | |||
| }; | |||
| // 代码语法校验 | |||
| const codeValid = () => { | |||
| try { | |||
| yaml.load(getValue()); | |||
| return true; | |||
| } catch (e) { | |||
| Message.error(e.reason || '代码语法错误'); | |||
| return false; | |||
| } | |||
| }; | |||
| // 编辑器初始化 | |||
| const initYamlEditor = () => { | |||
| yamlEditor.value = CodeMirror.fromTextArea(textarea.value, { | |||
| lineNumbers: true, // 显示行号 | |||
| mode: 'text/x-yaml', // 语法model | |||
| gutters: ['CodeMirror-lint-markers'], // 语法检查器 | |||
| theme: 'monokai', // 编辑器主题 | |||
| lint: true, // 开启语法检查 | |||
| }); | |||
| setValue(); | |||
| yamlEditor.value.on('change', (cm) => { | |||
| ctx.emit('changed', cm.getValue()); | |||
| ctx.emit('input', cm.getValue()); | |||
| }); | |||
| yamlEditor.value.on('blur', (cm) => { | |||
| ctx.emit('blur', cm.getValue()); | |||
| }); | |||
| }; | |||
| watch( | |||
| () => props.value, | |||
| (next) => { | |||
| if (next !== getValue()) { | |||
| yamlEditor.value.setValue(props.value); | |||
| } | |||
| } | |||
| ); | |||
| onMounted(initYamlEditor); | |||
| return { | |||
| textarea, | |||
| yamlEditor, | |||
| getValue, | |||
| setValue, | |||
| codeValid, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| ::v-deep.yaml-editor { | |||
| height: 100%; | |||
| .CodeMirror { | |||
| height: 100%; | |||
| border-radius: 5px 5px 0 0; | |||
| } | |||
| } | |||
| .tips { | |||
| font-size: 14px; | |||
| color: rgb(179, 175, 175); | |||
| background: #272822; | |||
| border-radius: 0 0 5px 5px; | |||
| p { | |||
| margin: 0 10px; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -30,6 +30,7 @@ export const API_MODULE_NAME = { | |||
| ATLAS: 'measure', // 模型炼知 | |||
| K8S: 'k8s', // K8S | |||
| DCM: 'dcm', // 医学dcm | |||
| TADL: 'tadl', // TADL | |||
| DUBHE_PRO: 'terminal', // 天枢专业版 | |||
| }; | |||
| @@ -25,3 +25,4 @@ export * from './dict'; | |||
| export * from './localStorage'; | |||
| export * from './pagination'; | |||
| export * from './sort'; | |||
| export * from './keepPageInfo'; | |||
| @@ -0,0 +1,55 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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 { nextTick } from '@vue/composition-api'; | |||
| import { noop } from '@/utils'; | |||
| import store from '@/store'; | |||
| const assert = require('assert'); | |||
| /** | |||
| * 支持使用 VUEX 来存储分页、排序等信息 | |||
| * @param {String} pageInfoGetter 用于获取 store 中 pageInfo 的 getter 字符串 | |||
| * @param {String} updateAction 用于设置 store 中 pageInfo 的 action 字符串 | |||
| * @param {Function} pageInfoSetter 对获取到的分页数据进行设置应用 | |||
| * @param {Function} afterEnter 完成进入页面后调用 | |||
| */ | |||
| export function useKeepPageInfo({ | |||
| pageInfoGetter, | |||
| updateAction, | |||
| pageInfoSetter = noop, | |||
| afterEnter = noop, | |||
| } = {}) { | |||
| assert(pageInfoGetter, '必须传入对应的 getter 名'); | |||
| assert(updateAction, '必须传入对应的 action 名'); | |||
| const pageEnter = (keepPageInfos) => { | |||
| if (keepPageInfos) { | |||
| pageInfoSetter(store.getters[pageInfoGetter]); | |||
| } | |||
| nextTick(afterEnter); | |||
| }; | |||
| const updatePageInfo = (info) => { | |||
| store.dispatch(updateAction, info); | |||
| }; | |||
| return { | |||
| pageEnter, | |||
| updatePageInfo, | |||
| }; | |||
| } | |||
| @@ -26,7 +26,7 @@ import store from '@/store'; | |||
| export function useMapGetters(getters) { | |||
| const map = reactive({}); | |||
| for (const getter of getters) { | |||
| map[getter] = store.getters[getter]; | |||
| Object.assign(map, { [getter]: store.getters[getter] }); | |||
| } | |||
| return map; | |||
| } | |||
| @@ -1,18 +1,10 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <BaseLayout | |||
| @@ -54,6 +46,16 @@ export default { | |||
| border: 1px solid $borderColor; | |||
| } | |||
| .app-container.fullWidth { | |||
| padding-right: 0; | |||
| padding-left: 0; | |||
| .app-page-header { | |||
| padding-bottom: 0; | |||
| border: none; | |||
| } | |||
| } | |||
| .app-page-header-title { | |||
| margin-right: 12px; | |||
| margin-bottom: 0; | |||
| @@ -73,6 +75,29 @@ export default { | |||
| margin: 16px 0 0; | |||
| } | |||
| .app-page-contaniner-extra { | |||
| min-width: 300px; | |||
| margin-left: 88px; | |||
| text-align: right; | |||
| } | |||
| .app-page-header-footer { | |||
| margin-top: 20px; | |||
| .el-tabs__header { | |||
| margin-bottom: 0; | |||
| .el-tabs__nav-wrap::after { | |||
| width: 0; | |||
| } | |||
| } | |||
| } | |||
| .profile-advance { | |||
| display: flex; | |||
| justify-content: space-between; | |||
| } | |||
| .app-page-form-steps-desc { | |||
| padding: 0 56px; | |||
| color: rgba(0, 0, 0, 0.55); | |||
| @@ -16,8 +16,7 @@ | |||
| import { api_version, api_prefix } from '../../config'; | |||
| import { findMatchRule, isURL } from './util'; | |||
| // eslint-disable-next-line import/no-extraneous-dependencies | |||
| const url = require('url'); | |||
| const urljoin = require('url-join'); | |||
| const { VUE_APP_DATA_API, VUE_APP_VISUAL_API, VUE_APP_BASE_API } = process.env; | |||
| @@ -27,14 +26,14 @@ const fullPrefix = `${api_prefix}/${api_version}`; | |||
| const rules = [ | |||
| { | |||
| match: /^\/data/, | |||
| host: url.resolve(VUE_APP_DATA_API, fullPrefix), | |||
| host: urljoin(VUE_APP_DATA_API, fullPrefix), | |||
| }, | |||
| { | |||
| match: /^\/visual\/api/, | |||
| host: VUE_APP_VISUAL_API, | |||
| }, | |||
| { | |||
| host: url.resolve(VUE_APP_BASE_API, fullPrefix), | |||
| host: urljoin(VUE_APP_BASE_API, fullPrefix), | |||
| }, | |||
| ]; | |||
| @@ -0,0 +1,50 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| const state = { | |||
| experimentPageInfo: { | |||
| current: 1, | |||
| pageSize: 10, | |||
| sort: { sort: null, order: null }, | |||
| query: {}, | |||
| }, | |||
| }; | |||
| const mutations = { | |||
| UPDATE_EXPERIMENT_PAGE_INFO(state, pageInfo) { | |||
| state.experimentPageInfo = pageInfo; | |||
| }, | |||
| }; | |||
| const actions = { | |||
| updateExperimentPageInfo({ commit }, pageInfo) { | |||
| commit('UPDATE_EXPERIMENT_PAGE_INFO', pageInfo); | |||
| }, | |||
| }; | |||
| const getters = { | |||
| pageInfo() { | |||
| return state.experimentPageInfo; | |||
| }, | |||
| }; | |||
| export default { | |||
| namespaced: true, | |||
| state, | |||
| mutations, | |||
| actions, | |||
| getters, | |||
| }; | |||
| @@ -19,11 +19,10 @@ import Config from '@/settings'; | |||
| import { getToken } from '@/utils/auth'; | |||
| import store from '@/store/modules/Visual/layout'; | |||
| // eslint-disable-next-line import/no-extraneous-dependencies | |||
| const url = require('url'); | |||
| const urljoin = require('url-join'); | |||
| const service = axios.create({ | |||
| baseURL: url.resolve(process.env.VUE_APP_VISUAL_API, '/visual'), | |||
| baseURL: urljoin(process.env.VUE_APP_VISUAL_API, '/visual'), | |||
| timeout: Config.timeout, // 请求超时时间 | |||
| withCredentials: true, | |||
| }); | |||
| @@ -27,6 +27,7 @@ import { | |||
| values, | |||
| minBy, | |||
| maxBy, | |||
| isFunction, | |||
| } from 'lodash'; | |||
| import { nanoid } from 'nanoid'; | |||
| @@ -55,6 +56,24 @@ export function mergeProps(...args) { | |||
| return props; | |||
| } | |||
| // 判断参数是否为函数 | |||
| export const callOrValue = (maybeFn, ...data) => { | |||
| if (isFunction(maybeFn)) { | |||
| return maybeFn(...data); | |||
| } | |||
| return maybeFn; | |||
| }; | |||
| /** | |||
| * 解析对象,解析其中的函数,并传递参数进去 | |||
| */ | |||
| export const restProps = (rest, ...data) => { | |||
| return Object.keys(rest).reduce((ret, cur) => { | |||
| ret[cur] = callOrValue(rest[cur], ...data); | |||
| return ret; | |||
| }, {}); | |||
| }; | |||
| // 生成唯一 id | |||
| export const generateUuid = (count = 6) => nanoid(count); | |||
| @@ -164,6 +183,8 @@ export const leadingZero = (num, targetLength = 2, char = '0') => { | |||
| // scale 放大倍数,length: 保留小数点位数 | |||
| // 0.5122 => 51 | |||
| export const toFixed = (num, scale = 2, length = 2) => { | |||
| // eslint-disable-next-line no-restricted-globals | |||
| if (isNaN(num)) return 0; | |||
| // eslint-disable-next-line | |||
| return Math.floor(num * Math.pow(10, scale + length)) / Math.pow(10, length); | |||
| }; | |||
| @@ -71,6 +71,7 @@ export const RESOURCES_MODULE_ENUM = { | |||
| NOTEBOOK: 1, | |||
| TRAIN: 2, | |||
| SERVING: 3, | |||
| TADL: 4, | |||
| }; | |||
| // 资源类型名称 | |||
| @@ -116,7 +117,11 @@ export const defaultProcessColors = [ | |||
| // 系统管理员ID | |||
| export const ADMIN_ROLE_ID = 1; | |||
| // 时间常量 | |||
| // 时间常量(毫秒) | |||
| export const ONE_MINUTE = 1000 * 60; | |||
| export const ONE_HOUR = ONE_MINUTE * 60; | |||
| export const ONE_DAY = ONE_HOUR * 24; | |||
| export const ONE_WEEK = ONE_DAY * 7; | |||
| @@ -20,10 +20,10 @@ | |||
| import { nanoid } from 'nanoid'; | |||
| import { Message } from 'element-ui'; | |||
| import { isNil } from 'lodash'; | |||
| import Config from '@/settings'; | |||
| import FileFilter from '@/components/UploadForm/FileFilter'; | |||
| import { isNil } from 'lodash'; | |||
| /** | |||
| * Parse the time to string | |||
| @@ -552,6 +552,33 @@ export const runFunc = (func, ...args) => { | |||
| } | |||
| }; | |||
| /** | |||
| * 从单一数据源对象中获取值匹配的指定属性 | |||
| * @param {*} map 单一数据源对象,必须有 value 属性 | |||
| * @param {*} value 用于匹配的值 | |||
| * @param {*} key 指定属性名 | |||
| * @returns 匹配的指定属性值 | |||
| */ | |||
| export const getValueFromMap = (map, value, key) => { | |||
| const selectedObj = Object.values(map).find((obj) => obj.value === value); | |||
| if (isNil(selectedObj)) return selectedObj; | |||
| return key ? selectedObj[key] : selectedObj; | |||
| }; | |||
| /** | |||
| * 对象与对象之间相同属性赋值 | |||
| * @param {Object} target 目标对象 | |||
| * @param {Object} source 数据源 | |||
| * @param {Function} validate 用于自定义判断属性值所需条件 | |||
| */ | |||
| export function propertyAssign(target, source, validate = isNil) { | |||
| Object.keys(target).forEach((key) => { | |||
| if (validate(source[key])) { | |||
| target[key] = source[key]; | |||
| } | |||
| }); | |||
| } | |||
| // 格式化时长 | |||
| export function durationTrans(time) { | |||
| let duration = ''; | |||
| @@ -29,6 +29,8 @@ const isValidName = (value) => { | |||
| return /^[\u4E00-\u9FA5\w-]+$/.test(value) && value.length <= 50; | |||
| }; | |||
| const isValidInteger = (value) => /^[1-9]\d*$/.test(value); | |||
| const isValidNameWithHyphen = (value) => { | |||
| return /^[\u4E00-\u9FA5A-Za-z0-9_-]+$/.test(value); | |||
| }; | |||
| @@ -85,6 +87,13 @@ extend('validateLesionId', { | |||
| }, | |||
| }); | |||
| extend('validInteger', { | |||
| validate: isValidInteger, | |||
| message: (_, params) => { | |||
| return `${params._field_}只支持正整数`; | |||
| }, | |||
| }); | |||
| export { ValidationProvider, ValidationObserver }; | |||
| /** | |||
| @@ -1,152 +1,154 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <BaseModal | |||
| :key="state.formKey" | |||
| title="创建数据集" | |||
| width="630px" | |||
| :okText="okText" | |||
| :loading="state.loading" | |||
| :visible="state.visible" | |||
| @change="handleClose" | |||
| @ok="handleOk" | |||
| > | |||
| <el-form ref="formRef" :model="state.form" :rules="rules" label-width="100px"> | |||
| <el-form-item label="数据集名称" prop="name"> | |||
| <el-input v-model="state.form.name" placeholder="数据集名称不能超过50字" maxlength="50" /> | |||
| </el-form-item> | |||
| <el-form-item label="数据类型" prop="dataType"> | |||
| <InfoRadio | |||
| v-model="state.form.dataType" | |||
| :dataSource="dataTypeList" | |||
| :transformOptions="transformOptions" | |||
| type="button" | |||
| @change="handleDataTypeChange" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="模型类型" prop="annotateType"> | |||
| <div | |||
| v-if="state.form.dataType !== dataTypeCodeMap.CUSTOM" | |||
| class="image-select flex flex-wrap" | |||
| > | |||
| <el-radio-group v-model="state.datasetRadio" class="my-20 pl-16" @change="onDatasetRadioChange"> | |||
| <el-radio :label="0">新建数据集</el-radio> | |||
| <el-radio :label="1">导入已有数据集</el-radio> | |||
| </el-radio-group> | |||
| <template v-if="!Boolean(state.datasetRadio)"> | |||
| <el-form ref="formRef" :model="state.form" :rules="rules" label-width="100px"> | |||
| <el-form-item label="数据集名称" prop="name"> | |||
| <el-input v-model="state.form.name" placeholder="数据集名称不能超过50字" maxlength="50" /> | |||
| </el-form-item> | |||
| <el-form-item label="数据类型" prop="dataType"> | |||
| <InfoRadio | |||
| v-model="state.form.dataType" | |||
| :dataSource="dataTypeList" | |||
| :transformOptions="transformOptions" | |||
| type="button" | |||
| @change="handleDataTypeChange" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="模型类型" prop="annotateType"> | |||
| <div | |||
| v-for="item in annotationList" | |||
| :key="item.code" | |||
| :class="getImageKlass(item)" | |||
| @click="selectAnnotationType(item)" | |||
| v-if="state.form.dataType !== dataTypeCodeMap.CUSTOM" | |||
| class="image-select flex flex-wrap" | |||
| > | |||
| <div class="image-title">{{ item.name }}</div> | |||
| <img class="pic responsive" :src="getImgUrl(item)" /> | |||
| <span> | |||
| <i class="check-icon" /> | |||
| </span> | |||
| <div | |||
| v-for="item in annotationList" | |||
| :key="item.code" | |||
| :class="getImageKlass(item)" | |||
| @click="selectAnnotationType(item)" | |||
| > | |||
| <div class="image-title">{{ item.name }}</div> | |||
| <img class="pic responsive" :src="getImgUrl(item)" /> | |||
| <span> | |||
| <i class="check-icon" /> | |||
| </span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div v-else> | |||
| <el-select | |||
| v-model="state.customAnnotationType" | |||
| @change="(val) => selectCustomAnnotationType(val)" | |||
| <div v-else> | |||
| <el-select | |||
| v-model="state.customAnnotationType" | |||
| @change="(val) => selectCustomAnnotationType(val)" | |||
| > | |||
| <el-option | |||
| v-for="item in allAnnotationList" | |||
| :key="item.value" | |||
| :label="item.label" | |||
| :value="item.value" | |||
| /> | |||
| </el-select> | |||
| </div> | |||
| <el-input :value="state.form.annotateType" class="dn" /> | |||
| </el-form-item> | |||
| <div style="position: relative; top: -10px; margin-left: 100px;"> | |||
| 更多标注类型说明参考<a | |||
| target="_blank" | |||
| type="primary" | |||
| :underline="false" | |||
| class="primary" | |||
| :href="`${VUE_APP_DOCS_URL}module/dataset/intro`" | |||
| >官方文档</a | |||
| > | |||
| <el-option | |||
| v-for="item in allAnnotationList" | |||
| :key="item.value" | |||
| :label="item.label" | |||
| :value="item.value" | |||
| /> | |||
| </el-select> | |||
| </div> | |||
| <el-input :value="state.form.annotateType" class="dn" /> | |||
| </el-form-item> | |||
| <div style="position: relative; top: -10px; margin-left: 100px;"> | |||
| 更多标注类型说明参考<a | |||
| target="_blank" | |||
| type="primary" | |||
| :underline="false" | |||
| class="primary" | |||
| :href="`${VUE_APP_DOCS_URL}module/dataset/intro`" | |||
| >官方文档</a | |||
| > | |||
| </div> | |||
| <el-form-item v-if="showlabelGroup" label="标签组" style="height: 32px;" prop="labelGroup"> | |||
| <el-cascader | |||
| v-model="state.form.labelGroup" | |||
| clearable | |||
| placeholder="标签组" | |||
| :options="labelGroupOptions" | |||
| :props="{ expandTrigger: 'hover' }" | |||
| :show-all-levels="false" | |||
| filterable | |||
| popper-class="group-cascader" | |||
| style="width: 100%; line-height: 32px;" | |||
| @change="handleGroupChange" | |||
| > | |||
| <div slot="empty"> | |||
| <span>没有找到标签组?去</span> | |||
| <a | |||
| <el-form-item v-if="showlabelGroup" label="标签组" style="height: 32px;" prop="labelGroup"> | |||
| <el-cascader | |||
| v-model="state.form.labelGroup" | |||
| clearable | |||
| placeholder="标签组" | |||
| :options="labelGroupOptions" | |||
| :props="{ expandTrigger: 'hover' }" | |||
| :show-all-levels="false" | |||
| filterable | |||
| popper-class="group-cascader" | |||
| style="width: 100%; line-height: 32px;" | |||
| @change="handleGroupChange" | |||
| > | |||
| <div slot="empty"> | |||
| <span>没有找到标签组?去</span> | |||
| <a | |||
| target="_blank" | |||
| type="primary" | |||
| :underline="false" | |||
| class="primary" | |||
| :href="`/data/labelgroup/create`" | |||
| > | |||
| 新建标签组 | |||
| </a> | |||
| <span>页面创建</span> | |||
| </div> | |||
| </el-cascader> | |||
| <div style="position: relative; top: -33px; right: 30px; float: right;"> | |||
| <el-link | |||
| v-if="state.form.labelGroupId !== null" | |||
| target="_blank" | |||
| type="primary" | |||
| :underline="false" | |||
| class="primary" | |||
| :href="`/data/labelgroup/create`" | |||
| class="vm" | |||
| :href="`/data/labelgroup/detail?id=${state.form.labelGroupId}`" | |||
| > | |||
| 新建标签组 | |||
| </a> | |||
| <span>页面创建</span> | |||
| 查看详情 | |||
| </el-link> | |||
| </div> | |||
| </el-cascader> | |||
| <div style="position: relative; top: -33px; right: 30px; float: right;"> | |||
| <el-link | |||
| v-if="state.form.labelGroupId !== null" | |||
| </el-form-item> | |||
| <div | |||
| v-if="state.form.labelGroupId === null && showlabelGroup" | |||
| style="position: relative; top: -10px; margin-left: 100px;" | |||
| > | |||
| <span>标签组需要在</span> | |||
| <a | |||
| target="_blank" | |||
| type="primary" | |||
| :underline="false" | |||
| class="vm" | |||
| :href="`/data/labelgroup/detail?id=${state.form.labelGroupId}`" | |||
| class="primary" | |||
| :href="`/data/labelgroup/create`" | |||
| > | |||
| 查看详情 | |||
| </el-link> | |||
| 新建标签组 | |||
| </a> | |||
| <span>页面创建</span> | |||
| </div> | |||
| </el-form-item> | |||
| <div | |||
| v-if="state.form.labelGroupId === null && showlabelGroup" | |||
| style="position: relative; top: -10px; margin-left: 100px;" | |||
| > | |||
| <span>标签组需要在</span> | |||
| <a | |||
| target="_blank" | |||
| type="primary" | |||
| :underline="false" | |||
| class="primary" | |||
| :href="`/data/labelgroup/create`" | |||
| > | |||
| 新建标签组 | |||
| </a> | |||
| <span>页面创建</span> | |||
| </div> | |||
| <el-form-item label="数据集描述"> | |||
| <el-input | |||
| v-model="state.form.remark" | |||
| type="textarea" | |||
| placeholder="数据集描述长度不能超过100字" | |||
| maxlength="100" | |||
| rows="3" | |||
| show-word-limit | |||
| /> | |||
| </el-form-item> | |||
| </el-form> | |||
| <el-form-item label="数据集描述"> | |||
| <el-input | |||
| v-model="state.form.remark" | |||
| type="textarea" | |||
| placeholder="数据集描述长度不能超过100字" | |||
| maxlength="100" | |||
| rows="3" | |||
| show-word-limit | |||
| /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </template> | |||
| <template v-else> | |||
| <ImportDataset /> | |||
| </template> | |||
| </BaseModal> | |||
| </template> | |||
| <script> | |||
| @@ -170,6 +172,7 @@ import { validateName } from '@/utils/validate'; | |||
| import { getLabelGroupList } from '@/api/preparation/labelGroup'; | |||
| import { add } from '@/api/preparation/dataset'; | |||
| import ImportDataset from './import-dataset'; | |||
| const annotationByDataType = annotationBy('dataType'); | |||
| @@ -192,6 +195,7 @@ export default { | |||
| components: { | |||
| BaseModal, | |||
| InfoRadio, | |||
| ImportDataset, | |||
| }, | |||
| props: { | |||
| visible: Boolean, | |||
| @@ -232,6 +236,7 @@ export default { | |||
| visible: props.visible, | |||
| loading: false, // 数据集创建进行中 | |||
| customAnnotationType: null, | |||
| datasetRadio: 0, | |||
| }); | |||
| const labelGroupOptions = ref(initialLabelGroupOptions); | |||
| @@ -255,6 +260,7 @@ export default { | |||
| () => | |||
| enableLabelGroup(state.form.annotateType) && state.form.dataType !== dataTypeCodeMap.CUSTOM | |||
| ); | |||
| const okText = computed(() => (state.datasetRadio ? '知道了' : '确定')); | |||
| const setForm = (params) => | |||
| Object.assign(state, { | |||
| @@ -388,12 +394,13 @@ export default { | |||
| type: 0, | |||
| }, | |||
| loading: false, | |||
| datasetRadio: 0, | |||
| }); | |||
| toggleVisible(); | |||
| onResetFresh(); | |||
| }; | |||
| const handleOk = () => { | |||
| const onSubmitDataset = () => { | |||
| formRef.value.validate((valid) => { | |||
| if (!valid) return; | |||
| const params = omit(state.form, ['labelGroup']); | |||
| @@ -411,6 +418,14 @@ export default { | |||
| }); | |||
| }; | |||
| const handleOk = () => { | |||
| state.datasetRadio ? toggleVisible() : onSubmitDataset(); | |||
| }; | |||
| const onDatasetRadioChange = () => { | |||
| resetForm(); | |||
| }; | |||
| watch( | |||
| () => props.visible, | |||
| (next) => { | |||
| @@ -445,7 +460,9 @@ export default { | |||
| allAnnotationList, | |||
| handleClose, | |||
| handleOk, | |||
| okText, | |||
| rules, | |||
| onDatasetRadioChange, | |||
| showlabelGroup, | |||
| }; | |||
| }, | |||
| @@ -1,341 +1,77 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <BaseModal | |||
| :key="state.formKey" | |||
| title="导入本地数据集" | |||
| width="600px" | |||
| center | |||
| :loading="state.loading" | |||
| :visible="state.visible" | |||
| @change="handleClose" | |||
| @ok="handleOk" | |||
| > | |||
| <el-form ref="formRef" :model="state.form" :rules="rules" label-width="100px"> | |||
| <el-alert class="info-alert" type="warning" show-icon :closable="false"> | |||
| <div slot="title" class="slot-content"> | |||
| <div>数据集创建完毕后,需要使用脚本工具上传本地已有数据集</div> | |||
| <a :href="`${VUE_APP_DOCS_URL}module/dataset/util`" target="_blank">使用文档</a> | |||
| </div> | |||
| </el-alert> | |||
| <el-form-item label="数据集名称" prop="name"> | |||
| <el-input v-model="state.form.name" placeholder="数据集名称不能超过50字" maxlength="50" /> | |||
| </el-form-item> | |||
| <el-form-item label="数据集来源" prop="sourceType"> | |||
| <InfoRadio v-model="state.form.sourceType" :dataSource="sourceTypeList" /> | |||
| <div> | |||
| 标准数据集是指天枢平台预置支持的部分数据集类型, | |||
| <a | |||
| target="_blank" | |||
| type="primary" | |||
| :underline="false" | |||
| class="primary" | |||
| :href="`${VUE_APP_DOCS_URL}module/dataset/intro`" | |||
| >详细参考</a | |||
| > | |||
| </div> | |||
| </el-form-item> | |||
| <el-form-item v-if="!sourceByCustom" label="数据类型" prop="dataType"> | |||
| <InfoRadio | |||
| v-model="state.form.dataType" | |||
| :dataSource="dataTypeList" | |||
| :transformOptions="transformOptions" | |||
| type="button" | |||
| @change="handleDataTypeChange" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item v-if="!sourceByCustom" label="标注类型" prop="annotateType"> | |||
| <InfoSelect | |||
| v-model="state.form.annotateType" | |||
| placeholder="标注类型" | |||
| :dataSource="annotationList" | |||
| width="200px" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item v-else label="模型类型" prop="annotateType"> | |||
| <InfoSelect | |||
| v-model="state.form.annotateType" | |||
| placeholder="模型类型" | |||
| :dataSource="allAnnotationList" | |||
| width="200px" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="数据集描述"> | |||
| <el-input | |||
| v-model="state.form.remark" | |||
| type="textarea" | |||
| placeholder="数据集描述长度不能超过100字" | |||
| maxlength="100" | |||
| rows="3" | |||
| show-word-limit | |||
| /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </BaseModal> | |||
| <div> | |||
| <div class="flex flex-between bg"> | |||
| <span> | |||
| <i class="el-icon-warning" style="color: #3253d6;" /> | |||
| 天枢命令行工具支持导入本地已有自定义数据集、标准数据集 | |||
| </span> | |||
| <a | |||
| class="primary" | |||
| href="http://docs.tianshu.org.cn/docs/module/dataset/cli/new" | |||
| target="_blank" | |||
| > | |||
| 使用文档 | |||
| </a> | |||
| </div> | |||
| <div v-for="item in datasetCode" :key="item.id"> | |||
| <span class="db mb-10 mt-20">{{ item.text }}</span> | |||
| <pre class="code flex flex-vertical-align flex-between"> | |||
| <code class="text ellipsis">{{item.code}}</code> | |||
| <copy-to-clipboard :text="item.code" @copy="handleCopy"> | |||
| <i class="el-icon-copy-document pointer copy" /> | |||
| </copy-to-clipboard> | |||
| </pre> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { reactive, watch, ref, computed } from '@vue/composition-api'; | |||
| import { Message } from 'element-ui'; | |||
| import { omit } from 'lodash'; | |||
| import BaseModal from '@/components/BaseModal'; | |||
| import InfoRadio from '@/components/InfoRadio'; | |||
| import InfoSelect from '@/components/InfoSelect'; | |||
| import { validateName } from '@/utils/validate'; | |||
| import { | |||
| annotationBy, | |||
| dataTypeMap, | |||
| dataTypeCodeMap, | |||
| annotationCodeMap, | |||
| annotationMap, | |||
| transformMapToList, | |||
| } from '@/views/dataset/util'; | |||
| import { add } from '@/api/preparation/dataset'; | |||
| const annotationByDataType = annotationBy('dataType'); | |||
| import CopyToClipboard from 'vue-copy-to-clipboard'; | |||
| import { datasetCode } from '../util'; | |||
| export default { | |||
| name: 'ImportDataset', | |||
| components: { | |||
| BaseModal, | |||
| InfoRadio, | |||
| InfoSelect, | |||
| CopyToClipboard, | |||
| }, | |||
| props: { | |||
| visible: { | |||
| type: Boolean, | |||
| default: false, | |||
| }, | |||
| toggleVisible: { | |||
| type: Function, | |||
| }, | |||
| onResetFresh: { | |||
| type: Function, | |||
| }, | |||
| }, | |||
| setup(props) { | |||
| const { toggleVisible, onResetFresh } = props; | |||
| const initialForm = { | |||
| name: '', | |||
| dataType: 0, | |||
| annotateType: null, | |||
| remark: '', | |||
| loading: false, | |||
| sourceType: 0, | |||
| }; | |||
| const formRef = ref(null); | |||
| // 标准数据集白名单:图像分类、目标检测、语义分割 | |||
| // 文本分类 | |||
| // 音频分类 | |||
| const stdAnnotateType = [ | |||
| annotationCodeMap.ANNOTATE, | |||
| annotationCodeMap.CLASSIFY, | |||
| annotationCodeMap.SEGMENTATION, | |||
| annotationCodeMap.TEXTCLASSIFY, | |||
| annotationCodeMap.AUDIOCLASSIFY, | |||
| ]; | |||
| const rules = { | |||
| name: [ | |||
| { | |||
| required: true, | |||
| message: '请输入数据集名称', | |||
| trigger: ['change', 'blur'], | |||
| }, | |||
| { validator: validateName, trigger: ['change', 'blur'] }, | |||
| ], | |||
| sourceType: [{ required: true, message: '请选择数据集来源', trigger: 'change' }], | |||
| dataType: [{ required: true, message: '请选择数据类型', trigger: 'change' }], | |||
| annotateType: [{ required: true, message: '请选择模型类型', trigger: ['change', 'blur'] }], | |||
| }; | |||
| const state = reactive({ | |||
| form: initialForm, | |||
| formKey: 1, | |||
| visible: props.visible, | |||
| loading: false, // 数据集创建进行中 | |||
| }); | |||
| const sourceTypeList = [ | |||
| { | |||
| label: '自定义数据集', | |||
| value: 0, | |||
| }, | |||
| { | |||
| label: '标准数据集', | |||
| value: 1, | |||
| }, | |||
| ]; | |||
| // 是否为自定义来源 | |||
| const sourceByCustom = computed(() => state.form.sourceType === 0); | |||
| const dataTypeList = computed(() => { | |||
| const transformed = transformMapToList( | |||
| omit(dataTypeMap, [dataTypeCodeMap.TABLE, dataTypeCodeMap.CUSTOM, dataTypeCodeMap.VIDEO]) | |||
| ); | |||
| return transformed.map((d) => ({ | |||
| ...d, | |||
| value: Number(d.value), | |||
| })); | |||
| }); | |||
| const annotationList = computed(() => | |||
| annotationByDataType(state.form.dataType) | |||
| .filter((d) => stdAnnotateType.includes(d.code)) | |||
| .map((d) => ({ | |||
| value: d.code, | |||
| label: d.name, | |||
| })) | |||
| ); | |||
| const allAnnotationList = computed(() => { | |||
| return Object.keys(annotationMap).map((d) => ({ | |||
| label: annotationMap[d].name, | |||
| value: annotationMap[d].code, | |||
| code: annotationMap[d].code, | |||
| })); | |||
| }); | |||
| const setForm = (params) => | |||
| Object.assign(state, { | |||
| form: { | |||
| ...state.form, | |||
| ...params, | |||
| }, | |||
| }); | |||
| // 更新加载状态 | |||
| const setLoading = (loading) => Object.assign(state, { loading }); | |||
| // 重置状态(reactive mutate 原始对象) | |||
| const resetForm = () => | |||
| Object.assign(state, { | |||
| form: { | |||
| name: '', | |||
| dataType: 0, | |||
| sourceType: 0, | |||
| annotateType: 2, | |||
| remark: '', | |||
| }, | |||
| }); | |||
| const handleDataTypeChange = () => { | |||
| // 默认定位到第一个标注场景 | |||
| if (annotationList.value.length) { | |||
| setForm({ | |||
| annotateType: annotationList.value[0].value, | |||
| }); | |||
| } | |||
| }; | |||
| const selectAnnotationType = (item) => { | |||
| if (item.code === Number(state.form.annotateType)) return; | |||
| setForm({ | |||
| annotateType: item.code, | |||
| }); | |||
| setup() { | |||
| const handleCopy = () => { | |||
| Message.success('复制成功'); | |||
| }; | |||
| const handleClose = () => { | |||
| Object.assign(state, { | |||
| formKey: state.formKey + 1, | |||
| // reactive mutate 原始对象 | |||
| form: { | |||
| name: '', | |||
| dataType: 0, | |||
| sourceType: 0, | |||
| annotateType: 2, | |||
| remark: '', | |||
| }, | |||
| loading: false, | |||
| }); | |||
| toggleVisible(false); | |||
| onResetFresh(); | |||
| }; | |||
| const handleOk = () => { | |||
| formRef.value.validate((valid) => { | |||
| if (!valid) return; | |||
| const params = { | |||
| type: 0, | |||
| import: true, | |||
| name: state.form.name, | |||
| remark: state.form.remark, | |||
| annotateType: state.form.annotateType, | |||
| }; | |||
| // 区分自定义数据集、标注数据集 | |||
| state.form.sourceType === 0 | |||
| ? Object.assign(params, { | |||
| dataType: dataTypeCodeMap.CUSTOM, | |||
| }) | |||
| : Object.assign(params, { | |||
| dataType: state.form.dataType, | |||
| }); | |||
| setLoading(true); | |||
| add(params) | |||
| .then(() => { | |||
| Message.success('数据集创建成功,请下载数据集脚本工具进行下一步操作'); | |||
| resetForm(); | |||
| toggleVisible(false); | |||
| }) | |||
| .finally(() => { | |||
| setLoading(false); | |||
| }); | |||
| }); | |||
| }; | |||
| const transformOptions = (list) => { | |||
| return list.map((d) => ({ | |||
| ...d, | |||
| label: d.label, | |||
| value: Number(d.value), | |||
| })); | |||
| }; | |||
| watch( | |||
| () => props.visible, | |||
| (next) => { | |||
| Object.assign(state, { | |||
| visible: next, | |||
| }); | |||
| } | |||
| ); | |||
| return { | |||
| VUE_APP_DOCS_URL: process.env.VUE_APP_DOCS_URL, | |||
| rules, | |||
| state, | |||
| formRef, | |||
| sourceTypeList, | |||
| sourceByCustom, | |||
| dataTypeList, | |||
| annotationList, | |||
| allAnnotationList, | |||
| transformOptions, | |||
| handleDataTypeChange, | |||
| selectAnnotationType, | |||
| handleClose, | |||
| handleOk, | |||
| datasetCode, | |||
| handleCopy, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| @import '@/assets/styles/variables.scss'; | |||
| .bg { | |||
| padding: 10px 20px; | |||
| background: #eef8ff; | |||
| } | |||
| .code { | |||
| height: 40px; | |||
| padding: 0 20px; | |||
| background: #ebedf0; | |||
| } | |||
| .copy { | |||
| font-size: 18px; | |||
| color: $primaryColor; | |||
| } | |||
| </style> | |||
| @@ -1,18 +1,10 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="app-container"> | |||
| @@ -29,15 +21,6 @@ | |||
| > | |||
| 创建数据集 | |||
| </el-button> | |||
| <el-button | |||
| slot="left" | |||
| class="filter-item" | |||
| icon="el-icon-upload" | |||
| round | |||
| @click="toggleImportDatasetEvent" | |||
| > | |||
| 导入数据集 | |||
| </el-button> | |||
| <span slot="right"> | |||
| <!-- 搜索 --> | |||
| <el-input | |||
| @@ -69,12 +52,6 @@ | |||
| :toggleVisible="closeCreateDatasetForm" | |||
| :onResetFresh="onResetFresh" | |||
| /> | |||
| <!--导入自定义数据集表单组件--> | |||
| <ImportDataset | |||
| :visible="importDatasetVisible" | |||
| :toggleVisible="toggleImportDataset" | |||
| :onResetFresh="onResetFresh" | |||
| /> | |||
| <!--单独导入数据表单组件--> | |||
| <UploadDataFile | |||
| :row="importRow" | |||
| @@ -312,6 +289,16 @@ import { isNil, omit, findKey } from 'lodash'; | |||
| import { mapState } from 'vuex'; | |||
| import CopyToClipboard from 'vue-copy-to-clipboard'; | |||
| import CRUD, { presenter, header, form, crud } from '@crud/crud'; | |||
| import rrOperation from '@crud/RR.operation'; | |||
| import cdOperation from '@crud/CD.operation'; | |||
| import { | |||
| publish, | |||
| autoAnnotate, | |||
| annotateStatus, | |||
| delAnnotation, | |||
| track, | |||
| } from '@/api/preparation/annotation'; | |||
| import crudDataset, { | |||
| editDataset, | |||
| detail, | |||
| @@ -320,16 +307,6 @@ import crudDataset, { | |||
| queryDatasetsProgress, | |||
| queryDatasetStatus, | |||
| } from '@/api/preparation/dataset'; | |||
| import { | |||
| publish, | |||
| autoAnnotate, | |||
| annotateStatus, | |||
| delAnnotation, | |||
| track, | |||
| } from '@/api/preparation/annotation'; | |||
| import CRUD, { presenter, header, form, crud } from '@crud/crud'; | |||
| import rrOperation from '@crud/RR.operation'; | |||
| import cdOperation from '@crud/CD.operation'; | |||
| import datePickerMixin from '@/mixins/datePickerMixin'; | |||
| import { | |||
| @@ -361,7 +338,6 @@ import CreateDataset from './create-dataset'; | |||
| import Status from './status'; | |||
| import Action from './action'; | |||
| import Publish from './publish'; | |||
| import ImportDataset from './import-dataset'; | |||
| import DataEnhance from './data-enhance'; | |||
| import EditDataset from './edit-dataset'; | |||
| import UploadDataFile from './upload-datafile'; | |||
| @@ -396,7 +372,6 @@ export default { | |||
| BaseModal, | |||
| CreateDataset, | |||
| Publish, | |||
| ImportDataset, | |||
| TenantSelector, | |||
| Status, | |||
| Action, | |||
| @@ -434,7 +409,6 @@ export default { | |||
| chosenDatasetStatus: 0, | |||
| createDatasetVisible: false, // 创建数据集对话框 | |||
| uploadDataFileVisible: false, // 单独导入数据文件的对话框 | |||
| importDatasetVisible: false, // 导入自定义数据集对话框 | |||
| enhanceKey: 1000, | |||
| editKey: 1, | |||
| currentRow: null, | |||
| @@ -685,13 +659,6 @@ export default { | |||
| closeCreateDatasetForm() { | |||
| this.createDatasetVisible = false; | |||
| }, | |||
| // 导入自定义数据集表单显隐切换 | |||
| toggleImportDataset(visible) { | |||
| this.importDatasetVisible = isNil(visible) ? !this.importDatasetVisible : visible; | |||
| }, | |||
| toggleImportDatasetEvent() { | |||
| return this.toggleImportDataset(); | |||
| }, | |||
| handleCopy(text, result, row) { | |||
| this.$set(row, 'copySuccess', false); | |||
| Object.assign(row, { | |||
| @@ -1,18 +1,10 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-dialog | |||
| @@ -24,6 +16,15 @@ | |||
| :title="state.title" | |||
| @close="handleClose" | |||
| > | |||
| <div class="flex flex-between py-10 px-20 mb-20" style="background: #eef8ff;"> | |||
| <span> | |||
| <i class="el-icon-warning" style="color: #3253d6;" /> | |||
| 单次上传大量文件(2000+)建议下载安装天枢命令行工具 | |||
| </span> | |||
| <a class="primary" href="http://docs.tianshu.org.cn/docs/module/dataset/cli" target="_blank"> | |||
| 使用文档 | |||
| </a> | |||
| </div> | |||
| <!--选择上传的文件--> | |||
| <div v-show="state.uploadStep === 0"> | |||
| <upload-inline | |||
| @@ -104,6 +105,7 @@ | |||
| import { last } from 'lodash'; | |||
| import { reactive, watch, computed, nextTick } from '@vue/composition-api'; | |||
| import { Message } from 'element-ui'; | |||
| import { toFixed } from '@/utils'; | |||
| import UploadInline from '@/components/UploadForm/inline'; | |||
| import { | |||
| @@ -113,7 +115,6 @@ import { | |||
| dataTypeCodeMap, | |||
| } from '@/views/dataset/util'; | |||
| import { submit, submitVideo } from '@/api/preparation/datafile'; | |||
| import { Message } from 'element-ui'; | |||
| // 每次最多上传的文件数量 | |||
| const MAX_FILE_COUNT = 200; | |||
| @@ -14,8 +14,8 @@ | |||
| * ============================================================= | |||
| */ | |||
| import { parseBbox, flatBbox, generateUuid, pos2Array, rawArr2Pos } from '@/utils'; | |||
| import { isNil, pick } from 'lodash'; | |||
| import { parseBbox, flatBbox, generateUuid, pos2Array, rawArr2Pos } from '@/utils'; | |||
| import { bucketName, bucketHost } from '@/utils/minIO'; | |||
| const assert = require('assert'); | |||
| @@ -581,3 +581,17 @@ export const getIcon = (ext) => { | |||
| const reg = /^(mp4|avi|mkv|mov|wmv|bmp|jpeg|jpg|png|txt|zip|dir|mp3)$/; | |||
| return reg.test(ext) ? ext : 'file'; | |||
| }; | |||
| // 导入数据集脚本 | |||
| export const datasetCode = [ | |||
| { | |||
| id: 0, | |||
| text: '导入自定义数据集', | |||
| code: 'ts-cli dataset import --type=custom --source /Users/myDataset --annotation_type=custom', | |||
| }, | |||
| { | |||
| id: 1, | |||
| text: '导入标准数据集', | |||
| code: 'ts-cli dataset import --type=ImageClassify --source /Users/myDataset', | |||
| }, | |||
| ]; | |||
| @@ -1,18 +1,9 @@ | |||
| /* | |||
| * Copyright 2019-2020 Zheng Jie | |||
| * | |||
| * 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. | |||
| */ | |||
| /* * Copyright 2019-2020 Zheng Jie * * 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. */ | |||
| <template> | |||
| <div class="app-container"> | |||
| @@ -242,14 +233,14 @@ | |||
| <script> | |||
| import Treeselect from '@riophae/vue-treeselect'; | |||
| import Editor from '@/components/editor'; | |||
| import { validateName, validateString, validateJSON, hasPermission } from '@/utils'; | |||
| import crudMenu, { getMenusTree } from '@/api/system/menu'; | |||
| import { iconList } from '@/components/IconFont/iconfont'; | |||
| import CRUD, { presenter, header, form, crud } from '@crud/crud'; | |||
| import rrOperation from '@crud/RR.operation'; | |||
| import cdOperation from '@crud/CD.operation'; | |||
| import udOperation from '@crud/UD.operation'; | |||
| import Editor from '@/components/editor'; | |||
| import { validateName, validateString, validateJSON, hasPermission } from '@/utils'; | |||
| import crudMenu, { getMenusTree } from '@/api/system/menu'; | |||
| import { iconList } from '@/components/IconFont/iconfont'; | |||
| import datePickerMixin from '@/mixins/datePickerMixin'; | |||
| import BaseModal from '@/components/BaseModal'; | |||
| @@ -271,7 +262,14 @@ const defaultForm = { | |||
| hidden: false, | |||
| type: 0, | |||
| permission: null, | |||
| extConfig: '{}', | |||
| extConfig: '', | |||
| }; | |||
| const validateExtConfig = (rule, value, callback) => { | |||
| if (value === '') callback(); | |||
| else { | |||
| validateJSON(rule, value, callback); | |||
| } | |||
| }; | |||
| export default { | |||
| @@ -315,7 +313,7 @@ export default { | |||
| ], | |||
| pid: [{ required: true, message: '请选择上级菜单', trigger: 'blur' }], | |||
| layout: [{ required: true, message: '请选择页面布局', trigger: 'blur' }], | |||
| extConfig: [{ validator: validateJSON, trigger: 'change' }], | |||
| extConfig: [{ validator: validateExtConfig, trigger: 'change' }], | |||
| }, | |||
| }; | |||
| }, | |||
| @@ -19,6 +19,7 @@ export const moduleMap = { | |||
| 1: 'notebook', | |||
| 2: 'train', | |||
| 3: 'serving', | |||
| 4: 'tadl', | |||
| }; | |||
| const resourcesPoolTypeMap = { | |||
| @@ -0,0 +1,34 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <component :is="type" v-bind="chartConfig" :data="chartData" /> | |||
| </template> | |||
| <script> | |||
| import { LineChart, ColumnChart, ScatterChart } from '@opd/g2plot-vue'; | |||
| export default { | |||
| name: 'Chart', | |||
| components: { | |||
| LineChart, | |||
| ColumnChart, | |||
| ScatterChart, | |||
| }, | |||
| props: { | |||
| type: String, | |||
| chartConfig: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| chartData: { | |||
| type: Array, | |||
| default: () => [], | |||
| }, | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,34 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-card> | |||
| <div slot="header"> | |||
| <span>{{ title }}</span> | |||
| <slot name="action" /> | |||
| </div> | |||
| <Chart :type="type" :chartData="chartData" :chartConfig="chartConfig" /> | |||
| <slot name="footer" /> | |||
| </el-card> | |||
| </template> | |||
| <script> | |||
| import Chart from './chart'; | |||
| export default { | |||
| name: 'ChartCard', | |||
| components: { | |||
| Chart, | |||
| }, | |||
| props: { | |||
| title: String, | |||
| type: String, | |||
| chartData: Array, | |||
| chartConfig: Object, | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,59 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="experiment-config"> | |||
| <div> | |||
| <el-button class="left-round-button" @click="toggleSearchSpace">Search Space</el-button> | |||
| </div> | |||
| <div> | |||
| <el-button class="left-round-button" @click="toggleSelectedSpace"> | |||
| Best Selected Space | |||
| </el-button> | |||
| </div> | |||
| <!-- TODO --> | |||
| <!-- <div> | |||
| <el-button class="left-round-button" @click="toggleExpConfig">Experiment Config</el-button> | |||
| </div> --> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| name: 'Config', | |||
| props: { | |||
| toggleSearchSpace: { | |||
| type: Function, | |||
| default: () => ({}), | |||
| }, | |||
| toggleSelectedSpace: { | |||
| type: Function, | |||
| default: () => ({}), | |||
| }, | |||
| toggleExpConfig: { | |||
| type: Function, | |||
| default: () => ({}), | |||
| }, | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .experiment-config { | |||
| position: fixed; | |||
| right: -18px; | |||
| top: 100px; | |||
| z-index: 1000; | |||
| .left-round-button { | |||
| margin-bottom: 12px; | |||
| box-shadow: 0 3px 3px rgba(0, 0, 0, 0.08); | |||
| border-radius: 16px 0 0 16px; | |||
| text-align: left; | |||
| min-width: 160px; | |||
| padding-left: 16px; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,230 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="detail-container"> | |||
| <div class="app-page-header-title">实验详情</div> | |||
| <div class="mt-50 flex flex-between"> | |||
| <div class="flex status-box"> | |||
| <div class="mr-10 my-auto"> | |||
| 状态:<span :style="statusColor">{{ statusName }}</span> | |||
| </div> | |||
| <div v-if="!isFinished" class="mx-10 my-auto"> | |||
| 当前阶段: | |||
| <span class="primary">{{ stageName }}</span> | |||
| </div> | |||
| <template v-else> | |||
| <div class="mx-10 my-auto"> | |||
| 最佳精度: | |||
| <span class="primary">{{ detail.bestAccuracy.toFixed(2) }}</span> | |||
| </div> | |||
| <div class="mx-10 my-auto"> | |||
| TRIAL-ID: | |||
| <span class="primary">{{ detail.bestTrialSequence }}</span> | |||
| </div> | |||
| </template> | |||
| </div> | |||
| <div class="flex f1 flex-end"> | |||
| <el-button | |||
| type="text" | |||
| class="primary mr-10" | |||
| icon="el-icon-refresh-right" | |||
| @click="refresh" | |||
| /> | |||
| <div class="app-page-header-extra"> | |||
| <el-dropdown v-show="state.activeTab === stageName" @command="command"> | |||
| <div class="primary mr-10 rel"> | |||
| {{ enableAutoRefresh ? `每${refreshTime}s刷新` : '定时刷新已关闭' }} | |||
| <i class="el-icon-arrow-down el-icon--right" /> | |||
| </div> | |||
| <el-dropdown-menu slot="dropdown"> | |||
| <el-dropdown-item | |||
| v-for="item in refreshControls" | |||
| :key="item.value" | |||
| :icon="item.icon" | |||
| :command="item.value" | |||
| > | |||
| {{ item.label }} | |||
| </el-dropdown-item> | |||
| </el-dropdown-menu> | |||
| </el-dropdown> | |||
| <el-button v-if="enablePause" type="primary" @click="pause"> | |||
| 暂停实验 | |||
| </el-button> | |||
| <el-button v-if="enableStart" type="primary" @click="start"> | |||
| 启动实验 | |||
| </el-button> | |||
| <el-button v-if="isFinished" type="primary" @click="saveModel"> | |||
| 保存模型 | |||
| </el-button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="flex flex-between mt-50"> | |||
| <Description :columns="infoList" /> | |||
| <el-button style="margin: auto auto 0 auto" @click="changeToLog">查看日志</el-button> | |||
| </div> | |||
| <SaveModelDialog ref="saveModelRef" type="tadl" /> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { Message } from 'element-ui'; | |||
| import { reactive, computed, watch, ref, onBeforeUnmount } from '@vue/composition-api'; | |||
| import Description from '@/components/Description'; | |||
| import { parseTime } from '@/utils'; | |||
| import { pauseExp, startExp } from '@/api/tadl'; | |||
| import SaveModelDialog from '@/components/Training/saveModelDialog'; | |||
| import { | |||
| refreshControls, | |||
| runTimeFormatter, | |||
| getModelByCode, | |||
| getExpByCode, | |||
| getStageName, | |||
| STAGE_SEQUENCE, | |||
| } from '../../util'; | |||
| export default { | |||
| name: 'DetailDashboard', | |||
| components: { | |||
| Description, | |||
| SaveModelDialog, | |||
| }, | |||
| props: { | |||
| saveRefreshTime: Function, | |||
| refreshTime: Number, | |||
| refresh: Function, | |||
| updateState: Function, | |||
| detail: Object, | |||
| isFinished: Boolean, | |||
| inProgress: Boolean, | |||
| enablePause: Boolean, | |||
| enableStart: Boolean, | |||
| activePath: Array, | |||
| command: Function, | |||
| }, | |||
| setup(props) { | |||
| const { updateState, refresh, command } = props; | |||
| const saveModelRef = ref(null); | |||
| const state = reactive({ | |||
| activeTab: props.activePath[0], | |||
| prevActiveTab: props.activePath[0], | |||
| }); | |||
| const changeToLog = () => { | |||
| Object.assign(state, { | |||
| prevActiveTab: null, | |||
| activeTab: null, | |||
| }); | |||
| command(0); // 关闭自动刷新 | |||
| updateState({ activePath: ['LOG', 'algrithom'], activeStage: '' }); | |||
| }; | |||
| const pause = async () => { | |||
| await pauseExp(props.detail.id).then(() => { | |||
| Message.success('实验已暂停'); | |||
| refresh(); | |||
| command(0); // 关闭自动刷新 | |||
| }); | |||
| }; | |||
| const start = async () => { | |||
| await startExp(props.detail.id).then(() => { | |||
| Message.success('实验启动中'); | |||
| refresh(); | |||
| command(0); // 关闭自动刷新 | |||
| }); | |||
| }; | |||
| const statusName = computed(() => getExpByCode(props.detail.status, 'label')); | |||
| const stageName = computed(() => getStageName(props.detail.runStage)); | |||
| const statusColor = computed(() => ({ color: getExpByCode(props.detail.status, 'bgColor') })); | |||
| const enableAutoRefresh = computed(() => props.refreshTime > 0); | |||
| const showBestAccuracy = computed(() => props.detail.runStage === STAGE_SEQUENCE.RETRAIN); | |||
| const infoList = computed(() => { | |||
| const runingTime = props.inProgress | |||
| ? { label: '运行时间', content: runTimeFormatter(props.detail.runTime) } | |||
| : { label: '结束时间', content: parseTime(props.detail.endTime) }; | |||
| return [ | |||
| [ | |||
| { label: '实验名称', content: props.detail.name }, | |||
| { label: '实验 ID', content: props.detail.id }, | |||
| { label: '模型类别', content: getModelByCode(props.detail.modelType, 'label') }, | |||
| ], | |||
| [ | |||
| { label: '算法名称', content: props.detail.algorithmName }, | |||
| { label: '算法版本', content: props.detail.algorithmVersion }, | |||
| { label: '创 建 人', content: props.detail.createUser }, | |||
| ], | |||
| [ | |||
| { label: '开始时间', content: parseTime(props.detail.startTime) }, | |||
| runingTime, | |||
| { label: '实验描述', content: props.detail.description, span: 2 }, | |||
| ], | |||
| ]; | |||
| }); | |||
| const saveModel = () => { | |||
| const modelParams = { | |||
| algorithmId: props.detail.algorithmId, | |||
| modelAddress: props.detail.bestCheckpointPath, | |||
| }; | |||
| saveModelRef.value.show(modelParams); | |||
| }; | |||
| watch( | |||
| () => props.activePath, | |||
| (next) => { | |||
| if (next && next.length) { | |||
| Object.assign(state, { | |||
| activeTab: next[0], | |||
| prevActiveTab: next[0], | |||
| }); | |||
| } | |||
| } | |||
| ); | |||
| onBeforeUnmount(() => command(0)); | |||
| return { | |||
| state, | |||
| saveModelRef, | |||
| statusColor, | |||
| changeToLog, | |||
| statusName, | |||
| stageName, | |||
| refreshControls, | |||
| enableAutoRefresh, | |||
| showBestAccuracy, | |||
| pause, | |||
| start, | |||
| infoList, | |||
| saveModel, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .detail-container { | |||
| padding: 32px; | |||
| background: #fff; | |||
| box-shadow: 0px 2px 7px 0px rgba(209, 209, 217, 0.5); | |||
| } | |||
| .description-items { | |||
| width: 100%; | |||
| } | |||
| .status-box { | |||
| div { | |||
| margin-right: 72px; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,38 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-card class="app-content-section" shadow="never"> | |||
| <div class="app-content-title mb-20">概览</div> | |||
| <div class="flex flex-vertical-align"> | |||
| <div v-if="!isOneTrial" style="width: 100%"> | |||
| <TrialStat :info="info" /> | |||
| </div> | |||
| <div v-else-if="stage !== 'RETRAIN'" style="width: 100%"> | |||
| <SingleTrialStat /> | |||
| </div> | |||
| </div> | |||
| </el-card> | |||
| </template> | |||
| <script> | |||
| import TrialStat from './stat'; | |||
| import SingleTrialStat from './singleTrialStat'; | |||
| export default { | |||
| name: 'General', | |||
| components: { | |||
| TrialStat, | |||
| SingleTrialStat, | |||
| }, | |||
| props: { | |||
| info: Object, | |||
| stage: String, | |||
| isOneTrial: Boolean, | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,181 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-card shadow="never" class="rel app-content-section"> | |||
| <div class="app-content-title flex flex-between" style="margin: 12px"> | |||
| <span>当前阶段实验参数</span> | |||
| <InfoRadio | |||
| v-model="state.activeParamType" | |||
| type="button" | |||
| :dataSource="paramType" | |||
| @change="handleParamChange" | |||
| /> | |||
| </div> | |||
| <Description v-if="state.activeParamType === 0" :columns="paramList" :data="param"> | |||
| <template v-slot:数据集> | |||
| <el-link type="primary" @click="gotoDataset">{{ datasetName }}</el-link> | |||
| </template> | |||
| </Description> | |||
| <div v-else> | |||
| <YamlEditor ref="yamlRef" :value="state.yaml" @blur="onYamlChange" /> | |||
| <el-button type="primary" class="mt-10" @click="saveParamChange">保存修改</el-button> | |||
| </div> | |||
| </el-card> | |||
| </template> | |||
| <script> | |||
| import yaml from 'js-yaml'; | |||
| import { reactive, computed, ref, watch } from '@vue/composition-api'; | |||
| import { Message, MessageBox } from 'element-ui'; | |||
| import YamlEditor from '@/components/YamlEditor'; | |||
| import InfoRadio from '@/components/InfoRadio'; | |||
| import Description from '@/components/Description'; | |||
| import { propertyAssign, parseTime } from '@/utils'; | |||
| import { updateExpYaml, expYaml } from '@/api/tadl'; | |||
| import { runTimeFormatter, getStageOrder } from '../../util'; | |||
| import { isNull, underlineShiftHump } from '../../strategy/util'; | |||
| export default { | |||
| name: 'ExpParameter', | |||
| components: { | |||
| YamlEditor, | |||
| InfoRadio, | |||
| Description, | |||
| }, | |||
| props: { | |||
| experimentId: String, | |||
| stage: String, | |||
| param: Object, | |||
| progress: Number, | |||
| }, | |||
| setup(props, ctx) { | |||
| const { $router } = ctx.root; | |||
| const stageOrder = getStageOrder(props.stage); | |||
| const state = reactive({ | |||
| activeParamType: 0, | |||
| yamlNotSaved: true, | |||
| yaml: '', | |||
| }); | |||
| const yamlRef = ref(null); | |||
| const paramType = [ | |||
| { | |||
| label: '查看模式', | |||
| value: 0, | |||
| }, | |||
| { | |||
| label: '编辑模式', | |||
| value: 1, | |||
| }, | |||
| ]; | |||
| const datasetName = computed(() => props.param.datasetName); | |||
| const paramList = computed(() => { | |||
| const runingTime = | |||
| props.progress === 0 | |||
| ? { label: '运行时间', content: runTimeFormatter(props.param.runTime) || '暂无数据' } | |||
| : { label: '结束时间', content: parseTime(props.param.endTime) || '暂无数据' }; | |||
| return [ | |||
| [ | |||
| { label: '数据集' }, | |||
| { label: '资 源', content: props.param.resourceName }, | |||
| { label: '算法入口', content: props.param.executeScript }, | |||
| ], | |||
| [ | |||
| { label: '开始时间', content: parseTime(props.param.startTime) || '暂无数据', span: 2 }, | |||
| { ...runingTime, span: 2 }, | |||
| ], | |||
| ]; | |||
| }); | |||
| const gotoDataset = () => { | |||
| $router.push({ path: `/data/datasets/${props.param.datasetId}/version` }); | |||
| }; | |||
| const saveParamChange = async () => { | |||
| updateExpYaml(props.experimentId, getStageOrder(props.stage), state.yaml) | |||
| .then(() => { | |||
| Message.success('保存成功'); | |||
| state.yamlNotSaved = false; | |||
| }) | |||
| .catch((err) => { | |||
| Message.error(err.message); | |||
| }); | |||
| }; | |||
| const handleParamChange = async (value) => { | |||
| if (state.activeParamType === 0 && state.yamlNotSaved) { | |||
| await MessageBox.confirm('是否保存当前修改?', '提示', { | |||
| confirmButtonText: '确定', | |||
| cancelButtonText: '取消', | |||
| type: 'warning', | |||
| }) | |||
| .then(() => { | |||
| saveParamChange(); | |||
| }) | |||
| .catch(() => { | |||
| Message.info('当前修改未保存'); | |||
| state.yamlNotSaved = false; | |||
| }); | |||
| state.yamlNotSaved = true; | |||
| state.activeParamType = value; | |||
| } | |||
| }; | |||
| // 直接编辑 Yaml 内容后触发解析 | |||
| const onYamlChange = (yamlValue) => { | |||
| state.yamlNotSaved = true; | |||
| try { | |||
| const yamlLoad = yaml.load(yamlValue); | |||
| if (!yamlLoad) return; | |||
| propertyAssign(state, underlineShiftHump(yamlLoad), (val) => !isNull(val)); | |||
| state.yaml = yamlValue; | |||
| } catch (err) { | |||
| console.error(err); | |||
| if (err.name === 'YAMLException') { | |||
| Message.error('Yaml 解析错误,请检查'); | |||
| } else { | |||
| throw err; | |||
| } | |||
| } | |||
| }; | |||
| watch( | |||
| () => state.activeParamType, | |||
| async (next) => { | |||
| if (next === 1) { | |||
| state.yaml = await expYaml(props.experimentId, stageOrder); | |||
| } | |||
| } | |||
| ); | |||
| return { | |||
| yamlRef, | |||
| state, | |||
| paramType, | |||
| gotoDataset, | |||
| datasetName, | |||
| paramList, | |||
| handleParamChange, | |||
| saveParamChange, | |||
| onYamlChange, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .description-items { | |||
| max-width: 80%; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,282 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="rel app-content-section run-parameter-card"> | |||
| <div class="app-content-title mb-20">当前阶段运行参数</div> | |||
| <el-form ref="form" :model="state.model" label-position="top"> | |||
| <el-row :gutter="16" class="mb-50"> | |||
| <div class="flex flex-between"> | |||
| <div>持续时间</div> | |||
| <div>当前阶段最长持续时间</div> | |||
| </div> | |||
| <div class="flex flex-between"> | |||
| <div class="el-form-item-explain"> | |||
| <span class="primary">{{ duration }}</span> / {{ maxExecDurationStr }} | |||
| </div> | |||
| <div> | |||
| <span>{{ maxExecDurationStr }}</span> | |||
| <Edit | |||
| class="edit-icon" | |||
| :row="state.rawModel" | |||
| valueBy="maxExecDuration" | |||
| title="修改最长持续时间" | |||
| rules="required|validInteger" | |||
| label="时间" | |||
| :beforeChange="validateParam" | |||
| @handleOk="handleMaxExecDurationChange" | |||
| > | |||
| <el-select | |||
| slot="append" | |||
| v-model="state.rawModel.maxExecDurationUnit" | |||
| placeholder="请选择" | |||
| > | |||
| <el-option | |||
| v-for="item in timeFmts" | |||
| :key="item.value" | |||
| :value="item.value" | |||
| :label="item.label" | |||
| /> | |||
| </el-select> | |||
| </Edit> | |||
| </div> | |||
| </div> | |||
| <el-progress :percentage="execDurPercent" :show-text="false"></el-progress> | |||
| </el-row> | |||
| <el-row :gutter="16" class="mb-50"> | |||
| <div class="flex flex-between"> | |||
| <div>Trial数量</div> | |||
| <div>当前阶段最大Trial数量</div> | |||
| </div> | |||
| <div class="flex flex-between"> | |||
| <div class="el-form-item-explain"> | |||
| <span class="primary">{{ state.model.trialNum }}</span> / | |||
| {{ state.model.maxTrialNum }} | |||
| </div> | |||
| <div> | |||
| <span>{{ state.model.maxTrialNum }}</span> | |||
| <Edit | |||
| class="edit-icon" | |||
| :row="state.model" | |||
| :disabled="isOneTrial" | |||
| valueBy="maxTrialNum" | |||
| title="修改最大 Trial 数量" | |||
| rules="required|validInteger" | |||
| label="Trial 数量" | |||
| :beforeChange="validateParam" | |||
| @handleOk="handleMaxTrialNumChange" | |||
| /> | |||
| </div> | |||
| </div> | |||
| <el-progress :percentage="trialPercent" :show-text="false"></el-progress> | |||
| </el-row> | |||
| <el-row :gutter="16"> | |||
| <div class="flex flex-between"> | |||
| <div></div> | |||
| <div>Trial并发数</div> | |||
| </div> | |||
| <div class="flex flex-end"> | |||
| <span>{{ state.model.trialConcurrentNum }}</span> | |||
| <Edit | |||
| class="edit-icon" | |||
| :row="state.model" | |||
| :disabled="isOneTrial" | |||
| valueBy="trialConcurrentNum" | |||
| title="修改 Trial 并发数" | |||
| rules="required|validInteger" | |||
| label="最大 Trial 数量" | |||
| :beforeChange="validateParam" | |||
| @handleOk="handleConcurrentNumChange" | |||
| /> | |||
| </div> | |||
| </el-row> | |||
| </el-form> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { reactive, computed, watch } from '@vue/composition-api'; | |||
| import { Message } from 'element-ui'; | |||
| import { pick } from 'lodash'; | |||
| import Edit from '@/components/InlineTableEdit'; | |||
| import { toFixed } from '@/utils'; | |||
| import { updateConcurrentNum, updateMaxTrialNum, updateMaxExecDuration } from '@/api/tadl'; | |||
| import { runTimeFormatter, timeFmts, parseRunTime, getStageOrder } from '../../util'; | |||
| export default { | |||
| name: 'ExpRunParameter', | |||
| components: { | |||
| Edit, | |||
| }, | |||
| props: { | |||
| param: Object, | |||
| isOneTrial: Boolean, | |||
| experimentId: String, | |||
| stage: String, | |||
| refresh: Function, | |||
| }, | |||
| setup(props) { | |||
| const stageOrder = computed(() => { | |||
| return getStageOrder(props.stage); | |||
| }); | |||
| // TODO: 修改单位不直接修改值,只有点击确认才修改 | |||
| const buildParams = (param) => | |||
| pick(param, ['maxExecDurationUnit', 'maxExecDuration', 'maxTrialNum', 'trialConcurrentNum']); | |||
| const state = reactive({ | |||
| model: props.param, | |||
| rawModel: buildParams(props.param), | |||
| }); | |||
| const maxExecDurationStr = computed(() => { | |||
| const newVal = state.model.maxExecDuration + state.model.maxExecDurationUnit; | |||
| return newVal; | |||
| }); | |||
| const maxExecDuration = computed(() => | |||
| parseRunTime(state.model.maxExecDuration, state.model.maxExecDurationUnit) | |||
| ); | |||
| const duration = computed(() => runTimeFormatter(state.model.runTime) || 0); | |||
| const execDurPercent = computed(() => { | |||
| return Math.min(100, toFixed(state.model.runTime / maxExecDuration.value, 2, 0)); | |||
| }); | |||
| const trialPercent = computed(() => { | |||
| return Math.min(100, toFixed(state.model.trialNum / state.model.maxTrialNum, 2, 0)); | |||
| }); | |||
| // 当前校验阶段最长持续时间和最大 trial 数量 | |||
| const applyErrors = (value, row, options) => { | |||
| // 获取修改类型 | |||
| const { valueBy } = options; | |||
| const errors = []; | |||
| if (valueBy === 'maxExecDuration') { | |||
| const changeTime = parseRunTime(value, row.maxExecDurationUnit); | |||
| const curTime = state.model.runTime; | |||
| if (changeTime <= curTime) { | |||
| errors.push('修改后时间不能小于当前运行时间'); | |||
| } | |||
| } else if (valueBy === 'maxTrialNum') { | |||
| if (value < state.model.trialNum) { | |||
| errors.push('修改后最大 trial 数量不能小于当前运行中数量'); | |||
| } | |||
| } else if (valueBy === 'trialConcurrentNum') { | |||
| if (value > state.model.maxTrialNum) { | |||
| errors.push('修改后 trial 并发数量不能大于总的 trial 数量'); | |||
| } | |||
| } | |||
| return errors; | |||
| }; | |||
| // 校验参数合理性 | |||
| const validateParam = (value, row, provider, options = {}) => { | |||
| const errors = applyErrors(value, row, options); | |||
| return new Promise((resolve, reject) => { | |||
| provider.value.applyResult({ | |||
| errors, | |||
| valid: false, // boolean state | |||
| failedRules: {}, // should be empty since this is a manual error. | |||
| }); | |||
| if (errors.length === 0) { | |||
| resolve(true); | |||
| } else { | |||
| reject(errors); | |||
| } | |||
| }); | |||
| }; | |||
| // 运行参数变更 | |||
| const handleParamChange = (value, row, { valueBy }) => { | |||
| const next = { ...state.model, ...row, ...{ [valueBy]: value } }; | |||
| Object.assign(state, { | |||
| model: next, | |||
| rawModel: buildParams(next), | |||
| }); | |||
| }; | |||
| // trial最大并发数变更 | |||
| const handleConcurrentNumChange = (value, row, { valueBy }) => { | |||
| updateConcurrentNum(props.experimentId, stageOrder.value, value).then(() => { | |||
| Message.success('修改运行参数成功'); | |||
| handleParamChange(value, row, { valueBy }); | |||
| props.refresh(); | |||
| }); | |||
| }; | |||
| // trial最大数量变更 | |||
| const handleMaxTrialNumChange = (value, row, { valueBy }) => { | |||
| updateMaxTrialNum(props.experimentId, stageOrder.value, value).then(() => { | |||
| Message.success('修改运行参数成功'); | |||
| handleParamChange(value, row, { valueBy }); | |||
| }); | |||
| props.refresh(); | |||
| }; | |||
| // 最大运行时间变更 | |||
| const handleMaxExecDurationChange = (value, row, { valueBy }) => { | |||
| updateMaxExecDuration( | |||
| props.experimentId, | |||
| stageOrder.value, | |||
| value, | |||
| row.maxExecDurationUnit | |||
| ).then(() => { | |||
| Message.success('修改运行参数成功'); | |||
| handleParamChange(value, row, { valueBy }); | |||
| }); | |||
| }; | |||
| watch( | |||
| () => props.param, | |||
| (next) => { | |||
| if (next) { | |||
| Object.assign(state, { | |||
| model: next, | |||
| rawModel: buildParams(next), | |||
| }); | |||
| } | |||
| } | |||
| ); | |||
| return { | |||
| state, | |||
| maxExecDurationStr, | |||
| maxExecDuration, | |||
| duration, | |||
| execDurPercent, | |||
| trialPercent, | |||
| timeFmts, | |||
| handleConcurrentNumChange, | |||
| handleMaxTrialNumChange, | |||
| handleMaxExecDurationChange, | |||
| validateParam, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .run-parameter-card { | |||
| border: 1px solid#DFE1E5; | |||
| padding: 32px; | |||
| ::v-deep .el-form-item { | |||
| margin-bottom: 0; | |||
| } | |||
| ::v-deep .el-form-item__label { | |||
| padding-bottom: 0; | |||
| } | |||
| } | |||
| .ptr { | |||
| padding-top: 22px; | |||
| } | |||
| </style> | |||
| <style lang="scss"> | |||
| .el-select .el-input { | |||
| min-width: 80px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,134 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <BaseModal | |||
| :visible="state.visible" | |||
| title="保存模型" | |||
| :loading="state.loading" | |||
| @change="handleClose" | |||
| @cancel="handleClose" | |||
| @ok="handleSave" | |||
| > | |||
| <el-form ref="saveForm" :model="state.saveForm" label-width="80px"> | |||
| <el-form-item label="模型名称" prop="modelName"> | |||
| <el-input | |||
| v-model="state.saveForm.modelName" | |||
| placeholder="请输入模型名称" | |||
| maxlength="50" | |||
| show-word-limit | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="框架" prop="frameType"> | |||
| <el-input v-model="state.saveForm.frameType" disabled /> | |||
| </el-form-item> | |||
| <el-form-item label="模型格式" prop="modelType"> | |||
| <el-select | |||
| v-model="state.saveForm.modelType" | |||
| placeholder="请选择模型格式" | |||
| filterable | |||
| style="width: 300px;" | |||
| disabled | |||
| > | |||
| </el-select> | |||
| </el-form-item> | |||
| <el-form-item label="模型类别" prop="modelClassName"> | |||
| <el-select v-model="state.saveForm.modelClassName" disabled></el-select> | |||
| </el-form-item> | |||
| <el-form-item label="描述" prop="modelDescription"> | |||
| <el-input | |||
| v-model.trim="state.saveForm.modelDescription" | |||
| type="textarea" | |||
| placeholder="请输入模型描述" | |||
| maxlength="255" | |||
| show-word-limit | |||
| /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </BaseModal> | |||
| </template> | |||
| <script> | |||
| import { reactive, watch, nextTick } from '@vue/composition-api'; | |||
| import { Message } from 'element-ui'; | |||
| import BaseModal from '@/components/BaseModal'; | |||
| import { add as saveModel } from '@/api/model/model'; | |||
| import { getModelByCode } from '../../util'; | |||
| export default { | |||
| name: 'SaveModelModal', | |||
| components: { BaseModal }, | |||
| props: { | |||
| detail: Object, | |||
| }, | |||
| setup(props) { | |||
| const state = reactive({ | |||
| saveForm: { | |||
| modelName: props.detail.name, | |||
| modelClassName: getModelByCode(props.detail.modelType, 'label'), | |||
| modelDescription: '', | |||
| frameType: 'pytorch', | |||
| modelType: 'pth', | |||
| }, | |||
| visible: false, | |||
| loading: false, | |||
| }); | |||
| const handleClose = () => { | |||
| state.visible = false; | |||
| state.saveForm = { | |||
| modelName: props.detail.name, | |||
| modelClassName: getModelByCode(props.detail.modelType, 'label'), | |||
| modelDescription: '', | |||
| frameType: 'pytorch', | |||
| modelType: 'pth', | |||
| }; | |||
| }; | |||
| const handleShow = () => { | |||
| state.visible = true; | |||
| }; | |||
| const handleSave = () => { | |||
| saveModel({ | |||
| modelClassName: getModelByCode(props.detail.modelType, 'label'), | |||
| modelDescription: state.saveForm.modelDescription, | |||
| name: state.saveForm.modelName, | |||
| modelSource: 1, | |||
| frameType: 3, | |||
| modelType: 8, | |||
| }).then(() => { | |||
| Message.success('保存成功'); | |||
| handleClose(); | |||
| }); | |||
| }; | |||
| watch( | |||
| () => props.detail, | |||
| () => { | |||
| nextTick(() => { | |||
| state.saveForm = { | |||
| modelName: props.detail.name, | |||
| modelClassName: getModelByCode(props.detail.modelType, 'label'), | |||
| modelDescription: '', | |||
| frameType: 'pytorch', | |||
| modelType: 'pth', | |||
| }; | |||
| }); | |||
| } | |||
| ); | |||
| return { | |||
| state, | |||
| handleClose, | |||
| handleShow, | |||
| handleSave, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,63 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="flex flex-wrap" style="margin: 0 10%;"> | |||
| <div v-for="item in stats" :key="item.label" style="width: 50%; margin-bottom: 20px;"> | |||
| <Statistic | |||
| :title="item.label" | |||
| :value="item.value" | |||
| class="styledBorder" | |||
| :style="{ borderLeftColor: item.color }" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import Statistic from '@/components/Statistic'; | |||
| export default { | |||
| name: 'SingleTrialStat', | |||
| components: { | |||
| Statistic, | |||
| }, | |||
| props: { | |||
| info: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| }, | |||
| setup() { | |||
| const stats = [ | |||
| { value: 1, label: 'trial 数', color: '#52C41A' }, | |||
| { value: 0.98, label: '最佳精度', color: '#F5222D' }, | |||
| ]; | |||
| return { | |||
| stats, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .styledBorder { | |||
| padding-left: 10px; | |||
| border-left-width: 4px; | |||
| border-left-style: solid; | |||
| margin-bottom: 40px; | |||
| ::v-deep { | |||
| .el-statistic-title { | |||
| font-size: 16px; | |||
| } | |||
| .el-statistic-content { | |||
| font-size: 36px; | |||
| line-height: 54px; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,69 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="flex flex-wrap" style="margin: 0 10%;"> | |||
| <div v-for="item in stats" :key="item.label" style="width: 50%; margin-bottom: 20px;"> | |||
| <Statistic | |||
| :title="item.label" | |||
| :value="item.value" | |||
| class="styledBorder" | |||
| :style="{ borderLeftColor: item.color }" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { computed } from '@vue/composition-api'; | |||
| import Statistic from '@/components/Statistic'; | |||
| import { TRIAL_STATUS_MAP } from '../../util'; | |||
| export default { | |||
| name: 'TrialStat', | |||
| components: { | |||
| Statistic, | |||
| }, | |||
| props: { | |||
| info: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| }, | |||
| setup(props) { | |||
| const stats = computed(() => | |||
| Object.keys(TRIAL_STATUS_MAP).map((key) => ({ | |||
| label: TRIAL_STATUS_MAP[key].label, | |||
| value: props.info[key], | |||
| color: TRIAL_STATUS_MAP[key].bgColor, | |||
| })) | |||
| ); | |||
| return { | |||
| stats, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .styledBorder { | |||
| padding-left: 10px; | |||
| border-left-width: 4px; | |||
| border-left-style: solid; | |||
| margin-bottom: 40px; | |||
| ::v-deep { | |||
| .el-statistic-title { | |||
| font-size: 16px; | |||
| } | |||
| .el-statistic-content { | |||
| font-size: 36px; | |||
| line-height: 54px; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,98 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-card shadow="never" class="rel app-content-section trials-card"> | |||
| <div class="app-content-title mb-20">Trials(最新 5 条)</div> | |||
| <BaseTable :columns="columns" :data="state.list" /> | |||
| <div class="mt-10"><el-link @click="changeToTrialsList">查看全部</el-link></div> | |||
| </el-card> | |||
| </template> | |||
| <script> | |||
| import { reactive, onMounted, computed } from '@vue/composition-api'; | |||
| import { expStageTrialRep } from '@/api/tadl'; | |||
| import BaseTable from '@/components/BaseTable'; | |||
| import { getStageOrder, getTrialByCode, runTimeFormatter } from '../../util'; | |||
| export default { | |||
| name: 'Trials', | |||
| components: { | |||
| BaseTable, | |||
| }, | |||
| props: { | |||
| stage: String, | |||
| changeTab: Function, | |||
| }, | |||
| setup(props, ctx) { | |||
| const { $route } = ctx.root; | |||
| const { params = {} } = $route; | |||
| const { experimentId } = params; | |||
| const { changeTab } = props; | |||
| const stageOrder = getStageOrder(props.stage); | |||
| const state = reactive({ | |||
| list: [], | |||
| }); | |||
| const changeToTrialsList = () => { | |||
| changeTab({ name: 'trials' }); | |||
| }; | |||
| const columns = computed(() => [ | |||
| { | |||
| prop: 'sequence', | |||
| label: 'Run', | |||
| formatter: (value) => `RUN ${value}`, | |||
| }, | |||
| { | |||
| prop: 'trialId', | |||
| label: 'Trial Id', | |||
| }, | |||
| { | |||
| prop: 'status', | |||
| label: '状态', | |||
| type: 'tag', | |||
| tagAttr: { | |||
| style: (col) => ({ | |||
| color: getTrialByCode(col.status, 'bgColor'), | |||
| borderColor: getTrialByCode(col.status, 'bgColor'), | |||
| }), | |||
| }, | |||
| formatter: (value) => getTrialByCode(value, 'label'), | |||
| }, | |||
| { | |||
| prop: 'runTime', | |||
| label: '持续时间', | |||
| formatter: runTimeFormatter, | |||
| }, | |||
| { | |||
| prop: 'value', | |||
| label: 'accuracy', | |||
| }, | |||
| ]); | |||
| onMounted(async () => { | |||
| const data = await expStageTrialRep(experimentId, stageOrder); | |||
| state.list = data; | |||
| }); | |||
| return { | |||
| state, | |||
| changeToTrialsList, | |||
| columns, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .trials-card { | |||
| height: 400px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,383 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-card shadow="never" class="rel app-content-section trials-card"> | |||
| <div class="app-content-title mb-20">Trials</div> | |||
| <ProTable | |||
| ref="proTable" | |||
| :showCreate="false" | |||
| :columns="columns" | |||
| :list-request="list" | |||
| :list-options="listOptions" | |||
| showRefresh | |||
| > | |||
| <div slot="left"> | |||
| <el-button :disabled="contrastDisabled" @click="showContrast"> | |||
| {{ contrastTitle }} | |||
| </el-button> | |||
| </div> | |||
| </ProTable> | |||
| <!-- 保存制品弹窗 --> | |||
| <BaseModal | |||
| :key="`prod${state.prodKey}`" | |||
| :visible="state.actionModal.show && state.actionModal.type === 'prod'" | |||
| title="保存制品" | |||
| :loading="state.actionModal.showOkLoading" | |||
| @change="handleCancel" | |||
| @ok="saveProd" | |||
| > | |||
| <el-form ref="saveForm" :model="state.saveForm" label-width="80px"> | |||
| <el-form-item label="制品名称" prop="prodName"> | |||
| <el-input | |||
| v-model.trim="state.saveForm.prodName" | |||
| placeholder="制品名称长度不能超过50字" | |||
| maxlength="50" | |||
| show-word-limit | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="描述" prop="description"> | |||
| <el-input | |||
| v-model.trim="state.saveForm.description" | |||
| type="textarea" | |||
| placeholder="制品描述长度不能超过100字" | |||
| maxlength="100" | |||
| rows="3" | |||
| show-word-limit | |||
| /> | |||
| </el-form-item> | |||
| </el-form> | |||
| </BaseModal> | |||
| <!-- trials对比弹窗 --> | |||
| <BaseModal | |||
| :key="`visual${state.visualKey}`" | |||
| :visible="state.actionModal.show && state.actionModal.type === 'visual'" | |||
| :title="visualTitle" | |||
| :loading="state.actionModal.showOkLoading" | |||
| :showCancel="false" | |||
| @change="handleCancel" | |||
| @ok="handleCancel" | |||
| > | |||
| <div v-if="!isEmpty(state.contrastChartConfig) && !isEmpty(state.contrastChartData)"> | |||
| <Chart | |||
| type="LineChart" | |||
| :chartConfig="state.contrastChartConfig" | |||
| :chartData="state.contrastChartData" | |||
| style="height: 400px" | |||
| /> | |||
| </div> | |||
| <div v-else>获取绘图数据失败</div> | |||
| </BaseModal> | |||
| <!-- 查看日志弹窗 --> | |||
| <BaseModal | |||
| :key="`log${state.logKey}`" | |||
| class="trialLogModal" | |||
| :visible="state.actionModal.show && state.actionModal.type === 'log'" | |||
| :loading="state.actionModal.showOkLoading" | |||
| title="trial日志" | |||
| width="50" | |||
| :showCancel="false" | |||
| @change="handleCancel" | |||
| @ok="handleCancel" | |||
| > | |||
| <PodLogContainer ref="podLogContainer" :pod="logOptions" /> | |||
| </BaseModal> | |||
| <!-- 查看参数弹窗 --> | |||
| <BaseModal | |||
| :key="`param${state.paramKey}`" | |||
| :visible="state.actionModal.show && state.actionModal.type === 'param'" | |||
| :loading="state.actionModal.showOkLoading" | |||
| title="trial参数" | |||
| :showCancel="false" | |||
| @change="handleCancel" | |||
| @ok="handleCancel" | |||
| > | |||
| 参数 | |||
| </BaseModal> | |||
| </el-card> | |||
| </template> | |||
| <script> | |||
| import { reactive, computed, ref, watch } from '@vue/composition-api'; | |||
| import { Message } from 'element-ui'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { expStageTrialList as list, expStageIntermediate } from '@/api/tadl'; | |||
| import { getPodLog } from '@/api/system/pod'; | |||
| import ProTable from '@/components/ProTable'; | |||
| import BaseModal from '@/components/BaseModal'; | |||
| import PodLogContainer from '@/components/LogContainer/podLogContainer'; | |||
| import { | |||
| getStageOrder, | |||
| getTrialByCode, | |||
| runTimeFormatter, | |||
| extractSeriesData, | |||
| TRIAL_STATUS_MAP, | |||
| } from '../../util'; | |||
| import Chart from './chart'; | |||
| import { allTrialStatusList } from '../util'; | |||
| export default { | |||
| name: 'TrialsList', | |||
| components: { | |||
| ProTable, | |||
| BaseModal, | |||
| Chart, | |||
| PodLogContainer, | |||
| }, | |||
| props: { | |||
| stage: String, | |||
| activeTab: String, | |||
| contrastTitle: String, | |||
| createUserId: Number, | |||
| }, | |||
| setup(props, ctx) { | |||
| const { $route } = ctx.root; | |||
| const { params = {} } = $route; | |||
| const { experimentId } = params; | |||
| const stageOrder = getStageOrder(props.stage); | |||
| const podLogContainer = ref(null); | |||
| const listOptions = computed(() => { | |||
| return { | |||
| experimentId, | |||
| stageOrder, | |||
| }; | |||
| }); | |||
| const defaultConfig = { | |||
| autoFit: true, | |||
| xField: null, | |||
| yField: null, | |||
| seriesField: null, | |||
| smooth: false, // 平滑曲线 | |||
| xAxis: { | |||
| title: { | |||
| text: 'sequence', | |||
| spacing: 30, | |||
| style: { | |||
| fontSize: 20, | |||
| }, | |||
| }, | |||
| }, | |||
| yAxis: { | |||
| title: { | |||
| text: '中间值', | |||
| autoRotate: false, | |||
| textStyle: { | |||
| fontSize: 20, | |||
| width: 20, | |||
| }, | |||
| position: 'center', | |||
| }, | |||
| }, | |||
| }; | |||
| const proTable = ref(null); | |||
| const state = reactive({ | |||
| isContrast: true, | |||
| contrastChartConfig: {}, | |||
| contrastChartData: [], | |||
| saveForm: {}, | |||
| actionModal: { | |||
| show: false, | |||
| row: undefined, | |||
| showOkLoading: false, | |||
| type: null, | |||
| }, | |||
| logKey: 1, | |||
| paramKey: 1, | |||
| prodKey: 1, | |||
| visualKey: 1, | |||
| activePod: '', | |||
| }); | |||
| const contrastDisabled = computed(() => proTable.value?.state.selectedRows.length <= 1); | |||
| const visualTitle = computed(() => (state.isContrast ? 'trials对比' : '')); | |||
| const showActionModal = (row, type) => { | |||
| Object.assign(state, { | |||
| actionModal: { | |||
| show: true, | |||
| row, | |||
| showOkLoading: false, | |||
| type, | |||
| }, | |||
| }); | |||
| }; | |||
| const resetLogger = () => { | |||
| setTimeout(() => { | |||
| podLogContainer.value.reset(true); | |||
| }, 0); | |||
| }; | |||
| const logOptions = computed(() => { | |||
| return { | |||
| podName: state.actionModal.row?.podName, | |||
| namespace: `namespace-${props.createUserId}`, | |||
| }; | |||
| }); | |||
| const showLog = async (row) => { | |||
| showActionModal(row, 'log'); | |||
| resetLogger(); | |||
| }; | |||
| const showVisual = async (row, isContrast = true) => { | |||
| showActionModal(row, 'visual'); | |||
| const contrastRowIds = row.map((d) => d.id); | |||
| const contrastMetric = await expStageIntermediate(experimentId, stageOrder, contrastRowIds); | |||
| Object.assign(state, { | |||
| isContrast, | |||
| contrastChartData: extractSeriesData(contrastMetric), | |||
| contrastChartConfig: { | |||
| ...defaultConfig, | |||
| ...contrastMetric.config, | |||
| xAxis: { title: { text: contrastMetric.config.xFieldName } }, | |||
| yAxis: { title: { text: contrastMetric.config.yFieldName } }, | |||
| }, | |||
| }); | |||
| }; | |||
| const showSingleVisual = (row) => { | |||
| showVisual([{ ...row }], false); | |||
| }; | |||
| const showContrast = () => { | |||
| showVisual(proTable.value?.state.selectedRows); | |||
| }; | |||
| const resetActionModal = () => { | |||
| const keyName = state.actionModal.type.concat('Key'); | |||
| state[keyName] += 1; | |||
| Object.assign(state, { | |||
| actionModal: { | |||
| show: false, | |||
| row: undefined, | |||
| showOkLoading: false, | |||
| type: null, | |||
| }, | |||
| }); | |||
| }; | |||
| const handleCancel = () => { | |||
| resetActionModal(); | |||
| }; | |||
| const saveProd = () => { | |||
| Message.info(state.saveForm, 400); | |||
| handleCancel(); | |||
| }; | |||
| const columns = computed(() => [ | |||
| { | |||
| prop: 'selections', | |||
| type: 'selection', | |||
| }, | |||
| { | |||
| prop: 'sequence', | |||
| label: 'Sequence', | |||
| }, | |||
| { | |||
| prop: 'status', | |||
| label: '状态', | |||
| type: 'tag', | |||
| tagAttr: { | |||
| style: (col) => ({ | |||
| color: getTrialByCode(col.status, 'bgColor'), | |||
| borderColor: getTrialByCode(col.status, 'bgColor'), | |||
| }), | |||
| }, | |||
| formatter: (value) => getTrialByCode(value, 'label'), | |||
| dropdownList: allTrialStatusList, | |||
| }, | |||
| { | |||
| prop: 'executeScript', | |||
| label: '算法文件', | |||
| }, | |||
| { | |||
| prop: 'value', | |||
| label: 'accuracy', | |||
| width: '120px', | |||
| }, | |||
| { | |||
| prop: 'runTime', | |||
| label: '持续时间', | |||
| formatter: runTimeFormatter, | |||
| width: '240px', | |||
| }, | |||
| { | |||
| prop: 'startTime', | |||
| label: '开始时间', | |||
| width: '240px', | |||
| type: 'time', | |||
| }, | |||
| { | |||
| prop: 'resourceName', | |||
| label: '计算资源', | |||
| }, | |||
| { | |||
| label: '操作', | |||
| type: 'operation', | |||
| width: '370px', | |||
| fixed: 'right', | |||
| operations: [ | |||
| { | |||
| label: '可视化', | |||
| func: showSingleVisual, | |||
| }, | |||
| { | |||
| label: '查看日志', | |||
| func: showLog, | |||
| hideFunc(row) { | |||
| // 待运行无podname故不可查询k8s日志 | |||
| return [TRIAL_STATUS_MAP.toRun.value, TRIAL_STATUS_MAP.waiting.value].includes( | |||
| row.status | |||
| ); | |||
| }, | |||
| }, | |||
| ], | |||
| }, | |||
| ]); | |||
| watch( | |||
| () => props.activeTab, | |||
| () => { | |||
| proTable.value.refresh(); | |||
| } | |||
| ); | |||
| return { | |||
| contrastDisabled, | |||
| visualTitle, | |||
| experimentId, | |||
| stageOrder, | |||
| list, | |||
| listOptions, | |||
| state, | |||
| columns, | |||
| handleCancel, | |||
| showContrast, | |||
| proTable, | |||
| isEmpty, | |||
| saveProd, | |||
| getPodLog, | |||
| logOptions, | |||
| podLogContainer, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss"> | |||
| .trialLogModal { | |||
| .prism-content { | |||
| max-height: 350px; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,513 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="app-container greyBg"> | |||
| <div class="detail-header"> | |||
| <DetailDashboard | |||
| :activePath="state.activePath" | |||
| :detail="state.detail" | |||
| :isFinished="isFinished" | |||
| :inProgress="inProgress" | |||
| :enablePause="enablePause" | |||
| :enableStart="enableStart" | |||
| :refreshTime="state.refreshTime" | |||
| :saveRefreshTime="saveRefreshTime" | |||
| :updateState="updateState" | |||
| :refresh="refresh" | |||
| :command="command" | |||
| /> | |||
| <Config | |||
| :toggleSearchSpace="toggleSearchSpace" | |||
| :toggleSelectedSpace="toggleSelectedSpace" | |||
| :toggleExpConfig="toggleExpConfig" | |||
| /> | |||
| </div> | |||
| <el-drawer title="Search Space" :visible.sync="state.searchSpaceVisible"> | |||
| <TextEditor :txt="state.searchSpace" class="my-auto f1" style="max-height: unset" /> | |||
| </el-drawer> | |||
| <el-drawer title="Best Selected Space" :visible.sync="state.selectedSpaceVisible"> | |||
| <TextEditor :txt="state.selectedSpace" class="my-auto f1" style="max-height: unset" /> | |||
| </el-drawer> | |||
| <el-drawer title="Experiment Config" :visible.sync="state.expConfigVisible"> | |||
| <TextEditor :txt="state.expConfig" class="my-auto f1" style="max-height: unset" /> | |||
| </el-drawer> | |||
| <div class="stage-content"> | |||
| <el-tabs | |||
| v-model="state.activeStage" | |||
| class="stage-tabs el-tabs-large" | |||
| type="card" | |||
| @tab-click="changeTab" | |||
| > | |||
| <el-tab-pane label="TRAIN" name="TRAIN" /> | |||
| <el-tab-pane label="SELECT" name="SELECT" /> | |||
| <el-tab-pane label="RETRAIN" name="RETRAIN" /> | |||
| </el-tabs> | |||
| <el-card v-if="state.activePath[0] === 'LOG'"> | |||
| <div class="mb-10">实验日志</div> | |||
| <LogContainer | |||
| ref="logContainer" | |||
| class="mt-20" | |||
| :log-getter="getExpLog" | |||
| :options="logOptions" | |||
| :log-lines="50" | |||
| /> | |||
| </el-card> | |||
| <Stage | |||
| v-else | |||
| :activePath="state.activePath" | |||
| :detail="state.detail" | |||
| :experimentId="experimentId" | |||
| :configMap="state.configMap" | |||
| :info="stageInfo" | |||
| :param="stageParam" | |||
| :runParam="stageRunParam" | |||
| :metric="stageMetric" | |||
| :updateState="updateState" | |||
| :refresh="refresh" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { reactive, computed, onMounted, watch, ref } from '@vue/composition-api'; | |||
| import { useLocalStorage } from '@/hooks'; | |||
| import { | |||
| expDetailOverview, | |||
| expStageInfo, | |||
| expStageParam, | |||
| expStageRuntimeParam, | |||
| getSearchSpace, | |||
| getSelectedSpace, | |||
| getExpConfig, | |||
| getExpLog, | |||
| expYaml, | |||
| expStageAccuracy, | |||
| expStageIntermediate, | |||
| expStageRuntime, | |||
| } from '@/api/tadl'; | |||
| import TextEditor from '@/components/textEditor'; | |||
| import LogContainer from '@/components/LogContainer'; | |||
| import { | |||
| expInprogress, | |||
| expIsFinished, | |||
| expEnablePause, | |||
| getStageName, | |||
| getStageOrder, | |||
| expEnableStart, | |||
| extractData, | |||
| extractScatterData, | |||
| extractSeriesData, | |||
| } from '../util'; | |||
| import DetailDashboard from './components/detailDashboard'; | |||
| import Config from './components/config'; | |||
| import Stage from './stage'; | |||
| export default { | |||
| name: 'DetailContainer', | |||
| components: { | |||
| DetailDashboard, | |||
| Config, | |||
| TextEditor, | |||
| Stage, | |||
| LogContainer, | |||
| }, | |||
| setup(props, ctx) { | |||
| const { $route } = ctx.root; | |||
| const { params = {} } = $route; | |||
| const [refreshTime, saveRefreshTime] = useLocalStorage('refreshTime', 10); | |||
| const { experimentId } = params; | |||
| const logContainer = ref(null); | |||
| const state = reactive({ | |||
| activePath: ['TRAIN', 'general'], | |||
| detail: {}, | |||
| loading: false, | |||
| error: null, | |||
| activeStage: 'TRAIN', // 当前所处阶段 | |||
| prevActiveStage: 'TRAIN', | |||
| stageInfoMap: {}, | |||
| stageParamMap: {}, | |||
| stageRunParamMap: {}, | |||
| configMap: {}, // 实验配置参数 | |||
| stageYamlMap: {}, // 分阶段 yaml 配置 | |||
| stageMetricMap: {}, // 基本Metric 展示 | |||
| refreshTime, // 刷新时间 | |||
| searchSpace: '', | |||
| selectedSpace: '', | |||
| expConfig: '', | |||
| searchSpaceVisible: false, | |||
| selectedSpaceVisible: false, | |||
| expConfigVisible: false, | |||
| algrithomLog: '', | |||
| systemLog: '', | |||
| }); | |||
| // 判断实验状态 | |||
| const isFinished = computed(() => expIsFinished(state.detail.status)); | |||
| const inProgress = computed(() => expInprogress(state.detail.status)); | |||
| const enablePause = computed(() => expEnablePause(state.detail.status)); | |||
| const enableStart = computed(() => expEnableStart(state.detail.status)); | |||
| const activeStageName = computed(() => state.activePath[0]); | |||
| // 阶段概览 | |||
| const stageInfo = computed(() => state.stageInfoMap[activeStageName.value]); | |||
| // 阶段参数概览 | |||
| const stageParam = computed(() => state.stageParamMap[activeStageName.value]); | |||
| // 阶段运行参数 | |||
| const stageRunParam = computed(() => state.stageRunParamMap[activeStageName.value]); | |||
| // 分阶段 yaml 配置 | |||
| const stageYaml = computed(() => state.stageYamlMap[activeStageName.value]); | |||
| // 阶段输出数据 | |||
| const stageMetric = computed(() => state.stageMetricMap[activeStageName.value]); | |||
| const updateState = (params) => { | |||
| return new Promise((resolve) => { | |||
| // 区分函数式更新和对象更新 | |||
| if (typeof params === 'function') { | |||
| const next = params(state); | |||
| Object.assign(state, next); | |||
| resolve(state); | |||
| } | |||
| // 普通更新 | |||
| Object.assign(state, params); | |||
| resolve(state); | |||
| }); | |||
| }; | |||
| // 查询实验详情 | |||
| const queryExpDetail = async () => { | |||
| const detail = await expDetailOverview(experimentId); | |||
| updateState({ | |||
| detail, | |||
| activeStage: getStageName(detail.runStage || 1), | |||
| activePath: [getStageName(detail.runStage || 1), 'general'], | |||
| }); | |||
| return detail; | |||
| }; | |||
| // 更新各个阶段详情 | |||
| const updateStateBy = (stageName, key, value) => { | |||
| updateState((state) => { | |||
| const next = { | |||
| ...state[key], | |||
| [stageName]: value, | |||
| }; | |||
| return { | |||
| ...state, | |||
| [key]: next, | |||
| }; | |||
| }); | |||
| }; | |||
| // 查询实验阶段信息 | |||
| const queryExpStageInfo = async ({ stageOrder }) => { | |||
| const stageInfo = await expStageInfo(experimentId, stageOrder); | |||
| const stageName = getStageName(stageOrder); | |||
| updateStateBy(stageName, 'stageInfoMap', stageInfo); | |||
| }; | |||
| // 查询实验阶段运行参数 | |||
| const queryExpStageParam = async ({ stageOrder }) => { | |||
| const stageParam = await expStageParam(experimentId, stageOrder); | |||
| const stageName = getStageName(stageOrder); | |||
| updateStateBy(stageName, 'stageParamMap', stageParam); | |||
| }; | |||
| // 查询实验参数 | |||
| const queryExpStageRuntimeParam = async ({ stageOrder }) => { | |||
| const stageRunParam = await expStageRuntimeParam(experimentId, stageOrder); | |||
| const stageName = getStageName(stageOrder); | |||
| updateStateBy(stageName, 'stageRunParamMap', stageRunParam); | |||
| }; | |||
| const queryExpYaml = async ({ stageOrder }) => { | |||
| const stageYaml = await expYaml(experimentId, stageOrder); | |||
| const stageName = getStageName(stageOrder); | |||
| updateStateBy(stageName, 'stageYamlMap', stageYaml); | |||
| }; | |||
| const defaultConfig = { | |||
| autoFit: true, | |||
| seriesField: null, | |||
| smooth: false, // 平滑曲线 | |||
| meta: { | |||
| value: { | |||
| // max: 21, // 坐标轴限定值范围 | |||
| }, | |||
| }, | |||
| xAxis: { | |||
| title: { | |||
| text: 'x轴', | |||
| spacing: 30, | |||
| style: { | |||
| fontSize: 20, | |||
| }, | |||
| }, | |||
| }, | |||
| yAxis: { | |||
| title: { | |||
| text: 'y轴', | |||
| style: { | |||
| fontSize: 20, | |||
| }, | |||
| }, | |||
| }, | |||
| }; | |||
| const scatterConfig = { | |||
| regressionLine: { | |||
| type: 'loess', | |||
| }, | |||
| }; | |||
| // 查询最佳精度图数据 | |||
| // 查询运行中间值图数据 | |||
| // 查询运行时间图数据 | |||
| const queryStageMetric = async ({ stageOrder }) => { | |||
| const rawAccuracy = await expStageAccuracy(experimentId, stageOrder); | |||
| const rawIntermediate = await expStageIntermediate(experimentId, stageOrder); | |||
| const rawRuntime = await expStageRuntime(experimentId, stageOrder); | |||
| const stageName = getStageName(stageOrder); | |||
| updateStateBy(stageName, 'stageMetricMap', { | |||
| accuracyData: extractData(rawAccuracy), | |||
| accuracyConfig: { | |||
| ...defaultConfig, | |||
| ...rawAccuracy.config, | |||
| xAxis: { title: { text: rawAccuracy.config.xFieldName }, tickInterval: 1 }, | |||
| yAxis: { title: { text: rawAccuracy.config.yFieldName } }, | |||
| }, | |||
| accuracyScatterData: extractScatterData(rawAccuracy), | |||
| accuracyScatterConfig: { | |||
| ...defaultConfig, | |||
| ...scatterConfig, | |||
| ...rawAccuracy.config, | |||
| xAxis: { title: { text: rawAccuracy.config.xFieldName }, tickInterval: 1 }, | |||
| yAxis: { title: { text: rawAccuracy.config.yFieldName }, min: 0 }, | |||
| }, | |||
| intermediateData: extractSeriesData(rawIntermediate), | |||
| intermediateConfig: { | |||
| ...defaultConfig, | |||
| ...rawIntermediate.config, | |||
| xAxis: { title: { text: rawIntermediate.config.xFieldName }, tickInterval: 1 }, | |||
| yAxis: { title: { text: rawIntermediate.config.yFieldName } }, | |||
| }, | |||
| runtimeData: extractData(rawRuntime), | |||
| runtimeConfig: { | |||
| ...defaultConfig, | |||
| ...rawRuntime.config, | |||
| xAxis: { title: { text: 'trial' }, tickInterval: 1 }, | |||
| yAxis: { title: { text: '运行时间/min' } }, | |||
| }, | |||
| }); | |||
| }; | |||
| const queryStageInfo = (params) => { | |||
| Promise.all([ | |||
| queryExpStageInfo(params), | |||
| queryExpStageParam(params), | |||
| queryExpStageRuntimeParam(params), | |||
| queryExpYaml(params), | |||
| queryStageMetric(params), | |||
| ]); | |||
| }; | |||
| const refresh = async () => { | |||
| const { runStage } = await queryExpDetail(); | |||
| queryStageInfo({ stageOrder: runStage || 1 }); | |||
| }; | |||
| // TODO | |||
| // 获取实验相关配置 | |||
| // const queryExpConfig = async () => { | |||
| // const configMap = await getExpConfig(experimentId); | |||
| // updateState((state) => { | |||
| // return { | |||
| // ...state, | |||
| // configMap, | |||
| // }; | |||
| // }); | |||
| // }; | |||
| const toggleSearchSpace = async () => { | |||
| const result = await getSearchSpace(experimentId).then((res) => JSON.parse(res.fileStr)); | |||
| state.searchSpaceVisible = !state.searchSpaceVisible; | |||
| state.searchSpace = result; | |||
| }; | |||
| const toggleSelectedSpace = async () => { | |||
| const result = await getSelectedSpace(experimentId).then((res) => JSON.parse(res.fileStr)); | |||
| state.selectedSpaceVisible = !state.selectedSpaceVisible; | |||
| state.selectedSpace = result; | |||
| }; | |||
| const toggleExpConfig = async () => { | |||
| const result = await getExpConfig(experimentId).then((res) => JSON.parse(res.fileStr)); | |||
| state.expConfigVisible = !state.expConfigVisible; | |||
| state.expConfig = result; | |||
| }; | |||
| const logOptions = computed(() => { | |||
| return { | |||
| experimentId, | |||
| }; | |||
| }); | |||
| const resetLogger = () => { | |||
| setTimeout(() => { | |||
| logContainer.value.reset(true); | |||
| }, 0); | |||
| }; | |||
| const setRefresher = (time) => { | |||
| if (time > 0) { | |||
| return setInterval(() => { | |||
| refresh(); | |||
| }, time * 1000); | |||
| } | |||
| return false; | |||
| }; | |||
| let refresher = setRefresher(props.refreshTime); | |||
| const command = (cmd) => { | |||
| saveRefreshTime(cmd); | |||
| updateState({ refreshTime: cmd }); | |||
| clearInterval(refresher); | |||
| refresher = setRefresher(cmd); | |||
| }; | |||
| const changeTab = (tab) => { | |||
| Object.assign(state, { | |||
| prevActiveStage: tab.name, | |||
| }); | |||
| command(0); | |||
| updateState({ activePath: [tab.name, 'general'] }); | |||
| }; | |||
| // 监听阶段变更 | |||
| watch( | |||
| () => state.activePath[0], | |||
| (next) => { | |||
| if (next === 'LOG') { | |||
| resetLogger(); | |||
| return; | |||
| } | |||
| const stageOrder = getStageOrder(next); | |||
| queryStageInfo({ stageOrder }); | |||
| } | |||
| ); | |||
| onMounted(() => { | |||
| refresh(); | |||
| // queryExpConfig(); | |||
| }); | |||
| return { | |||
| state, | |||
| isFinished, | |||
| inProgress, | |||
| enablePause, | |||
| enableStart, | |||
| updateState, | |||
| stageInfo, | |||
| stageParam, | |||
| stageRunParam, | |||
| stageYaml, | |||
| stageMetric, | |||
| refresh, | |||
| saveRefreshTime, | |||
| experimentId, | |||
| toggleSearchSpace, | |||
| toggleSelectedSpace, | |||
| toggleExpConfig, | |||
| getExpLog, | |||
| logOptions, | |||
| logContainer, | |||
| command, | |||
| changeTab, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss"> | |||
| @import '@/assets/styles/variables.scss'; | |||
| .stage-content { | |||
| margin: 30px 0; | |||
| .stage-tabs { | |||
| .el-tabs__header { | |||
| border: none; | |||
| margin: 0; | |||
| } | |||
| .el-tabs__nav { | |||
| border: none; | |||
| } | |||
| .el-tabs__item.is-active { | |||
| background-color: #fff; | |||
| color: $primaryColor; | |||
| } | |||
| .el-tabs__item { | |||
| background-color: $primaryColor; | |||
| color: #fff; | |||
| margin-left: 2px; | |||
| margin-right: 10px; | |||
| border-top-left-radius: 6px; | |||
| border-top-right-radius: 6px; | |||
| } | |||
| } | |||
| .app-content-section { | |||
| margin-bottom: 30px; | |||
| } | |||
| .stage-card { | |||
| .el-tabs__header { | |||
| margin: 0; | |||
| } | |||
| .el-tabs__nav-wrap { | |||
| background-color: #fff; | |||
| padding-left: 16px; | |||
| margin-bottom: 10px; | |||
| &::after { | |||
| height: 0; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .app-content-title { | |||
| color: rgba(0, 0, 0, 0.85); | |||
| font-weight: 500; | |||
| font-size: 18px; | |||
| line-height: 28px; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| white-space: nowrap; | |||
| } | |||
| .app-descriptions-header { | |||
| display: flex; | |||
| align-items: center; | |||
| margin-bottom: 20px; | |||
| .app-descriptions-title { | |||
| flex: auto; | |||
| overflow: hidden; | |||
| color: rgba(0, 0, 0, 0.85); | |||
| font-weight: 700; | |||
| font-size: 16px; | |||
| line-height: 1.5715; | |||
| white-space: nowrap; | |||
| text-overflow: ellipsis; | |||
| } | |||
| } | |||
| .app-container { | |||
| padding: 30px 32px; | |||
| .detail-header { | |||
| box-shadow: 0px 2px 7px 0px rgba(209, 209, 217, 0.5); | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,141 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-tabs v-model="state.activeTab" class="stage-card el-tabs-large" @tab-click="changeTab"> | |||
| <el-tab-pane label="概 览" name="general"> | |||
| <Parameter :param="param" :progress="progress" :experimentId="experimentId" :stage="stage" /> | |||
| <el-row :gutter="16"> | |||
| <el-col :span="12"> | |||
| <General :info="info" :stage="stage" :isOneTrial="isOneTrial" /> | |||
| </el-col> | |||
| <el-col :span="12"> | |||
| <RunParameter | |||
| :param="runParam" | |||
| :isOneTrial="isOneTrial" | |||
| :experimentId="experimentId" | |||
| :stage="stage" | |||
| :refresh="refresh" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| </el-tab-pane> | |||
| <el-tab-pane label="Trial 列表" name="trials"> | |||
| <el-row :gutter="20" class="mb-20"> | |||
| <el-col :span="12"> | |||
| <ChartCard | |||
| title="最佳精度" | |||
| type="ScatterChart" | |||
| :chartConfig="metric.accuracyScatterConfig" | |||
| :chartData="metric.accuracyScatterData" | |||
| /> | |||
| </el-col> | |||
| <el-col :span="12"> | |||
| <ChartCard | |||
| title="运行中间值" | |||
| type="LineChart" | |||
| :chartConfig="metric.intermediateConfig" | |||
| :chartData="metric.intermediateData" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| <el-row :gutter="20" class="mb-20"> | |||
| <el-col :span="12"> | |||
| <ChartCard | |||
| title="运行时间" | |||
| type="ColumnChart" | |||
| :chartConfig="metric.runtimeConfig" | |||
| :chartData="metric.runtimeData" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| <div class="dib"> | |||
| <TrialsList | |||
| :stage="stage" | |||
| :activeTab="state.activeTab" | |||
| contrastTitle="trial对比" | |||
| :createUserId="detail.createUserId" | |||
| /> | |||
| </div> | |||
| </el-tab-pane> | |||
| </el-tabs> | |||
| </template> | |||
| <script> | |||
| import { reactive } from '@vue/composition-api'; | |||
| import General from './components/general'; | |||
| import Parameter from './components/parameter'; | |||
| import RunParameter from './components/runParameter'; | |||
| import TrialsList from './components/trialsList'; | |||
| import ChartCard from './components/chartCard'; | |||
| export default { | |||
| name: 'TRAIN', | |||
| components: { | |||
| General, | |||
| Parameter, | |||
| RunParameter, | |||
| TrialsList, | |||
| ChartCard, | |||
| }, | |||
| props: { | |||
| activeTab: String, | |||
| stage: String, | |||
| // 阶段概览 | |||
| info: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| detail: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| experimentId: String, | |||
| // 阶段输出度量 | |||
| metric: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 参数 | |||
| param: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 运行参数 | |||
| runParam: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| updateState: Function, | |||
| // 实验阶段 | |||
| progress: Number, | |||
| // 是否为单一 trial | |||
| isOneTrial: Boolean, | |||
| refresh: Function, | |||
| }, | |||
| setup(props) { | |||
| const state = reactive({ | |||
| activeTab: props.activeTab, | |||
| }); | |||
| const changeTab = (tab) => { | |||
| if (tab.name === state.prevActiveTab) return; | |||
| Object.assign(state, { | |||
| activeTab: tab.name, | |||
| prevActiveTab: tab.name, | |||
| }); | |||
| props.updateState({ activePath: ['RETRAIN', tab.name] }); | |||
| }; | |||
| return { | |||
| changeTab, | |||
| state, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,130 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-tabs v-model="state.activeTab" class="stage-card el-tabs-large" @tab-click="changeTab"> | |||
| <el-tab-pane label="概 览" name="general"> | |||
| <Parameter :param="param" :progress="progress" :experimentId="experimentId" :stage="stage" /> | |||
| <el-row :gutter="16"> | |||
| <el-col :span="12"> | |||
| <General :info="info" :stage="stage" :isOneTrial="isOneTrial" /> | |||
| </el-col> | |||
| <el-col :span="12"> | |||
| <RunParameter | |||
| :param="runParam" | |||
| :experimentId="experimentId" | |||
| :stage="stage" | |||
| :refresh="refresh" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| </el-tab-pane> | |||
| <el-tab-pane label="Trial 列表" name="trials"> | |||
| <el-row :gutter="20" class="mb-20"> | |||
| <el-col :span="12"> | |||
| <ChartCard | |||
| title="运行中间值" | |||
| type="LineChart" | |||
| :chartConfig="metric.intermediateConfig" | |||
| :chartData="metric.intermediateData" | |||
| /> | |||
| </el-col> | |||
| <el-col :span="12"> | |||
| <ChartCard | |||
| title="运行时间" | |||
| type="ColumnChart" | |||
| :chartConfig="metric.runtimeConfig" | |||
| :chartData="metric.runtimeData" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| <div class="dib"> | |||
| <TrialsList | |||
| :stage="stage" | |||
| :activeTab="state.activeTab" | |||
| contrastTitle="trial对比" | |||
| :createUserId="detail.createUserId" | |||
| /> | |||
| </div> | |||
| </el-tab-pane> | |||
| </el-tabs> | |||
| </template> | |||
| <script> | |||
| import { reactive } from '@vue/composition-api'; | |||
| import General from './components/general'; | |||
| import Parameter from './components/parameter'; | |||
| import RunParameter from './components/runParameter'; | |||
| import TrialsList from './components/trialsList'; | |||
| import ChartCard from './components/chartCard'; | |||
| export default { | |||
| name: 'SELECT', | |||
| components: { | |||
| General, | |||
| Parameter, | |||
| RunParameter, | |||
| TrialsList, | |||
| ChartCard, | |||
| }, | |||
| props: { | |||
| activeTab: String, | |||
| stage: String, | |||
| // 阶段概览 | |||
| info: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| detail: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| experimentId: String, | |||
| // 阶段输出度量 | |||
| metric: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 参数 | |||
| param: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 运行参数 | |||
| runParam: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| updateState: Function, | |||
| // 实验阶段 | |||
| progress: Number, | |||
| // 是否为单一 trial | |||
| isOneTrial: Boolean, | |||
| refresh: Function, | |||
| }, | |||
| setup(props) { | |||
| const state = reactive({ | |||
| activeTab: props.activeTab, | |||
| }); | |||
| const changeTab = (tab) => { | |||
| if (tab.name === state.prevActiveTab) return; | |||
| Object.assign(state, { | |||
| activeTab: tab.name, | |||
| prevActiveTab: tab.name, | |||
| }); | |||
| props.updateState({ activePath: ['SELECT', tab.name] }); | |||
| }; | |||
| return { | |||
| changeTab, | |||
| state, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,71 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-card> | |||
| <component | |||
| :is="stage" | |||
| :stage="stage" | |||
| :activeTab="activeTab" | |||
| :config="config" | |||
| :isOneTrial="isOneTrial" | |||
| :progress="stageProgress" | |||
| :refresh="refresh" | |||
| :detail="detail" | |||
| v-bind="attrs" | |||
| /> | |||
| </el-card> | |||
| </template> | |||
| <script> | |||
| import { computed } from '@vue/composition-api'; | |||
| import { getStageOrder } from '../util'; | |||
| import TRAIN from './train'; | |||
| import SELECT from './select'; | |||
| import RETRAIN from './retrain'; | |||
| export default { | |||
| name: 'Stage', | |||
| components: { | |||
| TRAIN, | |||
| SELECT, | |||
| RETRAIN, | |||
| }, | |||
| props: { | |||
| activePath: [Array, String], | |||
| detail: Object, | |||
| configMap: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| refresh: Function, | |||
| }, | |||
| setup(props, ctx) { | |||
| const stage = computed(() => props.activePath[0]); | |||
| const attrs = computed(() => ctx.attrs); | |||
| const activeTab = computed(() => props.activePath[1]); | |||
| const sequence = computed(() => getStageOrder(stage)); | |||
| // 0: 当前阶段,- 已完成,+ 未完成 | |||
| const stageProgress = computed(() => sequence - props.detail.stageOrder || 0); | |||
| // 算法配置 | |||
| const config = computed(() => props.configMap[stage.value.toLowerCase()]); | |||
| // 是否为单一 trial | |||
| const isOneTrial = computed(() => config.maxTrialNum === 1); | |||
| return { | |||
| stage, | |||
| attrs, | |||
| sequence, | |||
| activeTab, | |||
| stageProgress, | |||
| config, | |||
| isOneTrial, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,144 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-tabs v-model="state.activeTab" class="stage-card el-tabs-large" @tab-click="changeTab"> | |||
| <el-tab-pane label="概 览" name="general"> | |||
| <Parameter :param="param" :progress="progress" :experimentId="experimentId" :stage="stage" /> | |||
| <el-row :gutter="16"> | |||
| <el-col :span="12"> | |||
| <General :info="info" :stage="stage" :isOneTrial="isOneTrial" /> | |||
| </el-col> | |||
| <el-col :span="12"> | |||
| <RunParameter | |||
| :param="runParam" | |||
| :isOneTrial="isOneTrial" | |||
| :experimentId="experimentId" | |||
| :stage="stage" | |||
| :refresh="refresh" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| </el-tab-pane> | |||
| <el-tab-pane label="Trial 列表" name="trials"> | |||
| <el-row :gutter="20" class="mb-20"> | |||
| <el-col :span="12"> | |||
| <ChartCard | |||
| title="最佳精度" | |||
| type="ScatterChart" | |||
| :chartConfig="metric.accuracyScatterConfig" | |||
| :chartData="metric.accuracyScatterData" | |||
| /> | |||
| </el-col> | |||
| <el-col :span="12"> | |||
| <ChartCard | |||
| title="运行中间值" | |||
| type="LineChart" | |||
| :chartConfig="metric.intermediateConfig" | |||
| :chartData="metric.intermediateData" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| <el-row :gutter="20" class="mb-20"> | |||
| <el-col :span="12"> | |||
| <ChartCard | |||
| title="运行时间" | |||
| type="ColumnChart" | |||
| :chartConfig="metric.runtimeConfig" | |||
| :chartData="metric.runtimeData" | |||
| /> | |||
| </el-col> | |||
| </el-row> | |||
| <div class="dib"> | |||
| <TrialsList | |||
| :stage="stage" | |||
| :activeTab="state.activeTab" | |||
| contrastTitle="trial对比" | |||
| :createUserId="detail.createUserId" | |||
| /> | |||
| </div> | |||
| </el-tab-pane> | |||
| </el-tabs> | |||
| </template> | |||
| <script> | |||
| import { reactive } from '@vue/composition-api'; | |||
| import General from './components/general'; | |||
| import Parameter from './components/parameter'; | |||
| import RunParameter from './components/runParameter'; | |||
| import TrialsList from './components/trialsList'; | |||
| import ChartCard from './components/chartCard'; | |||
| export default { | |||
| name: 'TRAIN', | |||
| components: { | |||
| General, | |||
| Parameter, | |||
| RunParameter, | |||
| TrialsList, | |||
| ChartCard, | |||
| }, | |||
| props: { | |||
| activeTab: String, | |||
| stage: String, | |||
| // 阶段概览 | |||
| info: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| detail: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| experimentId: String, | |||
| // 阶段输出度量 | |||
| metric: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 参数 | |||
| param: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 运行参数 | |||
| runParam: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| updateState: Function, | |||
| // 实验阶段 | |||
| progress: Number, | |||
| // 是否为单一 trial | |||
| isOneTrial: Boolean, | |||
| // 算法配置 | |||
| config: Object, | |||
| refresh: Function, | |||
| }, | |||
| setup(props) { | |||
| const state = reactive({ | |||
| activeTab: props.activeTab, | |||
| prevActiveTab: props.activeTab, | |||
| }); | |||
| const changeTab = (tab) => { | |||
| if (tab.name === state.prevActiveTab) return; | |||
| Object.assign(state, { | |||
| activeTab: tab.name, | |||
| prevActiveTab: tab.name, | |||
| }); | |||
| props.updateState({ activePath: ['TRAIN', tab.name] }); | |||
| }; | |||
| return { | |||
| changeTab, | |||
| state, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,24 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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 { TRIAL_STATUS_MAP } from '../util'; | |||
| export const allTrialStatusList = [{ label: '全部', value: null }].concat( | |||
| Object.values(TRIAL_STATUS_MAP).map((status) => ({ | |||
| label: status.label, | |||
| value: status.value, | |||
| })) | |||
| ); | |||
| @@ -0,0 +1,406 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> | |||
| <div class="area-title">基本信息</div> | |||
| <el-form-item label="实验名称" prop="name"> | |||
| <el-input | |||
| v-model.trim="form.name" | |||
| maxlength="32" | |||
| show-word-limit | |||
| placeholder="请输入实验名称" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="实验描述" prop="description"> | |||
| <el-input | |||
| v-model="form.description" | |||
| type="textarea" | |||
| :rows="4" | |||
| maxlength="200" | |||
| show-word-limit | |||
| placeholder="请输入实验描述" | |||
| /> | |||
| </el-form-item> | |||
| <div class="area-title">搜索策略</div> | |||
| <el-form-item ref="algorithmVersionIdRef" label="选择搜索策略" prop="algorithmVersionId"> | |||
| <el-select | |||
| v-model="form.algorithmId" | |||
| placeholder="spos" | |||
| clearable | |||
| @change="onAlgorithmIdChange" | |||
| > | |||
| <el-option | |||
| v-for="item in algorithmList" | |||
| :key="item.id" | |||
| :value="item.id" | |||
| :label="item.name" | |||
| /> | |||
| </el-select> | |||
| <el-select | |||
| v-model="form.algorithmVersionId" | |||
| placeholder="选择版本" | |||
| clearable | |||
| @change="onAlgorithmVersionChange" | |||
| > | |||
| <el-option | |||
| v-for="item in algorithmVersionList" | |||
| :key="item.id" | |||
| :value="item.id" | |||
| :label="item.versionName || '最新'" | |||
| /> | |||
| </el-select> | |||
| <BaseTooltip content="只支持预置搜索策略" /> | |||
| </el-form-item> | |||
| <el-tabs v-model="stageTab" class="eltabs-inlineblock mb-20" @tab-click="onTabsChange"> | |||
| <el-tab-pane label="TRAIN" :name="String(STAGE_SEQUENCE.TRAIN)" /> | |||
| <el-tab-pane label="SELECT" :name="String(STAGE_SEQUENCE.SELECT)" /> | |||
| <el-tab-pane label="RETRAIN" :name="String(STAGE_SEQUENCE.RETRAIN)" /> | |||
| </el-tabs> | |||
| <transition-group ref="tabPane" :name="transition" tag="div"> | |||
| <TadlStageForm | |||
| v-show="stageTab === String(STAGE_SEQUENCE.TRAIN)" | |||
| ref="trainStageForm" | |||
| :key="STAGE_SEQUENCE.TRAIN" | |||
| class="tab-form" | |||
| :model="form.stage[0]" | |||
| :use-gpu="useGpu" | |||
| @resource-change="onResourceChange" | |||
| /> | |||
| <TadlStageForm | |||
| v-show="stageTab === String(STAGE_SEQUENCE.SELECT)" | |||
| ref="selectStageForm" | |||
| :key="STAGE_SEQUENCE.SELECT" | |||
| class="tab-form" | |||
| :model="form.stage[1]" | |||
| :use-gpu="useGpu" | |||
| @resource-change="onResourceChange" | |||
| /> | |||
| <TadlStageForm | |||
| v-show="stageTab === String(STAGE_SEQUENCE.RETRAIN)" | |||
| :key="STAGE_SEQUENCE.RETRAIN" | |||
| ref="retrainStageForm" | |||
| class="tab-form" | |||
| :model="form.stage[2]" | |||
| :use-gpu="useGpu" | |||
| @resource-change="onResourceChange" | |||
| /> | |||
| </transition-group> | |||
| </el-form> | |||
| </template> | |||
| <script> | |||
| import { Message } from 'element-ui'; | |||
| import { nextTick, reactive, ref, toRefs, watch } from '@vue/composition-api'; | |||
| import { isNil } from 'lodash'; | |||
| import { getStrategyList, checkStrategy } from '@/api/tadl/strategy'; | |||
| import BaseTooltip from '@/components/BaseTooltip'; | |||
| import { validateNameWithHyphen } from '@/utils'; | |||
| import TadlStageForm from './tadlStageForm'; | |||
| import { defaultStageForm } from '../utils'; | |||
| import { STAGE_SEQUENCE } from '../../util'; | |||
| const defaultForm = { | |||
| id: null, // 实验 ID | |||
| modelType: null, // 模型类型 | |||
| name: null, // 实验名称 | |||
| description: null, // 实验描述 | |||
| algorithmId: null, // 算法 ID | |||
| algorithmVersionId: null, // 算法版本名称 | |||
| stage: [], // 阶段信息 | |||
| }; | |||
| export default { | |||
| name: 'TadlForm', | |||
| components: { | |||
| BaseTooltip, | |||
| TadlStageForm, | |||
| }, | |||
| setup() { | |||
| // 表单 ref | |||
| const formRef = ref(null); | |||
| const trainStageForm = ref(null); | |||
| const selectStageForm = ref(null); | |||
| const retrainStageForm = ref(null); | |||
| const algorithmVersionIdRef = ref(null); | |||
| const state = reactive({ | |||
| stageTab: String(STAGE_SEQUENCE.TRAIN), | |||
| transition: 'tabRight', | |||
| algorithmList: [], | |||
| algorithmVersionList: [], | |||
| useGpu: false, | |||
| }); | |||
| // 表单值 | |||
| const form = reactive({ ...defaultForm }); | |||
| // 枚举stage表单ref | |||
| const stageRefs = { | |||
| [STAGE_SEQUENCE.TRAIN]: trainStageForm, | |||
| [STAGE_SEQUENCE.SELECT]: selectStageForm, | |||
| [STAGE_SEQUENCE.RETRAIN]: retrainStageForm, | |||
| }; | |||
| // rules | |||
| const rules = { | |||
| name: [ | |||
| { required: true, message: '请输入实验名称', trigger: 'blur' }, | |||
| { | |||
| max: 32, | |||
| message: '长度在 32 个字符以内', | |||
| trigger: 'blur', | |||
| }, | |||
| { | |||
| validator: validateNameWithHyphen, | |||
| trigger: ['blur', 'change'], | |||
| }, | |||
| ], | |||
| algorithmVersionId: [ | |||
| { | |||
| required: true, | |||
| trigger: 'manual', | |||
| validator: (rule, value, callback) => { | |||
| if (!form.algorithmId) { | |||
| callback(new Error('请选择搜索策略')); | |||
| } | |||
| if (!form.algorithmVersionId) { | |||
| callback(new Error('请选择策略版本')); | |||
| } | |||
| callback(); | |||
| }, | |||
| }, | |||
| ], | |||
| }; | |||
| // 算法选择处理 | |||
| const onAlgorithmIdChange = (id, keepValue) => { | |||
| const algorithm = state.algorithmList.find((algorithm) => algorithm.id === id); | |||
| if (!algorithm) { | |||
| state.algorithmVersionList = []; | |||
| form.algorithmVersionId = null; | |||
| form.modelType = null; | |||
| return; | |||
| } | |||
| state.algorithmVersionList = algorithm.algorithmVersionVOList.filter( | |||
| (version) => version.versionName | |||
| ); | |||
| state.useGpu = algorithm.gpu; | |||
| form.modelType = algorithm.modelType; | |||
| if (!keepValue || !form.algorithmVersionId) { | |||
| form.algorithmVersionId = null; | |||
| return; | |||
| } | |||
| const version = state.algorithmVersionList.find( | |||
| (version) => version.id === form.algorithmVersionId | |||
| ); | |||
| if (!version) { | |||
| form.algorithmVersionId = null; | |||
| Message.warning('原有策略版本不存在,请重新选择'); | |||
| } | |||
| }; | |||
| // 获取算法列表 | |||
| const getStrategyInfo = async (keepValue = false) => { | |||
| state.algorithmList = await getStrategyList(); | |||
| if (!keepValue || !form.algorithmId) { | |||
| form.algorithmId = form.algorithmVersionId = null; | |||
| } else { | |||
| const algorithm = state.algorithmList.find((info) => info.id === form.algorithmId); | |||
| if (!algorithm) { | |||
| Message.warning('原有策略不存在,请重新选择'); | |||
| form.algorithmId = form.algorithmVersionId = null; | |||
| return; | |||
| } | |||
| onAlgorithmIdChange(algorithm.id, true); | |||
| } | |||
| }; | |||
| const onAlgorithmVersionChange = async (algorithmVersionId) => { | |||
| if (algorithmVersionId) { | |||
| // 查询查看接口, 回填阶段值 | |||
| const { stage } = await checkStrategy({ algorithmVersionId }, form.algorithmId); | |||
| stage.forEach((order, index) => { | |||
| // 由子表单负责确保不会将无用字段带入 | |||
| Object.assign(form.stage[index], defaultStageForm, order); | |||
| nextTick(() => { | |||
| stageRefs[order.stageOrder].value.initForm(); | |||
| }); | |||
| }); | |||
| } | |||
| algorithmVersionIdRef.value.validate('manual'); | |||
| }; | |||
| // 表单入口 | |||
| const initForm = async (originForm = {}) => { | |||
| // 普通字段赋值 | |||
| Object.keys(form).forEach((key) => { | |||
| if (!isNil(originForm[key])) { | |||
| form[key] = originForm[key]; | |||
| } | |||
| }); | |||
| // stage 数组非引用赋值 + 默认值 | |||
| form.stage = []; | |||
| if (originForm.stage) { | |||
| // 如果原表单有 stage 数组,则直接赋值 | |||
| for (const stage of originForm.stage) { | |||
| form.stage.push({ | |||
| ...defaultStageForm, | |||
| ...stage, | |||
| }); | |||
| } | |||
| } else { | |||
| form.stage = [ | |||
| { ...defaultStageForm, stageOrder: STAGE_SEQUENCE.TRAIN }, | |||
| { ...defaultStageForm, stageOrder: STAGE_SEQUENCE.SELECT }, | |||
| { ...defaultStageForm, stageOrder: STAGE_SEQUENCE.RETRAIN }, | |||
| ]; | |||
| } | |||
| // 获取表单选项数据 | |||
| await getStrategyInfo(true); | |||
| // 算法信息查询完成后,需要根据所选算法中的 gpu 字段才能确定子表单的 props | |||
| // 如果算法信息不存在,由于必须重新选择算法,因此不再进行数据初始化工作 | |||
| if (form.algorithmId && form.algorithmVersionId) { | |||
| if (originForm.stage) { | |||
| nextTick(() => { | |||
| trainStageForm.value.initForm(); | |||
| selectStageForm.value.initForm(); | |||
| retrainStageForm.value.initForm(); | |||
| }); | |||
| } else { | |||
| // 从 搜索策略 创建实验时,需要查询所选算法版本的阶段信息 | |||
| onAlgorithmVersionChange(form.algorithmVersionId); | |||
| } | |||
| } | |||
| }; | |||
| // 表单校验出口 | |||
| const validate = (resolve, reject) => { | |||
| let valid = true; | |||
| formRef.value.validate((isValid) => { | |||
| valid = valid && isValid; | |||
| }); | |||
| // 子表单校验 | |||
| Object.keys(stageRefs).forEach((stage, index) => { | |||
| if (!valid) return; | |||
| stageRefs[stage].value.validate( | |||
| (stageForm) => { | |||
| // 过滤掉后端返回的多余参数 | |||
| form.stage[index] = { | |||
| ...stageForm, | |||
| algorithmStageId: stageForm.algorithmStageId || stageForm.id, | |||
| stageName: stageForm.stageName || stageForm.name, | |||
| }; | |||
| }, | |||
| () => { | |||
| valid = false; | |||
| state.stageTab = String(stage); | |||
| } | |||
| ); | |||
| }); | |||
| if (valid) { | |||
| if (typeof resolve === 'function') { | |||
| return resolve(form); | |||
| } | |||
| return true; | |||
| } | |||
| if (typeof reject === 'function') { | |||
| return reject(form); | |||
| } | |||
| return false; | |||
| }; | |||
| // 清空表单校验方法 | |||
| const clearValidate = (...args) => { | |||
| formRef.value.clearValidate(...args); | |||
| trainStageForm.value.clearValidate(); | |||
| selectStageForm.value.clearValidate(); | |||
| retrainStageForm.value.clearValidate(); | |||
| }; | |||
| // 资源变更 | |||
| const onResourceChange = (resource) => { | |||
| Object.values(stageRefs).forEach((ref) => { | |||
| ref.value.setDefaultResource(resource); | |||
| }); | |||
| }; | |||
| const onTabsChange = () => { | |||
| // 切换 tab 时,需要更新 yaml 组件才能正常展示内容 | |||
| stageRefs[state.stageTab].value.setYamlValue(); | |||
| }; | |||
| watch( | |||
| () => state.stageTab, | |||
| (next, prev) => { | |||
| Object.assign(state, { | |||
| transition: Number(next) > Number(prev) ? 'tabRight' : 'tabLeft', | |||
| }); | |||
| } | |||
| ); | |||
| return { | |||
| STAGE_SEQUENCE, | |||
| formRef, | |||
| trainStageForm, | |||
| selectStageForm, | |||
| retrainStageForm, | |||
| algorithmVersionIdRef, | |||
| form, | |||
| rules, | |||
| ...toRefs(state), | |||
| initForm, | |||
| validate, | |||
| clearValidate, | |||
| onTabsChange, | |||
| onAlgorithmIdChange, | |||
| onAlgorithmVersionChange, | |||
| onResourceChange, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| @import '../style'; | |||
| .tab-form { | |||
| float: left; | |||
| width: 100%; | |||
| } | |||
| .tabRight-enter, | |||
| .tabLeft-leave-to { | |||
| position: absolute; | |||
| opacity: 0; | |||
| transform: translateX(100%); | |||
| } | |||
| .tabRight-leave-to, | |||
| .tabLeft-enter { | |||
| position: absolute; | |||
| opacity: 0; | |||
| transform: translateX(-100%); | |||
| } | |||
| .tabRight-enter-active, | |||
| .tabRight-leave-active, | |||
| .tabLeft-enter-active, | |||
| .tabLeft-leave-active { | |||
| transition: all 0.6s ease; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,523 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> | |||
| <el-form-item label="资源配置" prop="resourceId"> | |||
| <div class="resources-container"> | |||
| <span v-if="baseResourceList.length === 0" class="empty-text"> | |||
| 暂无数据 | |||
| </span> | |||
| <el-radio-group | |||
| v-model="form.resourceId" | |||
| class="flex flex-col" | |||
| @change="baseResourceChange" | |||
| > | |||
| <el-radio v-for="resource of baseResourceList" :key="resource.id" :label="resource.id">{{ | |||
| resource.specsName | |||
| }}</el-radio> | |||
| </el-radio-group> | |||
| <el-button | |||
| v-if="baseResourceList.length !== 0" | |||
| type="text" | |||
| class="db" | |||
| @click="selectOtherResource" | |||
| >选择其他</el-button | |||
| > | |||
| </div> | |||
| </el-form-item> | |||
| <el-form-item label="数据集" prop="datasetVersion"> | |||
| <el-input | |||
| v-model="form.datasetName" | |||
| placeholder="数据集名称" | |||
| disabled | |||
| style="width: 200px;" | |||
| /> | |||
| <el-input | |||
| v-model="form.datasetVersion" | |||
| placeholder="数据集版本" | |||
| disabled | |||
| style="width: 200px;" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="运行参数" prop="runParams"> | |||
| <div class="yaml"> | |||
| <YamlEditor ref="yamlRef" :value="form.yaml" @blur="onYamlChange" /> | |||
| </div> | |||
| </el-form-item> | |||
| <el-button type="text" @click="showMore = !showMore" | |||
| ><i :class="showMore ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" />{{ | |||
| showMore ? '收起' : '展开' | |||
| }}更多设置</el-button | |||
| > | |||
| <el-collapse-transition> | |||
| <div v-if="showMore"> | |||
| <div class="area-title">实验终止条件</div> | |||
| <el-form-item label="最大 Trial 次数" prop="maxTrialNum"> | |||
| <el-input | |||
| v-model.number="form.maxTrialNum" | |||
| class="w-200" | |||
| @change="changeYamlParams('maxTrialNum')" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="当前阶段最大运行时间" prop="maxExecDuration"> | |||
| <el-input | |||
| v-model="form.maxExecDuration" | |||
| class="w-200 input-suffix" | |||
| @change="onMaxExecDurationChange" | |||
| > | |||
| <template #append> | |||
| <el-select | |||
| v-model="form.maxExecDurationUnit" | |||
| class="w-80" | |||
| @change="changeYamlParams('maxExecDuration')" | |||
| > | |||
| <el-option | |||
| v-for="time in timeFmts" | |||
| :key="time.value" | |||
| :value="time.value" | |||
| :label="time.label" | |||
| /> | |||
| </el-select> | |||
| </template> | |||
| </el-input> | |||
| </el-form-item> | |||
| <div class="area-title">其他配置</div> | |||
| <el-form-item label="Trial 并发数量" prop="trialConcurrentNum"> | |||
| <el-input | |||
| v-model.number="form.trialConcurrentNum" | |||
| class="w-200" | |||
| @change="changeYamlParams('trialConcurrentNum')" | |||
| /> | |||
| </el-form-item> | |||
| </div> | |||
| </el-collapse-transition> | |||
| <BaseModal | |||
| :visible.sync="resourceVisible" | |||
| title="资源配置" | |||
| :showCancel="false" | |||
| @ok="onResourceSelected" | |||
| @close="onResourceClose" | |||
| > | |||
| <BaseTable | |||
| :columns="otherResourceColumns" | |||
| :data="resourceList" | |||
| :highlight-current-row="false" | |||
| > | |||
| <template #radio="scope"> | |||
| <el-radio v-model="selectedOtherResource" :label="scope.row.id"> </el-radio> | |||
| </template> | |||
| </BaseTable> | |||
| <el-pagination | |||
| layout="prev, pager, next" | |||
| :page-size="resourcePageInfo.size" | |||
| :total="resourcePageInfo.total" | |||
| :current-page="resourcePageInfo.current" | |||
| @current-change="onResourcePageChange" | |||
| /> | |||
| </BaseModal> | |||
| </el-form> | |||
| </template> | |||
| <script> | |||
| import { computed, nextTick, reactive, ref, toRefs } from '@vue/composition-api'; | |||
| import yaml from 'js-yaml'; | |||
| import { isNil } from 'lodash'; | |||
| import { Message } from 'element-ui'; | |||
| import BaseModal from '@/components/BaseModal'; | |||
| import BaseTable from '@/components/BaseTable'; | |||
| import YamlEditor from '@/components/YamlEditor'; | |||
| import { list as getResources } from '@/api/system/resources'; | |||
| import { propertyAssign, RESOURCES_MODULE_ENUM } from '@/utils'; | |||
| import { defaultStageForm, otherResourceColumns } from '../utils'; | |||
| import { timeFmts } from '../../util'; | |||
| import { isNull, underlineShiftHump, modifyTime } from '../../strategy/util'; | |||
| const useResources = ({ props, form }, { emit }) => { | |||
| const state = reactive({ | |||
| baseResourceList: [], // 资源配置简易列表 | |||
| resourceList: [], // 资源配置分页列表 | |||
| resourceVisible: false, // 资源配置弹窗 | |||
| selectedOtherResource: null, // 资源弹窗中的资源 | |||
| }); | |||
| // 资源配置 | |||
| // 分页 | |||
| const resourcePageInfo = reactive({ | |||
| current: 1, | |||
| size: 5, | |||
| total: 0, | |||
| }); | |||
| const setResourcePage = (pageInfo) => { | |||
| Object.assign(resourcePageInfo, pageInfo); | |||
| }; | |||
| const baseResourceParam = computed(() => { | |||
| return { | |||
| module: RESOURCES_MODULE_ENUM.TADL, | |||
| resourcesPoolType: props.useGpu ? 1 : 0, | |||
| multiGpu: props.useGpu ? props.model.multiGpu : undefined, | |||
| current: resourcePageInfo.current, | |||
| size: resourcePageInfo.size, | |||
| }; | |||
| }); | |||
| // 获取资源列表 | |||
| const getBaseResourceList = async () => { | |||
| const { result } = await getResources({ | |||
| ...baseResourceParam.value, | |||
| current: 1, | |||
| }); | |||
| state.baseResourceList = result; | |||
| }; | |||
| const getResourceList = async () => { | |||
| const { result, page } = await getResources({ | |||
| ...baseResourceParam.value, | |||
| }); | |||
| state.resourceList = result; | |||
| setResourcePage(page); | |||
| }; | |||
| const onResourcePageChange = (page) => { | |||
| setResourcePage({ | |||
| current: page, | |||
| }); | |||
| getResourceList(); | |||
| }; | |||
| // 选择其他 | |||
| const selectOtherResource = () => { | |||
| state.selectedOtherResource = form.resourceId; | |||
| state.resourceVisible = true; | |||
| getResourceList(); | |||
| }; | |||
| // 如果选中的弹窗表格里选中的值没有在baseResource, 展示在baseResource | |||
| const onResourceSelected = () => { | |||
| if (state.selectedOtherResource) { | |||
| const resource = state.resourceList.find((r) => r.id === state.selectedOtherResource); | |||
| const baseResource = state.baseResourceList.find( | |||
| (base) => base.id === state.selectedOtherResource | |||
| ); | |||
| if (baseResource === undefined && resource !== undefined) { | |||
| state.baseResourceList.unshift(resource); | |||
| } | |||
| form.resourceId = state.selectedOtherResource; | |||
| form.resourceName = resource?.specsName || null; | |||
| emit('resource-change', resource); | |||
| } | |||
| state.resourceVisible = false; | |||
| }; | |||
| const onResourceClose = () => { | |||
| setResourcePage({ | |||
| current: 1, | |||
| }); | |||
| }; | |||
| const baseResourceChange = (id) => { | |||
| const resource = state.baseResourceList.find((item) => item.id === id); | |||
| form.resourceName = resource?.specsName || null; | |||
| emit('resource-change', resource); | |||
| }; | |||
| // 当一个阶段选择了资源配置规格后,其他阶段自动填充默认值 | |||
| const setDefaultResource = (resource) => { | |||
| if (!form.resourceId) { | |||
| const baseResource = state.baseResourceList.find((base) => base.id === resource.id); | |||
| if (!baseResource) { | |||
| state.baseResourceList.unshift(resource); | |||
| } | |||
| form.resourceId = resource.id; | |||
| form.resourceName = resource.specsName; | |||
| } | |||
| }; | |||
| return { | |||
| state, | |||
| setResourcePage, | |||
| resourcePageInfo, | |||
| onResourcePageChange, | |||
| baseResourceChange, | |||
| getBaseResourceList, | |||
| selectOtherResource, | |||
| onResourceSelected, | |||
| onResourceClose, | |||
| setDefaultResource, | |||
| }; | |||
| }; | |||
| export default { | |||
| name: 'TadlStageForm', | |||
| components: { | |||
| BaseModal, | |||
| BaseTable, | |||
| YamlEditor, | |||
| }, | |||
| props: { | |||
| model: Object, | |||
| useGpu: { | |||
| type: Boolean, | |||
| default: undefined, | |||
| }, | |||
| }, | |||
| setup(props, ctx) { | |||
| // 表单 ref | |||
| const formRef = ref(null); | |||
| const yamlRef = ref(null); | |||
| // 表单 | |||
| const form = reactive({ ...defaultStageForm }); | |||
| const rules = { | |||
| resourceId: [{ required: true, message: '请选择资源配置', trigger: 'manual' }], | |||
| maxExecDuration: [ | |||
| { | |||
| required: true, | |||
| validator: (rule, value, callback) => { | |||
| if (!value) { | |||
| callback(new Error('请输入时间')); | |||
| } | |||
| // eslint-disable-next-line no-restricted-globals | |||
| if (isNaN(Number(value))) { | |||
| callback(new Error('时间为数值')); | |||
| } | |||
| if (Number(value) <= 0) { | |||
| callback(new Error('时间需要大于 0')); | |||
| } | |||
| if (!form.maxExecDurationUnit) { | |||
| callback(new Error('请选择时间单位')); | |||
| } | |||
| callback(); | |||
| }, | |||
| trigger: 'blur', | |||
| }, | |||
| ], | |||
| maxTrialNum: [ | |||
| { required: true, message: '请输入最大Trial次数', trigger: ['blur', 'change'] }, | |||
| { type: 'number', message: '所填必须为数字' }, | |||
| { | |||
| validator: (rule, value, callback) => { | |||
| if (!value && value !== 0) { | |||
| callback(); | |||
| } | |||
| if (value <= 0) { | |||
| callback(new Error('最大Trial次数需要大于 0')); | |||
| } | |||
| callback(); | |||
| }, | |||
| trigger: ['blur', 'change'], | |||
| }, | |||
| ], | |||
| trialConcurrentNum: [ | |||
| { required: true, message: '请输入Trial并发数量', trigger: ['blur', 'change'] }, | |||
| { type: 'number', message: '所填必须为数字' }, | |||
| { | |||
| validator: (rule, value, callback) => { | |||
| if (!value && value !== 0) { | |||
| callback(); | |||
| } | |||
| if (value <= 0) { | |||
| callback(new Error('Trial并发数量需要大于 0')); | |||
| } | |||
| callback(); | |||
| }, | |||
| trigger: ['blur', 'change'], | |||
| }, | |||
| ], | |||
| }; | |||
| const state = reactive({ | |||
| showMore: false, | |||
| }); | |||
| // 更新 Yaml 编辑器文本。切换 tab 页时需要手动更新才能正常显示 | |||
| const setYamlValue = () => { | |||
| nextTick(() => { | |||
| yamlRef.value.setValue(); | |||
| }); | |||
| }; | |||
| // 用于yaml改动进行的一些列联动效果 | |||
| const changeYamlParams = (field) => { | |||
| try { | |||
| const yamlLoad = yaml.load(yamlRef.value.getValue() || form.yaml); | |||
| if (!yamlLoad) return; | |||
| const underScoreField = field.replace(/([A-Z])/g, '_$1').toLowerCase(); | |||
| switch (field) { | |||
| case 'maxExecDuration': | |||
| if (!isNull(form.maxExecDuration) && !isNull(form.maxExecDurationUnit)) { | |||
| yamlLoad[underScoreField] = `${form.maxExecDuration}${form.maxExecDurationUnit}`; | |||
| } | |||
| break; | |||
| default: | |||
| if (!isNull(form[field])) { | |||
| yamlLoad[underScoreField] = form[field]; | |||
| } | |||
| } | |||
| form.yaml = yaml.dump(yamlLoad); | |||
| } catch (err) { | |||
| console.error(err); | |||
| if (err.name === 'YAMLException') { | |||
| Message.error('Yaml 解析错误,请检查'); | |||
| } else { | |||
| throw err; | |||
| } | |||
| } | |||
| }; | |||
| // 直接编辑 Yaml 内容后触发解析 | |||
| const onYamlChange = (yamlValue) => { | |||
| try { | |||
| const yamlLoad = yaml.load(yamlValue); | |||
| if (!yamlLoad) return; | |||
| propertyAssign(form, underlineShiftHump(yamlLoad), (val) => !isNull(val)); | |||
| if ('max_exec_duration' in yamlLoad) { | |||
| [form.maxExecDuration, form.maxExecDurationUnit] = modifyTime(yamlLoad.max_exec_duration); | |||
| } | |||
| form.yaml = yamlValue; | |||
| } catch (err) { | |||
| console.error(err); | |||
| if (err.name === 'YAMLException') { | |||
| Message.error('Yaml 解析错误,请检查'); | |||
| } else { | |||
| throw err; | |||
| } | |||
| } | |||
| }; | |||
| // 最大运行时间 | |||
| const onMaxExecDurationChange = (value) => { | |||
| // 先移除非数字和小数点字符,然后调用系统浮点数解析 | |||
| const float = parseFloat(value.replace(/[^\d.]/g, '')); | |||
| form.maxExecDuration = Number.isNaN(float) ? 0 : float; | |||
| changeYamlParams('maxExecDuration'); | |||
| }; | |||
| // 资源配置 | |||
| const { | |||
| state: resourceState, | |||
| setResourcePage, | |||
| resourcePageInfo, | |||
| onResourcePageChange, | |||
| baseResourceChange, | |||
| getBaseResourceList, | |||
| selectOtherResource, | |||
| onResourceSelected, | |||
| onResourceClose, | |||
| setDefaultResource, | |||
| } = useResources( | |||
| { | |||
| props, | |||
| form, | |||
| }, | |||
| ctx | |||
| ); | |||
| const initForm = async () => { | |||
| setResourcePage({ current: 1 }); | |||
| Object.keys(defaultStageForm).forEach((key) => { | |||
| form[key] = isNil(props.model[key]) ? defaultStageForm[key] : props.model[key]; | |||
| }); | |||
| await getBaseResourceList(); | |||
| // 如果修改实验时,原资源规格不在第一页,那么组装一个资源规格到列表顶部 | |||
| if ( | |||
| form.resourceId && | |||
| form.resourceName && | |||
| !resourceState.baseResourceList.find((resource) => resource.id === form.resourceId) | |||
| ) { | |||
| resourceState.baseResourceList.unshift({ | |||
| id: form.resourceId, | |||
| specsName: form.resourceName, | |||
| }); | |||
| } | |||
| }; | |||
| const validate = (resolve, reject) => { | |||
| let valid = true; | |||
| formRef.value.validate((isValid) => { | |||
| valid = valid && isValid; | |||
| }); | |||
| if (valid) { | |||
| if (typeof resolve === 'function') { | |||
| return resolve(form); | |||
| } | |||
| return true; | |||
| } | |||
| if (typeof reject === 'function') { | |||
| return reject(form); | |||
| } | |||
| return false; | |||
| }; | |||
| const clearValidate = (...args) => { | |||
| formRef.value.clearValidate(...args); | |||
| }; | |||
| return { | |||
| timeFmts, | |||
| formRef, | |||
| yamlRef, | |||
| form, | |||
| rules, | |||
| ...toRefs(state), | |||
| ...toRefs(resourceState), | |||
| initForm, | |||
| validate, | |||
| clearValidate, | |||
| setYamlValue, | |||
| resourcePageInfo, | |||
| selectOtherResource, | |||
| onResourceSelected, | |||
| onResourcePageChange, | |||
| onResourceClose, | |||
| setDefaultResource, | |||
| otherResourceColumns, | |||
| changeYamlParams, | |||
| onYamlChange, | |||
| baseResourceChange, | |||
| onMaxExecDurationChange, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| @import '@/assets/styles/variables.scss'; | |||
| @import '../style'; | |||
| .yaml { | |||
| height: 300px; | |||
| line-height: 18px; | |||
| } | |||
| .pb-22 { | |||
| padding-bottom: 22px; | |||
| } | |||
| .empty-text { | |||
| color: $infoColor; | |||
| } | |||
| .resources-container { | |||
| padding: 0 9px; | |||
| border: 1px solid #bbb; | |||
| .el-radio { | |||
| margin-top: 9px; | |||
| } | |||
| } | |||
| ::v-deep .input-suffix .el-input-group__append { | |||
| color: $labelColor; | |||
| background: white; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,170 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div id="form-page-wrapper" class="app-container"> | |||
| <TadlForm ref="formRef" /> | |||
| <div id="btns-wrapper"> | |||
| <!-- 新建实验 --> | |||
| <template v-if="isCreate"> | |||
| <el-button :loading="loadingState.save" @click="doSave">保存设置</el-button> | |||
| <el-button :loading="loadingState.create" type="primary" @click="doCreate" | |||
| >立即创建</el-button | |||
| > | |||
| </template> | |||
| <!-- 保存实验 --> | |||
| <template v-if="isSave"> | |||
| <el-button :loading="loadingState.save" type="primary" @click="doSave" | |||
| >保存设置,确认返回</el-button | |||
| > | |||
| </template> | |||
| <!-- 修改实验 --> | |||
| <template v-if="isEdit"> | |||
| <el-button @click="doCancel">取消</el-button> | |||
| <el-button :loading="loadingState.edit" type="primary" @click="doEdit">确定修改</el-button> | |||
| </template> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { computed, nextTick, reactive, ref } from '@vue/composition-api'; | |||
| import { Message, MessageBox } from 'element-ui'; | |||
| import { createExperiment, editExperiment } from '@/api/tadl'; | |||
| import { updateTitle } from '@/utils'; | |||
| import TadlForm from './components/tadlForm'; | |||
| const title = { | |||
| create: '创建实验', | |||
| save: '保存实验', | |||
| edit: '修改实验', | |||
| }; | |||
| export default { | |||
| name: 'TadlFormPage', | |||
| components: { TadlForm }, | |||
| beforeRouteEnter(to, from, next) { | |||
| const newTitle = title[to.params.formType || 'create']; | |||
| // 修改 navbar 中的 title | |||
| to.meta.title = newTitle; | |||
| // 修改页面 title | |||
| updateTitle(newTitle); | |||
| next(); | |||
| }, | |||
| setup(props, { root }) { | |||
| // 表单组件 ref | |||
| const formRef = ref(null); | |||
| // 表单类型:新建实验-create / 保存实验-save / 修改实验-edit | |||
| const formType = ref(root.$route.params.formType || 'create'); | |||
| const isCreate = computed(() => ['create', 'strategy'].includes(formType.value)); // 包括搜索策略中的创建实验 | |||
| const isSave = computed(() => formType.value === 'save'); | |||
| const isEdit = computed(() => formType.value === 'edit'); | |||
| // 不同按钮的 loading 状态 | |||
| const loadingState = reactive({ | |||
| create: false, | |||
| save: false, | |||
| edit: false, | |||
| }); | |||
| switch (formType.value) { | |||
| case 'edit': | |||
| case 'save': | |||
| case 'strategy': // 搜索策略中的创建实验 | |||
| nextTick(() => { | |||
| formRef.value.initForm(root.$route.params.formParams); | |||
| }); | |||
| break; | |||
| case 'create': | |||
| default: | |||
| nextTick(() => { | |||
| formRef.value.initForm(); | |||
| }); | |||
| break; | |||
| } | |||
| // 提交新建 | |||
| const doCreate = () => { | |||
| formRef.value.validate((form) => { | |||
| form.start = true; // 用于区分创建/保存实验 | |||
| loadingState.create = true; | |||
| createExperiment(form) | |||
| .then(() => { | |||
| Message.success(`实验创建成功`); | |||
| root.$router.push({ name: 'TadlList' }); | |||
| }) | |||
| .finally(() => { | |||
| loadingState.create = false; | |||
| }); | |||
| }); | |||
| }; | |||
| // 提交保存 | |||
| const doSave = () => { | |||
| formRef.value.validate((form) => { | |||
| form.start = false; // 用于区分创建/保存实验 | |||
| loadingState.save = true; | |||
| createExperiment(form) | |||
| .then(() => { | |||
| Message.success(`实验保存成功`); | |||
| root.$router.push({ name: 'TadlList' }); | |||
| }) | |||
| .finally(() => { | |||
| loadingState.save = false; | |||
| }); | |||
| }); | |||
| }; | |||
| // 提交修改 | |||
| const doEdit = () => { | |||
| formRef.value.validate((form) => { | |||
| loadingState.edit = true; | |||
| editExperiment(form) | |||
| .then(() => { | |||
| Message.success(`实验编辑成功`); | |||
| root.$router.push({ name: 'TadlList' }); | |||
| }) | |||
| .finally(() => { | |||
| loadingState.edit = false; | |||
| }); | |||
| }); | |||
| }; | |||
| // 取消 | |||
| const doCancel = () => { | |||
| MessageBox.confirm('取消将丢失所有信息', '确认').then(() => { | |||
| root.$router.back(); | |||
| }); | |||
| }; | |||
| return { | |||
| formRef, | |||
| isCreate, | |||
| isSave, | |||
| isEdit, | |||
| loadingState, | |||
| doCreate, | |||
| doSave, | |||
| doEdit, | |||
| doCancel, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| #form-page-wrapper { | |||
| max-width: 1400px; | |||
| margin-top: 50px; | |||
| } | |||
| #btns-wrapper { | |||
| margin: 50px 120px; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,21 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| .area-title { | |||
| padding: 12px 15px; | |||
| margin-bottom: 20px; | |||
| border-left: 4px solid #333; | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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 const defaultStageForm = { | |||
| // 从算法中获得的 stage 使用 id/name 字段 | |||
| // 从实验中获得的 stage 使用 algorithmStageId/stageName 字段 | |||
| // 在提交实验时需要统一为 algorithmStageId/stageName 字段 | |||
| id: null, // 阶段 ID | |||
| algorithmStageId: null, | |||
| name: null, // 阶段名 | |||
| stageName: null, | |||
| stageOrder: null, // 阶段序号 | |||
| datasetName: null, // 数据集名称 | |||
| datasetVersion: null, // 数据集版本 | |||
| resourceId: null, // 资源 ID | |||
| resourceName: null, // 资源名称 | |||
| yaml: '', // yaml 运行参数 | |||
| maxTrialNum: 10, // 最大 Trial 次数 | |||
| multiGpu: undefined, // 是否使用多卡 | |||
| trialConcurrentNum: 1, // Trial 并发数量 | |||
| maxExecDuration: 0, // 最大运行时间 | |||
| maxExecDurationUnit: 'min', // 最大运行时间单位 | |||
| }; | |||
| // 弹窗其他资源表格列定义 | |||
| export const otherResourceColumns = [ | |||
| { | |||
| prop: 'radio', | |||
| width: '50px', | |||
| }, | |||
| { | |||
| prop: 'specsName', | |||
| label: '名称', | |||
| }, | |||
| ]; | |||
| @@ -0,0 +1,127 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="list-status-container"> | |||
| <el-tooltip placement="top" enterable effect="light"> | |||
| <template #content> | |||
| <div class="flex"> | |||
| <template v-for="(stage, index) in stages"> | |||
| <div :key="'status' + index" class="stage-status-block"> | |||
| <div | |||
| class="status-circle" | |||
| :style=" | |||
| `background-color: ${getValueFromMap(STAGE_STATUS_MAP, stage.status, 'bgColor')}` | |||
| " | |||
| /> | |||
| <div class="f18">{{ stage.stageName }}</div> | |||
| <div>{{ stage.endTime && parseTime(stage.endTime) }}</div> | |||
| </div> | |||
| </template> | |||
| </div> | |||
| </template> | |||
| <span class="status-info"> | |||
| <template v-for="(stage, index) in stages"> | |||
| <span v-if="index !== 0" :key="'line' + index" class="split-line" /> | |||
| <span | |||
| :key="'status' + index" | |||
| class="status-circle" | |||
| :style=" | |||
| `background-color: ${getValueFromMap(STAGE_STATUS_MAP, stage.status, 'bgColor')}` | |||
| " | |||
| /> | |||
| </template> | |||
| </span> | |||
| </el-tooltip> | |||
| <span class="status-text"> | |||
| {{ getValueFromMap(EXPERIMENT_STATUS_MAP, status, 'label') }} | |||
| </span> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { getValueFromMap, parseTime } from '@/utils'; | |||
| import { EXPERIMENT_STATUS_MAP, STAGE_STATUS_MAP } from '../../util'; | |||
| export default { | |||
| name: 'ListStatus', | |||
| props: { | |||
| stages: { | |||
| type: Array, | |||
| default: () => [], | |||
| }, | |||
| status: { | |||
| type: Number, | |||
| required: true, | |||
| }, | |||
| }, | |||
| setup() { | |||
| return { | |||
| getValueFromMap, | |||
| parseTime, | |||
| EXPERIMENT_STATUS_MAP, | |||
| STAGE_STATUS_MAP, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .list-status-container, | |||
| .status-info { | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| .status-circle { | |||
| display: inline-block; | |||
| width: 6px; | |||
| height: 6px; | |||
| border-radius: 50%; | |||
| } | |||
| .split-line { | |||
| display: inline-block; | |||
| width: 7px; | |||
| height: 0; | |||
| border-top: 2px #bbb solid; | |||
| } | |||
| .status-text { | |||
| margin-left: 5px; | |||
| } | |||
| .stage-status-block { | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| width: 120px; | |||
| &::before { | |||
| display: inline-block; | |||
| width: 100px; | |||
| margin: 7px 60px -7px -60px; | |||
| content: ''; | |||
| border-bottom: 2px solid #bbb; | |||
| } | |||
| &:first-child { | |||
| padding-top: 2px; | |||
| &::before { | |||
| display: none; | |||
| } | |||
| } | |||
| .status-circle { | |||
| width: 12px; | |||
| height: 12px; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,220 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="app-container"> | |||
| <ProTable | |||
| ref="proTable" | |||
| create-title="创建实验" | |||
| :columns="columns" | |||
| :form-items="listQueryFormItems" | |||
| :list-request="list" | |||
| :before-list-fn="beforeListFn" | |||
| :after-list-fn="afterListFn" | |||
| show-refresh | |||
| loading-type="header" | |||
| :refresh-immediate="false" | |||
| :table-attrs="tableAttrs" | |||
| @add="onCreate" | |||
| > | |||
| <template #status="scope"> | |||
| <div class="flex"> | |||
| <ListStatus :stages="scope.row.stages" :status="scope.row.status" /> | |||
| <MsgPopover | |||
| v-if="scope.row.statusDetail" | |||
| :status-detail="scope.row.statusDetail" | |||
| class="ml-4" | |||
| /> | |||
| </div> | |||
| </template> | |||
| </ProTable> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { computed, nextTick, onUnmounted, reactive, ref } from '@vue/composition-api'; | |||
| import { Message, MessageBox } from 'element-ui'; | |||
| import ProTable from '@/components/ProTable'; | |||
| import MsgPopover from '@/components/MsgPopover'; | |||
| import { list, expDetail, pauseExp, startExp, deleteExp } from '@/api/tadl'; | |||
| import { getValueFromMap, Constant } from '@/utils'; | |||
| import { useKeepPageInfo } from '@/hooks'; | |||
| import { MODEL_TYPE_ENUM } from '../util'; | |||
| import ListStatus from './components/listStatus'; | |||
| import { getListColumns, listQueryFormItems, needPoll } from './util'; | |||
| export default { | |||
| name: 'TadlList', | |||
| components: { | |||
| ProTable, | |||
| ListStatus, | |||
| MsgPopover, | |||
| }, | |||
| beforeRouteEnter(to, from, next) { | |||
| // 如果不是从记录页返回到列表页的,页码重置为 1 | |||
| if (!['ExperimentDetail'].includes(from.name)) { | |||
| next((vm) => vm.pageEnter(false)); | |||
| return; | |||
| } | |||
| // 从记录页返回时保留页码和排序状态 | |||
| next((vm) => vm.pageEnter(true)); | |||
| }, | |||
| setup(props, { root }) { | |||
| // proTable ref | |||
| const proTable = ref(null); | |||
| const defaultSort = reactive({ prop: undefined, order: undefined }); | |||
| const setDefaultSort = (sort) => { | |||
| Object.assign(defaultSort, sort); | |||
| }; | |||
| // 创建按钮跳转表单页 | |||
| const onCreate = () => { | |||
| root.$router.push({ name: 'TadlForm', params: { formType: 'create' } }); | |||
| }; | |||
| const modelTypeFormatter = (modelType) => { | |||
| return getValueFromMap(MODEL_TYPE_ENUM, modelType, 'label'); | |||
| }; | |||
| // 列操作方法 | |||
| // 查看详情 | |||
| const toDetail = (row) => { | |||
| root.$router.push({ | |||
| path: `/tadl/experiment/${row.id}`, | |||
| }); | |||
| }; | |||
| // 开始运行 | |||
| const doStart = async (row) => { | |||
| await startExp(row.id); | |||
| Message.success('实验启动成功'); | |||
| proTable.value.refresh(); | |||
| }; | |||
| // 编辑 | |||
| const doEdit = async (row) => { | |||
| const formParams = await expDetail(row.id); | |||
| root.$router.push({ | |||
| name: 'TadlForm', | |||
| params: { | |||
| formType: 'edit', | |||
| formParams, | |||
| }, | |||
| }); | |||
| }; | |||
| // 删除 | |||
| const doDelete = (row) => { | |||
| MessageBox.confirm('确认删除该实验', '确认').then(async () => { | |||
| await deleteExp(row.id); | |||
| Message.success('实验删除成功'); | |||
| proTable.value.refresh(); | |||
| }); | |||
| }; | |||
| // 暂停 | |||
| const doPause = (row) => { | |||
| MessageBox.confirm('确认暂停该实验', '确认').then(async () => { | |||
| await pauseExp(row.id); | |||
| Message.success('实验暂停成功'); | |||
| proTable.value.refresh(); | |||
| }); | |||
| }; | |||
| // 获取列定义 | |||
| const columns = computed(() => { | |||
| return getListColumns({ | |||
| toDetail, | |||
| doStart, | |||
| doEdit, | |||
| doDelete, | |||
| doPause, | |||
| modelTypeFormatter, | |||
| }); | |||
| }); | |||
| const afterEnter = () => { | |||
| proTable.value.refresh(); | |||
| }; | |||
| const pageInfoSetter = ({ current, pageSize, sort: { sort, order }, query }) => { | |||
| setDefaultSort({ prop: sort, order: Constant.tableSortMap2Element[order] }); | |||
| nextTick(() => { | |||
| proTable.value.setPagination({ current, size: pageSize }); | |||
| proTable.value.setSort({ sort, order }); | |||
| proTable.value.setQuery(query); | |||
| }); | |||
| }; | |||
| const { pageEnter, updatePageInfo } = useKeepPageInfo({ | |||
| afterEnter, | |||
| pageInfoGetter: 'tadl/pageInfo', | |||
| updateAction: 'tadl/updateExperimentPageInfo', | |||
| pageInfoSetter, | |||
| }); | |||
| // 判断是否轮询 | |||
| const keepPoll = ref(true); | |||
| let timeoutId; | |||
| onUnmounted(() => { | |||
| keepPoll.value = false; | |||
| }); | |||
| const beforeListFn = () => { | |||
| if (timeoutId) { | |||
| clearTimeout(timeoutId); | |||
| } | |||
| }; | |||
| const afterListFn = (exps) => { | |||
| if (exps.some((exp) => needPoll(exp))) { | |||
| timeoutId = setTimeout(() => { | |||
| if (keepPoll.value) { | |||
| proTable.value.refresh(); | |||
| } | |||
| }, 3000); | |||
| } | |||
| const { currentPage: current, pageSize } = proTable.value.pagination; | |||
| updatePageInfo({ | |||
| current, | |||
| pageSize, | |||
| sort: { ...proTable.value.sortInfo }, | |||
| query: { ...proTable.value.state.queryFormModel }, | |||
| }); | |||
| }; | |||
| const tableAttrs = computed(() => ({ defaultSort })); | |||
| return { | |||
| proTable, | |||
| tableAttrs, | |||
| onCreate, | |||
| columns, | |||
| listQueryFormItems, | |||
| list, | |||
| beforeListFn, | |||
| afterListFn, | |||
| pageEnter, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| ::v-deep .name-col .el-link { | |||
| max-width: 100%; | |||
| span { | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,175 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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 { runTimeFormatter, EXPERIMENT_STATUS_MAP, MODEL_TYPE_ENUM } from '../util'; | |||
| const allExperimentStatusList = [{ label: '全部', value: null }].concat( | |||
| Object.values(EXPERIMENT_STATUS_MAP).map((status) => ({ | |||
| label: status.label, | |||
| value: status.value, | |||
| })) | |||
| ); | |||
| const allModelTypeList = [{ label: '全部', value: null }].concat( | |||
| Object.values(MODEL_TYPE_ENUM).map((status) => ({ | |||
| label: status.label, | |||
| value: status.value, | |||
| })) | |||
| ); | |||
| // 获取列表页表格列定义 | |||
| export function getListColumns({ | |||
| toDetail, | |||
| doStart, | |||
| doEdit, | |||
| doDelete, | |||
| doPause, | |||
| modelTypeFormatter, | |||
| }) { | |||
| return [ | |||
| { | |||
| label: 'ID', | |||
| prop: 'id', | |||
| width: 80, | |||
| sortable: 'custom', | |||
| fixed: true, | |||
| }, | |||
| { | |||
| label: '名称', | |||
| prop: 'name', | |||
| fixed: true, | |||
| type: 'link', | |||
| func: toDetail, | |||
| className: 'name-col', | |||
| }, | |||
| { | |||
| label: '状态', | |||
| prop: 'status', | |||
| minWidth: '120px', | |||
| showOverflowTooltip: false, | |||
| dropdownList: allExperimentStatusList, | |||
| }, | |||
| { | |||
| label: '模型类别', | |||
| prop: 'modelType', | |||
| minWidth: '110px', | |||
| formatter: modelTypeFormatter, | |||
| dropdownList: allModelTypeList, | |||
| }, | |||
| { | |||
| label: '开始时间', | |||
| prop: 'startTime', | |||
| type: 'time', | |||
| minWidth: '160px', | |||
| sortable: 'custom', | |||
| }, | |||
| { | |||
| label: '运行时间', | |||
| prop: 'runTime', | |||
| formatter: runTimeFormatter, | |||
| }, | |||
| { | |||
| label: '创建人', | |||
| prop: 'createUser', | |||
| }, | |||
| { | |||
| label: '描述', | |||
| prop: 'description', | |||
| minWidth: '200px', | |||
| }, | |||
| { | |||
| label: '操作', | |||
| type: 'operation', | |||
| width: '370px', | |||
| fixed: 'right', | |||
| operationLimit: 7, | |||
| operations: [ | |||
| { | |||
| label: '开始运行', | |||
| func: doStart, | |||
| hideFunc(row) { | |||
| // 待运行展示开始运行 | |||
| return ![ | |||
| EXPERIMENT_STATUS_MAP.TO_RUN.value, | |||
| EXPERIMENT_STATUS_MAP.FAILED.value, | |||
| EXPERIMENT_STATUS_MAP.PAUSED.value, | |||
| ].includes(row.status); | |||
| }, | |||
| clickOnceTime: 3000, | |||
| }, | |||
| { | |||
| label: '编辑', | |||
| func: doEdit, | |||
| hideFunc(row) { | |||
| // 待运行展示编辑 | |||
| return row.status !== EXPERIMENT_STATUS_MAP.TO_RUN.value; | |||
| }, | |||
| }, | |||
| { | |||
| label: '删除', | |||
| func: doDelete, | |||
| hideFunc(row) { | |||
| // 已完成、已暂停、运行失败 展示删除 | |||
| return ![ | |||
| EXPERIMENT_STATUS_MAP.FINISHED.value, | |||
| EXPERIMENT_STATUS_MAP.PAUSED.value, | |||
| EXPERIMENT_STATUS_MAP.FAILED.value, | |||
| ].includes(row.status); | |||
| }, | |||
| }, | |||
| { | |||
| label: '暂停', | |||
| func: doPause, | |||
| hideFunc(row) { | |||
| // 运行中、等待中 展示暂停 | |||
| return ![ | |||
| EXPERIMENT_STATUS_MAP.RUNNING.value, | |||
| EXPERIMENT_STATUS_MAP.WAITING.value, | |||
| ].includes(row.status); | |||
| }, | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| } | |||
| // 列表页查询表单项 | |||
| export const listQueryFormItems = [ | |||
| { | |||
| prop: 'name', | |||
| placeholder: '输入名称或ID查询', | |||
| class: 'w-200', | |||
| change: 'query', | |||
| }, | |||
| { | |||
| type: 'button', | |||
| btnText: '重置', | |||
| func: 'resetQuery', | |||
| }, | |||
| { | |||
| type: 'button', | |||
| btnText: '搜索', | |||
| btnType: 'primary', | |||
| func: 'query', | |||
| }, | |||
| ]; | |||
| // 判断实验是否需要轮询 | |||
| export function needPoll(exp) { | |||
| return [EXPERIMENT_STATUS_MAP.WAITING.value, EXPERIMENT_STATUS_MAP.RUNNING.value].includes( | |||
| exp.status | |||
| ); | |||
| } | |||
| @@ -0,0 +1,559 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div> | |||
| <div class="tabs"> | |||
| <el-tabs v-model="active" class="eltabs-inlineblock" @tab-click="handleClick"> | |||
| <el-tab-pane id="tab_0" label="基本配置" name="page" /> | |||
| <el-tab-pane id="tab_1" label="运行参数" name="params" /> | |||
| </el-tabs> | |||
| </div> | |||
| <el-form | |||
| v-show="active === 'page'" | |||
| ref="formRef" | |||
| :model="form" | |||
| :disabled="type === 'check'" | |||
| :rules="rules" | |||
| label-width="150px" | |||
| class="form" | |||
| > | |||
| <!-- 创建阶段 --> | |||
| <template v-if="steps === 0"> | |||
| <el-form-item label="默认指标" prop="default_metric"> | |||
| <el-input | |||
| id="default_metric" | |||
| v-model="createForm.default_metric" | |||
| placeholder="由上传的算法文件生成" | |||
| disabled | |||
| style="width: 200px;" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="GPU" prop="gpu"> | |||
| <el-switch | |||
| id="gpu" | |||
| v-model="createForm.gpu" | |||
| :active-value="true" | |||
| :inactive-value="false" | |||
| disabled | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="算法类型" prop="alg_type"> | |||
| <el-radio-group v-model="createForm.alg_type" disabled> | |||
| <el-radio label="NAS" border class="mr-0">NAS</el-radio> | |||
| </el-radio-group> | |||
| </el-form-item> | |||
| <el-form-item label="运行环境"> | |||
| <el-input | |||
| id="imageId" | |||
| v-model="createForm.platform" | |||
| placeholder="由上传的算法文件生成" | |||
| disabled | |||
| style="width: 200px;" | |||
| /> | |||
| <el-input | |||
| id="imagePath" | |||
| v-model="createForm.platform_version" | |||
| placeholder="由上传的算法文件生成" | |||
| disabled | |||
| style="width: 200px;" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item v-if="createForm.alg_type === 'NAS'" label="ONE_SHOT" prop="one_shot"> | |||
| <el-switch | |||
| id="one_shot" | |||
| v-model="createForm.one_shot" | |||
| :active-value="true" | |||
| :inactive-value="false" | |||
| disabled | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="算法描述" prop="description"> | |||
| <el-input | |||
| id="description" | |||
| v-model="createForm.description" | |||
| type="textarea" | |||
| :rows="3" | |||
| maxlength="256" | |||
| show-word-limit | |||
| placeholder | |||
| style="width: 400px;" | |||
| /> | |||
| </el-form-item> | |||
| </template> | |||
| <!-- 配置阶段 --> | |||
| <template v-else> | |||
| <el-form-item label="支持多卡训练"> | |||
| <el-switch | |||
| id="multi_gpu" | |||
| v-model="pageForm.multi_gpu" | |||
| :active-value="true" | |||
| :inactive-value="false" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item ref="datasetId" label="数据集" prop="datasetId"> | |||
| <InfoSelect | |||
| v-model="pageForm.dataset_id" | |||
| style="display: inline-block;" | |||
| width="200px" | |||
| placeholder="请选择数据集" | |||
| :dataSource="datasetIdList" | |||
| value-key="id" | |||
| label-key="name" | |||
| filterable | |||
| @change="onDatasetChange" | |||
| /> | |||
| <InfoSelect | |||
| v-model="pageForm.dataset_path" | |||
| style="display: inline-block;" | |||
| width="200px" | |||
| placeholder="请选择数据集版本" | |||
| :dataSource="datasetVersionList" | |||
| value-key="versionUrl" | |||
| label-key="versionName" | |||
| filterable | |||
| @change="onDatasetVersionChange" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="运行命令"> | |||
| <el-input | |||
| id="pythonVersion" | |||
| v-model="pageForm.python_version" | |||
| placeholder="由上传文件生成" | |||
| disabled | |||
| style="width: 200px;" | |||
| /> | |||
| <el-input | |||
| id="executeScript" | |||
| v-model="pageForm.execute_script" | |||
| placeholder="由上传文件生成" | |||
| disabled | |||
| style="width: 200px;" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item ref="maxExecDuration" label="现阶段最大运行时间" prop="maxExecDuration"> | |||
| <el-input | |||
| id="maxExecDuration" | |||
| v-model="pageForm.max_exec_duration" | |||
| placeholder="请输入时间" | |||
| clearable | |||
| style="width: 200px;" | |||
| @change="onMaxExecDurationChange" | |||
| /> | |||
| <InfoSelect | |||
| v-model="pageForm.max_exec_duration_unit" | |||
| style="display: inline-block;" | |||
| width="190" | |||
| placeholder="请选择时间单位" | |||
| :dataSource="timeFmts" | |||
| value-key="value" | |||
| label-key="label" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="最大Trial次数" prop="max_trial_num"> | |||
| <el-input | |||
| v-model.number="pageForm.max_trial_num" | |||
| placeholder="请输入最大Trial次数" | |||
| clearable | |||
| style="width: 200px;" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="Trial并发数量" prop="trial_concurrent_num"> | |||
| <el-input | |||
| v-model.number="pageForm.trial_concurrent_num" | |||
| placeholder="请输入Trial并发数量" | |||
| clearable | |||
| style="width: 200px;" | |||
| /> | |||
| </el-form-item> | |||
| </template> | |||
| </el-form> | |||
| <div v-show="active === 'params'" style="position: relative; height: 500px;"> | |||
| <YamlEditor ref="yamlRef" :value="yamlValue" :read-only="steps === 0 || type === 'check'" /> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import yaml from 'js-yaml'; | |||
| import { Message } from 'element-ui'; | |||
| import { computed, nextTick, reactive, toRefs } from '@vue/composition-api'; | |||
| import { getPublishedDatasets, getDatasetVersions } from '@/api/preparation/dataset'; | |||
| import { parseYamlParams } from '@/api/tadl/strategy'; | |||
| import InfoSelect from '@/components/InfoSelect'; | |||
| import YamlEditor from '@/components/YamlEditor/index'; | |||
| import { propertyAssign } from '@/utils'; | |||
| import { timeFmts, getModelByCode } from '../../util'; | |||
| import { modifyTime, isNull } from '../util'; | |||
| const defaultCreateForm = { | |||
| default_metric: null, // 默认指标 | |||
| alg_type: 'NAS', // 算法类型 | |||
| platform: null, // 框架名称 | |||
| platform_version: null, // 框架版本 | |||
| gpu: false, // 是否支持gpu计算 | |||
| one_shot: false, // 是否oneshot | |||
| description: null, // 算法描述 | |||
| }; | |||
| const defaultPageForm = { | |||
| multi_gpu: false, // 是否支持多卡 | |||
| dataset_id: null, // 数据集id | |||
| dataset_name: null, // 数据集名称 | |||
| dataset_path: null, // 数据集路径 | |||
| dataset_version: null, // 数据集版本 | |||
| python_version: null, // python版本 | |||
| execute_script: null, // 算法启动文件 | |||
| max_trial_num: null, // 最大trial次数 | |||
| max_exec_duration: null, // 当前阶段最大运行时间 | |||
| max_exec_duration_unit: null, // 最大时间单位 | |||
| trial_concurrent_num: null, // trial并发数量 | |||
| }; | |||
| export default { | |||
| name: 'CreatePageForm', | |||
| components: { YamlEditor, InfoSelect }, | |||
| props: { | |||
| // 用于第一阶段请求参数 | |||
| baseForm: { | |||
| type: Object, | |||
| default: () => ({}), | |||
| }, | |||
| // 阶段值 | |||
| steps: { | |||
| type: Number, | |||
| default: 0, | |||
| }, | |||
| // create/edit/check | |||
| type: { | |||
| type: String, | |||
| default: 'create', | |||
| }, | |||
| // 算法上传路径,创建时需要由此路径获取 yaml | |||
| zipPath: { | |||
| type: String, | |||
| }, | |||
| }, | |||
| setup(props, ctx) { | |||
| const data = reactive({ | |||
| active: 'page', | |||
| yamlValue: '', | |||
| yamlParams: {}, | |||
| datasetIdList: [], | |||
| datasetVersionList: [], | |||
| valueForm: {}, // 用于存入外部传进的form值 | |||
| }); | |||
| const pageForm = reactive({ ...defaultPageForm }); | |||
| const createForm = reactive({ ...defaultCreateForm }); | |||
| const refs = reactive({ | |||
| yamlRef: null, | |||
| formRef: null, | |||
| datasetId: null, | |||
| maxExecDuration: null, | |||
| }); | |||
| const rules = { | |||
| description: [{ required: true, message: '请输入算法描述', trigger: ['blur', 'change'] }], | |||
| datasetId: [ | |||
| { | |||
| required: true, | |||
| trigger: 'manual', | |||
| validator: (rule, value, callback) => { | |||
| if (!pageForm.dataset_id) { | |||
| callback(new Error('请选择数据集')); | |||
| } | |||
| if (!pageForm.dataset_path) { | |||
| callback(new Error('请选择数据集版本')); | |||
| } | |||
| callback(); | |||
| }, | |||
| }, | |||
| ], | |||
| maxExecDuration: [ | |||
| { | |||
| required: true, | |||
| validator: (rule, value, callback) => { | |||
| if (!pageForm.max_exec_duration) { | |||
| callback(new Error('请输入时间')); | |||
| } | |||
| // eslint-disable-next-line no-restricted-globals | |||
| if (isNaN(Number(pageForm.max_exec_duration))) { | |||
| callback(new Error('时间为数值')); | |||
| } | |||
| if (Number(pageForm.max_exec_duration) <= 0) { | |||
| callback(new Error('时间需要大于 0')); | |||
| } | |||
| if (!pageForm.max_exec_duration_unit) { | |||
| callback(new Error('请选择时间单位')); | |||
| } | |||
| callback(); | |||
| }, | |||
| trigger: 'blur', | |||
| }, | |||
| ], | |||
| max_trial_num: [ | |||
| { required: true, message: '请输入最大Trial次数', trigger: ['blur', 'change'] }, | |||
| { type: 'number', message: '所填必须为数字' }, | |||
| { | |||
| validator: (rule, value, callback) => { | |||
| if (!value && value !== 0) { | |||
| callback(); | |||
| } | |||
| if (value <= 0) { | |||
| callback(new Error('最大Trial次数需要大于 0')); | |||
| } | |||
| callback(); | |||
| }, | |||
| trigger: ['blur', 'change'], | |||
| }, | |||
| ], | |||
| trial_concurrent_num: [ | |||
| { required: true, message: '请输入Trial并发数量', trigger: ['blur', 'change'] }, | |||
| { type: 'number', message: '所填必须为数字' }, | |||
| { | |||
| validator: (rule, value, callback) => { | |||
| if (!value && value !== 0) { | |||
| callback(); | |||
| } | |||
| if (value <= 0) { | |||
| callback(new Error('Trial并发数量需要大于 0')); | |||
| } | |||
| callback(); | |||
| }, | |||
| trigger: ['blur', 'change'], | |||
| }, | |||
| ], | |||
| }; | |||
| const form = computed(() => (props.steps === 0 ? createForm : pageForm)); | |||
| // 数据集 | |||
| const getDatasetVersion = async (datasetId, keepValue = false) => { | |||
| data.datasetVersionList = await getDatasetVersions(datasetId); | |||
| if (keepValue && pageForm.dataset_path) { | |||
| const version = data.datasetVersionList.find( | |||
| (version) => version.versionUrl === pageForm.dataset_path | |||
| ); | |||
| if (!version) { | |||
| pageForm.dataset_path = null; | |||
| Message.warning('原有数据集版本不存在,请重新选择'); | |||
| } | |||
| } | |||
| }; | |||
| const getDataset = async (keepValue = false) => { | |||
| data.datasetIdList = ( | |||
| await getPublishedDatasets({ | |||
| size: 1000, | |||
| annotateType: getModelByCode(data.valueForm.model_type, 'label'), | |||
| }) | |||
| ).result; | |||
| if (!keepValue || !pageForm.dataset_id) { | |||
| pageForm.dataset_path = null; | |||
| } else { | |||
| const dataset = data.datasetIdList.find((dataset) => dataset.id === pageForm.dataset_id); | |||
| if (!dataset) { | |||
| Message.warning('原有数据集不存在,请重新选择'); | |||
| pageForm.dataset_id = pageForm.dataset_path = pageForm.dataset_version = null; | |||
| return; | |||
| } | |||
| getDatasetVersion(dataset.id, true); | |||
| } | |||
| }; | |||
| const onDatasetChange = (datasetId) => { | |||
| pageForm.dataset_path = pageForm.dataset_version = pageForm.dataset_name = null; | |||
| data.datasetVersionList = []; | |||
| if (!datasetId) return; | |||
| getDatasetVersion(datasetId); | |||
| const selectedDataset = data.datasetIdList.find((i) => i.id === datasetId); | |||
| pageForm.dataset_name = selectedDataset.name; | |||
| }; | |||
| const onDatasetVersionChange = () => { | |||
| const version = data.datasetVersionList.find( | |||
| (version) => version.versionUrl === pageForm.dataset_path | |||
| ); | |||
| pageForm.dataset_version = version ? version.versionName : null; | |||
| refs.datasetId.validate('manual'); | |||
| }; | |||
| // 最大运行时间 | |||
| const onMaxExecDurationChange = (value) => { | |||
| // 先移除非数字和小数点字符,然后调用系统浮点数解析 | |||
| const float = parseFloat(value.replace(/[^\d.]/g, '')); | |||
| pageForm.max_exec_duration = Number.isNaN(float) ? 0 : float; | |||
| }; | |||
| // yaml语法转换 | |||
| const yamlLoad = () => { | |||
| try { | |||
| // 将yaml字符转换成yaml对象格式 | |||
| data.yamlParams = yaml.load(refs.yamlRef.getValue() || data.yamlValue); | |||
| propertyAssign(form.value, data.yamlParams, (val) => !isNull(val)); | |||
| if ('command' in data.yamlParams) | |||
| [pageForm.python_version, pageForm.execute_script] = data.yamlParams.command.split(' '); | |||
| if ('max_exec_duration' in data.yamlParams) | |||
| [pageForm.max_exec_duration, pageForm.max_exec_duration_unit] = modifyTime( | |||
| data.yamlParams.max_exec_duration | |||
| ); | |||
| } catch (err) { | |||
| console.error(err); | |||
| throw err; | |||
| } | |||
| }; | |||
| // 初始解析yaml | |||
| const getYaml = async () => { | |||
| data.yamlValue = await parseYamlParams({ | |||
| algorithm: props.steps ? data.valueForm.name : props.baseForm.name || undefined, | |||
| zipPath: props.zipPath || undefined, | |||
| stageOrder: props.steps, | |||
| versionName: props.steps | |||
| ? data.valueForm.version_name | |||
| : props.baseForm.version_name || undefined, | |||
| }); | |||
| yamlLoad(); | |||
| if (!props.steps) { | |||
| // 用于回填模型类别 | |||
| ctx.emit('yaml-loaded', { | |||
| modelType: data.yamlParams?.model_type || 'ImageClassify', | |||
| name: data.yamlParams.alg_name, | |||
| }); | |||
| } | |||
| }; | |||
| // 外部调用传值 | |||
| const initForm = (originForm = {}) => { | |||
| // 保存外部传入的值 | |||
| data.valueForm = originForm; | |||
| if (props.steps) { | |||
| getDataset(true); | |||
| const order = originForm.stage.find((s) => s.stage_order === props.steps); | |||
| if (order) { | |||
| propertyAssign(form.value, order, (val) => !isNull(val)); | |||
| data.yamlValue = order.yaml; | |||
| data.yamlParams = yaml.load(data.yamlValue); | |||
| } else { | |||
| getYaml(); | |||
| } | |||
| } else { | |||
| propertyAssign(form.value, originForm, (val) => !isNull(val)); | |||
| data.yamlValue = originForm.yaml; | |||
| data.yamlParams = yaml.load(data.yamlValue); | |||
| } | |||
| }; | |||
| const shiftBasePage = () => { | |||
| nextTick(() => { | |||
| refs.yamlRef.codeValid() ? yamlLoad() : (data.active = 'params'); | |||
| }); | |||
| }; | |||
| const shiftYamlParams = () => { | |||
| propertyAssign(data.yamlParams, form.value, (val) => !isNull(val)); | |||
| if (props.steps) { | |||
| if (!isNull(pageForm.python_version) && !isNull(pageForm.execute_script)) { | |||
| data.yamlParams.command = `${pageForm.python_version} ${pageForm.execute_script}`; | |||
| } | |||
| if (!isNull(pageForm.max_exec_duration) && !isNull(pageForm.max_exec_duration_unit)) { | |||
| data.yamlParams.max_exec_duration = `${pageForm.max_exec_duration}${pageForm.max_exec_duration_unit}`; | |||
| } | |||
| } | |||
| // 将yaml对象格式转成字符串 | |||
| data.yamlValue = yaml.dump(data.yamlParams); | |||
| nextTick(() => { | |||
| refs.yamlRef.setValue(); | |||
| }); | |||
| }; | |||
| const handleClick = () => { | |||
| data.active === 'page' ? shiftBasePage() : shiftYamlParams(); | |||
| nextTick(() => { | |||
| ctx.emit('tabs-change', data.active); | |||
| }); | |||
| }; | |||
| const getFormValue = () => { | |||
| return [form.value, data.yamlValue]; | |||
| }; | |||
| // 表单校验方法 | |||
| const validateForm = (resolve, reject) => { | |||
| shiftYamlParams(); // 单击下一步时需要转换 | |||
| refs.formRef.validate((isValid) => { | |||
| if (isValid) { | |||
| if (typeof resolve === 'function') { | |||
| return resolve(form.value, data.yamlValue); | |||
| } | |||
| return true; | |||
| } | |||
| if (typeof reject === 'function') { | |||
| return reject(form.value); | |||
| } | |||
| return false; | |||
| }); | |||
| }; | |||
| // 清空表单 | |||
| const clearValidate = () => { | |||
| refs.formRef.clearValidate(); | |||
| }; | |||
| // 重置表单 | |||
| const resetForm = () => { | |||
| Object.assign(createForm, defaultCreateForm); | |||
| Object.assign(pageForm, defaultPageForm); | |||
| data.active = 'page'; | |||
| data.yamlValue = ''; | |||
| data.yamlParams = {}; | |||
| nextTick(() => { | |||
| clearValidate(); | |||
| ctx.emit('tabs-change', data.active); | |||
| }); | |||
| }; | |||
| return { | |||
| ...toRefs(data), | |||
| ...toRefs(refs), | |||
| createForm, | |||
| pageForm, | |||
| form, | |||
| rules, | |||
| handleClick, | |||
| onDatasetChange, | |||
| onDatasetVersionChange, | |||
| onMaxExecDurationChange, | |||
| getYaml, | |||
| initForm, | |||
| getFormValue, | |||
| validateForm, | |||
| resetForm, | |||
| timeFmts, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .form { | |||
| margin-left: 20px; | |||
| } | |||
| .tabs { | |||
| margin-bottom: 20px; | |||
| text-align: center; | |||
| } | |||
| .el-radio.is-bordered { | |||
| width: 100px; | |||
| height: 35px; | |||
| padding: 10px 0; | |||
| text-align: center; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,101 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="wrapper"> | |||
| <BaseModal | |||
| :visible.sync="visible" | |||
| title="发布搜索策略" | |||
| :loading="releasing" | |||
| @cancel="visible = false" | |||
| @ok="onVersionRelease" | |||
| > | |||
| <el-form ref="formRef" :model="form" label-width="100px"> | |||
| <el-form-item label="策略名称"> | |||
| <el-input | |||
| id="name" | |||
| v-model.trim="form.name" | |||
| placeholder | |||
| disabled | |||
| maxlength="50" | |||
| show-word-limit | |||
| style="width: 300px;" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="当前版本"> | |||
| <el-input | |||
| id="currentVersion" | |||
| v-model="form.currentVersion" | |||
| style="width: 200px;" | |||
| disabled | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="下一版本"> | |||
| <el-input id="nextVersion" v-model="form.nextVersion" disabled style="width: 200px;"> | |||
| </el-input> | |||
| </el-form-item> | |||
| </el-form> | |||
| </BaseModal> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { Message } from 'element-ui'; | |||
| import { reactive, ref } from '@vue/composition-api'; | |||
| import { versionRelease } from '@/api/tadl/strategy'; | |||
| import BaseModal from '@/components/BaseModal'; | |||
| const defaultForm = { | |||
| name: null, | |||
| currentVersion: null, | |||
| nextVersion: null, | |||
| }; | |||
| export default { | |||
| name: 'ReleaseDialog', | |||
| components: { BaseModal }, | |||
| setup(props, ctx) { | |||
| const formRef = ref(null); | |||
| const form = reactive({ ...defaultForm }); | |||
| const visible = ref(false); | |||
| const releasing = ref(false); | |||
| const handleShow = (info) => { | |||
| visible.value = true; | |||
| Object.assign(form, info); | |||
| }; | |||
| // 版本发布 | |||
| const onVersionRelease = () => { | |||
| formRef.value.validate((valid) => { | |||
| if (valid) { | |||
| releasing.value = true; | |||
| versionRelease(form) | |||
| .then(() => { | |||
| ctx.emit('release-success'); | |||
| Message.success('版本发布成功'); | |||
| visible.value = false; | |||
| }) | |||
| .finally(() => { | |||
| releasing.value = false; | |||
| }); | |||
| } | |||
| }); | |||
| }; | |||
| return { | |||
| formRef, | |||
| form, | |||
| visible, | |||
| releasing, | |||
| handleShow, | |||
| onVersionRelease, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| @@ -0,0 +1,514 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <el-drawer | |||
| ref="drawerRef" | |||
| :title="title" | |||
| :before-close="handleClose" | |||
| :visible.sync="visible" | |||
| size="40%" | |||
| > | |||
| <!-- 阶段步骤条 --> | |||
| <el-steps :active="active" finish-status="success" align-center> | |||
| <el-step title="创建策略"></el-step> | |||
| <el-step title="TRAIN阶段配置"></el-step> | |||
| <el-step title="SELECT阶段配置"></el-step> | |||
| <el-step title="RETRAIN配置"></el-step> | |||
| </el-steps> | |||
| <!-- 基本配置表单 --> | |||
| <el-form | |||
| v-if="active === 0" | |||
| ref="baseFormRef" | |||
| :rules="rules" | |||
| :model="baseForm" | |||
| :disabled="type === 'check'" | |||
| label-width="150px" | |||
| class="form" | |||
| > | |||
| <el-form-item | |||
| v-if="type === 'create'" | |||
| ref="algorithmPathRef" | |||
| label="上传算法文件" | |||
| prop="algorithm_path" | |||
| > | |||
| <upload-inline | |||
| ref="uploadRef" | |||
| action="fakeApi" | |||
| accept=".zip" | |||
| list-type="text" | |||
| :acceptSize="algorithmConfig.uploadFileAcceptSize" | |||
| :acceptSizeFormat="uploadSizeFomatter" | |||
| :params="uploadParams" | |||
| :show-file-count="false" | |||
| :auto-upload="true" | |||
| :hash="false" | |||
| :filters="uploadFilters" | |||
| :limit="1" | |||
| :on-remove="onFileRemove" | |||
| @uploadStart="uploadStart" | |||
| @uploadSuccess="uploadSuccess" | |||
| @uploadError="uploadError" | |||
| /> | |||
| <upload-progress | |||
| v-if="uploading" | |||
| :progress="progress" | |||
| :status="uploadStatus" | |||
| :size="size" | |||
| @onSetProgress="onSetProgress" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="策略名称" prop="name"> | |||
| <el-input | |||
| id="name" | |||
| v-model.trim="baseForm.name" | |||
| placeholder="由算法解析自动获取" | |||
| maxlength="50" | |||
| show-word-limit | |||
| disabled | |||
| style="width: 200px;" | |||
| /> | |||
| </el-form-item> | |||
| <el-form-item label="模型类别" prop="model_type"> | |||
| <el-select | |||
| id="modelType" | |||
| v-model="baseForm.model_type" | |||
| placeholder="由算法解析自动获取" | |||
| clearable | |||
| disabled | |||
| > | |||
| <el-option | |||
| v-for="item in MODEL_TYPE_ENUM" | |||
| :key="item.value" | |||
| :label="item.label" | |||
| :value="item.value" | |||
| /> | |||
| </el-select> | |||
| </el-form-item> | |||
| </el-form> | |||
| <!-- 阶段配置表单组件 --> | |||
| <CreatePageForm | |||
| ref="createPageFormRef" | |||
| :base-form="baseForm" | |||
| :zip-path="zipPath" | |||
| :steps="active" | |||
| :type="type" | |||
| @tabs-change="(tab) => (buttonShow = tab === 'page')" | |||
| @yaml-loaded="onYamlLoaded" | |||
| /> | |||
| <!-- 操作按钮 --> | |||
| <div v-if="buttonShow" class="operation"> | |||
| <el-button :disabled="submitting" @click="handleBack">{{ backButtonName }}</el-button> | |||
| <el-button type="primary" :loading="submitting" @click="handleNext">{{ | |||
| nextButtonName | |||
| }}</el-button> | |||
| </div> | |||
| </el-drawer> | |||
| </template> | |||
| <script> | |||
| import { Message, MessageBox } from 'element-ui'; | |||
| import { reactive, toRefs, computed, nextTick, ref } from '@vue/composition-api'; | |||
| import { | |||
| uploadSizeFomatter, | |||
| invalidFileNameChar, | |||
| propertyAssign, | |||
| validateName, | |||
| getUniqueId, | |||
| } from '@/utils'; | |||
| import { algorithmConfig } from '@/config'; | |||
| import { unpackZip, uploadStrategy, updateStrategy, checkStrategy } from '@/api/tadl/strategy'; | |||
| import UploadInline from '@/components/UploadForm/inline'; | |||
| import UploadProgress from '@/components/UploadProgress'; | |||
| import { useMapGetters } from '@/hooks'; | |||
| import CreatePageForm from './CreatePageForm'; | |||
| import { MODEL_TYPE_ENUM } from '../../util'; | |||
| import { underlineShiftHump, humpShiftUnderline, isNull } from '../util'; | |||
| const defaultForm = { | |||
| name: null, // 算法名称 | |||
| model_type: null, // 模型类别 | |||
| algorithm_path: null, // 算法路径 | |||
| }; | |||
| const useUpload = ({ customOnRemove, customUploadSuccess } = {}) => { | |||
| const state = reactive({ | |||
| uploadParams: { objectPath: null }, // 对象存储路径 | |||
| size: 0, // 文件大小 | |||
| progress: 0, // 上传进度 | |||
| uploadFilters: [invalidFileNameChar], // 文件校验 | |||
| uploading: false, | |||
| }); | |||
| const uploadRef = ref(null); | |||
| // 上传状态 | |||
| const uploadStatus = computed(() => { | |||
| state.progress === 100 ? 'success' : null; | |||
| }); | |||
| const { user } = useMapGetters(['user']); | |||
| // 生成随机临时文件夹路径 | |||
| const updateObjectPath = () => { | |||
| state.uploadParams.objectPath = `upload-temp/${user.id}/${getUniqueId()}`; | |||
| }; | |||
| // 移除文件 | |||
| const onRemove = () => { | |||
| state.uploading = false; | |||
| if (typeof customOnRemove === 'function') { | |||
| customOnRemove(); | |||
| } | |||
| }; | |||
| // 开始上传 | |||
| const uploadStart = (files) => { | |||
| updateObjectPath(); | |||
| state.uploading = true; | |||
| state.size = files.size; | |||
| state.progress = 0; | |||
| }; | |||
| // 上传成功 | |||
| const uploadSuccess = (res) => { | |||
| state.progress = 100; | |||
| setTimeout(() => { | |||
| state.uploading = false; | |||
| }, 1000); | |||
| if (typeof customUploadSuccess === 'function') { | |||
| customUploadSuccess(res); | |||
| } | |||
| }; | |||
| // 上传失败 | |||
| const uploadError = () => { | |||
| Message.error('上传文件失败'); | |||
| state.uploading = false; | |||
| }; | |||
| // 进度更新 | |||
| const onSetProgress = (val) => { | |||
| state.progress += val; | |||
| }; | |||
| return { | |||
| ...toRefs(state), | |||
| uploadRef, | |||
| uploadStatus, | |||
| updateObjectPath, | |||
| onRemove, | |||
| uploadStart, | |||
| uploadSuccess, | |||
| uploadError, | |||
| onSetProgress, | |||
| }; | |||
| }; | |||
| // 表单类型与中文操作对应关系 | |||
| const TYPE_MAP = { | |||
| create: '上传', | |||
| edit: '编辑', | |||
| check: '查看', | |||
| }; | |||
| export default { | |||
| name: 'StrategyDrawer', | |||
| components: { CreatePageForm, UploadInline, UploadProgress }, | |||
| setup(props, ctx) { | |||
| // 头部表单 | |||
| const baseForm = reactive({ ...defaultForm }); | |||
| const refs = reactive({ | |||
| baseFormRef: null, | |||
| algorithmPathRef: null, | |||
| createPageFormRef: null, | |||
| }); | |||
| const data = reactive({ | |||
| visible: false, | |||
| type: 'create', | |||
| active: 0, | |||
| buttonShow: true, | |||
| form: {}, // 用于存储表单数据 | |||
| submitting: false, | |||
| zipPath: null, // 存储上传的算法路径,创建算法时需要由此路径获取 yaml 信息 | |||
| }); | |||
| const title = computed(() => { | |||
| const action = TYPE_MAP[data.type] || ''; | |||
| const strategyName = data.form.name ? ` - ${data.form.name}` : ''; | |||
| return `${action}搜索策略${strategyName}`; | |||
| }); | |||
| const rules = { | |||
| name: [ | |||
| { required: true, message: '请输入策略名称', trigger: ['change', 'blur'] }, | |||
| { validator: validateName, trigger: ['change', 'blur'] }, | |||
| ], | |||
| model_type: [{ required: true, message: '请选择模型类别', trigger: 'change' }], | |||
| algorithm_path: [{ required: true, message: '请上传算法文件', trigger: ['blur', 'manual'] }], | |||
| }; | |||
| // 外部显示 | |||
| const handleShow = async (type, { algorithmVersionId, id }) => { | |||
| data.type = type; | |||
| data.visible = true; | |||
| if (type !== 'create') { | |||
| const params = await checkStrategy({ algorithmVersionId }, id); | |||
| data.form = humpShiftUnderline(params); | |||
| if (data.active === 0) { | |||
| propertyAssign(baseForm, data.form, (val) => !isNull(val)); | |||
| } | |||
| nextTick(() => { | |||
| refs.createPageFormRef.initForm(data.form); | |||
| }); | |||
| } | |||
| }; | |||
| const resetBaseForm = () => { | |||
| Object.assign(baseForm, defaultForm); | |||
| nextTick(() => { | |||
| refs.baseFormRef && refs.baseFormRef.clearValidate(); | |||
| }); | |||
| }; | |||
| const initState = async () => { | |||
| refs.createPageFormRef.resetForm(); | |||
| data.active = 0; | |||
| data.form = {}; | |||
| data.visible = false; | |||
| }; | |||
| // 上传算法 | |||
| // 文件移除处理 | |||
| const onFileRemove = () => { | |||
| baseForm.algorithm_path = null; | |||
| refs.algorithmPathRef.validate('manual'); | |||
| }; | |||
| // 上传成功处理 | |||
| const customUploadSuccess = (res) => { | |||
| const algorithmPath = res[0].data.objectName; | |||
| baseForm.algorithm_path = algorithmPath; | |||
| data.zipPath = algorithmPath; | |||
| refs.algorithmPathRef.validate('manual'); | |||
| // 文件上传成功后反馈给后端 | |||
| unpackZip({ zipPath: algorithmPath }).then(() => { | |||
| nextTick(() => { | |||
| // 文件解析成功后将基本配置数据传入 | |||
| refs.createPageFormRef.getYaml(); | |||
| // 上传后清空 form,所有数据由解析算法后获取 | |||
| data.form = {}; | |||
| }); | |||
| }); | |||
| }; | |||
| const { | |||
| uploadRef, | |||
| uploadParams, | |||
| size, | |||
| progress, | |||
| uploadFilters, | |||
| uploadStatus, | |||
| uploading, | |||
| updateObjectPath, | |||
| onRemove, | |||
| uploadStart, | |||
| onSetProgress, | |||
| uploadSuccess, | |||
| uploadError, | |||
| } = useUpload({ customOnRemove: onFileRemove, customUploadSuccess }); | |||
| const handleClose = () => { | |||
| MessageBox.confirm('关闭弹窗数据会消失,是否继续', '提示', { | |||
| confirmButtonText: '确定', | |||
| cancelButtonText: '取消', | |||
| type: 'warning', | |||
| }).then(() => { | |||
| if (!data.active && data.type === 'create') { | |||
| uploadRef.value.formRef.reset(); | |||
| data.loading = false; | |||
| resetBaseForm(); | |||
| } | |||
| initState(); | |||
| }); | |||
| }; | |||
| // buttons | |||
| // 上一步按钮名 | |||
| const backButtonName = computed(() => { | |||
| return data.active ? '上一步' : '取消'; | |||
| }); | |||
| // 下一步按钮名 | |||
| const nextButtonName = computed(() => { | |||
| if (data.active !== 3) { | |||
| return '下一步'; | |||
| } | |||
| return data.type === 'check' ? '关闭' : '确定'; | |||
| }); | |||
| // 上一步 | |||
| const handleBack = () => { | |||
| if (!data.active) { | |||
| handleClose(); | |||
| } else { | |||
| const [params, yaml] = refs.createPageFormRef.getFormValue(); | |||
| params.stage_order = data.active; | |||
| if (data.form.stage[params.stage_order - 1]) { | |||
| Object.assign(data.form.stage[params.stage_order - 1], { ...params, yaml }); | |||
| } else { | |||
| data.form.stage[params.stage_order - 1] = { ...params, yaml }; | |||
| } | |||
| data.active -= 1; | |||
| if (data.active === 0) { | |||
| propertyAssign(baseForm, data.form, (val) => !isNull(val)); | |||
| } | |||
| nextTick(() => { | |||
| refs.createPageFormRef.initForm(data.form); | |||
| }); | |||
| } | |||
| }; | |||
| // 策略基础表单校验 | |||
| const submitBaseForm = () => { | |||
| let baseValid = true; | |||
| let configValid = true; | |||
| // 基本信息表单校验 | |||
| refs.baseFormRef.validate((valid) => { | |||
| baseValid = valid && baseValid; | |||
| if (valid) { | |||
| Object.assign(data.form, baseForm); | |||
| } | |||
| }); | |||
| // 基本配置表单校验 | |||
| refs.createPageFormRef.validateForm( | |||
| (form, yaml) => { | |||
| Object.assign(data.form, form, { yaml }); | |||
| if (data.form.stage === undefined) data.form.stage = []; | |||
| }, | |||
| () => { | |||
| configValid = false; | |||
| } | |||
| ); | |||
| if (baseValid && configValid) { | |||
| resetBaseForm(); | |||
| refs.createPageFormRef.resetForm(); | |||
| nextTick(() => { | |||
| data.active += 1; | |||
| nextTick(() => { | |||
| refs.createPageFormRef.initForm(data.form); | |||
| }); | |||
| }); | |||
| } | |||
| }; | |||
| // 阶段表单校验 | |||
| const submitStageForm = () => { | |||
| refs.createPageFormRef.validateForm((form, yaml) => { | |||
| form.stage_order = data.active; | |||
| if (data.form.stage[form.stage_order - 1]) { | |||
| Object.assign(data.form.stage[form.stage_order - 1], { ...form, yaml }); | |||
| } else { | |||
| data.form.stage[form.stage_order - 1] = { ...form, yaml }; | |||
| } | |||
| if (data.active !== 3) { | |||
| refs.createPageFormRef.resetForm(); | |||
| data.active += 1; | |||
| nextTick(() => { | |||
| refs.createPageFormRef.initForm(data.form); | |||
| }); | |||
| } else { | |||
| if (data.type === 'check') { | |||
| initState(); | |||
| return; | |||
| } | |||
| if (data.submitting) return; | |||
| data.submitting = true; | |||
| const apiFunction = data.type === 'create' ? uploadStrategy : updateStrategy; | |||
| data.form.zipPath = data.zipPath; | |||
| apiFunction(underlineShiftHump(data.form)) | |||
| .then(() => { | |||
| initState(); | |||
| ctx.emit('submit-success'); | |||
| Message.success(`${TYPE_MAP[data.type]}成功`); | |||
| }) | |||
| .finally(() => { | |||
| data.submitting = false; | |||
| }); | |||
| } | |||
| }); | |||
| }; | |||
| // 下一步 | |||
| const handleNext = async () => { | |||
| if (data.active === 0) { | |||
| submitBaseForm(); | |||
| } else { | |||
| submitStageForm(); | |||
| } | |||
| }; | |||
| const onYamlLoaded = ({ modelType, name }) => { | |||
| baseForm.model_type = MODEL_TYPE_ENUM[modelType].value; | |||
| baseForm.name = name; | |||
| }; | |||
| updateObjectPath(); | |||
| return { | |||
| ...toRefs(data), | |||
| ...toRefs(refs), | |||
| title, | |||
| baseForm, | |||
| rules, | |||
| handleClose, | |||
| handleShow, | |||
| backButtonName, | |||
| nextButtonName, | |||
| handleNext, | |||
| handleBack, | |||
| // upload | |||
| uploadRef, | |||
| uploadParams, | |||
| size, | |||
| progress, | |||
| uploadFilters, | |||
| uploadStatus, | |||
| uploading, | |||
| onFileRemove: onRemove, | |||
| uploadStart, | |||
| onSetProgress, | |||
| uploadSuccess, | |||
| uploadError, | |||
| onYamlLoaded, | |||
| // 外部引入 | |||
| algorithmConfig, | |||
| uploadSizeFomatter, | |||
| MODEL_TYPE_ENUM, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .form { | |||
| margin: 30px 0 0 20px; | |||
| } | |||
| .operation { | |||
| margin-bottom: 20px; | |||
| text-align: center; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,329 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. * ============================================================= */ | |||
| <template> | |||
| <div class="app-container"> | |||
| <el-row v-loading="loading" class="card-row"> | |||
| <!-- 非管理员不能上传和编辑 --> | |||
| <el-col v-if="isAdmin" :xs="12" :sm="12" :lg="6" :xl="4" class="card-col"> | |||
| <el-card shadow="always"> | |||
| <div class="upload flex flex-center" @click="onDrawerShow('create')"> | |||
| <span> | |||
| <i class="el-icon-plus"></i> | |||
| 上传搜索策略 | |||
| </span> | |||
| </div> | |||
| </el-card> | |||
| </el-col> | |||
| <el-col | |||
| v-for="item in algorithmList" | |||
| :key="item.id" | |||
| :xs="12" | |||
| :sm="12" | |||
| :lg="6" | |||
| :xl="4" | |||
| class="card-col" | |||
| > | |||
| <el-card shadow="hover"> | |||
| <!-- 卡片头部 --> | |||
| <div class="flex flex-between card-title"> | |||
| <label>{{ item.name }}</label> | |||
| <el-dropdown v-if="item.algorithmVersionVOList.length > 1" @command="onVersionChange"> | |||
| <span class="el-dropdown-link"> | |||
| {{ item.selectedVersionName }}<i class="el-icon-arrow-down el-icon--right"></i> | |||
| </span> | |||
| <el-dropdown-menu slot="dropdown"> | |||
| <el-dropdown-item v-for="v in item.algorithmVersionVOList" :key="v.id" :command="v"> | |||
| {{ v.versionName || '最新' }} | |||
| <el-button | |||
| v-if="v.versionName" | |||
| class="dropdown-del-btn" | |||
| type="text" | |||
| @click.stop="doDelete(item, v)" | |||
| ><i class="el-icon-close" | |||
| /></el-button> | |||
| </el-dropdown-item> | |||
| </el-dropdown-menu> | |||
| </el-dropdown> | |||
| </div> | |||
| <!-- 标签 --> | |||
| <div class="tag"> | |||
| <i class="el-icon-price-tag tag-icon" /> | |||
| <el-tag class="ml-10">{{ item.algType }}</el-tag> | |||
| </div> | |||
| <!-- 文本内容 --> | |||
| <p class="introduce multiple-lines"> | |||
| {{ item.description }} | |||
| </p> | |||
| <el-divider /> | |||
| <!-- 卡片操作按钮 --> | |||
| <div class="operation-wrapper flex flex-around"> | |||
| <!-- 非管理员不能上传和编辑 --> | |||
| <template v-if="isAdmin"> | |||
| <el-tooltip effect="dark" :content="getEditContent(item)" placement="bottom"> | |||
| <i | |||
| class="cp iconfont icon-bianji" | |||
| :class="{ 'i-disabled': item.isReleased }" | |||
| @click.stop="onDrawerShow('edit', item)" | |||
| /> | |||
| </el-tooltip> | |||
| <el-divider direction="vertical" /> | |||
| </template> | |||
| <el-tooltip effect="dark" content="查看搜索策略" placement="bottom"> | |||
| <i class="cp iconfont icon-chaxun" @click.stop="onDrawerShow('check', item)" /> | |||
| </el-tooltip> | |||
| <el-divider direction="vertical" /> | |||
| <el-tooltip effect="dark" :content="getCreateContent(item)" placement="bottom"> | |||
| <i | |||
| class="cp iconfont icon-shiyanguanli" | |||
| :class="{ 'i-disabled': !item.isReleased }" | |||
| @click.stop="doCreate(item)" | |||
| /> | |||
| </el-tooltip> | |||
| <!-- 非管理员不能发布和删除 --> | |||
| <template v-if="isAdmin"> | |||
| <el-divider direction="vertical" /> | |||
| <el-tooltip effect="dark" :content="getReleaseContent(item)" placement="bottom"> | |||
| <i | |||
| class="cp iconfont icon-fabu" | |||
| :class="{ 'i-disabled': item.isReleased }" | |||
| @click.stop="doRelease(item)" | |||
| /> | |||
| </el-tooltip> | |||
| <el-divider direction="vertical" /> | |||
| <el-tooltip effect="dark" content="删除算法" placement="bottom"> | |||
| <i class="cp iconfont icon-shanchu" @click.stop="doDelete(item)" /> | |||
| </el-tooltip> | |||
| </template> | |||
| </div> | |||
| </el-card> | |||
| </el-col> | |||
| </el-row> | |||
| <!-- 上传/编辑/查看抽屉 --> | |||
| <StrategyDrawer ref="strategyDrawer" @submit-success="submitSuccess" /> | |||
| <!-- 发布搜索策略弹窗 --> | |||
| <ReleaseDialog ref="releaseDialog" @release-success="releaseSuccess" /> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { reactive, toRefs, onMounted } from '@vue/composition-api'; | |||
| import { Message, MessageBox } from 'element-ui'; | |||
| import { getStrategyList, getNextVersion, shiftVersion, deleteVersion } from '@/api/tadl/strategy'; | |||
| import { useMapGetters } from '@/hooks'; | |||
| import StrategyDrawer from './components/StrategyDrawer'; | |||
| import ReleaseDialog from './components/ReleaseDialog'; | |||
| const useGetAlgorithms = () => { | |||
| const state = reactive({ | |||
| algorithmList: [], // 接口获取的算法列表 | |||
| loading: false, | |||
| }); | |||
| // 列表刷新及搜索 | |||
| const refreshList = async (content) => { | |||
| state.loading = true; | |||
| state.algorithmList = await getStrategyList({ content }).finally(() => { | |||
| state.loading = false; | |||
| }); | |||
| // 为算法添加当前版本信息 | |||
| state.algorithmList.forEach((algorithm) => { | |||
| const selectedVersion = algorithm.algorithmVersionVOList.find( | |||
| (v) => v.id === algorithm.algorithmVersionId | |||
| ); | |||
| if (selectedVersion) { | |||
| algorithm.selectedVersionName = selectedVersion.versionName || '最新'; | |||
| algorithm.isReleased = selectedVersion.versionName !== null; | |||
| } else { | |||
| algorithm.selectedVersionName = '选择版本'; | |||
| algorithm.isReleased = false; | |||
| } | |||
| }); | |||
| }; | |||
| return { | |||
| ...toRefs(state), | |||
| refreshList, | |||
| }; | |||
| }; | |||
| export default { | |||
| name: 'Strategy', | |||
| components: { StrategyDrawer, ReleaseDialog }, | |||
| setup(props, ctx) { | |||
| const refs = reactive({ | |||
| strategyDrawer: null, | |||
| releaseDialog: null, | |||
| }); | |||
| const { isAdmin } = useMapGetters(['isAdmin']); | |||
| const { algorithmList, loading, refreshList } = useGetAlgorithms(); | |||
| const onDrawerShow = async (type, item = {}) => { | |||
| if (!(type === 'edit' && item.isReleased)) { | |||
| refs.strategyDrawer.handleShow(type, item); | |||
| } | |||
| }; | |||
| const doRelease = async (info) => { | |||
| if (info.isReleased) return; | |||
| const version = info.algorithmVersionVOList.find((v) => v.id === info.algorithmVersionId); | |||
| if (version) { | |||
| const releaseObj = await getNextVersion(version.algorithmId); | |||
| refs.releaseDialog.handleShow(releaseObj); | |||
| } | |||
| }; | |||
| const onVersionChange = ({ algorithmId, id }) => { | |||
| shiftVersion({ algorithmId, algorithmVersionId: id }).then(() => refreshList()); | |||
| }; | |||
| const submitSuccess = () => { | |||
| refreshList(); | |||
| }; | |||
| const releaseSuccess = () => { | |||
| refreshList(); | |||
| }; | |||
| const doCreate = ({ id, algorithmVersionId, isReleased }) => { | |||
| if (!isReleased) return; | |||
| ctx.root.$router.push({ | |||
| name: 'TadlForm', | |||
| params: { | |||
| formType: 'strategy', | |||
| formParams: { | |||
| algorithmId: id, | |||
| algorithmVersionId, | |||
| }, | |||
| }, | |||
| }); | |||
| }; | |||
| const doDelete = ({ name, id }, version) => { | |||
| MessageBox.confirm( | |||
| version | |||
| ? `确认删除算法${name}的${version.versionName}版本?` | |||
| : `删除算法${name}会同时删除其所有版本,是否确认?`, | |||
| '确认' | |||
| ).then(async () => { | |||
| await deleteVersion({ | |||
| algorithmId: id, | |||
| algorithmVersionId: version ? version.id : undefined, | |||
| }); | |||
| Message.success('算法删除成功!'); | |||
| refreshList(); | |||
| }); | |||
| }; | |||
| const getEditContent = ({ isReleased }) => | |||
| isReleased ? '编辑只可用于最新版本' : '编辑搜索策略'; | |||
| const getReleaseContent = ({ isReleased }) => (isReleased ? '发布只可用于最新版本' : '发布'); | |||
| const getCreateContent = ({ isReleased }) => | |||
| isReleased ? '创建实验' : '只有已发布版本才能创建实验'; | |||
| onMounted(() => { | |||
| refreshList(); | |||
| }); | |||
| return { | |||
| ...toRefs(refs), | |||
| isAdmin, | |||
| algorithmList, | |||
| loading, | |||
| submitSuccess, | |||
| releaseSuccess, | |||
| onDrawerShow, | |||
| doRelease, | |||
| onVersionChange, | |||
| doCreate, | |||
| doDelete, | |||
| getEditContent, | |||
| getReleaseContent, | |||
| getCreateContent, | |||
| }; | |||
| }, | |||
| }; | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| @import '@/assets/styles/variables.scss'; | |||
| .i-disabled { | |||
| color: #909399; | |||
| cursor: not-allowed; | |||
| } | |||
| .divider { | |||
| height: 12px; | |||
| margin-bottom: 20px; | |||
| background: #f5f7fa; | |||
| } | |||
| .card-col { | |||
| padding: 0 10px; | |||
| margin: 10px 0; | |||
| .upload { | |||
| height: 175px; | |||
| color: #88898a; | |||
| cursor: pointer; | |||
| } | |||
| .card-title { | |||
| height: 20px; | |||
| margin-bottom: 10px; | |||
| } | |||
| .tag { | |||
| height: 23px; | |||
| margin: 10px 0; | |||
| .tag-icon { | |||
| font-size: 18px; | |||
| color: #000; | |||
| transform: rotate(-30deg); | |||
| } | |||
| } | |||
| .introduce { | |||
| height: 65px; | |||
| font-size: 14px; | |||
| color: rgba(146, 146, 146, 100); | |||
| -webkit-line-clamp: 4; | |||
| } | |||
| .el-divider--horizontal { | |||
| margin: 10px 0; | |||
| } | |||
| .el-dropdown-link { | |||
| color: #2e4fde; | |||
| cursor: pointer; | |||
| } | |||
| .el-icon-arrow-down { | |||
| font-size: 12px; | |||
| } | |||
| } | |||
| .dropdown-del-btn { | |||
| float: right; | |||
| margin: 0 -10px 0 10px; | |||
| line-height: 18px; | |||
| color: $infoColor; | |||
| :hover { | |||
| color: $primaryHoverColor; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,75 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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. | |||
| * ============================================================= | |||
| */ | |||
| // 分割时间数值和单位名称 | |||
| const TIME_UNIT_RE = /^([\d.]+)([a-z]+)$/; | |||
| export function modifyTime(time) { | |||
| return time.match(TIME_UNIT_RE).slice(1, 3); | |||
| } | |||
| // 判断数据类型 | |||
| export function typeOf(type) { | |||
| return Object.prototype.toString.call(type).slice(8, -1); | |||
| } | |||
| // 对象属性名下划线转驼峰 | |||
| export function underlineShiftHump(obj) { | |||
| const newObj = {}; | |||
| Object.keys(obj).forEach((key) => { | |||
| const keyArr = key.split('_'); | |||
| let newKey = keyArr[0]; | |||
| keyArr.forEach((item, index) => { | |||
| if (index) newKey += item.slice(0, 1).toUpperCase() + item.slice(1); | |||
| }); | |||
| newObj[newKey] = obj[key]; | |||
| if (typeOf(obj[key]) === 'Array') { | |||
| obj[key].forEach((item, index) => { | |||
| if (typeOf(item) === 'Object') newObj[newKey][index] = underlineShiftHump(item); | |||
| }); | |||
| } | |||
| if (typeOf(obj[key]) === 'Object') { | |||
| newObj[newKey] = underlineShiftHump(obj[key]); | |||
| } | |||
| }); | |||
| return newObj; | |||
| } | |||
| // 对象属性名驼峰转下划线 | |||
| export function humpShiftUnderline(obj) { | |||
| const newObj = {}; | |||
| Object.keys(obj).forEach((key) => { | |||
| const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); | |||
| newObj[newKey] = obj[key]; | |||
| if (typeOf(obj[key]) === 'Array') { | |||
| obj[key].forEach((item, index) => { | |||
| if (typeOf(item) === 'Object') newObj[newKey][index] = humpShiftUnderline(item); | |||
| }); | |||
| } | |||
| if (typeOf(obj[key]) === 'Object') { | |||
| newObj[newKey] = humpShiftUnderline(obj[key]); | |||
| } | |||
| }); | |||
| return newObj; | |||
| } | |||
| // 判断值是否为空或空字符 | |||
| export const isNull = (value) => { | |||
| return ( | |||
| value === null || | |||
| value === undefined || | |||
| (typeof value === 'string' && value.trim().length === 0) | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,184 @@ | |||
| /** Copyright 2020 Tianshu AI Platform. 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 { isNil, invert } from 'lodash'; | |||
| import { ONE_DAY, ONE_HOUR, ONE_MINUTE, getValueFromMap } from '@/utils'; | |||
| import { expStageAccuracy, expStageIntermediate } from '@/api/tadl'; | |||
| // 实验状态枚举值 | |||
| export const EXPERIMENT_STATUS_MAP = { | |||
| TO_RUN: { value: 101, label: '待运行', bgColor: '#BFBFBF' }, | |||
| WAITING: { value: 102, label: '等待中', bgColor: '#409EFF' }, | |||
| RUNNING: { value: 103, label: '运行中', bgColor: '#1890FF' }, | |||
| PAUSED: { value: 104, label: '已暂停', bgColor: '#409EFF' }, | |||
| FINISHED: { value: 202, label: '已完成', bgColor: '#52C41A' }, | |||
| FAILED: { value: 203, label: '运行失败', bgColor: '#F5222D' }, | |||
| }; | |||
| // 成功的实验 | |||
| export const expIsFinished = (code) => [202].includes(code); | |||
| // 进行中实验 | |||
| export const expInprogress = (code) => [101, 102, 103, 104, 105].includes(code); | |||
| // 可暂停实验 | |||
| export const expEnablePause = (code) => [101, 102, 103].includes(code); | |||
| // 可停止实验 | |||
| export const expEnableStop = (code) => [102, 103, 104].includes(code); | |||
| // 可启动实验 | |||
| export const expEnableStart = (code) => [101, 104, 203].includes(code); | |||
| // Trial 状态枚举值 | |||
| export const TRIAL_STATUS_MAP = { | |||
| toRun: { value: 101, label: '待运行', bgColor: '#BFBFBF' }, | |||
| waiting: { value: 102, label: '等待中', bgColor: '#409EFF' }, | |||
| running: { value: 103, label: '运行中', bgColor: '#1890FF' }, | |||
| finished: { value: 201, label: '已完成', bgColor: '#52C41A' }, | |||
| failed: { value: 202, label: '运行失败', bgColor: '#F5222D' }, | |||
| // unknown: { value: 203, label: '未知', bgColor: '#409EFF' }, | |||
| }; | |||
| // 阶段状态枚举值 | |||
| export const STAGE_STATUS_MAP = { | |||
| TO_RUN: { value: 101, label: '待运行', bgColor: '#BFBFBF' }, | |||
| RUNNING: { value: 102, label: '运行中', bgColor: '#1890FF' }, | |||
| FINISHED: { value: 201, label: '已完成', bgColor: '#52C41A' }, | |||
| FAILED: { value: 202, label: '运行失败', bgColor: '#F5222D' }, | |||
| }; | |||
| // 阶段顺序 | |||
| export const STAGE_SEQUENCE = { | |||
| TRAIN: 1, | |||
| SELECT: 2, | |||
| RETRAIN: 3, | |||
| }; | |||
| // 模型类型枚举值 | |||
| export const MODEL_TYPE_ENUM = { | |||
| ImageClassify: { value: 101, label: '图像分类' }, // 图像分类 | |||
| TextClassify: { value: 301, label: '文本分类' }, // 文本分类 | |||
| }; | |||
| // 提供获取模型字段基类方法 | |||
| export const getExpByCode = (value, key) => getValueFromMap(EXPERIMENT_STATUS_MAP, value, key); | |||
| // 提供获取模型字段基类方法 | |||
| export const getModelByCode = (value, key) => getValueFromMap(MODEL_TYPE_ENUM, value, key); | |||
| // 提供获取 Trial 基类方法 | |||
| export const getTrialByCode = (value, key) => getValueFromMap(TRIAL_STATUS_MAP, value, key); | |||
| // 根据阶段 order 获取名称 | |||
| export const getStageName = (stageOrder) => invert(STAGE_SEQUENCE)[stageOrder]; | |||
| export const getStageOrder = (stageName) => STAGE_SEQUENCE[stageName]; | |||
| // 刷新频率 | |||
| export const refreshControls = [ | |||
| { icon: 'el-icon-remove-outline', label: '关闭自动刷新', value: 0 }, | |||
| { icon: 'el-icon-timer', label: '每 10s 刷新', value: 10 }, | |||
| { icon: 'el-icon-timer', label: '每 20s 刷新', value: 20 }, | |||
| { icon: 'el-icon-timer', label: '每 30s 刷新', value: 30 }, | |||
| { icon: 'el-icon-timer', label: '每 60s 刷新', value: 60 }, | |||
| ]; | |||
| // 时间格式 | |||
| export const timeFmts = [ | |||
| { label: 'day', value: 'day' }, | |||
| { label: 'hour', value: 'hour' }, | |||
| { label: 'min', value: 'min' }, | |||
| ]; | |||
| /** | |||
| * 运行时间格式化 | |||
| * @param {Number} ms 运行毫秒数 | |||
| * @returns {String} 返回格式化的时间 | |||
| */ | |||
| export const runTimeFormatter = (ms) => { | |||
| let day; | |||
| let hour; | |||
| let minute; | |||
| if (ms > ONE_DAY) { | |||
| day = Math.floor(ms / ONE_DAY); | |||
| ms %= ONE_DAY; | |||
| } | |||
| if (ms > ONE_HOUR) { | |||
| hour = Math.floor(ms / ONE_HOUR); | |||
| ms %= ONE_HOUR; | |||
| } | |||
| if (ms > ONE_MINUTE) { | |||
| minute = Math.floor(ms / ONE_MINUTE); | |||
| } | |||
| const dayStr = isNil(day) ? '' : `${day}day `; | |||
| const hourStr = isNil(hour) && !dayStr ? '' : `${hour || 0}hour `; | |||
| const minStr = isNil(minute) && !hourStr ? '' : `${minute || 0}min`; | |||
| return `${dayStr}${hourStr}${minStr}`; | |||
| }; | |||
| // 根据时间单位解析时间 | |||
| export const parseRunTime = (time, unit) => { | |||
| const unitMap = { | |||
| day: ONE_DAY, | |||
| hour: ONE_HOUR, | |||
| min: ONE_MINUTE, | |||
| }; | |||
| return time * unitMap[unit] || 0; | |||
| }; | |||
| // 提取图数据的data | |||
| export const extractData = (raw) => { | |||
| const { xField, yField } = raw.config; | |||
| const data = raw.data.map((point) => { | |||
| return { | |||
| [xField]: String(point[xField]), | |||
| [yField]: point[yField], | |||
| }; | |||
| }); | |||
| return data.flat(); // 将二维数组压平成一维数组 | |||
| }; | |||
| export const extractScatterData = (raw) => { | |||
| const { xField, yField } = raw.config; | |||
| const data = raw.data.map((point) => { | |||
| return { | |||
| [xField]: point[xField], | |||
| [yField]: point[yField], | |||
| }; | |||
| }); | |||
| return data.flat(); // 将二维数组压平成一维数组 | |||
| }; | |||
| export const extractSeriesData = (raw) => { | |||
| const { xField, yField, seriesField } = raw.config; | |||
| const data = raw.data.map((d) => { | |||
| return d.list.map((point) => { | |||
| return { | |||
| [xField]: String(point[xField]), | |||
| [yField]: point[yField], | |||
| [seriesField]: d[seriesField], | |||
| }; | |||
| }); | |||
| }); | |||
| return data.flat(); // 将二维数组压平成一维数组 | |||
| }; | |||
| const metricMap = { | |||
| accuracy: expStageAccuracy, | |||
| intermediate: expStageIntermediate, | |||
| }; | |||
| // 根据指定metric获取数据 | |||
| export const fetchMetric = async (experimentId, stageOrder, metricStr) => { | |||
| const metric = await metricMap[metricStr](experimentId, stageOrder); | |||
| return metric; | |||
| }; | |||
| @@ -1,240 +1,82 @@ | |||
| /* | |||
| * Copyright 2019-2020 Zheng Jie | |||
| * | |||
| * 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. | |||
| */ | |||
| /* * Copyright 2019-2020 Zheng Jie * * 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. */ | |||
| <template> | |||
| <div class="app-container"> | |||
| <el-card class="box-card"> | |||
| <!-- 个人信息 --> | |||
| <img :src="user.avatar" title="点击上传头像" class="avatar" @click="openUploadDialog" /> | |||
| <div class="info-row"> | |||
| <div class="info-label">登录账号</div> | |||
| <div class="info-text">{{ user.username }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户昵称</div> | |||
| <div class="info-text">{{ user.nickName }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户性别</div> | |||
| <div class="info-text">{{ user.sex }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">手机号码</div> | |||
| <div class="info-text">{{ user.phone }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户邮箱</div> | |||
| <div class="info-text">{{ user.email }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户角色</div> | |||
| <div class="info-text">{{ userRoles }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户设置</div> | |||
| <div class="info-text"> | |||
| <el-button type="text" @click="infoDialog = true">修改信息</el-button> | |||
| <el-button type="text" @click="$refs.pass.dialog = true">修改密码</el-button> | |||
| <el-button type="text" @click="$refs.email.dialog = true">修改邮箱</el-button> | |||
| </div> | |||
| </div> | |||
| <el-tabs tab-position="left" style="height: 400px;"> | |||
| <el-tab-pane> | |||
| <span slot="label"><i class="el-icon-user"></i> 基本设置</span> | |||
| <user-info></user-info> | |||
| </el-tab-pane> | |||
| <el-tab-pane> | |||
| <span slot="label"><i class="el-icon-setting"></i> 开发者信息</span> | |||
| <div style="margin-left: 30px;"> | |||
| <h4 class="my-10">Token</h4> | |||
| <span>当前用户的唯一登录信息,你可以在命令行里面使用,完成用户鉴权</span> | |||
| <pre class="code flex flex-vertical-align flex-between"> | |||
| <code class="text ellipsis">{{getToken()}}</code> | |||
| <copy-to-clipboard :text="getToken()" @copy="handleCopy"> | |||
| <i class="el-icon-copy-document pointer copy" /> | |||
| </copy-to-clipboard> | |||
| </pre> | |||
| </div> | |||
| </el-tab-pane> | |||
| </el-tabs> | |||
| </el-card> | |||
| <UploadForm | |||
| action="fakeApi" | |||
| title="上传头像" | |||
| :visible="uploadDialogVisible" | |||
| :toggleVisible="handleClose" | |||
| :params="uploadParams" | |||
| :multiple="false" | |||
| :limit="1" | |||
| :showFileCount="false" | |||
| :filters="uploadFilters" | |||
| @uploadSuccess="uploadSuccess" | |||
| @uploadError="uploadError" | |||
| /> | |||
| <BaseModal | |||
| :visible.sync="infoDialog" | |||
| title="修改信息" | |||
| width="450px" | |||
| @cancel="infoDialog = false" | |||
| @open="onDialogOpen" | |||
| @ok="doSubmit" | |||
| > | |||
| <el-form ref="form" :model="form" :rules="rules" style="margin-top: 10px;" label-width="65px"> | |||
| <el-form-item label="昵称" prop="nickName"> | |||
| <el-input v-model="form.nickName" style="width: 40%;" /> | |||
| <span style="margin-left: 10px; color: #c0c0c0;">用户昵称不作为登录使用</span> | |||
| </el-form-item> | |||
| <el-form-item label="手机号" prop="phone"> | |||
| <el-input v-model="form.phone" style="width: 40%;" /> | |||
| <span style="margin-left: 10px; color: #c0c0c0;">一个手机号只能注册一个用户</span> | |||
| </el-form-item> | |||
| <el-form-item label="性别"> | |||
| <el-radio-group v-model="form.sex" style="width: 178px;"> | |||
| <el-radio label="男">男</el-radio> | |||
| <el-radio label="女">女</el-radio> | |||
| </el-radio-group> | |||
| </el-form-item> | |||
| </el-form> | |||
| </BaseModal> | |||
| <updateEmail ref="email" :email="user.email" /> | |||
| <updatePass ref="pass" /> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { mapGetters } from 'vuex'; | |||
| import store from '@/store'; | |||
| import { bucketName, bucketHost } from '@/utils/minIO'; | |||
| import { validateName } from '@/utils/validate'; | |||
| import { invalidFileNameChar } from '@/utils'; | |||
| import { updateAvatar } from '@/api/user'; | |||
| import BaseModal from '@/components/BaseModal'; | |||
| import UploadForm from '@/components/UploadForm'; | |||
| import updateEmail from './components/updateEmail'; | |||
| import updatePass from './components/updatePass'; | |||
| import { Message } from 'element-ui'; | |||
| import CopyToClipboard from 'vue-copy-to-clipboard'; | |||
| import { getToken } from '@/utils/auth'; | |||
| import UserInfo from './userInfo.vue'; | |||
| export default { | |||
| name: 'Center', | |||
| components: { BaseModal, UploadForm, updatePass, updateEmail }, | |||
| data() { | |||
| components: { UserInfo, CopyToClipboard }, | |||
| setup() { | |||
| const handleCopy = () => { | |||
| Message.success('复制成功'); | |||
| }; | |||
| return { | |||
| saveLoading: false, | |||
| infoDialog: false, | |||
| uploadDialogVisible: false, | |||
| form: { | |||
| id: '', | |||
| nickName: '', | |||
| sex: '', | |||
| phone: '', | |||
| }, | |||
| rules: { | |||
| nickName: [ | |||
| { required: true, message: '请输入用户昵称', trigger: 'blur' }, | |||
| { validator: validateName, trigger: 'blur' }, | |||
| ], | |||
| phone: [ | |||
| { required: true, message: '请输入手机号码', trigger: 'blur' }, | |||
| { | |||
| pattern: /^1\d{10}$/, | |||
| message: '请输入正确的11位手机号码', | |||
| trigger: ['blur', 'change'], | |||
| }, | |||
| ], | |||
| }, | |||
| uploadFilters: [invalidFileNameChar], | |||
| handleCopy, | |||
| getToken, | |||
| }; | |||
| }, | |||
| computed: { | |||
| ...mapGetters(['user']), | |||
| userRoles() { | |||
| const roles = this.user.roles || []; | |||
| const names = roles.map((role) => role.name); | |||
| return names.join(' ') || '-'; | |||
| }, | |||
| uploadParams() { | |||
| return { | |||
| objectPath: `avatar/${this.user.id}`, // 对象存储路径 | |||
| }; | |||
| }, | |||
| }, | |||
| created() { | |||
| store.dispatch('GetUserInfo').then(() => {}); | |||
| }, | |||
| methods: { | |||
| uploadSuccess(res) { | |||
| if (!res.length) return; | |||
| const filePath = res[0].data.objectName; | |||
| updateAvatar({ | |||
| path: `${bucketName}/${filePath}`, | |||
| realName: `${bucketHost}/${bucketName}/${filePath}`, | |||
| }).then(() => { | |||
| this.$notify({ | |||
| title: '头像修改成功', | |||
| type: 'success', | |||
| duration: 2500, | |||
| }); | |||
| store.dispatch('GetUserInfo').then(() => {}); | |||
| }); | |||
| }, | |||
| uploadError() { | |||
| this.$message({ | |||
| message: '头像修改失败', | |||
| type: 'error', | |||
| }); | |||
| }, | |||
| openUploadDialog() { | |||
| this.uploadDialogVisible = true; | |||
| }, | |||
| handleClose() { | |||
| this.uploadDialogVisible = false; | |||
| }, | |||
| onDialogOpen() { | |||
| this.form = { | |||
| id: this.user.id, | |||
| nickName: this.user.nickName, | |||
| sex: this.user.sex, | |||
| phone: this.user.phone, | |||
| }; | |||
| }, | |||
| doSubmit() { | |||
| if (this.$refs.form) { | |||
| this.$refs.form.validate((valid) => { | |||
| if (valid) { | |||
| this.saveLoading = true; | |||
| store | |||
| .dispatch('UpdateUserInfo', this.form) | |||
| .then(() => { | |||
| this.editSuccessNotify(); | |||
| this.saveLoading = false; | |||
| this.infoDialog = false; | |||
| }) | |||
| .catch(() => { | |||
| this.saveLoading = false; | |||
| this.infoDialog = false; | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| }, | |||
| }, | |||
| }; | |||
| </script> | |||
| <style rel="stylesheet/scss" lang="scss"> | |||
| .avatar { | |||
| display: block; | |||
| width: 120px; | |||
| height: 120px; | |||
| margin: 20px 0; | |||
| cursor: pointer; | |||
| border-radius: 50%; | |||
| box-shadow: 0 4px 6px rgba(50, 50, 93, 0.31), 0 1px 3px rgba(0, 0, 0, 0.08); | |||
| <style rel="stylesheet/scss" lang="scss" scoped> | |||
| @import '@/assets/styles/variables.scss'; | |||
| ::v-deep.el-tabs--left { | |||
| .el-tabs__item.is-left { | |||
| text-align: left; | |||
| } | |||
| .el-tabs__item.is-active { | |||
| color: #1f89fc; | |||
| background: #e6f7ff; | |||
| } | |||
| } | |||
| .info-row { | |||
| display: flex; | |||
| height: 32px; | |||
| font-size: 14px; | |||
| line-height: 32px; | |||
| .code { | |||
| width: 80%; | |||
| height: 40px; | |||
| padding: 0 20px; | |||
| margin-top: 20px; | |||
| background: #ebedf0; | |||
| } | |||
| .info-label { | |||
| width: 100px; | |||
| .copy { | |||
| font-size: 18px; | |||
| color: $primaryColor; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,231 @@ | |||
| /* * Copyright 2019-2020 Zheng Jie * * 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. */ | |||
| <template> | |||
| <div style="margin-left: 30px;"> | |||
| <div class="box-card"> | |||
| <!-- 个人信息 --> | |||
| <img :src="user.avatar" title="点击上传头像" class="avatar" @click="openUploadDialog" /> | |||
| <div class="info-row"> | |||
| <div class="info-label">登录账号</div> | |||
| <div class="info-text">{{ user.username }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户昵称</div> | |||
| <div class="info-text">{{ user.nickName }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户性别</div> | |||
| <div class="info-text">{{ user.sex }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">手机号码</div> | |||
| <div class="info-text">{{ user.phone }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户邮箱</div> | |||
| <div class="info-text">{{ user.email }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户角色</div> | |||
| <div class="info-text">{{ userRoles }}</div> | |||
| </div> | |||
| <div class="info-row"> | |||
| <div class="info-label">用户设置</div> | |||
| <div class="info-text"> | |||
| <el-button type="text" @click="infoDialog = true">修改信息</el-button> | |||
| <el-button type="text" @click="$refs.pass.dialog = true">修改密码</el-button> | |||
| <el-button type="text" @click="$refs.email.dialog = true">修改邮箱</el-button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <UploadForm | |||
| action="fakeApi" | |||
| title="上传头像" | |||
| :visible="uploadDialogVisible" | |||
| :toggleVisible="handleClose" | |||
| :params="uploadParams" | |||
| :multiple="false" | |||
| :limit="1" | |||
| :showFileCount="false" | |||
| :filters="uploadFilters" | |||
| @uploadSuccess="uploadSuccess" | |||
| @uploadError="uploadError" | |||
| /> | |||
| <BaseModal | |||
| :visible.sync="infoDialog" | |||
| title="修改信息" | |||
| width="450px" | |||
| @cancel="infoDialog = false" | |||
| @open="onDialogOpen" | |||
| @ok="doSubmit" | |||
| > | |||
| <el-form ref="form" :model="form" :rules="rules" style="margin-top: 10px;" label-width="65px"> | |||
| <el-form-item label="昵称" prop="nickName"> | |||
| <el-input v-model="form.nickName" style="width: 40%;" /> | |||
| <span style="margin-left: 10px; color: #c0c0c0;">用户昵称不作为登录使用</span> | |||
| </el-form-item> | |||
| <el-form-item label="手机号" prop="phone"> | |||
| <el-input v-model="form.phone" style="width: 40%;" /> | |||
| <span style="margin-left: 10px; color: #c0c0c0;">一个手机号只能注册一个用户</span> | |||
| </el-form-item> | |||
| <el-form-item label="性别"> | |||
| <el-radio-group v-model="form.sex" style="width: 178px;"> | |||
| <el-radio label="男">男</el-radio> | |||
| <el-radio label="女">女</el-radio> | |||
| </el-radio-group> | |||
| </el-form-item> | |||
| </el-form> | |||
| </BaseModal> | |||
| <updateEmail ref="email" :email="user.email" /> | |||
| <updatePass ref="pass" /> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { mapGetters } from 'vuex'; | |||
| import store from '@/store'; | |||
| import { bucketName, bucketHost } from '@/utils/minIO'; | |||
| import { validateName } from '@/utils/validate'; | |||
| import { invalidFileNameChar } from '@/utils'; | |||
| import { updateAvatar } from '@/api/user'; | |||
| import BaseModal from '@/components/BaseModal'; | |||
| import UploadForm from '@/components/UploadForm'; | |||
| import updateEmail from './components/updateEmail'; | |||
| import updatePass from './components/updatePass'; | |||
| export default { | |||
| name: 'UserInfo', | |||
| components: { BaseModal, UploadForm, updatePass, updateEmail }, | |||
| data() { | |||
| return { | |||
| saveLoading: false, | |||
| infoDialog: false, | |||
| uploadDialogVisible: false, | |||
| form: { | |||
| id: '', | |||
| nickName: '', | |||
| sex: '', | |||
| phone: '', | |||
| }, | |||
| rules: { | |||
| nickName: [ | |||
| { required: true, message: '请输入用户昵称', trigger: 'blur' }, | |||
| { validator: validateName, trigger: 'blur' }, | |||
| ], | |||
| phone: [ | |||
| { required: true, message: '请输入手机号码', trigger: 'blur' }, | |||
| { | |||
| pattern: /^1\d{10}$/, | |||
| message: '请输入正确的11位手机号码', | |||
| trigger: ['blur', 'change'], | |||
| }, | |||
| ], | |||
| }, | |||
| uploadFilters: [invalidFileNameChar], | |||
| }; | |||
| }, | |||
| computed: { | |||
| ...mapGetters(['user']), | |||
| userRoles() { | |||
| const roles = this.user.roles || []; | |||
| const names = roles.map((role) => role.name); | |||
| return names.join(' ') || '-'; | |||
| }, | |||
| uploadParams() { | |||
| return { | |||
| objectPath: `avatar/${this.user.id}`, // 对象存储路径 | |||
| }; | |||
| }, | |||
| }, | |||
| created() { | |||
| store.dispatch('GetUserInfo').then(() => {}); | |||
| }, | |||
| methods: { | |||
| uploadSuccess(res) { | |||
| if (!res.length) return; | |||
| const filePath = res[0].data.objectName; | |||
| updateAvatar({ | |||
| path: `${bucketName}/${filePath}`, | |||
| realName: `${bucketHost}/${bucketName}/${filePath}`, | |||
| }).then(() => { | |||
| this.$notify({ | |||
| title: '头像修改成功', | |||
| type: 'success', | |||
| duration: 2500, | |||
| }); | |||
| store.dispatch('GetUserInfo').then(() => {}); | |||
| }); | |||
| }, | |||
| uploadError() { | |||
| this.$message({ | |||
| message: '头像修改失败', | |||
| type: 'error', | |||
| }); | |||
| }, | |||
| openUploadDialog() { | |||
| this.uploadDialogVisible = true; | |||
| }, | |||
| handleClose() { | |||
| this.uploadDialogVisible = false; | |||
| }, | |||
| onDialogOpen() { | |||
| this.form = { | |||
| id: this.user.id, | |||
| nickName: this.user.nickName, | |||
| sex: this.user.sex, | |||
| phone: this.user.phone, | |||
| }; | |||
| }, | |||
| doSubmit() { | |||
| if (this.$refs.form) { | |||
| this.$refs.form.validate((valid) => { | |||
| if (valid) { | |||
| this.saveLoading = true; | |||
| store | |||
| .dispatch('UpdateUserInfo', this.form) | |||
| .then(() => { | |||
| this.editSuccessNotify(); | |||
| this.saveLoading = false; | |||
| this.infoDialog = false; | |||
| }) | |||
| .catch(() => { | |||
| this.saveLoading = false; | |||
| this.infoDialog = false; | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| }, | |||
| }, | |||
| }; | |||
| </script> | |||
| <style rel="stylesheet/scss" lang="scss"> | |||
| .avatar { | |||
| display: block; | |||
| width: 120px; | |||
| height: 120px; | |||
| margin: 0 0 20px; | |||
| cursor: pointer; | |||
| border-radius: 50%; | |||
| box-shadow: 0 4px 6px rgba(50, 50, 93, 0.31), 0 1px 3px rgba(0, 0, 0, 0.08); | |||
| } | |||
| .info-row { | |||
| display: flex; | |||
| height: 32px; | |||
| font-size: 14px; | |||
| line-height: 32px; | |||
| } | |||
| .info-label { | |||
| width: 100px; | |||
| } | |||
| </style> | |||
| @@ -2,6 +2,199 @@ | |||
| # yarn lockfile v1 | |||
| "@antv/adjust@^0.2.1": | |||
| version "0.2.3" | |||
| resolved "https://registry.yarnpkg.com/@antv/adjust/-/adjust-0.2.3.tgz#c3884a680c3264cc125d7f2ab5398e8a1c0b9401" | |||
| integrity sha512-rihqcCdS7piQnK1nRlCvbIaj2QeaqghxINXiMpTJp+0c9cKlTUwL7/2r+gv9YN5R0P1WzSHTmK2Sn+bQCJDo0Q== | |||
| dependencies: | |||
| "@antv/util" "~2.0.0" | |||
| tslib "^1.10.0" | |||
| "@antv/attr@^0.3.1": | |||
| version "0.3.2" | |||
| resolved "https://registry.yarnpkg.com/@antv/attr/-/attr-0.3.2.tgz#e5866b64870c62f3a9c25b8a61f654ba2bfda051" | |||
| integrity sha512-31PfcVKeQdPBmr/QD+IC0NB/FbdtVKOXBCNMepFc5/dEs7jphmgG1V4tfAJmcXIHubCTHOjpscTrDIvoKSGvMQ== | |||
| dependencies: | |||
| "@antv/color-util" "^2.0.1" | |||
| "@antv/util" "~2.0.0" | |||
| tslib "^1.10.0" | |||
| "@antv/color-util@^2.0.1", "@antv/color-util@^2.0.2": | |||
| version "2.0.6" | |||
| resolved "https://registry.yarnpkg.com/@antv/color-util/-/color-util-2.0.6.tgz#5e129bb9ce3f2b9309b52102b3dc929430ccc016" | |||
| integrity sha512-KnPEaAH+XNJMjax9U35W67nzPI+QQ2x27pYlzmSIWrbj4/k8PGrARXfzDTjwoozHJY8qG62Z+Ww6Alhu2FctXQ== | |||
| dependencies: | |||
| "@antv/util" "^2.0.9" | |||
| tslib "^2.0.3" | |||
| "@antv/component@^0.8.7": | |||
| version "0.8.13" | |||
| resolved "https://registry.yarnpkg.com/@antv/component/-/component-0.8.13.tgz#7fa57ec01ff4fd8048979d6daa84bd53ee7167a8" | |||
| integrity sha512-VCCVQUA9/Scxlrc8N8VDY/hTCvpQwoHmSqgu15viYpyDMx8lBuzrWOdBy2IbHMbjrPFwMw7wOCmAGQL76yeFQA== | |||
| dependencies: | |||
| "@antv/dom-util" "~2.0.1" | |||
| "@antv/g-base" "0.5.6" | |||
| "@antv/matrix-util" "^3.1.0-beta.1" | |||
| "@antv/path-util" "~2.0.7" | |||
| "@antv/scale" "~0.3.1" | |||
| "@antv/util" "~2.0.0" | |||
| fecha "~4.2.0" | |||
| tslib "^2.0.3" | |||
| "@antv/coord@^0.3.0": | |||
| version "0.3.1" | |||
| resolved "https://registry.yarnpkg.com/@antv/coord/-/coord-0.3.1.tgz#982e261d8a1e06a198eb518ea7acc20ed875a019" | |||
| integrity sha512-rFE94C8Xzbx4xmZnHh2AnlB3Qm1n5x0VT3OROy257IH6Rm4cuzv1+tZaUBATviwZd99S+rOY9telw/+6C9GbRw== | |||
| dependencies: | |||
| "@antv/matrix-util" "^3.1.0-beta.2" | |||
| "@antv/util" "~2.0.12" | |||
| tslib "^2.1.0" | |||
| "@antv/dom-util@^2.0.2", "@antv/dom-util@~2.0.1": | |||
| version "2.0.3" | |||
| resolved "https://registry.yarnpkg.com/@antv/dom-util/-/dom-util-2.0.3.tgz#cbd158b1c88e0e8a4d865871a5969b1190554ff5" | |||
| integrity sha512-dUHsUT4U9X1T1/Y9bH3jRMe0MzvWJk2jSQm1vm3w9NX+Ra0ftq5VUBiGTNbthm3nFwG0fFFjip904rYjl50g4A== | |||
| dependencies: | |||
| tslib "^2.0.3" | |||
| "@antv/event-emitter@^0.1.1", "@antv/event-emitter@^0.1.2", "@antv/event-emitter@~0.1.0": | |||
| version "0.1.2" | |||
| resolved "https://registry.yarnpkg.com/@antv/event-emitter/-/event-emitter-0.1.2.tgz#a17b7cb86e6d071880dc6bfb232756f88624ecbc" | |||
| integrity sha512-6C6NJOdoNVptCr5y9BVOhKkCgW7LFs/SpcRyAExUeSjAm0zJqcqNkSIRGsXYhj4PJI+CZICHzGwwiSnIsE68Ug== | |||
| "@antv/g-base@0.5.6": | |||
| version "0.5.6" | |||
| resolved "https://registry.yarnpkg.com/@antv/g-base/-/g-base-0.5.6.tgz#d96da5fbf6c5f8b073072751e15e5eec70b393fc" | |||
| integrity sha512-szxqFQ/xdCnfaeSEEC2kVjXdKxJnvKKJNT0MvaOG3UXOfsjPDLgb3IKLr+bU3sLvTAQfPhsbtYh7mWb03+mGjA== | |||
| dependencies: | |||
| "@antv/event-emitter" "^0.1.1" | |||
| "@antv/g-math" "^0.1.6" | |||
| "@antv/matrix-util" "^3.1.0-beta.1" | |||
| "@antv/path-util" "~2.0.5" | |||
| "@antv/util" "~2.0.0" | |||
| "@types/d3-timer" "^2.0.0" | |||
| d3-ease "^1.0.5" | |||
| d3-interpolate "^1.3.2" | |||
| d3-timer "^1.0.9" | |||
| detect-browser "^5.1.0" | |||
| tslib "^2.0.3" | |||
| "@antv/g-base@^0.5.3", "@antv/g-base@~0.5.6": | |||
| version "0.5.9" | |||
| resolved "https://registry.yarnpkg.com/@antv/g-base/-/g-base-0.5.9.tgz#58d0e11d85157ada1408fbdf24f4f468f40e59cd" | |||
| integrity sha512-IAzuCLRmz9cKCWUKR3cKWgLZ/6OQYpTCIOgxAP8Bc+HRw0mu8iC3OTz+tWKGv9L8unpvCvpQd1H+OTTjdg/TpQ== | |||
| dependencies: | |||
| "@antv/event-emitter" "^0.1.1" | |||
| "@antv/g-math" "^0.1.6" | |||
| "@antv/matrix-util" "^3.1.0-beta.1" | |||
| "@antv/path-util" "~2.0.5" | |||
| "@antv/util" "~2.0.0" | |||
| "@types/d3-timer" "^2.0.0" | |||
| d3-ease "^1.0.5" | |||
| d3-interpolate "^1.3.2" | |||
| d3-timer "^1.0.9" | |||
| detect-browser "^5.1.0" | |||
| tslib "^2.0.3" | |||
| "@antv/g-canvas@~0.5.10": | |||
| version "0.5.10" | |||
| resolved "https://registry.yarnpkg.com/@antv/g-canvas/-/g-canvas-0.5.10.tgz#ad19a1dcd19edd12d29539e1dc5b521585b437c6" | |||
| integrity sha512-U454VM7TlO/y1fYP9B9Fbj4QCl/CjQDxaCAHzg8SKq5FecSUseh7Gjliv4YMb3QAb9UCaNx0RUpobUCFBZgLhg== | |||
| dependencies: | |||
| "@antv/g-base" "^0.5.3" | |||
| "@antv/g-math" "^0.1.6" | |||
| "@antv/matrix-util" "^3.1.0-beta.1" | |||
| "@antv/path-util" "~2.0.5" | |||
| "@antv/util" "~2.0.0" | |||
| gl-matrix "^3.0.0" | |||
| tslib "^2.0.3" | |||
| "@antv/g-math@^0.1.6": | |||
| version "0.1.7" | |||
| resolved "https://registry.yarnpkg.com/@antv/g-math/-/g-math-0.1.7.tgz#6ec2769269f7ccb67e58140d5739df74046cc04e" | |||
| integrity sha512-xGyXaloD1ynfp7gS4VuV+MjSptZIwHvLHr8ekXJSFAeWPYLu84yOW2wOZHDdp1bzDAIuRv6xDBW58YGHrWsFcA== | |||
| dependencies: | |||
| "@antv/util" "~2.0.0" | |||
| gl-matrix "^3.0.0" | |||
| "@antv/g-svg@~0.5.6": | |||
| version "0.5.6" | |||
| resolved "https://registry.yarnpkg.com/@antv/g-svg/-/g-svg-0.5.6.tgz#70b2fa980c431b39ad3c5b4b53e36a1d60957d65" | |||
| integrity sha512-Xve1EUGk4HMbl2nq4ozR4QLh6GyoZ8Xw/+9kHYI4B5P2lIUQU95MuRsaLFfW5NNpZDx85ZeH97tqEmC9L96E7A== | |||
| dependencies: | |||
| "@antv/g-base" "^0.5.3" | |||
| "@antv/g-math" "^0.1.6" | |||
| "@antv/util" "~2.0.0" | |||
| detect-browser "^5.0.0" | |||
| tslib "^2.0.3" | |||
| "@antv/g2@^4.1.0": | |||
| version "4.1.19" | |||
| resolved "https://registry.yarnpkg.com/@antv/g2/-/g2-4.1.19.tgz#d0da4a883e7674db463fd090cac7498237916730" | |||
| integrity sha512-dRO32/8TcdsalkQ1f4IHywLsfFNj2+seun+RzdGsV7B9yvITFeZN6PQlscCzCUuE0KljMIbCHpVhuLzp3bWR6A== | |||
| dependencies: | |||
| "@antv/adjust" "^0.2.1" | |||
| "@antv/attr" "^0.3.1" | |||
| "@antv/color-util" "^2.0.2" | |||
| "@antv/component" "^0.8.7" | |||
| "@antv/coord" "^0.3.0" | |||
| "@antv/dom-util" "^2.0.2" | |||
| "@antv/event-emitter" "~0.1.0" | |||
| "@antv/g-base" "~0.5.6" | |||
| "@antv/g-canvas" "~0.5.10" | |||
| "@antv/g-svg" "~0.5.6" | |||
| "@antv/matrix-util" "^3.1.0-beta.1" | |||
| "@antv/path-util" "^2.0.3" | |||
| "@antv/scale" "^0.3.7" | |||
| "@antv/util" "~2.0.5" | |||
| tslib "^2.0.0" | |||
| "@antv/g2plot@^2.3.17": | |||
| version "2.3.24" | |||
| resolved "https://registry.yarnpkg.com/@antv/g2plot/-/g2plot-2.3.24.tgz#92d8a6157a6e6d8999ea5072f57ebe5c73473c16" | |||
| integrity sha512-JP/8l72iOJSFuuZHe7nYvNqkwgDg5f6W5DbXXZiLOiXKBWwFTpiNiAvKI4F5yJBSYcxPcgz+hKHrEBc8jO5K5g== | |||
| dependencies: | |||
| "@antv/event-emitter" "^0.1.2" | |||
| "@antv/g2" "^4.1.0" | |||
| d3-hierarchy "^2.0.0" | |||
| d3-regression "^1.3.5" | |||
| pdfast "^0.2.0" | |||
| size-sensor "^1.0.1" | |||
| tslib "^2.0.3" | |||
| "@antv/matrix-util@^3.1.0-beta.1", "@antv/matrix-util@^3.1.0-beta.2": | |||
| version "3.1.0-beta.2" | |||
| resolved "https://registry.yarnpkg.com/@antv/matrix-util/-/matrix-util-3.1.0-beta.2.tgz#b4afafb70dbdf52affca308d3546c8a090fd23ca" | |||
| integrity sha512-Efwp0ZHxVDK/8RUa/RRWN7HKFHJmjn7Oq5HaNBbCmsxd7JTla3Zsoq1AZrjWMDlq0lplo77urclwI+XIW8NEHw== | |||
| dependencies: | |||
| "@antv/util" "^2.0.9" | |||
| gl-matrix "^3.3.0" | |||
| tslib "^1.10.0" | |||
| "@antv/path-util@^2.0.3", "@antv/path-util@~2.0.5", "@antv/path-util@~2.0.7": | |||
| version "2.0.9" | |||
| resolved "https://registry.yarnpkg.com/@antv/path-util/-/path-util-2.0.9.tgz#976e4a3cfb6219767a602d297b205c88d66d7b2c" | |||
| integrity sha512-kunEz4dNheQMVn4rVFsoBDx+n9Knfi3uRLvDk9SojZAqpninsjFhdoiYtbExwJGz1FYGtiV10Y6N1tp73kZFcg== | |||
| dependencies: | |||
| "@antv/util" "^2.0.9" | |||
| tslib "^2.0.3" | |||
| "@antv/scale@^0.3.7", "@antv/scale@~0.3.1": | |||
| version "0.3.11" | |||
| resolved "https://registry.yarnpkg.com/@antv/scale/-/scale-0.3.11.tgz#3ce10445e108a9bc208840c3394507f0cfb44e68" | |||
| integrity sha512-nARq88j77tADJZ+TqUgrZnJAXuVf5U553TOGrhJ5aL8eEkPxn6ZoroT78oK09zojrjeRPqOSGJl7Md3fmHk/kg== | |||
| dependencies: | |||
| "@antv/util" "~2.0.3" | |||
| fecha "~4.2.0" | |||
| tslib "^2.0.0" | |||
| "@antv/util@^2.0.9", "@antv/util@~2.0.0", "@antv/util@~2.0.12", "@antv/util@~2.0.3", "@antv/util@~2.0.5": | |||
| version "2.0.14" | |||
| resolved "https://registry.yarnpkg.com/@antv/util/-/util-2.0.14.tgz#1ac8c4f790beaf6572daecf62df6aa55fa0a31df" | |||
| integrity sha512-iwM4XKRzW7pbBnMnSGKqcNGo3FdDzMGbRojAiMQ2KC0bTwtLEphQ+hYWa1c+O9BuHtcMkVvTVDylHNESL5vE5g== | |||
| dependencies: | |||
| tslib "^2.0.3" | |||
| "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.12.13": | |||
| version "7.12.13" | |||
| resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" | |||
| @@ -964,6 +1157,14 @@ | |||
| "@nodelib/fs.scandir" "2.1.4" | |||
| fastq "^1.6.0" | |||
| "@opd/g2plot-vue@3.1.12": | |||
| version "3.1.12" | |||
| resolved "https://registry.yarnpkg.com/@opd/g2plot-vue/-/g2plot-vue-3.1.12.tgz#3d8b8a8a478d998808427873c5fbc10d59588070" | |||
| integrity sha512-Rp5ieCbTVe2CZgBBGhKowj7OrkI0Y3BE784kE5bYF6Ol0ALcnj+v8Z1W1NBg+lHZrkisMH1zhFDCWNQX5ObSNg== | |||
| dependencies: | |||
| core-js "^3.9.1" | |||
| vue-demi "^0.7.4" | |||
| "@riophae/vue-treeselect@0.1.0": | |||
| version "0.1.0" | |||
| resolved "https://registry.npmjs.org/@riophae/vue-treeselect/-/vue-treeselect-0.1.0.tgz#39bb5b6757047008e27b6cddf33efde5d94c6efc" | |||
| @@ -1010,6 +1211,11 @@ | |||
| remark "^13.0.0" | |||
| unist-util-find-all-after "^3.0.2" | |||
| "@types/d3-timer@^2.0.0": | |||
| version "2.0.0" | |||
| resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-2.0.0.tgz#9901bb02af38798764674df17d66b07329705632" | |||
| integrity sha512-l6stHr1VD1BWlW6u3pxrjLtJfpPZq9I3XmKIQtq7zHM/s6fwEtI1Yn6Sr5/jQTrUDCC5jkS6gWqlFGCDArDqNg== | |||
| "@types/glob@^7.1.1": | |||
| version "7.1.3" | |||
| resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" | |||
| @@ -1792,6 +1998,11 @@ argparse@^1.0.7: | |||
| dependencies: | |||
| sprintf-js "~1.0.2" | |||
| argparse@^2.0.1: | |||
| version "2.0.1" | |||
| resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" | |||
| integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== | |||
| arr-diff@^2.0.0: | |||
| version "2.0.0" | |||
| resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" | |||
| @@ -3108,6 +3319,11 @@ code-point-at@^1.0.0: | |||
| resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" | |||
| integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= | |||
| codemirror@^5.60.0: | |||
| version "5.62.0" | |||
| resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.62.0.tgz#e9ecd012e6f9eaf2e05ff4a449ff750f51619e22" | |||
| integrity sha512-Xnl3304iCc8nyVZuRkzDVVwc794uc9QNX0UcPGeNic1fbzkSrO4l4GVXho9tRNKBgPYZXgocUqXyfIv3BILhCQ== | |||
| codepage@~1.14.0: | |||
| version "1.14.0" | |||
| resolved "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99" | |||
| @@ -3425,6 +3641,11 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7, core-js@^2.6.5: | |||
| resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" | |||
| integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== | |||
| core-js@^3.9.1: | |||
| version "3.15.1" | |||
| resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.1.tgz#6c08ab88abdf56545045ccf5fd81f47f407e7f1a" | |||
| integrity sha512-h8VbZYnc9pDzueiS2610IULDkpFFPunHwIpl8yRwFahAEEdSpHlTy3h3z3rKq5h11CaUdBEeRViu9AYvbxiMeg== | |||
| core-util-is@1.0.2, core-util-is@~1.0.0: | |||
| version "1.0.2" | |||
| resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" | |||
| @@ -3805,9 +4026,9 @@ d3-dsv@1: | |||
| iconv-lite "0.4" | |||
| rw "1" | |||
| d3-ease@1: | |||
| d3-ease@1, d3-ease@^1.0.5: | |||
| version "1.0.7" | |||
| resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" | |||
| resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" | |||
| integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== | |||
| d3-fetch@1: | |||
| @@ -3844,9 +4065,14 @@ d3-hierarchy@1: | |||
| resolved "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" | |||
| integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== | |||
| d3-interpolate@1: | |||
| d3-hierarchy@^2.0.0: | |||
| version "2.0.0" | |||
| resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz#dab88a58ca3e7a1bc6cab390e89667fcc6d20218" | |||
| integrity sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw== | |||
| d3-interpolate@1, d3-interpolate@^1.3.2: | |||
| version "1.4.0" | |||
| resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" | |||
| resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" | |||
| integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== | |||
| dependencies: | |||
| d3-color "1" | |||
| @@ -3871,6 +4097,11 @@ d3-random@1: | |||
| resolved "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" | |||
| integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== | |||
| d3-regression@^1.3.5: | |||
| version "1.3.9" | |||
| resolved "https://registry.yarnpkg.com/d3-regression/-/d3-regression-1.3.9.tgz#61c34acb9b6bbd9172ede89f05d0b7fbd57ccdc0" | |||
| integrity sha512-PoMpToIvxSnVpgAZTCERVseRend40JIBICJxwATJ/T4laWGaI5dpRdRxrPITxD8hk8W455fKonVChwSmDyWEyg== | |||
| d3-scale-chromatic@1: | |||
| version "1.5.0" | |||
| resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98" | |||
| @@ -3915,9 +4146,9 @@ d3-time@1: | |||
| resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" | |||
| integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== | |||
| d3-timer@1: | |||
| d3-timer@1, d3-timer@^1.0.9: | |||
| version "1.0.10" | |||
| resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" | |||
| resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" | |||
| integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== | |||
| d3-transition@1: | |||
| @@ -4224,6 +4455,11 @@ destroy@~1.0.4: | |||
| resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" | |||
| integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= | |||
| detect-browser@^5.0.0, detect-browser@^5.1.0: | |||
| version "5.2.0" | |||
| resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.2.0.tgz#c9cd5afa96a6a19fda0bbe9e9be48a6b6e1e9c97" | |||
| integrity sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA== | |||
| detect-indent@^4.0.0: | |||
| version "4.0.0" | |||
| resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" | |||
| @@ -5284,6 +5520,11 @@ fb-watchman@^2.0.0: | |||
| dependencies: | |||
| bser "2.1.1" | |||
| fecha@~4.2.0: | |||
| version "4.2.1" | |||
| resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" | |||
| integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== | |||
| fflate@^0.3.8: | |||
| version "0.3.11" | |||
| resolved "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz#2c440d7180fdeb819e64898d8858af327b042a5d" | |||
| @@ -5792,6 +6033,11 @@ git-raw-commits@^2.0.0: | |||
| split2 "^3.0.0" | |||
| through2 "^4.0.0" | |||
| gl-matrix@^3.0.0, gl-matrix@^3.3.0: | |||
| version "3.3.0" | |||
| resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" | |||
| integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== | |||
| glob-base@^0.3.0: | |||
| version "0.3.0" | |||
| resolved "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" | |||
| @@ -7585,11 +7831,23 @@ js-yaml@^3.13.1, js-yaml@^3.7.0, js-yaml@^3.9.1: | |||
| argparse "^1.0.7" | |||
| esprima "^4.0.0" | |||
| js-yaml@^4.0.0: | |||
| version "4.1.0" | |||
| resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" | |||
| integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== | |||
| dependencies: | |||
| argparse "^2.0.1" | |||
| jsbn@~0.1.0: | |||
| version "0.1.1" | |||
| resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" | |||
| integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= | |||
| jschardet@^2.2.1: | |||
| version "2.3.0" | |||
| resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" | |||
| integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== | |||
| jsdom@^11.5.1: | |||
| version "11.12.0" | |||
| resolved "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" | |||
| @@ -9521,6 +9779,11 @@ pbkdf2@^3.0.3: | |||
| safe-buffer "^5.0.1" | |||
| sha.js "^2.4.8" | |||
| pdfast@^0.2.0: | |||
| version "0.2.0" | |||
| resolved "https://registry.yarnpkg.com/pdfast/-/pdfast-0.2.0.tgz#8cbc556e1bf2522177787c0de2e0d4373ba885c9" | |||
| integrity sha1-jLxVbhvyUiF3eHwN4uDUNzuohck= | |||
| performance-now@^2.1.0: | |||
| version "2.1.0" | |||
| resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" | |||
| @@ -11220,6 +11483,11 @@ sisteransi@^0.1.1: | |||
| resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" | |||
| integrity sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g== | |||
| size-sensor@^1.0.1: | |||
| version "1.0.1" | |||
| resolved "https://registry.yarnpkg.com/size-sensor/-/size-sensor-1.0.1.tgz#f84e46206d3e259faff1d548e4b3beca93219dbb" | |||
| integrity sha512-QTy7MnuugCFXIedXRpUSk9gUnyNiaxIdxGfUjr8xxXOqIB3QvBUYP9+b51oCg2C4dnhaeNk/h57TxjbvoJrJUA== | |||
| slash@^1.0.0: | |||
| version "1.0.0" | |||
| resolved "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" | |||
| @@ -12221,6 +12489,11 @@ tslib@^1.10.0, tslib@^1.9.0: | |||
| resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" | |||
| integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== | |||
| tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0: | |||
| version "2.3.0" | |||
| resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" | |||
| integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== | |||
| tslib@^2.2.0: | |||
| version "2.2.0" | |||
| resolved "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" | |||
| @@ -12446,6 +12719,11 @@ urix@^0.1.0: | |||
| resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" | |||
| integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= | |||
| url-join@^4.0.1: | |||
| version "4.0.1" | |||
| resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" | |||
| integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== | |||
| url-loader@^1.1.2: | |||
| version "1.1.2" | |||
| resolved "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" | |||
| @@ -12630,6 +12908,11 @@ vue-copy-to-clipboard@^1.0.3: | |||
| dependencies: | |||
| copy-to-clipboard "^3.3.1" | |||
| vue-demi@^0.7.4: | |||
| version "0.7.5" | |||
| resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.7.5.tgz#88dee7492fc99a0f911ff03fc02c658fa2a79af8" | |||
| integrity sha512-eFSQSvbQdY7C9ujOzvM6tn7XxwLjn0VQDXQsiYBLBwf28Na+2nTQR4BBBcomhmdP6mmHlBKAwarq6a0BPG87hQ== | |||
| vue-eslint-parser@^2.0.3: | |||
| version "2.0.3" | |||
| resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1" | |||