This commit is contained in:
wenyann 2020-12-14 18:12:00 +08:00
commit a76a5c3067
38 changed files with 508 additions and 144 deletions

View File

@ -55,11 +55,17 @@
- [x] 测试跟踪:支持对接禅道同步缺陷
- [x] 其他Jenkins 插件支持 pipeline 方式调用
## v1.6 (开发中)
- [ ] 新增接口管理功能
- [ ] 全新接口自动化使用方式
- [ ] 测试跟踪测试计划分类型展示
- [ ] 优化消息通知配置及实现方式
## 规划中
- [ ] 接口测试支持添加 WebSocket 协议请求
- [ ] 接口管理功能
- [ ] 集成云平台动态管理测试资源池
- [ ] 支持 K8s 集群作为测试资源池
- [ ] 测试跟踪测试用例及接口测试增加版本管理
- [ ] 测试跟踪测试用例增加思维导图展示形式
- [ ] 移动端测试支持
- [ ] UI 功能测试支持

View File

@ -58,6 +58,11 @@ public class ApiAutomationController {
apiAutomationService.removeToGc(ids);
}
@PostMapping("/reduction")
public void reduction(@RequestBody List<String> ids) {
apiAutomationService.reduction(ids);
}
@GetMapping("/getApiScenario/{id}")
public ApiScenario getScenarioDefinition(@PathVariable String id) {
return apiAutomationService.getApiScenario(id);

View File

@ -6,10 +6,7 @@ import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.dto.ApiTestImportRequest;
import io.metersphere.api.dto.automation.ApiScenarioRequest;
import io.metersphere.api.dto.automation.ReferenceDTO;
import io.metersphere.api.dto.definition.ApiDefinitionRequest;
import io.metersphere.api.dto.definition.ApiDefinitionResult;
import io.metersphere.api.dto.definition.RunDefinitionRequest;
import io.metersphere.api.dto.definition.SaveApiDefinitionRequest;
import io.metersphere.api.dto.definition.*;
import io.metersphere.api.service.ApiDefinitionService;
import io.metersphere.base.domain.ApiDefinition;
import io.metersphere.commons.constants.RoleConstants;
@ -64,6 +61,11 @@ public class ApiDefinitionController {
apiDefinitionService.removeToGc(ids);
}
@PostMapping("/reduction")
public void reduction(@RequestBody List<String> ids) {
apiDefinitionService.reduction(ids);
}
@GetMapping("/get/{id}")
public ApiDefinition get(@PathVariable String id) {
return apiDefinitionService.get(id);
@ -100,4 +102,10 @@ public class ApiDefinitionController {
return apiDefinitionService.getReference(request);
}
@PostMapping("/batch/edit")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public void editApiBath(@RequestBody ApiBatchRequest request) {
apiDefinitionService.editApiBath(request);
}
}

View File

@ -4,11 +4,13 @@ import io.metersphere.base.domain.ApiScenario;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class ApiScenarioDTO extends ApiScenario {
private String projectName;
private String userName;
private String tagName;
private List<String> tagNames;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.api.dto.definition;
import io.metersphere.base.domain.ApiDefinitionWithBLOBs;
import io.metersphere.controller.request.OrderRequest;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class ApiBatchRequest extends ApiDefinitionWithBLOBs {
private List<String> ids;
private List<OrderRequest> orders;
private String projectId;
}

View File

@ -22,6 +22,7 @@ import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.constants.ReportTriggerMode;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.i18n.Translator;
import io.metersphere.track.dto.TestPlanDTO;
@ -67,11 +68,12 @@ public class ApiAutomationService {
private static final String BODY_FILE_DIR = "/opt/metersphere/data/body";
public List<ApiScenarioDTO> list(ApiScenarioRequest request) {
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
List<ApiScenarioDTO> list = extApiScenarioMapper.list(request);
ApiTagExample example = new ApiTagExample();
example.createCriteria().andProjectIdEqualTo(request.getProjectId());
List<ApiTag> tags = apiTagMapper.selectByExample(example);
Map<String, String> tagMap = tags.stream().collect(Collectors.toMap(ApiTag::getId, ApiTag::getName));
List<ApiScenarioDTO> list = extApiScenarioMapper.list(request);
Gson gs = new Gson();
list.forEach(item -> {
if (item.getTagId() != null) {
@ -81,7 +83,11 @@ public class ApiAutomationService {
buf.append(",");
});
if (buf != null && buf.length() > 0) {
item.setTagName(buf.toString().substring(0, buf.toString().length() - 1));
String tagNames = buf.toString().substring(0, buf.toString().length() - 1);
List<String> tagList = Arrays.asList(tagNames.split(","));
item.setTagNames(tagList);
} else {
item.setTagNames(new ArrayList<>());
}
}
});
@ -166,12 +172,12 @@ public class ApiAutomationService {
apiScenarioMapper.deleteByExample(example);
}
public void removeToGc(List<String> ids) {
ApiScenario record = new ApiScenario();
record.setStatus(ScenarioStatus.Trash.name());
ApiScenarioExample example = new ApiScenarioExample();
example.createCriteria().andIdIn(ids);
apiScenarioMapper.updateByExampleSelective(record, example);
public void removeToGc(List<String> apiIds) {
extApiScenarioMapper.removeToGc(apiIds);
}
public void reduction(List<String> apiIds) {
extApiScenarioMapper.reduction(apiIds);
}
private void checkNameExist(SaveApiScenarioRequest request) {
@ -242,14 +248,16 @@ public class ApiAutomationService {
JSONObject element = JSON.parseObject(item.getScenarioDefinition());
MsScenario scenario = JSONObject.parseObject(item.getScenarioDefinition(), MsScenario.class);
// 多态JSON普通转换会丢失内容需要通过 ObjectMapper 获取
if (StringUtils.isNotEmpty(element.getString("hashTree"))) {
if (element!= null && StringUtils.isNotEmpty(element.getString("hashTree"))) {
LinkedList<MsTestElement> elements = mapper.readValue(element.getString("hashTree"),
new TypeReference<LinkedList<MsTestElement>>() {});
new TypeReference<LinkedList<MsTestElement>>() {
});
scenario.setHashTree(elements);
}
if (StringUtils.isNotEmpty(element.getString("variables"))) {
LinkedList<KeyValue> variables = mapper.readValue(element.getString("variables"),
new TypeReference<LinkedList<KeyValue>>() {});
new TypeReference<LinkedList<KeyValue>>() {
});
scenario.setVariables(variables);
}
LinkedList<MsTestElement> scenarios = new LinkedList<>();

View File

@ -163,6 +163,10 @@ public class ApiDefinitionService {
extApiDefinitionMapper.removeToGc(apiIds);
}
public void reduction(List<String> apiIds) {
extApiDefinitionMapper.reduction(apiIds);
}
public void deleteBodyFiles(String apiId) {
File file = new File(BODY_FILE_DIR + "/" + apiId);
FileUtil.deleteContents(file);
@ -371,4 +375,15 @@ public class ApiDefinitionService {
dto.setTestPlanList(extTestPlanMapper.selectReference(planRequest));
return dto;
}
public void editApiBath(ApiBatchRequest request) {
ApiDefinitionExample definitionExample = new ApiDefinitionExample();
definitionExample.createCriteria().andIdIn(request.getIds());
ApiDefinitionWithBLOBs definitionWithBLOBs = new ApiDefinitionWithBLOBs();
BeanUtils.copyBean(definitionWithBLOBs, request);
definitionWithBLOBs.setUpdateTime(System.currentTimeMillis());
apiDefinitionMapper.updateByExampleSelective(definitionWithBLOBs, definitionExample);
}
}

View File

@ -123,16 +123,17 @@ public class ApiScenarioModuleService {
}
}
private List<ApiScenarioDTO> queryByModuleIds(List<String> nodeIds) {
private List<ApiScenarioDTO> queryByModuleIds(DragApiScenarioModuleRequest request) {
ApiScenarioRequest apiScenarioRequest = new ApiScenarioRequest();
apiScenarioRequest.setModuleIds(nodeIds);
apiScenarioRequest.setProjectId(request.getProjectId());
apiScenarioRequest.setModuleIds(request.getNodeIds());
return apiAutomationService.list(apiScenarioRequest);
}
public int editNode(DragApiScenarioModuleRequest request) {
request.setUpdateTime(System.currentTimeMillis());
checkApiScenarioModuleExist(request);
List<ApiScenarioDTO> apiScenarios = queryByModuleIds(request.getNodeIds());
List<ApiScenarioDTO> apiScenarios = queryByModuleIds(request);
apiScenarios.forEach(apiScenario -> {
StringBuilder path = new StringBuilder(apiScenario.getModulePath());
@ -171,7 +172,7 @@ public class ApiScenarioModuleService {
List<String> nodeIds = request.getNodeIds();
List<ApiScenarioDTO> apiScenarios = queryByModuleIds(nodeIds);
List<ApiScenarioDTO> apiScenarios = queryByModuleIds(request);
ApiScenarioModuleDTO nodeTree = request.getNodeTree();

View File

@ -15,4 +15,6 @@ public interface ExtApiDefinitionMapper {
int removeToGc(@Param("ids") List<String> ids);
int reduction(@Param("ids") List<String> ids);
}

View File

@ -255,4 +255,15 @@
#{v}
</foreach>
</update>
<update id="reduction">
update api_definition
set
status = 'Underway'
where id in
<foreach collection="ids" item="v" separator="," open="(" close=")">
#{v}
</foreach>
</update>
</mapper>

View File

@ -16,4 +16,7 @@ public interface ExtApiScenarioMapper {
List<ApiScenario> selectReference(@Param("request") ApiScenarioRequest request);
int removeToGc(@Param("ids") List<String> ids);
int reduction(@Param("ids") List<String> ids);
}

View File

@ -88,5 +88,25 @@
</where>
</select>
<update id="removeToGc">
update api_scenario
set
status = 'Trash'
where id in
<foreach collection="ids" item="v" separator="," open="(" close=")">
#{v}
</foreach>
</update>
<update id="reduction">
update api_scenario
set
status = 'Underway'
where id in
<foreach collection="ids" item="v" separator="," open="(" close=")">
#{v}
</foreach>
</update>
</mapper>

View File

@ -106,7 +106,7 @@
<select id="checkLoadTestOwner" resultType="int">
SELECT COUNT(1)
FROM load_test
LEFT JOIN project ON api_test.project_id = project.id
LEFT JOIN project ON load_test.project_id = project.id
<where>
<if test="testId != null">
and load_test.id = #{testId}

View File

@ -5,14 +5,17 @@ import io.metersphere.commons.constants.UserSource;
import io.metersphere.commons.user.SessionUser;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.controller.request.LoginRequest;
import io.metersphere.service.BaseDisplayService;
import io.metersphere.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.env.Environment;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.IOException;
@RestController
@RequestMapping
@ -22,6 +25,8 @@ public class LoginController {
private UserService userService;
@Resource
private Environment env;
@Resource
private BaseDisplayService baseDisplayService;
@GetMapping(value = "/isLogin")
public ResultHolder isLogin() {
@ -66,4 +71,9 @@ public class LoginController {
public String getDefaultLanguage() {
return userService.getDefaultLanguage();
}
@GetMapping("display/file/{imageName}")
public ResponseEntity<byte[]> image(@PathVariable("imageName") String imageName) throws IOException {
return baseDisplayService.getImage(imageName);
}
}

View File

@ -0,0 +1,72 @@
package io.metersphere.service;
import io.metersphere.base.domain.SystemParameter;
import io.metersphere.base.domain.SystemParameterExample;
import io.metersphere.base.mapper.SystemParameterMapper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;
@Service
public class BaseDisplayService {
@Resource
private SystemParameterMapper systemParameterMapper;
@Resource
private FileService fileService;
public List<SystemParameter> getParamList(String type) {
SystemParameterExample example = new SystemParameterExample();
example.createCriteria().andParamKeyLike(type + "%");
return systemParameterMapper.selectByExample(example);
}
public byte[] loadFileAsBytes(String fileId) {
return fileService.loadFileAsBytes(fileId);
}
public ResponseEntity<byte[]> getImage(String imageName) throws IOException {
byte[] bytes = null;
List<SystemParameter> paramList = getParamList("ui." + imageName);
if (!CollectionUtils.isEmpty(paramList)) {
SystemParameter sp = paramList.get(0);
String paramValue = sp.getParamValue();
if (StringUtils.isNotBlank(paramValue)) {
bytes = loadFileAsBytes(paramValue);
}
}
MediaType contentType = MediaType.parseMediaType("application/octet-stream");
if (bytes == null) {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(getClass().getClassLoader());
switch (imageName) {
case "logo":
bytes = IOUtils.toByteArray(resolver.getResources("/static/img/logo-light-MeterSphere.*.svg")[0].getInputStream());
contentType = MediaType.valueOf("image/svg+xml");
break;
case "loginImage":
bytes = IOUtils.toByteArray(resolver.getResources("/static/img/info.*.png")[0].getInputStream());
break;
case "loginLogo":
bytes = IOUtils.toByteArray(resolver.getResources("/static/img/logo-dark-MeterSphere.*.svg")[0].getInputStream());
contentType = MediaType.valueOf("image/svg+xml");
break;
default:
break;
}
}
return ResponseEntity.ok()
.contentType(contentType)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + imageName + "\"")
.body(bytes);
}
}

View File

@ -28,7 +28,7 @@ import static io.metersphere.commons.constants.ResourceStatusEnum.VALID;
public class NodeResourcePoolService {
private final static String nodeControllerUrl = "http://%s:%s/status";
@Resource
@Resource(name = "restTemplateWithTimeOut")
private RestTemplate restTemplateWithTimeOut;
@Resource
private TestResourceMapper testResourceMapper;

@ -1 +1 @@
Subproject commit 2ba1351aa0135cdf3e5de740fa2d9c185b60ce9d
Subproject commit 1fe20ba15a7ca3fe9f77ddf866021e7c7dfe5969

View File

@ -9,7 +9,7 @@
<el-input v-model="tagForm.name" autocomplete="off" :placeholder="$t('commons.name')"/>
</el-col>
<el-col :span="4">
<el-button @click="saveTag">{{$t('commons.save')}}</el-button>
<el-button style="margin-left: 20px" @click="saveTag">{{$t('commons.save')}}</el-button>
</el-col>
</el-row>
</el-form-item>
@ -72,7 +72,7 @@
},
methods: {
saveTag() {
if (this.tagData.id != undefined && this.tagForm.id != null) {
if (this.tagForm.id != undefined && this.tagForm.id != null) {
this.path = "/api/tag/update";
} else {
this.path = "/api/tag/create";

View File

@ -25,9 +25,11 @@
</template>
</el-table-column>
<el-table-column prop="tagName" :label="$t('api_test.automation.tag')" show-overflow-tooltip>
<el-table-column prop="tagNames" :label="$t('api_test.automation.tag')" width="200px">
<template v-slot:default="scope">
<ms-tag type="success" effect="plain" v-if="scope.row.tagName!=undefined" :content="scope.row.tagName"/>
<div v-for="itemName in scope.row.tagNames" :key="itemName">
<ms-tag type="success" effect="plain" :content="itemName"/>
</div>
</template>
</el-table-column>
<el-table-column prop="userId" :label="$t('api_test.automation.creator')" show-overflow-tooltip/>
@ -210,8 +212,8 @@
this.$emit('edit', row);
},
reductionApi(row) {
let obj = {id: row.id, projectId: row.projectId, name: row.name, status: 'Underway'}
this.$fileUpload("/api/automation/update", null, [], obj, () => {
let obj = [row.id];
this.$post("/api/automation/reduction", obj, response => {
this.$success(this.$t('commons.save_success'));
this.search();
})

View File

@ -30,11 +30,9 @@
class="ms-el-input" size="mini"></el-input>
</template>
<!-- 如果不是编辑状态 -->
<span v-else>
<i class="el-icon-delete" v-if="data.id==='gc'"/>
<i class="el-icon-folder" v-else/>
<span class="node-title" v-text="data.name"></span>
</span>
<i class="el-icon-delete" v-if="data.isEdit!=1 && data.id==='gc'"/>
<i class="el-icon-folder" v-if="data.isEdit!=1 && data.id!='gc'"/>
<span class="node-title" v-if="data.isEdit!=1" v-text="data.name"></span>
<span class="node-operate child">
<el-tooltip
@ -117,6 +115,7 @@
},
methods: {
getApiModuleTree() {
this.nextFlag = true;
let projectId = getCurrentProjectID();
if (projectId) {
if (this.expandedNode.length === 0) {
@ -314,7 +313,7 @@
editApiModule(node, data) {
let projectId = getCurrentProjectID();
if (!projectId) {
this.$error("$t('api_test.select_project')");
this.$error(this.$t('api_test.select_project'));
return;
}
let url = "";

View File

@ -352,6 +352,9 @@
}
},
created() {
if (!this.currentScenario.apiScenarioModuleId) {
this.currentScenario.apiScenarioModuleId = "";
}
this.projectId = getCurrentProjectID();
this.operatingElements = ELEMENTS.get("ALL");
this.getMaintainerOptions();
@ -450,8 +453,12 @@
this.reload();
},
addScenario(arr) {
if (arr.length > 0) {
if (arr && arr.length > 0) {
arr.forEach(item => {
if (item.id === this.currentScenario.id) {
this.$error("不能引用或复制自身!");
return;
}
item.enable === undefined ? item.enable = true : item.enable;
this.scenarioDefinition.push(item);
})
@ -710,10 +717,6 @@
}
},
setParameter() {
this.currentScenario.projectId = this.projectId;
if (!this.currentScenario.id) {
this.currentScenario.id = getUUID();
}
this.currentScenario.stepTotal = this.scenarioDefinition.length;
this.currentScenario.modulePath = this.getPath(this.currentScenario.apiScenarioModuleId);
// 便

View File

@ -68,7 +68,7 @@
if (response.data) {
response.data.forEach(item => {
let scenarioDefinition = JSON.parse(item.scenarioDefinition);
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'Copy', resourceId: getUUID(), hashTree: scenarioDefinition};
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'Copy', resourceId: getUUID(), hashTree: scenarioDefinition.hashTree};
scenarios.push(obj);
})
this.$emit('addScenario', scenarios);

View File

@ -38,7 +38,6 @@
:currentRow="currentRow"
@editApi="editApi"
@handleCase="handleCase"
@handleEditBatch="handleEditBatch"
@showExecResult="showExecResult"
ref="apiList"/>
@ -209,11 +208,6 @@
}
this.handleTabsEdit(name, "ADD", row);
},
handleEditBatch(rows) {
rows.forEach(row => {
this.handleTabsEdit(this.$t('api_test.definition.request.edit_api') + "-" + row.name, "ADD", row);
})
},
handleCase(api) {
this.currentApi = api;
this.showCasePage = false;

View File

@ -66,10 +66,10 @@
</el-col>
<el-col :span="3">
<el-link type="danger" @click="showExecResult(item)" v-if="item.execResult && item.execResult==='error'">{{getResult(item.execResult)}}</el-link>
<div v-else>
<div v-if="item.type!='create'">{{getResult(item.execResult)}}</div>
</div>
<el-link type="danger" v-if="item.execResult && item.execResult==='error'" @click="showExecResult(item)">{{getResult(item.execResult)}}</el-link>
<el-link v-else-if="item.execResult && item.execResult==='success'" @click="showExecResult(item)">{{getResult(item.execResult)}}</el-link>
<div v-else> {{getResult(item.execResult)}}</div>
<div v-if="item.type!='create'" style="color: #999999;font-size: 12px">
<span> {{item.updateTime | timestampFormatDate }}</span>
{{item.updateUser}}
@ -271,7 +271,7 @@
});
},
copyCase(data) {
let obj = {name: data.name, priority: data.priority, type: 'create', active: false, request: data.request};
let obj = {name: "copy_"+data.name, priority: data.priority, type: 'create', active: false, request: data.request};
this.apiCaseList.unshift(obj);
},
addCase() {

View File

@ -82,6 +82,8 @@
:total="total"/>
</el-card>
<ms-api-case-list @refresh="initApiTable" @showExecResult="showExecResult" :currentApi="selectApi" ref="caseList"/>
<!--批量编辑-->
<ms-batch-edit ref="batchEdit" @batchEdit="batchEdit" :typeArr="typeArr" :value-arr="valueArr"/>
</div>
</template>
@ -99,8 +101,10 @@
import MsContainer from "../../../common/components/MsContainer";
import MsBottomContainer from "./BottomContainer";
import ShowMoreBtn from "../../../../components/track/case/components/ShowMoreBtn";
import {API_METHOD_COLOUR} from "../model/JsonData";
import MsBatchEdit from "./basis/BatchEdit";
import {API_METHOD_COLOUR, REQ_METHOD, API_STATUS} from "../model/JsonData";
import {getCurrentProjectID} from "@/common/js/utils";
import {WORKSPACE_ID} from '../../../../../common/js/constants';
export default {
name: "ApiList",
@ -114,7 +118,8 @@
MsApiCaseList,
MsContainer,
MsBottomContainer,
ShowMoreBtn
ShowMoreBtn,
MsBatchEdit
},
data() {
return {
@ -127,6 +132,16 @@
{name: this.$t('api_test.definition.request.batch_delete'), handleClick: this.handleDeleteBatch},
{name: this.$t('api_test.definition.request.batch_edit'), handleClick: this.handleEditBatch}
],
typeArr: [
{id: 'status', name: this.$t('api_test.definition.api_case_status')},
{id: 'method', name: this.$t('api_test.definition.api_type')},
{id: 'userId', name: this.$t('api_test.definition.api_principal')},
],
valueArr: {
status: API_STATUS,
method: REQ_METHOD,
userId: [],
},
methodColorMap: new Map(API_METHOD_COLOUR),
tableData: [],
currentPage: 1,
@ -147,6 +162,7 @@
created: function () {
this.projectId = getCurrentProjectID();
this.initApiTable();
this.getMaintainerOptions();
},
watch: {
currentModule() {
@ -158,6 +174,7 @@
},
methods: {
initApiTable() {
this.selectRows = new Set();
this.condition.filters = ["Prepare", "Underway", "Completed"];
if (this.currentModule != null) {
if (this.currentModule.id == "root") {
@ -181,6 +198,12 @@
this.tableData = response.data.listObject;
});
},
getMaintainerOptions() {
let workspaceId = localStorage.getItem(WORKSPACE_ID);
this.$post('/user/ws/member/tester/list', {workspaceId: workspaceId}, response => {
this.valueArr.userId = response.data;
});
},
handleSelect(selection, row) {
row.hashTree = [];
if (this.selectRows.has(row)) {
@ -230,10 +253,8 @@
this.$emit('editApi', row);
},
reductionApi(row) {
row.status = 'Underway';
row.request = null;
row.response = null;
this.$fileUpload("/api/definition/update", null, [], row, () => {
let ids = [row.id];
this.$post('/api/definition/reduction/', ids, () => {
this.$success(this.$t('commons.save_success'));
this.search();
});
@ -270,11 +291,27 @@
}
},
handleEditBatch() {
this.$emit('handleEditBatch', this.selectRows);
this.$refs.batchEdit.open();
},
batchEdit(form) {
let arr = Array.from(this.selectRows);
let ids = arr.map(row => row.id);
let param = {};
param[form.type] = form.value;
param.ids = ids;
this.$post('/api/definition/batch/edit', param, () => {
this.$success(this.$t('commons.save_success'));
this.initApiTable();
});
},
handleTestCase(api) {
this.selectApi = api;
let request = JSON.parse(api.request);
let request = {};
if (Object.prototype.toString.call(api.request).match(/\[object (\w+)\]/)[1].toLowerCase() === 'object') {
request = api.request;
} else {
request = JSON.parse(api.request);
}
if (!request.hashTree) {
request.hashTree = [];
}

View File

@ -35,11 +35,10 @@
@node-click="selectModule"
@node-drag-end="handleDragEnd"
:filter-node-method="filterNode"
draggable
:draggable="true"
:allow-drop="allowDrop"
:allow-drag="allowDrag" ref="tree">
<span class="custom-tree-node father"
slot-scope="{ node, data }">
<span class="custom-tree-node father" slot-scope="{ node, data }">
<!-- 如果是编辑状态 -->
<template v-if="data.isEdit==1">
<el-input ref="input"
@ -48,12 +47,9 @@
class="ms-el-input" size="mini"></el-input>
</template>
<!-- 如果不是编辑状态 -->
<div v-else>
<i class="el-icon-delete" v-if="data.id==='gc'"/>
<i class="el-icon-folder" v-else/>
<span class="node-title" v-text="data.name"></span>
</div>
<i class="el-icon-delete" v-if="data.isEdit!=1 && data.id==='gc'"/>
<i class="el-icon-folder" v-if="data.isEdit!=1 && data.id!='gc'"/>
<span class="node-title" v-if="data.isEdit!=1" v-text="data.name"></span>
<span class="node-operate child">
<el-tooltip
v-if="data.id!='root' && data.id!='gc'"
@ -138,6 +134,7 @@
if (this.expandedNode.length === 0) {
this.expandedNode.push("root");
}
this.nextFlag = true;
this.result = this.$get("/api/module/list/" + this.projectId + "/" + this.protocol, response => {
if (response.data != undefined && response.data != null) {
this.data[1].children = response.data;
@ -449,28 +446,6 @@
width: 100%;
}
.father .child {
display: none;
}
.father:hover .child {
display: block;
}
.node-title {
width: 0px;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0px 5px;
overflow: hidden;
}
.node-operate > i {
color: #409eff;
margin: 0px 5px;
}
/deep/ .el-tree-node__content {
height: 33px;
}
@ -502,4 +477,26 @@
width: 90px;
}
.father .child {
display: none;
}
.father:hover .child {
display: block;
}
.node-title {
width: 0px;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0px 5px;
overflow: hidden;
}
.node-operate > i {
color: #409eff;
margin: 0px 5px;
}
</style>

View File

@ -103,8 +103,12 @@
let threadGroup = new ThreadGroup();
threadGroup.hashTree = [];
testPlan.hashTree = [threadGroup];
this.runData.forEach(item => {
if (!item.useEnvironment) {
this.$error(this.$t("api_test.environment.select_environment"));
this.$emit('runRefresh', {});
return;
}
threadGroup.hashTree.push(item);
})
let reqObj = {id: this.reportId, testElement: testPlan};

View File

@ -0,0 +1,100 @@
<template>
<div>
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="25%"
class="batch-edit-dialog"
:destroy-on-close="true"
@close="handleClose"
>
<el-form :model="form" label-position="right" label-width="150px" size="medium" ref="form" :rules="rules">
<el-form-item :label="$t('test_track.case.batch_update', [size])" prop="type">
<el-select v-model="form.type" style="width: 80%" @change="changeType">
<el-option v-for="(type, index) in typeArr" :key="index" :value="type.id" :label="type.name"/>
</el-select>
</el-form-item>
<el-form-item :label="$t('test_track.case.updated_attr_value')" prop="value">
<el-select v-model="form.value" style="width: 80%" :filterable="filterable">
<el-option v-for="(option, index) in options" :key="index" :value="option.id" :label="option.label">
<div v-if="option.email">
<span>{{option.id}}({{option.name}})</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-form>
<template v-slot:footer>
<ms-dialog-footer
@cancel="dialogVisible = false"
@confirm="submit('form')"/>
</template>
</el-dialog>
</div>
</template>
<script>
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
import {listenGoBack, removeGoBackListener} from "@/common/js/utils";
export default {
name: "BatchEdit",
components: {
MsDialogFooter
},
props: {
typeArr: Array,
valueArr: Object,
dialogTitle: {
type: String,
default() {
return this.$t('api_test.definition.request.batch_edit')
}
}
},
data() {
return {
dialogVisible: false,
form: {},
size: 0,
rules: {
type: {required: true, message: this.$t('test_track.case.please_select_attr'), trigger: ['blur', 'change']},
value: {required: true, message: this.$t('test_track.case.please_select_attr_value'), trigger: ['blur', 'change']}
},
options: [],
filterable: false,
}
},
methods: {
submit(form) {
this.$refs[form].validate((valid) => {
if (valid) {
this.$emit("batchEdit", this.form);
this.dialogVisible = false;
} else {
return false;
}
});
},
open() {
this.dialogVisible = true;
this.size = this.$parent.selectRows.size;
listenGoBack(this.handleClose);
},
handleClose() {
this.form = {};
this.options = [];
removeGoBackListener(this.handleClose);
},
changeType(val) {
this.$set(this.form, "value", "");
this.filterable = val === "maintainerOptions";
this.options = this.valueArr[val];
}
}
}
</script>
<style scoped>
</style>

View File

@ -103,6 +103,7 @@
}
},
created() {
this.environment = undefined;
this.getEnvironments();
},
watch: {
@ -118,16 +119,6 @@
this.environments.forEach(environment => {
parseEnvironment(environment);
});
let hasEnvironment = false;
for (let i in this.environments) {
if (this.environments[i].id === this.api.environmentId) {
hasEnvironment = true;
break;
}
}
if (!hasEnvironment) {
this.environment = undefined;
}
});
} else {
this.environment = undefined;

View File

@ -236,7 +236,7 @@
let hasEnvironment = false;
for (let i in this.environments) {
if (this.environments[i].id === this.api.environmentId) {
this.api.environmentId = this.environments[i];
this.api.environmentId = this.environments[i].id;
hasEnvironment = true;
break;
}

View File

@ -6,8 +6,12 @@
<el-submenu :class="{'deactivation':!isProjectActivation}"
v-permission="['test_manager','test_user','test_viewer']" index="3">
<template v-slot:title>{{ $t('commons.project') }}</template>
<search-list ref="projectRecent" :options="projectRecent"/>
<template v-slot:title>
<span style="display: inline-block;width: 150px;white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" :title="currentProject">
{{ $t('commons.project') }}: {{currentProject}}
</span>
</template>
<search-list ref="projectRecent" :options="projectRecent" :current-project.sync="currentProject"/>
<el-divider class="menu-divider"/>
<el-menu-item :index="'/setting/project/create'">
<font-awesome-icon :icon="['fa', 'plus']"/>
@ -108,6 +112,7 @@ export default {
isProjectActivation: true,
isRouterAlive: true,
apiTestProjectPath: '',
currentProject: ''
}
},
// watch: {

View File

@ -27,6 +27,7 @@
<script>
import {
PROJECT_ID,
ROLE_ORG_ADMIN,
ROLE_TEST_MANAGER,
ROLE_TEST_USER,
@ -107,6 +108,7 @@
if (response.data.workspaceId) {
localStorage.setItem("workspace_id", response.data.workspaceId);
}
localStorage.removeItem(PROJECT_ID)
this.$router.push('/');
window.location.reload();
});
@ -119,6 +121,7 @@
this.$post("/user/switch/source/ws/" + workspaceId, {}, response => {
saveLocalStorage(response);
localStorage.setItem("workspace_id", workspaceId);
localStorage.removeItem(PROJECT_ID)
this.$router.push('/');
window.location.reload();
})

View File

@ -2,7 +2,7 @@
<div v-loading="result.loading" class="search-list">
<el-input placeholder="搜索项目"
prefix-icon="el-icon-search"
v-model="search_text"
v-model="searchString"
clearable
class="search-input"
size="small"/>
@ -11,13 +11,13 @@
无数据
</span>
</div>
<div v-else style="height: 120px;overflow: auto">
<div v-else style="height: 150px;overflow: auto">
<el-menu-item :key="i.id" v-for="i in items" @click="change(i.id)">
<template slot="title">
<div class="title">
{{ i.name }}
<i class="el-icon-check" v-if="i.id === currentProjectId"></i>
</div>
<i class="el-icon-check" v-if="i.id === currentProjectId"></i>
</template>
</el-menu-item>
</div>
@ -32,53 +32,91 @@ import {PROJECT_ID, ROLE_TEST_MANAGER, ROLE_TEST_USER, ROLE_TEST_VIEWER} from "@
export default {
name: "SearchList",
props: {
options: Object
options: Object,
currentProject: String
},
created() {
if (getCurrentUser().lastProjectId) {
localStorage.setItem(PROJECT_ID, getCurrentUser().lastProjectId);
}
},
mounted() {
this.init();
},
computed: {
currentProjectId() {
return localStorage.getItem(PROJECT_ID)
}
},
data() {
return {
result: {},
items: [],
search_text: '',
searchArray: [],
searchString: '',
userId: getCurrentUser().id,
currentProjectId: localStorage.getItem(PROJECT_ID)
}
},
watch: {
search_text(val) {
if (!val) {
this.init();
} else {
this.search();
}
searchString(val) {
this.query(val)
}
},
methods: {
init: function () {
if (hasRoles(ROLE_TEST_VIEWER, ROLE_TEST_USER, ROLE_TEST_MANAGER)) {
this.result = this.$get(this.options.url, (response) => {
this.result = this.$get("/project/listAll", response => {
this.items = response.data;
this.items = this.items.splice(0, 3);
this.searchArray = response.data;
if (!getCurrentProjectID() && this.items.length > 0) {
this.change(this.items[0].id);
}
});
let projectId = getCurrentProjectID();
this.changeProjectName(projectId);
})
}
},
search() {
if (hasRoles(ROLE_TEST_VIEWER, ROLE_TEST_USER, ROLE_TEST_MANAGER)) {
this.result = this.$post("/project/search", {name: this.search_text},response => {
this.result = this.$post("/project/search", {name: this.searchString},response => {
this.items = response.data;
})
}
},
query(queryString) {
this.items = queryString ? this.searchArray.filter(this.createFilter(queryString)) : this.searchArray;
},
createFilter(queryString) {
return item => {
return (item.name.toLowerCase().indexOf(queryString.toLowerCase()) !== -1);
};
},
change(projectId) {
let currentProjectId = getCurrentProjectID();
if (projectId === currentProjectId) {
return;
}
this.$post("/user/update/current", {id: this.userId, lastProjectId: projectId}, () => {
localStorage.setItem(PROJECT_ID, projectId);
if (this.$route.path.indexOf('/track/review/view/') >= 0) {
this.$router.replace('/track/review/all');
} else if (this.$route.path.indexOf('/track/plan/view/') >= 0) {
this.$router.replace('/track/plan/all');
} else {
window.location.reload();
}
this.changeProjectName(projectId);
});
},
changeProjectName(projectId) {
if (projectId) {
let project = this.searchArray.filter(p => p.id === projectId);
if (project.length > 0) {
this.$emit("update:currentProject", project[0].name);
}
} else {
this.$emit("update:currentProject", '选择项目');
}
}
}
}

View File

@ -1,13 +1,17 @@
<template>
<div id="menu-bar">
<el-row type="flex">
<el-col :span="8">
<el-col :span="10">
<el-menu class="header-menu" :unique-opened="true" mode="horizontal" router :default-active='$route.path'>
<el-submenu v-permission="['test_manager','test_user','test_viewer']"
index="3" popper-class="submenu">
<template v-slot:title>{{ $t('commons.project') }}</template>
<search-list ref="projectRecent" :options="projectRecent"/>
<template v-slot:title>
<span style="display: inline-block;width: 150px;white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" :title="currentProject">
{{ $t('commons.project') }}: {{currentProject}}
</span>
</template>
<search-list ref="projectRecent" :options="projectRecent" :current-project.sync="currentProject"/>
<el-divider/>
<el-menu-item :index="'/setting/project/create'">
<font-awesome-icon :icon="['fa', 'plus']"/>
@ -39,12 +43,12 @@
</el-submenu>
</el-menu>
</el-col>
<el-col :span="8">
<el-col :span="4" >
<el-row type="flex" justify="center">
<ms-create-test :to="'/performance/test/create'"/>
</el-row>
</el-col>
<el-col :span="8"/>
<el-col :span="10"/>
</el-row>
</div>
</template>
@ -98,7 +102,7 @@ export default {
router(item) {
}
},
input2: ''
currentProject: ''
}
},
methods: {

View File

@ -42,7 +42,7 @@
</el-submenu>
<el-menu-item v-for="menu in project" :key="menu.index" :index="'/setting/project/all'" class="setting-item"
v-permission="menu.roles">
v-permission="['test_user','test_manager', 'org_admin', 'admin']">
<template v-slot:title>
<font-awesome-icon class="icon" :icon="['fa', 'bars']" size="lg"/>
<span>{{ $t(menu.title) }}</span>

View File

@ -2,13 +2,17 @@
<div id="menu-bar" v-if="isRouterAlive">
<el-row type="flex">
<el-col :span="12">
<el-col :span="16">
<el-menu class="header-menu" :unique-opened="true" mode="horizontal" router
:default-active='$route.path'>
<el-submenu :class="{'deactivation':!isProjectActivation}"
v-permission="['test_manager','test_user','test_viewer']" index="3" popper-class="submenu">
<template v-slot:title>{{ $t('commons.project') }}</template>
<search-list ref="projectRecent" :options="projectRecent"/>
<template v-slot:title>
<span style="display: inline-block;width: 150px;white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" :title="currentProject">
{{ $t('commons.project') }}: {{currentProject}}
</span>
</template>
<search-list ref="projectRecent" :options="projectRecent" :current-project.sync="currentProject"/>
<el-divider/>
<el-menu-item :index="'/setting/project/create'">
<font-awesome-icon :icon="['fa', 'plus']"/>
@ -54,7 +58,7 @@
</el-submenu>
</el-menu>
</el-col>
<el-col :span="12"/>
<el-col :span="8"/>
</el-row>
</div>
@ -78,6 +82,7 @@ export default {
testCaseReviewEditPath: '',
testCaseProjectPath: '',
isProjectActivation: true,
currentProject: '',
projectRecent: {
title: this.$t('project.recent'),
url: "/project/recent/5",

View File

@ -96,7 +96,6 @@ export function getCurrentProjectID() {
export function saveLocalStorage(response) {
// 登录信息保存 cookie
localStorage.setItem(TokenKey, JSON.stringify(response.data));
localStorage.setItem(PROJECT_ID, response.data.lastProjectId);
let rolesArray = response.data.roles;
let roles = rolesArray.map(r => r.id);
// 保存角色

View File

@ -264,6 +264,10 @@ export default {
.login-logo {
background: url(../assets/logo-dark-MeterSphere.svg);
}
.logo-header {
background: url(../assets/logo-light-MeterSphere.svg);
}
</style>
<style>