@@ -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" | |||