Browse Source

release v2.1 webapp

tags/V3.0
之江天枢 yeyue 2 years ago
parent
commit
0bdd432153
75 changed files with 8090 additions and 878 deletions
  1. +6
    -0
      webapp/CHANGELOG.md
  2. +1
    -1
      webapp/babel.config.js
  3. +8
    -1
      webapp/package.json
  4. +292
    -0
      webapp/src/api/tadl/index.js
  5. +106
    -0
      webapp/src/api/tadl/strategy.js
  6. +16
    -0
      webapp/src/assets/styles/atomic.scss
  7. +4
    -0
      webapp/src/assets/styles/common.scss
  8. +26
    -0
      webapp/src/assets/styles/element-ui.scss
  9. +6
    -0
      webapp/src/assets/styles/index.scss
  10. +7
    -0
      webapp/src/assets/styles/mixin.scss
  11. +19
    -17
      webapp/src/components/BaseTable/index.vue
  12. +42
    -0
      webapp/src/components/BaseTooltip/index.vue
  13. +66
    -0
      webapp/src/components/Description/index.vue
  14. +50
    -0
      webapp/src/components/Description/item.vue
  15. +1
    -1
      webapp/src/components/IconFont/index.js
  16. +49
    -26
      webapp/src/components/InlineTableEdit/index.vue
  17. +13
    -15
      webapp/src/components/ProTable/header.vue
  18. +51
    -21
      webapp/src/components/ProTable/index.vue
  19. +43
    -0
      webapp/src/components/Statistic/index.vue
  20. +8
    -15
      webapp/src/components/Training/saveModelDialog.vue
  21. +133
    -0
      webapp/src/components/YamlEditor/index.vue
  22. +1
    -0
      webapp/src/config/index.js
  23. +1
    -0
      webapp/src/hooks/index.js
  24. +55
    -0
      webapp/src/hooks/keepPageInfo/index.js
  25. +1
    -1
      webapp/src/hooks/mapGetters/index.js
  26. +40
    -15
      webapp/src/layout/DetailLayout.vue
  27. +3
    -4
      webapp/src/lib/api-map/index.js
  28. +50
    -0
      webapp/src/store/modules/tadl.js
  29. +2
    -3
      webapp/src/utils/VisualUtils/request.js
  30. +21
    -0
      webapp/src/utils/base.js
  31. +6
    -1
      webapp/src/utils/constant.js
  32. +28
    -1
      webapp/src/utils/utils.js
  33. +9
    -0
      webapp/src/utils/validate.js
  34. +141
    -124
      webapp/src/views/dataset/list/create-dataset.vue
  35. +58
    -322
      webapp/src/views/dataset/list/import-dataset.vue
  36. +17
    -50
      webapp/src/views/dataset/list/index.vue
  37. +17
    -16
      webapp/src/views/dataset/list/upload-datafile.vue
  38. +15
    -1
      webapp/src/views/dataset/util/index.js
  39. +19
    -21
      webapp/src/views/system/menu/index.vue
  40. +1
    -0
      webapp/src/views/system/resources/utils.js
  41. +34
    -0
      webapp/src/views/tadl/detail/components/chart.vue
  42. +34
    -0
      webapp/src/views/tadl/detail/components/chartCard.vue
  43. +59
    -0
      webapp/src/views/tadl/detail/components/config.vue
  44. +230
    -0
      webapp/src/views/tadl/detail/components/detailDashboard.vue
  45. +38
    -0
      webapp/src/views/tadl/detail/components/general.vue
  46. +181
    -0
      webapp/src/views/tadl/detail/components/parameter.vue
  47. +282
    -0
      webapp/src/views/tadl/detail/components/runParameter.vue
  48. +134
    -0
      webapp/src/views/tadl/detail/components/saveModelModal.vue
  49. +63
    -0
      webapp/src/views/tadl/detail/components/singleTrialStat.vue
  50. +69
    -0
      webapp/src/views/tadl/detail/components/stat.vue
  51. +98
    -0
      webapp/src/views/tadl/detail/components/trials.vue
  52. +383
    -0
      webapp/src/views/tadl/detail/components/trialsList.vue
  53. +513
    -0
      webapp/src/views/tadl/detail/index.vue
  54. +141
    -0
      webapp/src/views/tadl/detail/retrain.vue
  55. +130
    -0
      webapp/src/views/tadl/detail/select.vue
  56. +71
    -0
      webapp/src/views/tadl/detail/stage.vue
  57. +144
    -0
      webapp/src/views/tadl/detail/train.vue
  58. +24
    -0
      webapp/src/views/tadl/detail/util.js
  59. +406
    -0
      webapp/src/views/tadl/formPage/components/tadlForm.vue
  60. +523
    -0
      webapp/src/views/tadl/formPage/components/tadlStageForm.vue
  61. +170
    -0
      webapp/src/views/tadl/formPage/index.vue
  62. +21
    -0
      webapp/src/views/tadl/formPage/style/index.scss
  63. +48
    -0
      webapp/src/views/tadl/formPage/utils.js
  64. +127
    -0
      webapp/src/views/tadl/list/components/listStatus.vue
  65. +220
    -0
      webapp/src/views/tadl/list/index.vue
  66. +175
    -0
      webapp/src/views/tadl/list/util.js
  67. +559
    -0
      webapp/src/views/tadl/strategy/components/CreatePageForm.vue
  68. +101
    -0
      webapp/src/views/tadl/strategy/components/ReleaseDialog.vue
  69. +514
    -0
      webapp/src/views/tadl/strategy/components/StrategyDrawer.vue
  70. +329
    -0
      webapp/src/views/tadl/strategy/index.vue
  71. +75
    -0
      webapp/src/views/tadl/strategy/util.js
  72. +184
    -0
      webapp/src/views/tadl/util.js
  73. +58
    -216
      webapp/src/views/user/center.vue
  74. +231
    -0
      webapp/src/views/user/userInfo.vue
  75. +289
    -6
      webapp/yarn.lock

+ 6
- 0
webapp/CHANGELOG.md View File

@@ -1,3 +1,9 @@
## 2.1.0 (2021-12-22)

### Breaking Change

- [自动机器学习] 新增自动机器学习模块

## 2.0.0 (2021-08-30)

### Breaking Change


+ 1
- 1
webapp/babel.config.js View File

@@ -8,5 +8,5 @@ if (process.env.NODE_ENV === "production") {
}
module.exports = {
plugins,
presets: ["@vue/app"],
presets: [["@vue/app",{ useBuiltIns: "entry" }]],
};

+ 8
- 1
webapp/package.json View File

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


+ 292
- 0
webapp/src/api/tadl/index.js View File

@@ -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,
});
}

+ 106
- 0
webapp/src/api/tadl/strategy.js View File

@@ -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,
});
}

+ 16
- 0
webapp/src/assets/styles/atomic.scss View File

@@ -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;
}

+ 4
- 0
webapp/src/assets/styles/common.scss View File

@@ -264,6 +264,10 @@ pre {
color: $infoColor;
}

.CodeMirror-lint-tooltip {
z-index: 10000 !important;
}

.app-result-content {
padding: 24px 40px;
margin-top: 24px;


+ 26
- 0
webapp/src/assets/styles/element-ui.scss View File

@@ -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);
}

+ 6
- 0
webapp/src/assets/styles/index.scss View File

@@ -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;
}


+ 7
- 0
webapp/src/assets/styles/mixin.scss View File

@@ -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%;


+ 19
- 17
webapp/src/components/BaseTable/index.vue View File

@@ -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,
};
},
};


+ 42
- 0
webapp/src/components/BaseTooltip/index.vue View File

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

+ 66
- 0
webapp/src/components/Description/index.vue View File

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

+ 50
- 0
webapp/src/components/Description/item.vue View File

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

+ 1
- 1
webapp/src/components/IconFont/index.js View File

@@ -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' },
});



+ 49
- 26
webapp/src/components/InlineTableEdit/index.vue View File

@@ -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,
};
},


+ 13
- 15
webapp/src/components/ProTable/header.vue View File

@@ -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 }) {
// 点击创建按钮,抛出创建事件


+ 51
- 21
webapp/src/components/ProTable/index.vue View File

@@ -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,
};
},
};


+ 43
- 0
webapp/src/components/Statistic/index.vue View File

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

+ 8
- 15
webapp/src/components/Training/saveModelDialog.vue View File

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


+ 133
- 0
webapp/src/components/YamlEditor/index.vue View File

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

+ 1
- 0
webapp/src/config/index.js View File

@@ -30,6 +30,7 @@ export const API_MODULE_NAME = {
ATLAS: 'measure', // 模型炼知
K8S: 'k8s', // K8S
DCM: 'dcm', // 医学dcm
TADL: 'tadl', // TADL
DUBHE_PRO: 'terminal', // 天枢专业版
};



+ 1
- 0
webapp/src/hooks/index.js View File

@@ -25,3 +25,4 @@ export * from './dict';
export * from './localStorage';
export * from './pagination';
export * from './sort';
export * from './keepPageInfo';

+ 55
- 0
webapp/src/hooks/keepPageInfo/index.js View File

@@ -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,
};
}

+ 1
- 1
webapp/src/hooks/mapGetters/index.js View File

@@ -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;
}

+ 40
- 15
webapp/src/layout/DetailLayout.vue View File

@@ -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);


+ 3
- 4
webapp/src/lib/api-map/index.js View File

@@ -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),
},
];



+ 50
- 0
webapp/src/store/modules/tadl.js View File

@@ -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,
};

+ 2
- 3
webapp/src/utils/VisualUtils/request.js View File

@@ -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,
});


+ 21
- 0
webapp/src/utils/base.js View File

@@ -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);
};


+ 6
- 1
webapp/src/utils/constant.js View File

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

+ 28
- 1
webapp/src/utils/utils.js View File

@@ -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 = '';


+ 9
- 0
webapp/src/utils/validate.js View File

@@ -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 };

/**


+ 141
- 124
webapp/src/views/dataset/list/create-dataset.vue View File

@@ -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,
};
},


+ 58
- 322
webapp/src/views/dataset/list/import-dataset.vue View File

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

+ 17
- 50
webapp/src/views/dataset/list/index.vue View File

@@ -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, {


+ 17
- 16
webapp/src/views/dataset/list/upload-datafile.vue View File

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


+ 15
- 1
webapp/src/views/dataset/util/index.js View File

@@ -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',
},
];

+ 19
- 21
webapp/src/views/system/menu/index.vue View File

@@ -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' }],
},
};
},


+ 1
- 0
webapp/src/views/system/resources/utils.js View File

@@ -19,6 +19,7 @@ export const moduleMap = {
1: 'notebook',
2: 'train',
3: 'serving',
4: 'tadl',
};

const resourcesPoolTypeMap = {


+ 34
- 0
webapp/src/views/tadl/detail/components/chart.vue View File

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

+ 34
- 0
webapp/src/views/tadl/detail/components/chartCard.vue View File

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

+ 59
- 0
webapp/src/views/tadl/detail/components/config.vue View File

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

+ 230
- 0
webapp/src/views/tadl/detail/components/detailDashboard.vue View File

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

+ 38
- 0
webapp/src/views/tadl/detail/components/general.vue View File

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

+ 181
- 0
webapp/src/views/tadl/detail/components/parameter.vue View File

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

+ 282
- 0
webapp/src/views/tadl/detail/components/runParameter.vue View File

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

+ 134
- 0
webapp/src/views/tadl/detail/components/saveModelModal.vue View File

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

+ 63
- 0
webapp/src/views/tadl/detail/components/singleTrialStat.vue View File

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

+ 69
- 0
webapp/src/views/tadl/detail/components/stat.vue View File

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

+ 98
- 0
webapp/src/views/tadl/detail/components/trials.vue View File

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

+ 383
- 0
webapp/src/views/tadl/detail/components/trialsList.vue View File

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

+ 513
- 0
webapp/src/views/tadl/detail/index.vue View File

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

+ 141
- 0
webapp/src/views/tadl/detail/retrain.vue View File

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

+ 130
- 0
webapp/src/views/tadl/detail/select.vue View File

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

+ 71
- 0
webapp/src/views/tadl/detail/stage.vue View File

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

+ 144
- 0
webapp/src/views/tadl/detail/train.vue View File

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

+ 24
- 0
webapp/src/views/tadl/detail/util.js View File

@@ -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,
}))
);

+ 406
- 0
webapp/src/views/tadl/formPage/components/tadlForm.vue View File

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

+ 523
- 0
webapp/src/views/tadl/formPage/components/tadlStageForm.vue View File

@@ -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">&nbsp;</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>

+ 170
- 0
webapp/src/views/tadl/formPage/index.vue View File

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

+ 21
- 0
webapp/src/views/tadl/formPage/style/index.scss View File

@@ -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;
}

+ 48
- 0
webapp/src/views/tadl/formPage/utils.js View File

@@ -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: '名称',
},
];

+ 127
- 0
webapp/src/views/tadl/list/components/listStatus.vue View File

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

+ 220
- 0
webapp/src/views/tadl/list/index.vue View File

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

+ 175
- 0
webapp/src/views/tadl/list/util.js View File

@@ -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
);
}

+ 559
- 0
webapp/src/views/tadl/strategy/components/CreatePageForm.vue View File

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

+ 101
- 0
webapp/src/views/tadl/strategy/components/ReleaseDialog.vue View File

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

+ 514
- 0
webapp/src/views/tadl/strategy/components/StrategyDrawer.vue View File

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

+ 329
- 0
webapp/src/views/tadl/strategy/index.vue View File

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

+ 75
- 0
webapp/src/views/tadl/strategy/util.js View File

@@ -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)
);
};

+ 184
- 0
webapp/src/views/tadl/util.js View File

@@ -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;
};

+ 58
- 216
webapp/src/views/user/center.vue View File

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

+ 231
- 0
webapp/src/views/user/userInfo.vue View File

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

+ 289
- 6
webapp/yarn.lock View File

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


Loading…
Cancel
Save