diff --git a/backend/src/main/java/io/metersphere/api/controller/ApiAutomationController.java b/backend/src/main/java/io/metersphere/api/controller/ApiAutomationController.java index f8c9895250..2f0b746cda 100644 --- a/backend/src/main/java/io/metersphere/api/controller/ApiAutomationController.java +++ b/backend/src/main/java/io/metersphere/api/controller/ApiAutomationController.java @@ -22,6 +22,7 @@ import io.metersphere.task.service.TaskService; import io.metersphere.track.request.testcase.ApiCaseRelevanceRequest; import io.metersphere.track.request.testplan.FileOperationRequest; import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -96,7 +97,7 @@ public class ApiAutomationController { @PostMapping(value = "/create") @MsAuditLog(module = "api_automation", type = OperLogConstants.CREATE, title = "#request.name", content = "#msClass.getLogDetails(#request.id)", msClass = ApiAutomationService.class) - @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_READ_CREATE) + @RequiresPermissions(value={PermissionConstants.PROJECT_API_SCENARIO_READ_CREATE, PermissionConstants.PROJECT_API_SCENARIO_READ_COPY}, logical = Logical.OR) @SendNotice(taskType = NoticeConstants.TaskType.API_AUTOMATION_TASK, event = NoticeConstants.Event.CREATE, mailTemplate = "api/AutomationCreate", subject = "接口自动化通知") public ApiScenario create(@RequestPart("request") SaveApiScenarioRequest request, @RequestPart(value = "bodyFiles", required = false) List bodyFiles, @RequestPart(value = "scenarioFiles", required = false) List scenarioFiles) { @@ -105,7 +106,7 @@ public class ApiAutomationController { @PostMapping(value = "/update") @MsAuditLog(module = "api_automation", type = OperLogConstants.UPDATE, beforeEvent = "#msClass.getLogDetails(#request.id)", title = "#request.name", content = "#msClass.getLogDetails(#request.id)", msClass = ApiAutomationService.class) - @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_READ_EDIT) + @RequiresPermissions(value={PermissionConstants.PROJECT_API_SCENARIO_READ_EDIT, PermissionConstants.PROJECT_API_SCENARIO_READ_COPY}, logical = Logical.OR) @SendNotice(taskType = NoticeConstants.TaskType.API_AUTOMATION_TASK, event = NoticeConstants.Event.UPDATE, mailTemplate = "api/AutomationUpdate", subject = "接口自动化通知") public ApiScenario update(@RequestPart("request") SaveApiScenarioRequest request, @RequestPart(value = "bodyFiles", required = false) List bodyFiles, @RequestPart(value = "scenarioFiles", required = false) List scenarioFiles) { @@ -335,7 +336,7 @@ public class ApiAutomationController { return apiAutomationService.setDomain(request.getDefinition()); } - @PostMapping(value = "/export/jmx") + @PostMapping(value = "/export/zip") @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_READ_EXPORT_SCENARIO) @MsAuditLog(module = "api_automation", type = OperLogConstants.EXPORT, sourceId = "#request.id", title = "#request.name", project = "#request.projectId") public ResponseEntity downloadBodyFiles(@RequestBody ApiScenarioBatchRequest request) { @@ -345,5 +346,19 @@ public class ApiAutomationController { .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + "场景JMX文件集.zip") .body(bytes); } + + @PostMapping(value = "/export/jmx") + @RequiresPermissions(PermissionConstants.PROJECT_API_SCENARIO_READ_EXPORT_SCENARIO) + @MsAuditLog(module = "api_automation", type = OperLogConstants.EXPORT, sourceId = "#request.id", title = "#request.name", project = "#request.projectId") + public List exportJmx(@RequestBody ApiScenarioBatchRequest request) { + return apiAutomationService.exportJmx(request); + } + + @PostMapping(value = "/checkScenarioEnv") + public boolean checkScenarioEnv(@RequestBody ApiScenarioWithBLOBs request) { + return apiAutomationService.checkScenarioEnv(request); + } + + } diff --git a/backend/src/main/java/io/metersphere/api/dto/ApiCaseBatchRequest.java b/backend/src/main/java/io/metersphere/api/dto/ApiCaseBatchRequest.java index 4d1b92087f..45004d0ab0 100644 --- a/backend/src/main/java/io/metersphere/api/dto/ApiCaseBatchRequest.java +++ b/backend/src/main/java/io/metersphere/api/dto/ApiCaseBatchRequest.java @@ -1,5 +1,6 @@ package io.metersphere.api.dto; +import io.metersphere.api.dto.definition.ApiTestCaseRequest; import io.metersphere.base.domain.ApiTestCaseWithBLOBs; import io.metersphere.controller.request.BaseQueryRequest; import io.metersphere.controller.request.OrderRequest; @@ -15,5 +16,5 @@ public class ApiCaseBatchRequest extends ApiTestCaseWithBLOBs { private List orders; private String projectId; private String environmentId; - private BaseQueryRequest condition; + private ApiTestCaseRequest condition; } diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/MsApiExportResult.java b/backend/src/main/java/io/metersphere/api/dto/definition/MsApiExportResult.java index 0b839273b6..07684fe4e6 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/MsApiExportResult.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/MsApiExportResult.java @@ -1,5 +1,6 @@ package io.metersphere.api.dto.definition; +import io.metersphere.api.dto.mockconfig.MockConfigImportDTO; import io.metersphere.base.domain.ApiDefinitionWithBLOBs; import io.metersphere.base.domain.ApiTestCaseWithBLOBs; @@ -17,4 +18,5 @@ public class MsApiExportResult extends ApiExportResult { private String version; private List data; private List cases; + private List mocks; } diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/RunCaseRequest.java b/backend/src/main/java/io/metersphere/api/dto/definition/RunCaseRequest.java index 6ab3648046..01d5383c61 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/RunCaseRequest.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/RunCaseRequest.java @@ -1,5 +1,7 @@ package io.metersphere.api.dto.definition; +import io.metersphere.base.domain.ApiDefinitionExecResult; +import io.metersphere.base.domain.ApiTestCaseWithBLOBs; import lombok.Getter; import lombok.Setter; @@ -16,4 +18,8 @@ public class RunCaseRequest { private String environmentId; private String testPlanId; + + private ApiTestCaseWithBLOBs bloBs; + + private ApiDefinitionExecResult report; } diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/parse/ApiDefinitionImport.java b/backend/src/main/java/io/metersphere/api/dto/definition/parse/ApiDefinitionImport.java index ab80415bb1..bf17ae66f8 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/parse/ApiDefinitionImport.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/parse/ApiDefinitionImport.java @@ -1,6 +1,7 @@ package io.metersphere.api.dto.definition.parse; import io.metersphere.api.dto.definition.parse.ms.NodeTree; +import io.metersphere.api.dto.mockconfig.MockConfigImportDTO; import io.metersphere.base.domain.ApiDefinitionWithBLOBs; import io.metersphere.base.domain.ApiTestCaseWithBLOBs; import io.metersphere.base.domain.EsbApiParamsWithBLOBs; @@ -21,5 +22,8 @@ public class ApiDefinitionImport { //ESB文件导入的附属数据类 private Map esbApiParamsMap; + //Mock数据相关 + private List mocks; + private List nodeTree; } diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/ElementUtil.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/ElementUtil.java index 15bd6362e0..48067aefb2 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/ElementUtil.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/ElementUtil.java @@ -268,18 +268,33 @@ public class ElementUtil { } }; + private static void formatSampler(JSONObject element) { + if (element == null || StringUtils.isEmpty(element.getString("type"))) { + return; + } + if (element.get("clazzName") == null && element.getString("type").equals("TCPSampler")) { + if (element.getString("tcpPreProcessor") != null) { + JSONObject tcpPreProcessor = JSON.parseObject(element.getString("tcpPreProcessor")); + if (tcpPreProcessor != null && tcpPreProcessor.get("clazzName") == null) { + tcpPreProcessor.fluentPut("clazzName", clazzMap.get(tcpPreProcessor.getString("type"))); + element.fluentPut("tcpPreProcessor", tcpPreProcessor); + } + } + } else if (element.getString("type").equals("HTTPSamplerProxy")) { + if (element.getString("authManager") != null) { + JSONObject authManager = JSON.parseObject(element.getString("authManager")); + if (authManager != null && authManager.get("clazzName") == null) { + authManager.fluentPut("clazzName", clazzMap.get(authManager.getString("type"))); + element.fluentPut("authManager", authManager); + } + } + } + } + public static void dataFormatting(JSONArray hashTree) { for (int i = 0; i < hashTree.size(); i++) { JSONObject element = hashTree.getJSONObject(i); - if (element != null && element.get("clazzName") == null && element.getString("type").equals("TCPSampler")) { - if (element.getString("tcpPreProcessor") != null) { - JSONObject tcpPreProcessor = JSON.parseObject(element.getString("tcpPreProcessor")); - if (tcpPreProcessor != null && tcpPreProcessor.get("clazzName") == null) { - tcpPreProcessor.fluentPut("clazzName", clazzMap.get(tcpPreProcessor.getString("type"))); - element.fluentPut("tcpPreProcessor", tcpPreProcessor); - } - } - } + formatSampler(element); if (element != null && element.get("clazzName") == null && clazzMap.containsKey(element.getString("type"))) { element.fluentPut("clazzName", clazzMap.get(element.getString("type"))); } @@ -294,6 +309,7 @@ public class ElementUtil { if (element != null && element.get("clazzName") == null && clazzMap.containsKey(element.getString("type"))) { element.fluentPut("clazzName", clazzMap.get(element.getString("type"))); } + formatSampler(element); if (element != null && element.containsKey("hashTree")) { JSONArray elementJSONArray = element.getJSONArray("hashTree"); dataFormatting(elementJSONArray); @@ -304,7 +320,28 @@ public class ElementUtil { try { for (int i = 0; i < hashTree.size(); i++) { JSONObject element = hashTree.getJSONObject(i); - if (element != null && element.get("type").toString().equals("HTTPSamplerProxy")) { + boolean isScenarioEnv = false; + ParameterConfig config = new ParameterConfig(); + if (element != null && element.get("type").toString().equals("scenario")) { + MsScenario scenario = JSONObject.toJavaObject(element, MsScenario.class); + if (scenario.isEnvironmentEnable()) { + isScenarioEnv = true; + Map envConfig = new HashMap<>(16); + Map environmentMap = (Map) element.get("environmentMap"); + if (environmentMap != null && !environmentMap.isEmpty()) { + environmentMap.keySet().forEach(projectId -> { + ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class); + ApiTestEnvironmentWithBLOBs environment = environmentService.get(environmentMap.get(projectId)); + if (environment != null && environment.getConfig() != null) { + EnvironmentConfig env = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class); + env.setApiEnvironmentid(environment.getId()); + envConfig.put(projectId, env); + } + }); + config.setConfig(envConfig); + } + } + } else if (element != null && element.get("type").toString().equals("HTTPSamplerProxy")) { MsHTTPSamplerProxy httpSamplerProxy = JSON.toJavaObject(element, MsHTTPSamplerProxy.class); if (httpSamplerProxy != null && (!httpSamplerProxy.isCustomizeReq() || (httpSamplerProxy.isCustomizeReq() && httpSamplerProxy.getIsRefEnvironment()))) { @@ -320,7 +357,11 @@ public class ElementUtil { } if (element.containsKey("hashTree")) { JSONArray elementJSONArray = element.getJSONArray("hashTree"); - dataSetDomain(elementJSONArray, msParameter); + if (isScenarioEnv) { + dataSetDomain(elementJSONArray, config); + } else { + dataSetDomain(elementJSONArray, msParameter); + } } } } catch (Exception e) { diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/MsScenario.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/MsScenario.java index dad8a24edd..48fae7b315 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/MsScenario.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/MsScenario.java @@ -64,9 +64,12 @@ public class MsScenario extends MsTestElement { @JSONField(ordinal = 27) private Map environmentMap; - @JSONField(ordinal = 24) + @JSONField(ordinal = 28) private Boolean onSampleError; + @JSONField(ordinal = 29) + private boolean environmentEnable; + private static final String BODY_FILE_DIR = FileUtils.BODY_FILE_DIR; public MsScenario() { @@ -152,8 +155,7 @@ public class MsScenario extends MsTestElement { } } else { Map map = config.getConfig(); - for (EnvironmentConfig evnConfig : - map.values()) { + for (EnvironmentConfig evnConfig : map.values()) { if (evnConfig.getHttpConfig() != null) { this.setMockEnvironment(evnConfig.getHttpConfig().isMock()); } @@ -174,12 +176,32 @@ public class MsScenario extends MsTestElement { //setHeader(tree, this.headers); config.setHeaders(this.headers); } + ParameterConfig newConfig = new ParameterConfig(); + if (this.isEnvironmentEnable() && this.environmentMap != null && !this.environmentMap.isEmpty()) { + environmentMap.keySet().forEach(projectId -> { + ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class); + ApiTestEnvironmentWithBLOBs environment = environmentService.get(this.environmentMap.get(projectId)); + if (environment != null && environment.getConfig() != null) { + EnvironmentConfig env = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class); + env.setApiEnvironmentid(environment.getId()); + envConfig.put(projectId, env); + if (StringUtils.equals(environment.getName(), MockConfigStaticData.MOCK_EVN_NAME)) { + this.setMockEnvironment(true); + } + } + }); + newConfig.setConfig(envConfig); + } if (CollectionUtils.isNotEmpty(hashTree)) { for (MsTestElement el : hashTree) { // 给所有孩子加一个父亲标志 el.setParent(this); el.setMockEnvironment(this.isMockEnvironment()); - el.toHashTree(tree, el.getHashTree(), config); + if (this.isEnvironmentEnable()) { + el.toHashTree(tree, el.getHashTree(), newConfig); + } else { + el.toHashTree(tree, el.getHashTree(), config); + } } } } diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/dns/MsDNSCacheManager.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/dns/MsDNSCacheManager.java index 5a36f8e1ff..fb771e9f96 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/dns/MsDNSCacheManager.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/dns/MsDNSCacheManager.java @@ -47,7 +47,7 @@ public class MsDNSCacheManager extends MsTestElement { } public static void addEnvironmentDNS(HashTree samplerHashTree, String name, EnvironmentConfig config, HttpConfig httpConfig) { - if (config.getCommonConfig().isEnableHost() && CollectionUtils.isNotEmpty(config.getCommonConfig().getHosts()) && httpConfig != null) { + if (config.getCommonConfig().isEnableHost() && CollectionUtils.isNotEmpty(config.getCommonConfig().getHosts()) && httpConfig != null && httpConfig.getDomain() != null) { String domain = httpConfig.getDomain().trim(); List hosts = new ArrayList<>(); config.getCommonConfig().getHosts().forEach(host -> { diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsHTTPSamplerProxy.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsHTTPSamplerProxy.java index 773e0f0da8..c64b66b22d 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsHTTPSamplerProxy.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/sampler/MsHTTPSamplerProxy.java @@ -1,7 +1,6 @@ package io.metersphere.api.dto.definition.request.sampler; import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.annotation.JSONField; import com.alibaba.fastjson.annotation.JSONType; @@ -225,16 +224,14 @@ public class MsHTTPSamplerProxy extends MsTestElement { setSamplerPath(config, httpConfig, sampler); - // 请求体 - if (!StringUtils.equals(this.getMethod(), "GET")) { - if (this.body != null) { - List bodyParams = this.body.getBodyParams(sampler, this.getId()); - if (StringUtils.isNotEmpty(this.body.getType()) && "Form Data".equals(this.body.getType())) { - sampler.setDoMultipart(true); - } - if (CollectionUtils.isNotEmpty(bodyParams)) { - sampler.setArguments(httpArguments(bodyParams)); - } + // 请求体处理 + if (this.body != null) { + List bodyParams = this.body.getBodyParams(sampler, this.getId()); + if (StringUtils.isNotEmpty(this.body.getType()) && "Form Data".equals(this.body.getType())) { + sampler.setDoMultipart(true); + } + if (CollectionUtils.isNotEmpty(bodyParams)) { + sampler.setArguments(httpArguments(bodyParams)); } } @@ -305,7 +302,7 @@ public class MsHTTPSamplerProxy extends MsTestElement { } preProcessor.toHashTree(httpSamplerTree, preProcessor.getHashTree(), config); } - if (postProcessor != null && StringUtils.isNotEmpty(preProcessor.getScript())) { + if (postProcessor != null && StringUtils.isNotEmpty(postProcessor.getScript())) { if (postProcessor.getEnvironmentId() == null) { if (this.getEnvironmentId() == null) { postProcessor.setEnvironmentId(useEnvironment); diff --git a/backend/src/main/java/io/metersphere/api/dto/mockconfig/MockConfigImportDTO.java b/backend/src/main/java/io/metersphere/api/dto/mockconfig/MockConfigImportDTO.java new file mode 100644 index 0000000000..a3478bff5f --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/mockconfig/MockConfigImportDTO.java @@ -0,0 +1,15 @@ +package io.metersphere.api.dto.mockconfig; + +import io.metersphere.base.domain.MockExpectConfigWithBLOBs; +import lombok.Getter; +import lombok.Setter; + +/** + * @author song.tianyang + * @Date 2021/9/27 5:54 下午 + */ +@Getter +@Setter +public class MockConfigImportDTO extends MockExpectConfigWithBLOBs { + public String apiId; +} diff --git a/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerHandler.java b/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerHandler.java index 4b14129810..c5c7c237d7 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerHandler.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerHandler.java @@ -56,5 +56,8 @@ public class APIBackendListenerHandler { if (!MessageCache.reportCache.containsKey(testId) && resultService.getProcessCache().containsKey(testId)) { resultService.getProcessCache().remove(testId); } + if(StringUtils.isNotEmpty(testId)) { + MessageCache.executionQueue.remove(testId); + } } } diff --git a/backend/src/main/java/io/metersphere/api/jmeter/FixedTask.java b/backend/src/main/java/io/metersphere/api/jmeter/FixedTask.java index 96a751aebd..b29b807434 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/FixedTask.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/FixedTask.java @@ -32,12 +32,12 @@ public class FixedTask { MessageCache.cache.remove(key); } else { try { - int number = 0; if (guardTask.containsKey(key)) { - number = guardTask.get(key); - guardTask.put(key, number++); - } else { + int number = guardTask.get(key); + number +=1; guardTask.put(key, number); + } else { + guardTask.put(key, 0); } if (CollectionUtils.isNotEmpty(counter.getPoolUrls()) && counter.getNumber() > 0 && guardTask.get(key) > 200) { // 资源池中已经没有执行的请求了 diff --git a/backend/src/main/java/io/metersphere/api/jmeter/JMeterVars.java b/backend/src/main/java/io/metersphere/api/jmeter/JMeterVars.java index e43b4a2ecc..a2900fb1d5 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/JMeterVars.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/JMeterVars.java @@ -43,6 +43,7 @@ public class JMeterVars { for (String item : extracts) { String nrKey = item + "_matchNr"; Object nr = vars.get(nrKey); + JMeterVariables jMeterVariables = new JMeterVariables(); if (nr != null) { int nrv = 0; try { @@ -55,10 +56,14 @@ public class JMeterVars { data.add(vars.get(item + "_" + i)); } String array = JSON.toJSONString(data); - vars.put(item, array); + jMeterVariables.put(item, array); } } - vs.put(item, vars.get(item) == null ? "" : vars.get(item)); + if (jMeterVariables.get(item) != null) { + vs.put(item, jMeterVariables.get(item)); + } else { + vs.put(item, vars.get(item) == null ? "" : vars.get(item)); + } } vs.remove("TESTSTART.MS"); // 标示变量移除 } diff --git a/backend/src/main/java/io/metersphere/api/jmeter/MessageCache.java b/backend/src/main/java/io/metersphere/api/jmeter/MessageCache.java index 9544e14293..47bd821598 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/MessageCache.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/MessageCache.java @@ -1,5 +1,6 @@ package io.metersphere.api.jmeter; +import io.metersphere.base.domain.ApiDefinitionExecResult; import org.apache.jmeter.engine.StandardJMeterEngine; import javax.websocket.Session; @@ -16,4 +17,10 @@ public class MessageCache { public static ConcurrentHashMap runningEngine = new ConcurrentHashMap<>(); public static ConcurrentLinkedDeque terminationOrderDeque = new ConcurrentLinkedDeque<>(); + + public static ConcurrentHashMap batchTestCases = new ConcurrentHashMap<>(); + + // 串行执行队列 KEY=报告ID VALUE=开始时间 + public static Map executionQueue = new HashMap<>(); + } diff --git a/backend/src/main/java/io/metersphere/api/jmeter/TestResult.java b/backend/src/main/java/io/metersphere/api/jmeter/TestResult.java index c270c6a47e..bbbb05d2b8 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/TestResult.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/TestResult.java @@ -121,9 +121,9 @@ public class TestResult { if (StringUtils.isNotEmpty(item.getName()) && item.getName().indexOf(SEPARATOR) != -1) { String array[] = item.getName().split(SEPARATOR); item.setName(array[1] + array[0]); - item.getSubRequestResults().forEach(subItem -> { - subItem.setName(array[0]); - }); +// item.getSubRequestResults().forEach(subItem -> { +// subItem.setName(array[0]); +// }); } else { this.genScenarioInSubRequestResult(item); } diff --git a/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java b/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java index 265f138ded..1b5821ff6a 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiAutomationService.java @@ -51,7 +51,9 @@ import io.metersphere.track.request.testcase.ApiCaseRelevanceRequest; import io.metersphere.track.request.testcase.QueryTestPlanRequest; import io.metersphere.track.request.testplan.FileOperationRequest; import io.metersphere.track.service.TestPlanScenarioCaseService; +import org.apache.commons.beanutils.BeanComparator; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections4.comparators.FixedOrderComparator; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.session.ExecutorType; @@ -714,6 +716,10 @@ public class ApiAutomationService { } if (StringUtils.equals(tr.getType(), "scenario")) { + MsScenario scenario = (MsScenario) tr; + if (scenario.isEnvironmentEnable()) { + continue; + } env.getProjectIds().add(tr.getProjectId()); } if (CollectionUtils.isNotEmpty(tr.getHashTree())) { @@ -786,6 +792,10 @@ public class ApiAutomationService { } } if (StringUtils.equals(tr.getType(), "scenario")) { + MsScenario scenario = (MsScenario) tr; + if (scenario.isEnvironmentEnable()) { + continue; + } env.getProjectIds().add(tr.getProjectId()); } if (CollectionUtils.isNotEmpty(tr.getHashTree())) { @@ -989,11 +999,19 @@ public class ApiAutomationService { // 生成集成报告 String serialReportId = null; - StringBuilder idStr = new StringBuilder(); - ids.forEach(item -> { - idStr.append("\"").append(item).append("\"").append(","); - }); - List apiScenarios = extApiScenarioMapper.selectByIds(idStr.toString().substring(0, idStr.toString().length() - 1), "\"" + StringUtils.join(ids, ",") + "\""); + ApiScenarioExample example = new ApiScenarioExample(); + example.createCriteria().andIdIn(ids); + List apiScenarios = apiScenarioMapper.selectByExampleWithBLOBs(example); + if (request.getConfig() != null && request.getConfig().getMode().equals(RunModeConstants.SERIAL.toString())) { + if (request.getCondition() == null || !request.getCondition().isSelectAll()) { + // 按照id指定顺序排序 + FixedOrderComparator fixedOrderComparator = new FixedOrderComparator(ids); + fixedOrderComparator.setUnknownObjectBehavior(FixedOrderComparator.UnknownObjectBehavior.BEFORE); + BeanComparator beanComparator = new BeanComparator("id", fixedOrderComparator); + Collections.sort(apiScenarios, beanComparator); + } + } + // 只有一个场景且没有测试步骤,则提示 if (apiScenarios != null && apiScenarios.size() == 1 && (apiScenarios.get(0).getStepTotal() == null || apiScenarios.get(0).getStepTotal() == 0)) { MSException.throwException((apiScenarios.get(0).getName() + "," + Translator.get("automation_exec_info"))); @@ -1154,6 +1172,7 @@ public class ApiAutomationService { MessageCache.terminationOrderDeque.remove(key); break; } + MessageCache.executionQueue.put(key, System.currentTimeMillis()); reportIds.add(key); APIScenarioReportResult report = executeQueue.get(key).getReport(); if (StringUtils.isNotEmpty(serialReportId)) { @@ -1185,6 +1204,7 @@ public class ApiAutomationService { executeEnvParams = hashTreeUtil.mergeParamDataMap(executeEnvParams, envParamsMap); } catch (Exception e) { reportIds.remove(key); + MessageCache.executionQueue.remove(key); LogUtil.error("执行终止:" + e.getMessage()); break; } @@ -1852,6 +1872,7 @@ public class ApiAutomationService { List useUrl = this.parseUrl(scenarioWithBLOBs); scenarioWithBLOBs.setUseUrl(JSONArray.toJSONString(useUrl)); scenarioWithBLOBs.setOrder(getImportNextOrder(request.getProjectId())); + scenarioWithBLOBs.setId(UUID.randomUUID().toString()); batchMapper.insert(scenarioWithBLOBs); apiScenarioReferenceIdService.saveByApiScenario(scenarioWithBLOBs); } @@ -1952,21 +1973,21 @@ public class ApiAutomationService { hashTree.set(i, object); } } else { - ApiScenarioWithBLOBs bloBs = this.getDto(object.getString("id")); + ApiScenarioWithBLOBs bloBs = apiScenarioMapper.selectByPrimaryKey(object.getString("id")); if (bloBs != null) { object = JSON.parseObject(bloBs.getScenarioDefinition()); hashTree.set(i, object); } } } else if ("scenario".equals(object.getString("type"))) { - ApiScenarioWithBLOBs bloBs = this.getDto(object.getString("id")); + ApiScenarioWithBLOBs bloBs = apiScenarioMapper.selectByPrimaryKey(object.getString("id")); if (bloBs != null) { object = JSON.parseObject(bloBs.getScenarioDefinition()); hashTree.set(i, object); } } } - if (CollectionUtils.isNotEmpty(object.getJSONArray("hashTree"))) { + if (object != null && CollectionUtils.isNotEmpty(object.getJSONArray("hashTree"))) { setHashTree(object.getJSONArray("hashTree")); } } @@ -1985,8 +2006,10 @@ public class ApiAutomationService { if (StringUtils.isNotEmpty(item.getScenarioDefinition())) { JSONObject scenario = JSONObject.parseObject(item.getScenarioDefinition()); JSONArray hashTree = scenario.getJSONArray("hashTree"); - setHashTree(hashTree); - scenario.put("hashTree", hashTree); + if (hashTree != null) { + setHashTree(hashTree); + scenario.put("hashTree", hashTree); + } item.setScenarioDefinition(JSON.toJSONString(scenario)); } }); @@ -2608,4 +2631,9 @@ public class ApiAutomationService { extApiScenarioMapper::getLastOrder, apiScenarioMapper::updateByPrimaryKeySelective); } + + public boolean checkScenarioEnv(ApiScenarioWithBLOBs request) { + return this.checkScenarioEnv(request, null); + } + } diff --git a/backend/src/main/java/io/metersphere/api/service/ApiDefinitionExecResultService.java b/backend/src/main/java/io/metersphere/api/service/ApiDefinitionExecResultService.java index ba4b181169..5bea05bdff 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiDefinitionExecResultService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiDefinitionExecResultService.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import io.metersphere.api.cache.TestPlanReportExecuteCatch; import io.metersphere.api.dto.datacount.ExecutedCaseInfoResult; +import io.metersphere.api.jmeter.MessageCache; import io.metersphere.api.jmeter.TestResult; import io.metersphere.base.domain.*; import io.metersphere.base.mapper.ApiDefinitionExecResultMapper; @@ -57,7 +58,8 @@ public class ApiDefinitionExecResultService { TestCaseReviewApiCaseMapper testCaseReviewApiCaseMapper; @Resource - SqlSessionFactory sqlSessionFactory; + private ApiDefinitionService apiDefinitionService; + @Resource private NoticeSendService noticeSendService; @@ -67,13 +69,14 @@ public class ApiDefinitionExecResultService { public void saveApiResult(TestResult result, String type, String triggerMode) { if (CollectionUtils.isNotEmpty(result.getScenarios())) { - SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); - ApiDefinitionExecResultMapper definitionExecResultMapper = sqlSession.getMapper(ApiDefinitionExecResultMapper.class); final boolean[] isFirst = {true}; result.getScenarios().forEach(scenarioResult -> { if (scenarioResult != null && CollectionUtils.isNotEmpty(scenarioResult.getRequestResults())) { scenarioResult.getRequestResults().forEach(item -> { - ApiDefinitionExecResult saveResult = definitionExecResultMapper.selectByPrimaryKey(result.getTestId()); + ApiDefinitionExecResult saveResult = MessageCache.batchTestCases.get(result.getTestId()); + if (saveResult == null) { + saveResult = apiDefinitionExecResultMapper.selectByPrimaryKey(result.getTestId()); + } item.getResponseResult().setConsole(result.getConsole()); boolean saved = true; if (saveResult == null || scenarioResult.getRequestResults().size() > 1) { @@ -88,6 +91,7 @@ public class ApiDefinitionExecResultService { saveResult.setName(item.getName()); saveResult.setTriggerMode(triggerMode); saveResult.setType(type); + saveResult.setCreateTime(item.getStartTime()); if (StringUtils.isNotEmpty(result.getUserId())) { saveResult.setUserId(result.getUserId()); } else { @@ -98,7 +102,6 @@ public class ApiDefinitionExecResultService { String status = item.isSuccess() ? "success" : "error"; saveResult.setName(getName(type, item.getName(), status, saveResult.getCreateTime(), saveResult.getId())); saveResult.setStatus(status); - saveResult.setCreateTime(item.getStartTime()); saveResult.setResourceId(item.getName()); saveResult.setContent(JSON.toJSONString(item)); saveResult.setStartTime(item.getStartTime()); @@ -108,23 +111,23 @@ public class ApiDefinitionExecResultService { ApiDefinitionExecResult prevResult = extApiDefinitionExecResultMapper.selectMaxResultByResourceIdAndType(item.getName(), type); if (prevResult != null) { prevResult.setContent(null); - definitionExecResultMapper.updateByPrimaryKeyWithBLOBs(prevResult); + apiDefinitionExecResultMapper.updateByPrimaryKeyWithBLOBs(prevResult); } if (StringUtils.isNotEmpty(saveResult.getTriggerMode()) && saveResult.getTriggerMode().equals("CASE")) { saveResult.setTriggerMode(TriggerMode.MANUAL.name()); } if (!saved) { - definitionExecResultMapper.insert(saveResult); + apiDefinitionExecResultMapper.insert(saveResult); } else { - definitionExecResultMapper.updateByPrimaryKeyWithBLOBs(saveResult); + apiDefinitionExecResultMapper.updateByPrimaryKeyWithBLOBs(saveResult); } + apiDefinitionService.removeCache(result.getTestId()); // 发送通知 sendNotice(saveResult); }); } }); - sqlSession.flushStatements(); } } @@ -205,6 +208,7 @@ public class ApiDefinitionExecResultService { // 更新用例最后执行结果 caseWithBLOBs.setLastResultId(resourceId); caseWithBLOBs.setStatus(status); + caseWithBLOBs.setUpdateTime(System.currentTimeMillis()); apiTestCaseMapper.updateByPrimaryKey(caseWithBLOBs); return caseWithBLOBs.getName(); } @@ -283,7 +287,7 @@ public class ApiDefinitionExecResultService { if (StringUtils.equals(type, ApiRunMode.SCHEDULE_API_PLAN.name())) { TestPlanApiCase apiCase = testPlanApiCaseService.getById(item.getName()); - if(StringUtils.isEmpty(creator)){ + if (StringUtils.isEmpty(creator)) { creator = testPlanService.findScheduleCreateUserById(apiCase.getTestPlanId()); } apiCase.setStatus(status); @@ -298,7 +302,7 @@ public class ApiDefinitionExecResultService { testPlanApiCaseService.setExecResult(item.getName(), status, item.getStartTime()); testCaseReviewApiCaseService.setExecResult(item.getName(), status, item.getStartTime()); } - if(creator == null){ + if (creator == null) { creator = ""; } saveResult.setUserId(creator); diff --git a/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java b/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java index 716ec384f4..4517f33043 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java @@ -14,6 +14,7 @@ import io.metersphere.api.dto.definition.parse.Swagger3Parser; import io.metersphere.api.dto.definition.request.ParameterConfig; import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy; import io.metersphere.api.dto.definition.request.sampler.MsTCPSampler; +import io.metersphere.api.dto.mockconfig.MockConfigImportDTO; import io.metersphere.api.dto.scenario.Body; import io.metersphere.api.dto.scenario.environment.EnvironmentConfig; import io.metersphere.api.dto.scenario.request.RequestType; @@ -97,8 +98,6 @@ public class ApiDefinitionService { @Resource private ExtSwaggerUrlScheduleMapper extSwaggerUrlScheduleMapper; @Resource - private ScheduleMapper scheduleMapper; - @Resource private ApiTestCaseMapper apiTestCaseMapper; @Resource private ApiTestEnvironmentService environmentService; @@ -117,7 +116,7 @@ public class ApiDefinitionService { @Resource private ExtApiTestCaseMapper extApiTestCaseMapper; - private static Cache cache = Cache.newHardMemoryCache(0, 3600 * 24); + private static Cache cache = Cache.newHardMemoryCache(0, 3600); private ThreadLocal currentApiOrder = new ThreadLocal<>(); private ThreadLocal currentApiCaseOrder = new ThreadLocal<>(); @@ -212,6 +211,8 @@ public class ApiDefinitionService { extApiDefinitionExecResultMapper.deleteByResourceId(apiId); apiDefinitionMapper.deleteByPrimaryKey(apiId); esbApiParamService.deleteByResourceId(apiId); + MockConfigService mockConfigService = CommonBeanFactory.getBean(MockConfigService.class); + mockConfigService.deleteMockConfigByApiId(apiId); FileUtils.deleteBodyFiles(apiId); } @@ -220,6 +221,11 @@ public class ApiDefinitionService { example.createCriteria().andIdIn(apiIds); esbApiParamService.deleteByResourceIdIn(apiIds); apiDefinitionMapper.deleteByExample(example); + apiTestCaseService.deleteBatchByDefinitionId(apiIds); + MockConfigService mockConfigService = CommonBeanFactory.getBean(MockConfigService.class); + for (String apiId : apiIds) { + mockConfigService.deleteMockConfigByApiId(apiId); + } } public void removeToGc(List apiIds) { @@ -467,7 +473,7 @@ public class ApiDefinitionService { } private ApiDefinition importCreate(ApiDefinitionWithBLOBs apiDefinition, ApiDefinitionMapper batchMapper, - ApiTestCaseMapper apiTestCaseMapper, ApiTestImportRequest apiTestImportRequest, List cases, + ApiTestCaseMapper apiTestCaseMapper, ApiTestImportRequest apiTestImportRequest, List cases, List mocks, Boolean repeatable) { SaveApiDefinitionRequest saveReq = new SaveApiDefinitionRequest(); BeanUtils.copyBean(saveReq, apiDefinition); @@ -494,7 +500,7 @@ public class ApiDefinitionService { sameRequest = getSameRequestById(apiDefinition.getId(), apiTestImportRequest.getProjectId()); } if (StringUtils.equals("fullCoverage", apiTestImportRequest.getModeId())) { - _importCreate(sameRequest, batchMapper, apiDefinition, apiTestCaseMapper, apiTestImportRequest, cases); + _importCreate(sameRequest, batchMapper, apiDefinition, apiTestCaseMapper, apiTestImportRequest, cases, mocks); } else if (StringUtils.equals("incrementalMerge", apiTestImportRequest.getModeId())) { if (CollectionUtils.isEmpty(sameRequest)) { //postman 可能含有前置脚本,接口定义去掉脚本 @@ -505,10 +511,10 @@ public class ApiDefinitionService { String requestStr = setImportHashTree(apiDefinition); reSetImportCasesApiId(cases, originId, apiDefinition.getId()); apiDefinition.setRequest(requestStr); - importApiCase(apiDefinition, apiTestCaseMapper, apiTestImportRequest, true); + importApiCase(apiDefinition, apiTestImportRequest); } } else { - _importCreate(sameRequest, batchMapper, apiDefinition, apiTestCaseMapper, apiTestImportRequest, cases); + _importCreate(sameRequest, batchMapper, apiDefinition, apiTestCaseMapper, apiTestImportRequest, cases, mocks); } return apiDefinition; @@ -535,17 +541,18 @@ public class ApiDefinitionService { } private void _importCreate(List sameRequest, ApiDefinitionMapper batchMapper, ApiDefinitionWithBLOBs apiDefinition, - ApiTestCaseMapper apiTestCaseMapper, ApiTestImportRequest apiTestImportRequest, List cases) { + ApiTestCaseMapper apiTestCaseMapper, ApiTestImportRequest apiTestImportRequest, List cases ,List mocks) { String originId = apiDefinition.getId(); if (CollectionUtils.isEmpty(sameRequest)) { apiDefinition.setId(UUID.randomUUID().toString()); apiDefinition.setOrder(getImportNextOrder(apiTestImportRequest.getProjectId())); reSetImportCasesApiId(cases, originId, apiDefinition.getId()); + reSetImportMocksApiId(mocks, originId, apiDefinition.getId()); if (StringUtils.equalsIgnoreCase(apiDefinition.getProtocol(), RequestType.HTTP)) { batchMapper.insert(apiDefinition); String request = setImportHashTree(apiDefinition); apiDefinition.setRequest(request); - importApiCase(apiDefinition, apiTestCaseMapper, apiTestImportRequest, true); + importApiCase(apiDefinition, apiTestImportRequest); } else { if (StringUtils.equalsAnyIgnoreCase(apiDefinition.getProtocol(), RequestType.TCP)) { setImportTCPHashTree(apiDefinition); @@ -565,7 +572,7 @@ public class ApiDefinitionService { apiDefinitionMapper.updateByPrimaryKeyWithBLOBs(apiDefinition); apiDefinition.setRequest(request); reSetImportCasesApiId(cases, originId, apiDefinition.getId()); - importApiCase(apiDefinition, apiTestCaseMapper, apiTestImportRequest, false); + importApiCase(apiDefinition, apiTestImportRequest); } else { apiDefinition.setId(sameRequest.get(0).getId()); if (StringUtils.equalsAnyIgnoreCase(apiDefinition.getProtocol(), RequestType.TCP)) { @@ -596,6 +603,16 @@ public class ApiDefinitionService { } } + private void reSetImportMocksApiId(List mocks, String originId, String newId) { + if (CollectionUtils.isNotEmpty(mocks)) { + mocks.forEach(item -> { + if (StringUtils.equals(item.getApiId(), originId)) { + item.setApiId(newId); + } + }); + } + } + private String setImportHashTree(ApiDefinitionWithBLOBs apiDefinition) { String request = apiDefinition.getRequest(); MsHTTPSamplerProxy msHTTPSamplerProxy = JSONObject.parseObject(request, MsHTTPSamplerProxy.class); @@ -614,7 +631,7 @@ public class ApiDefinitionService { return request; } - private void importMsCase(ApiDefinitionImport apiImport, SqlSession sqlSession, ApiTestCaseMapper apiTestCaseMapper, + private void importMsCase(ApiDefinitionImport apiImport, SqlSession sqlSession, ApiTestImportRequest request) { List cases = apiImport.getCases(); if (CollectionUtils.isNotEmpty(cases)) { @@ -625,7 +642,7 @@ public class ApiDefinitionService { if (apiDefinitionWithBLOBs == null) { continue; } - insertOrUpdateImportCase(item, request); + insertOrUpdateImportCase(item, request, apiDefinitionWithBLOBs); } if (batchCount % 300 == 0) { sqlSession.flushStatements(); @@ -637,8 +654,7 @@ public class ApiDefinitionService { * 导入是插件或者postman时创建用例 * postman考虑是否有前置脚本 */ - private void importApiCase(ApiDefinitionWithBLOBs apiDefinition, ApiTestCaseMapper apiTestCaseMapper, - ApiTestImportRequest apiTestImportRequest, Boolean isInsert) { + private void importApiCase(ApiDefinitionWithBLOBs apiDefinition, ApiTestImportRequest apiTestImportRequest) { try { if (StringUtils.equalsAnyIgnoreCase(apiTestImportRequest.getPlatform(), ApiImportPlatform.Plugin.name(), ApiImportPlatform.Postman.name())) { ApiTestCaseWithBLOBs apiTestCase = new ApiTestCaseWithBLOBs(); @@ -648,14 +664,14 @@ public class ApiDefinitionService { if (apiTestCase.getName().length() > 255) { apiTestCase.setName(apiTestCase.getName().substring(0, 255)); } - insertOrUpdateImportCase(apiTestCase, apiTestImportRequest); + insertOrUpdateImportCase(apiTestCase, apiTestImportRequest, apiDefinition); } } catch (Exception e) { LogUtil.error("导入创建用例异常", e); } } - private void insertOrUpdateImportCase(ApiTestCaseWithBLOBs apiTestCase, ApiTestImportRequest apiTestImportRequest) { + private void insertOrUpdateImportCase(ApiTestCaseWithBLOBs apiTestCase, ApiTestImportRequest apiTestImportRequest, ApiDefinitionWithBLOBs apiDefinition) { SaveApiTestCaseRequest checkRequest = new SaveApiTestCaseRequest(); checkRequest.setName(apiTestCase.getName()); checkRequest.setApiDefinitionId(apiTestCase.getApiDefinitionId()); @@ -667,7 +683,7 @@ public class ApiDefinitionService { apiTestCase.setUpdateUserId(SessionUtils.getUserId()); if (sameCase == null) { apiTestCase.setId(UUID.randomUUID().toString()); - apiTestCase.setNum(apiTestCaseService.getNextNum(apiTestCase.getApiDefinitionId())); + apiTestCase.setNum(apiTestCaseService.getNextNum(apiTestCase.getApiDefinitionId(), apiDefinition.getNum())); apiTestCase.setCreateTime(System.currentTimeMillis()); apiTestCase.setUpdateTime(System.currentTimeMillis()); apiTestCase.setCreateUserId(SessionUtils.getUserId()); @@ -800,6 +816,12 @@ public class ApiDefinitionService { return null; } + public void removeCache(String testId) { + if (StringUtils.isNotEmpty(testId)) { + cache.remove(testId); + } + } + /** * 获取存储执行结果报告 * @@ -932,14 +954,14 @@ public class ApiDefinitionService { if (apiImport.getEsbApiParamsMap() != null) { String apiId = item.getId(); EsbApiParamsWithBLOBs model = apiImport.getEsbApiParamsMap().get(apiId); - importCreate(item, batchMapper, apiTestCaseMapper, request, apiImport.getCases(), project.getRepeatable()); + importCreate(item, batchMapper, apiTestCaseMapper, request, apiImport.getCases(), apiImport.getMocks(), project.getRepeatable()); if (model != null) { apiImport.getEsbApiParamsMap().remove(apiId); model.setResourceId(item.getId()); apiImport.getEsbApiParamsMap().put(item.getId(), model); } } else { - importCreate(item, batchMapper, apiTestCaseMapper, request, apiImport.getCases(), project.getRepeatable()); + importCreate(item, batchMapper, apiTestCaseMapper, request, apiImport.getCases(), apiImport.getMocks(), project.getRepeatable()); } if (i % 300 == 0) { sqlSession.flushStatements(); @@ -963,8 +985,13 @@ public class ApiDefinitionService { } } + if (!CollectionUtils.isEmpty(apiImport.getMocks())) { + MockConfigService mockConfigService = CommonBeanFactory.getBean(MockConfigService.class); + mockConfigService.importMock(apiImport, sqlSession, request); + } + if (!CollectionUtils.isEmpty(apiImport.getCases())) { - importMsCase(apiImport, sqlSession, apiTestCaseMapper, request); + importMsCase(apiImport, sqlSession, request); } } @@ -1051,6 +1078,7 @@ public class ApiDefinitionService { public void deleteByParams(ApiBatchRequest request) { apiDefinitionMapper.deleteByExample(getBatchExample(request)); + apiTestCaseService.deleteBatchByDefinitionId(request.getIds()); } public ApiDefinitionExample getBatchExample(ApiBatchRequest request) { @@ -1248,9 +1276,11 @@ public class ApiDefinitionService { example.createCriteria().andIdIn(request.getIds()); if (StringUtils.equals(type, "MS")) { // 导出为 Metersphere 格式 + MockConfigService mockConfigService = CommonBeanFactory.getBean(MockConfigService.class); apiExportResult = new MsApiExportResult(); ((MsApiExportResult) apiExportResult).setData(apiDefinitionMapper.selectByExampleWithBLOBs(example)); ((MsApiExportResult) apiExportResult).setCases(apiTestCaseService.selectCasesBydApiIds(request.getIds())); + ((MsApiExportResult) apiExportResult).setMocks(mockConfigService.selectMockExpectConfigByApiIdIn(request.getIds())); ((MsApiExportResult) apiExportResult).setProjectName(request.getProjectId()); ((MsApiExportResult) apiExportResult).setProtocol(request.getProtocol()); ((MsApiExportResult) apiExportResult).setProjectId(request.getProjectId()); diff --git a/backend/src/main/java/io/metersphere/api/service/ApiModuleService.java b/backend/src/main/java/io/metersphere/api/service/ApiModuleService.java index 04696bd2bb..6a9eb321c9 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiModuleService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiModuleService.java @@ -342,7 +342,7 @@ public class ApiModuleService extends NodeTreeService { SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); ApiDefinitionMapper apiDefinitionMapper = sqlSession.getMapper(ApiDefinitionMapper.class); apiModule.forEach((value) -> { - apiDefinitionMapper.updateByPrimaryKey(value); + apiDefinitionMapper.updateByPrimaryKeySelective(value); }); sqlSession.flushStatements(); } diff --git a/backend/src/main/java/io/metersphere/api/service/ApiScenarioModuleService.java b/backend/src/main/java/io/metersphere/api/service/ApiScenarioModuleService.java index a9876e0118..2739aa8ad1 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiScenarioModuleService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiScenarioModuleService.java @@ -287,7 +287,7 @@ public class ApiScenarioModuleService extends NodeTreeService apiScenarios) { SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); ApiScenarioMapper apiScenarioMapper = sqlSession.getMapper(ApiScenarioMapper.class); - apiScenarios.forEach(apiScenarioMapper::updateByPrimaryKey); + apiScenarios.forEach(apiScenarioMapper::updateByPrimaryKeySelective); sqlSession.flushStatements(); } diff --git a/backend/src/main/java/io/metersphere/api/service/ApiTestCaseService.java b/backend/src/main/java/io/metersphere/api/service/ApiTestCaseService.java index 1e99bd08b3..2404211d5e 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiTestCaseService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiTestCaseService.java @@ -18,6 +18,7 @@ import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy; import io.metersphere.api.dto.scenario.environment.EnvironmentConfig; import io.metersphere.api.dto.scenario.request.RequestType; import io.metersphere.api.jmeter.JMeterService; +import io.metersphere.api.jmeter.MessageCache; import io.metersphere.base.domain.*; import io.metersphere.base.mapper.*; import io.metersphere.base.mapper.ext.*; @@ -396,6 +397,18 @@ public class ApiTestCaseService { } } + public int getNextNum(String definitionId, Integer definitionNum) { + ApiTestCase apiTestCase = extApiTestCaseMapper.getNextNum(definitionId); + if (apiTestCase == null) { + if (definitionNum == null) { + return apiDefinitionMapper.selectByPrimaryKey(definitionId).getNum() * 1000 + 1; + } + return definitionNum * 1000 + 1; + } else { + return Optional.of(apiTestCase.getNum() + 1) + .orElse(apiDefinitionMapper.selectByPrimaryKey(definitionId).getNum() * 1000 + 1); + } + } private void deleteFileByTestId(String testId) { ApiTestFileExample ApiTestFileExample = new ApiTestFileExample(); @@ -433,6 +446,17 @@ public class ApiTestCaseService { apiTestCaseMapper.deleteByExample(example); } + public void deleteBatchByDefinitionId(List definitionIds) { + ApiTestCaseExample example = new ApiTestCaseExample(); + example.createCriteria().andApiDefinitionIdIn(definitionIds); + apiTestCaseMapper.deleteByExample(example); + List apiTestCases = apiTestCaseMapper.selectByExample(example); + List caseIds = apiTestCases.stream().map(ApiTestCase::getId).collect(Collectors.toList()); + for (String testId : caseIds) { + extTestPlanTestCaseMapper.deleteByTestCaseID(testId); + } + } + public void relevanceByApi(ApiCaseRelevanceRequest request) { if (CollectionUtils.isEmpty(request.getSelectIds())) { return; @@ -471,7 +495,7 @@ public class ApiTestCaseService { ExtTestPlanApiCaseMapper batchMapper = sqlSession.getMapper(ExtTestPlanApiCaseMapper.class); Long nextOrder = ServiceUtils.getNextOrder(request.getPlanId(), extTestPlanApiCaseMapper::getLastOrder); - for (ApiTestCase apiTestCase: apiTestCases) { + for (ApiTestCase apiTestCase : apiTestCases) { TestPlanApiCase testPlanApiCase = new TestPlanApiCase(); testPlanApiCase.setId(UUID.randomUUID().toString()); testPlanApiCase.setCreateUser(SessionUtils.getUserId()); @@ -678,50 +702,54 @@ public class ApiTestCaseService { public void batchRun(ApiCaseBatchRequest request) { ServiceUtils.getSelectAllIds(request, request.getCondition(), - (query) -> extApiTestCaseMapper.selectIdsByQuery(query)); - Map executeQueue = new HashMap<>(); + (query) -> extApiTestCaseMapper.selectIdsByQuery((ApiTestCaseRequest) query)); + List executeQueue = new LinkedList<>(); SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); ApiDefinitionExecResultMapper batchMapper = sqlSession.getMapper(ApiDefinitionExecResultMapper.class); - for (String testCaseId : request.getIds()) { - ApiDefinitionExecResult report = addResult(testCaseId, APITestStatus.Running.name()); - ApiTestCaseWithBLOBs caseWithBLOBs = apiTestCaseMapper.selectByPrimaryKey(testCaseId); - if (caseWithBLOBs != null) { - report.setName(caseWithBLOBs.getName()); - caseWithBLOBs.setLastResultId(report.getId()); - caseWithBLOBs.setUpdateTime(System.currentTimeMillis()); - caseWithBLOBs.setStatus(APITestStatus.Running.name()); - apiTestCaseMapper.updateByPrimaryKey(caseWithBLOBs); - } - batchMapper.insert(report); - executeQueue.put(testCaseId, report); - } - sqlSession.flushStatements(); - for (String caseId : executeQueue.keySet()) { + ApiTestCaseExample example = new ApiTestCaseExample(); + example.createCriteria().andIdIn(request.getIds()); + List list = apiTestCaseMapper.selectByExampleWithBLOBs(example); + + ApiTestCaseMapper sqlSessionMapper = sqlSession.getMapper(ApiTestCaseMapper.class); + for (ApiTestCaseWithBLOBs caseWithBLOBs : list) { + ApiDefinitionExecResult report = addResult(caseWithBLOBs.getId(), APITestStatus.Running.name()); + report.setName(caseWithBLOBs.getName()); + caseWithBLOBs.setLastResultId(report.getId()); + caseWithBLOBs.setUpdateTime(System.currentTimeMillis()); + caseWithBLOBs.setStatus(APITestStatus.Running.name()); + sqlSessionMapper.updateByPrimaryKey(caseWithBLOBs); + + // 执行对象 RunCaseRequest runCaseRequest = new RunCaseRequest(); runCaseRequest.setRunMode(ApiRunMode.DEFINITION.name()); - runCaseRequest.setCaseId(caseId); - runCaseRequest.setReportId(executeQueue.get(caseId).getId()); + runCaseRequest.setCaseId(caseWithBLOBs.getId()); + runCaseRequest.setReportId(report.getId()); runCaseRequest.setEnvironmentId(request.getEnvironmentId()); + runCaseRequest.setBloBs(caseWithBLOBs); + runCaseRequest.setReport(report); + + batchMapper.insert(report); + executeQueue.add(runCaseRequest); + } + sqlSession.flushStatements(); + for (RunCaseRequest runCaseRequest : executeQueue) { run(runCaseRequest); + MessageCache.batchTestCases.put(runCaseRequest.getReportId(), runCaseRequest.getReport()); } } public String run(RunCaseRequest request) { - ApiTestCaseWithBLOBs testCaseWithBLOBs = null; + ApiTestCaseWithBLOBs testCaseWithBLOBs = request.getBloBs(); if (StringUtils.equals(request.getRunMode(), ApiRunMode.JENKINS_API_PLAN.name())) { testCaseWithBLOBs = apiTestCaseMapper.selectByPrimaryKey(request.getReportId()); request.setCaseId(request.getReportId()); - } else { - testCaseWithBLOBs = apiTestCaseMapper.selectByPrimaryKey(request.getCaseId()); + //通过测试计划id查询环境 + request.setReportId(request.getTestPlanId()); } if (StringUtils.equals(request.getRunMode(), ApiRunMode.JENKINS.name())) { request.setReportId(request.getEnvironmentId()); } - if (StringUtils.equals(request.getRunMode(), ApiRunMode.JENKINS_API_PLAN.name())) { - //通过测试计划id查询环境 - request.setReportId(request.getTestPlanId()); - } // 多态JSON普通转换会丢失内容,需要通过 ObjectMapper 获取 if (testCaseWithBLOBs != null && StringUtils.isNotEmpty(testCaseWithBLOBs.getRequest())) { try { @@ -1066,6 +1094,7 @@ public class ApiTestCaseService { /** * 用例自定义排序 + * * @param request */ public void updateOrder(ResetOrderRequest request) { diff --git a/backend/src/main/java/io/metersphere/api/service/MockConfigService.java b/backend/src/main/java/io/metersphere/api/service/MockConfigService.java index f3f3104045..f3b8ca06ca 100644 --- a/backend/src/main/java/io/metersphere/api/service/MockConfigService.java +++ b/backend/src/main/java/io/metersphere/api/service/MockConfigService.java @@ -4,9 +4,12 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONValidator; +import io.metersphere.api.dto.ApiTestImportRequest; import io.metersphere.api.dto.automation.EsbDataStruct; import io.metersphere.api.dto.automation.TcpTreeTableDataStruct; import io.metersphere.api.dto.automation.parse.TcpTreeTableDataParser; +import io.metersphere.api.dto.definition.parse.ApiDefinitionImport; +import io.metersphere.api.dto.mockconfig.MockConfigImportDTO; import io.metersphere.api.dto.mockconfig.MockConfigRequest; import io.metersphere.api.dto.mockconfig.MockExpectConfigRequest; import io.metersphere.api.dto.mockconfig.response.JsonSchemaReturnObj; @@ -23,6 +26,7 @@ import io.metersphere.jmeter.utils.ScriptEngineUtils; import io.metersphere.i18n.Translator; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.session.SqlSession; import org.json.XML; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -65,6 +69,28 @@ public class MockConfigService { return this.assemblyMockConfingResponse(configList); } + public List selectMockExpectConfigByApiId(String apiId){ + return extMockExpectConfigMapper.selectByApiId(apiId); + } + + public List selectMockExpectConfigByApiIdIn(List apiIds){ + if(CollectionUtils.isNotEmpty(apiIds)){ + List returnDTO = new ArrayList<>(); + for (String apiId : apiIds) { + List mockExpectConfigWithBLOBsList = extMockExpectConfigMapper.selectByApiId(apiId); + for (MockExpectConfigWithBLOBs model : mockExpectConfigWithBLOBsList) { + MockConfigImportDTO dto = new MockConfigImportDTO(); + BeanUtils.copyBean(dto, model); + dto.setApiId(apiId); + returnDTO.add(dto); + } + } + return returnDTO; + }else { + return new ArrayList<>(); + } + } + private MockConfigResponse assemblyMockConfingResponse(List configList) { if (!configList.isEmpty()) { MockConfig config = configList.get(0); @@ -610,6 +636,19 @@ public class MockConfigService { mockExpectConfigMapper.deleteByPrimaryKey(id); } + public void deleteMockConfigByApiId(String apiId){ + MockConfigExample configExample = new MockConfigExample(); + configExample.createCriteria().andApiIdEqualTo(apiId); + List mockConfigList = mockConfigMapper.selectByExample(configExample); + MockExpectConfigExample example = new MockExpectConfigExample(); + for (MockConfig mockConfig : mockConfigList) { + example.clear(); + example.createCriteria().andMockConfigIdEqualTo(mockConfig.getId()); + mockExpectConfigMapper.deleteByExample(example); + } + mockConfigMapper.deleteByExample(configExample); + } + public JSONObject getGetParamMap(String urlParams, ApiDefinitionWithBLOBs api, HttpServletRequest request) { JSONObject paramMap = this.getSendRestParamMapByIdAndUrl(api, urlParams); Enumeration paramNameItor = request.getParameterNames(); @@ -1187,4 +1226,51 @@ public class MockConfigService { } return isJson; } + + public void importMock(ApiDefinitionImport apiImport, SqlSession sqlSession, ApiTestImportRequest request) { + if(CollectionUtils.isNotEmpty(apiImport.getMocks())){ + Map> saveMap = new HashMap<>(); + for (MockConfigImportDTO dto : apiImport.getMocks()) { + String apiId = dto.getApiId();//de33108c-26e2-4d4f-826a-a5f8e017d2f4 + if(saveMap.containsKey(apiId)){ + saveMap.get(apiId).add(dto); + }else { + List list = new ArrayList<>(); + list.add(dto); + saveMap.put(apiId,list); + } + } + + for (Map.Entry> entry : saveMap.entrySet()) { + String apiId = entry.getKey(); + this.deleteMockConfigByApiId(apiId); + + List list = entry.getValue(); + + String mockId = UUID.randomUUID().toString(); + MockConfig config = new MockConfig(); + config.setProjectId(request.getProjectId()); + config.setId(mockId); + config.setCreateUserId(SessionUtils.getUserId()); + config.setCreateTime(System.currentTimeMillis()); + config.setUpdateTime(System.currentTimeMillis()); + config.setApiId(apiId); + mockConfigMapper.insert(config); + + int batchCount = 0; + for (MockExpectConfigWithBLOBs mockExpect : list) { + mockExpect.setId(UUID.randomUUID().toString()); + mockExpect.setMockConfigId(mockId); + mockExpect.setCreateTime(System.currentTimeMillis()); + mockExpect.setUpdateTime(System.currentTimeMillis()); + mockExpect.setCreateUserId(SessionUtils.getUserId()); + mockExpectConfigMapper.insert(mockExpect); + } + if (batchCount % 300 == 0) { + sqlSession.flushStatements(); + } + } + + } + } } diff --git a/backend/src/main/java/io/metersphere/api/service/TestResultService.java b/backend/src/main/java/io/metersphere/api/service/TestResultService.java index a87416adf8..1d6274f33f 100644 --- a/backend/src/main/java/io/metersphere/api/service/TestResultService.java +++ b/backend/src/main/java/io/metersphere/api/service/TestResultService.java @@ -150,11 +150,7 @@ public class TestResultService { testPlanTestCaseService.updateTestCaseStates(ids, TestPlanTestCaseStatus.Failure.name()); } } - if (reportTask != null) { - if (!StringUtils.equals(ApiRunMode.SCHEDULE_SCENARIO_PLAN.name(), runMode) && !StringUtils.equals(ApiRunMode.JENKINS_SCENARIO_PLAN.name(), runMode) && StringUtils.equals(ReportTriggerMode.API.name(), reportTask.getTriggerMode()) || StringUtils.equals(ReportTriggerMode.SCHEDULE.name(), reportTask.getTriggerMode())) { - sendTask(reportTask, reportUrl, testResult); - } - } + } catch (Exception e) { e.printStackTrace(); LogUtil.error(e.getMessage(), e); @@ -193,66 +189,4 @@ public class TestResultService { } } - private static void sendTask(ApiTestReportVariable report, String reportUrl, TestResult testResult) { - if (report == null) { - return; - } - SystemParameterService systemParameterService = CommonBeanFactory.getBean(SystemParameterService.class); - NoticeSendService noticeSendService = CommonBeanFactory.getBean(NoticeSendService.class); - assert systemParameterService != null; - assert noticeSendService != null; - BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo(); - String url = baseSystemConfigDTO.getUrl() + "/#/api/report/view/" + report.getId(); - String url2 = baseSystemConfigDTO.getUrl() + "/#/api/automation/report/view/" + report.getId(); - - String successContext = ""; - String failedContext = ""; - String subject = ""; - String event = ""; - if (StringUtils.equals(ReportTriggerMode.API.name(), report.getTriggerMode())) { - successContext = "接口测试 API任务通知:jenkins所执行的" + report.getName() + "'执行成功" + "\n" + "执行环境:" + report.getExecutionEnvironment() + "\n" + "[接口定义暂无报告链接]" + "\n" + "请点击下面链接进入测试报告页面" + "\n" + "(旧版)接口测试路径" + url + "\n" + "(新版)接口测试路径" + url2; - failedContext = "接口测试 API任务通知:jenkins所执行的" + report.getName() + "'执行失败" + "\n" + "执行环境:" + report.getExecutionEnvironment() + "\n" + "[接口定义暂无报告链接]" + "\n" + "请点击下面链接进入测试报告页面" + "\n" + "(旧版)接口测试路径" + url + "\n" + "(新版)接口测试路径" + url2; - subject = Translator.get("task_notification_jenkins"); - } - if (StringUtils.equals(ReportTriggerMode.SCHEDULE.name(), report.getTriggerMode())) { - successContext = "接口测试定时任务通知:定时任务所执行的" + report.getName() + "'执行成功" + "\n" + "执行环境:" + report.getExecutionEnvironment() + "\n" + "[接口定义暂无报告链接]" + "\n" + "请点击下面链接进入测试报告页面" + "\n" + "(旧版)接口测试路径" + url + "\n" + "(新版)接口测试路径" + url2; - failedContext = "接口测试定时任务通知:定时任务所执行的" + report.getName() + "'执行失败" + "\n" + "执行环境:" + report.getExecutionEnvironment() + "\n" + "[接口定义暂无报告链接]" + "\n" + "请点击下面链接进入测试报告页面" + "\n" + "(旧版)接口测试路径" + url + "\n" + "(新版)接口测试路径" + url2; - subject = Translator.get("task_notification"); - } - if (StringUtils.equals("Success", report.getStatus())) { - event = NoticeConstants.Event.EXECUTE_SUCCESSFUL; - } - if (StringUtils.equals("success", report.getStatus())) { - event = NoticeConstants.Event.EXECUTE_SUCCESSFUL; - } - if (StringUtils.equals("Error", report.getStatus())) { - event = NoticeConstants.Event.EXECUTE_FAILED; - } - if (StringUtils.equals("error", report.getStatus())) { - event = NoticeConstants.Event.EXECUTE_FAILED; - } - Map paramMap = new HashMap<>(); - paramMap.put("testName", report.getName()); - paramMap.put("id", report.getId()); - paramMap.put("type", "api"); - paramMap.put("url", baseSystemConfigDTO.getUrl()); - paramMap.put("status", report.getStatus()); - paramMap.put("executor", report.getExecutor()); - paramMap.put("executionTime", report.getExecutionTime()); - paramMap.put("executionEnvironment", report.getExecutionEnvironment()); - paramMap.put("principal", report.getPrincipal()); - NoticeModel noticeModel = NoticeModel.builder() - .operator(SessionUtils.getUserId()) - .successContext(successContext) - .successMailTemplate("ApiSuccessfulNotification") - .failedContext(failedContext) - .failedMailTemplate("ApiFailedNotification") - .testId(testResult.getTestId()) - .status(report.getStatus()) - .event(event) - .subject(subject) - .paramMap(paramMap) - .build(); - noticeSendService.send(report.getTriggerMode(), NoticeConstants.TaskType.API_DEFINITION_TASK, noticeModel); - } } diff --git a/backend/src/main/java/io/metersphere/api/service/task/SerialScenarioExecTask.java b/backend/src/main/java/io/metersphere/api/service/task/SerialScenarioExecTask.java index 0151101252..8d2825d347 100644 --- a/backend/src/main/java/io/metersphere/api/service/task/SerialScenarioExecTask.java +++ b/backend/src/main/java/io/metersphere/api/service/task/SerialScenarioExecTask.java @@ -34,7 +34,7 @@ public class SerialScenarioExecTask implements Callable { @Override public T call() { try { - if (runModeDataDTO.getReport()!=null && MessageCache.terminationOrderDeque.contains(runModeDataDTO.getReport().getId())) { + if (runModeDataDTO.getReport() != null && MessageCache.terminationOrderDeque.contains(runModeDataDTO.getReport().getId())) { MessageCache.terminationOrderDeque.remove(runModeDataDTO.getReport().getId()); return null; } @@ -43,25 +43,24 @@ public class SerialScenarioExecTask implements Callable { } else { jMeterService.runLocal(runModeDataDTO.getReport().getId(), runModeDataDTO.getHashTree(), TriggerMode.BATCH.name().equals(request.getTriggerMode()) ? TriggerMode.BATCH.name() : request.getReportId(), request.getRunMode()); } - // 轮询查看报告状态,最多200次,防止死循环 - int index = 1; - while (index < 200) { - Thread.sleep(3000); - index++; - report = apiScenarioReportMapper.selectByPrimaryKey(runModeDataDTO.getReport().getId()); - if (report != null && !report.getStatus().equals(APITestStatus.Running.name())) { + while (MessageCache.executionQueue.containsKey(runModeDataDTO.getReport().getId())) { + long currentSecond = (System.currentTimeMillis() - MessageCache.executionQueue.get(runModeDataDTO.getReport().getId())) / 1000 / 60; + // 设置五分钟超时 + if (currentSecond > 5) { + // 执行失败了,恢复报告状态 + report = apiScenarioReportMapper.selectByPrimaryKey(runModeDataDTO.getReport().getId()); + if (report != null) { + report.setStatus(APITestStatus.Error.name()); + apiScenarioReportMapper.updateByPrimaryKey(report); + } break; } - if (runModeDataDTO.getReport()!=null && MessageCache.terminationOrderDeque.contains(runModeDataDTO.getReport().getId())) { + if (runModeDataDTO.getReport() != null && MessageCache.terminationOrderDeque.contains(runModeDataDTO.getReport().getId())) { MessageCache.terminationOrderDeque.remove(runModeDataDTO.getReport().getId()); break; } } - // 执行失败了,恢复报告状态 - if (index == 200 && report != null && report.getStatus().equals(APITestStatus.Running.name())) { - report.setStatus(APITestStatus.Error.name()); - apiScenarioReportMapper.updateByPrimaryKey(report); - } + report = apiScenarioReportMapper.selectByPrimaryKey(runModeDataDTO.getReport().getId()); return (T) report; } catch (Exception ex) { LogUtil.error(ex); diff --git a/backend/src/main/java/io/metersphere/api/tcp/server/TCPServer.java b/backend/src/main/java/io/metersphere/api/tcp/server/TCPServer.java index 561e577593..cf60f4509c 100644 --- a/backend/src/main/java/io/metersphere/api/tcp/server/TCPServer.java +++ b/backend/src/main/java/io/metersphere/api/tcp/server/TCPServer.java @@ -34,7 +34,7 @@ public class TCPServer implements Runnable { } public boolean isSocketOpen(){ - if (this.serverSocket != null && !this.serverSocket.isClosed() &&this.servicer != null) { + if (this.serverSocket != null && !this.serverSocket.isClosed()) { return true; }else { return false; diff --git a/backend/src/main/java/io/metersphere/base/domain/TestCaseReviewTestCase.java b/backend/src/main/java/io/metersphere/base/domain/TestCaseReviewTestCase.java index 57ee239129..8eb587f353 100644 --- a/backend/src/main/java/io/metersphere/base/domain/TestCaseReviewTestCase.java +++ b/backend/src/main/java/io/metersphere/base/domain/TestCaseReviewTestCase.java @@ -23,5 +23,7 @@ public class TestCaseReviewTestCase implements Serializable { private String createUser; + private Long order; + private static final long serialVersionUID = 1L; -} \ No newline at end of file +} diff --git a/backend/src/main/java/io/metersphere/base/domain/TestCaseReviewTestCaseExample.java b/backend/src/main/java/io/metersphere/base/domain/TestCaseReviewTestCaseExample.java index 1942553bf5..910f082a44 100644 --- a/backend/src/main/java/io/metersphere/base/domain/TestCaseReviewTestCaseExample.java +++ b/backend/src/main/java/io/metersphere/base/domain/TestCaseReviewTestCaseExample.java @@ -713,6 +713,66 @@ public class TestCaseReviewTestCaseExample { addCriterion("create_user not between", value1, value2, "createUser"); return (Criteria) this; } + + public Criteria andOrderIsNull() { + addCriterion("`order` is null"); + return (Criteria) this; + } + + public Criteria andOrderIsNotNull() { + addCriterion("`order` is not null"); + return (Criteria) this; + } + + public Criteria andOrderEqualTo(Long value) { + addCriterion("`order` =", value, "order"); + return (Criteria) this; + } + + public Criteria andOrderNotEqualTo(Long value) { + addCriterion("`order` <>", value, "order"); + return (Criteria) this; + } + + public Criteria andOrderGreaterThan(Long value) { + addCriterion("`order` >", value, "order"); + return (Criteria) this; + } + + public Criteria andOrderGreaterThanOrEqualTo(Long value) { + addCriterion("`order` >=", value, "order"); + return (Criteria) this; + } + + public Criteria andOrderLessThan(Long value) { + addCriterion("`order` <", value, "order"); + return (Criteria) this; + } + + public Criteria andOrderLessThanOrEqualTo(Long value) { + addCriterion("`order` <=", value, "order"); + return (Criteria) this; + } + + public Criteria andOrderIn(List values) { + addCriterion("`order` in", values, "order"); + return (Criteria) this; + } + + public Criteria andOrderNotIn(List values) { + addCriterion("`order` not in", values, "order"); + return (Criteria) this; + } + + public Criteria andOrderBetween(Long value1, Long value2) { + addCriterion("`order` between", value1, value2, "order"); + return (Criteria) this; + } + + public Criteria andOrderNotBetween(Long value1, Long value2) { + addCriterion("`order` not between", value1, value2, "order"); + return (Criteria) this; + } } public static class Criteria extends GeneratedCriteria { @@ -807,4 +867,4 @@ public class TestCaseReviewTestCaseExample { this(condition, value, secondValue, null); } } -} \ No newline at end of file +} diff --git a/backend/src/main/java/io/metersphere/base/domain/TestPlanPrincipal.java b/backend/src/main/java/io/metersphere/base/domain/TestPlanPrincipal.java new file mode 100644 index 0000000000..57d14eff1f --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/TestPlanPrincipal.java @@ -0,0 +1,13 @@ +package io.metersphere.base.domain; + +import java.io.Serializable; +import lombok.Data; + +@Data +public class TestPlanPrincipal implements Serializable { + private String testPlanId; + + private String principalId; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/domain/TestPlanPrincipalExample.java b/backend/src/main/java/io/metersphere/base/domain/TestPlanPrincipalExample.java new file mode 100644 index 0000000000..82dccf2e9e --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/TestPlanPrincipalExample.java @@ -0,0 +1,340 @@ +package io.metersphere.base.domain; + +import java.util.ArrayList; +import java.util.List; + +public class TestPlanPrincipalExample { + protected String orderByClause; + + protected boolean distinct; + + protected List oredCriteria; + + public TestPlanPrincipalExample() { + oredCriteria = new ArrayList(); + } + + public void setOrderByClause(String orderByClause) { + this.orderByClause = orderByClause; + } + + public String getOrderByClause() { + return orderByClause; + } + + public void setDistinct(boolean distinct) { + this.distinct = distinct; + } + + public boolean isDistinct() { + return distinct; + } + + public List getOredCriteria() { + return oredCriteria; + } + + public void or(Criteria criteria) { + oredCriteria.add(criteria); + } + + public Criteria or() { + Criteria criteria = createCriteriaInternal(); + oredCriteria.add(criteria); + return criteria; + } + + public Criteria createCriteria() { + Criteria criteria = createCriteriaInternal(); + if (oredCriteria.size() == 0) { + oredCriteria.add(criteria); + } + return criteria; + } + + protected Criteria createCriteriaInternal() { + Criteria criteria = new Criteria(); + return criteria; + } + + public void clear() { + oredCriteria.clear(); + orderByClause = null; + distinct = false; + } + + protected abstract static class GeneratedCriteria { + protected List criteria; + + protected GeneratedCriteria() { + super(); + criteria = new ArrayList(); + } + + public boolean isValid() { + return criteria.size() > 0; + } + + public List getAllCriteria() { + return criteria; + } + + public List getCriteria() { + return criteria; + } + + protected void addCriterion(String condition) { + if (condition == null) { + throw new RuntimeException("Value for condition cannot be null"); + } + criteria.add(new Criterion(condition)); + } + + protected void addCriterion(String condition, Object value, String property) { + if (value == null) { + throw new RuntimeException("Value for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value)); + } + + protected void addCriterion(String condition, Object value1, Object value2, String property) { + if (value1 == null || value2 == null) { + throw new RuntimeException("Between values for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value1, value2)); + } + + public Criteria andTestPlanIdIsNull() { + addCriterion("test_plan_id is null"); + return (Criteria) this; + } + + public Criteria andTestPlanIdIsNotNull() { + addCriterion("test_plan_id is not null"); + return (Criteria) this; + } + + public Criteria andTestPlanIdEqualTo(String value) { + addCriterion("test_plan_id =", value, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdNotEqualTo(String value) { + addCriterion("test_plan_id <>", value, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdGreaterThan(String value) { + addCriterion("test_plan_id >", value, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdGreaterThanOrEqualTo(String value) { + addCriterion("test_plan_id >=", value, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdLessThan(String value) { + addCriterion("test_plan_id <", value, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdLessThanOrEqualTo(String value) { + addCriterion("test_plan_id <=", value, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdLike(String value) { + addCriterion("test_plan_id like", value, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdNotLike(String value) { + addCriterion("test_plan_id not like", value, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdIn(List values) { + addCriterion("test_plan_id in", values, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdNotIn(List values) { + addCriterion("test_plan_id not in", values, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdBetween(String value1, String value2) { + addCriterion("test_plan_id between", value1, value2, "testPlanId"); + return (Criteria) this; + } + + public Criteria andTestPlanIdNotBetween(String value1, String value2) { + addCriterion("test_plan_id not between", value1, value2, "testPlanId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdIsNull() { + addCriterion("principal_id is null"); + return (Criteria) this; + } + + public Criteria andPrincipalIdIsNotNull() { + addCriterion("principal_id is not null"); + return (Criteria) this; + } + + public Criteria andPrincipalIdEqualTo(String value) { + addCriterion("principal_id =", value, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdNotEqualTo(String value) { + addCriterion("principal_id <>", value, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdGreaterThan(String value) { + addCriterion("principal_id >", value, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdGreaterThanOrEqualTo(String value) { + addCriterion("principal_id >=", value, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdLessThan(String value) { + addCriterion("principal_id <", value, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdLessThanOrEqualTo(String value) { + addCriterion("principal_id <=", value, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdLike(String value) { + addCriterion("principal_id like", value, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdNotLike(String value) { + addCriterion("principal_id not like", value, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdIn(List values) { + addCriterion("principal_id in", values, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdNotIn(List values) { + addCriterion("principal_id not in", values, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdBetween(String value1, String value2) { + addCriterion("principal_id between", value1, value2, "principalId"); + return (Criteria) this; + } + + public Criteria andPrincipalIdNotBetween(String value1, String value2) { + addCriterion("principal_id not between", value1, value2, "principalId"); + return (Criteria) this; + } + } + + public static class Criteria extends GeneratedCriteria { + + protected Criteria() { + super(); + } + } + + public static class Criterion { + private String condition; + + private Object value; + + private Object secondValue; + + private boolean noValue; + + private boolean singleValue; + + private boolean betweenValue; + + private boolean listValue; + + private String typeHandler; + + public String getCondition() { + return condition; + } + + public Object getValue() { + return value; + } + + public Object getSecondValue() { + return secondValue; + } + + public boolean isNoValue() { + return noValue; + } + + public boolean isSingleValue() { + return singleValue; + } + + public boolean isBetweenValue() { + return betweenValue; + } + + public boolean isListValue() { + return listValue; + } + + public String getTypeHandler() { + return typeHandler; + } + + protected Criterion(String condition) { + super(); + this.condition = condition; + this.typeHandler = null; + this.noValue = true; + } + + protected Criterion(String condition, Object value, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.typeHandler = typeHandler; + if (value instanceof List) { + this.listValue = true; + } else { + this.singleValue = true; + } + } + + protected Criterion(String condition, Object value) { + this(condition, value, null); + } + + protected Criterion(String condition, Object value, Object secondValue, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.secondValue = secondValue; + this.typeHandler = typeHandler; + this.betweenValue = true; + } + + protected Criterion(String condition, Object value, Object secondValue) { + this(condition, value, secondValue, null); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/TestCaseReviewTestCaseMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/TestCaseReviewTestCaseMapper.xml index 71ec423e60..f761467d23 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/TestCaseReviewTestCaseMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/TestCaseReviewTestCaseMapper.xml @@ -11,6 +11,7 @@ + @@ -71,7 +72,8 @@ - id, review_id, case_id, `status`, `result`, reviewer, create_time, update_time, create_user + id, review_id, case_id, `status`, `result`, reviewer, create_time, update_time, create_user, + `order` @@ -210,6 +218,9 @@ create_user = #{record.createUser,jdbcType=VARCHAR}, + + `order` = #{record.order,jdbcType=BIGINT}, + @@ -225,7 +236,8 @@ reviewer = #{record.reviewer,jdbcType=VARCHAR}, create_time = #{record.createTime,jdbcType=BIGINT}, update_time = #{record.updateTime,jdbcType=BIGINT}, - create_user = #{record.createUser,jdbcType=VARCHAR} + create_user = #{record.createUser,jdbcType=VARCHAR}, + `order` = #{record.order,jdbcType=BIGINT} @@ -257,6 +269,9 @@ create_user = #{createUser,jdbcType=VARCHAR}, + + `order` = #{order,jdbcType=BIGINT}, + where id = #{id,jdbcType=VARCHAR} @@ -269,7 +284,8 @@ reviewer = #{reviewer,jdbcType=VARCHAR}, create_time = #{createTime,jdbcType=BIGINT}, update_time = #{updateTime,jdbcType=BIGINT}, - create_user = #{createUser,jdbcType=VARCHAR} + create_user = #{createUser,jdbcType=VARCHAR}, + `order` = #{order,jdbcType=BIGINT} where id = #{id,jdbcType=VARCHAR} - \ No newline at end of file + diff --git a/backend/src/main/java/io/metersphere/base/mapper/TestCaseTestMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/TestCaseTestMapper.xml index 58c095e1b4..ee7ee42e10 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/TestCaseTestMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/TestCaseTestMapper.xml @@ -144,7 +144,7 @@ from api_test_case atc left join test_case_test tct on atc.id = tct.test_id inner join api_definition ad on ad.id = atc.api_definition_id - where tct.test_id is NULL and ad.status != 'Trash' + where tct.test_id is NULL and atc.status != 'Trash' and ad.protocol = #{request.protocol} diff --git a/backend/src/main/java/io/metersphere/base/mapper/TestPlanPrincipalMapper.java b/backend/src/main/java/io/metersphere/base/mapper/TestPlanPrincipalMapper.java new file mode 100644 index 0000000000..09a94a42d9 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/TestPlanPrincipalMapper.java @@ -0,0 +1,22 @@ +package io.metersphere.base.mapper; + +import io.metersphere.base.domain.TestPlanPrincipal; +import io.metersphere.base.domain.TestPlanPrincipalExample; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface TestPlanPrincipalMapper { + long countByExample(TestPlanPrincipalExample example); + + int deleteByExample(TestPlanPrincipalExample example); + + int insert(TestPlanPrincipal record); + + int insertSelective(TestPlanPrincipal record); + + List selectByExample(TestPlanPrincipalExample example); + + int updateByExampleSelective(@Param("record") TestPlanPrincipal record, @Param("example") TestPlanPrincipalExample example); + + int updateByExample(@Param("record") TestPlanPrincipal record, @Param("example") TestPlanPrincipalExample example); +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/TestPlanPrincipalMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/TestPlanPrincipalMapper.xml new file mode 100644 index 0000000000..d6dfbffb90 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/TestPlanPrincipalMapper.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + test_plan_id, principal_id + + + + delete from test_plan_principal + + + + + + insert into test_plan_principal (test_plan_id, principal_id) + values (#{testPlanId,jdbcType=VARCHAR}, #{principalId,jdbcType=VARCHAR}) + + + insert into test_plan_principal + + + test_plan_id, + + + principal_id, + + + + + #{testPlanId,jdbcType=VARCHAR}, + + + #{principalId,jdbcType=VARCHAR}, + + + + + + update test_plan_principal + + + test_plan_id = #{record.testPlanId,jdbcType=VARCHAR}, + + + principal_id = #{record.principalId,jdbcType=VARCHAR}, + + + + + + + + update test_plan_principal + set test_plan_id = #{record.testPlanId,jdbcType=VARCHAR}, + principal_id = #{record.principalId,jdbcType=VARCHAR} + + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiTestCaseMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiTestCaseMapper.java index 1933160b42..49439a4ec7 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiTestCaseMapper.java +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiTestCaseMapper.java @@ -48,7 +48,7 @@ public interface ExtApiTestCaseMapper { List selectNameByIdIn(@Param("ids")List ids); String selectNameById(String id); - List selectIdsByQuery(BaseQueryRequest query); + List selectIdsByQuery(@Param("request") ApiTestCaseRequest request); List selectProjectIds(); diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiTestCaseMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiTestCaseMapper.xml index 18a4ece5b8..80f278a383 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiTestCaseMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtApiTestCaseMapper.xml @@ -559,6 +559,9 @@ #{nodeId} + + and a.protocol = #{request.protocol} + @@ -571,7 +574,7 @@ - + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.java new file mode 100644 index 0000000000..87e088bccd --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.java @@ -0,0 +1,14 @@ +package io.metersphere.base.mapper.ext; + + +import io.metersphere.reportstatistics.dto.TestAnalysisChartRequest; +import io.metersphere.reportstatistics.dto.TestAnalysisChartResult; + +import java.util.List; + +public interface ExtTestAnalysisMapper { + + List getCraeteCaseReport(TestAnalysisChartRequest request); + + List getUpdateCaseReport(TestAnalysisChartRequest request); +} diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.xml new file mode 100644 index 0000000000..c4c9e36489 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestAnalysisMapper.xml @@ -0,0 +1,100 @@ + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.java new file mode 100644 index 0000000000..060f2735ee --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.java @@ -0,0 +1,65 @@ +package io.metersphere.base.mapper.ext; + +import io.metersphere.reportstatistics.dto.TestCaseCountChartResult; +import io.metersphere.reportstatistics.dto.TestCaseCountRequest; + +import java.util.List; + +public interface ExtTestCaseCountMapper { + + /** + * 创建人 维护人 用例类型 用例状态 用例等级 + * + * create_user + * maintainer + * '功能用例' + * status + * priority + * + * @ request + * @return + */ + List getFunctionCaseCount(TestCaseCountRequest request); + + /** + * 创建人 维护人 用例类型 用例状态 用例等级 + * + * create_user_id + * ----不知道 + * '接口用例' + * status + * ----不知道 + * + * @param request + * @return + */ + List getApiCaseCount(TestCaseCountRequest request); + + /** + * 创建人 维护人 用例类型 用例状态 用例等级 + * + * create_user + * principal + * '场景用例' + * status + * level + * + * @param request + * @return + */ + List getScenarioCaseCount(TestCaseCountRequest request); + + /** + * 创建人 维护人 用例类型 用例状态 用例等级 + * + * create_user + * follow_people + * '性能用例' + * status + * 不知道 + * + * @param request + * @return + */ + List getLoadCaseCount(TestCaseCountRequest request); +} diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.xml new file mode 100644 index 0000000000..33ea3c3bf3 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseCountMapper.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.xml index f5365a7133..ba4a2edee3 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.xml @@ -390,39 +390,38 @@ GROUP BY test_case.priority - - + + + + + + + + diff --git a/backend/src/main/java/io/metersphere/commons/utils/JsonStructUtils.java b/backend/src/main/java/io/metersphere/commons/utils/JsonStructUtils.java index 496ab6c3ec..214a98fb48 100644 --- a/backend/src/main/java/io/metersphere/commons/utils/JsonStructUtils.java +++ b/backend/src/main/java/io/metersphere/commons/utils/JsonStructUtils.java @@ -10,6 +10,7 @@ import java.util.Set; /** * JSON数据结构相关的工具类 + * * @author song.tianyang * @Date 2021/8/16 3:50 下午 */ @@ -26,14 +27,18 @@ public class JsonStructUtils { if (sourceObj == null && matchObj == null) { return true; } else if (sourceObj != null && matchObj != null) { - boolean isMatch = false; + boolean lastMatchResultIsTrue = false; + boolean hasNotMatchResult = false; try { Set matchKeys = matchObj.keySet(); for (String key : matchKeys) { if (sourceObj.containsKey(key)) { Object sourceObjItem = sourceObj.get(key); Object matchObjItem = matchObj.get(key); - isMatch = checkObjCompliance(sourceObjItem, matchObjItem); + lastMatchResultIsTrue = checkObjCompliance(sourceObjItem, matchObjItem); + if (!lastMatchResultIsTrue) { + hasNotMatchResult = true; + } } else { return false; } @@ -41,7 +46,7 @@ public class JsonStructUtils { } catch (Exception e) { e.printStackTrace(); } - return isMatch; + return lastMatchResultIsTrue && !hasNotMatchResult; } else { return false; } @@ -52,9 +57,9 @@ public class JsonStructUtils { return true; } else if (sourceArray != null && matchArray != null && sourceArray.size() > matchArray.size()) { try { - for (int i = 0; i < matchArray.size(); i ++) { + for (int i = 0; i < matchArray.size(); i++) { Object obj = matchArray.get(i); - if(!sourceArray.contains(obj)){ + if (!sourceArray.contains(obj)) { return false; } } @@ -70,13 +75,13 @@ public class JsonStructUtils { public static boolean checkJsonArrayContainsObj(JSONArray sourceArray, JSONObject matchObj) { if (sourceArray == null && matchObj == null) { return true; - } else if (sourceArray != null && matchObj != null ) { + } else if (sourceArray != null && matchObj != null) { try { - for (int i = 0; i < sourceArray.size(); i ++) { + for (int i = 0; i < sourceArray.size(); i++) { Object obj = sourceArray.get(i); - if(obj instanceof JSONObject){ - boolean isMatch = checkJsonObjCompliance((JSONObject) obj,matchObj); - if(isMatch){ + if (obj instanceof JSONObject) { + boolean isMatch = checkJsonObjCompliance((JSONObject) obj, matchObj); + if (isMatch) { return isMatch; } } @@ -92,6 +97,7 @@ public class JsonStructUtils { /** * 检查一个JSON对象的数据集合是否包含另一个对象(包含) + * * @param sourceArray * @param matchObj * @return @@ -103,14 +109,14 @@ public class JsonStructUtils { boolean isMatch = false; try { Set matchKeys = matchObj.keySet(); - for(int sourceIndex = 0;sourceIndex < sourceArray.size();sourceIndex ++){ + for (int sourceIndex = 0; sourceIndex < sourceArray.size(); sourceIndex++) { JSONObject sourceObj = sourceArray.getJSONObject(sourceIndex); for (String key : matchKeys) { if (sourceObj.containsKey(key)) { Object sourceObjItem = sourceObj.get(key); Object matchObjItem = matchObj.get(key); isMatch = checkObjCompliance(sourceObjItem, matchObjItem); - if(!isMatch){ + if (!isMatch) { break; } } else { @@ -119,7 +125,7 @@ public class JsonStructUtils { } } - if(isMatch){ + if (isMatch) { break; } } @@ -173,12 +179,12 @@ public class JsonStructUtils { public static void deepParseKeyByJsonObject(JSONObject jsonObject, List keyList) { for (String key : jsonObject.keySet()) { Object obj = jsonObject.get(key); - if(obj instanceof JSONArray) { + if (obj instanceof JSONArray) { deepParseKeyByJsonArray((JSONArray) obj, keyList); - }else if(obj instanceof JSONObject){ - deepParseKeyByJsonObject((JSONObject) obj,keyList); - }else { - if(!keyList.contains(key)){ + } else if (obj instanceof JSONObject) { + deepParseKeyByJsonObject((JSONObject) obj, keyList); + } else { + if (!keyList.contains(key)) { keyList.add(key); } } @@ -186,10 +192,10 @@ public class JsonStructUtils { } public static void deepParseKeyByJsonArray(JSONArray jsonArray, List keyList) { - for (int i = 0; i < jsonArray.size(); i ++) { + for (int i = 0; i < jsonArray.size(); i++) { Object itemObj = jsonArray.get(i); - if(itemObj instanceof JSONObject){ - deepParseKeyByJsonObject((JSONObject)itemObj,keyList); + if (itemObj instanceof JSONObject) { + deepParseKeyByJsonObject((JSONObject) itemObj, keyList); } } } @@ -204,31 +210,31 @@ public class JsonStructUtils { JSONValidator matchValidator = JSONValidator.from(matchJson); String sourceType = sourceValidator.getType().name(); String matchType = matchValidator.getType().name(); - if(StringUtils.equalsIgnoreCase(sourceType,"array")&&StringUtils.equalsIgnoreCase(matchType,"array")){ + if (StringUtils.equalsIgnoreCase(sourceType, "array") && StringUtils.equalsIgnoreCase(matchType, "array")) { isSourceJsonIsArray = true; isMatchJsonIsArray = true; - }else if(StringUtils.equalsIgnoreCase(sourceType,"array")){ + } else if (StringUtils.equalsIgnoreCase(sourceType, "array")) { isSourceJsonIsArray = true; - }else if(StringUtils.equalsIgnoreCase(matchType,"array")){ + } else if (StringUtils.equalsIgnoreCase(matchType, "array")) { isMatchJsonIsArray = true; } - if(isSourceJsonIsArray && isMatchJsonIsArray){ + if (isSourceJsonIsArray && isMatchJsonIsArray) { JSONArray sourceArr = JSONArray.parseArray(sourceJson); JSONArray compArr = JSONArray.parseArray(matchJson); - isMatch = checkJsonArrayCompliance(sourceArr,compArr); - }else if(isSourceJsonIsArray && !isMatchJsonIsArray){ + isMatch = checkJsonArrayCompliance(sourceArr, compArr); + } else if (isSourceJsonIsArray && !isMatchJsonIsArray) { JSONArray sourceArr = JSONArray.parseArray(sourceJson); JSONObject compObj = JSONObject.parseObject(matchJson); - isMatch = checkJsonArrayContainsObj(sourceArr,compObj); - }else if(!isSourceJsonIsArray && !isMatchJsonIsArray){ + isMatch = checkJsonArrayContainsObj(sourceArr, compObj); + } else if (!isSourceJsonIsArray && !isMatchJsonIsArray) { JSONObject sourceObj = JSONObject.parseObject(sourceJson); JSONObject compObj = JSONObject.parseObject(matchJson); - isMatch = checkJsonObjCompliance(sourceObj,compObj); - }else { + isMatch = checkJsonObjCompliance(sourceObj, compObj); + } else { isMatch = false; } - }catch (Exception e){ + } catch (Exception e) { } return isMatch; diff --git a/backend/src/main/java/io/metersphere/commons/utils/ServiceUtils.java b/backend/src/main/java/io/metersphere/commons/utils/ServiceUtils.java index 8c12a976a2..8e259c5d10 100644 --- a/backend/src/main/java/io/metersphere/commons/utils/ServiceUtils.java +++ b/backend/src/main/java/io/metersphere/commons/utils/ServiceUtils.java @@ -35,6 +35,10 @@ public class ServiceUtils { return getDefaultOrderByField(null, orders, "order"); } + public static List getDefaultSortOrder(String prefix, List orders) { + return getDefaultOrderByField(prefix, orders, "order"); + } + public static List getDefaultOrder(String prefix, List orders) { return getDefaultOrderByField(prefix, orders, "update_time"); } diff --git a/backend/src/main/java/io/metersphere/dto/CustomFieldItemDTO.java b/backend/src/main/java/io/metersphere/dto/CustomFieldItemDTO.java index 668bcb06d6..8b26585e7a 100644 --- a/backend/src/main/java/io/metersphere/dto/CustomFieldItemDTO.java +++ b/backend/src/main/java/io/metersphere/dto/CustomFieldItemDTO.java @@ -6,7 +6,7 @@ import lombok.Data; public class CustomFieldItemDTO { private String id; private String name; - private String value; + private Object value; private String type; private String customData; } diff --git a/backend/src/main/java/io/metersphere/excel/listener/TestCaseNoModelDataListener.java b/backend/src/main/java/io/metersphere/excel/listener/TestCaseNoModelDataListener.java index 0b706e3215..343ebbf855 100644 --- a/backend/src/main/java/io/metersphere/excel/listener/TestCaseNoModelDataListener.java +++ b/backend/src/main/java/io/metersphere/excel/listener/TestCaseNoModelDataListener.java @@ -416,6 +416,9 @@ public class TestCaseNoModelDataListener extends AnalysisEventListener searchMessageByTypeBySend(String type, String projectId) { try { String orgId = ""; - if (null == SessionUtils.getUser()) { + if (StringUtils.isNotEmpty(projectId)) { Organization organization = extProjectMapper.getOrganizationByProjectId(projectId); orgId = organization.getId(); } else { diff --git a/backend/src/main/java/io/metersphere/performance/controller/JmeterFileController.java b/backend/src/main/java/io/metersphere/performance/controller/JmeterFileController.java index aeeafce010..f37877c13e 100644 --- a/backend/src/main/java/io/metersphere/performance/controller/JmeterFileController.java +++ b/backend/src/main/java/io/metersphere/performance/controller/JmeterFileController.java @@ -48,7 +48,7 @@ public class JmeterFileController { @RequestParam("ratio") String ratio, @RequestParam("reportId") String reportId, @RequestParam("resourceIndex") int resourceIndex) { double[] ratios = Arrays.stream(ratio.split(",")).mapToDouble(Double::parseDouble).toArray(); - byte[] bytes = jmeterFileService.downloadZip(testId, ratios, reportId, resourceIndex); + byte[] bytes = jmeterFileService.downloadZip(reportId, ratios, resourceIndex); return ResponseEntity.ok() .contentType(MediaType.parseMediaType("application/octet-stream")) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + testId + ".zip\"") diff --git a/backend/src/main/java/io/metersphere/performance/engine/AbstractEngine.java b/backend/src/main/java/io/metersphere/performance/engine/AbstractEngine.java index b7cf5e605b..2c17313cea 100644 --- a/backend/src/main/java/io/metersphere/performance/engine/AbstractEngine.java +++ b/backend/src/main/java/io/metersphere/performance/engine/AbstractEngine.java @@ -4,7 +4,7 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import io.metersphere.api.dto.RunRequest; -import io.metersphere.base.domain.LoadTestWithBLOBs; +import io.metersphere.base.domain.LoadTestReportWithBLOBs; import io.metersphere.base.domain.TestResource; import io.metersphere.base.domain.TestResourcePool; import io.metersphere.commons.constants.PerformanceTestStatus; @@ -21,15 +21,12 @@ import org.apache.commons.lang3.StringUtils; import java.util.Iterator; import java.util.List; -import java.util.UUID; public abstract class AbstractEngine implements Engine { protected String JMETER_IMAGE; protected String HEAP; protected String GC_ALGO; - private Long startTime; - private String reportId; - protected LoadTestWithBLOBs loadTest; + protected LoadTestReportWithBLOBs loadTestReport; protected PerformanceTestService performanceTestService; protected Integer threadNum; protected List resourceList; @@ -43,8 +40,6 @@ public abstract class AbstractEngine implements Engine { JMETER_IMAGE = CommonBeanFactory.getBean(JmeterProperties.class).getImage(); HEAP = CommonBeanFactory.getBean(JmeterProperties.class).getHeap(); GC_ALGO = CommonBeanFactory.getBean(JmeterProperties.class).getGcAlgo(); - this.startTime = System.currentTimeMillis(); - this.reportId = UUID.randomUUID().toString(); } protected void initApiConfig(RunRequest runRequest) { @@ -81,16 +76,16 @@ public abstract class AbstractEngine implements Engine { } } - protected void init(LoadTestWithBLOBs loadTest) { - if (loadTest == null) { + protected void init(LoadTestReportWithBLOBs loadTestReport) { + if (loadTestReport == null) { MSException.throwException("LoadTest is null."); } - this.loadTest = loadTest; + this.loadTestReport = loadTestReport; this.performanceTestService = CommonBeanFactory.getBean(PerformanceTestService.class); - threadNum = getThreadNum(loadTest); - String resourcePoolId = loadTest.getTestResourcePoolId(); + threadNum = getThreadNum(loadTestReport); + String resourcePoolId = loadTestReport.getTestResourcePoolId(); if (StringUtils.isBlank(resourcePoolId)) { MSException.throwException("Resource Pool ID is empty"); } @@ -127,16 +122,16 @@ public abstract class AbstractEngine implements Engine { } protected Integer getRunningThreadNum() { - List loadTests = performanceTestService.selectByTestResourcePoolId(loadTest.getTestResourcePoolId()); + List loadTestReports = performanceTestService.selectReportsByTestResourcePoolId(loadTestReport.getTestResourcePoolId()); // 使用当前资源池正在运行的测试占用的并发数 - return loadTests.stream() + return loadTestReports.stream() .filter(t -> PerformanceTestStatus.Running.name().equals(t.getStatus())) .map(this::getThreadNum) .reduce(Integer::sum) .orElse(0); } - private Integer getThreadNum(LoadTestWithBLOBs t) { + private Integer getThreadNum(LoadTestReportWithBLOBs t) { Integer s = 0; String loadConfiguration = t.getLoadConfiguration(); JSONArray jsonArray = JSON.parseArray(loadConfiguration); @@ -175,14 +170,4 @@ public abstract class AbstractEngine implements Engine { } return s; } - - @Override - public Long getStartTime() { - return startTime; - } - - @Override - public String getReportId() { - return reportId; - } } diff --git a/backend/src/main/java/io/metersphere/performance/engine/Engine.java b/backend/src/main/java/io/metersphere/performance/engine/Engine.java index 541d1134d8..328be1d87a 100644 --- a/backend/src/main/java/io/metersphere/performance/engine/Engine.java +++ b/backend/src/main/java/io/metersphere/performance/engine/Engine.java @@ -1,10 +1,6 @@ package io.metersphere.performance.engine; public interface Engine { - Long getStartTime(); - - String getReportId(); - void start(); void stop(); diff --git a/backend/src/main/java/io/metersphere/performance/engine/EngineFactory.java b/backend/src/main/java/io/metersphere/performance/engine/EngineFactory.java index 6b78a53272..44f219fe88 100644 --- a/backend/src/main/java/io/metersphere/performance/engine/EngineFactory.java +++ b/backend/src/main/java/io/metersphere/performance/engine/EngineFactory.java @@ -6,10 +6,11 @@ import io.metersphere.Application; import io.metersphere.api.dto.RunRequest; import io.metersphere.base.domain.FileContent; import io.metersphere.base.domain.FileMetadata; -import io.metersphere.base.domain.LoadTestWithBLOBs; +import io.metersphere.base.domain.LoadTestReportWithBLOBs; import io.metersphere.base.domain.TestResourcePool; import io.metersphere.commons.constants.FileType; import io.metersphere.commons.constants.ResourcePoolTypeEnum; +import io.metersphere.commons.constants.ResourceStatusEnum; import io.metersphere.commons.exception.MSException; import io.metersphere.commons.utils.LogUtil; import io.metersphere.i18n.Translator; @@ -65,8 +66,8 @@ public class EngineFactory { } } - public static Engine createEngine(LoadTestWithBLOBs loadTest) { - String resourcePoolId = loadTest.getTestResourcePoolId(); + public static Engine createEngine(LoadTestReportWithBLOBs loadTestReport) { + String resourcePoolId = loadTestReport.getTestResourcePoolId(); if (StringUtils.isBlank(resourcePoolId)) { MSException.throwException(Translator.get("test_resource_pool_id_is_null")); } @@ -75,15 +76,18 @@ public class EngineFactory { if (resourcePool == null) { MSException.throwException(Translator.get("test_resource_pool_id_is_null")); } + if (ResourceStatusEnum.INVALID.name().equals(resourcePool.getStatus())) { + MSException.throwException(Translator.get("test_resource_pool_invalid")); + } final ResourcePoolTypeEnum type = ResourcePoolTypeEnum.valueOf(resourcePool.getType()); if (type == ResourcePoolTypeEnum.NODE) { - return new DockerTestEngine(loadTest); + return new DockerTestEngine(loadTestReport); } if (type == ResourcePoolTypeEnum.K8S) { try { - return (Engine) ConstructorUtils.invokeConstructor(kubernetesTestEngineClass, loadTest); + return (Engine) ConstructorUtils.invokeConstructor(kubernetesTestEngineClass, loadTestReport); } catch (Exception e) { LogUtil.error(e); return null; @@ -102,10 +106,10 @@ public class EngineFactory { return null; } - public static EngineContext createContext(LoadTestWithBLOBs loadTest, double[] ratios, String reportId, int resourceIndex) { - final List fileMetadataList = performanceTestService.getFileMetadataByTestId(loadTest.getId()); + public static EngineContext createContext(LoadTestReportWithBLOBs loadTestReport, double[] ratios, String reportId, int resourceIndex) { + final List fileMetadataList = performanceTestService.getFileMetadataByTestId(loadTestReport.getTestId()); if (org.springframework.util.CollectionUtils.isEmpty(fileMetadataList)) { - MSException.throwException(Translator.get("run_load_test_file_not_found") + loadTest.getId()); + MSException.throwException(Translator.get("run_load_test_file_not_found") + loadTestReport.getTestId()); } List jmxFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.JMX.name())).collect(Collectors.toList()); @@ -113,17 +117,17 @@ public class EngineFactory { // 合并上传的jmx byte[] jmxBytes = mergeJmx(jmxFiles); final EngineContext engineContext = new EngineContext(); - engineContext.setTestId(loadTest.getId()); - engineContext.setTestName(loadTest.getName()); - engineContext.setNamespace(loadTest.getProjectId()); + engineContext.setTestId(loadTestReport.getTestId()); + engineContext.setTestName(loadTestReport.getName()); + engineContext.setNamespace(loadTestReport.getProjectId()); engineContext.setFileType(FileType.JMX.name()); - engineContext.setResourcePoolId(loadTest.getTestResourcePoolId()); + engineContext.setResourcePoolId(loadTestReport.getTestResourcePoolId()); engineContext.setReportId(reportId); engineContext.setResourceIndex(resourceIndex); engineContext.setRatios(ratios); - if (StringUtils.isNotEmpty(loadTest.getLoadConfiguration())) { - final JSONArray jsonArray = JSONObject.parseArray(loadTest.getLoadConfiguration()); + if (StringUtils.isNotEmpty(loadTestReport.getLoadConfiguration())) { + final JSONArray jsonArray = JSONObject.parseArray(loadTestReport.getLoadConfiguration()); for (int i = 0; i < jsonArray.size(); i++) { if (jsonArray.get(i) instanceof List) { @@ -160,8 +164,8 @@ public class EngineFactory { {"timeout":10,"statusCode":["302","301"],"params":[{"name":"param1","enable":true,"value":"0","edit":false}],"domains":[{"domain":"baidu.com","enable":true,"ip":"127.0.0.1","edit":false}]} */ Map testResourceFiles = new HashMap<>(); - byte[] props = getJMeterProperties(loadTest, engineContext); - byte[] hosts = getDNSConfig(loadTest, engineContext); + byte[] props = getJMeterProperties(loadTestReport, engineContext); + byte[] hosts = getDNSConfig(loadTestReport, engineContext); // JMeter Properties testResourceFiles.put("ms.properties", props); // DNS @@ -195,10 +199,10 @@ public class EngineFactory { return engineContext; } - private static byte[] getDNSConfig(LoadTestWithBLOBs loadTest, EngineContext engineContext) { + private static byte[] getDNSConfig(LoadTestReportWithBLOBs loadTestReport, EngineContext engineContext) { StringBuilder dns = new StringBuilder("# DNS Config\n"); - if (StringUtils.isNotEmpty(loadTest.getAdvancedConfiguration())) { - JSONObject advancedConfiguration = JSONObject.parseObject(loadTest.getAdvancedConfiguration()); + if (StringUtils.isNotEmpty(loadTestReport.getAdvancedConfiguration())) { + JSONObject advancedConfiguration = JSONObject.parseObject(loadTestReport.getAdvancedConfiguration()); engineContext.addProperties(advancedConfiguration); JSONArray domains = advancedConfiguration.getJSONArray("domains"); if (domains != null) { @@ -214,10 +218,10 @@ public class EngineFactory { return dns.toString().getBytes(StandardCharsets.UTF_8); } - private static byte[] getJMeterProperties(LoadTestWithBLOBs loadTest, EngineContext engineContext) { + private static byte[] getJMeterProperties(LoadTestReportWithBLOBs loadTestReportWithBLOBs, EngineContext engineContext) { StringBuilder props = new StringBuilder("# JMeter Properties\n"); - if (StringUtils.isNotEmpty(loadTest.getAdvancedConfiguration())) { - JSONObject advancedConfiguration = JSONObject.parseObject(loadTest.getAdvancedConfiguration()); + if (StringUtils.isNotEmpty(loadTestReportWithBLOBs.getAdvancedConfiguration())) { + JSONObject advancedConfiguration = JSONObject.parseObject(loadTestReportWithBLOBs.getAdvancedConfiguration()); engineContext.addProperties(advancedConfiguration); JSONArray properties = advancedConfiguration.getJSONArray("properties"); if (properties != null) { diff --git a/backend/src/main/java/io/metersphere/performance/engine/docker/DockerTestEngine.java b/backend/src/main/java/io/metersphere/performance/engine/docker/DockerTestEngine.java index e6c21108c7..6a3cf98e4d 100644 --- a/backend/src/main/java/io/metersphere/performance/engine/docker/DockerTestEngine.java +++ b/backend/src/main/java/io/metersphere/performance/engine/docker/DockerTestEngine.java @@ -1,6 +1,7 @@ package io.metersphere.performance.engine.docker; import com.alibaba.fastjson.JSON; +import io.metersphere.base.domain.LoadTestReportWithBLOBs; import io.metersphere.base.domain.LoadTestWithBLOBs; import io.metersphere.base.domain.TestResource; import io.metersphere.commons.constants.ResourceStatusEnum; @@ -26,13 +27,13 @@ public class DockerTestEngine extends AbstractEngine { private RestTemplate restTemplate; private RestTemplate restTemplateWithTimeOut; - public DockerTestEngine(LoadTestWithBLOBs loadTest) { - this.init(loadTest); + public DockerTestEngine(LoadTestReportWithBLOBs loadTestReport) { + this.init(loadTestReport); } @Override - protected void init(LoadTestWithBLOBs loadTest) { - super.init(loadTest); + protected void init(LoadTestReportWithBLOBs loadTestReport) { + super.init(loadTestReport); this.restTemplate = (RestTemplate) CommonBeanFactory.getBean("restTemplate"); this.restTemplateWithTimeOut = (RestTemplate) CommonBeanFactory.getBean("restTemplateWithTimeOut"); } @@ -85,9 +86,9 @@ public class DockerTestEngine extends AbstractEngine { env.put("RATIO", StringUtils.join(ratios, ",")); env.put("RESOURCE_INDEX", "" + resourceIndex); env.put("METERSPHERE_URL", metersphereUrl); - env.put("START_TIME", "" + this.getStartTime()); - env.put("TEST_ID", this.loadTest.getId()); - env.put("REPORT_ID", this.getReportId()); + env.put("START_TIME", "" + System.currentTimeMillis()); + env.put("TEST_ID", this.loadTestReport.getTestId()); + env.put("REPORT_ID", this.loadTestReport.getId()); env.put("BOOTSTRAP_SERVERS", kafkaProperties.getBootstrapServers()); env.put("LOG_TOPIC", kafkaProperties.getLog().getTopic()); env.put("JMETER_REPORTS_TOPIC", kafkaProperties.getReport().getTopic()); @@ -95,7 +96,7 @@ public class DockerTestEngine extends AbstractEngine { env.put("THREAD_NUM", "0");// 传入0表示不用修改线程数 env.put("HEAP", HEAP); env.put("GC_ALGO", GC_ALGO); - env.put("GRANULARITY", performanceTestService.getGranularity(this.getReportId()).toString()); + env.put("GRANULARITY", performanceTestService.getGranularity(this.loadTestReport.getId()).toString()); env.put("BACKEND_LISTENER", resourcePool.getBackendListener().toString()); @@ -121,7 +122,7 @@ public class DockerTestEngine extends AbstractEngine { @Override public void stop() { - String testId = loadTest.getId(); + String testId = loadTestReport.getTestId(); this.resourceList.forEach(r -> { NodeDTO node = JSON.parseObject(r.getConfiguration(), NodeDTO.class); String ip = node.getIp(); diff --git a/backend/src/main/java/io/metersphere/performance/service/JmeterFileService.java b/backend/src/main/java/io/metersphere/performance/service/JmeterFileService.java index 9d4092ff29..beb33b7600 100644 --- a/backend/src/main/java/io/metersphere/performance/service/JmeterFileService.java +++ b/backend/src/main/java/io/metersphere/performance/service/JmeterFileService.java @@ -1,18 +1,13 @@ package io.metersphere.performance.service; - import com.alibaba.excel.util.CollectionUtils; import io.metersphere.base.domain.LoadTestReportWithBLOBs; -import io.metersphere.base.domain.LoadTestWithBLOBs; -import io.metersphere.base.domain.TestPlanLoadCase; -import io.metersphere.base.mapper.LoadTestMapper; -import io.metersphere.base.mapper.TestPlanLoadCaseMapper; +import io.metersphere.base.mapper.LoadTestReportMapper; import io.metersphere.base.mapper.ext.ExtLoadTestReportMapper; import io.metersphere.commons.exception.MSException; import io.metersphere.commons.utils.LogUtil; import io.metersphere.performance.engine.EngineContext; import io.metersphere.performance.engine.EngineFactory; -import org.codehaus.plexus.util.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,40 +23,18 @@ import java.util.zip.ZipOutputStream; @Service @Transactional(rollbackFor = Exception.class) public class JmeterFileService { - - @Resource - private LoadTestMapper loadTestMapper; @Resource private ExtLoadTestReportMapper extLoadTestReportMapper; @Resource - private TestPlanLoadCaseMapper testPlanLoadCaseMapper; + private LoadTestReportMapper loadTestReportMapper; - public byte[] downloadZip(String testId, double[] ratios, String reportId, int resourceIndex) { + public byte[] downloadZip(String reportId, double[] ratios, int resourceIndex) { try { - LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(testId); - TestPlanLoadCase testPlanLoadCase = null; - if (loadTest == null) { - // 通过测试计划执行性能用例时,testId为测试计划性能用例表的主键ID,根据ID查询用例自身的压力配置 - testPlanLoadCase = testPlanLoadCaseMapper.selectByPrimaryKey(testId); - if (testPlanLoadCase != null) { - loadTest = loadTestMapper.selectByPrimaryKey(testPlanLoadCase.getLoadCaseId()); - if (loadTest != null) { - // 用例自身设置了资源池ID - if (StringUtils.isNotBlank(testPlanLoadCase.getTestResourcePoolId())) { - loadTest.setTestResourcePoolId(testPlanLoadCase.getTestResourcePoolId()); - } - // 用例自身设置了压力配置 - if (StringUtils.isNotBlank(testPlanLoadCase.getLoadConfiguration())) { - loadTest.setLoadConfiguration(testPlanLoadCase.getLoadConfiguration()); - } - } - } - } - EngineContext context = EngineFactory.createContext(loadTest, ratios, reportId, resourceIndex); - if (testPlanLoadCase != null) { - // ID - context.setTestId(testPlanLoadCase.getId()); + LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId); + if (loadTestReport == null) { + MSException.throwException("测试报告不存在或还没产生"); } + EngineContext context = EngineFactory.createContext(loadTestReport, ratios, reportId, resourceIndex); return zipFilesToByteArray(context); } catch (MSException e) { LogUtil.error(e.getMessage(), e); diff --git a/backend/src/main/java/io/metersphere/performance/service/PerformanceReportService.java b/backend/src/main/java/io/metersphere/performance/service/PerformanceReportService.java index 7c43e7e5ca..244f96f64b 100644 --- a/backend/src/main/java/io/metersphere/performance/service/PerformanceReportService.java +++ b/backend/src/main/java/io/metersphere/performance/service/PerformanceReportService.java @@ -97,14 +97,14 @@ public class PerformanceReportService { MSException.throwException("report id cannot be null"); } - LoadTestReport loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId); + LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId); LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(loadTestReport.getTestId()); LogUtil.info("Delete report started, report ID: %s" + reportId); if (loadTest != null) { try { - final Engine engine = EngineFactory.createEngine(loadTest); + final Engine engine = EngineFactory.createEngine(loadTestReport); if (engine == null) { MSException.throwException(String.format("Delete report fail. create engine fail,report ID:%s", reportId)); } diff --git a/backend/src/main/java/io/metersphere/performance/service/PerformanceTestService.java b/backend/src/main/java/io/metersphere/performance/service/PerformanceTestService.java index 5fa2dd22dc..be5b4256a1 100644 --- a/backend/src/main/java/io/metersphere/performance/service/PerformanceTestService.java +++ b/backend/src/main/java/io/metersphere/performance/service/PerformanceTestService.java @@ -313,6 +313,9 @@ public class PerformanceTestService { if (request.getUserId() != null) { loadTest.setUserId(request.getUserId()); } + if (StringUtils.isNotEmpty(request.getProjectId())) { + loadTest.setProjectId(request.getProjectId()); + } if (loadTest == null) { MSException.throwException(Translator.get("run_load_test_not_found") + request.getId()); } @@ -320,40 +323,12 @@ public class PerformanceTestService { if (StringUtils.equalsAny(loadTest.getStatus(), PerformanceTestStatus.Running.name(), PerformanceTestStatus.Starting.name())) { MSException.throwException(Translator.get("load_test_is_running")); } - String testResourcePoolId = loadTest.getTestResourcePoolId(); - TestResourcePool testResourcePool = testResourcePoolMapper.selectByPrimaryKey(testResourcePoolId); - if (testResourcePool == null) { - MSException.throwException(Translator.get("test_resource_pool_not_exists")); - } - if (ResourceStatusEnum.INVALID.name().equals(testResourcePool.getStatus())) { - MSException.throwException(Translator.get("test_resource_pool_invalid")); - } // check kafka checkKafka(); LogUtil.info("Load test started " + loadTest.getName()); - LoadTestWithBLOBs copyTest = new LoadTestWithBLOBs(); - BeanUtils.copyBean(copyTest, loadTest); - // 如果是执行测试计划用例,把EngineFactory.createEngine参数对象的id 设置为测试计划用例id - // 设置用例id目的是当 JmeterFileService 下载zip,拼装 jmx 文件时,如果用例自身带有压力配置,使用用例自身压力配置拼装 jmx - String testPlanLoadId = request.getTestPlanLoadId(); - if (StringUtils.isNotBlank(testPlanLoadId)) { - copyTest.setId(testPlanLoadId); - // 设置本次报告中的压力配置信息 - TestPlanLoadCase testPlanLoadCase = testPlanLoadCaseMapper.selectByPrimaryKey(testPlanLoadId); - if (testPlanLoadCase != null && StringUtils.isNotBlank(testPlanLoadCase.getLoadConfiguration())) { - loadTest.setLoadConfiguration(testPlanLoadCase.getLoadConfiguration()); - } - } - // engine type (NODE) - final Engine engine = EngineFactory.createEngine(copyTest); - if (engine == null) { - MSException.throwException(String.format("Test cannot be run,test ID:%s", request.getId())); - } - startEngine(loadTest, engine, request.getTriggerMode()); - - return engine.getReportId(); + return startEngine(loadTest, request); } private void checkKafka() { @@ -377,14 +352,16 @@ public class PerformanceTestService { } } - private void startEngine(LoadTestWithBLOBs loadTest, Engine engine, String triggerMode) { + private String startEngine(LoadTestWithBLOBs loadTest, RunTestPlanRequest request) { + + LoadTestReportWithBLOBs testReport = new LoadTestReportWithBLOBs(); - testReport.setId(engine.getReportId()); - testReport.setCreateTime(engine.getStartTime()); - testReport.setUpdateTime(engine.getStartTime()); + testReport.setId(UUID.randomUUID().toString()); + testReport.setCreateTime(System.currentTimeMillis()); + testReport.setUpdateTime(System.currentTimeMillis()); testReport.setTestId(loadTest.getId()); testReport.setName(loadTest.getName()); - testReport.setTriggerMode(triggerMode); + testReport.setTriggerMode(request.getTriggerMode()); if (SessionUtils.getUser() == null) { testReport.setUserId(loadTest.getUserId()); } else { @@ -394,18 +371,36 @@ public class PerformanceTestService { LoadTestWithBLOBs updateTest = new LoadTestWithBLOBs(); updateTest.setId(loadTest.getId()); // 启动测试 + Engine engine = null; try { - // 启动插入 report + // 保存测试里的配置 + testReport.setTestResourcePoolId(loadTest.getTestResourcePoolId()); testReport.setLoadConfiguration(loadTest.getLoadConfiguration()); + + String testPlanLoadId = request.getTestPlanLoadId(); + if (StringUtils.isNotBlank(testPlanLoadId)) { + // 设置本次报告中的压力配置信息 + TestPlanLoadCase testPlanLoadCase = testPlanLoadCaseMapper.selectByPrimaryKey(testPlanLoadId); + if (testPlanLoadCase != null && StringUtils.isNotBlank(testPlanLoadCase.getLoadConfiguration())) { + testReport.setLoadConfiguration(testPlanLoadCase.getLoadConfiguration()); + } + if (StringUtils.isNotBlank(testPlanLoadCase.getTestResourcePoolId())) { + testReport.setTestResourcePoolId(testPlanLoadCase.getTestResourcePoolId()); + } + } + // 启动插入 report testReport.setAdvancedConfiguration(loadTest.getAdvancedConfiguration()); testReport.setStatus(PerformanceTestStatus.Starting.name()); testReport.setProjectId(loadTest.getProjectId()); - testReport.setTestResourcePoolId(loadTest.getTestResourcePoolId()); testReport.setTestName(loadTest.getName()); loadTestReportMapper.insertSelective(testReport); - engine.start(); - // 启动正常修改状态 starting + // engine + engine = EngineFactory.createEngine(testReport); + if (engine == null) { + MSException.throwException(String.format("Test cannot be run,test ID:%s", loadTest.getId())); + } + updateTest.setStatus(PerformanceTestStatus.Starting.name()); loadTestMapper.updateByPrimaryKeySelective(updateTest); @@ -423,9 +418,14 @@ public class PerformanceTestService { reportResult.setReportKey(ReportKeys.ResultStatus.name()); reportResult.setReportValue("Ready"); // 初始化一个 result_status, 这个值用在data-streaming中 loadTestReportResultMapper.insertSelective(reportResult); + // 启动测试 + engine.start(); + return testReport.getId(); } catch (MSException e) { // 启动失败之后清理任务 - engine.stop(); + if (engine != null) { + engine.stop(); + } LogUtil.error(e.getMessage(), e); updateTest.setStatus(PerformanceTestStatus.Error.name()); updateTest.setDescription(e.getMessage()); @@ -482,12 +482,6 @@ public class PerformanceTestService { return results; } - public List selectByTestResourcePoolId(String resourcePoolId) { - LoadTestExample example = new LoadTestExample(); - example.createCriteria().andTestResourcePoolIdEqualTo(resourcePoolId); - return loadTestMapper.selectByExampleWithBLOBs(example); - } - public List dashboardTests(String workspaceId) { Instant oneYearAgo = Instant.now().plus(-365, ChronoUnit.DAYS); long startTimestamp = oneYearAgo.toEpochMilli(); @@ -572,9 +566,9 @@ public class PerformanceTestService { } private void stopEngine(String reportId) { - LoadTestReport loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId); + LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId); LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(loadTestReport.getTestId()); - final Engine engine = EngineFactory.createEngine(loadTest); + final Engine engine = EngineFactory.createEngine(loadTestReport); if (engine == null) { MSException.throwException(String.format("Stop report fail. create engine fail,report ID:%s", reportId)); } @@ -832,13 +826,12 @@ public class PerformanceTestService { Integer granularity = CommonBeanFactory.getBean(JmeterProperties.class).getReport().getGranularity(); try { LoadTestReportWithBLOBs report = loadTestReportMapper.selectByPrimaryKey(reportId); - LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(report.getTestId()); - JSONObject advancedConfig = JSON.parseObject(loadTest.getAdvancedConfiguration()); + JSONObject advancedConfig = JSON.parseObject(report.getAdvancedConfiguration()); if (advancedConfig.getInteger("granularity") != null) { return advancedConfig.getInteger("granularity") * 1000;// 单位是ms } AtomicReference maxDuration = new AtomicReference<>(0); - List> pressureConfigLists = JSON.parseObject(loadTest.getLoadConfiguration(), new TypeReference>>() { + List> pressureConfigLists = JSON.parseObject(report.getLoadConfiguration(), new TypeReference>>() { }); // 按照最长的执行时间来确定 pressureConfigLists.forEach(pcList -> { @@ -884,6 +877,7 @@ public class PerformanceTestService { /** * 用例自定义排序 + * * @param request */ public void updateOrder(ResetOrderRequest request) { @@ -893,4 +887,11 @@ public class PerformanceTestService { extLoadTestMapper::getLastOrder, loadTestMapper::updateByPrimaryKeySelective); } + + public List selectReportsByTestResourcePoolId(String resourcePoolId) { + LoadTestReportExample example = new LoadTestReportExample(); + example.createCriteria().andTestResourcePoolIdEqualTo(resourcePoolId) + .andStatusIn(Arrays.asList(PerformanceTestStatus.Running.name(), PerformanceTestStatus.Starting.name())); + return loadTestReportMapper.selectByExampleWithBLOBs(example); + } } diff --git a/backend/src/main/java/io/metersphere/reportstatistics/controller/HistoryReportController.java b/backend/src/main/java/io/metersphere/reportstatistics/controller/HistoryReportController.java new file mode 100644 index 0000000000..ce62705bdb --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/controller/HistoryReportController.java @@ -0,0 +1,56 @@ +package io.metersphere.reportstatistics.controller; + +import com.alibaba.fastjson.JSONArray; +import io.metersphere.base.domain.ReportStatistics; +import io.metersphere.base.domain.ReportStatisticsWithBLOBs; +import io.metersphere.commons.utils.LogUtil; +import io.metersphere.reportstatistics.dto.ReportStatisticsSaveRequest; +import io.metersphere.reportstatistics.service.ReportStatisticsService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author song.tianyang + * @Date 2021/9/14 2:58 下午 + */ +@RestController +@RequestMapping(value = "/history/report") +public class HistoryReportController { + + @Resource + private ReportStatisticsService reportStatisticsService; + + @PostMapping("/selectByParams") + public List selectByParams(@RequestBody ReportStatisticsSaveRequest request) { + List returnList = reportStatisticsService.selectByProjectIdAndReportType(request.getProjectId(),request.getReportType()); + LogUtil.info("报表查询结果:"+JSONArray.toJSONString(returnList)); + return returnList; + } + + @PostMapping("/saveReport") + public ReportStatisticsWithBLOBs saveReport(@RequestBody ReportStatisticsSaveRequest request){ + ReportStatisticsWithBLOBs returnData = reportStatisticsService.saveByRequest(request); + return returnData; + } + + @PostMapping("/updateReport") + public ReportStatisticsWithBLOBs updateReport(@RequestBody ReportStatisticsSaveRequest request){ + ReportStatisticsWithBLOBs returnData = reportStatisticsService.updateByRequest(request); + return returnData; + } + + @PostMapping("/deleteByParam") + public int deleteById(@RequestBody ReportStatisticsSaveRequest request) { + return reportStatisticsService.deleteById(request.getId()); + } + + @PostMapping("/selectById") + public ReportStatisticsWithBLOBs selectById(@RequestBody ReportStatisticsSaveRequest request) { + return reportStatisticsService.selectById(request.getId()); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/controller/TestAnalysisController.java b/backend/src/main/java/io/metersphere/reportstatistics/controller/TestAnalysisController.java new file mode 100644 index 0000000000..f7746b183b --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/controller/TestAnalysisController.java @@ -0,0 +1,24 @@ +package io.metersphere.reportstatistics.controller; + +import io.metersphere.reportstatistics.dto.TestAnalysisChartRequest; +import io.metersphere.reportstatistics.dto.TestAnalysisResult; +import io.metersphere.reportstatistics.service.TestAnalysisService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping(value = "/report/test/analysis") +public class TestAnalysisController { + + @Resource + TestAnalysisService testAnalysisService; + + @PostMapping("/getReport") + public TestAnalysisResult getReport(@RequestBody TestAnalysisChartRequest request) { + return testAnalysisService.getReport(request); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/controller/TestCaseCountController.java b/backend/src/main/java/io/metersphere/reportstatistics/controller/TestCaseCountController.java new file mode 100644 index 0000000000..d42d0d6b15 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/controller/TestCaseCountController.java @@ -0,0 +1,34 @@ +package io.metersphere.reportstatistics.controller; + +import io.metersphere.reportstatistics.dto.TestCaseCountRequest; +import io.metersphere.reportstatistics.dto.TestCaseCountResponse; +import io.metersphere.reportstatistics.service.TestCaseCountService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping(value = "/report/test/case/count") +public class TestCaseCountController { + + @Resource + TestCaseCountService testCaseCountService; + + @PostMapping("/initDatas") + public Map>> initDatas(@RequestBody TestCaseCountRequest request) { + Map>> returnMap = testCaseCountService.getSelectFilterDatas(request.getProjectId()); + + return returnMap; + } + + @PostMapping("/getReport") + public TestCaseCountResponse getReport(@RequestBody TestCaseCountRequest request) { + TestCaseCountResponse response = testCaseCountService.getReport(request); + return response; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/PieChartDTO.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/PieChartDTO.java new file mode 100644 index 0000000000..c5ed790f2c --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/PieChartDTO.java @@ -0,0 +1,27 @@ +package io.metersphere.reportstatistics.dto; + +import com.alibaba.fastjson.JSONObject; +import io.metersphere.reportstatistics.dto.charts.Series; +import io.metersphere.reportstatistics.dto.charts.Title; +import io.metersphere.reportstatistics.dto.charts.XAxis; +import io.metersphere.reportstatistics.dto.charts.YAxis; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class PieChartDTO { + private JSONObject dataset; + private JSONObject tooltip; + private XAxis xAxis; + private YAxis yAxis; + private List series; + private List title; + private int width; + + public PieChartDTO() { + tooltip = new JSONObject(); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsSaveRequest.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsSaveRequest.java new file mode 100644 index 0000000000..a206aa2fdb --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsSaveRequest.java @@ -0,0 +1,14 @@ +package io.metersphere.reportstatistics.dto; + +import io.metersphere.base.domain.ReportStatisticsWithBLOBs; +import lombok.Getter; +import lombok.Setter; + +/** + * @author song.tianyang + * @Date 2021/9/14 4:51 下午 + */ +@Getter +@Setter +public class ReportStatisticsSaveRequest extends ReportStatisticsWithBLOBs { +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsType.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsType.java new file mode 100644 index 0000000000..34dfbd8c9e --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/ReportStatisticsType.java @@ -0,0 +1,5 @@ +package io.metersphere.reportstatistics.dto; + +public enum ReportStatisticsType { + TEST_CASE_COUNT,TEST_CASE_ANALYSIS +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartDTO.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartDTO.java new file mode 100644 index 0000000000..8e10195daa --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartDTO.java @@ -0,0 +1,29 @@ +package io.metersphere.reportstatistics.dto; + +import io.metersphere.reportstatistics.dto.charts.Legend; +import io.metersphere.reportstatistics.dto.charts.Series; +import io.metersphere.reportstatistics.dto.charts.XAxis; +import io.metersphere.reportstatistics.dto.charts.YAxis; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestAnalysisChartDTO { + private Legend legend; + private XAxis xAxis; + private YAxis yAxis; + private List<Series> series; + + public TestAnalysisChartDTO() { + } + + public TestAnalysisChartDTO(Legend legend, XAxis xAxis, YAxis yAxis, List<Series> series) { + this.legend = legend; + this.xAxis = xAxis; + this.yAxis = yAxis; + this.series = series; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartRequest.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartRequest.java new file mode 100644 index 0000000000..3e3c84b9ce --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartRequest.java @@ -0,0 +1,21 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestAnalysisChartRequest { + private boolean createCase; + private boolean updateCase; + private String order; + private List<Long> times; + private String startTime; + private String endTime; + private List<String> prioritys; + private List<String> projects; + private List<String> modules; + private List<String> users; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartResult.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartResult.java new file mode 100644 index 0000000000..34bd70c583 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisChartResult.java @@ -0,0 +1,12 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TestAnalysisChartResult { + private String dateStr; + private String countNum; + +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisResult.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisResult.java new file mode 100644 index 0000000000..e6e682ad79 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisResult.java @@ -0,0 +1,22 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestAnalysisResult { + private TestAnalysisChartDTO chartDTO; + private List<TestAnalysisTableDTO> tableDTOs; + + public TestAnalysisResult() { + + } + + public TestAnalysisResult(TestAnalysisChartDTO chartDTO, List<TestAnalysisTableDTO> tableDTOs) { + this.chartDTO = chartDTO; + this.tableDTOs = tableDTOs; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisTableDTO.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisTableDTO.java new file mode 100644 index 0000000000..7f7a62d273 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestAnalysisTableDTO.java @@ -0,0 +1,29 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +public class TestAnalysisTableDTO { + private String id; + private String name; + private String createCount; + private String updateCount; + private List<TestAnalysisTableDTO> children; + + public TestAnalysisTableDTO() { + + } + + public TestAnalysisTableDTO(String name, String createCount, String updateCount, List<TestAnalysisTableDTO> children) { + this.id = UUID.randomUUID().toString(); + this.name = name; + this.createCount = createCount; + this.updateCount = updateCount; + this.children = children; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountChartResult.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountChartResult.java new file mode 100644 index 0000000000..4c023d1428 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountChartResult.java @@ -0,0 +1,15 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TestCaseCountChartResult { + private String groupName; + private long countNum; + + public String getCountNumStr(){ + return String.valueOf(countNum); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountRequest.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountRequest.java new file mode 100644 index 0000000000..227e2c0c1d --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountRequest.java @@ -0,0 +1,79 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class TestCaseCountRequest { + //x轴字段 + private String xaxis; + //y轴字段 + private List<String> yaxis; + + //搜索条件 + private String projectId; + private String timeType; + private TimeFilter timeFilter; + private List<Long> times; + private String order; + + //起始时间 + private long startTime = 0; + //结束时间 + private long endTime = 0; + + //其余条件 + private String filterType; + private List<Map<String,Object>> filters; + + /** + * 功能用例、接口用例、场景用例、性能用例的分组字段 + */ + private String testCaseGroupColumn; + private String apiCaseGroupColumn; + private String scenarioCaseGroupColumn; + private String loadCaseGroupColumn; + + /** + * filter整理后的查询数据 + * @return + */ + private Map<String,List<String>> filterSearchList; + private Map<String,List<String>> apiFilterSearchList; + private Map<String,List<String>> loadFilterSearchList; + + public int getTimeRange(){ + if(timeFilter != null){ + return timeFilter.getTimeRange(); + }else { + return 0; + } + } + + public String getTimeRangeUnit(){ + if(timeFilter != null){ + return timeFilter.getTimeRangeUnit(); + }else { + return null; + } + } + + public void setFilterSearchList(String key,List<String> values){ + if(this.filterSearchList == null){ + this.filterSearchList = new HashMap<>(); + } + filterSearchList.put(key,values); + } +} + +@Getter +@Setter +class TimeFilter{ + private int timeRange; + private String timeRangeUnit; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountResponse.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountResponse.java new file mode 100644 index 0000000000..70bfeaccab --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountResponse.java @@ -0,0 +1,24 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class TestCaseCountResponse { + private TestAnalysisChartDTO barChartDTO; + private PieChartDTO pieChartDTO; + private List<TestCaseCountTableDTO> tableDTOs; + + public TestCaseCountResponse() { + + } + + public TestCaseCountResponse(TestAnalysisChartDTO chartDTO, PieChartDTO pieChartDTO, List<TestCaseCountTableDTO> tableDTOs) { + this.pieChartDTO = pieChartDTO; + this.barChartDTO = chartDTO; + this.tableDTOs = tableDTOs; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountSummary.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountSummary.java new file mode 100644 index 0000000000..29f6042e28 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountSummary.java @@ -0,0 +1,22 @@ +package io.metersphere.reportstatistics.dto; + +/** + * @author song.tianyang + * @Date 2021/9/8 5:36 下午 + */ +public class TestCaseCountSummary { + public String groupName; + + public long testCaseCount = 0; + public long apiCaseCount = 0; + public long scenarioCaseCount = 0; + public long loadCaseCount = 0; + + public TestCaseCountSummary(String groupName) { + this.groupName = groupName; + } + + public long getAllCount() { + return this.testCaseCount + this.apiCaseCount + this.scenarioCaseCount + this.loadCaseCount; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountTableDTO.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountTableDTO.java new file mode 100644 index 0000000000..2ce5c86a2b --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/TestCaseCountTableDTO.java @@ -0,0 +1,34 @@ +package io.metersphere.reportstatistics.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +public class TestCaseCountTableDTO { + private String id; + private String name; + private String allCount; + private String testCaseCount; + private String apiCaseCount; + private String scenarioCaseCount; + private String loadCaseCount; + + private List<TestCaseCountTableDTO> children; + + public TestCaseCountTableDTO(String name, long testCaseCount, long apiCaseCount, long scenarioCaseCount, long loadCaseCount) { + this.id = UUID.randomUUID().toString(); + this.name = name; + this.testCaseCount = String.valueOf(testCaseCount); + this.apiCaseCount = String.valueOf(apiCaseCount); + this.scenarioCaseCount = String.valueOf(scenarioCaseCount); + this.loadCaseCount = String.valueOf(loadCaseCount); + this.allCount = String.valueOf(testCaseCount+apiCaseCount+scenarioCaseCount+loadCaseCount); + + children = new ArrayList<>(); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Legend.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Legend.java new file mode 100644 index 0000000000..5ac9f6545e --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Legend.java @@ -0,0 +1,20 @@ +package io.metersphere.reportstatistics.dto.charts; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class Legend { + private final String x = "center"; + private final String y = "bottom"; + private final String type = "scroll"; + private final List<Integer> padding = Arrays.asList(0, 40, 0, 0); + private Map<String, Boolean> selected; + private List<String> data; + +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/PieData.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/PieData.java new file mode 100644 index 0000000000..3222b624e5 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/PieData.java @@ -0,0 +1,20 @@ +package io.metersphere.reportstatistics.dto.charts; + +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PieData { + private String name; + private long value; + private JSONObject itemStyle; + + public void setColor(String color){ + if(itemStyle == null){ + itemStyle = new JSONObject(); + } + itemStyle.put("color",color); + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Series.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Series.java new file mode 100644 index 0000000000..823e066a05 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Series.java @@ -0,0 +1,20 @@ +package io.metersphere.reportstatistics.dto.charts; + +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class Series { + private String name; + private List<Object> data; + private String color = "#783887"; + private String type = "line"; + private String radius = "50"; + private String stack; + private JSONObject encode; + private List<String> center; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Title.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Title.java new file mode 100644 index 0000000000..f79d210710 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/Title.java @@ -0,0 +1,14 @@ +package io.metersphere.reportstatistics.dto.charts; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Title { + private String name; + private String subtext; + private String left; + private String top = "75%"; + private String textAlign = "center"; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/XAxis.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/XAxis.java new file mode 100644 index 0000000000..543a3488ac --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/XAxis.java @@ -0,0 +1,17 @@ +package io.metersphere.reportstatistics.dto.charts; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class XAxis { + private final String type = "category"; + private List<String> data; + private String name; + private Map<String,Integer> axisLabel = new HashMap<String,Integer>(){ {this.put("interval",0);this.put("rotate",30);}}; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/YAxis.java b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/YAxis.java new file mode 100644 index 0000000000..30b70d9b31 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/dto/charts/YAxis.java @@ -0,0 +1,14 @@ +package io.metersphere.reportstatistics.dto.charts; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class YAxis { + private String type; + private List<String> data; + private String name; +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/service/ReportStatisticsService.java b/backend/src/main/java/io/metersphere/reportstatistics/service/ReportStatisticsService.java new file mode 100644 index 0000000000..f2e1e71c38 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/service/ReportStatisticsService.java @@ -0,0 +1,77 @@ +package io.metersphere.reportstatistics.service; + +import io.metersphere.base.domain.ReportStatistics; +import io.metersphere.base.domain.ReportStatisticsExample; +import io.metersphere.base.domain.ReportStatisticsWithBLOBs; +import io.metersphere.base.mapper.ReportStatisticsMapper; +import io.metersphere.commons.utils.SessionUtils; +import io.metersphere.reportstatistics.dto.ReportStatisticsSaveRequest; +import io.metersphere.reportstatistics.dto.ReportStatisticsType; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.UUID; + +/** + * @author song.tianyang + * @Date 2021/9/14 4:50 下午 + */ +@Service +@Transactional(rollbackFor = Exception.class) +public class ReportStatisticsService { + @Resource + private ReportStatisticsMapper reportStatisticsMapper; + + public ReportStatisticsWithBLOBs saveByRequest(ReportStatisticsSaveRequest request) { + ReportStatisticsWithBLOBs model = new ReportStatisticsWithBLOBs(); + model.setId(UUID.randomUUID().toString()); + String name = "用例分析报表"; + if(StringUtils.equalsIgnoreCase(ReportStatisticsType.TEST_CASE_COUNT.name(),request.getReportType())){ + name = "用例统计报表"; + model.setReportType(ReportStatisticsType.TEST_CASE_COUNT.name()); + }else { + model.setReportType(ReportStatisticsType.TEST_CASE_ANALYSIS.name()); + } + model.setName(name); + model.setDataOption(request.getDataOption()); + model.setSelectOption(request.getSelectOption()); + model.setCreateTime(System.currentTimeMillis()); + model.setUpdateTime(System.currentTimeMillis()); + model.setProjectId(request.getProjectId()); + String userId = SessionUtils.getUserId(); + model.setCreateUser(userId); + model.setUpdateUser(userId); + + reportStatisticsMapper.insert(model); + + return model; + } + + public int deleteById(String id) { + return reportStatisticsMapper.deleteByPrimaryKey(id); + } + + public List<ReportStatistics> selectByProjectIdAndReportType(String projectId, String reportType) { + ReportStatisticsExample example = new ReportStatisticsExample(); + example.createCriteria().andProjectIdEqualTo(projectId).andReportTypeEqualTo(reportType); + example.setOrderByClause("create_time DESC"); + return reportStatisticsMapper.selectByExample(example); + } + + public ReportStatisticsWithBLOBs selectById(String id) { + return reportStatisticsMapper.selectByPrimaryKey(id); + } + + public ReportStatisticsWithBLOBs updateByRequest(ReportStatisticsSaveRequest request) { + ReportStatisticsWithBLOBs updateModel = new ReportStatisticsWithBLOBs(); + updateModel.setId(request.getId()); + updateModel.setName(request.getName()); + updateModel.setUpdateTime(request.getUpdateTime()); + updateModel.setUpdateUser(SessionUtils.getUserId()); + reportStatisticsMapper.updateByPrimaryKeySelective(updateModel); + return updateModel; + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/service/TestAnalysisService.java b/backend/src/main/java/io/metersphere/reportstatistics/service/TestAnalysisService.java new file mode 100644 index 0000000000..aebd939897 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/service/TestAnalysisService.java @@ -0,0 +1,171 @@ +package io.metersphere.reportstatistics.service; + +import io.metersphere.base.mapper.ext.ExtTestAnalysisMapper; +import io.metersphere.commons.utils.DateUtils; +import io.metersphere.commons.utils.SessionUtils; +import io.metersphere.controller.request.ProjectRequest; +import io.metersphere.dto.ProjectDTO; +import io.metersphere.reportstatistics.dto.*; +import io.metersphere.reportstatistics.dto.charts.Legend; +import io.metersphere.reportstatistics.dto.charts.Series; +import io.metersphere.reportstatistics.dto.charts.XAxis; +import io.metersphere.reportstatistics.dto.charts.YAxis; +import io.metersphere.service.ProjectService; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Transactional(rollbackFor = Exception.class) +public class TestAnalysisService { + @Resource + private ExtTestAnalysisMapper extTestAnalysisMapper; + @Resource + private ProjectService projectService; + + private final String ADD = "新增用例"; + private final String UPDATE = "修改用例"; + + public TestAnalysisResult getReport(TestAnalysisChartRequest request) { + if (CollectionUtils.isEmpty(request.getTimes())) { + // 最近七天 + request.setTimes(Arrays.asList(System.currentTimeMillis() - 7 * 24 * 3600 * 1000L, System.currentTimeMillis())); + } + request.setStartTime(DateUtils.getDataStr(request.getTimes().get(0))); + request.setEndTime(DateUtils.getDataStr(request.getTimes().get(1))); + if (CollectionUtils.isEmpty(request.getProjects())) { + // 获取当前组织空间下所有项目 + String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId(); + ProjectRequest projectRequest = new ProjectRequest(); + projectRequest.setWorkspaceId(currentWorkspaceId); + List<ProjectDTO> projectDTOS = projectService.getProjectList(projectRequest); + if (CollectionUtils.isNotEmpty(projectDTOS)) { + request.setProjects(projectDTOS.stream().map(ProjectDTO::getId).collect(Collectors.toList())); + } else { + request.setProjects(new LinkedList<String>(){{this.add(UUID.randomUUID().toString());}}); + } + } + TestAnalysisChartDTO dto = new TestAnalysisChartDTO(); + List<TestAnalysisTableDTO> dtos = new LinkedList<>(); + + List<Series> seriesList = new LinkedList<>(); + XAxis xAxis = new XAxis(); + + if (CollectionUtils.isEmpty(request.getUsers())) { + // 组织charts格式数据 + Legend legend = new Legend(); + formatLegend(legend, null, request); + dto.setLegend(legend); + List<TestAnalysisChartResult> createResults = extTestAnalysisMapper.getCraeteCaseReport(request); + // 获取修改的用例统计报表 + List<TestAnalysisChartResult> updateResults = extTestAnalysisMapper.getUpdateCaseReport(request); + formatXaxisSeries(xAxis, seriesList, "", dto, createResults, updateResults); + formatTable(dtos, createResults, updateResults); + } else { + List<String> users = request.getUsers(); + Legend legend = new Legend(); + formatLegend(legend, users, request); + dto.setLegend(legend); + + // 按用户展示 + boolean isFlag = true; + for (String item : users) { + request.setUsers(Arrays.asList(item)); + List<TestAnalysisChartResult> createResults = extTestAnalysisMapper.getCraeteCaseReport(request); + // 获取修改的用例统计报表 + List<TestAnalysisChartResult> updateResults = extTestAnalysisMapper.getUpdateCaseReport(request); + formatXaxisSeries(xAxis, seriesList, item + "-", dto, createResults, updateResults); + + // 初始化列表总量,按天统计总量 + if (isFlag) { + formatTable(dtos, createResults, updateResults); + isFlag = false; + } + // 增加子项 + for (int j = 0; j < dtos.size(); j++) { + TestAnalysisTableDTO childItem = new TestAnalysisTableDTO(item, createResults.get(j).getCountNum(), updateResults.get(j).getCountNum(), null); + dtos.get(j).getChildren().add(childItem); + } + } + } + // 每行总计 + dtos.forEach(item -> { + if (CollectionUtils.isNotEmpty(item.getChildren())) { + // table 总和计算 + List<Integer> collect = item.getChildren().stream().map(childItem -> Integer.valueOf(childItem.getCreateCount())).collect(Collectors.toList()); + // reduce求和 + Optional<Integer> createCount = collect.stream().reduce(Integer::sum); + List<Integer> upCollect = item.getChildren().stream().map(childItem -> Integer.valueOf(childItem.getUpdateCount())).collect(Collectors.toList()); + // reduce求和 + Optional<Integer> updateCount = upCollect.stream().reduce(Integer::sum); + item.setCreateCount(createCount.get().toString()); + item.setUpdateCount(updateCount.get().toString()); + } + }); + // table 总和计算 + List<Integer> collect = dtos.stream().map(item -> Integer.valueOf(item.getCreateCount())).collect(Collectors.toList()); + // reduce求和 + Optional<Integer> createCount = collect.stream().reduce(Integer::sum); + List<Integer> upCollect = dtos.stream().map(item -> Integer.valueOf(item.getUpdateCount())).collect(Collectors.toList()); + // reduce求和 + Optional<Integer> updateCount = upCollect.stream().reduce(Integer::sum); + dtos.add(new TestAnalysisTableDTO("总计", createCount.get().toString(), updateCount.get().toString(), new LinkedList<>())); + + TestAnalysisResult testAnalysisResult = new TestAnalysisResult(); + testAnalysisResult.setChartDTO(dto); + testAnalysisResult.setTableDTOs(dtos); + return testAnalysisResult; + } + + private void formatXaxisSeries(XAxis xAxis, List<Series> seriesList, String name, TestAnalysisChartDTO dto, List<TestAnalysisChartResult> createResults, List<TestAnalysisChartResult> updateResults) { + if (CollectionUtils.isNotEmpty(createResults)) { + xAxis.setData(createResults.stream().map(TestAnalysisChartResult::getDateStr).collect(Collectors.toList())); + Series series = new Series(); + series.setName(name + ADD); + series.setData(createResults.stream().map(TestAnalysisChartResult::getCountNum).collect(Collectors.toList())); + seriesList.add(series); + } + if (CollectionUtils.isNotEmpty(updateResults)) { + xAxis.setData(updateResults.stream().map(TestAnalysisChartResult::getDateStr).collect(Collectors.toList())); + Series series = new Series(); + series.setName(name + UPDATE); + series.setColor("#B8741A"); + series.setData(updateResults.stream().map(TestAnalysisChartResult::getCountNum).collect(Collectors.toList())); + seriesList.add(series); + } + dto.setXAxis(xAxis); + dto.setYAxis(new YAxis()); + dto.setSeries(seriesList); + } + + private void formatLegend(Legend legend, List<String> datas, TestAnalysisChartRequest request) { + Map<String, Boolean> selected = new LinkedHashMap<>(); + List<String> list = new LinkedList<>(); + if (CollectionUtils.isEmpty(datas)) { + selected.put(ADD, request.isCreateCase()); + selected.put(UPDATE, request.isUpdateCase()); + list.add(ADD); + list.add(UPDATE); + } else { + datas.forEach(item -> { + selected.put(item + "-" + ADD, request.isCreateCase()); + selected.put(item + "-" + UPDATE, request.isUpdateCase()); + list.add(item + "-" + ADD); + list.add(item + "-" + UPDATE); + }); + } + legend.setSelected(selected); + legend.setData(list); + } + + private void formatTable(List<TestAnalysisTableDTO> dtos, List<TestAnalysisChartResult> createResults, List<TestAnalysisChartResult> updateResults) { + for (int i = 0; i < createResults.size(); i++) { + TestAnalysisTableDTO dto = new TestAnalysisTableDTO(createResults.get(i).getDateStr(), createResults.get(i).getCountNum(), updateResults.get(i).getCountNum(), new LinkedList<>()); + dtos.add(dto); + } + } +} diff --git a/backend/src/main/java/io/metersphere/reportstatistics/service/TestCaseCountService.java b/backend/src/main/java/io/metersphere/reportstatistics/service/TestCaseCountService.java new file mode 100644 index 0000000000..1f438af677 --- /dev/null +++ b/backend/src/main/java/io/metersphere/reportstatistics/service/TestCaseCountService.java @@ -0,0 +1,775 @@ +package io.metersphere.reportstatistics.service; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import io.metersphere.base.domain.CustomField; +import io.metersphere.base.domain.User; +import io.metersphere.base.mapper.ext.ExtTestCaseCountMapper; +import io.metersphere.commons.utils.CommonBeanFactory; +import io.metersphere.commons.utils.DateUtils; +import io.metersphere.controller.request.member.QueryMemberRequest; +import io.metersphere.dto.TestCaseTemplateDao; +import io.metersphere.i18n.Translator; +import io.metersphere.reportstatistics.dto.*; +import io.metersphere.reportstatistics.dto.charts.*; +import io.metersphere.service.TestCaseTemplateService; +import io.metersphere.service.UserService; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; + +@Service +@Transactional(rollbackFor = Exception.class) +public class TestCaseCountService { + @Resource + private ExtTestCaseCountMapper extTestCaseCountMapper; + @Resource + UserService userService; + + public TestCaseCountResponse getReport(TestCaseCountRequest request) { + request.setFilterType(request.getFilterType().toUpperCase(Locale.ROOT)); + + TestAnalysisChartDTO dto = new TestAnalysisChartDTO(); + PieChartDTO pieChartDTO = new PieChartDTO(); + List<TestCaseCountTableDTO> dtos = new LinkedList<>(); + + List<Series> seriesList = new LinkedList<>(); + XAxis xAxis = new XAxis(); + xAxis.setAxisLabel(new HashMap<String,Integer>(){ {this.put("interval",0);this.put("rotate",0);}}); + + // 组织charts格式数据 + Legend legend = new Legend(); + formatLegend(legend, request.getYaxis(), request); + dto.setLegend(legend); + //根据X轴(分组计算字段)来整理不同表对应的字段 注意:x轴维护人/查询条件有维护人时 不查接口和性能; x轴为用例等级/查询条件有用例等级的,不查性能 + + boolean yAxisSelectTestCase = false; + boolean yAxisSelectApi = false; + boolean yAxisSelectScenarioCase = false; + boolean yAxisSelectLoad = false; + + boolean selectApi = true; + boolean selectLoad = true; + + boolean parseUser = false; + boolean parseStatus = false; + + switch (request.getXaxis()) { + case "creator": + request.setTestCaseGroupColumn("create_user"); + request.setApiCaseGroupColumn("create_user_id"); + request.setScenarioCaseGroupColumn("create_user"); + request.setLoadCaseGroupColumn("create_user"); + parseUser = true; + break; + case "maintainer": + request.setTestCaseGroupColumn("maintainer"); + request.setApiCaseGroupColumn("'无维护人'"); + request.setScenarioCaseGroupColumn("principal"); + request.setLoadCaseGroupColumn("'无维护人'"); + selectApi = false; + selectLoad = false; + parseUser = true; + break; + case "casetype": + Map<String, String> caseDescMap = this.getCaseDescMap(); + request.setTestCaseGroupColumn("'" + caseDescMap.get("testCaseDesc") + "'"); + request.setApiCaseGroupColumn("'" + caseDescMap.get("apiCaseDesc") + "'"); + request.setScenarioCaseGroupColumn("'" + caseDescMap.get("scenarioCaseDesc") + "'"); + request.setLoadCaseGroupColumn("'" + caseDescMap.get("loadCaseDesc") + "'"); + break; + case "casestatus": + request.setTestCaseGroupColumn("status"); + request.setApiCaseGroupColumn("status"); + request.setScenarioCaseGroupColumn("status"); + request.setLoadCaseGroupColumn("status"); + selectApi = false; + parseStatus = true; + break; + case "caselevel": + request.setTestCaseGroupColumn("priority"); + request.setApiCaseGroupColumn("priority"); + request.setScenarioCaseGroupColumn("level"); + request.setLoadCaseGroupColumn("'无用例等级'"); + selectLoad = false; + break; + default: + return new TestCaseCountResponse(); + } + + //计算时间 + if (StringUtils.equalsIgnoreCase(request.getTimeType(), "dynamicTime")) { + int dateCountType = 0; + if (StringUtils.equalsIgnoreCase(request.getTimeRangeUnit(), "day")) { + dateCountType = Calendar.DAY_OF_MONTH; + } else if (StringUtils.equalsIgnoreCase(request.getTimeRangeUnit(), "month")) { + dateCountType = Calendar.MONTH; + } else if (StringUtils.equalsIgnoreCase(request.getTimeRangeUnit(), "year")) { + dateCountType = Calendar.YEAR; + } + + if (dateCountType != 0 && request.getTimeRange() != 0) { + long startTime = DateUtils.dateSum(new Date(), (0 - request.getTimeRange()), dateCountType).getTime(); + request.setStartTime(startTime); + } + + } else if (StringUtils.equalsIgnoreCase(request.getTimeType(), "fixedTime")) { + if (CollectionUtils.isNotEmpty(request.getTimes()) && request.getTimes().size() == 2) { + request.setStartTime(request.getTimes().get(0)); + request.setEndTime(request.getTimes().get(1)); + } + } + + //计算更多属性 + if (CollectionUtils.isNotEmpty(request.getFilters())) { + for (Map<String, Object> filterMap : request.getFilters()) { + String filterType = String.valueOf(filterMap.get("type")); + + if (StringUtils.equalsAnyIgnoreCase(filterType, "casetype", "caselevel", "creator", "maintainer")) { + Object valueObj = filterMap.get("values"); + if (valueObj instanceof List) { + List<String> searchList = (List) valueObj; + if(!searchList.isEmpty()){ + request.setFilterSearchList(filterType, searchList); + } + } + + if (StringUtils.equalsIgnoreCase(filterType, "caselevel")) { + selectLoad = false; + }else if (StringUtils.equalsIgnoreCase(filterType, "maintainer")) { + selectApi = false; + selectLoad = false; + } + }else if(StringUtils.equalsAnyIgnoreCase(filterType, "casestatus")){ + List<String> searchList = new ArrayList<>(); + Object valueObj = filterMap.get("values"); + if (valueObj instanceof List) { + for (String statusStr : (List<String>) valueObj) { + searchList.add(statusStr.toUpperCase(Locale.ROOT)); + } + } + //如果包含Running + if(searchList.contains("RUNNING")){ + if(!searchList.contains("Starting")){ + searchList.add("STARTING"); + } + if(!searchList.contains("Underway")){ + searchList.add("UNDERWAY"); + } + } + + if(searchList.contains("FINISHED")){ + if(!searchList.contains("Completed")){ + searchList.add("Completed"); + } + } + + if(!searchList.isEmpty()){ + request.setFilterSearchList(filterType, searchList); + } + selectApi = false; + } + } + } + + //获取测试用例、接口用例、场景用例、性能用例的统计 + List<TestCaseCountChartResult> functionCaseCountResult = new ArrayList<>(); + List<TestCaseCountChartResult> apiCaseCountResult = new ArrayList<>(); + List<TestCaseCountChartResult> scenarioCaseCount = new ArrayList<>(); + List<TestCaseCountChartResult> loadCaseCount = new ArrayList<>(); + + List<String> moreOptionsAboutCaseType = new ArrayList<>(); + if (StringUtils.equalsIgnoreCase(request.getFilterType(), "And") && MapUtils.isNotEmpty(request.getFilterSearchList())) { + if (request.getFilterSearchList().containsKey("maintainer")) { + selectApi = false; + } + if (request.getFilterSearchList().containsKey("caselevel")) { + selectLoad = false; + } + + if (request.getFilterSearchList().containsKey("casetype")) { + //如果"且"查询,同时针对案例类型做过筛选,那么则分开批量查询 + List<String> selectCaseTypeList = request.getFilterSearchList().get("casetype"); + request.getFilterSearchList().remove("casetype"); + if (CollectionUtils.isNotEmpty(selectCaseTypeList)) { + moreOptionsAboutCaseType.addAll(selectCaseTypeList); + } + } + } + //没有选择的话默认搜索条件是所有类型的案例 + if(moreOptionsAboutCaseType.isEmpty()){ + moreOptionsAboutCaseType.add("testCase"); + moreOptionsAboutCaseType.add("apiCase"); + moreOptionsAboutCaseType.add("scenarioCase"); + moreOptionsAboutCaseType.add("loadCase"); + } + + //解析Y轴,判断要查询的案例类型 + if(CollectionUtils.isNotEmpty(request.getYaxis())){ + + for (String selectType:request.getYaxis()) { + if(moreOptionsAboutCaseType.contains(selectType)){ + if (StringUtils.equalsIgnoreCase(selectType, "testCase")) { + yAxisSelectTestCase = true; + } else if (StringUtils.equalsIgnoreCase(selectType, "apiCase")) { + if(selectApi){ + yAxisSelectApi = true; + } + } else if (StringUtils.equalsIgnoreCase(selectType, "scenarioCase")) { + yAxisSelectScenarioCase = true; + } else if (StringUtils.equalsIgnoreCase(selectType, "loadCase")) { + if(selectLoad){ + yAxisSelectLoad = true; + } + } + } + } + } + + + if(yAxisSelectTestCase){ + functionCaseCountResult = extTestCaseCountMapper.getFunctionCaseCount(request); + } + if (yAxisSelectApi) { + Map<String,List<String>> apiCaseFilterList = new HashMap<>(); + if(MapUtils.isNotEmpty(request.getFilterSearchList())){ + for (Map.Entry<String,List<String>> entry : request.getFilterSearchList().entrySet()) { + String type = entry.getKey(); + if(!StringUtils.equalsAnyIgnoreCase(type,"maintainer","casestatus")){ + apiCaseFilterList.put(entry.getKey(),entry.getValue()); + } + } + } + request.setApiFilterSearchList(apiCaseFilterList); + apiCaseCountResult = extTestCaseCountMapper.getApiCaseCount(request); + } + if(yAxisSelectScenarioCase){ + scenarioCaseCount = extTestCaseCountMapper.getScenarioCaseCount(request); + } + if (yAxisSelectLoad) { + Map<String,List<String>> loadCaseFilterMap = new HashMap<>(); + if(MapUtils.isNotEmpty(request.getFilterSearchList())){ + for (Map.Entry<String,List<String>> entry : request.getFilterSearchList().entrySet()) { + String type = entry.getKey(); + if(!StringUtils.equalsAnyIgnoreCase(type,"maintainer","caselevel")){ + loadCaseFilterMap.put(entry.getKey(),entry.getValue()); + } + } + } + request.setLoadFilterSearchList(loadCaseFilterMap); + loadCaseCount = extTestCaseCountMapper.getLoadCaseCount(request); + } + + Map<String, TestCaseCountSummary> summaryMap = this.summaryCountResult(parseUser, parseStatus,request.getProjectId(),request.getOrder(), + functionCaseCountResult, apiCaseCountResult, scenarioCaseCount, loadCaseCount); + + formatXaxisSeries(xAxis, seriesList, dto, summaryMap); + formatTable(dtos, summaryMap); + + formatPieChart(pieChartDTO, request.getXaxis(), summaryMap,yAxisSelectTestCase,yAxisSelectApi,yAxisSelectScenarioCase,yAxisSelectLoad); + + TestCaseCountResponse testCaseCountResult = new TestCaseCountResponse(); + testCaseCountResult.setBarChartDTO(dto); + testCaseCountResult.setTableDTOs(dtos); + testCaseCountResult.setPieChartDTO(pieChartDTO); + return testCaseCountResult; + } + + private void formatPieChart(PieChartDTO pieChartDTO, String groupName, Map<String, TestCaseCountSummary> summaryMap, + boolean selectTestCase, boolean selectApi, boolean selectScenarioCase, boolean selectLoad) { + JSONArray titleArray = new JSONArray(); + titleArray.add("type"); + titleArray.add("count"); + titleArray.add(groupName); + + List<Series> seriesArr = new ArrayList<>(); + List<Title> titles = new ArrayList<>(); + + int leftPx = 200; + Map<String, String> caseDescMap = this.getCaseDescMap(); + for (TestCaseCountSummary summary : summaryMap.values()) { + String leftPxStr = String.valueOf(leftPx); + + List<Object> dataList = new ArrayList<>(); + + if(selectTestCase && summary.testCaseCount > 0){ + PieData pieData = new PieData(); + pieData.setName(caseDescMap.get("testCaseDesc")); + pieData.setValue(summary.testCaseCount); + pieData.setColor("#F38F1F"); + dataList.add(pieData); + } + + if(selectApi && summary.apiCaseCount > 0){ + PieData apicasePieData = new PieData(); + apicasePieData.setName(caseDescMap.get("apiCaseDesc")); + apicasePieData.setValue(summary.apiCaseCount); + apicasePieData.setColor("#6FD999"); + dataList.add(apicasePieData); + } + + if(selectScenarioCase && summary.scenarioCaseCount > 0){ + PieData scenarioPieData = new PieData(); + scenarioPieData.setName(caseDescMap.get("scenarioCaseDesc")); + scenarioPieData.setValue(summary.scenarioCaseCount); + scenarioPieData.setColor("#2884F3"); + dataList.add(scenarioPieData); + } + + if(selectLoad && summary.loadCaseCount > 0){ + PieData loadCasePieData = new PieData(); + loadCasePieData.setName(caseDescMap.get("loadCaseDesc")); + loadCasePieData.setValue(summary.loadCaseCount); + loadCasePieData.setColor("#F45E53"); + dataList.add(loadCasePieData); + } + + Series series = new Series(); + series.setType("pie"); + series.setRadius("50"); + series.setEncode(new JSONObject() {{ + this.put("itemName", "groupname"); + this.put("value", summary.groupName); + }}); + seriesArr.add(series); + + series.setData(dataList); + series.setCenter(new ArrayList<String>() {{ + this.add(leftPxStr); + this.add("50%"); + }}); + + Title title = new Title(); + title.setSubtext(summary.groupName); + title.setLeft(leftPxStr); + titles.add(title); + + leftPx = leftPx + 350; + } + + pieChartDTO.setSeries(seriesArr); + pieChartDTO.setTitle(titles); + pieChartDTO.setWidth(leftPx); + } + + private Map<String, TestCaseCountSummary> summaryCountResult(boolean parseGroupNameToUserName, boolean parseGrouNameToCaseStatus, String projectId, String order, + List<TestCaseCountChartResult> functionCaseCountResult, List<TestCaseCountChartResult> apiCaseCountResult, List<TestCaseCountChartResult> scenarioCaseCount, List<TestCaseCountChartResult> loadCaseCount) { + Map<String, TestCaseCountSummary> summaryMap = new LinkedHashMap<>(); + + //groupName 解析对象 + Map<String, String> groupNameParseMap = new HashMap<>(); + if (parseGroupNameToUserName) { + groupNameParseMap.putAll(this.getUserIdMap()); + } + if (parseGrouNameToCaseStatus) { + groupNameParseMap.putAll(this.getCaseStatusMap(projectId)); + } + + if (CollectionUtils.isNotEmpty(functionCaseCountResult)) { + for (TestCaseCountChartResult result : functionCaseCountResult) { + if(result.getGroupName() == null){ + result.setGroupName(groupNameParseMap.get("running")); + }else { + if (groupNameParseMap.containsKey(result.getGroupName().toLowerCase(Locale.ROOT))) { + result.setGroupName(groupNameParseMap.get(result.getGroupName().toLowerCase(Locale.ROOT))); + } + } + + String groupName = result.getGroupName(); + if (StringUtils.isNotEmpty(groupName)) { + TestCaseCountSummary summary = summaryMap.get(groupName); + if (summary == null) { + summary = new TestCaseCountSummary(groupName); + } + summary.testCaseCount = result.getCountNum(); + summaryMap.put(groupName, summary); + } + } + } + + if (CollectionUtils.isNotEmpty(apiCaseCountResult)) { + for (TestCaseCountChartResult result : apiCaseCountResult) { + if(result.getGroupName() == null){ + result.setGroupName(groupNameParseMap.get("running")); + }else { + if (groupNameParseMap.containsKey(result.getGroupName().toLowerCase(Locale.ROOT))) { + result.setGroupName(groupNameParseMap.get(result.getGroupName().toLowerCase(Locale.ROOT))); + } + } + String groupName = result.getGroupName(); + if (StringUtils.isNotEmpty(groupName)) { + TestCaseCountSummary summary = summaryMap.get(groupName); + if (summary == null) { + summary = new TestCaseCountSummary(groupName); + } + summary.apiCaseCount = result.getCountNum(); + summaryMap.put(groupName, summary); + } + } + } + + if (CollectionUtils.isNotEmpty(scenarioCaseCount)) { + for (TestCaseCountChartResult result : scenarioCaseCount) { + if(result.getGroupName() == null){ + result.setGroupName(groupNameParseMap.get("running")); + }else { + if (groupNameParseMap.containsKey(result.getGroupName().toLowerCase(Locale.ROOT))) { + result.setGroupName(groupNameParseMap.get(result.getGroupName().toLowerCase(Locale.ROOT))); + } + } + String groupName = result.getGroupName(); + if (StringUtils.isNotEmpty(groupName)) { + TestCaseCountSummary summary = summaryMap.get(groupName); + if (summary == null) { + summary = new TestCaseCountSummary(groupName); + } + summary.scenarioCaseCount = result.getCountNum(); + summaryMap.put(groupName, summary); + } + } + } + + if (CollectionUtils.isNotEmpty(loadCaseCount)) { + for (TestCaseCountChartResult result : loadCaseCount) { + if(result.getGroupName() == null){ + result.setGroupName(groupNameParseMap.get("running")); + }else { + if (groupNameParseMap.containsKey(result.getGroupName().toLowerCase(Locale.ROOT))) { + result.setGroupName(groupNameParseMap.get(result.getGroupName().toLowerCase(Locale.ROOT))); + } + } + String groupName = result.getGroupName(); + if (StringUtils.isNotEmpty(groupName)) { + TestCaseCountSummary summary = summaryMap.get(groupName); + if (summary == null) { + summary = new TestCaseCountSummary(groupName); + } + summary.loadCaseCount = result.getCountNum(); + summaryMap.put(groupName, summary); + } + } + } + + Map<String, TestCaseCountSummary> returmMap = new LinkedHashMap<>(); + + if(StringUtils.equalsIgnoreCase(order,"desc")){ + TreeMap<Long,List<TestCaseCountSummary>> treeMap = new TreeMap<>(); + for (TestCaseCountSummary model : summaryMap.values()) { + if(treeMap.containsKey(model.getAllCount())){ + treeMap.get(model.getAllCount()).add(model); + }else { + List<TestCaseCountSummary> list = new ArrayList<>(); + list.add(model); + treeMap.put(model.getAllCount(),list); + } + } + ArrayList<TestCaseCountSummary> sortedList = new ArrayList<>(); + for (List<TestCaseCountSummary> list : treeMap.values()) { + sortedList.addAll(list); + } + + for (int i = sortedList.size(); i > 0; i --) { + TestCaseCountSummary model = sortedList.get(i-1); + returmMap.put(model.groupName,model); + } + }else if(StringUtils.equalsIgnoreCase(order,"asc")){ + TreeMap<Long,List<TestCaseCountSummary>> treeMap = new TreeMap<>(); + for (TestCaseCountSummary model : summaryMap.values()) { + if(treeMap.containsKey(model.getAllCount())){ + treeMap.get(model.getAllCount()).add(model); + }else { + List<TestCaseCountSummary> list = new ArrayList<>(); + list.add(model); + treeMap.put(model.getAllCount(),list); + } + } + for (List<TestCaseCountSummary> list : treeMap.values()) { + for (TestCaseCountSummary model : list ) { + returmMap.put(model.groupName,model); + } + } + }else { + returmMap = summaryMap; + } + + + return returmMap; + } + + private Map<String, String> getUserIdMap() { + List<User> userList = userService.getUserList(); + Map<String, String> userIdMap = new HashMap<>(); + for (User model : userList) { + userIdMap.put(model.getId(), model.getId() + "\n(" + model.getName() + ")"); + } + return userIdMap; + } + + private Map<String, String> getCaseStatusMap(String projectId) { + + Map<String, String> caseStatusMap = new HashMap<>(); + + TestCaseTemplateService testCaseTemplateService = CommonBeanFactory.getBean(TestCaseTemplateService.class); + TestCaseTemplateDao testCaseTemplate = testCaseTemplateService.getTemplate(projectId); + + caseStatusMap.put("prepare", Translator.get("test_case_status_prepare")); + caseStatusMap.put("error", Translator.get("test_case_status_error")); + caseStatusMap.put("success", Translator.get("test_case_status_success")); + caseStatusMap.put("trash", Translator.get("test_case_status_trash")); + caseStatusMap.put("underway", Translator.get("test_case_status_running")); + caseStatusMap.put("starting", Translator.get("test_case_status_running")); + caseStatusMap.put("saved", Translator.get("test_case_status_saved")); + caseStatusMap.put("running", Translator.get("test_case_status_running")); + caseStatusMap.put("finished", Translator.get("test_case_status_finished")); + caseStatusMap.put("completed", Translator.get("test_case_status_finished")); + + if (testCaseTemplate != null && CollectionUtils.isNotEmpty(testCaseTemplate.getCustomFields())) { + for (CustomField customField : testCaseTemplate.getCustomFields()) { + if (StringUtils.equals(customField.getName(), "用例状态")) { + JSONArray optionsArr = JSONArray.parseArray(customField.getOptions()); + for (int i = 0; i < optionsArr.size(); i++) { + JSONObject jsonObject = optionsArr.getJSONObject(i); + if (jsonObject.containsKey("value") && jsonObject.containsKey("text") && + !StringUtils.equalsAnyIgnoreCase(jsonObject.getString("value"), "Prepare", "Error", "Success", "Trash", "Underway", "Starting", "Saved")) { + caseStatusMap.put(jsonObject.getString("value"), jsonObject.getString("text")); + } + } + } + } + } + + return caseStatusMap; + } + + + private void formatXaxisSeries(XAxis xAxis, List<Series> seriesList, TestAnalysisChartDTO dto, + Map<String, TestCaseCountSummary> summaryMap) { + List<String> xAxisDataList = new ArrayList<>(); + + List<Object> testCaseCountList = new ArrayList<>(); + List<Object> apiCaseCountList = new ArrayList<>(); + List<Object> scenarioCaseCountList = new ArrayList<>(); + List<Object> loadCaseCountList = new ArrayList<>(); + for (TestCaseCountSummary summary : summaryMap.values()) { + xAxisDataList.add(summary.groupName); + testCaseCountList.add(String.valueOf(summary.testCaseCount)); + apiCaseCountList.add(String.valueOf(summary.apiCaseCount)); + scenarioCaseCountList.add(String.valueOf(summary.scenarioCaseCount)); + loadCaseCountList.add(String.valueOf(summary.loadCaseCount)); + } + xAxis.setData(xAxisDataList); + + Map<String, String> caseDescMap = this.getCaseDescMap(); + + Series tetcaseSeries = new Series(); + tetcaseSeries.setName(caseDescMap.get("testCaseDesc")); + tetcaseSeries.setColor("#F38F1F"); + tetcaseSeries.setRadius("20%"); + tetcaseSeries.setType("bar"); + tetcaseSeries.setStack("total"); + tetcaseSeries.setData(testCaseCountList); + seriesList.add(tetcaseSeries); + + Series apiSeries = new Series(); + apiSeries.setName(caseDescMap.get("apiCaseDesc")); + apiSeries.setColor("#6FD999"); + apiSeries.setType("bar"); + apiSeries.setStack("total"); + apiSeries.setData(apiCaseCountList); + seriesList.add(apiSeries); + + Series scenarioSeries = new Series(); + scenarioSeries.setName(caseDescMap.get("scenarioCaseDesc")); + scenarioSeries.setColor("#2884F3"); + scenarioSeries.setType("bar"); + scenarioSeries.setStack("total"); + scenarioSeries.setData(scenarioCaseCountList); + seriesList.add(scenarioSeries); + + Series loadSeries = new Series(); + loadSeries.setName(caseDescMap.get("loadCaseDesc")); + loadSeries.setColor("#F45E53"); + loadSeries.setType("bar"); + loadSeries.setStack("total"); + loadSeries.setData(loadCaseCountList); + seriesList.add(loadSeries); + + dto.setXAxis(xAxis); + dto.setYAxis(new YAxis()); + dto.setSeries(seriesList); + } + + private void formatLegend(Legend legend, List<String> datas, TestCaseCountRequest yrequest) { + Map<String, Boolean> selected = new LinkedHashMap<>(); + List<String> list = new LinkedList<>(); + legend.setSelected(selected); + legend.setData(datas); + } + + private void formatTable(List<TestCaseCountTableDTO> dtos, Map<String, TestCaseCountSummary> summaryMap) { + for (TestCaseCountSummary summary : summaryMap.values()) { + TestCaseCountTableDTO dto = new TestCaseCountTableDTO(summary.groupName, summary.testCaseCount, summary.apiCaseCount, summary.scenarioCaseCount, summary.loadCaseCount); + dtos.add(dto); + } + } + + private Map<String, String> getCaseDescMap() { + Map<String, String> map = new HashMap<>(); + map.put("testCaseDesc", Translator.get("test_case")); + map.put("apiCaseDesc", Translator.get("api_case")); + map.put("scenarioCaseDesc", Translator.get("scenario_case")); + map.put("loadCaseDesc", Translator.get("performance_case")); + return map; + } + + public Map<String, List<Map<String, String>>> getSelectFilterDatas(String projectId) { + Map<String, List<Map<String, String>>> returnMap = new HashMap<>(); + + //组装用户 + QueryMemberRequest memberRequest = new QueryMemberRequest(); + memberRequest.setProjectId(projectId); + List<User> userList = userService.getUserList(); + + List<Map<String, String>> returnUserList = new ArrayList<>(); + for (User user : userList) { + Map<String, String> map = new HashMap<>(); + map.put("id", user.getId()); + map.put("label", user.getId() + "(" + user.getName() + ")"); + returnUserList.add(map); + } + + //组装用例等级和用例状态 + TestCaseTemplateService testCaseTemplateService = CommonBeanFactory.getBean(TestCaseTemplateService.class); + TestCaseTemplateDao testCaseTemplate = testCaseTemplateService.getTemplate(projectId); + + List<Map<String, String>> caseLevelList = new ArrayList<>(); + List<Map<String, String>> caseStatusList = new ArrayList<>(); + Map<String, String> statusMap1 = new HashMap<>(); + statusMap1.put("id", "Prepare"); + statusMap1.put("label", Translator.get("test_case_status_prepare")); + +// Map<String, String> statusMap2 = new HashMap<>(); +// statusMap2.put("id", "Error"); +// statusMap2.put("label", Translator.get("test_case_status_error")); +// +// Map<String, String> statusMap3 = new HashMap<>(); +// statusMap3.put("id", "Success"); +// statusMap3.put("label", Translator.get("test_case_status_success")); + +// Map<String, String> statusMap4 = new HashMap<>(); +// statusMap4.put("id", "Trash"); +// statusMap4.put("label", Translator.get("test_case_status_trash")); + +// Map<String, String> statusMap5 = new HashMap<>(); +// statusMap5.put("id", "Underway"); +// statusMap5.put("label", Translator.get("test_case_status_running")); + +// Map<String, String> statusMap6 = new HashMap<>(); +// statusMap6.put("id", "Starting"); +// statusMap6.put("label", Translator.get("test_case_status_running")); + + Map<String, String> statusMap7 = new HashMap<>(); + statusMap7.put("id", "Saved"); + statusMap7.put("label", Translator.get("test_case_status_saved")); + + Map<String, String> statusMap8 = new HashMap<>(); + statusMap8.put("id", "Running"); + statusMap8.put("label", Translator.get("test_case_status_running")); + + Map<String, String> statusMap9 = new HashMap<>(); + statusMap9.put("id", "Finished"); + statusMap9.put("label", Translator.get("test_case_status_finished")); + + caseStatusList.add(statusMap1); +// caseStatusList.add(statusMap2); +// caseStatusList.add(statusMap3); +// caseStatusList.add(statusMap4); +// caseStatusList.add(statusMap5); +// caseStatusList.add(statusMap6); + caseStatusList.add(statusMap7); + caseStatusList.add(statusMap8); + caseStatusList.add(statusMap9); + + Map<String, String> levelMap1 = new HashMap<>(); + levelMap1.put("id", "P0"); + levelMap1.put("label", "P0"); + Map<String, String> levelMap2 = new HashMap<>(); + levelMap2.put("id", "P1"); + levelMap2.put("label", "P1"); + Map<String, String> levelMap3 = new HashMap<>(); + levelMap3.put("id", "P2"); + levelMap3.put("label", "P2"); + Map<String, String> levelMap4 = new HashMap<>(); + levelMap4.put("id", "P3"); + levelMap4.put("label", "P3"); + caseLevelList.add(levelMap1); + caseLevelList.add(levelMap2); + caseLevelList.add(levelMap3); + caseLevelList.add(levelMap4); + + + if (testCaseTemplate != null && CollectionUtils.isNotEmpty(testCaseTemplate.getCustomFields())) { + for (CustomField customField : testCaseTemplate.getCustomFields()) { + if (StringUtils.equals(customField.getName(), "用例状态")) { + JSONArray optionsArr = JSONArray.parseArray(customField.getOptions()); + for (int i = 0; i < optionsArr.size(); i++) { + JSONObject jsonObject = optionsArr.getJSONObject(i); + if (jsonObject.containsKey("value") && jsonObject.containsKey("text")) { + String value = jsonObject.getString("value"); + if(!StringUtils.equalsAnyIgnoreCase(value, "Prepare", "Error", "Success", "Trash", "Underway", "Starting", "Saved", + "Completed","test_track.case.status_finished")){ + Map<String, String> statusMap = new HashMap<>(); + statusMap.put("id", jsonObject.getString("value")); + statusMap.put("label", jsonObject.getString("text")); + caseStatusList.add(statusMap); + } + } + } + } else if (StringUtils.equals(customField.getName(), "用例等级")) { + JSONArray optionsArr = JSONArray.parseArray(customField.getOptions()); + for (int i = 0; i < optionsArr.size(); i++) { + JSONObject jsonObject = optionsArr.getJSONObject(i); + if (jsonObject.containsKey("value") && jsonObject.containsKey("text") && + !StringUtils.equalsAnyIgnoreCase(jsonObject.getString("value"), "P0", "P1", "P2", "P3")) { + Map<String, String> levelMap = new HashMap<>(); + levelMap.put("id", jsonObject.getString("value")); + levelMap.put("label", jsonObject.getString("text")); + caseLevelList.add(levelMap); + } + } + } + } + } + Map<String, String> caseDescMap = this.getCaseDescMap(); + // 组装用例类型 + List<Map<String, String>> caseTypeList = new ArrayList<>(); + Map<String, String> caseTypeMap1 = new HashMap<>(); + caseTypeMap1.put("id", "testCase"); + caseTypeMap1.put("label", caseDescMap.get("testCaseDesc")); + Map<String, String> caseTypeMap2 = new HashMap<>(); + caseTypeMap2.put("id", "apiCase"); + caseTypeMap2.put("label", caseDescMap.get("apiCaseDesc")); + Map<String, String> caseTypeMap3 = new HashMap<>(); + caseTypeMap3.put("id", "scenarioCase"); + caseTypeMap3.put("label", caseDescMap.get("scenarioCaseDesc")); + Map<String, String> caseTypeMap4 = new HashMap<>(); + caseTypeMap4.put("id", "loadCase"); + caseTypeMap4.put("label", caseDescMap.get("loadCaseDesc")); + caseTypeList.add(caseTypeMap1); + caseTypeList.add(caseTypeMap2); + caseTypeList.add(caseTypeMap3); + caseTypeList.add(caseTypeMap4); + + returnMap.put("casetype", caseTypeList); + returnMap.put("caselevel", caseLevelList); + returnMap.put("casestatus", caseStatusList); + returnMap.put("creator", returnUserList); + returnMap.put("maintainer", returnUserList); + return returnMap; + } +} diff --git a/backend/src/main/java/io/metersphere/service/TestPlanPrincipalService.java b/backend/src/main/java/io/metersphere/service/TestPlanPrincipalService.java new file mode 100644 index 0000000000..e6dce1499b --- /dev/null +++ b/backend/src/main/java/io/metersphere/service/TestPlanPrincipalService.java @@ -0,0 +1,33 @@ +package io.metersphere.service; + +import io.metersphere.base.domain.TestPlanPrincipal; +import io.metersphere.base.domain.TestPlanPrincipalExample; +import io.metersphere.base.mapper.TestPlanPrincipalMapper; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; + +@Service +@Transactional(rollbackFor = Exception.class) +public class TestPlanPrincipalService { + + @Resource + private TestPlanPrincipalMapper testPlanPrincipalMapper; + + + public void deleteTestPlanPrincipalByPlanId(String planId) { + if (StringUtils.isBlank(planId)) { + return; + } + TestPlanPrincipalExample example = new TestPlanPrincipalExample(); + example.createCriteria().andTestPlanIdEqualTo(planId); + testPlanPrincipalMapper.deleteByExample(example); + } + + public TestPlanPrincipal insert(TestPlanPrincipal testPlanPrincipal) { + testPlanPrincipalMapper.insert(testPlanPrincipal); + return testPlanPrincipal; + } +} diff --git a/backend/src/main/java/io/metersphere/track/controller/TestPlanController.java b/backend/src/main/java/io/metersphere/track/controller/TestPlanController.java index 5e1f54af8e..4a072663a1 100644 --- a/backend/src/main/java/io/metersphere/track/controller/TestPlanController.java +++ b/backend/src/main/java/io/metersphere/track/controller/TestPlanController.java @@ -5,10 +5,7 @@ import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import io.metersphere.api.dto.datacount.request.ScheduleInfoRequest; import io.metersphere.api.service.ApiAutomationService; -import io.metersphere.base.domain.Project; -import io.metersphere.base.domain.Schedule; -import io.metersphere.base.domain.TestPlan; -import io.metersphere.base.domain.TestPlanWithBLOBs; +import io.metersphere.base.domain.*; import io.metersphere.commons.constants.NoticeConstants; import io.metersphere.commons.constants.OperLogConstants; import io.metersphere.commons.constants.PermissionConstants; @@ -107,8 +104,8 @@ public class TestPlanController { @RequiresPermissions(PermissionConstants.PROJECT_TRACK_PLAN_READ_EDIT) @MsAuditLog(module = "track_test_plan", type = OperLogConstants.UPDATE, beforeEvent = "#msClass.getLogDetails(#testPlanDTO.id)", content = "#msClass.getLogDetails(#testPlanDTO.id)", msClass = TestPlanService.class) @SendNotice(taskType = NoticeConstants.TaskType.TEST_PLAN_TASK, event = NoticeConstants.Event.UPDATE, mailTemplate = "track/TestPlanEnd", subject = "测试计划通知") - public TestPlan editTestPlan(@RequestBody TestPlanDTO testPlanDTO) { - return testPlanService.editTestPlan(testPlanDTO); + public TestPlan editTestPlan(@RequestBody AddTestPlanRequest testPlanDTO) { + return testPlanService.editTestPlanWithRequest(testPlanDTO); } @PostMapping("/edit/status/{planId}") @@ -259,4 +256,9 @@ public class TestPlanController { public boolean haveExecCase(@PathVariable String id) { return testPlanService.haveExecCase(id); } + + @GetMapping("/principal/{planId}") + public List<User> getPlanPrincipal(@PathVariable String planId) { + return testPlanService.getPlanPrincipal(planId); + } } diff --git a/backend/src/main/java/io/metersphere/track/controller/TestPlanLoadCaseController.java b/backend/src/main/java/io/metersphere/track/controller/TestPlanLoadCaseController.java index ad53a54dd5..5402d34c4e 100644 --- a/backend/src/main/java/io/metersphere/track/controller/TestPlanLoadCaseController.java +++ b/backend/src/main/java/io/metersphere/track/controller/TestPlanLoadCaseController.java @@ -5,6 +5,7 @@ import com.github.pagehelper.PageHelper; import io.metersphere.base.domain.LoadTest; import io.metersphere.base.domain.TestPlanLoadCase; import io.metersphere.commons.constants.OperLogConstants; +import io.metersphere.commons.constants.TriggerMode; import io.metersphere.commons.utils.PageUtils; import io.metersphere.commons.utils.Pager; import io.metersphere.controller.request.ResetOrderRequest; @@ -66,6 +67,11 @@ public class TestPlanLoadCaseController { @PostMapping("/run/batch") @MsAuditLog(module = "track_test_plan", type = OperLogConstants.EXECUTE, content = "#msClass.getRunLogDetails(#request.requests)", msClass = TestPlanLoadCaseService.class) public void runBatch(@RequestBody RunBatchTestPlanRequest request) { + if (request.getRequests() != null) { + for (RunTestPlanRequest req : request.getRequests()) { + req.setTriggerMode(TriggerMode.BATCH.name()); + } + } testPlanLoadCaseService.runBatch(request); } @@ -81,13 +87,13 @@ public class TestPlanLoadCaseController { } @PostMapping("/update") - @MsAuditLog(module = "track_test_plan", type = OperLogConstants.UPDATE, content = "#msClass.getLogDetails(#testPlanLoadCase.id)", msClass = TestPlanLoadCaseService.class) + @MsAuditLog(module = "track_test_plan", type = OperLogConstants.UPDATE, content = "#msClass.getLogDetails(#testPlanLoadCase.id)", msClass = TestPlanLoadCaseService.class) public void update(@RequestBody TestPlanLoadCase testPlanLoadCase) { testPlanLoadCaseService.update(testPlanLoadCase); } @PostMapping("/update/api") - @MsAuditLog(module = "track_test_plan", type = OperLogConstants.UPDATE, content = "#msClass.getLogDetails(#testPlanLoadCase.id)", msClass = TestPlanLoadCaseService.class) + @MsAuditLog(module = "track_test_plan", type = OperLogConstants.UPDATE, content = "#msClass.getLogDetails(#testPlanLoadCase.id)", msClass = TestPlanLoadCaseService.class) public void updateByApi(@RequestBody TestPlanLoadCase testPlanLoadCase) { testPlanLoadCaseService.updateByApi(testPlanLoadCase); } diff --git a/backend/src/main/java/io/metersphere/track/controller/TestReviewTestCaseController.java b/backend/src/main/java/io/metersphere/track/controller/TestReviewTestCaseController.java index e60565a774..ef0b56192f 100644 --- a/backend/src/main/java/io/metersphere/track/controller/TestReviewTestCaseController.java +++ b/backend/src/main/java/io/metersphere/track/controller/TestReviewTestCaseController.java @@ -6,6 +6,7 @@ import io.metersphere.base.domain.TestCaseReviewTestCase; import io.metersphere.commons.constants.OperLogConstants; import io.metersphere.commons.utils.PageUtils; import io.metersphere.commons.utils.Pager; +import io.metersphere.controller.request.ResetOrderRequest; import io.metersphere.log.annotation.MsAuditLog; import io.metersphere.track.dto.TestReviewCaseDTO; import io.metersphere.track.request.testplancase.TestReviewCaseBatchRequest; @@ -75,4 +76,9 @@ public class TestReviewTestCaseController { return testReviewTestCaseService.getTestCaseReviewDTOList(request); } + @PostMapping("/edit/order") + public void orderCase(@RequestBody ResetOrderRequest request) { + testReviewTestCaseService.updateOrder(request); + } + } diff --git a/backend/src/main/java/io/metersphere/track/dto/TestPlanDTO.java b/backend/src/main/java/io/metersphere/track/dto/TestPlanDTO.java index b16f00970e..1394765eeb 100644 --- a/backend/src/main/java/io/metersphere/track/dto/TestPlanDTO.java +++ b/backend/src/main/java/io/metersphere/track/dto/TestPlanDTO.java @@ -12,7 +12,7 @@ public class TestPlanDTO extends TestPlanWithBLOBs { private String projectName; private String userName; private List<String> projectIds; - + private List<String> principals; /** * 定时任务ID */ diff --git a/backend/src/main/java/io/metersphere/track/issue/AbstractIssuePlatform.java b/backend/src/main/java/io/metersphere/track/issue/AbstractIssuePlatform.java index b1d71197d7..c75fa9a464 100644 --- a/backend/src/main/java/io/metersphere/track/issue/AbstractIssuePlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/AbstractIssuePlatform.java @@ -34,6 +34,7 @@ import org.jsoup.safety.Whitelist; import org.springframework.http.HttpHeaders; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import javax.net.ssl.SSLContext; @@ -348,4 +349,13 @@ public abstract class AbstractIssuePlatform implements IssuesPlatform { .andIssuesIdEqualTo(id); testCaseIssuesMapper.deleteByExample(example); } + + protected void addCustomFields(IssuesUpdateRequest issuesRequest, MultiValueMap<String, Object> paramMap) { + List<CustomFieldItemDTO> customFields = getCustomFields(issuesRequest.getCustomFields()); + customFields.forEach(item -> { + if (StringUtils.isNotBlank(item.getCustomData())) { + paramMap.add(item.getCustomData(), item.getValue()); + } + }); + } } diff --git a/backend/src/main/java/io/metersphere/track/issue/JiraPlatform.java b/backend/src/main/java/io/metersphere/track/issue/JiraPlatform.java index 108a5fa300..d2900d6173 100644 --- a/backend/src/main/java/io/metersphere/track/issue/JiraPlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/JiraPlatform.java @@ -79,7 +79,7 @@ public class JiraPlatform extends AbstractIssuePlatform { return issues; } - public void parseIssue(IssuesWithBLOBs item, JiraIssue jiraIssue) { + public void parseIssue(IssuesWithBLOBs item, JiraIssue jiraIssue, String customFieldsStr) { String lastmodify = ""; String status = ""; JSONObject fields = jiraIssue.getFields(); @@ -89,9 +89,11 @@ public class JiraPlatform extends AbstractIssuePlatform { String description = fields.getString("description"); Parser parser = Parser.builder().build(); - Node document = parser.parse(description); - HtmlRenderer renderer = HtmlRenderer.builder().build(); - description = renderer.render(document); + if (StringUtils.isNotBlank(description)) { + Node document = parser.parse(description); + HtmlRenderer renderer = HtmlRenderer.builder().build(); + description = renderer.render(document); + } if (assignee != null) { lastmodify = assignee.getString("displayName"); @@ -103,6 +105,43 @@ public class JiraPlatform extends AbstractIssuePlatform { item.setDescription(description); item.setPlatformStatus(status); item.setPlatform(IssuesManagePlatform.Jira.toString()); + item.setCustomFields(parseIssueCustomField(customFieldsStr, jiraIssue)); + } + + public String parseIssueCustomField(String customFieldsStr, JiraIssue jiraIssue) { + List<CustomFieldItemDTO> customFields = getCustomFields(customFieldsStr); + JSONObject fields = jiraIssue.getFields(); + + customFields.forEach(item -> { + String fieldName = item.getCustomData(); + Object value = fields.get(fieldName); + if (value != null) { + if (value instanceof JSONObject) { + if (!fieldName.equals("assignee") && !fieldName.equals("reporter")) { // 获取不到账号名 + item.setValue(((JSONObject)value).getString("id")); + } + } else { + if (StringUtils.isNotBlank(item.getType()) && + StringUtils.equalsAny(item.getType(), "multipleSelect", "checkbox", "multipleMember")) { + List<String> values = new ArrayList<>(); + if (item.getValue() != null) { + JSONArray attrs = (JSONArray) item.getValue(); + attrs.forEach(attr -> { + if (attr instanceof JSONObject) { + values.add(((JSONObject)attr).getString("id")); + } else { + values.add((String) attr); + } + }); + } + item.setValue(values); + } else { + item.setValue(value); + } + } + } + }); + return JSONObject.toJSONString(customFields); } private String getStatus(JSONObject fields) { @@ -253,7 +292,7 @@ public class JiraPlatform extends AbstractIssuePlatform { customFields.forEach(item -> { String fieldName = item.getCustomData(); if (StringUtils.isNotBlank(fieldName)) { - if (StringUtils.isNotBlank(item.getValue())) { + if (item.getValue() != null) { if (StringUtils.isNotBlank(item.getType()) && StringUtils.equalsAny(item.getType(), "select", "radio", "member")) { JSONObject param = new JSONObject(); @@ -266,8 +305,8 @@ public class JiraPlatform extends AbstractIssuePlatform { } else if (StringUtils.isNotBlank(item.getType()) && StringUtils.equalsAny(item.getType(), "multipleSelect", "checkbox", "multipleMember")) { JSONArray attrs = new JSONArray(); - if (StringUtils.isNotBlank(item.getValue())) { - JSONArray values = JSONObject.parseArray(item.getValue()); + if (item.getValue() != null) { + JSONArray values = (JSONArray)item.getValue(); values.forEach(v -> { JSONObject param = new JSONObject(); param.put("id", v); @@ -327,11 +366,12 @@ public class JiraPlatform extends AbstractIssuePlatform { setConfig(); try { IssuesWithBLOBs issuesWithBLOBs = issuesMapper.selectByPrimaryKey(item.getId()); - parseIssue(item, jiraClientV2.getIssues(item.getId())); + parseIssue(item, jiraClientV2.getIssues(item.getId()), issuesWithBLOBs.getCustomFields()); String desc = htmlDesc2MsDesc(item.getDescription()); // 保留之前上传的图片 String images = getImages(issuesWithBLOBs.getDescription()); item.setDescription(desc + "\n" + images); + issuesMapper.updateByPrimaryKeySelective(item); } catch (HttpClientErrorException e) { if (e.getRawStatusCode() == 404) { @@ -339,6 +379,8 @@ public class JiraPlatform extends AbstractIssuePlatform { item.setPlatformStatus(IssuesStatus.DELETE.toString()); issuesMapper.deleteByPrimaryKey(item.getId()); } + } catch (Exception e) { + LogUtil.error(e); } }); } diff --git a/backend/src/main/java/io/metersphere/track/issue/TapdPlatform.java b/backend/src/main/java/io/metersphere/track/issue/TapdPlatform.java index e5d1c944ce..17ad97bb4a 100644 --- a/backend/src/main/java/io/metersphere/track/issue/TapdPlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/TapdPlatform.java @@ -87,7 +87,7 @@ public class TapdPlatform extends AbstractIssuePlatform { @Override public void addIssue(IssuesUpdateRequest issuesRequest) { - MultiValueMap<String, String> param = buildUpdateParam(issuesRequest); + MultiValueMap<String, Object> param = buildUpdateParam(issuesRequest); TapdBug bug = tapdClient.addIssue(param); Map<String, String> statusMap = tapdClient.getStatusMap(getProjectId(this.projectId)); issuesRequest.setPlatformStatus(statusMap.get(bug.getStatus())); @@ -102,18 +102,16 @@ public class TapdPlatform extends AbstractIssuePlatform { @Override public void updateIssue(IssuesUpdateRequest request) { - MultiValueMap<String, String> param = buildUpdateParam(request); + MultiValueMap<String, Object> param = buildUpdateParam(request); param.add("id", request.getId()); handleIssueUpdate(request); tapdClient.updateIssue(param); } - private MultiValueMap<String, String> buildUpdateParam(IssuesUpdateRequest issuesRequest) { + private MultiValueMap<String, Object> buildUpdateParam(IssuesUpdateRequest issuesRequest) { issuesRequest.setPlatform(IssuesManagePlatform.Tapd.toString()); setConfig(); - List<CustomFieldItemDTO> customFields = getCustomFields(issuesRequest.getCustomFields()); - String tapdId = getProjectId(issuesRequest.getProjectId()); if (StringUtils.isBlank(tapdId)) { @@ -131,17 +129,14 @@ public class TapdPlatform extends AbstractIssuePlatform { reporter = SessionUtils.getUser().getName(); } - MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>(); + MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>(); paramMap.add("title", issuesRequest.getTitle()); paramMap.add("workspace_id", tapdId); paramMap.add("description", msDescription2Tapd(issuesRequest.getDescription())); paramMap.add("current_owner", usersStr); - customFields.forEach(item -> { - if (StringUtils.isNotBlank(item.getCustomData())) { - paramMap.add(item.getCustomData(), item.getValue()); - } - }); + addCustomFields(issuesRequest, paramMap); + paramMap.add("reporter", reporter); return paramMap; } diff --git a/backend/src/main/java/io/metersphere/track/issue/ZentaoPlatform.java b/backend/src/main/java/io/metersphere/track/issue/ZentaoPlatform.java index d7ee608abe..5e3239f773 100644 --- a/backend/src/main/java/io/metersphere/track/issue/ZentaoPlatform.java +++ b/backend/src/main/java/io/metersphere/track/issue/ZentaoPlatform.java @@ -204,7 +204,6 @@ public class ZentaoPlatform extends AbstractIssuePlatform { private MultiValueMap<String, Object> buildUpdateParam(IssuesUpdateRequest issuesRequest) { issuesRequest.setPlatform(IssuesManagePlatform.Zentao.toString()); - List<CustomFieldItemDTO> customFields = getCustomFields(issuesRequest.getCustomFields()); zentaoClient.setConfig(getUserConfig()); String projectId = getProjectId(issuesRequest.getProjectId()); if (StringUtils.isBlank(projectId)) { @@ -214,11 +213,7 @@ public class ZentaoPlatform extends AbstractIssuePlatform { paramMap.add("product", projectId); paramMap.add("title", issuesRequest.getTitle()); - customFields.forEach(item -> { - if (StringUtils.isNotBlank(item.getCustomData())) { - paramMap.add(item.getCustomData(), item.getValue()); - } - }); + addCustomFields(issuesRequest, paramMap); String description = issuesRequest.getDescription(); String zentaoSteps = description; diff --git a/backend/src/main/java/io/metersphere/track/issue/client/TapdClient.java b/backend/src/main/java/io/metersphere/track/issue/client/TapdClient.java index decda1d9c6..60bcc1babd 100644 --- a/backend/src/main/java/io/metersphere/track/issue/client/TapdClient.java +++ b/backend/src/main/java/io/metersphere/track/issue/client/TapdClient.java @@ -57,9 +57,9 @@ public class TapdClient extends BaseClient { return (TapdGetIssueResponse) getResultForObject(TapdGetIssueResponse.class, response); } - public TapdBug addIssue(MultiValueMap<String, String> paramMap) { + public TapdBug addIssue(MultiValueMap<String, Object> paramMap) { String url = getBaseUrl() + "/bugs"; - HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(paramMap, getAuthHeader()); + HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(paramMap, getAuthHeader()); ResponseEntity<String> response = null; try { response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); @@ -70,7 +70,7 @@ public class TapdClient extends BaseClient { return ((AddTapdIssueResponse) getResultForObject(AddTapdIssueResponse.class, response)).getData().getBug(); } - public TapdBug updateIssue(MultiValueMap<String, String> paramMap) { + public TapdBug updateIssue(MultiValueMap<String, Object> paramMap) { // 带id为更新 return addIssue(paramMap); } diff --git a/backend/src/main/java/io/metersphere/track/issue/client/ZentaoClient.java b/backend/src/main/java/io/metersphere/track/issue/client/ZentaoClient.java index 2fe4418e17..3ed951714a 100644 --- a/backend/src/main/java/io/metersphere/track/issue/client/ZentaoClient.java +++ b/backend/src/main/java/io/metersphere/track/issue/client/ZentaoClient.java @@ -41,7 +41,7 @@ public abstract class ZentaoClient extends BaseClient { ResponseEntity<String> response = restTemplate.exchange(loginUrl + sessionId, HttpMethod.POST, requestEntity, String.class); getUserResponse = (GetUserResponse) getResultForObject(GetUserResponse.class, response); } catch (Exception e) { - LogUtil.error("get result for object error," + e.getMessage()); + LogUtil.error(e); MSException.throwException(e.getMessage()); } GetUserResponse.User user = getUserResponse.getUser(); diff --git a/backend/src/main/java/io/metersphere/track/request/testcase/TestCaseMinderEditRequest.java b/backend/src/main/java/io/metersphere/track/request/testcase/TestCaseMinderEditRequest.java index ad6cd78cc7..3661a13f0a 100644 --- a/backend/src/main/java/io/metersphere/track/request/testcase/TestCaseMinderEditRequest.java +++ b/backend/src/main/java/io/metersphere/track/request/testcase/TestCaseMinderEditRequest.java @@ -11,5 +11,12 @@ import java.util.List; public class TestCaseMinderEditRequest { private String projectId; private List<String> ids; - List<TestCaseWithBLOBs> data; + List<TestCaseMinderEditItem> data; + + @Getter + @Setter + public static class TestCaseMinderEditItem extends TestCaseWithBLOBs { + private String targetId; + private String moveMode; + } } diff --git a/backend/src/main/java/io/metersphere/track/request/testplan/AddTestPlanRequest.java b/backend/src/main/java/io/metersphere/track/request/testplan/AddTestPlanRequest.java index 76ce3b8f3d..d538e4c957 100644 --- a/backend/src/main/java/io/metersphere/track/request/testplan/AddTestPlanRequest.java +++ b/backend/src/main/java/io/metersphere/track/request/testplan/AddTestPlanRequest.java @@ -10,4 +10,5 @@ import java.util.List; @Setter public class AddTestPlanRequest extends TestPlanWithBLOBs { private List<String> projectIds; + private List<String> principals; } diff --git a/backend/src/main/java/io/metersphere/track/service/TestCaseNodeService.java b/backend/src/main/java/io/metersphere/track/service/TestCaseNodeService.java index ed883e1061..3dbc331bcf 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestCaseNodeService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestCaseNodeService.java @@ -576,7 +576,7 @@ public class TestCaseNodeService extends NodeTreeService<TestCaseNodeDTO> { SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); TestCaseMapper testCaseMapper = sqlSession.getMapper(TestCaseMapper.class); testCases.forEach((value) -> { - testCaseMapper.updateByPrimaryKey(value); + testCaseMapper.updateByPrimaryKeySelective(value); }); sqlSession.flushStatements(); } diff --git a/backend/src/main/java/io/metersphere/track/service/TestCaseReviewService.java b/backend/src/main/java/io/metersphere/track/service/TestCaseReviewService.java index 27df8b8918..2cafb7b675 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestCaseReviewService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestCaseReviewService.java @@ -294,9 +294,9 @@ public class TestCaseReviewService { SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); TestCaseReviewTestCaseMapper batchMapper = sqlSession.getMapper(TestCaseReviewTestCaseMapper.class); - + Long nextOrder = ServiceUtils.getNextOrder(request.getReviewId(), extTestReviewCaseMapper::getLastOrder); if (!testCaseIds.isEmpty()) { - testCaseIds.forEach(caseId -> { + for (String caseId : testCaseIds) { TestCaseReviewTestCase caseReview = new TestCaseReviewTestCase(); caseReview.setId(UUID.randomUUID().toString()); caseReview.setReviewer(SessionUtils.getUser().getId()); @@ -306,8 +306,10 @@ public class TestCaseReviewService { caseReview.setUpdateTime(System.currentTimeMillis()); caseReview.setReviewId(request.getReviewId()); caseReview.setStatus(TestCaseReviewStatus.Prepare.name()); + caseReview.setOrder(nextOrder); batchMapper.insert(caseReview); - }); + nextOrder += 5000; + } } sqlSession.flushStatements(); diff --git a/backend/src/main/java/io/metersphere/track/service/TestCaseService.java b/backend/src/main/java/io/metersphere/track/service/TestCaseService.java index 67747358d3..421dad94c5 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestCaseService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestCaseService.java @@ -1427,7 +1427,7 @@ public class TestCaseService { } public void minderEdit(TestCaseMinderEditRequest request) { - List<TestCaseWithBLOBs> data = request.getData(); + List<TestCaseMinderEditRequest.TestCaseMinderEditItem> data = request.getData(); if (CollectionUtils.isNotEmpty(data)) { List<String> editIds = data.stream() .filter(t -> StringUtils.isNotBlank(t.getId()) && t.getId().length() > 20) @@ -1451,12 +1451,14 @@ public class TestCaseService { item.setId(UUID.randomUUID().toString()); item.setMaintainer(SessionUtils.getUserId()); addTestCase(item); + changeOrder(item, request.getProjectId()); } else { TestCaseWithBLOBs dbCase = finalTestCaseMap.get(item.getId()); if (editCustomFieldsPriority(dbCase, item.getPriority())) { item.setCustomFields(dbCase.getCustomFields()); } editTestCase(item); + changeOrder(item, request.getProjectId()); } }); } @@ -1468,6 +1470,17 @@ public class TestCaseService { } } + private void changeOrder(TestCaseMinderEditRequest.TestCaseMinderEditItem item, String projectId) { + if (StringUtils.isNotBlank(item.getTargetId())) { + ResetOrderRequest resetOrderRequest = new ResetOrderRequest(); + resetOrderRequest.setGroupId(projectId); + resetOrderRequest.setMoveId(item.getId()); + resetOrderRequest.setTargetId(item.getTargetId()); + resetOrderRequest.setMoveMode(item.getMoveMode()); + updateOrder(resetOrderRequest); + } + } + /** * 脑图编辑之后修改用例等级,同时修改自定义字段的用例等级 * @@ -1493,7 +1506,7 @@ public class TestCaseService { public List<TestCase> getTestCaseByProjectId(String projectId) { TestCaseExample example = new TestCaseExample(); - example.createCriteria().andProjectIdEqualTo(projectId); + example.createCriteria().andProjectIdEqualTo(projectId).andStatusNotEqualTo("Trash"); return testCaseMapper.selectByExample(example); } diff --git a/backend/src/main/java/io/metersphere/track/service/TestPlanService.java b/backend/src/main/java/io/metersphere/track/service/TestPlanService.java index b91ec9fed5..e14d34ed49 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestPlanService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestPlanService.java @@ -50,6 +50,7 @@ import io.metersphere.plugin.core.MsTestElement; import io.metersphere.service.IssueTemplateService; import io.metersphere.service.ScheduleService; import io.metersphere.service.SystemParameterService; +import io.metersphere.service.TestPlanPrincipalService; import io.metersphere.track.Factory.ReportComponentFactory; import io.metersphere.track.domain.ReportComponent; import io.metersphere.track.dto.*; @@ -183,6 +184,10 @@ public class TestPlanService { private PerformanceReportService performanceReportService; @Resource private MetricQueryService metricQueryService; + @Resource + private TestPlanPrincipalService testPlanPrincipalService; + @Resource + private TestPlanPrincipalMapper testPlanPrincipalMapper; private final ExecutorService executorService = Executors.newFixedThreadPool(20); @@ -194,6 +199,16 @@ public class TestPlanService { testPlan.setCreateTime(System.currentTimeMillis()); testPlan.setUpdateTime(System.currentTimeMillis()); testPlan.setCreator(SessionUtils.getUser().getId()); + + String planId = testPlan.getId(); + List<String> principals = testPlan.getPrincipals(); + for (String principal : principals) { + TestPlanPrincipal testPlanPrincipal = new TestPlanPrincipal(); + testPlanPrincipal.setTestPlanId(planId); + testPlanPrincipal.setPrincipalId(principal); + testPlanPrincipalService.insert(testPlanPrincipal); + } + if (StringUtils.isBlank(testPlan.getProjectId())) { testPlan.setProjectId(SessionUtils.getCurrentProjectId()); } @@ -214,6 +229,22 @@ public class TestPlanService { return Optional.ofNullable(testPlanMapper.selectByPrimaryKey(testPlanId)).orElse(new TestPlanWithBLOBs()); } + public TestPlan editTestPlanWithRequest(AddTestPlanRequest request) { + List<String> principals = request.getPrincipals(); + if (!CollectionUtils.isEmpty(principals)) { + if (StringUtils.isNotBlank(request.getId())) { + testPlanPrincipalService.deleteTestPlanPrincipalByPlanId(request.getId()); + for (String principal : principals) { + TestPlanPrincipal testPlanPrincipal = new TestPlanPrincipal(); + testPlanPrincipal.setTestPlanId(request.getId()); + testPlanPrincipal.setPrincipalId(principal); + testPlanPrincipalService.insert(testPlanPrincipal); + } + } + } + return this.editTestPlan(request); + } + public TestPlan editTestPlan(TestPlanWithBLOBs testPlan) { checkTestPlanExist(testPlan); TestPlan res = testPlanMapper.selectByPrimaryKey(testPlan.getId()); // 先查一次库 @@ -317,7 +348,7 @@ public class TestPlanService { TestPlanExample example = new TestPlanExample(); example.createCriteria() .andNameEqualTo(testPlan.getName()) - .andWorkspaceIdEqualTo(SessionUtils.getCurrentWorkspaceId()) + .andProjectIdEqualTo(testPlan.getProjectId()) .andIdNotEqualTo(testPlan.getId()); if (testPlanMapper.selectByExample(example).size() > 0) { MSException.throwException(Translator.get("plan_name_already_exists")); @@ -326,6 +357,7 @@ public class TestPlanService { } public int deleteTestPlan(String planId) { + testPlanPrincipalService.deleteTestPlanPrincipalByPlanId(planId); deleteTestCaseByPlanId(planId); testPlanApiCaseService.deleteByPlanId(planId); testPlanScenarioCaseService.deleteByPlanId(planId); @@ -1243,7 +1275,6 @@ public class TestPlanService { targetPlan.setWorkspaceId(testPlan.getWorkspaceId()); targetPlan.setDescription(testPlan.getDescription()); targetPlan.setStage(testPlan.getStage()); - targetPlan.setPrincipal(testPlan.getPrincipal()); targetPlan.setTags(testPlan.getTags()); targetPlan.setProjectId(testPlan.getProjectId()); testPlan.setAutomaticStatusUpdate(testPlan.getAutomaticStatusUpdate()); @@ -1253,11 +1284,26 @@ public class TestPlanService { targetPlan.setUpdateTime(System.currentTimeMillis()); testPlanMapper.insert(targetPlan); + copyPlanPrincipal(targetPlanId, planId); copyPlanCase(sourcePlanId, targetPlanId); return targetPlan; } + private void copyPlanPrincipal(String targetPlanId, String sourcePlanId) { + TestPlanPrincipalExample example = new TestPlanPrincipalExample(); + example.createCriteria().andTestPlanIdEqualTo(sourcePlanId); + List<TestPlanPrincipal> testPlanPrincipals = testPlanPrincipalMapper.selectByExample(example); + if (!CollectionUtils.isEmpty(testPlanPrincipals)) { + for (TestPlanPrincipal tpp : testPlanPrincipals) { + TestPlanPrincipal testPlanPrincipal = new TestPlanPrincipal(); + testPlanPrincipal.setPrincipalId(tpp.getPrincipalId()); + testPlanPrincipal.setTestPlanId(targetPlanId); + testPlanPrincipalMapper.insert(testPlanPrincipal); + } + } + } + @Transactional(rollbackFor = Exception.class) public void copyPlanCase(String sourcePlanId, String targetPlanId) { TestPlanTestCaseExample testPlanTestCaseExample = new TestPlanTestCaseExample(); @@ -1992,4 +2038,21 @@ public class TestPlanService { List<TestPlanLoadCase> testPlanLoadCases = testPlanLoadCaseMapper.selectByExample(loadCaseExample); return !CollectionUtils.isEmpty(testPlanLoadCases); } + + public List<User> getPlanPrincipal(String planId) { + List<User> result = new ArrayList<>(); + if (StringUtils.isBlank(planId)) { + return result; + } + TestPlanPrincipalExample example = new TestPlanPrincipalExample(); + example.createCriteria().andTestPlanIdEqualTo(planId); + List<TestPlanPrincipal> testPlanPrincipals = testPlanPrincipalMapper.selectByExample(example); + List<String> userIds = testPlanPrincipals.stream().map(TestPlanPrincipal::getPrincipalId).distinct().collect(Collectors.toList()); + if (CollectionUtils.isEmpty(userIds)) { + return result; + } + UserExample userExample = new UserExample(); + userExample.createCriteria().andIdIn(userIds); + return userMapper.selectByExample(userExample); + } } diff --git a/backend/src/main/java/io/metersphere/track/service/TestReviewTestCaseService.java b/backend/src/main/java/io/metersphere/track/service/TestReviewTestCaseService.java index c88672778b..7ed1df2d61 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestReviewTestCaseService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestReviewTestCaseService.java @@ -9,6 +9,7 @@ import io.metersphere.commons.exception.MSException; import io.metersphere.commons.utils.ServiceUtils; import io.metersphere.commons.utils.SessionUtils; import io.metersphere.controller.request.OrderRequest; +import io.metersphere.controller.request.ResetOrderRequest; import io.metersphere.controller.request.member.QueryMemberRequest; import io.metersphere.log.vo.DetailColumn; import io.metersphere.log.vo.OperatingLogDetails; @@ -60,7 +61,7 @@ public class TestReviewTestCaseService { ExtTestPlanTestCaseMapper extTestPlanTestCaseMapper; public List<TestReviewCaseDTO> list(QueryCaseReviewRequest request) { - request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders())); + request.setOrders(ServiceUtils.getDefaultSortOrder(request.getOrders())); List<TestReviewCaseDTO> list = extTestReviewCaseMapper.list(request); QueryMemberRequest queryMemberRequest = new QueryMemberRequest(); queryMemberRequest.setWorkspaceId(SessionUtils.getCurrentProjectId()); @@ -75,7 +76,7 @@ public class TestReviewTestCaseService { } public List<String> selectIds(QueryCaseReviewRequest request) { - request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders())); + request.setOrders(ServiceUtils.getDefaultSortOrder(request.getOrders())); List<String> list = extTestReviewCaseMapper.selectIds(request); return list; } @@ -150,7 +151,7 @@ public class TestReviewTestCaseService { } public List<TestReviewCaseDTO> getTestCaseReviewDTOList(QueryCaseReviewRequest request) { - request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders())); + request.setOrders(ServiceUtils.getDefaultSortOrder(request.getOrders())); return extTestReviewCaseMapper.list(request); } @@ -344,7 +345,7 @@ public class TestReviewTestCaseService { } public List<TestReviewCaseDTO> listForMinder(QueryCaseReviewRequest request) { - List<OrderRequest> orders = ServiceUtils.getDefaultOrder("tcrtc", request.getOrders()); + List<OrderRequest> orders = ServiceUtils.getDefaultSortOrder("tcrtc", request.getOrders()); orders.forEach(order -> { if (order.getName().equals("update_time")) { order.setPrefix("tcrtc"); @@ -353,4 +354,22 @@ public class TestReviewTestCaseService { request.setOrders(orders); return extTestReviewCaseMapper.listForMinder(request); } + + public void initOrderField() { + ServiceUtils.initOrderField(TestCaseReviewTestCase.class, TestCaseReviewMapper.class, + extTestReviewCaseMapper::selectReviewIds, + extTestReviewCaseMapper::getIdsOrderByUpdateTime); + } + + /** + * 用例自定义排序 + * @param request + */ + public void updateOrder(ResetOrderRequest request) { + ServiceUtils.updateOrderField(request, TestCaseReviewTestCase.class, + testCaseReviewTestCaseMapper::selectByPrimaryKey, + extTestReviewCaseMapper::getPreOrder, + extTestReviewCaseMapper::getLastOrder, + testCaseReviewTestCaseMapper::updateByPrimaryKeySelective); + } } diff --git a/backend/src/main/java/io/metersphere/track/service/TrackService.java b/backend/src/main/java/io/metersphere/track/service/TrackService.java index a2aae3ce11..f9df6fdcc5 100644 --- a/backend/src/main/java/io/metersphere/track/service/TrackService.java +++ b/backend/src/main/java/io/metersphere/track/service/TrackService.java @@ -118,6 +118,12 @@ public class TrackService { int totalBugSize = 0; int totalCaseSize = 0; for (TestPlan plan : plans) { + int planBugSize = getPlanBugSize(plan.getId()); + // bug为0不记录 + if (planBugSize == 0) { + continue; + } + TestPlanBugCount testPlanBug = new TestPlanBugCount(); testPlanBug.setIndex(index++); testPlanBug.setPlanName(plan.getName()); @@ -127,7 +133,6 @@ public class TrackService { int planCaseSize = getPlanCaseSize(plan.getId()); testPlanBug.setCaseSize(planCaseSize); - int planBugSize = getPlanBugSize(plan.getId()); testPlanBug.setBugSize(planBugSize); double planPassRage = getPlanPassRage(plan.getId()); testPlanBug.setPassRage(planPassRage + "%"); diff --git a/backend/src/main/java/io/metersphere/xpack b/backend/src/main/java/io/metersphere/xpack index 8f10fac361..6edae7aeeb 160000 --- a/backend/src/main/java/io/metersphere/xpack +++ b/backend/src/main/java/io/metersphere/xpack @@ -1 +1 @@ -Subproject commit 8f10fac36134bfe4d9520051fdf1cfb9b44f67e4 +Subproject commit 6edae7aeeb9d5ade65d64a115d00af96e4dc56d6 diff --git a/backend/src/main/resources/db/migration/V96__v1.13.1__release.sql b/backend/src/main/resources/db/migration/V96__v1.13.1__release.sql new file mode 100644 index 0000000000..b9d9837c3b --- /dev/null +++ b/backend/src/main/resources/db/migration/V96__v1.13.1__release.sql @@ -0,0 +1,33 @@ +CREATE INDEX load_test_report_test_resource_pool_id_index + ON load_test_report (test_resource_pool_id); + + + +create table if not exists test_plan_principal +( + test_plan_id varchar(50) null, + principal_id varchar(50) null, + constraint test_plan_principal_pk + unique (test_plan_id, principal_id) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE utf8mb4_general_ci; + +insert into test_plan_principal(test_plan_id, principal_id) select id test_plan_id, principal principal_id from test_plan; +alter table test_plan modify principal varchar(50) null comment 'Plan principal'; + + +ALTER TABLE test_case_review_test_case ADD `order` bigint(20) NOT NULL COMMENT '自定义排序,间隔5000'; + + +-- 报表默认权限插入 + +-- 项目管理员 +insert into user_group_permission (id, group_id, permission_id, module_id) +values (UUID(), 'project_admin', 'PROJECT_REPORT_ANALYSIS:READ+UPDATE', 'PROJECT_REPORT_ANALYSIS'); +insert into user_group_permission (id, group_id, permission_id, module_id) +values (UUID(), 'project_admin', 'PROJECT_REPORT_ANALYSIS:READ+CREATE', 'PROJECT_REPORT_ANALYSIS'); + +-- 项目成员 +insert into user_group_permission (id, group_id, permission_id, module_id) +values (UUID(), 'project_member', 'PROJECT_REPORT_ANALYSIS:READ+UPDATE', 'PROJECT_REPORT_ANALYSIS'); +insert into user_group_permission (id, group_id, permission_id, module_id) +values (UUID(), 'project_member', 'PROJECT_REPORT_ANALYSIS:READ+CREATE', 'PROJECT_REPORT_ANALYSIS'); diff --git a/backend/src/main/resources/i18n/messages_en_US.properties b/backend/src/main/resources/i18n/messages_en_US.properties index 34ea53303e..e2e516dfaa 100644 --- a/backend/src/main/resources/i18n/messages_en_US.properties +++ b/backend/src/main/resources/i18n/messages_en_US.properties @@ -193,7 +193,7 @@ quota_max_threads_excess_workspace=The maximum number of concurrent threads exce quota_max_threads_excess_organization=The maximum number of concurrent threads exceeds the organization quota quota_duration_excess_workspace=The stress test duration exceeds the work space quota quota_duration_excess_organization=The stress test duration exceeds the organization quota -import_xmind_count_error=The number of use cases imported into the mind map cannot exceed 500 +import_xmind_count_error=The number of use cases imported into the mind map cannot exceed 800 import_xmind_not_found=Test case not found license_valid_license_error=Authorization authentication failed test_review_task_notice=Test review task notice @@ -282,4 +282,4 @@ scenario_case=SCENARIO create_user=Create user test_case_status=Case status -id_not_rightful=ID is not rightful \ No newline at end of file +id_not_rightful=ID is not rightful diff --git a/backend/src/main/resources/i18n/messages_zh_CN.properties b/backend/src/main/resources/i18n/messages_zh_CN.properties index 84c3e784bd..da7ce149ce 100644 --- a/backend/src/main/resources/i18n/messages_zh_CN.properties +++ b/backend/src/main/resources/i18n/messages_zh_CN.properties @@ -193,7 +193,7 @@ quota_max_threads_excess_workspace=最大并发数超过工作空间限额 quota_max_threads_excess_organization=最大并发数超过组织限额 quota_duration_excess_workspace=压测时长超过工作空间限额 quota_duration_excess_organization=压测时长超过组织限额 -import_xmind_count_error=思维导图导入用例数量不能超过 500 条 +import_xmind_count_error=思维导图导入用例数量不能超过 800 条 license_valid_license_error=授权认证失败 import_xmind_not_found=未找到测试用例 test_review_task_notice=测试评审任务通知 diff --git a/backend/src/main/resources/i18n/messages_zh_TW.properties b/backend/src/main/resources/i18n/messages_zh_TW.properties index 77d054c2f3..d1acf792d0 100644 --- a/backend/src/main/resources/i18n/messages_zh_TW.properties +++ b/backend/src/main/resources/i18n/messages_zh_TW.properties @@ -195,7 +195,7 @@ quota_duration_excess_workspace=壓測時長超過工作空間限額 quota_duration_excess_organization=壓測時長超過組織限額 license_valid_license_error=授權驗證失敗 license_valid_license_code=授權碼已經存在 -import_xmind_count_error=思維導圖導入用例數量不能超過 500 條 +import_xmind_count_error=思維導圖導入用例數量不能超過 800 條 import_xmind_not_found=未找到测试用例 test_review_task_notice=測試評審任務通知 swagger_url_scheduled_import_notification=swagger_url定時導入通知 diff --git a/backend/src/main/resources/mail/ApiFailedNotification.html b/backend/src/main/resources/mail/ApiFailedNotification.html index 90d282ed72..179c816b4c 100644 --- a/backend/src/main/resources/mail/ApiFailedNotification.html +++ b/backend/src/main/resources/mail/ApiFailedNotification.html @@ -12,8 +12,8 @@ </div> <div style="margin-left: 100px"> - <p>${testName} 接口测试运行失败<br/> - <p>执行人:${executor}</p> + <p>${name} 接口测试运行失败<br/> + <p>执行人:${operator}</p> <p>执行环境:${executionEnvironment}</p> <p>执行时间:${executionTime}</p> 请点击下面链接进入测试报告页面【接口定义暂无报告路径】<br/> diff --git a/backend/src/main/resources/mail/ApiSuccessfulNotification.html b/backend/src/main/resources/mail/ApiSuccessfulNotification.html index e0f141a2a7..9ecdcc11a4 100644 --- a/backend/src/main/resources/mail/ApiSuccessfulNotification.html +++ b/backend/src/main/resources/mail/ApiSuccessfulNotification.html @@ -10,8 +10,8 @@ <p style="margin-left: 60px">您好: </div> <div style="margin-left: 100px"> - <p>${testName} 接口测试运行成功<br/> - <p>执行人:${executor}</p> + <p>${name} 接口测试运行成功<br/> + <p>执行人:${operator}</p> <p>执行环境:${executionEnvironment}</p> <p>执行时间:${executionTime}</p> 请点击下面链接进入测试报告页面【接口定义暂无报告路径】<br/> diff --git a/backend/src/main/resources/mail/PerformanceApiSuccessNotification.html b/backend/src/main/resources/mail/PerformanceApiSuccessNotification.html index 047ce019c1..04bca545f6 100644 --- a/backend/src/main/resources/mail/PerformanceApiSuccessNotification.html +++ b/backend/src/main/resources/mail/PerformanceApiSuccessNotification.html @@ -10,7 +10,7 @@ <p style="margin-left: 60px">您好: </div> <div style="margin-left: 100px"> - <p>您所执行的 ${testName} 性能测试已完成<br/> + <p>您所执行的 ${name} 性能测试已完成<br/> 请点击下面链接进入测试报告页面</p> <a href="${url}/#/${type}/report/view/${id}">${url}/#/${type}/report/view/${id}</a> </div> diff --git a/backend/src/main/resources/mail/PerformanceFailedNotification.html b/backend/src/main/resources/mail/PerformanceFailedNotification.html index 97ee5ec1fb..41da8c827f 100644 --- a/backend/src/main/resources/mail/PerformanceFailedNotification.html +++ b/backend/src/main/resources/mail/PerformanceFailedNotification.html @@ -11,7 +11,7 @@ <p style="margin-left: 60px">您好: </div> <div style="margin-left: 100px"> - <p>您所执行的 ${testName} 启动失败<br/> + <p>您所执行的 ${name} 启动失败<br/> </div> </div> diff --git a/backend/src/main/resources/mail/TestPlanFailedNotification.html b/backend/src/main/resources/mail/TestPlanFailedNotification.html index f6c3ddb8e7..99d37baf5e 100644 --- a/backend/src/main/resources/mail/TestPlanFailedNotification.html +++ b/backend/src/main/resources/mail/TestPlanFailedNotification.html @@ -11,9 +11,9 @@ <p style="margin-left: 60px">您好: </div> <div style="margin-left: 100px"> - <p>您所执行的 ${testName} 测试计划运行失败<br/> + <p>您所执行的 ${name} 测试计划运行失败<br/> 请点击下面链接进入测试报告页面</p> - <a href="${url}/#/${type}/report/view/${id}">${url}/#/${type}/report/view/${id}</a> + <a href="${planShareUrl}">${planShareUrl}</a> </div> </div> diff --git a/backend/src/main/resources/mail/TestPlanSuccessfulNotification.html b/backend/src/main/resources/mail/TestPlanSuccessfulNotification.html index 738087eb37..e0dbfa885f 100644 --- a/backend/src/main/resources/mail/TestPlanSuccessfulNotification.html +++ b/backend/src/main/resources/mail/TestPlanSuccessfulNotification.html @@ -10,9 +10,9 @@ <p style="margin-left: 60px">您好: </div> <div style="margin-left: 100px"> - <p>您所执行的 ${testName} 测试计划运行成功<br/> + <p>您所执行的 ${name} 测试计划运行成功<br/> 请点击下面链接进入测试报告页面</p> - <a href="${url}/#/${type}/report/view/${id}">${url}/#/${type}/report/view/${id}</a> + <a href="${planShareUrl}">${planShareUrl}</a> </div> </body> </html> \ No newline at end of file diff --git a/backend/src/main/resources/mail/track/TestPlanDelete.html b/backend/src/main/resources/mail/track/TestPlanDelete.html index 3529bd3122..dd3144763c 100644 --- a/backend/src/main/resources/mail/track/TestPlanDelete.html +++ b/backend/src/main/resources/mail/track/TestPlanDelete.html @@ -6,7 +6,7 @@ </head> <body> <div> - <p style="text-align: left">${creator}创建的:<br> + <p style="text-align: left">${operator}创建的:<br> ${name}<br> 计划开始时间是:${start}<br> 计划结束时间为:${end}<br> diff --git a/backend/src/main/resources/permission.json b/backend/src/main/resources/permission.json index c51c2f4e80..5d8b7e01d1 100644 --- a/backend/src/main/resources/permission.json +++ b/backend/src/main/resources/permission.json @@ -809,13 +809,25 @@ "id": "PROJECT_REPORT_ANALYSIS:READ", "name": "查看", "resourceId": "PROJECT_REPORT_ANALYSIS", - "license": true + "license": false }, { "id": "PROJECT_REPORT_ANALYSIS:READ+EXPORT", "name": "导出", "resourceId": "PROJECT_REPORT_ANALYSIS", - "license": true + "license": false + }, + { + "id": "PROJECT_REPORT_ANALYSIS:READ+UPDATE", + "name": "保存", + "resourceId": "PROJECT_REPORT_ANALYSIS", + "license": false + }, + { + "id": "PROJECT_REPORT_ANALYSIS:READ+CREATE", + "name": "另存为", + "resourceId": "PROJECT_REPORT_ANALYSIS", + "license": false } ], "resource": [ @@ -957,8 +969,7 @@ }, { "id": "PROJECT_REPORT_ANALYSIS", - "name": "报表", - "license": true + "name": "报表" } ] } \ No newline at end of file diff --git a/frontend/src/business/components/api/automation/report/ApiReportDetail.vue b/frontend/src/business/components/api/automation/report/ApiReportDetail.vue index 928f587b43..c935d0da25 100644 --- a/frontend/src/business/components/api/automation/report/ApiReportDetail.vue +++ b/frontend/src/business/components/api/automation/report/ApiReportDetail.vue @@ -157,16 +157,15 @@ export default { label: nodeArray[i], value: item, }; - - if (i !== nodeArray.length) { + if (i !== (nodeArray.length -1)) { node.children = []; - } - if(item.subRequestResults && item.subRequestResults.length > 0){ - let itemChildren = this.deepFormatTreeNode(item.subRequestResults); - node.children = itemChildren; - - if (node.label.indexOf("UUID=")) { - node.label = node.label.split("UUID=")[0]; + }else { + if(item.subRequestResults && item.subRequestResults.length > 0){ + let itemChildren = this.deepFormatTreeNode(item.subRequestResults); + node.children = itemChildren; + if (node.label.indexOf("UUID=")) { + node.label = node.label.split("UUID=")[0]; + } } } if (children.length === 0) { @@ -225,10 +224,11 @@ export default { }, deepFormatTreeNode(array) { - let children = []; + let returnChildren = []; array.map((item) => { - let key = item.name; - let nodeArray = key.split('^@~@^'); + let children = []; + let key = item.name.split('^@~@^')[0]; + let nodeArray = key.split('<->'); //运行场景中如果连续将1个场景引入多次,会出现运行结果合并的情况。 //为了解决这种问题,在转hashTree的时候给场景放了个新ID,前台加载解析的时候也要做处理 let scenarioId = ""; @@ -242,75 +242,22 @@ export default { } } // 循环构建子节点 - for (let i = 0; i < nodeArray.length; i++) { - if (!nodeArray[i]) { - continue; - } - let node = { - label: nodeArray[i], - value: item, - }; - if (i !== nodeArray.length) { - node.children = []; - } - if(item.subRequestResults && item.subRequestResults.length > 0){ - let itemChildren = this.deepFormatTreeNode(item.subRequestResults); - node.children = itemChildren; - } - if (children.length === 0) { - children.push(node); - } - - let isExist = false; - for (let j in children) { - if (children[j].label === node.label) { - - let idIsPath = true; - //判断ID是否匹配 目前发现问题的只有重复场景,而重复场景是在第二个节点开始合并的。所以这里暂时只判断第二个场景问题。 - //如果出现了其他问题,则需要检查其他问题的数据结构。暂时采用具体问题具体分析的策略 - if (i === nodeArray.length - 2) { - idIsPath = false; - let childId = ""; - let childName = ""; - if (children[j].value && children[j].value.scenario) { - let scenarioArr = JSON.parse(children[j].value.scenario); - if (scenarioArr.length > 1) { - let childArr = scenarioArr[0].split("_"); - childId = childArr[0]; - if (childArr.length > 1) { - childName = childArr[1]; - } - } - } - if (scenarioId === "") { - idIsPath = true; - } else if (scenarioId === childId) { - idIsPath = true; - } else if (scenarioName !== childName) { - //如果两个名字不匹配则默认通过,不匹配ID - idIsPath = true; - } - } - if (idIsPath) { - if (i !== nodeArray.length - 1 && !children[j].children) { - children[j].children = []; - } - children = (i === nodeArray.length - 1 ? children : children[j].children); - isExist = true; - break; - } - } - } - if (!isExist) { - children.push(node); - if (i !== nodeArray.length - 1 && !children[children.length - 1].children) { - children[children.length - 1].children = []; - } - children = (i === nodeArray.length - 1 ? children : children[children.length - 1].children); - } + let node = { + label: nodeArray[0], + value: item, + children: [] + }; + if(item.subRequestResults && item.subRequestResults.length > 0){ + let itemChildren = this.deepFormatTreeNode(item.subRequestResults); + node.children = itemChildren; } + children.push(node); + children.forEach(itemNode => { + returnChildren.push(itemNode); + }); + }); - return children; + return returnChildren; }, recursiveSorting(arr) { for (let i in arr) { @@ -341,16 +288,23 @@ export default { this.buildReport(); } else if (this.isShare) { getShareScenarioReport(this.shareId, this.reportId, (data) => { - this.report = data || {}; - this.buildReport(); + this.handleGetScenarioReport(data); }); } else { getScenarioReport(this.reportId, (data) => { - this.report = data || {}; - this.buildReport(); + this.handleGetScenarioReport(data); }); } }, + handleGetScenarioReport(data) { + if (data) { + this.report = data; + this.buildReport(); + } else { + this.$emit('invisible'); + this.$warning('报告已删除'); + } + }, buildReport() { if (this.report) { if (this.isNotRunning) { diff --git a/frontend/src/business/components/api/automation/report/SysnApiReportDetail.vue b/frontend/src/business/components/api/automation/report/SysnApiReportDetail.vue index 3961d75367..6ecb3cdfd4 100644 --- a/frontend/src/business/components/api/automation/report/SysnApiReportDetail.vue +++ b/frontend/src/business/components/api/automation/report/SysnApiReportDetail.vue @@ -270,7 +270,7 @@ export default { this.reqTotal++; let key = subItem.resourceId; if (resMap.get(key)) { - if (resMap.get(key).indexOf(subItem) === -1) { + if (resMap.get(key).indexOf(subItem) === -1 && subItem.method !== 'Request') { resMap.get(key).push(subItem); } } else { diff --git a/frontend/src/business/components/api/automation/scenario/ApiScenarioList.vue b/frontend/src/business/components/api/automation/scenario/ApiScenarioList.vue index 92b550de7d..990af5ab3e 100644 --- a/frontend/src/business/components/api/automation/scenario/ApiScenarioList.vue +++ b/frontend/src/business/components/api/automation/scenario/ApiScenarioList.vue @@ -18,6 +18,8 @@ operator-width="200" :enable-order-drag="enableOrderDrag" row-key="id" + :row-order-group-id="condition.projectId" + :row-order-func="editApiScenarioCaseOrder" @refresh="search(projectId)" @callBackSelectAll="callBackSelectAll" @callBackSelect="callBackSelect" @@ -218,7 +220,7 @@ <el-drawer :visible.sync="showReportVisible" :destroy-on-close="true" direction="ltr" :withHeader="true" :modal="false" size="90%"> - <ms-api-report-detail @refresh="search" :infoDb="infoDb" :report-id="showReportId" :currentProjectId="projectId"/> + <ms-api-report-detail @invisible="showReportVisible = false" @refresh="search" :infoDb="infoDb" :report-id="showReportId" :currentProjectId="projectId"/> </el-drawer> <!--测试计划--> <el-drawer :visible.sync="planVisible" :destroy-on-close="true" direction="ltr" :withHeader="false" @@ -244,7 +246,7 @@ import {downloadFile, getCurrentProjectID, getUUID, strMapToObj} from "@/common/ import {API_SCENARIO_CONFIGS} from "@/business/components/common/components/search/search-components"; import {API_SCENARIO_LIST} from "../../../../../common/js/constants"; -import {getCustomTableHeader, getCustomTableWidth, getLastTableSortField, handleRowDrop} from "@/common/js/tableUtils"; +import {getCustomTableHeader, getCustomTableWidth, getLastTableSortField} from "@/common/js/tableUtils"; import {API_SCENARIO_FILTERS} from "@/common/js/table-constants"; import {scenario} from "@/business/components/track/plan/event-bus"; import MsTable from "@/business/components/common/components/table/MsTable"; @@ -253,6 +255,7 @@ import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOpe import {editApiScenarioCaseOrder} from "@/business/components/api/automation/api-automation"; import {TYPE_TO_C} from "@/business/components/api/automation/scenario/Setting"; import axios from "axios"; +import {error} from "@/common/js/message"; export default { name: "MsApiScenarioList", @@ -388,7 +391,7 @@ export default { tip: this.$t('api_test.automation.copy'), icon: "el-icon-document-copy", exec: this.copy, - permissions: ['PROJECT_API_SCENARIO:READ+EDIT'] + permissions: ['PROJECT_API_SCENARIO:READ+COPY'] }, { tip: this.$t('commons.delete'), @@ -551,6 +554,9 @@ export default { isNotRunning() { return "Running" !== this.report.status; }, + editApiScenarioCaseOrder() { + return editApiScenarioCaseOrder; + } }, methods: { getProjectName() { @@ -617,20 +623,6 @@ export default { item.tags = JSON.parse(item.tags); } }); - - this.$nextTick(() => { - handleRowDrop(this.tableData, (param) => { - param.groupId = this.condition.projectId; - editApiScenarioCaseOrder(param); - }); - - if (this.$refs.scenarioTable) { - this.$refs.scenarioTable.clear(); - this.$refs.scenarioTable.doLayout(); - } - - }); - this.$emit('getTrashCase'); }); } @@ -885,6 +877,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.sort(stepArray[i].hashTree); } @@ -1031,6 +1026,9 @@ export default { link.download = "场景JMX文件集.zip"; this.result.loading = false; link.click(); + },error => { + this.result.loading = false; + this.$error("导出JMX文件失败"); }); }, @@ -1042,7 +1040,7 @@ export default { return; } this.result.loading = true; - this.fileDownload("/api/automation/export/jmx", param); + this.fileDownload("/api/automation/export/zip", param); }, getConditions() { return this.condition; diff --git a/frontend/src/business/components/api/automation/scenario/DebugRun.vue b/frontend/src/business/components/api/automation/scenario/DebugRun.vue index 0bc97a02ce..1c0a55fe8e 100644 --- a/frontend/src/business/components/api/automation/scenario/DebugRun.vue +++ b/frontend/src/business/components/api/automation/scenario/DebugRun.vue @@ -39,6 +39,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.sort(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue b/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue index 0fb813de1f..f43b144a9d 100644 --- a/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue +++ b/frontend/src/business/components/api/automation/scenario/EditApiScenario.vue @@ -8,7 +8,7 @@ <el-link type="primary" style="margin-right: 20px" @click="openHis" v-if="path === '/api/automation/update'">{{ $t('operating_log.change_history') }}</el-link> <el-button id="inputDelay" type="primary" size="small" v-prevent-re-click @click="editScenario" - title="ctrl + s" v-permission="['PROJECT_API_SCENARIO:READ+EDIT']"> + title="ctrl + s" v-permission="['PROJECT_API_SCENARIO:READ+EDIT', 'PROJECT_API_SCENARIO:READ+CREATE', 'PROJECT_API_SCENARIO:READ+COPY']"> {{ $t('commons.save') }} </el-button> </div> @@ -132,10 +132,10 @@ :{{ getVariableSize() }} </el-col> <el-col :span="3" class="ms-col-one ms-font"> - <el-checkbox v-model="enableCookieShare">共享cookie</el-checkbox> + <el-checkbox v-model="enableCookieShare"><span style="font-size: 13px;">共享cookie</span></el-checkbox> </el-col> <el-col :span="3" class="ms-col-one ms-font"> - <el-checkbox v-model="onSampleError">{{ $t('commons.failure_continues') }}</el-checkbox> + <el-checkbox v-model="onSampleError"><span style="font-size: 13px;">{{ $t('commons.failure_continues') }}</span></el-checkbox> </el-col> <el-col :span="8"> @@ -146,7 +146,7 @@ :isReadOnly="scenarioDefinition.length < 1" @showPopover="showPopover" :project-list="projectList" ref="envPopover" class="ms-message-right"/> <el-tooltip v-if="!debugLoading" content="Ctrl + R" placement="top"> - <el-dropdown split-button type="primary" @click="runDebug" class="ms-message-right" size="mini" @command="handleCommand"> + <el-dropdown split-button type="primary" @click="runDebug" class="ms-message-right" size="mini" @command="handleCommand" v-permission="['PROJECT_API_SCENARIO:READ+EDIT', 'PROJECT_API_SCENARIO:READ+CREATE']"> {{ $t('api_test.request.debug') }} <el-dropdown-menu slot="dropdown"> <el-dropdown-item>{{ $t('api_test.automation.generate_report') }}</el-dropdown-item> @@ -196,7 +196,7 @@ highlight-current @node-expand="nodeExpand" @node-collapse="nodeCollapse" - :allow-drop="allowDrop" @node-drag-end="allowDrag" @node-click="nodeClick" draggable ref="stepTree"> + :allow-drop="allowDrop" @node-drag-end="allowDrag" @node-click="nodeClick" draggable ref="stepTree" v-if="showHideTree"> <span class="custom-tree-node father" slot-scope="{ node, data}" style="width: 96%"> <!-- 步骤组件--> <ms-component-config @@ -214,6 +214,8 @@ @copyRow="copyRow" @suggestClick="suggestClick" @refReload="refReload" + @runScenario="runDebug" + @stopScenario="stop" @openScenario="openScenario"/> </span> </el-tree> @@ -221,7 +223,7 @@ </el-col> <!-- 按钮列表 --> <el-col :span="3"> - <div @click="fabClick"> + <div @click="fabClick" v-permission="['PROJECT_API_SCENARIO:READ+EDIT', 'PROJECT_API_SCENARIO:READ+CREATE']"> <vue-fab id="fab" mainBtnColor="#783887" size="small" :global-options="globalOptions" :click-auto-close="false" v-outside-click="outsideClick" ref="refFab"> <fab-item @@ -313,6 +315,8 @@ :stepReEnable="stepEnable" :message="message" @openScenario="openScenario" + @runScenario="runDebug" + @stopScenario="stop" ref="maximizeScenario"/> </ms-drawer> <ms-change-history ref="changeHistory"/> @@ -405,6 +409,7 @@ export default { levels: PRIORITY, scenario: {}, loading: false, + showHideTree: true, apiListVisible: false, customizeVisible: false, isBtnHide: false, @@ -453,6 +458,7 @@ export default { stepFilter: new STEP, plugins: [], clearMessage: "", + runScenario: undefined, } }, created() { @@ -570,6 +576,7 @@ export default { this.debugLoading = false; } }); + this.runScenario = undefined; }, clearDebug() { this.reqError = 0; @@ -654,7 +661,9 @@ export default { this.message = getUUID(); if (data.end) { this.removeReport(); + this.runScenario = undefined; this.debugLoading = false; + this.message = "stop"; this.stopDebug = "stop"; } } @@ -774,7 +783,11 @@ export default { this.reqTotalTime = endTime - startTime + 100; } this.debugResult = resMap; - this.sort(); + if (this.runScenario && this.runScenario.hashTree) { + this.sort(this.runScenario.hashTree); + } else { + this.sort(); + } this.reloadDebug = getUUID(); }, removeReport() { @@ -956,6 +969,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (!stepArray[i].projectId) { // 如果自身没有ID并且场景有ID则赋值场景ID,否则赋值当前项目ID stepArray[i].projectId = scenarioProjectId ? scenarioProjectId : this.projectId; @@ -1120,7 +1136,13 @@ export default { this.loading = false }); }, - runDebug() { + showHide() { + this.showHideTree = false + this.$nextTick(() => { + this.showHideTree = true + }); + }, + runDebug(runScenario) { if (this.scenarioDefinition.length < 1) { return; } @@ -1142,22 +1164,32 @@ export default { this.clearMessage = getUUID().substring(0, 8); return; } + let scenario = undefined; + if (runScenario && runScenario.type === 'scenario') { + scenario = runScenario; + this.runScenario = runScenario; + } //调试时不再保存 this.debugData = { - id: this.currentScenario.id, - name: this.currentScenario.name, + id: scenario ? scenario.id : this.currentScenario.id, + name: scenario ? scenario.name : this.currentScenario.name, type: "scenario", - variables: this.currentScenario.variables, + variables: scenario ? scenario.variables : this.currentScenario.variables, referenced: 'Created', - enableCookieShare: this.enableCookieShare, - headers: this.currentScenario.headers, - environmentMap: this.projectEnvMap, - hashTree: this.scenarioDefinition, - onSampleError: this.onSampleError, + enableCookieShare: scenario ? scenario.enableCookieShare : this.enableCookieShare, + headers: scenario ? scenario.headers : this.currentScenario.headers, + environmentMap: scenario && scenario.environmentEnable ? scenario.environmentMap : this.projectEnvMap, + hashTree: scenario ? scenario.hashTree : this.scenarioDefinition, + onSampleError: scenario ? scenario.onSampleError : this.onSampleError, }; + if (scenario && scenario.environmentEnable) { + this.debugData.environmentEnable = scenario.environmentEnable; + this.debugLoading = false; + }else{ + this.debugLoading = true; + } this.reportId = getUUID().substring(0, 8); this.debug = true; - this.debugLoading = true; }) }) } else { @@ -1212,8 +1244,7 @@ export default { } return true; } else if (dropType === "inner" && dropNode.data.referenced !== 'REF' && dropNode.data.referenced !== 'Deleted' - && (this.stepFilter.get(dropNode.data.type) && this.stepFilter.get(dropNode.data.type).indexOf(draggingNode.data.type) != -1) - && !draggingNode.data.disabled) { + && (this.stepFilter.get(dropNode.data.type) && this.stepFilter.get(dropNode.data.type).indexOf(draggingNode.data.type) != -1)) { return true; } return false; @@ -1221,7 +1252,6 @@ export default { allowDrag(draggingNode, dropNode, dropType) { if (dropNode && draggingNode && dropType) { this.sort(); - this.reload(); } }, nodeExpand(data, node) { @@ -1353,6 +1383,9 @@ export default { if (!hashTree[i].clazzName) { hashTree[i].clazzName = TYPE_TO_C.get(hashTree[i].type); } + if (hashTree[i] && hashTree[i].authManager && !hashTree[i].authManager.clazzName) { + hashTree[i].authManager.clazzName = TYPE_TO_C.get(hashTree[i].authManager.type); + } if (hashTree[i].hashTree && hashTree[i].hashTree.length > 0) { this.formatData(hashTree[i].hashTree); } @@ -1406,6 +1439,8 @@ export default { this.debugLoading = false; this.debugVisible = false; this.loading = false; + this.runScenario = undefined; + this.message = "stop"; this.clearMessage = getUUID().substring(0, 8); }, showScenarioParameters() { @@ -1494,7 +1529,7 @@ export default { this.expandedStatus = false; this.expandedNode = []; this.changeNodeStatus(this.scenarioDefinition); - this.reload(); + this.showHide(); }, stepStatus(nodes) { for (let i in nodes) { diff --git a/frontend/src/business/components/api/automation/scenario/MsInputTag.vue b/frontend/src/business/components/api/automation/scenario/MsInputTag.vue index 98a0a1a802..336d12ffde 100644 --- a/frontend/src/business/components/api/automation/scenario/MsInputTag.vue +++ b/frontend/src/business/components/api/automation/scenario/MsInputTag.vue @@ -41,22 +41,26 @@ type: Boolean, default: false }, - size: {type: String, default: "small"} + size: {type: String, default: "small"}, + prop: { + type: String, + default: "tags" + } }, created() { - if (!this.currentScenario.tags) { - this.currentScenario.tags = []; + if (!this.currentScenario[this.prop]) { + this.currentScenario[this.prop] = []; } }, data() { return { newTag: '', - innerTags: this.currentScenario.tags ? [...this.currentScenario.tags] : [] + innerTags: this.currentScenario[this.prop] ? [...this.currentScenario[this.prop]] : [] } }, watch: { innerTags() { - this.currentScenario.tags = this.innerTags; + this.currentScenario[this.prop] = this.innerTags; } }, methods: { diff --git a/frontend/src/business/components/api/automation/scenario/api/RelevanceApiList.vue b/frontend/src/business/components/api/automation/scenario/api/RelevanceApiList.vue index b10802d30b..b42d9f908f 100644 --- a/frontend/src/business/components/api/automation/scenario/api/RelevanceApiList.vue +++ b/frontend/src/business/components/api/automation/scenario/api/RelevanceApiList.vue @@ -283,13 +283,6 @@ } }); this.genProtocalFilter(this.condition.protocol); - this.$nextTick(function () { - if (this.$refs.apitable) { - this.$refs.apitable.doLayout(); - this.$refs.apitable.checkTableRowIsSelect(); - this.$refs.apitable.clear(); - } - }); }); }, diff --git a/frontend/src/business/components/api/automation/scenario/api/RelevanceCaseList.vue b/frontend/src/business/components/api/automation/scenario/api/RelevanceCaseList.vue index 7cd3c850ea..679189f7fa 100644 --- a/frontend/src/business/components/api/automation/scenario/api/RelevanceCaseList.vue +++ b/frontend/src/business/components/api/automation/scenario/api/RelevanceCaseList.vue @@ -221,13 +221,6 @@ export default { item.tags = JSON.parse(item.tags); } }); - this.$nextTick(function () { - if (this.$refs.table) { - this.$refs.table.doLayout(); - this.$refs.table.checkTableRowIsSelect(); - this.$refs.table.clear(); - } - }); }); }, clear() { diff --git a/frontend/src/business/components/api/automation/scenario/common/ApiBaseComponent.vue b/frontend/src/business/components/api/automation/scenario/common/ApiBaseComponent.vue index b1cebd82ff..cc7f6f1384 100644 --- a/frontend/src/business/components/api/automation/scenario/common/ApiBaseComponent.vue +++ b/frontend/src/business/components/api/automation/scenario/common/ApiBaseComponent.vue @@ -26,6 +26,9 @@ </el-tooltip> </span> </slot> + + <slot name="scenarioEnable"/> + </span> <div class="header-right" @click.stop> diff --git a/frontend/src/business/components/api/automation/scenario/component/ApiScenarioComponent.vue b/frontend/src/business/components/api/automation/scenario/component/ApiScenarioComponent.vue index 831c4448a9..f00496b2c9 100644 --- a/frontend/src/business/components/api/automation/scenario/component/ApiScenarioComponent.vue +++ b/frontend/src/business/components/api/automation/scenario/component/ApiScenarioComponent.vue @@ -19,7 +19,6 @@ <el-tag size="mini" class="ms-tag" v-if="scenario.referenced==='Deleted'" type="danger">{{ $t('api_test.automation.reference_deleted') }}</el-tag> <el-tag size="mini" class="ms-tag" v-if="scenario.referenced==='Copy'">{{ $t('commons.copy') }}</el-tag> <el-tag size="mini" class="ms-tag" v-if="scenario.referenced==='REF'">{{ $t('api_test.scenario.reference') }}</el-tag> - <span class="ms-tag">{{ getProjectName(scenario.projectId) }}</span> </template> <template v-slot:debugStepCode> @@ -31,6 +30,23 @@ {{ getCode() }} </span> </template> + <template v-slot:scenarioEnable> + <el-tooltip content="启用场景环境:当前步骤使用场景原始环境配置运行" placement="top"> + <el-checkbox v-model="scenario.environmentEnable" @change="checkEnv">启用场景环境</el-checkbox> + </el-tooltip> + </template> + <template v-slot:button> + <el-tooltip :content="$t('api_test.run')" placement="top" v-if="!scenario.run"> + <el-button :disabled="!scenario.enable" @click="run" icon="el-icon-video-play" style="padding: 5px" class="ms-btn" size="mini" circle/> + </el-tooltip> + <el-tooltip :content="$t('report.stop_btn')" placement="top" :enterable="false" v-else> + <el-button :disabled="!scenario.enable" @click.once="stop" size="mini" style="color:white;padding: 0 0.1px;width: 24px;height: 24px;" class="stop-btn" circle> + <div style="transform: scale(0.66)"> + <span style="margin-left: -4.5px;font-weight: bold;">STOP</span> + </div> + </el-button> + </el-tooltip> + </template> </api-base-component> </template> @@ -66,6 +82,9 @@ export default { }, watch: { message() { + if (this.message === "stop") { + this.scenario.run = false; + } this.reload(); }, }, @@ -121,6 +140,35 @@ export default { }, }, methods: { + run() { + this.scenario.run = true; + this.$emit('runScenario', this.scenario); + }, + stop() { + this.scenario.run = false; + this.$emit('stopScenario'); + this.reload(); + }, + checkEnv() { + this.$post("/api/automation/checkScenarioEnv", {scenarioDefinition: JSON.stringify(this.scenario), projectId: this.projectId}, res => { + if (this.scenario.environmentEnable && !res.data) { + this.scenario.environmentEnable = false; + this.$warning("当前场景没有环境,需要先设置自身环境"); + return; + } + this.setDomain(); + }); + }, + setDomain() { + if (this.scenario.environmentEnable) { + this.$post("/api/automation/setDomain", {definition: JSON.stringify(this.scenario)}, res => { + if (res.data) { + let data = JSON.parse(res.data); + this.scenario.hashTree = data.hashTree; + } + }) + } + }, getCode() { if (this.node && this.node.data.code && this.node.data.debug) { if (this.node.data.code && this.node.data.code === 'error') { @@ -210,10 +258,30 @@ export default { color: #F56C6C; } +.ms-test-running { + color: #6D317C; +} + .ms-req-success { color: #67C23A; } +.ms-btn { + background-color: #409EFF; + color: white; +} + +.ms-btn-flot { + margin: 20px; + float: right; +} + +.stop-btn { + background-color: #E62424; + border-color: #EE6161; + color: white; +} + .ms-step-debug-code { display: inline-block; margin: 0 5px; @@ -224,6 +292,7 @@ export default { white-space: nowrap; width: 60px; } + .ms-test-running { color: #6D317C; } diff --git a/frontend/src/business/components/api/automation/scenario/component/ComponentConfig.vue b/frontend/src/business/components/api/automation/scenario/component/ComponentConfig.vue index 3f13b85e96..45f4c0de9a 100644 --- a/frontend/src/business/components/api/automation/scenario/component/ComponentConfig.vue +++ b/frontend/src/business/components/api/automation/scenario/component/ComponentConfig.vue @@ -4,7 +4,7 @@ <component v-bind:is="component" :isMax="isMax" :show-btn="showBtn" :expandedNode="expandedNode" :scenario="scenario" :controller="scenario" :timer="scenario" :assertions="scenario" :extract="scenario" :jsr223-processor="scenario" :request="scenario" :currentScenario="currentScenario" :currentEnvironmentId="currentEnvironmentId" :node="node" :draggable="draggable" :title="title" :color="titleColor" :background-color="backgroundColor" @suggestClick="suggestClick(node)" :response="response" - @remove="remove" @copyRow="copyRow" @refReload="refReload" @openScenario="openScenario" :project-list="projectList" :env-map="envMap" :message="message"/> + @remove="remove" @runScenario="runScenario" @stopScenario="stopScenario" @copyRow="copyRow" @refReload="refReload" @openScenario="openScenario" :project-list="projectList" :env-map="envMap" :message="message"/> </keep-alive> </div> </template> @@ -184,6 +184,12 @@ export default { }, refReload(data, node) { this.$emit('refReload', data, node); + }, + runScenario(scenario) { + this.$emit('runScenario', scenario); + }, + stopScenario(){ + this.$emit('stopScenario'); } } } diff --git a/frontend/src/business/components/api/automation/scenario/component/ConstantTimer.vue b/frontend/src/business/components/api/automation/scenario/component/ConstantTimer.vue index 2e765bcee9..d0f659471b 100644 --- a/frontend/src/business/components/api/automation/scenario/component/ConstantTimer.vue +++ b/frontend/src/business/components/api/automation/scenario/component/ConstantTimer.vue @@ -19,70 +19,48 @@ </template> <script> - import ApiBaseComponent from "../common/ApiBaseComponent"; +import ApiBaseComponent from "../common/ApiBaseComponent"; - export default { - name: "MsConstantTimer", - components: {ApiBaseComponent}, - props: { - timer: {}, - node: {}, - isMax: { - type: Boolean, - default: false, - }, - showBtn: { - type: Boolean, - default: true, - }, - draggable: { - type: Boolean, - default: false, - }, +export default { + name: "MsConstantTimer", + components: {ApiBaseComponent}, + props: { + timer: {}, + node: {}, + isMax: { + type: Boolean, + default: false, }, - data() { - return {} + showBtn: { + type: Boolean, + default: true, }, - created() { - this.$nextTick(() => { - this.$refs.nameInput.focus(); - }); + draggable: { + type: Boolean, + default: false, + }, + }, + data() { + return {} + }, + created() { + this.$nextTick(() => { + this.$refs.nameInput.focus(); + }); + }, + methods: { + remove() { + this.$emit('remove', this.timer, this.node); + }, + copyRow() { + this.$emit('copyRow', this.timer, this.node); }, - methods: { - remove() { - this.$emit('remove', this.timer, this.node); - }, - copyRow() { - this.$emit('copyRow', this.timer, this.node); - }, - } } +} </script> <style scoped> - .width-100 { - width: 40% - } - - .ms-api-col { - background-color: #F2F9EE; - border-color: #67C23A; - margin-right: 10px; - color: #67C23A; - } - - .ms-title-buttion { - background-color: #F2F9EE; - color: #67C23A; - margin-right: 20px; - } - - /deep/ .el-card__body { - padding: 15px; - } - - .time-input { - width: 30%; - /*margin-left: 20px;*/ - } +.time-input { + width: 30%; +} </style> diff --git a/frontend/src/business/components/api/automation/scenario/maximize/MaximizeScenario.vue b/frontend/src/business/components/api/automation/scenario/maximize/MaximizeScenario.vue index d9df7bb286..187e6504a9 100644 --- a/frontend/src/business/components/api/automation/scenario/maximize/MaximizeScenario.vue +++ b/frontend/src/business/components/api/automation/scenario/maximize/MaximizeScenario.vue @@ -33,7 +33,7 @@ highlight-current @node-expand="nodeExpand" @node-collapse="nodeCollapse" - :allow-drop="allowDrop" @node-drag-end="allowDrag" @node-click="nodeClick" v-if="!loading" draggable> + :allow-drop="allowDrop" @node-drag-end="allowDrag" @node-click="nodeClick" draggable v-if="showHideTree"> <span class="custom-tree-node father" slot-scope="{ node, data}"> <!-- 步骤组件--> <ms-component-config @@ -48,6 +48,8 @@ :env-map="projectEnvMap" :message="message" @remove="remove" @copyRow="copyRow" + @runScenario="runScenario" + @stopScenario="stopScenario" @suggestClick="suggestClick" @refReload="refReload" @openScenario="openScenario"/> </span> @@ -218,6 +220,7 @@ export default { levels: PRIORITY, scenario: {}, loading: false, + showHideTree: true, apiListVisible: false, customizeVisible: false, scenarioVisible: false, @@ -551,8 +554,13 @@ export default { } this.sort(); this.reload(); - } - , + }, + showHide() { + this.showHideTree = false + this.$nextTick(() => { + this.showHideTree = true + }); + }, reload() { this.loading = true this.$nextTick(() => { @@ -585,7 +593,7 @@ export default { this.reportId = getUUID().substring(0, 8); } }); - }else{ + } else { this.errorRefresh(); } }) @@ -642,7 +650,6 @@ export default { allowDrag(draggingNode, dropNode, dropType) { if (dropNode && draggingNode && dropType) { this.sort(); - this.reload(); } }, nodeExpand(data) { @@ -764,7 +771,7 @@ export default { this.debugVisible = true; this.loading = false; }, - errorRefresh(){ + errorRefresh() { this.debugVisible = false; this.loading = false; }, @@ -818,30 +825,12 @@ export default { this.debugResult = result; this.sort() }, - shrinkTreeNode() { - //改变每个节点的状态 - for (let i in this.scenarioDefinition) { - if (i > 30 && this.expandedStatus) { - continue; - } - if (this.scenarioDefinition[i]) { - if (this.expandedStatus) { - this.expandedNode.push(this.scenarioDefinition[i].resourceId); - } - this.scenarioDefinition[i].active = this.expandedStatus; - if (this.scenarioDefinition[i].hashTree && this.scenarioDefinition[i].hashTree.length > 0) { - this.changeNodeStatus(this.scenarioDefinition[i].hashTree); - } - } - } - }, changeNodeStatus(nodes) { for (let i in nodes) { if (nodes[i]) { if (this.expandedStatus) { this.expandedNode.push(nodes[i].resourceId); } - nodes[i].active = this.expandedStatus; if (nodes[i].hashTree != undefined && nodes[i].hashTree.length > 0) { this.changeNodeStatus(nodes[i].hashTree); } @@ -849,28 +838,15 @@ export default { } }, openExpansion() { - if (this.scenarioDefinition && this.scenarioDefinition.length > 30) { - this.$alert(this.$t('api_test.definition.request.step_message'), '', { - confirmButtonText: this.$t('commons.confirm'), - callback: (action) => { - if (action === 'confirm') { - this.expandedNode = []; - this.expandedStatus = true; - this.shrinkTreeNode(); - } - } - }); - } else { - this.expandedNode = []; - this.expandedStatus = true; - this.shrinkTreeNode(); - } + this.expandedNode = []; + this.expandedStatus = true; + this.changeNodeStatus(this.scenarioDefinition); }, closeExpansion() { this.expandedStatus = false; this.expandedNode = []; - this.shrinkTreeNode(); - this.reload(); + this.changeNodeStatus(this.scenarioDefinition); + this.showHide(); }, stepNode() { //改变每个节点的状态 @@ -900,6 +876,12 @@ export default { disableAll() { this.stepEnable = false; this.stepNode(); + }, + runScenario(scenario) { + this.$emit('runScenario', scenario); + }, + stopScenario(){ + this.$emit('stopScenario'); } } } @@ -952,16 +934,6 @@ export default { z-index: 5; } -/deep/ .el-tree-node__content { - height: 100%; - margin-top: 8px; - vertical-align: center; -} - -/deep/ .el-card__body { - padding: 6px 10px; -} - /deep/ .el-drawer__body { overflow: auto; } diff --git a/frontend/src/business/components/api/automation/schedule/ScheduleNotification.vue b/frontend/src/business/components/api/automation/schedule/ScheduleNotification.vue index 7a40da9c3d..3db82c8d81 100644 --- a/frontend/src/business/components/api/automation/schedule/ScheduleNotification.vue +++ b/frontend/src/business/components/api/automation/schedule/ScheduleNotification.vue @@ -168,8 +168,8 @@ export default { ' <p style="margin-left: 60px">您好:\n' + ' </div>\n' + ' <div style="margin-left: 100px">\n' + - ' <p>${testName} 接口测试运行失败<br/>\n' + - ' <p>执行人:${executor}</p>' + + ' <p>${name} 接口测试运行失败<br/>\n' + + ' <p>执行人:${operator}</p>' + ' <p>负责人:${principal}</p>' + ' <p>执行环境:${executionEnvironment}</p>' + ' <p>执行时间:${executionTime}</p>' + @@ -183,8 +183,8 @@ export default { '</body>\n' + '</html>', robotTitle: - "测试【任务通知】:'${testName} ${type}测试运行${status}\n" + - "执行人:${executor}" + "\n" + + "测试'${name} ${type}测试运行${status}\n" + + "执行人:${operator}" + "\n" + "负责人:${principal}" + "\n" + "测试环境为:${executionEnvironment}\n" + "执行时间:${executionTime}\n" + diff --git a/frontend/src/business/components/api/definition/components/ApiVariable.vue b/frontend/src/business/components/api/definition/components/ApiVariable.vue index 02e24adaac..0a34327de3 100644 --- a/frontend/src/business/components/api/definition/components/ApiVariable.vue +++ b/frontend/src/business/components/api/definition/components/ApiVariable.vue @@ -10,7 +10,7 @@ <el-row type="flex" :gutter="20" justify="space-between" align="middle"> <el-col class="kv-checkbox" v-if="isShowEnable"> <el-checkbox v-if="!isDisable(index)" v-model="item.enable" - :disabled="isReadOnly"/> + :disabled="isReadOnly"/> </el-col> <span style="margin-left: 10px" v-else></span> <i class="el-icon-top" style="cursor:pointer" @click="moveTop(index)"/> @@ -24,6 +24,7 @@ @change="typeChange(item)"> <el-option value="text"/> <el-option value="file"/> + <el-option value="json"/> </el-select> </template> </el-input> @@ -90,6 +91,7 @@ <ms-api-variable-advance ref="variableAdvance" :environment="environment" :scenario="scenario" :parameters="parameters" :current-item="currentItem"/> + <ms-api-variable-json ref="variableJson" @callback="callback"/> <api-variable-setting ref="apiVariableSetting"/> @@ -98,235 +100,248 @@ </template> <script> - import {KeyValue, Scenario} from "../model/ApiTestModel"; - import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants"; - import MsApiVariableAdvance from "./ApiVariableAdvance"; - import MsApiBodyFileUpload from "./body/ApiBodyFileUpload"; - import {REQUIRED} from "../model/JsonData"; - import Vue from 'vue'; - import ApiVariableSetting from "@/business/components/api/definition/components/ApiVariableSetting"; +import {KeyValue, Scenario} from "../model/ApiTestModel"; +import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants"; +import MsApiVariableAdvance from "./ApiVariableAdvance"; +import MsApiVariableJson from "./ApiVariableJson"; +import MsApiBodyFileUpload from "./body/ApiBodyFileUpload"; +import {REQUIRED} from "../model/JsonData"; +import Vue from 'vue'; +import ApiVariableSetting from "@/business/components/api/definition/components/ApiVariableSetting"; - export default { - name: "MsApiVariable", - components: {ApiVariableSetting, MsApiBodyFileUpload, MsApiVariableAdvance}, - props: { - keyPlaceholder: String, - valuePlaceholder: String, - description: String, - parameters: Array, - rest: Array, - environment: Object, - scenario: Scenario, - type: { - type: String, - default: '' - }, - isReadOnly: { - type: Boolean, - default: false - }, - isShowEnable: { - type: Boolean, - default: true - }, - suggestions: Array, - withMorSetting: Boolean +export default { + name: "MsApiVariable", + components: {ApiVariableSetting, MsApiBodyFileUpload, MsApiVariableAdvance, MsApiVariableJson}, + props: { + keyPlaceholder: String, + valuePlaceholder: String, + description: String, + parameters: Array, + rest: Array, + environment: Object, + scenario: Scenario, + type: { + type: String, + default: '' }, - data() { - return { - currentItem: null, - requireds: [ - {name: this.$t('commons.selector.required'), id: true}, - {name: this.$t('commons.selector.not_required'), id: false} - ], - isSelectAll: true, - isActive: true + isReadOnly: { + type: Boolean, + default: false + }, + isShowEnable: { + type: Boolean, + default: true + }, + suggestions: Array, + withMorSetting: Boolean + }, + data() { + return { + currentItem: null, + requireds: [ + {name: this.$t('commons.selector.required'), id: true}, + {name: this.$t('commons.selector.not_required'), id: false} + ], + isSelectAll: true, + isActive: true, + } + }, + watch: { + isSelectAll: function (to, from) { + if (from == false && to == true) { + this.selectAll(); + } else if (from == true && to == false) { + this.invertSelect(); } }, - watch: { - isSelectAll: function(to, from) { - if(from == false && to == true) { - this.selectAll(); - } else if(from == true && to == false) { - this.invertSelect(); - } - }, + }, + computed: { + keyText() { + return this.keyPlaceholder || this.$t("api_test.key"); }, - computed: { - keyText() { - return this.keyPlaceholder || this.$t("api_test.key"); - }, - valueText() { - return this.valuePlaceholder || this.$t("api_test.value"); + valueText() { + return this.valuePlaceholder || this.$t("api_test.value"); + } + }, + methods: { + moveBottom(index) { + if (this.parameters.length < 2 || index === this.parameters.length - 2) { + return; } + let thisRow = this.parameters[index]; + let nextRow = this.parameters[index + 1]; + Vue.set(this.parameters, index + 1, thisRow); + Vue.set(this.parameters, index, nextRow) }, - methods: { - moveBottom(index) { - if (this.parameters.length < 2 || index === this.parameters.length - 2) { - return; - } - let thisRow = this.parameters[index]; - let nextRow = this.parameters[index + 1]; - Vue.set(this.parameters, index + 1, thisRow); - Vue.set(this.parameters, index, nextRow) - }, - moveTop(index) { - if (index === 0) { - return; - } - let thisRow = this.parameters[index]; - let lastRow = this.parameters[index - 1]; - Vue.set(this.parameters, index - 1, thisRow); - Vue.set(this.parameters, index, lastRow) + moveTop(index) { + if (index === 0) { + return; + } + let thisRow = this.parameters[index]; + let lastRow = this.parameters[index - 1]; + Vue.set(this.parameters, index - 1, thisRow); + Vue.set(this.parameters, index, lastRow) - }, - remove: function (index) { - // 移除整行输入控件及内容 - this.parameters.splice(index, 1); - this.$emit('change', this.parameters); - }, - change: function () { - let isNeedCreate = true; - let removeIndex = -1; - this.parameters.forEach((item, index) => { - if (!item.name && !item.value) { - // 多余的空行 - if (index !== this.parameters.length - 1) { - removeIndex = index; - } - // 没有空行,需要创建空行 - isNeedCreate = false; + }, + remove: function (index) { + // 移除整行输入控件及内容 + this.parameters.splice(index, 1); + this.$emit('change', this.parameters); + }, + change: function () { + let isNeedCreate = true; + let removeIndex = -1; + this.parameters.forEach((item, index) => { + if (!item.name && !item.value) { + // 多余的空行 + if (index !== this.parameters.length - 1) { + removeIndex = index; } - }); - if (isNeedCreate) { - this.parameters.push(new KeyValue({ - type: 'text', - enable: true, - uuid: this.uuid(), - contentType: 'text/plain' - })); + // 没有空行,需要创建空行 + isNeedCreate = false; } - this.$emit('change', this.parameters); - // TODO 检查key重复 - }, - isDisable: function (index) { - return this.parameters.length - 1 == index; - }, - querySearch(queryString, cb) { - let suggestions = this.suggestions; - let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions; - cb(results); - }, - createFilter(queryString) { - return (restaurant) => { - return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0); - }; - }, - funcSearch(queryString, cb) { - let funcs = MOCKJS_FUNC.concat(JMETER_FUNC); - let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs; - // 调用 callback 返回建议列表的数据 - cb(results); - }, - funcFilter(queryString) { - return (func) => { - return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1); - }; - }, - uuid: function () { - return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1); - }, - advanced(item) { - this.$refs.variableAdvance.open(); - this.currentItem = item; - }, - typeChange(item) { - if (item.type === 'file') { - item.contentType = 'application/octet-stream'; - } else { - item.contentType = 'text/plain'; - } - this.reload(); - }, - selectAll() { - this.parameters.forEach(item => { - item.enable = true; - }); - }, - invertSelect() { - this.parameters.forEach(item => { - item.enable = false; - }); - }, - reload() { - this.isActive = false; - this.$nextTick(() => { - this.isActive = true; - }); - }, - openApiVariableSetting(item) { - this.$refs.apiVariableSetting.open(item); - } - }, - created() { - if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) { + }); + if (isNeedCreate) { this.parameters.push(new KeyValue({ type: 'text', enable: true, - required: true, uuid: this.uuid(), contentType: 'text/plain' })); } + this.$emit('change', this.parameters); + // TODO 检查key重复 + }, + isDisable: function (index) { + return this.parameters.length - 1 == index; + }, + querySearch(queryString, cb) { + let suggestions = this.suggestions; + let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions; + cb(results); + }, + createFilter(queryString) { + return (restaurant) => { + return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0); + }; + }, + funcSearch(queryString, cb) { + let funcs = MOCKJS_FUNC.concat(JMETER_FUNC); + let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs; + // 调用 callback 返回建议列表的数据 + cb(results); + }, + funcFilter(queryString) { + return (func) => { + return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1); + }; + }, + uuid: function () { + return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1); + }, + advanced(item) { + if (item.type === 'json') { + this.$refs.variableJson.open(item); + this.currentItem = item; + } else { + this.$refs.variableAdvance.open(); + this.currentItem = item; + } + + }, + typeChange(item) { + if (item.type === 'file') { + item.contentType = 'application/octet-stream'; + } else if (item.type === 'text') { + item.contentType = 'text/plain'; + } else { + item.contentType = 'application/json' + } + this.reload(); + }, + selectAll() { + this.parameters.forEach(item => { + item.enable = true; + }); + }, + invertSelect() { + this.parameters.forEach(item => { + item.enable = false; + }); + }, + reload() { + this.isActive = false; + this.$nextTick(() => { + this.isActive = true; + }); + }, + openApiVariableSetting(item) { + this.$refs.apiVariableSetting.open(item); + }, + callback(item) { + this.currentItem.value = item; + this.currentItem = null; + } + }, + created() { + if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) { + this.parameters.push(new KeyValue({ + type: 'text', + enable: true, + required: true, + uuid: this.uuid(), + contentType: 'text/plain' + })); } } +} </script> <style scoped> - .kv-description { - font-size: 13px; - } +.kv-description { + font-size: 13px; +} - .kv-row { - margin-top: 10px; - } +.kv-row { + margin-top: 10px; +} - .kv-delete { - width: 60px; - } +.kv-delete { + width: 60px; +} - .kv-select { - width: 50%; - } +.kv-select { + width: 50%; +} - .el-autocomplete { - width: 100%; - } +.el-autocomplete { + width: 100%; +} - .kv-checkbox { - width: 20px; - margin-right: 10px; - } +.kv-checkbox { + width: 20px; + margin-right: 10px; +} - .advanced-item-value >>> .el-dialog__body { - padding: 15px 25px; - } +.advanced-item-value >>> .el-dialog__body { + padding: 15px 25px; +} - .el-row { - margin-bottom: 5px; - } +.el-row { + margin-bottom: 5px; +} - .kv-type { - width: 70px; - } +.kv-type { + width: 70px; +} - .pointer { - cursor: pointer; - color: #1E90FF; - } +.pointer { + cursor: pointer; + color: #1E90FF; +} - .kv-setting { - width: 40px; - padding: 0px !important; - } +.kv-setting { + width: 40px; + padding: 0px !important; +} </style> diff --git a/frontend/src/business/components/api/definition/components/ApiVariableJson.vue b/frontend/src/business/components/api/definition/components/ApiVariableJson.vue new file mode 100644 index 0000000000..c78ee428c5 --- /dev/null +++ b/frontend/src/business/components/api/definition/components/ApiVariableJson.vue @@ -0,0 +1,108 @@ +<template> + + <el-dialog + :visible.sync="dialogVisible" destroy-on-close @close="close"> + <div style="padding: 10px"> + <el-switch active-text="JSON-SCHEMA" v-model="item.jsonType" @change="formatChange" active-value="JSON-SCHEMA"/> + </div> + <div v-if="codeEditActive"> + <ms-json-code-edit + v-if="item.jsonType==='JSON-SCHEMA'" + :body="item" + ref="jsonCodeEdit"/> + <ms-code-edit + v-else + :read-only="isReadOnly" + :data.sync="item.value" + :mode="'json'" + height="400px" + ref="codeEdit"/> + </div> + </el-dialog> +</template> + +<script> + +import MsCodeEdit from "../../../common/components/MsCodeEdit"; +import Convert from "@/business/components/common/json-schema/convert/convert"; +import MsJsonCodeEdit from "../../../common/json-schema/JsonSchemaEditor"; + + +export default { + name: "MsApiVariableJson", + components: {MsJsonCodeEdit, MsCodeEdit}, + props: { + isReadOnly: { + type: Boolean, + default: false + }, + }, + data() { + return { + dialogVisible: false, + jsonSchema: "JSON", + codeEditActive: true, + item: {} + } + }, + watch: { + 'item.value'() { + if (this.item.jsonType !== 'JSON-SCHEMA' && this.item.value) { + try { + const MsConvert = new Convert(); + let data = MsConvert.format(JSON.parse(this.item.value)); + if (this.item.jsonSchema) { + this.item.jsonSchema = this.deepAssign(this.item.jsonSchema, data); + } + } catch (ex) { + this.item.jsonSchema = ""; + } + } + } + }, + + methods: { + open(item) { + this.item.value = item.value; + this.dialogVisible = true; + this.reloadCodeEdit(); + }, + reloadCodeEdit() { + this.codeEditActive = false; + this.$nextTick(() => { + this.codeEditActive = true; + }); + }, + formatChange() { + const MsConvert = new Convert(); + if (this.item.jsonType === 'JSON-SCHEMA') { + if (this.item.value && !this.item.jsonSchema) { + this.item.jsonSchema = MsConvert.format(JSON.parse(this.item.value)); + } + } else { + if (this.item.jsonSchema) { + MsConvert.schemaToJsonStr(this.item.jsonSchema, (result) => { + this.$set(this.item, 'value', result); + this.$emit('callback', result); + }); + } + } + }, + saveAdvanced() { + this.dialogVisible = false; + if (this.item.jsonType === 'JSON-SCHEMA') { + this.item.jsonType = 'JSON'; + this.formatChange(); + } else { + this.$emit('callback', this.item.value); + } + this.item = {}; + this.reloadCodeEdit(); + }, + close(){ + this.saveAdvanced(); + } + }, + +} +</script> diff --git a/frontend/src/business/components/api/definition/components/EditCompleteContainer.vue b/frontend/src/business/components/api/definition/components/EditCompleteContainer.vue index fc1aa4fef7..edf9163e46 100644 --- a/frontend/src/business/components/api/definition/components/EditCompleteContainer.vue +++ b/frontend/src/business/components/api/definition/components/EditCompleteContainer.vue @@ -109,6 +109,8 @@ import TcpMockConfig from "@/business/components/api/definition/components/mock/ import ApiCaseSimpleList from "./list/ApiCaseSimpleList"; import MsApiCaseList from "./case/ApiCaseList"; import {getUUID} from "@/common/js/utils"; +import {Body} from "@/business/components/api/definition/model/ApiTestModel"; +import {TYPE_TO_C} from "@/business/components/api/automation/scenario/Setting"; export default { name: "EditCompleteContainer", @@ -158,6 +160,7 @@ export default { if (this.currentApi.id && (this.currentProtocol === "HTTP" || this.currentProtocol === "TCP")) { this.mockSetting(); } + this.formatApi(); }, watch: { showMock() { @@ -174,10 +177,45 @@ export default { } }, methods: { + sort(stepArray) { + if (stepArray) { + for (let i in stepArray) { + if (!stepArray[i].clazzName) { + stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); + } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } + if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { + this.sort(stepArray[i].hashTree); + } + } + } + }, + formatApi() { + if (this.currentApi.response != null && this.currentApi.response != 'null' && this.currentApi.response != undefined) { + if (Object.prototype.toString.call(this.currentApi.response).match(/\[object (\w+)\]/)[1].toLowerCase() !== 'object') { + this.currentApi.response = JSON.parse(this.currentApi.response); + } + } + if (this.currentApi.request != null && this.currentApi.request != 'null' && this.currentApi.request != undefined) { + if (Object.prototype.toString.call(this.currentApi.request).match(/\[object (\w+)\]/)[1].toLowerCase() !== 'object') { + this.currentApi.request = JSON.parse(this.currentApi.request); + } + } + if (!this.currentApi.request.hashTree) { + this.currentApi.request.hashTree = []; + } + if (this.currentApi.request.body && !this.currentApi.request.body.binary) { + this.currentApi.request.body.binary = []; + } + this.currentApi.request.clazzName = TYPE_TO_C.get(this.currentApi.request.type); + this.sort(this.currentApi.request.hashTree); + }, mockSetting() { let mockParam = {}; mockParam.projectId = this.projectId; - if(this.currentApi.id){ + if (this.currentApi.id) { mockParam.apiId = this.currentApi.id; this.$post('/mockConfig/genMockConfig', mockParam, response => { let mockConfig = response.data; @@ -248,6 +286,7 @@ export default { this.api = this.$store.state.currentApiCase.api; this.$refs.caseList.open(); } + this.$store.state.currentApiCase = {case: true}; } else if (tabType === "test") { this.showApiList = false; this.showTestCaseList = false; diff --git a/frontend/src/business/components/api/definition/components/Run.vue b/frontend/src/business/components/api/definition/components/Run.vue index 3866d4c9bf..88b08e7a60 100644 --- a/frontend/src/business/components/api/definition/components/Run.vue +++ b/frontend/src/business/components/api/definition/components/Run.vue @@ -64,6 +64,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.sort(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/api/definition/components/assertion/ApiAssertions.vue b/frontend/src/business/components/api/definition/components/assertion/ApiAssertions.vue index 4e32617d4b..2d6111c3fc 100644 --- a/frontend/src/business/components/api/definition/components/assertion/ApiAssertions.vue +++ b/frontend/src/business/components/api/definition/components/assertion/ApiAssertions.vue @@ -58,135 +58,138 @@ </template> <script> - import MsApiAssertionText from "./ApiAssertionText"; - import MsApiAssertionRegex from "./ApiAssertionRegex"; - import MsApiAssertionDuration from "./ApiAssertionDuration"; - import {ASSERTION_TYPE, JSONPath} from "../../model/ApiTestModel"; - import MsApiAssertionsEdit from "./ApiAssertionsEdit"; - import MsApiAssertionJsonPath from "./ApiAssertionJsonPath"; - import MsApiAssertionJsr223 from "./ApiAssertionJsr223"; - import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList"; - import MsApiAssertionXPath2 from "./ApiAssertionXPath2"; - import {getUUID} from "@/common/js/utils"; - import ApiJsonPathSuggestButton from "./ApiJsonPathSuggestButton"; - import MsApiJsonpathSuggest from "./ApiJsonpathSuggest"; - import ApiBaseComponent from "../../../automation/scenario/common/ApiBaseComponent"; +import MsApiAssertionText from "./ApiAssertionText"; +import MsApiAssertionRegex from "./ApiAssertionRegex"; +import MsApiAssertionDuration from "./ApiAssertionDuration"; +import {ASSERTION_TYPE, JSONPath} from "../../model/ApiTestModel"; +import MsApiAssertionsEdit from "./ApiAssertionsEdit"; +import MsApiAssertionJsonPath from "./ApiAssertionJsonPath"; +import MsApiAssertionJsr223 from "./ApiAssertionJsr223"; +import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList"; +import MsApiAssertionXPath2 from "./ApiAssertionXPath2"; +import {getUUID} from "@/common/js/utils"; +import ApiJsonPathSuggestButton from "./ApiJsonPathSuggestButton"; +import MsApiJsonpathSuggest from "./ApiJsonpathSuggest"; +import ApiBaseComponent from "../../../automation/scenario/common/ApiBaseComponent"; - export default { - name: "MsApiAssertions", - components: { - ApiBaseComponent, - MsApiJsonpathSuggest, - ApiJsonPathSuggestButton, - MsApiAssertionXPath2, - MsApiAssertionJsr223, - MsApiJsonpathSuggestList, - MsApiAssertionJsonPath, - MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText +export default { + name: "MsApiAssertions", + components: { + ApiBaseComponent, + MsApiJsonpathSuggest, + ApiJsonPathSuggestButton, + MsApiAssertionXPath2, + MsApiAssertionJsr223, + MsApiJsonpathSuggestList, + MsApiAssertionJsonPath, + MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText + }, + props: { + draggable: { + type: Boolean, + default: false, }, - props: { - draggable: { - type: Boolean, - default: false, - }, - isMax: { - type: Boolean, - default: false, - }, - showBtn: { - type: Boolean, - default: true, - }, - assertions: {}, - node: {}, - request: {}, - response: {}, - customizeStyle: { - type: String, - default: "margin-top: 10px" - }, - isReadOnly: { - type: Boolean, - default: false - } + isMax: { + type: Boolean, + default: false, }, - data() { - return { - options: ASSERTION_TYPE, - time: "", - type: "", - loading: false, - reloadData: "", - } + showBtn: { + type: Boolean, + default: true, }, - methods: { - after() { - this.type = ""; - this.reloadData = getUUID().substring(0, 8); - this.reload(); - }, - copyRow() { - this.$emit('copyRow', this.assertions, this.node); - }, - suggestJsonOpen() { - this.$emit('suggestClick'); - this.$nextTick(() => { - if (!this.response || !this.response.responseResult || !this.response.responseResult.body) { - this.$message(this.$t('api_test.request.assertions.debug_first')); - return; - } - this.$refs.jsonpathSuggest.open(this.response.responseResult.body); - }) - }, - reload() { - this.loading = true - this.$nextTick(() => { - this.loading = false - }) - }, - active() { - this.assertions.active = !this.assertions.active; - this.reload(); - }, - remove() { - this.$emit('remove', this.assertions, this.node); - }, - addJsonPathSuggest(data) { - let jsonItem = new JSONPath(); - jsonItem.expression = data.path; - jsonItem.expect = data.value; - jsonItem.setJSONPathDescription(); - let expect = jsonItem.expect.replaceAll('\\', "\\\\").replaceAll('(', "\\(").replaceAll(')', "\\)") + assertions: {}, + node: {}, + request: {}, + response: {}, + customizeStyle: { + type: String, + default: "margin-top: 10px" + }, + isReadOnly: { + type: Boolean, + default: false + } + }, + data() { + return { + options: ASSERTION_TYPE, + time: "", + type: "", + loading: false, + reloadData: "", + } + }, + methods: { + after() { + this.type = ""; + this.reloadData = getUUID().substring(0, 8); + this.reload(); + }, + copyRow() { + this.$emit('copyRow', this.assertions, this.node); + }, + suggestJsonOpen() { + this.$emit('suggestClick'); + this.$nextTick(() => { + if (!this.response || !this.response.responseResult || !this.response.responseResult.body) { + this.$message(this.$t('api_test.request.assertions.debug_first')); + return; + } + this.$refs.jsonpathSuggest.open(this.response.responseResult.body); + }) + }, + reload() { + this.loading = true + this.$nextTick(() => { + this.loading = false + }) + }, + active() { + this.assertions.active = !this.assertions.active; + this.reload(); + }, + remove() { + this.$emit('remove', this.assertions, this.node); + }, + addJsonPathSuggest(data) { + let jsonItem = new JSONPath(); + jsonItem.expression = data.path; + jsonItem.expect = data.value; + jsonItem.setJSONPathDescription(); + let expect = jsonItem.expect; + if (expect) { + expect = expect.replaceAll('\\', "\\\\").replaceAll('(', "\\(").replaceAll(')', "\\)") .replaceAll('+', "\\+").replaceAll('.', "\\.").replaceAll('[', "\\[").replaceAll(']', "\\]") .replaceAll('?', "\\?").replaceAll('/', "\\/").replaceAll('*', "\\*") .replaceAll('^', "\\^").replaceAll('{', "\\{").replaceAll('}', "\\}").replaceAll('$', "\\$"); - jsonItem.expect = expect; - this.assertions.jsonPath.push(jsonItem); - }, - clearJson() { - this.assertions.jsonPath = []; } + jsonItem.expect = expect; + this.assertions.jsonPath.push(jsonItem); + }, + clearJson() { + this.assertions.jsonPath = []; } } +} </script> <style scoped> - .assertion-item { - width: 100%; - } +.assertion-item { + width: 100%; +} - .assertion-add { - padding: 10px; - margin: 5px 0; - border-radius: 5px; - border: #DCDFE6 solid 1px; - } +.assertion-add { + padding: 10px; + margin: 5px 0; + border-radius: 5px; + border: #DCDFE6 solid 1px; +} - .icon.is-active { - transform: rotate(90deg); - } +.icon.is-active { + transform: rotate(90deg); +} - /deep/ .el-card__body { - padding: 6px 10px; - } +/deep/ .el-card__body { + padding: 6px 10px; +} </style> diff --git a/frontend/src/business/components/api/definition/components/basis/AddBasisApi.vue b/frontend/src/business/components/api/definition/components/basis/AddBasisApi.vue index 6abaf3d750..14791020af 100644 --- a/frontend/src/business/components/api/definition/components/basis/AddBasisApi.vue +++ b/frontend/src/business/components/api/definition/components/basis/AddBasisApi.vue @@ -108,6 +108,9 @@ if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.compatibleHistory(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/api/definition/components/body/ApiBody.vue b/frontend/src/business/components/api/definition/components/body/ApiBody.vue index 2d86e275bf..bd00d8731c 100644 --- a/frontend/src/business/components/api/definition/components/body/ApiBody.vue +++ b/frontend/src/business/components/api/definition/components/body/ApiBody.vue @@ -208,6 +208,7 @@ export default { }, formatChange() { const MsConvert = new Convert(); + if (this.body.format === 'JSON-SCHEMA') { if (this.body.raw && !this.body.jsonSchema) { this.body.jsonSchema = MsConvert.format(JSON.parse(this.body.raw)); @@ -266,24 +267,48 @@ export default { batchAdd() { this.$refs.batchAddParameter.open(); }, + format(array, obj) { + if (array) { + let isAdd = true; + for (let i in array) { + let item = array[i]; + if (item.name === obj.name) { + item.value = obj.value; + isAdd = false; + } + } + if (isAdd) { + this.body.kvs.unshift(obj); + } + } + }, batchSave(data) { if (data) { let params = data.split("\n"); let keyValues = []; params.forEach(item => { - let line = item.split(/,|,/); + let line = []; + line[0] = item.substring(0,item.indexOf(":")); + line[1] = item.substring(item.indexOf(":")+1,item.length); let required = false; - if (line[1] === '必填' || line[1] === 'Required' || line[1] === 'true') { - required = true; - } - keyValues.push(new KeyValue({name: line[0], required: required, value: line[2], description: line[3], type: "text", valid: false, file: false, encode: true, enable: true, contentType: "text/plain"})); + keyValues.unshift(new KeyValue({ + name: line[0], + required: required, + value: line[1], + description: line[2], + type: "text", + valid: false, + file: false, + encode: true, + enable: true, + contentType: "text/plain" + })); }) keyValues.forEach(item => { - this.body.kvs.unshift(item); + this.format(this.body.kvs, item); }) } }, - }, created() { if (!this.body.type) { @@ -296,6 +321,7 @@ export default { } }); } + } } </script> diff --git a/frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue b/frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue index 33406de5b8..de4714ff10 100644 --- a/frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue +++ b/frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue @@ -24,7 +24,7 @@ <span @click.stop> <i class="icon el-icon-arrow-right" :class="{'is-active': apiCase.active}" @click="active(apiCase)"/> <el-input v-if="!apiCase.id || isShowInput" size="small" v-model="apiCase.name" :name="index" :key="index" - class="ms-api-header-select" style="width: 180px" readonly="hasPermission('PROJECT_API_DEFINITION:READ+EDIT_CASE')" + class="ms-api-header-select" style="width: 180px" :readonly="!hasPermission('PROJECT_API_DEFINITION:READ+EDIT_CASE')" @blur="saveTestCase(apiCase,true)" :placeholder="$t('commons.input_name')" ref="nameEdit"/> <span v-else> <el-tooltip :content="apiCase.id ? apiCase.name : ''" placement="top"> @@ -178,6 +178,7 @@ const esbDefinitionResponse = (requireComponent != null && requireComponent.keys import {API_METHOD_COLOUR} from "../../model/JsonData"; import MsChangeHistory from "../../../../history/ChangeHistory"; import {TYPE_TO_C} from "@/business/components/api/automation/scenario/Setting"; +import {hasPermission} from '@/common/js/utils'; export default { name: "ApiCaseItem", @@ -282,6 +283,7 @@ export default { } }, methods: { + hasPermission, openHis(row) { this.$refs.changeHistory.open(row.id); }, @@ -395,6 +397,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.sort(stepArray[i].hashTree); } @@ -416,6 +421,7 @@ export default { if (tmp.id) { url = "/api/testcase/update"; } else { + tmp.request.id = getUUID(); tmp.id = tmp.request.id; tmp.request.path = this.api.path; if (tmp.request.protocol != "dubbo://" && tmp.request.protocol != "DUBBO") { @@ -435,6 +441,9 @@ export default { } if (tmp.request) { tmp.request.clazzName = TYPE_TO_C.get(tmp.request.type); + if (tmp.request.authManager) { + tmp.request.authManager.clazzName = TYPE_TO_C.get(tmp.request.authManager.type); + } this.sort(tmp.request.hashTree); } this.result = this.$fileUpload(url, null, bodyFiles, tmp, (response) => { diff --git a/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue b/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue index d71e1ed2bd..28c2798eee 100644 --- a/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue +++ b/frontend/src/business/components/api/definition/components/case/ApiCaseList.vue @@ -58,22 +58,19 @@ <script> import ApiCaseHeader from "./ApiCaseHeader"; -import ApiCaseItem from "./ApiCaseItem"; -import MsRun from "../Run"; import {getCurrentProjectID, getUUID} from "@/common/js/utils"; import MsDrawer from "../../../../common/components/MsDrawer"; import {CASE_ORDER, CASE_PRIORITY, DUBBO_METHOD, REQ_METHOD, SQL_METHOD, TCP_METHOD} from "../../model/JsonData"; import {API_CASE_CONFIGS} from "@/business/components/common/components/search/search-components"; -import MsBatchEdit from "../basis/BatchEdit"; export default { name: 'ApiCaseList', components: { MsDrawer, - MsRun, + MsRun: () => import("../Run"), ApiCaseHeader, - ApiCaseItem, - MsBatchEdit, + ApiCaseItem: () => import("./ApiCaseItem"), + MsBatchEdit: () => import("../basis/BatchEdit"), MsTaskCenter: () => import("../../../../task/TaskCenter"), }, props: { @@ -258,6 +255,7 @@ export default { this.runResult = {testId: getUUID()}; this.$refs.apiCaseItem.runLoading = false; this.$success(this.$t('organization.integration.successful_operation')); + this.$store.state.currentApiCase = {refresh: true}; this.getApiTest(); }, errorRefresh() { @@ -302,6 +300,10 @@ export default { const index = this.runData.findIndex(d => d.name === apiCase.id); if (index !== -1) { apiCase.active = true; + }else { + if(this.condition.id && this.condition.id != ""){ + apiCase.active = true; + } } }); this.apiCaseList = data; diff --git a/frontend/src/business/components/api/definition/components/debug/DebugDubboPage.vue b/frontend/src/business/components/api/definition/components/debug/DebugDubboPage.vue index 076e6fee38..5c829bde94 100644 --- a/frontend/src/business/components/api/definition/components/debug/DebugDubboPage.vue +++ b/frontend/src/business/components/api/definition/components/debug/DebugDubboPage.vue @@ -165,6 +165,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.compatibleHistory(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/api/definition/components/debug/DebugHttpPage.vue b/frontend/src/business/components/api/definition/components/debug/DebugHttpPage.vue index 7432d482c1..15640afd12 100644 --- a/frontend/src/business/components/api/definition/components/debug/DebugHttpPage.vue +++ b/frontend/src/business/components/api/definition/components/debug/DebugHttpPage.vue @@ -217,6 +217,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.compatibleHistory(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/api/definition/components/document/ApiDocumentAnchor.vue b/frontend/src/business/components/api/definition/components/document/ApiDocumentAnchor.vue index 5aaec3d253..ac581cb2dd 100644 --- a/frontend/src/business/components/api/definition/components/document/ApiDocumentAnchor.vue +++ b/frontend/src/business/components/api/definition/components/document/ApiDocumentAnchor.vue @@ -3,31 +3,6 @@ <el-container v-loading="isLoading"> <el-main style="padding-top: 0px;padding-bottom: 0px"> <el-row v-if="sharePage" style="margin-top: 10px"> - <el-select size="small" :placeholder="$t('api_test.definition.document.order')" v-model="apiSearch.orderCondition" style="float: right;width: 180px;margin-right: 5px" - class="ms-api-header-select" @change="initApiDocSimpleList" clearable> - <el-option key="createTimeDesc" :label="$t('api_test.definition.document.create_time_sort')" value="createTimeDesc" /> - <el-option key="editTimeAsc" :label="$t('api_test.definition.document.edit_time_positive_sequence')" value="editTimeAsc"/> - <el-option key="editTimeDesc" :label="$t('api_test.definition.document.edit_time_Reverse_order')" value="editTimeDesc"/> - </el-select> - - <el-select size="small" :placeholder="$t('api_test.definition.document.request_method')" v-model="apiSearch.type" style="float: right;width: 180px;margin-right: 5px" - class="ms-api-header-select" @change="initApiDocSimpleList" clearable> - <el-option key="ALL" :label="$t('api_test.definition.document.data_set.all')" value="ALL"/> - <el-option key="GET" :label="'GET '+$t('api_test.definition.document.request_interface')" value="GET"/> - <el-option key="POST" :label="'POST '+$t('api_test.definition.document.request_interface')" value="POST"/> - <el-option key="PUT" :label="'PUT '+$t('api_test.definition.document.request_interface')" value="PUT"/> - <el-option key="DELETE" :label="'DELETE '+$t('api_test.definition.document.request_interface')" value="DELETE"/> - <el-option key="PATCH" :label="'PATCH '+$t('api_test.definition.document.request_interface')" value="PATCH"/> - <el-option key="OPTIONS" :label="'OPTIONS '+$t('api_test.definition.document.request_interface')" value="OPTIONS"/> - <el-option key="HEAD" :label="'HEAD '+$t('api_test.definition.document.request_interface')" value="HEAD"/> - <el-option key="CONNECT" :label="'CONNECT '+$t('api_test.definition.document.request_interface')" value="CONNECT"/> - </el-select> - <el-input :placeholder="$t('api_test.definition.document.search_by_api_name')" @blur="initApiDocSimpleList()" style="float: right;width: 180px;margin-right: 5px" size="small" - @keyup.enter.native="initApiDocSimpleList()" v-model="apiSearch.name"/> - <api-document-batch-share v-xpack v-if="showXpackCompnent" @shareApiDocument="shareApiDocument" :project-id="projectId" :share-url="batchShareUrl" style="float: right;margin: 6px;font-size: 17px"/> - </el-row> - <el-row v-else - style="margin-top: 0px;position: fixed;float: right;margin-right: 0px;margin-left: 400px;top: 135px; right: 90px; z-index: 9999"> <el-select size="small" :placeholder="$t('api_test.definition.document.order')" v-model="apiSearch.orderCondition" style="float: right;width: 180px;margin-right: 5px" class="ms-api-header-select" @change="initApiDocSimpleList" clearable> @@ -46,19 +21,63 @@ <el-option key="GET" :label="'GET '+$t('api_test.definition.document.request_interface')" value="GET"/> <el-option key="POST" :label="'POST '+$t('api_test.definition.document.request_interface')" value="POST"/> <el-option key="PUT" :label="'PUT '+$t('api_test.definition.document.request_interface')" value="PUT"/> - <el-option key="DELETE" :label="'DELETE '+$t('api_test.definition.document.request_interface')" value="DELETE"/> - <el-option key="PATCH" :label="'PATCH '+$t('api_test.definition.document.request_interface')" value="PATCH"/> - <el-option key="OPTIONS" :label="'OPTIONS '+$t('api_test.definition.document.request_interface')" value="OPTIONS"/> + <el-option key="DELETE" :label="'DELETE '+$t('api_test.definition.document.request_interface')" + value="DELETE"/> + <el-option key="PATCH" :label="'PATCH '+$t('api_test.definition.document.request_interface')" + value="PATCH"/> + <el-option key="OPTIONS" :label="'OPTIONS '+$t('api_test.definition.document.request_interface')" + value="OPTIONS"/> <el-option key="HEAD" :label="'HEAD '+$t('api_test.definition.document.request_interface')" value="HEAD"/> - <el-option key="CONNECT" :label="'CONNECT '+$t('api_test.definition.document.request_interface')" value="CONNECT"/> + <el-option key="CONNECT" :label="'CONNECT '+$t('api_test.definition.document.request_interface')" + value="CONNECT"/> </el-select> - <el-input :placeholder="$t('api_test.definition.document.search_by_api_name')" @blur="initApiDocSimpleList()" style="float: right;width: 180px;margin-right: 5px" size="small" + <el-input :placeholder="$t('api_test.definition.document.search_by_api_name')" @blur="initApiDocSimpleList()" + style="float: right;width: 180px;margin-right: 5px" size="small" @keyup.enter.native="initApiDocSimpleList()" v-model="apiSearch.name"/> - <api-document-batch-share v-xpack v-if="showXpackCompnent" @shareApiDocument="shareApiDocument" :project-id="projectId" :share-url="batchShareUrl" style="float: right;margin: 6px;font-size: 17px"/> + <api-document-batch-share v-xpack v-if="showXpackCompnent" @shareApiDocument="shareApiDocument" + :project-id="projectId" :share-url="batchShareUrl" + style="float: right;margin: 6px;font-size: 17px"/> + </el-row> + <el-row v-else + style="margin-top: 0px;position: fixed;float: right;margin-right: 0px;margin-left: 400px;top: 135px; right: 90px;"> + <el-select size="small" :placeholder="$t('api_test.definition.document.order')" + v-model="apiSearch.orderCondition" style="float: right;width: 180px;margin-right: 5px" + class="ms-api-header-select" @change="initApiDocSimpleList" clearable> + <el-option key="createTimeDesc" :label="$t('api_test.definition.document.create_time_sort')" + value="createTimeDesc"/> + <el-option key="editTimeAsc" :label="$t('api_test.definition.document.edit_time_positive_sequence')" + value="editTimeAsc"/> + <el-option key="editTimeDesc" :label="$t('api_test.definition.document.edit_time_Reverse_order')" + value="editTimeDesc"/> + </el-select> + + <el-select size="small" :placeholder="$t('api_test.definition.document.request_method')" + v-model="apiSearch.type" style="float: right;width: 180px;margin-right: 5px" + class="ms-api-header-select" @change="initApiDocSimpleList" clearable> + <el-option key="ALL" :label="$t('api_test.definition.document.data_set.all')" value="ALL"/> + <el-option key="GET" :label="'GET '+$t('api_test.definition.document.request_interface')" value="GET"/> + <el-option key="POST" :label="'POST '+$t('api_test.definition.document.request_interface')" value="POST"/> + <el-option key="PUT" :label="'PUT '+$t('api_test.definition.document.request_interface')" value="PUT"/> + <el-option key="DELETE" :label="'DELETE '+$t('api_test.definition.document.request_interface')" + value="DELETE"/> + <el-option key="PATCH" :label="'PATCH '+$t('api_test.definition.document.request_interface')" + value="PATCH"/> + <el-option key="OPTIONS" :label="'OPTIONS '+$t('api_test.definition.document.request_interface')" + value="OPTIONS"/> + <el-option key="HEAD" :label="'HEAD '+$t('api_test.definition.document.request_interface')" value="HEAD"/> + <el-option key="CONNECT" :label="'CONNECT '+$t('api_test.definition.document.request_interface')" + value="CONNECT"/> + </el-select> + <el-input :placeholder="$t('api_test.definition.document.search_by_api_name')" @blur="initApiDocSimpleList()" + style="float: right;width: 180px;margin-right: 5px" size="small" + @keyup.enter.native="initApiDocSimpleList()" v-model="apiSearch.name"/> + <api-document-batch-share v-xpack v-if="showXpackCompnent" @shareApiDocument="shareApiDocument" + :project-id="projectId" :share-url="batchShareUrl" + style="float: right;margin: 6px;font-size: 17px"/> </el-row> <el-divider></el-divider> - <div ref="apiDocInfoDiv" @scroll="handleScroll" > + <div ref="apiDocInfoDiv" @scroll="handleScroll"> <div v-for="(apiInfo) in apiShowArray" :key="apiInfo.id" ref="apiDocInfoDivItem"> <div style="font-size: 17px"> <el-popover @@ -66,10 +85,11 @@ placement="right" width="260" @show="shareApiDocument('false')"> - <p>{{shareUrl}}</p> + <p>{{ shareUrl }}</p> <div style="text-align: right; margin: 0"> <el-button type="primary" size="mini" - v-clipboard:copy="shareUrl">{{ $t("commons.copy") }}</el-button> + v-clipboard:copy="shareUrl">{{ $t("commons.copy") }} + </el-button> </div> <i class="el-icon-share" slot="reference" style="margin-right: 10px;cursor: pointer"></i> </el-popover> @@ -131,10 +151,6 @@ :label="$t('api_test.definition.document.table_coloum.name')" min-width="120px" show-overflow-tooltip/> -<!-- <el-table-column prop="isEnable"--> -<!-- :label="$t('api_test.definition.document.table_coloum.is_required')"--> -<!-- min-width="80px"--> -<!-- show-overflow-tooltip/>--> <el-table-column prop="required" :label="$t('api_test.definition.document.table_coloum.is_required')" :formatter="formatBoolean" @@ -187,12 +203,11 @@ show-overflow-tooltip/> </el-table> <div v-else-if="apiInfo.requestBodyParamType == 'JSON-SCHEMA'" style="margin-left: 10px"> - <ms-json-code-edit :body="apiInfo.jsonSchemaBody" ref="jsonCodeEdit"/> + <ms-json-code-edit :show-preview="false" :body="apiInfo.jsonSchemaBody" ref="jsonCodeEdit"/> + </div> + <div v-else-if="formatRowDataToJsonSchema(apiInfo,'request') " style="margin-left: 10px"> + <ms-json-code-edit :show-preview="false" :body="apiInfo.requestJsonSchema" ref="jsonCodeEdit"/> </div> -<!-- <div v-else-if="apiInfo.requestBodyParamType == 'XML'" style="margin-left: 10px">--> -<!-- <ms-json-code-edit :body="apiInfo.jsonSchemaBody" ref="jsonCodeEdit"/>--> -<!-- <editor v-model="formatData" :lang="mode" @init="editorInit" :theme="theme" :height="height"/>--> -<!-- </div>--> <div v-else class="showDataDiv"> <br/> <p style="margin: 0px 20px;" @@ -274,7 +289,10 @@ show-overflow-tooltip/> </el-table> <div v-else-if="apiInfo.responseBodyParamType == 'JSON-SCHEMA'" style="margin-left: 10px"> - <ms-json-code-edit :body="apiInfo.jsonSchemaResponseBody" ref="jsonCodeEdit"/> + <ms-json-code-edit :show-preview="false" :body="apiInfo.jsonSchemaResponseBody" ref="jsonCodeEdit"/> + </div> + <div v-else-if="formatRowDataToJsonSchema(apiInfo,'response') " style="margin-left: 10px"> + <ms-json-code-edit :show-preview="false" :body="apiInfo.responseJsonSchema" ref="jsonCodeEdit"/> </div> <div v-else class="showDataDiv"> <br/> @@ -305,7 +323,7 @@ </el-main> <!-- 右侧列表 --> <el-aside width="200px" style="margin-top: 30px;"> - <div ref="apiDocList" > + <div ref="apiDocList"> <el-steps style="height: 40%" direction="vertical" :active="apiStepIndex"> <el-step v-for="(apiInfo) in apiInfoArray" :key="apiInfo.id" @click.native="clickStep(apiInfo.id)"> <el-link slot="title">{{ apiInfo.name }}</el-link> @@ -326,9 +344,10 @@ import {calculate} from "@/business/components/api/definition/model/ApiTestModel import MsJsonCodeEdit from "@/business/components/common/json-schema/JsonSchemaEditor"; import Api from "@/business/components/api/router"; import {generateApiDocumentShareInfo} from "@/network/share"; +import Convert from "@/business/components/common/json-schema/convert/convert"; const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/); -const apiDocumentBatchShare = (requireComponent!=null&&requireComponent.keys().length) > 0 ? requireComponent("./share/ApiDocumentBatchShare.vue") : {}; +const apiDocumentBatchShare = (requireComponent != null && requireComponent.keys().length) > 0 ? requireComponent("./share/ApiDocumentBatchShare.vue") : {}; export default { name: "ApiDocumentAnchor", @@ -341,21 +360,21 @@ export default { data() { return { isLoading: false, - shareUrl:"", - batchShareUrl:"", + shareUrl: "", + batchShareUrl: "", apiStepIndex: 0, - showXpackCompnent:false, + showXpackCompnent: false, apiInfoArray: [], modes: ['text', 'json', 'xml', 'html'], formParamTypes: ['form-data', 'x-www-from-urlencoded', 'BINARY'], mockVariableFuncs: [], - apiSearch:{ - name:"", - type:"ALL", - orderCondition:"createTimeDesc", + apiSearch: { + name: "", + type: "ALL", + orderCondition: "createTimeDesc", }, apiInfoBaseObj: { - selectedFlag:false, + selectedFlag: false, method: "无", uri: "无", name: "无", @@ -365,9 +384,9 @@ export default { requestBodyParamType: "无", requestBodyFormData: '[]', requestBodyStrutureData: "", - sharePopoverVisible:false, + sharePopoverVisible: false, jsonSchemaBody: {}, - JsonSchemaResponseBody:{}, + JsonSchemaResponseBody: {}, responseHead: "无", responseBody: "", responseBodyParamType: "无", @@ -377,19 +396,19 @@ export default { }, methodColorMap: new Map(API_METHOD_COLOUR), clientHeight: '',//浏览器高度, - maxCompnentSize : 5, //浏览器最多渲染的api信息体数量 - apiShowArray:[],//浏览器要渲染的api信息集合 + maxCompnentSize: 5, //浏览器最多渲染的api信息体数量 + apiShowArray: [],//浏览器要渲染的api信息集合 needAsyncSelect: false, //是否需要异步查询api详细数据做展现。只有本次要展示的数据总量大于maxCompnentSize时为true currentApiIndexInApiShowArray: 0,//当前主要展示的api信息在apiShowArray的索引 - clickStepFlag:false, - } + clickStepFlag: false, + }; }, props: { projectId: String, documentId: String, moduleIds: Array, - sharePage:Boolean, - pageHeaderHeight:Number, + sharePage: Boolean, + pageHeaderHeight: Number, trashEnable: { type: Boolean, default: false, @@ -402,10 +421,10 @@ export default { window.onresize = function () { this.clientHeight = `${document.documentElement.clientHeight}`; this.changeFixed(this.clientHeight); - } + }; }, created: function () { - if(requireComponent!=null && JSON.stringify(apiDocumentBatchShare) != '{}'){ + if (requireComponent != null && JSON.stringify(apiDocumentBatchShare) != '{}') { this.showXpackCompnent = true; } this.initApiDocSimpleList(); @@ -415,7 +434,7 @@ export default { this.clientHeight = `${document.documentElement.clientHeight}`; this.changeFixed(this.clientHeight); }; - window.addEventListener('scroll',that.handleScroll); + window.addEventListener('scroll', that.handleScroll); }, mounted() { let that = this; @@ -424,10 +443,9 @@ export default { that.changeFixed(that.clientHeight); }; // 监听滚动事件,然后用handleScroll这个方法进行相应的处理 - window.addEventListener('scroll',this.handleScroll); - }, - computed: { + window.addEventListener('scroll', this.handleScroll); }, + computed: {}, watch: { moduleIds() { this.initApiDocSimpleList(); @@ -440,18 +458,39 @@ export default { }, }, methods: { + formatRowDataToJsonSchema(api, jsonType) { + if (jsonType === 'request' && api.requestBodyStrutureData) { + try { + JSON.parse(api.requestBodyStrutureData); + api.requestJsonSchema = {'raw': api.requestBodyStrutureData}; + return true; + } catch (e) { + return false; + } + } else if (jsonType === 'response' && api.responseBodyStrutureData) { + try { + JSON.parse(api.responseBodyStrutureData); + api.responseJsonSchema = {'raw': api.responseBodyStrutureData}; + return true; + } catch (e) { + return false; + } + } else { + return false; + } + }, formatRowData(dataType, data) { var returnData = data; if (data) { - returnData = "<xmp>"+returnData+"</xmp>"; + returnData = "<xmp>" + returnData + "</xmp>"; } return returnData; }, changeFixed(clientHeight) { if (this.$refs.apiDocInfoDiv) { let countPageHeight = 210; - if(this.pageHeaderHeight!=0 && this.pageHeaderHeight != null){ - countPageHeight = this.pageHeaderHeight + if (this.pageHeaderHeight != 0 && this.pageHeaderHeight != null) { + countPageHeight = this.pageHeaderHeight; } this.$refs.apiDocInfoDiv.style.height = clientHeight - countPageHeight + 'px'; @@ -461,7 +500,7 @@ export default { }, initApiDocSimpleList() { //首先跳转到第一个节点(为了让滚动条变为0,防止重新加载后滚动条位置出现动乱导致页面混乱) - if(this.apiInfoArray.length > 0){ + if (this.apiInfoArray.length > 0) { this.clickStep(this.apiInfoArray[0].id); } this.apiInfoArray = []; @@ -487,21 +526,21 @@ export default { this.apiInfoArray = response.data; this.apiStepIndex = 0; if (this.apiInfoArray.length > 0) { - this.checkApiInfoNode(this.apiStepIndex,true); + this.checkApiInfoNode(this.apiStepIndex, true); } - if(response.data.length > this.maxCompnentSize){ + if (response.data.length > this.maxCompnentSize) { this.needAsyncSelect = true; - }else{ + } else { this.needAsyncSelect = false; } }); }, - shareApiDocument(isBatchShare){ + shareApiDocument(isBatchShare) { this.shareUrl = ""; this.batchShareUrl = ""; let shareIdArr = []; let shareType = "Single"; - if(isBatchShare == 'true'){ + if (isBatchShare == 'true') { this.apiInfoArray.forEach(f => { if (!f.id) { return; @@ -509,7 +548,7 @@ export default { shareIdArr.push(f.id); }); shareType = "Batch"; - }else{ + } else { shareIdArr.push(this.apiInfoArray[this.apiStepIndex].id); } let genShareInfoParam = {}; @@ -518,49 +557,49 @@ export default { generateApiDocumentShareInfo(genShareInfoParam, (data) => { let thisHost = window.location.host; - if(shareType == "Batch"){ + if (shareType == "Batch") { this.batchShareUrl = thisHost + "/document" + data.shareUrl; - }else{ + } else { this.shareUrl = thisHost + "/document" + data.shareUrl; } }); }, - selectApiInfo(index,apiId,needUpdateShowArray) { + selectApiInfo(index, apiId, needUpdateShowArray) { let simpleInfoUrl = "/share/info/selectApiInfoById/" + apiId; this.$get(simpleInfoUrl, response => { let returnData = response.data; - this.$set(this.apiInfoArray,index,returnData); - if(needUpdateShowArray){ + this.$set(this.apiInfoArray, index, returnData); + if (needUpdateShowArray) { let showApiIndex = -1; - for(let i = 0;i< this.apiShowArray.length;i++){ - if(this.apiShowArray[i].id === apiId){ + for (let i = 0; i < this.apiShowArray.length; i++) { + if (this.apiShowArray[i].id === apiId) { showApiIndex = i; } } - if(showApiIndex > -1){ - this.$set(this.apiShowArray,showApiIndex,returnData); + if (showApiIndex > -1) { + this.$set(this.apiShowArray, showApiIndex, returnData); } } }); }, //itemIndex,afterNodeIndex,beforeNodeIndex 三个是回调参数,用于重新构建showArray的数据 isRedirectScroll:是否调用跳转函数 - selectApiInfoBatch(indexArr,apiIdArr,itemIndex,afterNodeIndex,beforeNodeIndex,isRedirectScroll) { - if(indexArr.length != apiIdArr.length){ + selectApiInfoBatch(indexArr, apiIdArr, itemIndex, afterNodeIndex, beforeNodeIndex, isRedirectScroll) { + if (indexArr.length != apiIdArr.length) { this.isLoading = false; this.clickStepFlag = false; return; - }else { + } else { let params = {}; params.apiIdList = apiIdArr; this.$post("/share/info/selectApiInfoByParam", params, response => { let returnDatas = response.data; - for(let dataIndex = 0; dataIndex < returnDatas.length;dataIndex ++){ + for (let dataIndex = 0; dataIndex < returnDatas.length; dataIndex++) { let index = indexArr[dataIndex]; let data = returnDatas[dataIndex]; - this.$set(this.apiInfoArray,index,data); + this.$set(this.apiInfoArray, index, data); } - this.updateShowArray(itemIndex,afterNodeIndex,beforeNodeIndex); - if(isRedirectScroll){ + this.updateShowArray(itemIndex, afterNodeIndex, beforeNodeIndex); + if (isRedirectScroll) { this.redirectScroll(); } }); @@ -577,17 +616,17 @@ export default { } } //检查数据 - this.checkApiInfoNode(this.apiStepIndex,true); + this.checkApiInfoNode(this.apiStepIndex, true); }, getColor(enable, method) { return this.methodColorMap.get(method); }, formatBoolean(row, column, cellValue) { - var ret = '' //你想在页面展示的值 + var ret = ''; //你想在页面展示的值 if (cellValue) { - ret = "是" //根据自己的需求设定 + ret = "是"; //根据自己的需求设定 } else { - ret = "否" + ret = "否"; } return ret; }, @@ -614,7 +653,7 @@ export default { for (var key in previewData) { // showDataObj.set(key,previewData[key]); let value = previewData[key]; - if(typeof(value)=='string'){ + if (typeof (value) == 'string') { if (value.indexOf("@") >= 0) { value = this.showPreview(value); } @@ -650,7 +689,7 @@ export default { return itemValue; }, onCopySuccess: function (e) { - if(this.apiStepIndex < this.apiInfoArray.length){ + if (this.apiStepIndex < this.apiInfoArray.length) { this.apiInfoArray[this.apiStepIndex].sharePopoverVisible = false; } this.$message({ @@ -659,146 +698,146 @@ export default { }); }, onCopyError: function (e) { - if(this.apiStepIndex < this.apiInfoArray.length){ + if (this.apiStepIndex < this.apiInfoArray.length) { this.apiInfoArray[this.apiStepIndex].sharePopoverVisible = false; } this.$message.error(this.$t('api_report.error')); }, - handleScroll(){ - if(!this.clickStepFlag && this.$refs.apiDocInfoDiv){ + handleScroll() { + if (!this.clickStepFlag && this.$refs.apiDocInfoDiv) { //apiDocInfoDiv的总高度,是(每个item的高度+20)数量 let apiDocDivScrollTop = 0; - if(this.$refs.apiDocInfoDiv&&this.$refs.apiDocInfoDiv.scrollTop){ + if (this.$refs.apiDocInfoDiv && this.$refs.apiDocInfoDiv.scrollTop) { apiDocDivScrollTop = this.$refs.apiDocInfoDiv.scrollTop; } let apiDocDivClientTop = this.$refs.apiDocInfoDiv.clientHeight; - let scrolledHeigh = apiDocDivScrollTop+apiDocDivClientTop; + let scrolledHeigh = apiDocDivScrollTop + apiDocDivClientTop; let lastIndex = 0; for (let index = 0; index < this.apiShowArray.length; index++) { //判断移动到了第几个元素. 公式: 移动过的高度+页面显示高度-第index子元素的高度(含20px)>0 的 index最大值 - if(scrolledHeigh>0){ + if (scrolledHeigh > 0) { lastIndex = index; - let itemHeight = this.$refs.apiDocInfoDivItem[index].offsetHeight+10; + let itemHeight = this.$refs.apiDocInfoDivItem[index].offsetHeight + 10; scrolledHeigh = scrolledHeigh - itemHeight; - }else{ + } else { break; } } - if(lastIndex < this.currentApiIndexInApiShowArray){ + if (lastIndex < this.currentApiIndexInApiShowArray) { //上移 // if(this.needAsyncSelect){ - //进行判断:是否还需要为apiShowArray 增加数据。 由于在当前数据前后最多展现2条数据, - //可得: apiStepIndex-1- 2 < apiInfoArray,需要添加数据 - let dataIndex = this.apiStepIndex -3; - if(dataIndex >= 0){ - let apiInfo = this.apiInfoArray[dataIndex]; - let haveData = false; - //检查showArray是否存在这条数据。不存在才加入 - this.apiShowArray.forEach(api => { - if(api.id === apiInfo.id){ - haveData = true; - } - }); - if(!haveData){ - this.apiShowArray.unshift(apiInfo); - if(!apiInfo.selectedFlag){ - this.selectApiInfo(dataIndex,this.apiInfoArray[dataIndex].id,true); - } - }else { - this.currentApiIndexInApiShowArray--; + //进行判断:是否还需要为apiShowArray 增加数据。 由于在当前数据前后最多展现2条数据, + //可得: apiStepIndex-1- 2 < apiInfoArray,需要添加数据 + let dataIndex = this.apiStepIndex - 3; + if (dataIndex >= 0) { + let apiInfo = this.apiInfoArray[dataIndex]; + let haveData = false; + //检查showArray是否存在这条数据。不存在才加入 + this.apiShowArray.forEach(api => { + if (api.id === apiInfo.id) { + haveData = true; } - }else{ + }); + if (!haveData) { + this.apiShowArray.unshift(apiInfo); + if (!apiInfo.selectedFlag) { + this.selectApiInfo(dataIndex, this.apiInfoArray[dataIndex].id, true); + } + } else { this.currentApiIndexInApiShowArray--; } + } else { + this.currentApiIndexInApiShowArray--; + } - if(this.apiShowArray.length > (this.currentApiIndexInApiShowArray+3)){ - this.apiShowArray.pop(); - } + if (this.apiShowArray.length > (this.currentApiIndexInApiShowArray + 3)) { + this.apiShowArray.pop(); + } // } - this.apiStepIndex --; - }else if(lastIndex > this.currentApiIndexInApiShowArray){ + this.apiStepIndex--; + } else if (lastIndex > this.currentApiIndexInApiShowArray) { //下滚 //进行判断:是否还需要为apiShowArray 增加数据。 由于在当前数据前后最多展现2条数据, //可得: apiStepIndex+1+ 2 < apiInfoArray,需要添加数据 - let dataIndex = this.apiStepIndex +3; - if(dataIndex < this.apiInfoArray.length){ + let dataIndex = this.apiStepIndex + 3; + if (dataIndex < this.apiInfoArray.length) { let apiInfo = this.apiInfoArray[dataIndex]; let haveData = false; //检查showArray是否存在这条数据。不存在才加入 this.apiShowArray.forEach(api => { - if(api.id === apiInfo.id){ + if (api.id === apiInfo.id) { haveData = true; } }); - if(!haveData){ + if (!haveData) { this.apiShowArray.push(apiInfo); - if(!apiInfo.selectedFlag){ - this.selectApiInfo(dataIndex,this.apiInfoArray[dataIndex].id,true); + if (!apiInfo.selectedFlag) { + this.selectApiInfo(dataIndex, this.apiInfoArray[dataIndex].id, true); } } } - if(this.apiShowArray.length <= this.maxCompnentSize){ + if (this.apiShowArray.length <= this.maxCompnentSize) { //判断currentApiIndexInApiShowArray 是否需要添加,以及是否需要删除第一个元素 this.currentApiIndexInApiShowArray++; - }else{ + } else { this.apiShowArray.shift(); - let itemHeight = this.$refs.apiDocInfoDivItem[0].offsetHeight+10; - if(this.$refs.apiDocInfoDiv&&this.$refs.apiDocInfoDiv.scrollTop){ - this.$refs.apiDocInfoDiv.scrollTop = (apiDocDivScrollTop-itemHeight); + let itemHeight = this.$refs.apiDocInfoDivItem[0].offsetHeight + 10; + if (this.$refs.apiDocInfoDiv && this.$refs.apiDocInfoDiv.scrollTop) { + this.$refs.apiDocInfoDiv.scrollTop = (apiDocDivScrollTop - itemHeight); } } - this.apiStepIndex ++; + this.apiStepIndex++; } } this.clickStepFlag = false; }, - redirectScroll(){ - if(!this.$refs.apiDocInfoDiv){ + redirectScroll() { + if (!this.$refs.apiDocInfoDiv) { return; } //滚动条跳转:将滚动条下拉到显示对应对api接口的位置 let apiDocDivClientTop = 0; let itemHeightCount = 0; - if(this.currentApiIndexInApiShowArray > 0){ - for (let i = 0; i <= this.currentApiIndexInApiShowArray-1; i++) { - let itemHeight = this.$refs.apiDocInfoDivItem[i].offsetHeight+10; - itemHeightCount+=itemHeight; + if (this.currentApiIndexInApiShowArray > 0) { + for (let i = 0; i <= this.currentApiIndexInApiShowArray - 1; i++) { + let itemHeight = this.$refs.apiDocInfoDivItem[i].offsetHeight + 10; + itemHeightCount += itemHeight; } } this.clickStepFlag = true; let scrollTopIndex = this.$refs.apiDocInfoDiv.scrollTop; - if(this.$refs.apiDocInfoDiv&&this.$refs.apiDocInfoDiv.scrollTop){ - this.$refs.apiDocInfoDiv.scrollTop = (apiDocDivClientTop+itemHeightCount); - }else if(scrollTopIndex === 0){ - this.$refs.apiDocInfoDiv.scrollTop = (apiDocDivClientTop+itemHeightCount); + if (this.$refs.apiDocInfoDiv && this.$refs.apiDocInfoDiv.scrollTop) { + this.$refs.apiDocInfoDiv.scrollTop = (apiDocDivClientTop + itemHeightCount); + } else if (scrollTopIndex === 0) { + this.$refs.apiDocInfoDiv.scrollTop = (apiDocDivClientTop + itemHeightCount); } this.isLoading = false; }, //检查要展示的api信息节点,和上下个2个及以内的范围内数据有没有查询过。并赋值为showArray //isRedirectScroll 最后是否调用跳转函数 - checkApiInfoNode(itemIndex,isRedirectScroll){ - let beforeNodeIndex = itemIndex<2?0:(itemIndex-2); - let afterNodeIndex = (itemIndex+2)<this.apiInfoArray.length?(itemIndex+2):this.apiInfoArray.length; + checkApiInfoNode(itemIndex, isRedirectScroll) { + let beforeNodeIndex = itemIndex < 2 ? 0 : (itemIndex - 2); + let afterNodeIndex = (itemIndex + 2) < this.apiInfoArray.length ? (itemIndex + 2) : this.apiInfoArray.length; this.apiShowArray = []; let selectIndexArr = []; let selectApiId = []; //查当前节点前两个 - for(let afterIndex = beforeNodeIndex;afterIndex <itemIndex;afterIndex++){ + for (let afterIndex = beforeNodeIndex; afterIndex < itemIndex; afterIndex++) { let apiInfo = this.apiInfoArray[afterIndex]; - if(apiInfo==null){ + if (apiInfo == null) { continue; } - if(apiInfo == null || !apiInfo.selectedFlag){ + if (apiInfo == null || !apiInfo.selectedFlag) { let apiId = apiInfo.id; - if(!apiInfo.isSearching) { + if (!apiInfo.isSearching) { apiInfo.isSearching = true; selectIndexArr.push(afterIndex); selectApiId.push(apiId); @@ -808,14 +847,14 @@ export default { } this.currentApiIndexInApiShowArray = this.apiShowArray.length; //查当前节点以及后三个 - for(let beforeIndex = itemIndex;beforeIndex <= afterNodeIndex;beforeIndex++){ + for (let beforeIndex = itemIndex; beforeIndex <= afterNodeIndex; beforeIndex++) { let apiInfo = this.apiInfoArray[beforeIndex]; - if(apiInfo==null){ + if (apiInfo == null) { continue; } - if(apiInfo == null || !apiInfo.selectedFlag){ + if (apiInfo == null || !apiInfo.selectedFlag) { let apiId = apiInfo.id; - if(!apiInfo.isSearching){ + if (!apiInfo.isSearching) { apiInfo.isSearching = true; selectIndexArr.push(beforeIndex); selectApiId.push(apiId); @@ -823,10 +862,10 @@ export default { } this.apiShowArray.push(apiInfo); } - if(selectIndexArr.length>0){ - this.selectApiInfoBatch(selectIndexArr,selectApiId,itemIndex,afterNodeIndex,beforeNodeIndex,isRedirectScroll); - }else{ - if(isRedirectScroll){ + if (selectIndexArr.length > 0) { + this.selectApiInfoBatch(selectIndexArr, selectApiId, itemIndex, afterNodeIndex, beforeNodeIndex, isRedirectScroll); + } else { + if (isRedirectScroll) { //进行跳转 this.$nextTick(() => { this.redirectScroll(); @@ -835,28 +874,28 @@ export default { } }, //该方法只用于批量查询后的函数处理。 因为查询完成,数据更新,重新为apiShowArray赋值 - updateShowArray(itemIndex,afterNodeIndex,beforeNodeIndex){ + updateShowArray(itemIndex, afterNodeIndex, beforeNodeIndex) { this.apiShowArray = []; //查当前节点前两个 - for(let afterIndex = beforeNodeIndex;afterIndex <itemIndex;afterIndex++){ + for (let afterIndex = beforeNodeIndex; afterIndex < itemIndex; afterIndex++) { let apiInfo = this.apiInfoArray[afterIndex]; - if(apiInfo==null){ + if (apiInfo == null) { continue; } this.apiShowArray.push(apiInfo); } this.currentApiIndexInApiShowArray = this.apiShowArray.length; //查当前节点以及后三个 - for(let beforeIndex = itemIndex;beforeIndex <= afterNodeIndex;beforeIndex++){ + for (let beforeIndex = itemIndex; beforeIndex <= afterNodeIndex; beforeIndex++) { let apiInfo = this.apiInfoArray[beforeIndex]; - if(apiInfo==null){ + if (apiInfo == null) { continue; } this.apiShowArray.push(apiInfo); } } }, -} +}; </script> <style scoped> @@ -898,6 +937,11 @@ export default { /* 步骤条中,已经完成后的节点样式和里面a标签的样式 */ + +/deep/ .el-step { + flex-basis: 40px !important; +} + /deep/ .el-step__head.is-finish { color: #C0C4CC; border-color: #C0C4CC; @@ -910,15 +954,38 @@ export default { /* 步骤条中,当前节点样式和当前a标签的样式 */ +/deep/ .el-step__head { + width: 20px; +} + /deep/ .el-step__head.is-process { color: #783887; border-color: #783887; + width: 20px; } -/deep/ .el-step__title.is-process /deep/ .el-link.el-link--default { +/deep/ .el-step__title.is-process .el-link.el-link--default.is-underline { color: #783887; } +/deep/ .el-link--inner { + font-size: 12px; +} + +/deep/ .el-step__icon-inner { + font-size: 12px; +} + +/deep/ .el-step.is-vertical .el-step__line { + left: 9px; +} + + +/deep/ .el-step__icon { + width: 20px; + height: 20px; +} + .document-table { margin: 10px 10px; width: auto; @@ -942,6 +1009,7 @@ export default { background-color: #FAFAFA; border-right: 0px solid #EBEEF5 } + .el-divider--horizontal { margin: 12px 0; } diff --git a/frontend/src/business/components/api/definition/components/document/ApiDocumentItem.vue b/frontend/src/business/components/api/definition/components/document/ApiDocumentItem.vue deleted file mode 100644 index d60b718e8d..0000000000 --- a/frontend/src/business/components/api/definition/components/document/ApiDocumentItem.vue +++ /dev/null @@ -1,717 +0,0 @@ -<template> - <div> - <el-container> - <el-main style="padding-top: 0px;padding-bottom: 0px"> - <el-row style="margin-top: 10px"> - <el-select size="small" :placeholder="$t('api_test.definition.document.order')" v-model="apiSearch.orderCondition" style="float: right;width: 180px;margin-right: 5px" - class="ms-api-header-select" @change="initApiDocSimpleList" clearable> - <el-option key="createTimeDesc" :label="$t('api_test.definition.document.create_time_sort')" value="createTimeDesc" /> - <el-option key="editTimeAsc" :label="$t('api_test.definition.document.edit_time_positive_sequence')" value="editTimeAsc"/> - <el-option key="editTimeDesc" :label="$t('api_test.definition.document.edit_time_Reverse_order')" value="editTimeDesc"/> - </el-select> - - <el-select size="small" :placeholder="$t('api_test.definition.document.request_method')" v-model="apiSearch.type" style="float: right;width: 180px;margin-right: 5px" - class="ms-api-header-select" @change="initApiDocSimpleList" clearable> - <el-option key="ALL" :label="$t('api_test.definition.document.data_set.all')" value="ALL"/> - <el-option key="GET" :label="'GET '+$t('api_test.definition.document.request_interface')" value="GET"/> - <el-option key="POST" :label="'POST '+$t('api_test.definition.document.request_interface')" value="POST"/> - <el-option key="PUT" :label="'PUT '+$t('api_test.definition.document.request_interface')" value="PUT"/> - <el-option key="DELETE" :label="'DELETE '+$t('api_test.definition.document.request_interface')" value="DELETE"/> - <el-option key="PATCH" :label="'PATCH '+$t('api_test.definition.document.request_interface')" value="PATCH"/> - <el-option key="OPTIONS" :label="'OPTIONS '+$t('api_test.definition.document.request_interface')" value="OPTIONS"/> - <el-option key="HEAD" :label="'HEAD '+$t('api_test.definition.document.request_interface')" value="HEAD"/> - <el-option key="CONNECT" :label="'CONNECT '+$t('api_test.definition.document.request_interface')" value="CONNECT"/> - </el-select> - <el-input :placeholder="$t('api_test.definition.document.search_by_api_name')" @blur="initApiDocSimpleList()" style="float: right;width: 180px;margin-right: 5px" size="small" - @keyup.enter.native="initApiDocSimpleList()" v-model="apiSearch.name"/> - <api-document-batch-share v-xpack v-if="showXpackCompnent" @shareApiDocument="shareApiDocument" :project-id="projectId" :share-url="batchShareUrl" style="float: right;margin: 6px;font-size: 17px"/> - <!-- <api-document-batch-share v-xpack v-if="showXpackCompnent"/>--> - </el-row> - <el-divider></el-divider> - <div ref="apiDocInfoDiv" @scroll="handleScroll" > - <div v-for="(apiInfo) in apiInfoArray" :key="apiInfo.id" ref="apiDocInfoDivItem"> - <div style="font-size: 17px"> - <el-popover - v-if="projectId" - placement="right" - width="260" - @show="shareApiDocument('false')"> - <p>{{shareUrl}}</p> - <div style="text-align: right; margin: 0"> - <el-button type="primary" size="mini" - v-clipboard:copy="shareUrl">{{ $t("commons.copy") }}</el-button> - </div> - <i class="el-icon-share" slot="reference" style="margin-right: 10px;cursor: pointer"></i> - </el-popover> - {{ apiInfo.name }} - <span class="apiStatusTag"> - <api-status :value="apiInfo.status"/> - </span> - </div> - <!--api请求信息--> - <el-row class="apiInfoRow"> - <div class="tip"> - {{ $t('api_test.definition.document.request_info') }} - </div> - </el-row> - <el-row class="apiInfoRow"> - <div class="simpleFontClass"> - <el-tag size="medium" - :style="{'background-color': getColor(true,apiInfo.method), border: getColor(true,apiInfo.method),borderRadius:'0px', marginRight:'20px',color:'white'}"> - {{ apiInfo.method }} - </el-tag> - {{ apiInfo.uri }} - </div> - </el-row> - <!--api请求头--> - <el-row class="apiInfoRow"> - <div class="blackFontClass"> - {{ $t('api_test.definition.document.request_head') }}: - <div v-if="getJsonArr(apiInfo.requestHead).length==0"> - <div class="simpleFontClass" style="margin-top: 10px"> - {{ $t('api_test.definition.document.data_set.none') }} - </div> - </div> - <div v-else> - <el-table border :show-header="false" - :data="getJsonArr(apiInfo.requestHead)" row-key="name" class="test-content document-table"> - <el-table-column prop="name" - :label="$t('api_test.definition.document.table_coloum.name')" - show-overflow-tooltip/> - <el-table-column prop="value" - :label="$t('api_test.definition.document.table_coloum.value')" - show-overflow-tooltip/> - </el-table> - </div> - </div> - </el-row> - <!--URL参数--> - <el-row class="apiInfoRow"> - <div class="blackFontClass"> - URL{{ $t('api_test.definition.document.request_param') }}: - <div v-if="getJsonArr(apiInfo.urlParams).length==0"> - <div class="simpleFontClass" style="margin-top: 10px"> - {{ $t('api_test.definition.document.data_set.none') }} - </div> - </div> - <div v-else> - <el-table border - :data="getJsonArr(apiInfo.urlParams)" row-key="name" class="test-content document-table"> - <el-table-column prop="name" - :label="$t('api_test.definition.document.table_coloum.name')" - min-width="120px" - show-overflow-tooltip/> - <el-table-column prop="isEnable" - :label="$t('api_test.definition.document.table_coloum.is_required')" - min-width="80px" - show-overflow-tooltip/> - <el-table-column prop="value" - :label="$t('api_test.definition.document.table_coloum.value')" - min-width="120px" - show-overflow-tooltip/> - <el-table-column prop="description" - :label="$t('api_test.definition.document.table_coloum.desc')" - min-width="280px" - show-overflow-tooltip/> - </el-table> - </div> - </div> - </el-row> - <!--api请求体 以及表格--> - <el-row class="apiInfoRow"> - <div class="blackFontClass"> - {{ $t('api_test.definition.document.request_body') }} - </div> - <div class="smallFontClass"> - {{ $t('api_test.definition.document.table_coloum.type') }}:{{ apiInfo.requestBodyParamType }} - </div> - <div> - <el-table border v-if="formParamTypes.includes(apiInfo.requestBodyParamType)" - :data="getJsonArr(apiInfo.requestBodyFormData)" row-key="name" - class="test-content document-table"> - <el-table-column prop="name" - :label="$t('api_test.definition.document.table_coloum.name')" - min-width="120px" - show-overflow-tooltip/> - <el-table-column prop="contentType" - :label="$t('api_test.definition.document.table_coloum.type')" - min-width="120px" - show-overflow-tooltip/> - <el-table-column prop="description" - :label="$t('api_test.definition.document.table_coloum.desc')" - min-width="280px" - show-overflow-tooltip/> - <el-table-column prop="required" - :label="$t('api_test.definition.document.table_coloum.is_required')" - :formatter="formatBoolean" - min-width="80px" - show-overflow-tooltip/> - <el-table-column prop="value" - :label="$t('api_test.definition.document.table_coloum.default_value')" - min-width="120px" - show-overflow-tooltip/> - </el-table> - <div v-else-if="apiInfo.requestBodyParamType == 'JSON-SCHEMA'" style="margin-left: 10px"> - <ms-json-code-edit :body="apiInfo.jsonSchemaBody" ref="jsonCodeEdit"/> - </div> - <div v-else class="showDataDiv"> - <br/> - <p style="margin: 0px 20px;" - v-html="formatRowData(apiInfo.requestBodyParamType,apiInfo.requestBodyStrutureData)"> - </p> - <br/> - </div> - </div> - </el-row> - <!--范例展示--> - <el-row class="apiInfoRow"> - <div class="blackFontClass"> - {{ $t('api_test.definition.document.example_presentation') }} - </div> - <div class="showDataDiv"> - <br/> - <p style="margin: 0px 20px;" - v-html="genPreviewData(apiInfo.requestPreviewData)"> - </p> - <br/> - </div> - </el-row> - <!--响应信息--> - <el-row class="apiInfoRow"> - <div class="tip"> - {{ $t('api_test.definition.document.response_info') }} - </div> - </el-row> - <el-row class="apiInfoRow"> - - </el-row> - <!--响应头--> - <el-row class="apiInfoRow"> - <div class="blackFontClass"> - {{ $t('api_test.definition.document.response_head') }}: - <el-table border :show-header="false" - :data="getJsonArr(apiInfo.responseHead)" row-key="name" class="test-content document-table"> - <el-table-column prop="name" - :label="$t('api_test.definition.document.table_coloum.name')" - show-overflow-tooltip/> - <el-table-column prop="value" - :label="$t('api_test.definition.document.table_coloum.value')" - show-overflow-tooltip/> - </el-table> - </div> - </el-row> - <!--响应体--> - <el-row class="apiInfoRow"> - <div class="blackFontClass"> - {{ $t('api_test.definition.document.response_body') }} - </div> - <div class="smallFontClass"> - {{ $t('api_test.definition.document.table_coloum.type') }}:{{ apiInfo.responseBodyParamType }} - </div> - <div> - <el-table border v-if="formParamTypes.includes(apiInfo.responseBodyParamType)" - :data="getJsonArr(apiInfo.responseBodyFormData)" row-key="id" - class="test-content document-table"> - <el-table-column prop="name" - :label="$t('api_test.definition.document.table_coloum.name')" - min-width="120px" - show-overflow-tooltip/> - <el-table-column prop="contentType" - :label="$t('api_test.definition.document.table_coloum.type')" - min-width="120px" - show-overflow-tooltip/> - <el-table-column prop="description" - :label="$t('api_test.definition.document.table_coloum.desc')" - min-width="280px" - show-overflow-tooltip/> - <el-table-column prop="required" - :label="$t('api_test.definition.document.table_coloum.is_required')" - :formatter="formatBoolean" - min-width="80px" - show-overflow-tooltip/> - <el-table-column prop="value" - :label="$t('api_test.definition.document.table_coloum.default_value')" - min-width="120px" - show-overflow-tooltip/> - </el-table> - <div v-else class="showDataDiv"> - <br/> - <p style="margin: 0px 20px;" - v-html="formatRowData(apiInfo.responseBodyParamType,apiInfo.responseBodyStrutureData)"> - </p> - <br/> - </div> - </div> - </el-row> - <!--响应状态码--> - <el-row class="apiInfoRow"> - <div class="blackFontClass"> - {{ $t('api_test.definition.document.response_code') }}: - <el-table border :show-header="false" - :data="getJsonArr(apiInfo.responseCode)" row-key="name" class="test-content document-table"> - <el-table-column prop="name" - :label="$t('api_test.definition.document.table_coloum.name')" - show-overflow-tooltip/> - <el-table-column prop="value" - :label="$t('api_test.definition.document.table_coloum.value')" - show-overflow-tooltip/> - </el-table> - </div> - </el-row> - </div> - </div> - </el-main> - <!-- 右侧列表 --> - <el-aside width="200px" style="margin-top: 70px;"> - <div ref="apiDocList" > - <el-steps style="height: 40%" direction="vertical" :active="apiStepIndex"> - <el-step v-for="(apiInfo) in apiInfoArray" :key="apiInfo.id" @click.native="clickStep(apiInfo.id)"> - <el-link slot="title">{{ apiInfo.name }}</el-link> - </el-step> - </el-steps> - </div> - </el-aside> - </el-container> - </div> -</template> - -<script> -import {API_METHOD_COLOUR} from "@/business/components/api/definition/model/JsonData"; -import MsCodeEdit from "@/business/components/common/components/MsCodeEdit"; -import {formatJson,} from "@/common/js/format-utils"; -import ApiStatus from "@/business/components/api/definition/components/list/ApiStatus"; -import {calculate} from "@/business/components/api/definition/model/ApiTestModel"; -import MsJsonCodeEdit from "@/business/components/common/json-schema/JsonSchemaEditor"; -import Api from "@/business/components/api/router"; - -const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/); -const apiDocumentBatchShare = (requireComponent!=null&&requireComponent.keys().length) > 0 ? requireComponent("./share/ApiDocumentBatchShare.vue") : {}; - -export default { - name: "ApiDocumentItem", - components: { - Api, - MsJsonCodeEdit, - ApiStatus, MsCodeEdit, - "ApiDocumentBatchShare": apiDocumentBatchShare.default - }, - data() { - return { - shareUrl:"", - batchShareUrl:"", - apiStepIndex: 0, - showXpackCompnent:false, - apiInfoArray: [], - modes: ['text', 'json', 'xml', 'html'], - formParamTypes: ['form-data', 'x-www-from-urlencoded', 'BINARY'], - mockVariableFuncs: [], - apiSearch:{ - name:"", - type:"ALL", - orderCondition:"createTimeDesc", - }, - apiInfoBaseObj: { - selectedFlag:false, - method: "无", - uri: "无", - name: "无", - id: "", - requestHead: "无", - urlParams: "无", - requestBodyParamType: "无", - requestBodyFormData: '[]', - requestBodyStrutureData: "", - sharePopoverVisible:false, - jsonSchemaBody: {}, - responseHead: "无", - responseBody: "", - responseBodyParamType: "无", - responseBodyFormData: "无", - responseBodyStrutureData: "无", - responseCode: "无", - }, - methodColorMap: new Map(API_METHOD_COLOUR), - clientHeight: '',//坚挺浏览器高度 - } - }, - props: { - projectId: String, - documentId: String, - moduleIds: Array, - pageHeaderHeight:Number, - }, - activated() { - this.initApiDocSimpleList(); - this.clientHeight = `${document.documentElement.clientHeight}`;//获取浏览器可视区域高度 - let that = this; - window.onresize = function () { - this.clientHeight = `${document.documentElement.clientHeight}`; - this.changeFixed(this.clientHeight); - } - }, - created: function () { - if(requireComponent!=null && JSON.stringify(apiDocumentBatchShare) != '{}'){ - this.showXpackCompnent = true; - } - this.initApiDocSimpleList(); - this.clientHeight = `${document.documentElement.clientHeight}`;//获取浏览器可视区域高度 - let that = this; - window.onresize = function () { - this.clientHeight = `${document.documentElement.clientHeight}`; - this.changeFixed(this.clientHeight); - }; - window.addEventListener('scroll',that.handleScroll); - }, - mounted() { - let that = this; - window.onresize = function () { - that.clientHeight = `${document.documentElement.clientHeight}`; - that.changeFixed(that.clientHeight); - }; - // 监听滚动事件,然后用handleScroll这个方法进行相应的处理 - window.addEventListener('scroll',this.handleScroll); - }, - computed: { - }, - watch: { - moduleIds() { - this.initApiDocSimpleList(); - }, - clientHeight() { //如果clientHeight 发生改变,这个函数就会运行 - this.changeFixed(this.clientHeight); - } - }, - methods: { - formatRowData(dataType, data) { - var returnData = data; - if (data) { - returnData = data.replace(/\n/g, '<br>'); - } - return returnData; - }, - changeFixed(clientHeight) { - if (this.$refs.apiDocInfoDiv) { - let countPageHeight = 350; - if(this.pageHeaderHeight!=0 && this.pageHeaderHeight != null){ - countPageHeight = this.pageHeaderHeight - } - - this.$refs.apiDocInfoDiv.style.height = clientHeight - countPageHeight + 'px'; - this.$refs.apiDocInfoDiv.style.overflow = 'auto'; - this.$refs.apiDocList.style.height = clientHeight - countPageHeight + 'px'; - } - }, - initApiDocSimpleList() { - let simpleRequest = this.apiSearch; - if (this.projectId != null && this.projectId != "") { - simpleRequest.projectId = this.projectId; - } - if (this.documentId != null && this.documentId != "") { - simpleRequest.shareId = this.documentId; - } - if (this.moduleIds.length > 0) { - simpleRequest.moduleIds = this.moduleIds; - } - - let simpleInfoUrl = "/share/info/selectApiSimpleInfo"; - this.apiInfoArray = []; - this.$post(simpleInfoUrl, simpleRequest, response => { - this.apiInfoArray = response.data; - this.apiStepIndex = 0; - if (this.apiInfoArray.length > 0) { - this.checkApiInfoNode(this.apiStepIndex); - } - }); - }, - shareApiDocument(isBatchShare){ - let thisHost = window.location.host; - this.shareUrl = ""; - this.batchShareUrl = ""; - let shareIdArr = []; - let shareType = "Single"; - if(isBatchShare == 'true'){ - this.apiInfoArray.forEach(f => { - if (!f.id) { - return; - } - shareIdArr.push(f.id); - }); - shareType = "Batch"; - }else{ - shareIdArr.push(this.apiInfoArray[this.apiStepIndex].id); - } - let genShareInfoParam = {}; - genShareInfoParam.shareApiIdList = shareIdArr; - genShareInfoParam.shareType = shareType; - - this.$post("/share/info/generateApiDocumentShareInfo", genShareInfoParam, res => { - if(shareType == "Batch"){ - this.batchShareUrl = thisHost+"/document"+res.data.shareUrl; - }else{ - this.shareUrl = thisHost+"/document"+res.data.shareUrl; - } - }, (error) => { - }); - }, - selectApiInfo(index,apiId) { - let simpleInfoUrl = "/share/info/selectApiInfoById/" + apiId; - this.$get(simpleInfoUrl, response => { - let returnData = response.data; - this.$set(this.apiInfoArray,index,returnData); - }); - }, - clickStep(apiId) { - for (let index = 0; index < this.apiInfoArray.length; index++) { - if (apiId == this.apiInfoArray[index].id) { - this.apiStepIndex = index; - break; - } - } - //检查数据 - this.checkApiInfoNode(this.apiStepIndex); - //进行跳转 - this.redirectScroll(this.apiStepIndex); - }, - stepClick(stepIndex) { - this.apiStepIndex = stepIndex; - }, - getColor(enable, method) { - return this.methodColorMap.get(method); - }, - formatBoolean(row, column, cellValue) { - var ret = '' //你想在页面展示的值 - if (cellValue) { - ret = "是" //根据自己的需求设定 - } else { - ret = "否" - } - return ret; - }, - getJsonArr(jsonString) { - let returnJsonArr = []; - if (jsonString == '无' || jsonString == null) { - return returnJsonArr; - } - - let jsonArr = JSON.parse(jsonString); - //遍历,把必填项空的数据去掉 - for (var index = 0; index < jsonArr.length; index++) { - var item = jsonArr[index]; - if (item.name != "" && item.name != null) { - returnJsonArr.push(item); - } - } - return returnJsonArr; - }, - //构建预览数据 - genPreviewData(previewData) { - if (previewData != null && previewData != '') { - let showDataObj = {}; - for (var key in previewData) { - // showDataObj.set(key,previewData[key]); - let value = previewData[key]; - if(typeof(value)=='string'){ - if (value.indexOf("@") >= 0) { - value = this.showPreview(value); - } - } - showDataObj[key] = value; - } - showDataObj = JSON.stringify(showDataObj); - previewData = formatJson(showDataObj); - } - return previewData; - }, - showPreview(itemValue) { - // 找到变量本身 - if (!itemValue) { - return; - } - let index = itemValue.indexOf("|"); - if (index > -1) { - itemValue = itemValue.substring(0, index).trim(); - } - - this.mockVariableFuncs.forEach(f => { - if (!f.name) { - return; - } - itemValue += "|" + f.name; - if (f.params) { - itemValue += ":" + f.params.map(p => p.value).join(","); - } - }); - - itemValue = calculate(itemValue); - return itemValue; - }, - onCopySuccess: function (e) { - if(this.apiStepIndex < this.apiInfoArray.length){ - this.apiInfoArray[this.apiStepIndex].sharePopoverVisible = false; - } - this.$message({ - message: this.$t('commons.copy_success'), - type: 'success' - }); - }, - onCopyError: function (e) { - if(this.apiStepIndex < this.apiInfoArray.length){ - this.apiInfoArray[this.apiStepIndex].sharePopoverVisible = false; - } - this.$message.error(this.$t('api_report.error')); - }, - handleScroll(){ - if(!this.$refs.apiDocInfoDiv){ - return; - } - //apiDocInfoDiv的总高度,是(每个item的高度+20)数量 - let apiDocDivScrollTop = this.$refs.apiDocInfoDiv.scrollTop; - let apiDocDivClientTop = this.$refs.apiDocInfoDiv.clientHeight; - - let scrolledHeigh = apiDocDivScrollTop+apiDocDivClientTop; - let lastIndex = 0; - for (let index = 0; index < this.apiInfoArray.length; index++) { - //判断移动到了第几个元素. 公式: 移动过的高度+页面显示高度-第index子元素的高度(含20px)>0 的 index最大值 - if(scrolledHeigh>0){ - lastIndex = index; - let itemHeight = this.$refs.apiDocInfoDivItem[index].offsetHeight+20; - scrolledHeigh = scrolledHeigh - itemHeight; - }else{ - break; - } - } - this.apiStepIndex = lastIndex; - //检查上下文 3个以内的节点有没有查询出来 - this.checkApiInfoNode(this.apiStepIndex); - }, - redirectScroll(itemIndex){ - //滚动条跳转:将滚动条下拉到显示对应对api接口的位置 - // let apiDocDivClientTop = this.$refs.apiDocInfoDiv.clientHeight; - let apiDocDivClientTop = 0; - let itemHeightCount = 0; - for (let i = 0; i <= itemIndex-1; i++) { - let itemHeight = this.$refs.apiDocInfoDivItem[i].offsetHeight+20; - itemHeightCount+=itemHeight; - } - if(this.$refs.apiDocInfoDiv){ - this.$refs.apiDocInfoDiv.scrollTop = (apiDocDivClientTop+itemHeightCount); - } - }, - checkApiInfoNode(itemIndex){ - //检查要展示的api信息节点,和上下个3个及以内的范围内数据有没有查询过 - let beforeNodeIndex = itemIndex<3?0:(itemIndex-3); - let afterNodeIndex = (itemIndex+3)<this.apiInfoArray.length?(itemIndex+3):this.apiInfoArray.length; - - for(let beforeIndex = itemIndex;beforeIndex < afterNodeIndex;beforeIndex++){ - let apiInfo = this.apiInfoArray[beforeIndex]; - if(apiInfo==null){ - continue; - } - if(apiInfo == null || !apiInfo.selectedFlag){ - let apiId = apiInfo.id; - if(!apiInfo.isSearching){ - apiInfo.isSearching = true; - this.selectApiInfo(beforeIndex,apiId); - } - } - } - - for(let afterIndex = beforeNodeIndex;afterIndex <itemIndex;afterIndex++){ - let apiInfo = this.apiInfoArray[afterIndex]; - if(apiInfo==null){ - continue; - } - if(apiInfo == null || !apiInfo.selectedFlag){ - let apiId = apiInfo.id; - if(!apiInfo.isSearching) { - apiInfo.isSearching = true; - this.selectApiInfo(afterIndex,apiId); - } - } - } - } - }, -} -</script> - -<style scoped> -.simpleFontClass { - font-weight: normal; - font-size: 14px; - margin-left: 10px; -} - -.blackFontClass { - font-weight: bold; - font-size: 14px; -} - -.smallFontClass { - font-size: 13px; - margin: 20px 10px; -} - -.apiInfoRow { - margin: 20px 10px; -} - -.apiStatusTag { - margin: 20px 5px; -} - -.showDataDiv { - background-color: #F5F7F9; - margin: 20px 10px; -} - -/* -步骤条中,已经完成后的节点样式和里面a标签的样式 -*/ -/deep/ .el-step__head.is-finish { - color: #C0C4CC; - border-color: #C0C4CC; -} - -/deep/ .el-step__title.is-finish /deep/ .el-link.el-link--default { - color: #C0C4CC; -} - -/* -步骤条中,当前节点样式和当前a标签的样式 -*/ -/deep/ .el-step__head.is-process { - color: #783887; - border-color: #783887; -} - -/deep/ .el-step__title.is-process /deep/ .el-link.el-link--default { - color: #783887; -} - -.document-table { - margin: 20px 10px; - width: auto; -} - -.document-table /deep/ .el-table__row { - font-size: 12px; - font-weight: initial; -} - -.document-table /deep/ .has-gutter { - font-size: 12px; - color: #404040; -} - -.document-table /deep/ td { - border-right: 0px solid #EBEEF5 -} - -.document-table /deep/ th { - background-color: #FAFAFA; - border-right: 0px solid #EBEEF5 -} -.el-divider--horizontal { - margin: 12px 0; -} -</style> diff --git a/frontend/src/business/components/api/definition/components/list/ApiCaseBatchRun.vue b/frontend/src/business/components/api/definition/components/list/ApiCaseBatchRun.vue index bc7990e83d..33a486403b 100644 --- a/frontend/src/business/components/api/definition/components/list/ApiCaseBatchRun.vue +++ b/frontend/src/business/components/api/definition/components/list/ApiCaseBatchRun.vue @@ -36,6 +36,7 @@ export default { }, save() { this.$emit('batchRun', this.environment); + this.close(); } } } diff --git a/frontend/src/business/components/api/definition/components/list/ApiCaseSimpleList.vue b/frontend/src/business/components/api/definition/components/list/ApiCaseSimpleList.vue index 2fd7e7f8f1..6149969d0f 100644 --- a/frontend/src/business/components/api/definition/components/list/ApiCaseSimpleList.vue +++ b/frontend/src/business/components/api/definition/components/list/ApiCaseSimpleList.vue @@ -11,6 +11,7 @@ @click="addTestCase" v-if="apiDefinitionId">{{ $t('commons.add') }} </el-button> <ms-table + v-loading="result.loading" :data="tableData" :select-node-ids="selectNodeIds" :condition="condition" @@ -22,6 +23,8 @@ :fields.sync="fields" :field-key="tableHeaderKey" :remember-order="true" + :row-order-group-id="condition.projectId" + :row-order-func="editApiTestCaseOrder" :enable-order-drag="enableOrderDrag" row-key="id" operator-width="190px" @@ -157,6 +160,7 @@ @showCaseRef="showCaseRef" @showEnvironment="showEnvironment" @createPerformance="createPerformance" + @showHistory="openHis" :row="scope.row"/> </template> @@ -183,6 +187,8 @@ <api-case-batch-run :project-id="projectId" @batchRun="runBatch" ref="batchRun"/> + <ms-task-center ref="taskCenter"/> + <el-dialog :close-on-click-modal="false" :title="$t('test_track.plan_view.test_result')" width="60%" :visible.sync="resVisible" class="api-import" destroy-on-close @close="resVisible=false"> <ms-request-result-tail :response="response" ref="debugResult"/> @@ -256,7 +262,8 @@ export default { MsTableAdvSearchBar, MsTable, MsTableColumn, - MsRequestResultTail + MsRequestResultTail, + MsTaskCenter: () => import("../../../../task/TaskCenter"), }, data() { return { @@ -355,6 +362,7 @@ export default { environments: [], resVisible: false, response: {}, + timeoutIndex: 0, }; }, props: { @@ -452,9 +460,15 @@ export default { }, selectRows() { return this.$refs.caseTable.getSelectRows(); + }, + editApiTestCaseOrder() { + return editApiTestCaseOrder; } }, methods: { + openHis(row) { + this.$refs.taskCenter.openHistory(row.id); + }, getExecResult(apiCase) { if (apiCase.lastResultId) { let url = "/api/definition/report/get/" + apiCase.lastResultId; @@ -496,9 +510,15 @@ export default { this.$refs.batchRun.open(); }, runBatch(environment) { - this.condition.environmentId = environment.id; - this.condition.ids = this.$refs.caseTable.selectIds; - this.$post('/api/testcase/batch/run', this.condition, () => { + let obj = {}; + obj.projectId = this.projectId; + obj.selectAllDate = this.selectAll; + obj.unSelectIds = this.unSelection; + obj.ids = Array.from(this.selectRows).map(row => row.id); + obj.environmentId = environment.id; + obj.condition = this.condition; + obj.condition.status = ""; + this.$post('/api/testcase/batch/run', obj, () => { this.condition.ids = []; this.$refs.batchRun.close(); this.search(); @@ -508,6 +528,7 @@ export default { this.$refs.caseTable.openCustomHeader(); }, initTable(id) { + this.timeoutIndex = 0; if (this.$refs.caseTable) { this.$refs.caseTable.clearSelectRows(); } @@ -585,22 +606,72 @@ export default { isNext = true; } }); - this.$nextTick(function () { + this.$nextTick(() => { + if (this.$refs.caseTable) { + this.$refs.caseTable.clear(); + } handleRowDrop(this.tableData, (param) => { param.groupId = this.condition.projectId; editApiTestCaseOrder(param); }); - - if (this.$refs.caseTable) { - this.$refs.caseTable.doLayout(); - this.$refs.caseTable.checkTableRowIsSelect(); + }) + if (isNext) { + this.refreshStatus(); + } + }); + } + }, + refreshStatus() { + if (this.apiDefinitionId) { + this.condition.apiDefinitionId = this.apiDefinitionId; + } + this.condition.status = ""; + this.condition.moduleIds = this.selectNodeIds; + if (this.condition.filters && !this.condition.filters.status) { + this.$delete(this.condition.filters, 'status'); + } + if (!this.selectAll) { + this.selectAll = false; + this.unSelection = []; + this.selectDataCounts = 0; + } + this.condition.projectId = this.projectId; + if (this.currentProtocol != null) { + this.condition.protocol = this.currentProtocol; + } + //检查是否只查询本周数据 + this.isSelectThissWeekData(); + this.condition.selectThisWeedData = false; + this.condition.id = null; + if (this.selectDataRange == 'thisWeekCount') { + this.condition.selectThisWeedData = true; + } else if (this.selectDataRange != null) { + let selectParamArr = this.selectDataRange.split("single:"); + if (selectParamArr.length === 2) { + this.condition.id = selectParamArr[1]; + } + } + if (this.condition.projectId) { + this.result = this.$post('/api/testcase/list/' + this.currentPage + "/" + this.pageSize, this.condition, response => { + let isNext = false; + let tableData = response.data.listObject; + this.tableData.forEach(item => { + for (let i in tableData) { + if (item.id === tableData[i].id) { + item.status = tableData[i].status; + item.lastResultId = tableData[i].lastResultId; + } + } + if (item.status === 'Running') { + isNext = true; } }); - if (isNext) { + if (isNext && this.$store.state.currentApiCase && this.$store.state.currentApiCase.case && this.timeoutIndex < 12) { + this.timeoutIndex++; setTimeout(() => { - this.initTable(); - }, 5000); + this.refreshStatus(); + }, 12000); } }); } @@ -969,6 +1040,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.sortHashTree(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/api/definition/components/list/ApiDocumentsPage.vue b/frontend/src/business/components/api/definition/components/list/ApiDocumentsPage.vue index 1f36c31078..7d2e532109 100644 --- a/frontend/src/business/components/api/definition/components/list/ApiDocumentsPage.vue +++ b/frontend/src/business/components/api/definition/components/list/ApiDocumentsPage.vue @@ -1,6 +1,5 @@ <template> <div> -<!-- <api-document-item :project-id="projectId" :module-ids="moduleIds"/>--> <api-document-anchor :is-share-page="isSharePage" :trash-enable="trashEnable" :project-id="projectId" :module-ids="moduleIds"></api-document-anchor> </div> </template> diff --git a/frontend/src/business/components/api/definition/components/list/ApiList.vue b/frontend/src/business/components/api/definition/components/list/ApiList.vue index 0dc1978839..504614e673 100644 --- a/frontend/src/business/components/api/definition/components/list/ApiList.vue +++ b/frontend/src/business/components/api/definition/components/list/ApiList.vue @@ -15,6 +15,8 @@ :remember-order="true" @refresh="initTable" :fields.sync="fields" + :row-order-func="editApiDefinitionOrder" + :row-order-group-id="condition.projectId" :table-is-loading="this.result.loading" :field-key="tableHeaderKey" :enable-order-drag="enableOrderDrag" @@ -208,12 +210,10 @@ import MsTipButton from "@/business/components/common/components/MsTipButton"; import CaseBatchMove from "@/business/components/api/definition/components/basis/BatchMove"; import { initCondition, - getCustomTableHeader, getCustomTableWidth, buildBatchParam, checkTableRowIsSelected, - saveLastTableSortField, getLastTableSortField, handleRowDrop + getCustomTableHeader, getCustomTableWidth, buildBatchParam, getLastTableSortField } from "@/common/js/tableUtils"; import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate"; import {Body} from "@/business/components/api/definition/model/ApiTestModel"; -import {buildNodePath} from "@/business/components/api/definition/model/NodeTree"; import {editApiDefinitionOrder} from "@/network/api"; @@ -420,6 +420,9 @@ export default { } else { return this.$t('api_test.definition.api_type'); } + }, + editApiDefinitionOrder() { + return editApiDefinitionOrder; } }, created: function () { @@ -474,7 +477,7 @@ export default { initCondition(this.condition, false); this.closeCaseModel(); this.initTable(); - } + }, }, methods: { getProjectName() { @@ -548,18 +551,6 @@ export default { item.tags = JSON.parse(item.tags); } }); - - checkTableRowIsSelected(this, this.$refs.table); - - this.$nextTick(() => { - if (this.$refs.table) { - this.$refs.table.clear(); - } - handleRowDrop(this.tableData, (param) => { - param.groupId = this.condition.projectId; - editApiDefinitionOrder(param); - }); - }) }); } if (this.needRefreshModule()) { diff --git a/frontend/src/business/components/api/definition/components/reference/ApiCaseTableExtendBtns.vue b/frontend/src/business/components/api/definition/components/reference/ApiCaseTableExtendBtns.vue index eeafb7da94..007d3ee8ab 100644 --- a/frontend/src/business/components/api/definition/components/reference/ApiCaseTableExtendBtns.vue +++ b/frontend/src/business/components/api/definition/components/reference/ApiCaseTableExtendBtns.vue @@ -5,9 +5,11 @@ </el-link> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="ref">{{ $t('api_test.automation.view_ref') }}</el-dropdown-item> + <el-dropdown-item command="history" v-modules="['history']"> + {{ $t('commons.execute_history') }} + </el-dropdown-item> <el-dropdown-item command="create_performance" v-modules="['performance']" - v-permission="['PROJECT_API_DEFINITION:READ+CREATE_PERFORMANCE']" - > + v-permission="['PROJECT_API_DEFINITION:READ+CREATE_PERFORMANCE']"> {{ $t('api_test.create_performance_test') }} </el-dropdown-item> </el-dropdown-menu> @@ -38,6 +40,9 @@ export default { case "create_performance": this.$emit("showEnvironment", this.row); break; + case "history": + this.$emit("showHistory", this.row); + break; } } else { this.$warning(this.$t('api_test.automation.save_case_info')) diff --git a/frontend/src/business/components/api/definition/components/reference/ApiExtendBtns.vue b/frontend/src/business/components/api/definition/components/reference/ApiExtendBtns.vue index f772c9183c..b0a98faffa 100644 --- a/frontend/src/business/components/api/definition/components/reference/ApiExtendBtns.vue +++ b/frontend/src/business/components/api/definition/components/reference/ApiExtendBtns.vue @@ -53,6 +53,9 @@ if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.sortHashTree(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/api/definition/components/request/tcp/TcpFormatParameters.vue b/frontend/src/business/components/api/definition/components/request/tcp/TcpFormatParameters.vue index 26113a4294..377de40f36 100644 --- a/frontend/src/business/components/api/definition/components/request/tcp/TcpFormatParameters.vue +++ b/frontend/src/business/components/api/definition/components/request/tcp/TcpFormatParameters.vue @@ -28,18 +28,18 @@ raw </el-radio> </el-radio-group> - <div style="min-width: 1200px;" v-if="request.reportType === 'xml'"> + <div v-if="request.reportType === 'xml'"> <tcp-xml-table :table-data="request.xmlDataStruct" :show-options-button="true" @xmlTablePushRow="xmlTablePushRow" @initXmlTableData="initXmlTableData" @saveTableData="saveXmlTableData" ref="treeTable"></tcp-xml-table> </div> - <div style="min-width: 1200px;" v-if="request.reportType === 'json'"> + <div v-if="request.reportType === 'json'"> <div class="send-request"> <ms-code-edit mode="json" :read-only="isReadOnly" :data.sync="request.jsonDataStruct" :modes="['text', 'json', 'xml', 'html']" theme="eclipse"/> </div> </div> - <div style="min-width: 1200px;" v-if="request.reportType === 'raw'"> + <div v-if="request.reportType === 'raw'"> <div class="send-request"> <ms-code-edit mode="text" :read-only="isReadOnly" :data.sync="request.rawDataStruct" :modes="['text', 'json', 'xml', 'html']" theme="eclipse"/> </div> diff --git a/frontend/src/business/components/api/definition/components/runtest/RunTestHTTPPage.vue b/frontend/src/business/components/api/definition/components/runtest/RunTestHTTPPage.vue index 75531c0f23..30419f9688 100644 --- a/frontend/src/business/components/api/definition/components/runtest/RunTestHTTPPage.vue +++ b/frontend/src/business/components/api/definition/components/runtest/RunTestHTTPPage.vue @@ -225,6 +225,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.compatibleHistory(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/api/test/components/environment/EnvironmentEdit.vue b/frontend/src/business/components/api/test/components/environment/EnvironmentEdit.vue index 10f024b802..e600ac2fcc 100644 --- a/frontend/src/business/components/api/test/components/environment/EnvironmentEdit.vue +++ b/frontend/src/business/components/api/test/components/environment/EnvironmentEdit.vue @@ -109,7 +109,7 @@ }, created() { if(!this.environment.config.preProcessor){ - this.environment.config.preProcessor = createComponent("JDBCPreProcessor"); + this.environment.config.preProcessor = createComponent("JSR223PreProcessor"); } if(!this.environment.config.postProcessor){ this.environment.config.postProcessor = createComponent("JSR223PostProcessor"); @@ -125,7 +125,7 @@ watch: { environment: function (o) { if(!this.environment.config.preProcessor){ - this.environment.config.preProcessor = createComponent("JDBCPreProcessor"); + this.environment.config.preProcessor = createComponent("JSR223PreProcessor"); if(!this.environment.config.preProcessor.script){ this.environment.config.preProcessor.script = ""; } diff --git a/frontend/src/business/components/api/test/components/environment/EnvironmentHttpConfig.vue b/frontend/src/business/components/api/test/components/environment/EnvironmentHttpConfig.vue index 869905a3c9..92b2fe917a 100644 --- a/frontend/src/business/components/api/test/components/environment/EnvironmentHttpConfig.vue +++ b/frontend/src/business/components/api/test/components/environment/EnvironmentHttpConfig.vue @@ -267,11 +267,11 @@ export default { this.condition = {type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "http", socket: "", domain: "", headers: [new KeyValue()]}; this.reload(); } - this.$refs.envTable.setCurrentRow(-1); + this.$refs.envTable.setCurrentRow(0); }, clear() { this.condition = {type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "http", socket: "", domain: "", headers: [new KeyValue()]}; - this.$refs.envTable.setCurrentRow(-1); + this.$refs.envTable.setCurrentRow(0); }, reload() { this.loading = true diff --git a/frontend/src/business/components/common/components/table/MsCustomTableHeader.vue b/frontend/src/business/components/common/components/table/MsCustomTableHeader.vue index dd734b57ba..0d5b3482a1 100644 --- a/frontend/src/business/components/common/components/table/MsCustomTableHeader.vue +++ b/frontend/src/business/components/common/components/table/MsCustomTableHeader.vue @@ -20,6 +20,7 @@ import MsDialogFooter from "@/business/components/common/components/MsDialogFooter"; import treeTransfer from 'el-tree-transfer' import {getAllFieldWithCustomFields, saveCustomTableHeader} from "@/common/js/tableUtils"; +import {SYSTEM_FIELD_NAME_MAP} from "@/common/js/table-constants"; export default { name: "MsCustomTableHeader", @@ -45,6 +46,12 @@ export default { }, open(items) { items = JSON.parse(JSON.stringify(items)); + items.forEach(it => { + if (it.isCustom) { + // i18n + it.label = SYSTEM_FIELD_NAME_MAP[it.id] ? this.$t(SYSTEM_FIELD_NAME_MAP[it.id]) : it.label; + } + }) let fields = getAllFieldWithCustomFields(this.type, this.customFields); this.selectedKeys = []; this.fromFields = []; @@ -52,6 +59,9 @@ export default { this.selectedFields = items; fields.forEach(field => { if (this.selectedKeys.indexOf(field.key) < 0) { + if (field.isCustom) { + field.label = SYSTEM_FIELD_NAME_MAP[field.id] ? this.$t(SYSTEM_FIELD_NAME_MAP[field.id]) : field.label + } this.fromFields.push(field); } }); diff --git a/frontend/src/business/components/common/components/table/MsTable.vue b/frontend/src/business/components/common/components/table/MsTable.vue index 61a4add24d..af3f453a9c 100644 --- a/frontend/src/business/components/common/components/table/MsTable.vue +++ b/frontend/src/business/components/common/components/table/MsTable.vue @@ -94,7 +94,7 @@ import { getSelectDataCounts, setUnSelectIds, toggleAllSelection, - checkTableRowIsSelect, getCustomTableHeader, saveCustomTableWidth, saveLastTableSortField, + checkTableRowIsSelect, getCustomTableHeader, saveCustomTableWidth, saveLastTableSortField, handleRowDrop, } from "@/common/js/tableUtils"; import MsTableHeaderSelectPopover from "@/business/components/common/components/table/MsTableHeaderSelectPopover"; import MsTablePagination from "@/business/components/common/pagination/TablePagination"; @@ -105,6 +105,7 @@ import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOpe import HeaderCustom from "@/business/components/common/head/HeaderCustom"; import MsCustomTableHeader from "@/business/components/common/components/table/MsCustomTableHeader"; import {lineToHump} from "@/common/js/utils"; +import {editTestCaseOrder} from "@/network/testCase"; /** * 参考 ApiList @@ -219,6 +220,9 @@ export default { rememberOrder: Boolean, enableOrderDrag: Boolean, rowKey: [String, Function], + // 自定义排序,需要传资源所属的项目id或者测试计划id,并且传排序的方法 + rowOrderGroupId: String, + rowOrderFunc: Function }, mounted() { this.setDefaultOrders(); @@ -226,6 +230,18 @@ export default { watch: { selectNodeIds() { this.selectDataCounts = 0; + }, + // 刷新列表后做统一处理 + data(newVar, oldVar) { + // 不知为何,勾选选择框也会进到这里,但是这种情况 newVar === oldVar + if (newVar !== oldVar) { + this.$nextTick(() => { + this.clear(); + this.doLayout(); + this.checkTableRowIsSelect(); + this.listenRowDrop(); + }); + } } }, methods: { @@ -255,6 +271,16 @@ export default { } } }, + listenRowDrop() { + if (this.rowOrderGroupId) { + handleRowDrop(this.data, (param) => { + param.groupId = this.rowOrderGroupId; + if (this.rowOrderFunc) { + this.rowOrderFunc(param); + } + }); + } + }, isScrollShow(column, tableTop){ //判断元素是否因为超过表头 let columnTop = column.getBoundingClientRect().top; return columnTop - tableTop > 30; @@ -447,4 +473,20 @@ export default { .ms-icon-more:first-child { margin-right: -5px; } + +.ms-table >>> .el-table__body tr.hover-row.current-row>td, +.ms-table >>> .el-table__body tr.hover-row.el-table__row--striped.current-row>td, +.ms-table >>> .el-table__body tr.hover-row.el-table__row--striped>td, +.ms-table >>> .el-table__body tr.hover-row>td { + background-color: #ffffff; +} +/* 解决拖拽排序后hover阴影错乱问题 */ +.ms-table >>> .el-table__body tr:hover>td + { + background-color: #F5F7FA; +} + +.disable-hover >>> tr:hover>td{ + background-color: #ffffff !important; +} </style> diff --git a/frontend/src/business/components/common/head/HeaderTopMenus.vue b/frontend/src/business/components/common/head/HeaderTopMenus.vue index 6c90a03261..5bb43af740 100644 --- a/frontend/src/business/components/common/head/HeaderTopMenus.vue +++ b/frontend/src/business/components/common/head/HeaderTopMenus.vue @@ -24,7 +24,7 @@ </el-menu-item> <el-menu-item index="/report" onselectstart="return false" v-permission="['PROJECT_TRACK_CASE:READ','PROJECT_TRACK_PLAN:READ','PROJECT_TRACK_REVIEW:READ']" - v-if="isReport && check('reportStat')"> + > {{ $t('commons.report_statistics.title') }} </el-menu-item> @@ -40,9 +40,9 @@ import {mapGetters} from "vuex"; import {hasLicense} from "@/common/js/utils"; import {MODULE_CHANGE, ModuleEvent} from "@/business/components/common/head/ListEvent"; -const requireContext = require.context('@/business/components/xpack/', true, /router\.js$/); -const report = requireContext.keys().map(key => requireContext(key).report); -const isReport = report && report != null && report.length > 0 && report[0] != undefined ? true : false; +// const requireContext = require.context('@/business/components/xpack/', true, /router\.js$/); +// const report = requireContext.keys().map(key => requireContext(key).report); +// const isReport = report && report != null && report.length > 0 && report[0] != undefined ? true : false; const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/); const module = requireComponent.keys().length > 0 ? requireComponent("./module/Module.vue") : {}; @@ -52,7 +52,7 @@ export default { data() { return { activeIndex: '/', - isReport: isReport, + isReport: true, modules: {}, menuKey: 0, }; diff --git a/frontend/src/business/components/common/json-schema/JsonSchemaEditor.vue b/frontend/src/business/components/common/json-schema/JsonSchemaEditor.vue index 78dbd180ad..94c5298e9a 100644 --- a/frontend/src/business/components/common/json-schema/JsonSchemaEditor.vue +++ b/frontend/src/business/components/common/json-schema/JsonSchemaEditor.vue @@ -7,7 +7,7 @@ <json-schema-editor class="schema" :value="schema" lang="zh_CN" custom/> </div> </el-tab-pane> - <el-tab-pane :label="$t('schema.preview')" name="preview"> + <el-tab-pane v-if="showPreview" :label="$t('schema.preview')" name="preview"> <div style="min-height: 200px"> <pre>{{this.preview}}</pre> </div> @@ -29,6 +29,10 @@ components: {MsImportJson}, props: { body: {}, + showPreview: { + type: Boolean, + default: true + }, }, created() { if (!this.body.jsonSchema && this.body.raw && this.checkIsJson(this.body.raw)) { @@ -46,6 +50,19 @@ this.body.jsonSchema = this.schema.root; }, deep: true + }, + body: { + handler(newValue, oldValue) { + if (!this.body.jsonSchema && this.body.raw && this.checkIsJson(this.body.raw)) { + let obj = {"root": MsConvert.format(JSON.parse(this.body.raw))} + this.schema = obj; + } + else if (this.body.jsonSchema) { + this.schema = {"root": this.body.jsonSchema}; + } + this.body.jsonSchema = this.schema.root; + }, + deep: true } }, data() { diff --git a/frontend/src/business/components/common/json-schema/schema/editor/main.vue b/frontend/src/business/components/common/json-schema/schema/editor/main.vue index abc3926b07..433c9f72e3 100644 --- a/frontend/src/business/components/common/json-schema/schema/editor/main.vue +++ b/frontend/src/business/components/common/json-schema/schema/editor/main.vue @@ -39,10 +39,10 @@ </el-col> </el-row> - <template v-if="!hidden&&pickValue.properties && !isArray"> - <json-schema-editor v-for="(item,key,index) in pickValue.properties" :value="{[key]:item}" :parent="pickValue" :key="index" :deep="deep+1" :root="false" class="children" :lang="lang" :custom="custom" @changeAllItemsType="changeAllItemsType"/> + <template v-if="!hidden&&pickValue.properties && !isArray && reloadItemOver"> + <json-schema-editor v-for="(item,key,index) in pickValue.properties" :value="{[key]:item}" :parent="pickValue" :key="index" :deep="deep+1" :root="false" class="children" :lang="lang" :custom="custom" @changeAllItemsType="changeAllItemsType" @reloadItems="reloadItems"/> </template> - <template v-if="isArray"> + <template v-if="isArray && reloadItemOver"> <!-- <json-schema-editor :value="{items:pickValue.items}" :deep="deep+1" disabled isItem :root="false" class="children" :lang="lang" :custom="custom"/>--> <json-schema-editor v-for="(item,key,index) in pickValue.items" :value="{[key]:item}" :parent="pickValue" :key="index" :deep="deep+1" :root="false" class="children" :lang="lang" :custom="custom" @changeAllItemsType="changeAllItemsType"/> </template> @@ -199,6 +199,7 @@ hidden: false, countAdd: 1, modalVisible: false, + reloadItemOver: true, advancedValue: {}, addProp: {},// 自定义属性 customProps: [], @@ -322,6 +323,7 @@ required.length === 0 && this.$delete(this.parent, 'required') } } + this.parentReloadItems(); }, _joinName() { return `feild_${this.deep}_${this.countAdd++}_${getUUID().substring(0, 5)}` @@ -349,6 +351,15 @@ for (const item of this.customProps) { this.$set(this.pickValue, item.key, item.value) } + }, + parentReloadItems(){ + this.$emit("reloadItems"); + }, + reloadItems(){ + this.reloadItemOver = false; + this.$nextTick(() => { + this.reloadItemOver = true; + }) } } } diff --git a/frontend/src/business/components/common/router/router.js b/frontend/src/business/components/common/router/router.js index 312338f335..d3ff0889c9 100644 --- a/frontend/src/business/components/common/router/router.js +++ b/frontend/src/business/components/common/router/router.js @@ -5,11 +5,12 @@ import Setting from "@/business/components/settings/router"; import API from "@/business/components/api/router"; import Performance from "@/business/components/performance/router"; import Track from "@/business/components/track/router"; +import ReportStatistics from "@/business/components/reportstatistics/router"; import {getCurrentUserId} from "@/common/js/utils"; -const requireContext = require.context('@/business/components/xpack/', true, /router\.js$/); -const Report = requireContext.keys().map(key => requireContext(key).report); -const ReportObj = Report && Report != null && Report.length > 0 && Report[0] != undefined ? Report : [{path: "/sidebar"}]; +// const requireContext = require.context('@/business/components/xpack/', true, /router\.js$/); +// const Report = requireContext.keys().map(key => requireContext(key).report); +// const ReportObj = Report && Report != null && Report.length > 0 && Report[0] != undefined ? Report : [{path: "/sidebar"}]; Vue.use(VueRouter); @@ -26,7 +27,8 @@ const router = new VueRouter({ API, Performance, Track, - ...ReportObj + ReportStatistics, + // ...ReportStatistics ] }); diff --git a/frontend/src/business/components/common/select-tree/SelectTree.vue b/frontend/src/business/components/common/select-tree/SelectTree.vue index 73f42f9a05..4937d27f38 100644 --- a/frontend/src/business/components/common/select-tree/SelectTree.vue +++ b/frontend/src/business/components/common/select-tree/SelectTree.vue @@ -290,11 +290,14 @@ export default { this.$refs.tree.setCheckedKeys(thisKeys); this.returnDataKeys = thisKeys; let t = []; + this.options = []; thisKeys.map((item) => {//设置option选项 let node = this.$refs.tree.getNode(item); // 所有被选中的节点对应的node - t.push(node.data); - this.options.push({label: node.label, value: node.key}); - return {label: node.label, value: node.key}; + if(node){ + t.push(node.data); + this.options.push({label: node.label, value: node.key}); + return {label: node.label, value: node.key}; + } }); this.returnDatas = t; this.popoverHide() @@ -398,6 +401,27 @@ export default { }, deep: true }, + defaultKey:{ + handler:function(){ + this.init(); + if(this.data && this.data.length > 0){ + if(this.defaultKey instanceof Array){ + this.$refs.tree.setCheckedKeys(this.defaultKey); + } + } + }, + deep:true + }, + data:{ + handler:function(){ + if(this.defaultKey && this.defaultKey.length > 0 && this.defaultKey instanceof Array){ + if(this.defaultKey instanceof Array){ + this.$refs.tree.setCheckedKeys(this.defaultKey); + } + } + }, + deep:true + }, filterText(val) { this.$nextTick(() => { this.$refs.tree.filter(val); diff --git a/frontend/src/business/components/performance/test/PerformanceTestList.vue b/frontend/src/business/components/performance/test/PerformanceTestList.vue index 84e1b7a40f..a011d42acd 100644 --- a/frontend/src/business/components/performance/test/PerformanceTestList.vue +++ b/frontend/src/business/components/performance/test/PerformanceTestList.vue @@ -19,6 +19,8 @@ :remember-order="true" :enable-order-drag="enableOrderDrag" row-key="id" + :row-order-group-id="projectId" + :row-order-func="editLoadTestCaseOrder" operator-width="190px" :screen-height="screenHeight" :enable-selection="false" @@ -104,7 +106,7 @@ import MsTableOperators from "../../common/components/MsTableOperators"; import {getCurrentProjectID, getCurrentWorkspaceId} from "@/common/js/utils"; import MsTableHeader from "../../common/components/MsTableHeader"; import {TEST_CONFIGS} from "../../common/components/search/search-components"; -import {getLastTableSortField, handleRowDrop} from "@/common/js/tableUtils"; +import {getLastTableSortField} from "@/common/js/tableUtils"; import MsTable from "@/business/components/common/components/table/MsTable"; import {editLoadTestCaseOrder} from "@/network/load-test"; @@ -172,8 +174,13 @@ export default { this.initTableData(); } }, + computed: { + editLoadTestCaseOrder() { + return editLoadTestCaseOrder; + } + }, created: function () { - this.projectId = this.$route.params.projectId; + this.projectId = getCurrentProjectID(); this.initTableData(); this.getMaintainerOptions(); }, @@ -203,16 +210,6 @@ export default { this.$set(test, 'reportCount', response.data); }); }); - - this.$nextTick(() => { - handleRowDrop(this.tableData, (param) => { - param.groupId = getCurrentProjectID(); - editLoadTestCaseOrder(param); - }); - if (this.$refs.table) { - this.$refs.table.clear(); - } - }); }); }, search(combine) { diff --git a/frontend/src/business/components/reportstatistics/ReportAnalysis.vue b/frontend/src/business/components/reportstatistics/ReportAnalysis.vue new file mode 100644 index 0000000000..c2cb0a8f98 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/ReportAnalysis.vue @@ -0,0 +1,101 @@ +<template> + <div> + <el-row type="flex"> + <p class="tip"> + <span class="ms-span">{{$t('commons.report_statistics.name')}}</span> + <el-select v-model="reportType" class="ms-col-type" size="mini" style="width: 120px"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in reportTypes"/> + </el-select> + </p> + </el-row> + <transition> + <keep-alive> + <report-card @openCard="openCard"/> + </keep-alive> + </transition> + + <!-- 测试用例趋势页面 --> + <ms-drawer :visible="testCaseTrendDrawer" :size="100" @close="close" direction="right" :show-full-screen="false" :is-show-close="false" style="overflow: hidden"> + <template v-slot:header> + <report-header :title="$t('commons.report_statistics.test_case_analysis')" :history-report-id="historyReportId" + @closePage="close" @saveReport="saveReport" @selectAndSaveReport="selectAndSaveReport"/> + </template> + <test-analysis-container @initHistoryReportId="initHistoryReportId" ref="testAnalysisContainer"/> + </ms-drawer> + + <!-- 测试用例分析页面 --> + <ms-drawer :visible="testCaseCountDrawer" :size="100" @close="close" direction="right" :show-full-screen="false" :is-show-close="false" style="overflow: hidden"> + <template v-slot:header> + <report-header :title="$t('commons.report_statistics.test_case_count')" :history-report-id="historyReportId" + @closePage="close" @saveReport="saveReport" @selectAndSaveReport="selectAndSaveReport"/> + </template> + <test-case-count-container @initHistoryReportId="initHistoryReportId" ref="testCaseCountContainer"/> + </ms-drawer> + </div> +</template> + +<script> + import ReportCard from "./ReportCard"; + import TestAnalysisContainer from "./track/TestAnalysisContainer"; + import MsDrawer from "@/business/components/common/components/MsDrawer"; + import ReportHeader from './base/ReportHeader'; + import TestCaseCountContainer from "./testCaseCount/TestCaseCountContainer"; + + export default { + name: "ReportAnalysis", + components: {ReportCard, TestAnalysisContainer, MsDrawer, ReportHeader, TestCaseCountContainer}, + data() { + return { + reportType: "track", + testCaseTrendDrawer: false, + testCaseCountDrawer: false, + historyReportId:"", + reportTypes: [{id: 'track', name: this.$t('test_track.test_track')}], + } + }, + methods: { + openCard(type) { + if(type === 'trackTestCase'){ + this.testCaseTrendDrawer = true; + }else if(type === 'countTestCase'){ + this.testCaseCountDrawer = true; + } + }, + close() { + this.testCaseTrendDrawer = false; + this.testCaseCountDrawer = false; + }, + saveReport(){ + if(this.testCaseTrendDrawer){ + this.$refs.testAnalysisContainer.saveReport(); + }else if(this.testCaseCountDrawer){ + this.$refs.testCaseCountContainer.saveReport(); + } + }, + selectAndSaveReport(){ + if(this.testCaseTrendDrawer){ + this.$refs.testAnalysisContainer.selectAndSaveReport(); + }else if(this.testCaseCountDrawer){ + this.$refs.testCaseCountContainer.selectAndSaveReport(); + } + }, + initHistoryReportId(reportId){ + this.historyReportId = reportId; + }, + }, + } +</script> + +<style scoped> + .ms-span { + margin: 10px 10px 0px + } + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 10px 20px 0px; + } +</style> diff --git a/frontend/src/business/components/reportstatistics/ReportCard.vue b/frontend/src/business/components/reportstatistics/ReportCard.vue new file mode 100644 index 0000000000..648ad41b63 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/ReportCard.vue @@ -0,0 +1,138 @@ +<template> + <div class="ms-content"> + <el-row> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col" @click.native="openCard('trackTestCase')"> + <img src="../../../assets/track.jpg" class="image"> + <div style="padding: 10px;"> + <span>{{$t('commons.report_statistics.test_case_analysis')}}</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col" @click.native="openCard('countTestCase')"> + <img src="../../../assets/track.jpg" class="image"> + <div style="padding: 10px;"> + <span>{{$t('commons.report_statistics.test_case_count')}}</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_count_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col"> + <img src="../../../assets/other.png" class="image"> + <div style="padding: 10px;"> + <span>预留模块敬请期待</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col"> + <img src="../../../assets/other.png" class="image"> + <div style="padding: 10px;"> + <span>预留模块敬请期待</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col"> + <img src="../../../assets/other.png" class="image"> + <div style="padding: 10px;"> + <span>预留模块敬请期待</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + <el-col :span="4"> + <el-card :body-style="{ padding: '0px' }" class="ms-col"> + <img src="../../../assets/other.png" class="image"> + <div style="padding: 10px;"> + <span>预留模块敬请期待</span> + <div class="bottom clearfix"> + <time class="time">{{$t('commons.report_statistics.test_case_activity')}}</time> + </div> + </div> + </el-card> + </el-col> + </el-row> + </div> +</template> + +<script> + import {hasPermission} from "@/common/js/utils"; + + export default { + name: "ReportCard", + components: {}, + data() { + return {} + }, + methods: { + openCard(type) { + if (!hasPermission('PROJECT_REPORT_ANALYSIS:READ')) { + this.$warning("无查看权限!"); + return; + } + this.$emit('openCard', type); + } + }, + } +</script> + +<style scoped> + .time { + font-size: 13px; + color: #999; + } + + .bottom { + margin-top: 13px; + line-height: 12px; + } + + .button { + padding: 0; + float: right; + } + + .image { + width: 100%; + display: block; + } + + .clearfix:before, + .clearfix:after { + display: table; + content: ""; + } + + .clearfix:after { + clear: both + } + + .ms-col { + margin: 5px; + } + + .ms-content { + padding: 15px 10px 15px 15px; + } + + .ms-col:hover { + cursor: pointer; + border-color: #783887; + } +</style> diff --git a/frontend/src/business/components/reportstatistics/base/HistoryReportData.vue b/frontend/src/business/components/reportstatistics/base/HistoryReportData.vue new file mode 100644 index 0000000000..5922f31499 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/base/HistoryReportData.vue @@ -0,0 +1,86 @@ +<template> + <div> + <el-tabs v-model="activeName"> + <el-tab-pane :label="$t('commons.report_statistics.report_data.all_report')" name="allReport"> + <history-report-data-card :report-data="allReportData" :show-options-button="false" @deleteReport="deleteReport" @selectReport="selectReport"/> + </el-tab-pane> + <el-tab-pane :label="$t('commons.report_statistics.report_data.my_report')" name="myReport"> + <history-report-data-card :report-data="myReportData" :show-options-button="true" @deleteReport="deleteReport" @selectReport="selectReport"/> + </el-tab-pane> + </el-tabs> + </div> +</template> + +<script> + import {getCurrentProjectID,getCurrentUserId} from "@/common/js/utils"; + import HistoryReportDataCard from "./compose/HistoryReportDataCard"; + export default { + name: "HistoryReportData", + components: {HistoryReportDataCard}, + data() { + return { + activeName: 'allReport', + allReportData: [], + myReportData: [], + } + }, + props:{ + reportType:String + }, + created(){ + this.initReportData(); + }, + watch :{ + activeName(){ + this.initReportData(); + } + }, + methods: { + initReportData(){ + let projectId = getCurrentProjectID(); + let userId = getCurrentUserId(); + this.allReportData = []; + this.myReportData = []; + + let paramsObj = { + projectId:getCurrentProjectID(), + reportType:this.reportType, + }; + this.$post('/history/report/selectByParams',paramsObj, response => { + let allData = response.data; + allData.forEach(item => { + if(item){ + this.allReportData.push(item); + if(item.createUser === userId){ + this.myReportData.push(item); + } + } + }); + }); + }, + deleteReport(deleteId){ + let paramObj = { + id:deleteId + } + this.$post('/history/report/deleteByParam',paramObj, response => { + this.initReportData(); + }); + this.$emit("removeHistoryReportId"); + }, + selectReport(id){ + this.$emit("selectReport",id); + } + }, +} +</script> + +<style scoped> + +.historyCard{ + border: 0px; +} +/deep/ .el-card__header{ + border: 0px; +} + +</style> diff --git a/frontend/src/business/components/reportstatistics/base/ReportHeader.vue b/frontend/src/business/components/reportstatistics/base/ReportHeader.vue new file mode 100644 index 0000000000..c16290229d --- /dev/null +++ b/frontend/src/business/components/reportstatistics/base/ReportHeader.vue @@ -0,0 +1,115 @@ +<template> + <div class="ms-header"> + <el-row> + <div class="ms-div">{{title}}</div> + <div class="ms-header-right"> + <el-button type="primary" v-if="isSaveAsButtonShow" size="mini" @click="handleSaveAs" :disabled="readOnly">{{ $t('commons.save_as') }}<i class="el-icon-files el-icon--right"></i></el-button> + <el-button type="primary" v-if="isSaveButtonShow" size="mini" @click="handleSave" :disabled="readOnly">{{ $t('commons.save') }}<i class="el-icon-files el-icon--right"></i></el-button> + <el-button type="" size="mini" @click="handleExport" :disabled="readOnly">{{ $t('report.export') }}<i class="el-icon-download el-icon--right"></i></el-button> + <span class="ms-span">|</span> + <i class="el-icon-close report-alt-ico" @click="close"/> + </div> + </el-row> + </div> +</template> + +<script> +import {exportPdf, hasPermission} from "@/common/js/utils"; + import html2canvas from 'html2canvas'; + + export default { + name: "ReportHeader", + components: {}, + data() { + return {} + }, + props:{ + title:String, + historyReportId:String, + }, + created() { + }, + computed: { + readOnly() { + return !hasPermission('PROJECT_REPORT_ANALYSIS:READ+EXPORT'); + }, + isSaveAsButtonShow(){ + if(!this.historyReportId || this.historyReportId === null || this.historyReportId === ''){ + return false; + }else { + if(hasPermission('PROJECT_REPORT_ANALYSIS:READ+CREATE')){ + return true; + }else { + return false; + } + } + }, + isSaveButtonShow(){ + if(hasPermission('PROJECT_REPORT_ANALYSIS:READ+UPDATE')){ + return true; + }else { + return false; + } + } + }, + methods: { + handleExport() { + let name = this.title; + this.$nextTick(function () { + setTimeout(() => { + html2canvas(document.getElementById('reportAnalysis'), { + scale: 2 + }).then(function (canvas) { + exportPdf(name, [canvas]); + }); + }, 1000); + }); + }, + handleSave(){ + this.$emit("saveReport"); + }, + handleSaveAs(){ + this.$emit("selectAndSaveReport"); + }, + close() { + this.$emit('closePage'); + }, + }, + } +</script> + +<style scoped> + .ms-header { + border-bottom: 1px solid #E6E6E6; + background-color: #FFF; + } + + .ms-div { + float: left; + margin-left: 20px; + margin-top: 12px; + } + + .ms-span { + margin: 0px 10px 10px; + } + + .ms-header-right { + float: right; + /*width: 320px;*/ + margin-bottom: 10px; + margin-top: 10px; + margin-right: 20px; + } + + .report-alt-ico { + font-size: 17px; + top: auto; + } + + .report-alt-ico:hover { + color: black; + cursor: pointer; + font-size: 18px; + } +</style> diff --git a/frontend/src/business/components/reportstatistics/base/compose/HistoryReportDataCard.vue b/frontend/src/business/components/reportstatistics/base/compose/HistoryReportDataCard.vue new file mode 100644 index 0000000000..737f2d643f --- /dev/null +++ b/frontend/src/business/components/reportstatistics/base/compose/HistoryReportDataCard.vue @@ -0,0 +1,86 @@ +<template> + <div v-if="loadIsOver" v-loading="loading"> + <el-card class="historyCard" v-for="item in reportData" :key="item.id"> + <div slot="header"> + <li style="color:var(--count_number); font-size: 18px"> + <el-input v-if="item.isEdit === 'edit'" size="mini" @blur="updateReport(item)" v-model="item.name"/> + <el-link v-if="item.isEdit !== 'edit'" type="info" @click="selectReport(item.id)" target="_blank" style="color:#303133; font-size: 14px"> + {{ item.name }} + </el-link> + <el-button v-if="showOptionsButton && item.isEdit !== 'edit'" style="float: right; padding: 3px 0; border: 0px;margin-left: 5px" icon="el-icon-delete" size="mini" @click="deleteReport(item.id)"></el-button> + <el-button v-if="showOptionsButton && item.isEdit !== 'edit'" style="float: right; padding: 3px 0; border: 0px" icon="el-icon-edit" size="mini" @click="renameReport(item)"></el-button> + </li> + </div> + <div class="text item"> + <span>{{ item.createTime | timestampFormatDate }}</span> + </div> + </el-card> + </div> +</template> +<script> + +export default { + name: "HistoryReportDataCard", + components: {}, + data() { + return { + loadIsOver: true, + loading: false, + } + }, + props:{ + reportData:Array, + showOptionsButton:Boolean + }, + watch:{ + reportData:{ + handler:function (){ + this.loading = false; + }, + deep:true + } + }, + methods: { + reload(){ + this.loadIsOver = false; + this.$nextTick(() => { + this.loadIsOver = true; + }) + }, + deleteReport(id){ + this.loading = true; + this.$emit("deleteReport",id); + }, + renameReport(item){ + item.isEdit = 'edit'; + this.reload(); + }, + selectReport(id){ + this.$emit("selectReport",id); + }, + updateReport(item){ + let obj = { + name: item.name, + id: item.id + }; + this.$post("/history/report/updateReport", obj, response => { + }); + item.isEdit = ""; + this.reload(); + } + }, +} +</script> + +<style scoped> + +.historyCard{ + border: 0px; +} +/deep/ .el-card__header{ + border: 0px; + padding-bottom: 0px; + padding-top: 5px; +} + +</style> diff --git a/frontend/src/business/components/reportstatistics/router.js b/frontend/src/business/components/reportstatistics/router.js new file mode 100644 index 0000000000..825771c740 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/router.js @@ -0,0 +1,16 @@ +const reportForm = () => import('./ReportAnalysis'); + +export default { + path: "/report", + name: "report", + redirect: "/report/home", + components: { + content: reportForm + }, + children: [ + { + path: 'home', + name: 'reportHome', + }, + ] +} diff --git a/frontend/src/business/components/reportstatistics/testCaseCount/TestCaseCountContainer.vue b/frontend/src/business/components/reportstatistics/testCaseCount/TestCaseCountContainer.vue new file mode 100644 index 0000000000..fb5c3cf686 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/testCaseCount/TestCaseCountContainer.vue @@ -0,0 +1,217 @@ +<template> + <div> + + <el-container v-loading="loading" id="reportAnalysis" style="overflow: scroll"> + <el-container class="ms-row"> + <el-aside v-if="!isHide" :width="!isHide ?'235px':'0px'" :style="{ 'max-height': h-50 + 'px', 'margin-left': '5px'}" > + <history-report-data report-type="TEST_CASE_COUNT" + @selectReport="selectReport" @removeHistoryReportId="removeHistoryReportId" + ref="historyReport"/> + </el-aside> + <el-main class="ms-main" style="padding: 0px 5px 0px"> + <div> + <test-case-count-chart @hidePage="hidePage" @orderCharts="orderCharts" ref="analysisChart" + :chart-width="chartWidth" :load-option="loadOption" :pie-option="pieOption"/> + </div> + <div class="ms-row" v-if="!isHide"> + <test-case-count-table :group-name="getGroupNameStr(options.xaxis)" :show-coloums="options.yaxis" :tableData="tableData"/> + </div> + </el-main> + <el-aside v-if="!isHide" style="height: 100%" :width="!isHide ?'485px':'0px'"> + <test-case-count-filter @filterCharts="filterCharts" ref="countFilter"/> + </el-aside> + </el-container> + </el-container> + </div> +</template> + +<script> +import TestCaseCountChart from "./chart/TestCaseCountChart"; +import TestCaseCountTable from "@/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable"; +import TestCaseCountFilter from "./filter/TestCaseCountFilter"; +import {exportPdf,getCurrentProjectID} from "@/common/js/utils"; +import html2canvas from 'html2canvas'; +import HistoryReportData from "../base/HistoryReportData"; + +export default { + name: "TestCaseCountContainer", + components: {TestCaseCountChart, TestCaseCountTable, TestCaseCountFilter, HistoryReportData}, + data() { + return { + isHide: false, + loading: false, + options: {}, + chartWidth: 0, + tableHeight: 300, + loadOption: { + legend: {}, + xAxis: {}, + yAxis: {}, + label: {}, + tooltip: {}, + series: [] + }, + pieOption: { + legend: {}, + label: {}, + tooltip: {}, + series: [], + title: [], + }, + + tableData: [], + h: document.documentElement.clientHeight - 40, + }; + }, + methods: { + handleExport() { + let name = this.$t('commons.report_statistics.test_case_analysis'); + this.$nextTick(function () { + setTimeout(() => { + html2canvas(document.getElementById('reportAnalysis'), { + scale: 2 + }).then(function (canvas) { + exportPdf(name, [canvas]); + }); + }, 1000); + }); + }, + hidePage(isHide) { + this.isHide = isHide; + }, + close() { + this.$emit('closePage'); + }, + init(opt) { + this.loading = true; + this.options = opt; + this.$post(' /report/test/case/count/getReport', opt, response => { + let data = response.data.barChartDTO; + let pieData = response.data.pieChartDTO; + let selectTableData = response.data.tableDTOs; + this.initPic(data,pieData,selectTableData); + + },error => { + this.loading = false; + }); + }, + initPic(barData,pieData,selectTableData){ + this.loading = true; + if (barData) { + this.loadOption.legend = barData.legend; + this.loadOption.xAxis = barData.xaxis; + this.loadOption.xaxis = barData.xaxis; + this.loadOption.series = barData.series; + this.loadOption.grid = { + bottom: '75px',//距离下边距 + }; + this.loadOption.series.forEach(item => { + item.type = this.$refs.analysisChart.chartType; + }); + } + if (pieData) { + this.pieOption.legend = pieData.legend; + this.pieOption.series = pieData.series; + this.pieOption.title = pieData.title; + this.pieOption.grid = { + bottom: '75px',//距离下边距 + }; + if (pieData.width) { + this.pieOption.width = pieData.width; + this.chartWidth = pieData.width; + } + this.pieOption.series.forEach(item => { + item.type = this.$refs.analysisChart.chartType; + }); + } + if (selectTableData) { + this.tableData = selectTableData; + } + this.loading = false; + this.$refs.analysisChart.reload(); + }, + filterCharts(opt) { + this.init(opt); + }, + orderCharts(order) { + this.options.order = order; + this.filterCharts(this.options); + }, + saveReport() { + let obj = {}; + obj.projectId = getCurrentProjectID(); + obj.selectOption = JSON.stringify(this.options); + let dataOptionObj = { + loadOption: this.loadOption, + pieOption: this.pieOption, + tableData: this.tableData, + }; + obj.dataOption = JSON.stringify(dataOptionObj); + obj.reportType = 'TEST_CASE_COUNT'; + this.$post("/history/report/saveReport", obj, response => { + this.$alert(this.$t('commons.save_success')); + this.$refs.historyReport.initReportData(); + }); + }, + selectReport(selectId){ + if(selectId){ + this.loading = true; + let paramObj = { + id:selectId + } + this.$post('/history/report/selectById',paramObj, response => { + let reportData = response.data; + if(reportData){ + if(reportData.dataOption){ + let dataOptionObj = JSON.parse(reportData.dataOption); + this.initPic(dataOptionObj.loadOption,dataOptionObj.pieOption,dataOptionObj.tableData); + } + if(reportData.selectOption){ + let selectOptionObj = JSON.parse(reportData.selectOption); + this.$refs.countFilter.initSelectOption(selectOptionObj); + } + + this.loading = false; + } + }, (error) => { + this.loading = false; + }); + this.$emit('initHistoryReportId',selectId); + } + }, + removeHistoryReportId(){ + this.$emit('initHistoryReportId',""); + }, + getGroupNameStr(groupName){ + if(groupName === 'creator') { + return this.$t('commons.report_statistics.report_filter.select_options.creator'); + }else if(groupName === 'maintainer'){ + return this.$t('commons.report_statistics.report_filter.select_options.maintainer'); + }else if(groupName === 'casetype'){ + return this.$t('commons.report_statistics.report_filter.select_options.case_type'); + }else if(groupName === 'casestatus'){ + return this.$t('commons.report_statistics.report_filter.select_options.case_status'); + }else if(groupName === 'caselevel'){ + return this.$t('commons.report_statistics.report_filter.select_options.case_level'); + }else { + return ""; + } + }, + selectAndSaveReport(){ + let opt = this.$refs.countFilter.getOption(); + this.options = opt; + this.saveReport(); + } + }, +}; +</script> + +<style scoped> +.ms-row { + padding-top: 5px; +} + +/deep/ .el-main { + padding: 0px 20px 0px; +} +</style> diff --git a/frontend/src/business/components/reportstatistics/testCaseCount/chart/TestCaseCountChart.vue b/frontend/src/business/components/reportstatistics/testCaseCount/chart/TestCaseCountChart.vue new file mode 100644 index 0000000000..5ada182cfa --- /dev/null +++ b/frontend/src/business/components/reportstatistics/testCaseCount/chart/TestCaseCountChart.vue @@ -0,0 +1,284 @@ +<template> + <div v-loading="loading"> + <el-card class="ms-test-chart" :style="{ width: w+'px', height: h + 'px'}" ref="msDrawer"> + <el-row class="ms-row"> + <p class="tip"><span style="margin-left: 5px"></span> {{$t('commons.report_statistics.chart')}} </p> + <div class="ms-test-chart-header"> + <el-dropdown @command="exportCommand" :hide-on-click="false"> + <span class="el-dropdown-link"> + {{ $t('commons.export') }}<i class="el-icon-arrow-down el-icon--right"></i> + </span> + <el-dropdown-menu slot="dropdown"> + <el-dropdown-item command="jpg">JPG</el-dropdown-item> + <el-dropdown-item command="png">PNG</el-dropdown-item> + </el-dropdown-menu> + </el-dropdown> + <span style="margin: 0px 10px 10px">|</span> + <el-select v-model="chartType" class="ms-col-type" size="mini" style="width: 100px" @change="generateOption"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in charts"/> + </el-select> + <span style="margin: 0px 10px 10px">|</span> + <el-select v-model="order" class="ms-col-type" size="mini" style="width: 120px" @change="orderCharts"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in orders"/> + </el-select> + <span style="margin: 0px 10px 10px">|</span> + <font-awesome-icon v-if="!isFullScreen && showFullScreen" class="report-alt-ico" :icon="['fa', 'expand-alt']" size="lg" @click="fullScreen"/> + <font-awesome-icon v-if="isFullScreen && showFullScreen" class="report-alt-ico" :icon="['fa', 'compress-alt']" size="lg" @click="unFullScreen"/> + </div> + </el-row> + <el-row style="overflow: auto"> + <ms-chart ref="chart1" v-if="!loading" :options="dataOption" :style="{width: chartWidthNumber+'px', height: (h-50) + 'px'}" class="chart-config" :autoresize="true" id="picChart"/> + </el-row> + </el-card> + </div> +</template> + +<script> + import echarts from "echarts"; + import MsChart from "@/business/components/common/chart/MsChart"; + + export default { + name: "TestCaseCountChart", + components: {MsChart}, + props: { + loadOption: {}, + pieOption: {}, + chartWidth:Number, + }, + data() { + return { + dataOption:{}, + x: 0, + y: 0, + w: document.documentElement.clientWidth - 760, + h: document.documentElement.clientHeight * 0.5 , + chartWidthNumber:document.documentElement.clientWidth - 760, + isFullScreen: false, + originalW: 100, + originalH: 100, + showFullScreen: { + type: Boolean, + default() { + return true; + } + }, + // 头部部分 + chartType: "bar", + charts: [ + {id: 'bar', name: this.$t('commons.report_statistics.bar')}, + {id: 'pie', name: this.$t('commons.report_statistics.pie')} + ], + order: "", + orders: [{id: '', name: '默认排序'},{id: 'desc', name: this.$t('commons.report_statistics.desc')}, {id: 'asc', name: this.$t('commons.report_statistics.asc')}], + loading: false, + options: {}, + pieItemOption:{ + dataset: [{ + source: [ + ['Product', 'Sales', 'Price', 'Year'], + ['Cake', 123, 32, 2011], + ['Cereal', 231, 14, 2011], + ['Tofu', 235, 5, 2011], + ['Dumpling', 341, 25, 2011], + ['Biscuit', 122, 29, 2011], + ['Cake', 143, 30, 2012], + ['Cereal', 201, 19, 2012], + ['Tofu', 255, 7, 2012], + ['Dumpling', 241, 27, 2012], + ['Biscuit', 102, 34, 2012], + ['Cake', 153, 28, 2013], + ['Cereal', 181, 21, 2013], + ['Tofu', 395, 4, 2013], + ['Dumpling', 281, 31, 2013], + ['Biscuit', 92, 39, 2013], + ['Cake', 223, 29, 2014], + ['Cereal', 211, 17, 2014], + ['Tofu', 345, 3, 2014], + ['Dumpling', 211, 35, 2014], + ['Biscuit', 72, 24, 2014], + ], + }, { + transform: { + type: 'filter', + config: { dimension: 'Year', value: 2011 } + }, + }, { + transform: { + type: 'filter', + config: { dimension: 'Year', value: 2012 } + } + }, { + transform: { + type: 'filter', + config: { dimension: 'Year', value: 2013 } + } + }], + series: [{ + type: 'pie', radius: 50, center: ['50%', '25%'], + datasetIndex: 1 + }, { + type: 'pie', radius: 50, center: ['50%', '50%'], + datasetIndex: 2 + }, { + type: 'pie', radius: 50, center: ['50%', '75%'], + datasetIndex: 3 + }], + media: [{ + query: { minAspectRatio: 1 }, + option: { + series: [ + { center: ['25%', '50%'] }, + { center: ['50%', '50%'] }, + { center: ['75%', '50%'] } + ] + } + }, { + option: { + series: [ + { center: ['50%', '25%'] }, + { center: ['50%', '50%'] }, + { center: ['50%', '75%'] } + ] + } + }] + }, + } + }, + created() { + this.dataOption = this.loadOption; + }, + watch:{ + chartWidth(){ + this.countChartWidth(); + }, + chartType(){ + this.countChartWidth(); + } + }, + methods: { + countChartWidth(){ + if(this.chartWidth === 0 || this.chartType === 'bar'){ + this.chartWidthNumber = this.w; + }else { + this.chartWidthNumber = this.chartWidth; + } + }, + orderCharts() { + this.$emit('orderCharts', this.order); + }, + generateOption() { + if(this.chartType === 'pie'){ + this.dataOption = this.pieOption; + }else { + this.dataOption = this.loadOption; + } + this.dataOption.series.forEach(item => { + item.type = this.chartType; + }) + this.reload(); + }, + reload() { + this.loading = true + this.$nextTick(() => { + this.loading = false + }) + }, + fullScreen() { + this.originalW = this.w; + this.originalH = this.h; + this.w = document.body.clientWidth - 50; + this.h = document.body.clientHeight; + this.isFullScreen = true; + this.$emit('hidePage', true); + }, + unFullScreen() { + this.w = this.originalW; + this.h = this.originalH; + this.isFullScreen = false; + this.$emit('hidePage', false); + }, + exportCommand(command){ + let fileName = 'report_pic.'+command; + if (document.getElementById('picChart')) { + let chartsCanvas = document.getElementById('picChart').querySelectorAll('canvas')[0] + let mime = 'image/png'; + if(command === 'jpg'){ + mime = 'image/jpg'; + } + if (chartsCanvas) { + // toDataURL()是canvas对象的一种方法,用于将canvas对象转换为base64位编码 + let imageUrl = chartsCanvas && chartsCanvas.toDataURL(mime) + if (navigator.userAgent.indexOf('Trident') > -1) { + // IE11 + let arr = imageUrl.split(',') + // atob() 函数对已经使用base64编码编码的数据字符串进行解码 + let bstr = atob(arr[1]) + let bstrLen = bstr.length + // Uint8Array, 开辟 8 位无符号整数值的类型化数组。内容将初始化为 0 + let u8arr = new Uint8Array(bstrLen) + while (bstrLen--) { + // charCodeAt() 方法可返回指定位置的字符的 Unicode 编码 + u8arr[bstrLen] = bstr.charCodeAt(bstrLen) + } + // msSaveOrOpenBlob 方法允许用户在客户端上保存文件,方法如同从 Internet 下载文件,这是此类文件保存到“下载”文件夹的原因 + window.navigator.msSaveOrOpenBlob(new Blob([u8arr], {type: mime}), fileName ); + } else { + // 其他浏览器 + let $a = document.createElement('a') + $a.setAttribute('href', imageUrl) + $a.setAttribute('download', fileName) + $a.click() + } + } + } + }, + }, + } +</script> + +<style scoped> + + .ms-test-chart-header { + z-index: 999; + width: 380px; + float: right; + margin-right: 10px; + } + + .report-alt-ico { + font-size: 15px; + margin: 0px 10px 0px; + color: #8c939d; + } + + .report-alt-ico:hover { + color: black; + cursor: pointer; + font-size: 18px; + } + + /deep/ .echarts { + height: calc(100vh / 1.95); + } + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 20px 0px; + } + + .ms-row { + padding-top: 10px; + } + + .chart-config { + width: 100%; + } + + /deep/ .el-card__body { + padding: 0px; + } + +</style> diff --git a/frontend/src/business/components/reportstatistics/testCaseCount/filter/TestCaseCountFilter.vue b/frontend/src/business/components/reportstatistics/testCaseCount/filter/TestCaseCountFilter.vue new file mode 100644 index 0000000000..cf43fdbce6 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/testCaseCount/filter/TestCaseCountFilter.vue @@ -0,0 +1,401 @@ +<template> + <div v-loading="loading"> + <el-card :style="{height: h + 'px'}" class="ms-card"> + <el-row style="padding-top: 10px"> + <p class="tip"><span style="margin-left: 5px"></span> {{ $t('commons.report_statistics.options') }}</p> + </el-row> + <el-row class="ms-row"> + <p>{{ $t('commons.report_statistics.report_filter.xaxis') }}</p> + <el-select class="ms-http-select" size="small" v-model="option.xaxis" style="width: 100%"> + <el-option v-for="item in xAxisOptions" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </el-row> + <el-row class="ms-row"> + <p>{{ $t('commons.report_statistics.report_filter.yaxis') }}</p> + <el-select class="ms-http-select" size="small" v-model="option.yaxis" multiple style="width: 100%"> + <el-option v-for="item in yAxisOptions" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </el-row> + <el-row class="ms-row"> + <p>{{ $t('commons.create_time')}}</p> + <div style="width: 25%;float: left"> + <el-select class="ms-http-select" size="small" v-model="option.timeType" > + <el-option v-for="item in timeTypeOptions" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </div> + <div v-if="option.timeType === 'fixedTime'" style="width: 70%;margin-left: 20px;float: left"> + <el-date-picker + size="small" + v-model="option.times" + type="datetimerange" + value-format="timestamp" + :range-separator="$t('api_monitor.to')" + :start-placeholder="$t('commons.date.start_date')" + :end-placeholder="$t('commons.date.end_date')" + :picker-options="datePickerOptions" + style="margin-left: 10px;width: 100%"> + </el-date-picker> + </div> + <div v-if="option.timeType === 'dynamicTime'" style="width: 70%;margin-left: 20px;float: left"> + <span style="width: 20%">{{ $t('commons.report_statistics.report_filter.recently') }}</span> + <el-select class="ms-http-select" size="small" v-model="option.timeFilter.timeRange" style="width: 30%;margin-left: 10px;width: 40%"> + <el-option v-for="item in timeRangeNumberMax" :key="item" :label="item" :value="item"/> + </el-select> + <el-select class="ms-http-select" size="small" v-model="option.timeFilter.timeRangeUnit" + @change="timeRangeUnitChange" + style="width: 30%;margin-left: 10px;width: 40%"> + <el-option v-for="item in timeRangeUnitOptions" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </div> + </el-row> + <el-row class="ms-row" style="margin-left: 0px;margin-right: 0px; margin-top: 20px"> + <el-collapse v-model="collapseActiveNames"> + <el-collapse-item :title="$t('commons.report_statistics.report_filter.more_options')" name="1"> + <el-container> + <el-aside width="73px" style="overflow: hidden"> + <div v-if="option.filters.length > 1" style="height: 100%" id="moreOptionTypeDiv"> + <div class="top-line-box" :style="{ height:lineDivHeight+'px'}"> + </div> + <div> + <el-select class="ms-http-select" size="small" v-model="option.filterType" style="width: 70px"> + <el-option v-for="item in filterTypes" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </div> + <div class="bottom-line-box" :style="{ height:lineDivHeight+'px'}"> + </div> + </div> + </el-aside> + <el-main v-if="optionLoad" style="padding: 0px"> + <el-row v-for="filterItem in option.filters" :key="filterItem.id" style="margin-bottom: 5px"> + <el-col :span="24"> + <el-select style="width: 100px" class="ms-http-select" size="small" v-model="filterItem.type"> + <el-option v-for="item in getFilterOptionKey(filterItem.type)" :key="item.type" :label="item.name" :value="item.type"/> + </el-select> + <span style="margin-left:10px;margin-right:10px">{{ $t('commons.report_statistics.report_filter.belone') }}</span> + + <el-select style="width:173px" :collapse-tags="true" class="ms-http-select" size="small" multiple filterable v-model="filterItem.values" v-if="getFilterOptions(filterItem.type).length > 0"> + <el-option v-for="itemOption in getFilterOptions(filterItem.type)" :key="itemOption.id" :label="itemOption.label" :value="itemOption.id"/> + </el-select> + <el-input style="width:173px" v-model="filterItem.value" size="small" v-else ></el-input> + <el-button @click="addFilterOptions(filterItem.type)" + @keydown.enter.native.prevent + type="primary" + icon="el-icon-plus" + circle + style="color:white;padding: 0px 0.1px;width: 20px;height: 20px;margin-left:5px;" + size="mini"/> + <el-button @click="removeFilterOptions(filterItem.type)" + @keydown.enter.native.prevent + type="danger" + icon="el-icon-minus" + circle + style="color:white;padding: 0px 0.1px;width: 20px;height: 20px;margin-left:5px;" + size="mini"/> + </el-col> + </el-row> + </el-main> + </el-container> + </el-collapse-item> + </el-collapse> + </el-row> + + + <el-row type="flex"> + <el-col style="height: 100%" :span="4" > + + </el-col> + <el-col :span="20"> + + </el-col> + </el-row> + <el-row align="middle"> + <el-button style="margin-left: 200px;margin-top: 20px" type="primary" size="mini" @click="init">{{ $t('commons.confirm') }}</el-button> + </el-row> + </el-card> + </div> +</template> + +<script> +import {getCurrentProjectID, getUUID} from "@/common/js/utils"; +import MsSelectTree from "@/business/components/common/select-tree/SelectTree"; + +export default { + name: "TestAnalysisTable", + components: {MsSelectTree}, + data() { + return { + collapseActiveNames: "", + option: { + xaxis: "creator", + yaxis: ["testCase","apiCase","scenarioCase","loadCase"], + timeType: "dynamicTime", + projectId: getCurrentProjectID(), + filterType: "And", + timeFilter:{ + timeRange: 7, + timeRangeUnit: "day", + }, + times: [new Date().getTime() - 6 * 24 * 3600 * 1000, new Date().getTime()], + filters:[ + { + type:"", + name:"", + compType:"input", + isShow:false, + }, + ], + }, + h: document.documentElement.clientHeight + 80, + lineDivHeight: 0, + disabled: false, + loading: false, + optionLoad: true, + result: {}, + items: [], + modules: [], + xAxisOptions: [ + {id: 'creator', label: this.$t('commons.report_statistics.report_filter.select_options.creator')}, + {id: 'maintainer', label: this.$t('commons.report_statistics.report_filter.select_options.maintainer')}, + {id: 'casetype', label: this.$t('commons.report_statistics.report_filter.select_options.case_type')}, + {id: 'casestatus', label: this.$t('commons.report_statistics.report_filter.select_options.case_status')}, + {id: 'caselevel', label: this.$t('commons.report_statistics.report_filter.select_options.case_level')}, + ], + yAxisOptions: [ + {id: 'testCase', label: this.$t('api_test.home_page.failed_case_list.table_value.case_type.functional')}, + {id: 'apiCase', label: this.$t('api_test.home_page.failed_case_list.table_value.case_type.api')}, + {id: 'scenarioCase', label: this.$t('api_test.home_page.failed_case_list.table_value.case_type.scene')}, + {id: 'loadCase', label: this.$t('api_test.home_page.failed_case_list.table_value.case_type.load')}, + ], + filterTypes: [ + {id: 'And', label: 'And'}, + {id: 'Or', label: 'Or'}, + ], + timeTypeOptions: [ + {id: 'fixedTime', label: this.$t('commons.report_statistics.report_filter.time_options.fixed_time')}, + {id: 'dynamicTime', label: this.$t('commons.report_statistics.report_filter.time_options.dynamic_time')}, + ], + timeRangeNumberMax: 31, + timeRangeUnitOptions: [ + {id: 'day', label: this.$t('commons.report_statistics.report_filter.time_options.day')}, + {id: 'month', label: this.$t('commons.report_statistics.report_filter.time_options.month')}, + {id: 'year', label: this.$t('commons.report_statistics.report_filter.time_options.year')}, + ], + priorityFilters: [ + {id: 'P0', label: 'P0'}, + {id: 'P1', label: 'P1'}, + {id: 'P2', label: 'P2'}, + {id: 'P3', label: 'P3'} + ], + moduleObj: { + id: 'id', + label: 'name', + }, + moreOptionsSelectorKeys:[ + { + type:"casetype", + name:this.$t('commons.report_statistics.report_filter.select_options.case_type'), + }, + { + type:"creator", + name:this.$t('commons.report_statistics.report_filter.select_options.creator'), + }, + { + type:"maintainer", + name:this.$t('commons.report_statistics.report_filter.select_options.maintainer'), + }, + { + type:"casestatus", + name:this.$t('commons.report_statistics.report_filter.select_options.case_status'), + }, + { + type:"caselevel", + name:this.$t('commons.report_statistics.report_filter.select_options.case_level'), + }, + ], + moreOptionsSelectorValues: { + id: 'id', + label: 'label', + }, + datePickerOptions: { + disabledDate: (time) => { + let nowDate = new Date(); + let oneDay = 1000 * 60 * 60 * 24; + let oneYearLater = new Date(nowDate.getTime() + (oneDay * 365)); + return time.getTime() > nowDate || time.getTime() > oneYearLater;//注意是||不是&& + } + }, + }; + }, + created() { + this.init(); + this.initMoreOptionsSelectorValues(); + }, + computed: { + }, + watch: { + option: { + handler: function () { + this.$nextTick(() => { + this.lineDivHeight = 0; + // let elem = document.getElementById("moreOptionTypeDiv"); + if(this.option.filters.length > 1){ + let countPageHeight = (this.option.filters.length)* 32 + (this.option.filters.length-1)*5; + if(countPageHeight > 32){ + this.lineDivHeight = (countPageHeight-32)/2-11; + } + } + }); + }, + deep: true + } + }, + methods: { + initSelectOption(opt){ + this.loading = true; + this.option = opt; + this.$nextTick(() => { + this.loading = false; + }); + }, + addFilterOptions: function (type){ + this.optionLoad = false; + let otherOptionKeys = this.getFilterOptionKey(""); + if(otherOptionKeys.length > 0 && this.option.filters.length < 5) { + let addOptions = { + type: "", + id: getUUID(), + name: "", + compType: "selector", + isShow: false, + itemOptions: this.priorityFilters, + }; + this.option.filters.push(addOptions); + } else { + this.$alert(this.$t('commons.report_statistics.alert.cannot_add_more_options')); + } + this.$nextTick(() => { + this.optionLoad = true; + }); + }, + getFilterOptions(type){ + let optionArray = []; + if(this.moreOptionsSelectorValues && this.moreOptionsSelectorValues[type]){ + optionArray = this.moreOptionsSelectorValues[type]; + } + return optionArray; + }, + getFilterOptionKey(type){ + let optionArray = []; + for(let i = 0; i < this.moreOptionsSelectorKeys.length; i++){ + let keyObj = this.moreOptionsSelectorKeys[i]; + let inOptions = false; + if(keyObj.type !== type){ + for(let j = 0; j < this.option.filters.length; j ++){ + if(keyObj.type === this.option.filters[j].type){ + inOptions = true; + } + } + } + if(!inOptions){ + optionArray.push(keyObj); + } + } + return optionArray; + }, + removeFilterOptions: function (type){ + let removeOptionsIndex = -1; + for(let index = 0; index < this.option.filters.length; index ++){ + let item = this.option.filters[index]; + if(item.type === type){ + removeOptionsIndex = index; + } + } + if(removeOptionsIndex >= 0){ + this.option.filters.splice(removeOptionsIndex,1); + } + if(this.option.filters.length === 0){ + let addOptions = { + type: "", + id: getUUID(), + name: "", + compType: "selector", + isShow: false, + itemOptions: this.priorityFilters, + }; + this.option.filters.push(addOptions); + } + }, + init: function () { + this.$emit('filterCharts', this.option); + }, + onTimeChange() { + if (this.option.times[1] > new Date().getTime()) { + this.$alert(this.$t('commons.report_statistics.alert.end_time_cannot_over_than_start_time')); + } + }, + initMoreOptionsSelectorValues() { + let selectParam = { + projectId:getCurrentProjectID() + }; + this.$post('/report/test/case/count/initDatas',selectParam, response => { + this.moreOptionsSelectorValues = response.data; + }); + }, + timeRangeUnitChange(val){ + if(val === 'day'){ + this.timeRangeNumberMax = 31; + }else if(val === 'month'){ + this.timeRangeNumberMax = 12; + }else { + this.timeRangeNumberMax = 1; + } + this.option.timeFilter.timeRange = 1; + }, + getOption(){ + return this.option; + } + }, +}; +</script> + +<style scoped> + +.tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 10px 0px; +} + +.ms-row { + margin: 0px 10px 0px; +} + +.ms-card { + width: 480px; +} + +.top-line-box{ + border-top: 1px solid; + border-left: 1px solid; + margin-left: 32px; + margin-top: 10px; + border-top-left-radius: 10px; +} + +.bottom-line-box{ + border-bottom: 1px solid; + border-left: 1px solid; + margin-left: 32px; + border-bottom-left-radius: 10px; +} + +/deep/ .el-select__tags-text { + display: inline-block; + max-width: 50px; + overflow: hidden; + text-overflow:ellipsis; + white-space: nowrap; +} +</style> diff --git a/frontend/src/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable.vue b/frontend/src/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable.vue new file mode 100644 index 0000000000..833ec2da74 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/testCaseCount/table/TestCaseCountTable.vue @@ -0,0 +1,113 @@ +<template> + <div v-loading="loading" class="ms-div"> + <el-card :style="{ width: w+'px'}"> + <el-row style="padding-top: 10px"> + <p class="tip"><span style="margin-left: 5px"></span>{{$t('commons.report_statistics.excel')}} </p> + </el-row> + <el-row> + <el-table + :data="tableData" + :max-height="tableHeight" + :tree-props="{children: 'children', hasChildren: 'hasChildren'}" + row-key="id" + border + class="ms-table"> + <el-table-column + prop="name" + :label="groupName"> + </el-table-column> + <el-table-column + prop="allCount" + label="总计"> + </el-table-column> + <el-table-column + prop="testCaseCount" + :label="$t('api_test.home_page.failed_case_list.table_value.case_type.functional')" + v-if="isShowColumn('testCase')" + > + </el-table-column> + <el-table-column + prop="apiCaseCount" + :label="$t('api_test.home_page.failed_case_list.table_value.case_type.api')" + v-if="isShowColumn('apiCase')" + > + </el-table-column> + <el-table-column + prop="scenarioCaseCount" + :label="$t('api_test.home_page.failed_case_list.table_value.case_type.scene')" + v-if="isShowColumn('scenarioCase')" + > + </el-table-column> + <el-table-column + prop="loadCaseCount" + :label="$t('api_test.home_page.failed_case_list.table_value.case_type.load')" + v-if="isShowColumn('loadCase')" + > + </el-table-column> + </el-table> + </el-row> + </el-card> + </div> +</template> + +<script> + export default { + name: "TestAnalysisTable", + components: {}, + props: { + tableData: Array, + groupName: String, + showColoums: Array, + }, + data() { + return { + tableHeight : "100px", + w: document.documentElement.clientWidth - 760, + loading: false, + } + }, + created() { + this.getTableHeight(); + }, + methods: { + isShowColumn(type){ + if(this.showColoums){ + return this.showColoums.findIndex(item => item=== type) >= 0; + }else { + return false; + } + + }, + getTableHeight(){ + let countNumber = document.documentElement.clientHeight * 0.4 /1 - 140; + countNumber = Math.ceil(countNumber); + this.tableHeight = countNumber + 'px'; + } + }, + } +</script> + +<style scoped> + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 20px 0px; + } + + .ms-div { + margin-bottom: 20px; + } + + .ms-table { + width: 95%; + margin: 20px; + } + + /deep/ .el-card__body { + padding: 0px; + } + +</style> diff --git a/frontend/src/business/components/reportstatistics/track/TestAnalysisContainer.vue b/frontend/src/business/components/reportstatistics/track/TestAnalysisContainer.vue new file mode 100644 index 0000000000..a68a6ae9ee --- /dev/null +++ b/frontend/src/business/components/reportstatistics/track/TestAnalysisContainer.vue @@ -0,0 +1,168 @@ +<template> + <div :style="{ height: h + 'px'}"> + <el-container v-loading="loading" id="reportAnalysis" style="overflow: scroll"> + <el-container class="ms-row"> + <el-aside :width="!isHide ?'235px':'0px'" style="margin-left: 5px; max-height: 843px"> + <history-report-data report-type="TEST_CASE_ANALYSIS" + @selectReport="selectReport" @removeHistoryReportId="removeHistoryReportId" + ref="historyReport"/> + </el-aside> + <el-main class="ms-main"> + <div> + <test-analysis-chart @hidePage="hidePage" @orderCharts="orderCharts" ref="analysisChart" :load-option="loadOption"/> + </div> + <div class="ms-row" v-if="!isHide"> + <test-analysis-table :tableData="tableData"/> + </div> + </el-main> + <el-aside :width="!isHide ?'485px':'0px'"> + <test-analysis-filter @filterCharts="filterCharts" ref="analysisFilter"/> + </el-aside> + </el-container> + </el-container> + </div> +</template> + +<script> + import TestAnalysisChart from "./chart/TestAnalysisChart"; + import TestAnalysisTable from "./table/TestAnalysisTable"; + import TestAnalysisFilter from "./filter/TestAnalysisFilter"; + import {exportPdf, getCurrentProjectID} from "@/common/js/utils"; + import html2canvas from 'html2canvas'; + import HistoryReportData from "../base/HistoryReportData"; + + export default { + name: "TestAnalysisContainer", + components: {TestAnalysisChart, TestAnalysisTable, TestAnalysisFilter, HistoryReportData}, + data() { + return { + isHide: false, + loading: false, + options: {}, + loadOption: { + legend: {}, + xAxis: {}, + yAxis: {}, + label: {}, + tooltip: {}, + series: [] + }, + tableData: [], + h: document.documentElement.clientHeight - 40, + } + }, + methods: { + handleExport() { + let name = this.$t('commons.report_statistics.test_case_analysis'); + this.$nextTick(function () { + setTimeout(() => { + html2canvas(document.getElementById('reportAnalysis'), { + scale: 2 + }).then(function (canvas) { + exportPdf(name, [canvas]); + }); + }, 1000); + }); + }, + hidePage(isHide) { + this.isHide = isHide; + }, + close() { + this.$emit('closePage'); + }, + init(opt) { + this.loading = true; + this.options = opt; + this.$post(' /report/test/analysis/getReport', opt, response => { + let data = response.data.chartDTO; + let tableDTOs = response.data.tableDTOs; + this.initPic(data,tableDTOs); + }); + }, + filterCharts(opt) { + this.init(opt); + }, + orderCharts(order) { + this.options.order = order; + this.filterCharts(this.options); + }, + saveReport() { + let obj = {}; + obj.projectId = getCurrentProjectID(); + obj.selectOption = JSON.stringify(this.options); + let dataOptionObj = { + loadOption: this.loadOption, + pieOption: this.pieOption, + tableData: this.tableData, + }; + obj.dataOption = JSON.stringify(dataOptionObj); + obj.reportType = 'TEST_CASE_ANALYSIS'; + this.$post("/history/report/saveReport", obj, response => { + this.$alert(this.$t('commons.save_success')); + this.$refs.historyReport.initReportData(); + }); + }, + initPic(loadOption,tableData){ + this.loading = true; + if (loadOption) { + this.loadOption.legend = loadOption.legend; + this.loadOption.xAxis = loadOption.xAxis; + this.loadOption.series = loadOption.series; + this.loadOption.grid = { + bottom: '75px',//距离下边距 + } + this.loadOption.series.forEach(item => { + item.type = this.$refs.analysisChart.chartType; + }) + } + if (tableData) { + this.tableData = tableData; + } + this.loading = false; + }, + selectReport(selectId){ + if(selectId){ + this.loading = true; + let paramObj = { + id:selectId + } + this.$post('/history/report/selectById',paramObj, response => { + let reportData = response.data; + if(reportData){ + if(reportData.dataOption){ + let dataOptionObj = JSON.parse(reportData.dataOption); + this.initPic(dataOptionObj.loadOption,dataOptionObj.pieOption,dataOptionObj.tableData); + } + if(reportData.selectOption){ + let selectOptionObj = JSON.parse(reportData.selectOption); + this.$refs.analysisFilter.initSelectOption(selectOptionObj); + } + } + this.loading = false; + }, (error) => { + this.loading = false; + }); + this.$emit('initHistoryReportId',selectId); + } + }, + removeHistoryReportId(){ + this.$emit('initHistoryReportId',""); + }, + selectAndSaveReport(){ + let opt = this.$refs.analysisFilter.getOption(); + this.options = opt; + this.saveReport(); + } + }, + } +</script> + +<style scoped> + .ms-row { + padding-top: 10px; + } + + /deep/ .el-main { + padding: 0px 20px 0px; + } +</style> diff --git a/frontend/src/business/components/reportstatistics/track/chart/TestAnalysisChart.vue b/frontend/src/business/components/reportstatistics/track/chart/TestAnalysisChart.vue new file mode 100644 index 0000000000..cc88b0abc2 --- /dev/null +++ b/frontend/src/business/components/reportstatistics/track/chart/TestAnalysisChart.vue @@ -0,0 +1,143 @@ +<template> + <div v-loading="loading"> + <el-card class="ms-test-chart" :style="{ width: w+'px', height: h + 'px'}" ref="msDrawer"> + <el-row class="ms-row"> + <p class="tip"><span style="margin-left: 5px"></span> {{$t('commons.report_statistics.chart')}} </p> + <div class="ms-test-chart-header"> + <el-select v-model="chartType" class="ms-col-type" size="mini" style="width: 100px" @change="generateOption"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in charts"/> + </el-select> + <span style="margin: 0px 10px 10px">|</span> + <el-select v-model="order" class="ms-col-type" size="mini" style="width: 120px" @change="orderCharts"> + <el-option :key="t.id" :value="t.id" :label="t.name" v-for="t in orders"/> + </el-select> + <span style="margin: 0px 10px 10px">|</span> + <font-awesome-icon v-if="!isFullScreen && showFullScreen" class="report-alt-ico" :icon="['fa', 'expand-alt']" size="lg" @click="fullScreen"/> + <font-awesome-icon v-if="isFullScreen && showFullScreen" class="report-alt-ico" :icon="['fa', 'compress-alt']" size="lg" @click="unFullScreen"/> + </div> + </el-row> + <el-row> + <ms-chart ref="chart1" :options="loadOption" class="chart-config" :autoresize="true"/> + </el-row> + </el-card> + </div> +</template> + +<script> + // 这个引用不能删除,删除后图例不显示 + import echarts from "echarts"; + import MsChart from "@/business/components/common/chart/MsChart"; + + export default { + name: "TestAnalysisChart", + components: {MsChart}, + props: { + loadOption: {}, + }, + data() { + return { + x: 0, + y: 0, + w: document.documentElement.clientWidth - 760, + h: document.documentElement.clientHeight / 1.7, + isFullScreen: false, + originalW: 100, + originalH: 100, + showFullScreen: { + type: Boolean, + default() { + return true; + } + }, + // 头部部分 + chartType: "line", + charts: [{id: 'line', name: this.$t('commons.report_statistics.line')}, {id: 'bar', name: this.$t('commons.report_statistics.bar')}], + order: "", + orders: [{id: '', name: '默认排序'},{id: 'desc', name: this.$t('commons.report_statistics.desc')}, {id: 'asc', name: this.$t('commons.report_statistics.asc')}], + loading: false, + options: {}, + } + }, + methods: { + orderCharts() { + this.$emit('orderCharts', this.order); + }, + generateOption() { + this.loadOption.series.forEach(item => { + item.type = this.chartType; + }) + this.reload(); + }, + reload() { + this.loading = true + this.$nextTick(() => { + this.loading = false + }) + }, + fullScreen() { + this.originalW = this.w; + this.originalH = this.h; + this.w = document.body.clientWidth - 50; + this.h = document.body.clientHeight; + this.isFullScreen = true; + this.$emit('hidePage', true); + }, + unFullScreen() { + this.w = this.originalW; + this.h = this.originalH; + this.isFullScreen = false; + this.$emit('hidePage', false); + }, + getOptions(){ + return this.loadOption; + } + }, + } +</script> + +<style scoped> + + .ms-test-chart-header { + z-index: 999; + width: 320px; + float: right; + margin-right: 10px; + } + + .report-alt-ico { + font-size: 15px; + margin: 0px 10px 0px; + color: #8c939d; + } + + .report-alt-ico:hover { + color: black; + cursor: pointer; + font-size: 18px; + } + + /deep/ .echarts { + height: calc(100vh / 1.95); + } + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 20px 0px; + } + + .ms-row { + padding-top: 10px; + } + + .chart-config { + width: 100%; + } + + /deep/ .el-card__body { + padding: 0px; + } + +</style> diff --git a/frontend/src/business/components/reportstatistics/track/filter/TestAnalysisFilter.vue b/frontend/src/business/components/reportstatistics/track/filter/TestAnalysisFilter.vue new file mode 100644 index 0000000000..5cb3e70a8e --- /dev/null +++ b/frontend/src/business/components/reportstatistics/track/filter/TestAnalysisFilter.vue @@ -0,0 +1,225 @@ +<template> + <div v-loading="loading"> + <el-card :style="{height: h + 'px'}" class="ms-card"> + <el-row style="padding-top: 10px"> + <p class="tip"><span style="margin-left: 5px"></span> {{$t('commons.report_statistics.options')}}</p> + </el-row> + <el-row class="ms-row"> + <p>{{$t('commons.report_statistics.type')}}</p> + <el-checkbox v-model="option.createCase">{{$t('commons.report_statistics.add_case')}}</el-checkbox> + <el-checkbox v-model="option.updateCase">{{$t('commons.report_statistics.change_case')}}</el-checkbox> + </el-row> + <el-row class="ms-row"> + <p> {{$t('api_monitor.date')}}</p> + <el-date-picker + size="small" + v-model="option.times" + type="datetimerange" + value-format="timestamp" + range-separator="至" + start-placeholder="开始日期" + end-placeholder="结束日期" + :picker-options="datePickerOptions" + style="width: 100%"> + </el-date-picker> + </el-row> + <el-row class="ms-row"> + <p>{{$t('commons.project')}}</p> + <ms-select-tree size="small" :data="items" :default-key="projectDefaultKey" @getValue="setProjects" :obj="obj" clearable checkStrictly multiple ref="projectSelector"/> + </el-row> + <el-row class="ms-row"> + <p>{{$t('test_track.module.module')}}</p> + <ms-select-tree size="small" :data="modules" :default-key="moduleDefaultKey" :disabled="disabled" @getValue="setModules" :obj="moduleObj" clearable checkStrictly multiple ref="moduleSelector"/> + </el-row> + <el-row class="ms-row"> + <p>{{$t('api_test.automation.case_level')}}</p> + <el-select class="ms-http-select" size="small" v-model="option.prioritys" multiple style="width: 100%"> + <el-option v-for="item in priorityFilters" :key="item.id" :label="item.label" :value="item.id"/> + </el-select> + </el-row> + <el-row class="ms-row"> + <p>{{$t('test_track.case.maintainer')}}</p> + <ms-select-tree size="small" :data="maintainerOptions" :default-key="userDefaultKey" @getValue="setUsers" :obj="moduleObj" clearable checkStrictly multiple ref="userSelector"/> + </el-row> + + </el-card> + </div> +</template> + +<script> + import {getCurrentProjectID} from "@/common/js/utils"; + import MsSelectTree from "@/business/components/common/select-tree/SelectTree"; + + export default { + name: "TestAnalysisTable", + components: {MsSelectTree}, + data() { + return { + option: {createCase: true, updateCase: true, projects: [], times: [new Date().getTime() - 6 * 24 * 3600 * 1000, new Date().getTime()]}, + h: document.documentElement.clientHeight + 80, + disabled: false, + loading: false, + result: {}, + items: [], + projectDefaultKey:[], + moduleDefaultKey:[], + userDefaultKey:[], + modules: [], + maintainerOptions: [], + priorityFilters: [ + {id: 'P0', label: 'P0'}, + {id: 'P1', label: 'P1'}, + {id: 'P2', label: 'P2'}, + {id: 'P3', label: 'P3'} + ], + syncReport: true, + moduleObj: { + id: 'id', + label: 'name', + }, + obj: { + id: 'id', + label: 'label', + }, + datePickerOptions: { + disabledDate: (time) => { + let nowDate = new Date(); + let oneDay = 1000 * 60 * 60 * 24; + let oneYearLater = new Date(nowDate.getTime() + (oneDay * 365)); + return time.getTime() > nowDate || time.getTime() > oneYearLater;//注意是||不是&& + } + }, + } + }, + created() { + this.init(); + this.initUsers(); + }, + watch: { + option: { + handler: function () { + if(this.syncReport){ + this.$emit('filterCharts', this.option); + } + }, + deep: true + } + }, + methods: { + initSelectOption(opt){ + if(opt){ + this.syncReport = false; + this.loading = true; + this.option = opt; + if(opt.projects){ + this.projectDefaultKey = opt.projects; + }else { + this.projectDefaultKey = []; + } + if(opt.modules && this.projectDefaultKey.length === 1){ + this.moduleDefaultKey = opt.modules; + }else { + this.moduleDefaultKey = []; + } + if(opt.users){ + this.userDefaultKey = opt.users; + }else { + this.userDefaultKey = []; + } + this.$nextTick(() => { + this.loading = false; + this.syncReport = true; + }); + } + }, + init: function () { + this.result = this.$get("/project/listAll", response => { + let projects = response.data; + if (projects) { + this.items = []; + projects.forEach(item => { + let data = {id: item.id, label: item.name}; + this.items.push(data); + }) + } + }) + }, + onTimeChange() { + if (this.option.times[1] > new Date().getTime()) { + this.$error("结束时间不能超过当前时间"); + } + }, + initModule() { + this.result = this.$get("/case/node/list/" + this.option.projects[0], response => { + this.modules = response.data; + this.$refs.moduleSelector.setKeys(this.moduleDefaultKey); + }) + }, + initUsers() { + this.$post('/user/project/member/tester/list', {projectId: getCurrentProjectID()}, response => { + this.maintainerOptions = response.data; + }); + }, + setProjects(key, data) {//获取子组件值 + if(!key || key === ""){ + key = []; + } + this.option.projects = key; + this.modules = []; + if (key && key.length > 1) { + this.moduleDefaultKey = []; + this.disabled = true; + } else { + this.disabled = false; + } + if (this.option.projects && this.option.projects.length == 1) { + this.initModule(); + } + if(this.syncReport){ + this.$emit('filterCharts', this.option); + } + }, + setModules(key, data) {//获取子组件值 + if(!key || key === ""){ + key = []; + } + this.option.modules = key; + if(this.syncReport){ + this.$emit('filterCharts', this.option); + } + }, + setUsers(key, data) {//获取子组件值 + if(!key || key === ""){ + key = []; + } + this.option.users = key; + if(this.syncReport){ + this.$emit('filterCharts', this.option); + } + }, + getOption(){ + return this.option; + } + }, + } +</script> + +<style scoped> + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 10px 0px; + } + + .ms-row { + margin: 0px 10px 0px; + } + + .ms-card { + width: 480px; + } + +</style> diff --git a/frontend/src/business/components/reportstatistics/track/table/TestAnalysisTable.vue b/frontend/src/business/components/reportstatistics/track/table/TestAnalysisTable.vue new file mode 100644 index 0000000000..78613a4f3a --- /dev/null +++ b/frontend/src/business/components/reportstatistics/track/table/TestAnalysisTable.vue @@ -0,0 +1,77 @@ +<template> + <div v-loading="loading" class="ms-div"> + <el-card :style="{ width: w+'px'}"> + <el-row style="padding-top: 10px"> + <p class="tip"><span style="margin-left: 5px"></span>{{$t('commons.report_statistics.excel')}} </p> + </el-row> + <el-row> + <el-table + :data="tableData" + :height="h" + :tree-props="{children: 'children', hasChildren: 'hasChildren'}" + row-key="id" + border + class="ms-table"> + <el-table-column + prop="name" + :label="$t('api_monitor.date')" + sortable> + </el-table-column> + <el-table-column + prop="createCount" + :label="$t('commons.report_statistics.add_case')" + sortable> + </el-table-column> + <el-table-column + prop="updateCount" + sortable + :label="$t('commons.report_statistics.change_case')"> + </el-table-column> + </el-table> + </el-row> + </el-card> + </div> +</template> + +<script> + export default { + name: "TestAnalysisTable", + components: {}, + props: { + tableData: Array, + }, + data() { + return { + w: document.documentElement.clientWidth - 760, + h: document.body.clientHeight / 2.3 - 20, + loading: false, + } + }, + methods: {}, + } +</script> + +<style scoped> + + .tip { + float: left; + font-size: 14px; + border-radius: 2px; + border-left: 2px solid #783887; + margin: 0px 20px 0px; + } + + .ms-div { + margin-bottom: 20px; + } + + .ms-table { + width: 95%; + margin: 20px; + } + + /deep/ .el-card__body { + padding: 0px; + } + +</style> diff --git a/frontend/src/business/components/settings/operatinglog/config.js b/frontend/src/business/components/settings/operatinglog/config.js index 00175e4791..cd8f0140b9 100644 --- a/frontend/src/business/components/settings/operatinglog/config.js +++ b/frontend/src/business/components/settings/operatinglog/config.js @@ -107,11 +107,15 @@ export const sysList = [ export function getUrl(d) { let url = "/#"; let resourceId = d.sourceId; - if (resourceId && resourceId.startsWith("\"" || resourceId.startsWith("["))) { + if (resourceId && (resourceId.startsWith("\"") || resourceId.startsWith("["))) { resourceId = JSON.parse(d.sourceId); } if (resourceId instanceof Array) { - resourceId = resourceId[0]; + if (resourceId.length === 1) { + resourceId = resourceId[0]; + } else { + return url; + } } switch (d.operModule) { case "接口自动化" || "Api automation" || "接口自動化": diff --git a/frontend/src/business/components/settings/organization/components/ScheduleTaskNotification.vue b/frontend/src/business/components/settings/organization/components/ScheduleTaskNotification.vue index a44995fd80..5836d34116 100644 --- a/frontend/src/business/components/settings/organization/components/ScheduleTaskNotification.vue +++ b/frontend/src/business/components/settings/organization/components/ScheduleTaskNotification.vue @@ -167,8 +167,8 @@ export default { ' <p style="margin-left: 60px">您好:\n' + ' </div>\n' + ' <div style="margin-left: 100px">\n' + - ' <p>${testName} 接口测试运行失败/成功<br/>\n' + - ' <p>执行人:${executor}</p>' + + ' <p>${name} 接口测试运行失败/成功<br/>\n' + + ' <p>执行人:${operator}</p>' + ' <p>负责人:${principal}</p>' + ' <p>执行环境:${executionEnvironment}</p>' + ' <p>执行时间:${executionTime}</p>' + @@ -182,8 +182,8 @@ export default { '</body>\n' + '</html>', robotTitle: - "测试【任务通知】:'${testName} ${type}测试运行${status}," + "\n" + - "执行人:${executor}" + "\n" + + "测试'${name} ${type}测试运行${status}," + "\n" + + "执行人:${operator}" + "\n" + "负责人:${principal}" + "\n" + "测试环境为:${executionEnvironment}" + "\n" + "执行时间:${executionTime}" + "\n" + diff --git a/frontend/src/business/components/settings/organization/components/api/ApiAutomationNotification.vue b/frontend/src/business/components/settings/organization/components/api/ApiAutomationNotification.vue index 4a49656407..2bf68b00b9 100644 --- a/frontend/src/business/components/settings/organization/components/api/ApiAutomationNotification.vue +++ b/frontend/src/business/components/settings/organization/components/api/ApiAutomationNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}创建了测试用例</p>\n" + + " <p>${operator}创建了接口自动化${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}创建了测试用例", + robotTitle: "${operator}创建了接口自动化${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/api/ApiDefinitionNotification.vue b/frontend/src/business/components/settings/organization/components/api/ApiDefinitionNotification.vue index 76ac94c389..d9cd38cc5e 100644 --- a/frontend/src/business/components/settings/organization/components/api/ApiDefinitionNotification.vue +++ b/frontend/src/business/components/settings/organization/components/api/ApiDefinitionNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}创建了测试用例</p>\n" + + " <p>${operator}创建了接口定义${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}创建了测试用例", + robotTitle: "${operator}创建了接口定义${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/api/ApiHomeNotification.vue b/frontend/src/business/components/settings/organization/components/api/ApiHomeNotification.vue index da49d238ad..5366721461 100644 --- a/frontend/src/business/components/settings/organization/components/api/ApiHomeNotification.vue +++ b/frontend/src/business/components/settings/organization/components/api/ApiHomeNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}关闭了定时任务</p>\n" + + " <p>${operator}关闭了定时任务</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}发起了一个缺陷:${issuesName},请跟进", + robotTitle: "${operator}发起了一个缺陷:${issuesName},请跟进", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/api/ApiReportNotification.vue b/frontend/src/business/components/settings/organization/components/api/ApiReportNotification.vue index 5723464228..eab5664d21 100644 --- a/frontend/src/business/components/settings/organization/components/api/ApiReportNotification.vue +++ b/frontend/src/business/components/settings/organization/components/api/ApiReportNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}创建了测试用例</p>\n" + + " <p>${operator}删除了测试报告${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}创建了测试用例", + robotTitle: "${operator}删除了测试报告${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/jenkins/JenkinsNotification.vue b/frontend/src/business/components/settings/organization/components/jenkins/JenkinsNotification.vue index 36b8d1c353..fc04426d19 100644 --- a/frontend/src/business/components/settings/organization/components/jenkins/JenkinsNotification.vue +++ b/frontend/src/business/components/settings/organization/components/jenkins/JenkinsNotification.vue @@ -184,8 +184,8 @@ export default { ' <p style="margin-left: 60px">您好:\n' + ' </div>\n' + ' <div style="margin-left: 100px">\n' + - ' <p>${testName} 接口测试运行失败/成功<br/>\n' + - ' <p>执行人:${executor}</p>' + + ' <p>${name} 接口测试运行失败/成功<br/>\n' + + ' <p>执行人:${operator}</p>' + ' <p>执行环境:${executionEnvironment}</p>' + ' <p>执行时间:${executionTime}</p>' + ' 请点击下面链接进入测试报告页面</p>\n' + @@ -198,7 +198,7 @@ export default { '</body>\n' + '</html>', robotTitle: - "测试【任务通知】:'${executor}所执行的 ${testName} ${type}测试运行${status}\n" + + "测试'${operator}所执行的 ${name} ${type}测试运行${status}\n" + "测试环境为:${executionEnvironment}\n" + "执行时间:${executionTime}\n" + "请点击下面链接进入测试报告页面\n" + diff --git a/frontend/src/business/components/settings/organization/components/performance/PerformanceReportNotification.vue b/frontend/src/business/components/settings/organization/components/performance/PerformanceReportNotification.vue index f3d2075a52..f63ca7d47b 100644 --- a/frontend/src/business/components/settings/organization/components/performance/PerformanceReportNotification.vue +++ b/frontend/src/business/components/settings/organization/components/performance/PerformanceReportNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}创建了测试用例</p>\n" + + " <p>${operator}删除了测试报告${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}创建了测试用例", + robotTitle: "${operator}删除了测试报告${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/performance/PerformanceTestNotification.vue b/frontend/src/business/components/settings/organization/components/performance/PerformanceTestNotification.vue index 0a5cbdca7a..56da0b02f7 100644 --- a/frontend/src/business/components/settings/organization/components/performance/PerformanceTestNotification.vue +++ b/frontend/src/business/components/settings/organization/components/performance/PerformanceTestNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}创建了测试用例</p>\n" + + " <p>${operator}创建了性能测试${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}创建了测试用例", + robotTitle: "${operator}创建了性能测试${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/track/DefectTaskNotification.vue b/frontend/src/business/components/settings/organization/components/track/DefectTaskNotification.vue index 2a06d16450..2785bb3e63 100644 --- a/frontend/src/business/components/settings/organization/components/track/DefectTaskNotification.vue +++ b/frontend/src/business/components/settings/organization/components/track/DefectTaskNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}发起了一个缺陷:${issuesName},请跟进</p>\n" + + " <p>${operator}发起了一个缺陷:${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}发起了一个缺陷:${issuesName},请跟进", + robotTitle: "${operator}发起了一个缺陷:${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/track/TestCaseNotification.vue b/frontend/src/business/components/settings/organization/components/track/TestCaseNotification.vue index ad2f30e41d..d9e7f35d79 100644 --- a/frontend/src/business/components/settings/organization/components/track/TestCaseNotification.vue +++ b/frontend/src/business/components/settings/organization/components/track/TestCaseNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}创建了测试用例</p>\n" + + " <p>${operator}创建了测试用例:${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}创建了测试用例", + robotTitle: "${operator}创建了测试用例:${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/track/TestPlanTaskNotification.vue b/frontend/src/business/components/settings/organization/components/track/TestPlanTaskNotification.vue index 0c749f8b88..9dc4002aba 100644 --- a/frontend/src/business/components/settings/organization/components/track/TestPlanTaskNotification.vue +++ b/frontend/src/business/components/settings/organization/components/track/TestPlanTaskNotification.vue @@ -175,10 +175,8 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p style=\"text-align: left\">${creator} 创建的:<br>\n" + - " ${testPlanName}<br>\n" + - " 计划开始时间是:${start}<br>\n" + - " 计划结束时间为:${end}<br>\n" + + " <p style=\"text-align: left\">${operator} 创建测试计划: ${name}<br>\n" + + " <br>\n" + " 请跟进!<br>\n" + " 点击下面链接进入测试计划页面</p>\n" + " <a href=\"${url}/#/track/plan/all\">${url}/#/track/plan/all</a>\n" + @@ -186,7 +184,7 @@ export default { "</body>\n" + "</html>", robotTitle: - " 【任务通知】:${creator} 创建的:${testPlanName}计划开始时间是:${start}计划结束时间是:${end}请跟进!/ ${status}!" + + "${operator} 创建的:${name} " + "点击下面链接进入测试计划页面${url}/#/track/plan/all", testCasePlanTask: [{ taskType: "testPlanTask", diff --git a/frontend/src/business/components/settings/organization/components/track/TestReviewNotification.vue b/frontend/src/business/components/settings/organization/components/track/TestReviewNotification.vue index da9427346a..2f71eaa953 100644 --- a/frontend/src/business/components/settings/organization/components/track/TestReviewNotification.vue +++ b/frontend/src/business/components/settings/organization/components/track/TestReviewNotification.vue @@ -175,18 +175,15 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p style=\"text-align: left\">${creator} 创建的:<br>\n" + - " ${reviewName}待开始<br>\n" + - " 计划开始时间是:${start}<br>\n" + - " 计划结束时间为:${end}<br>\n" + + " <p style=\"text-align: left\">${operator} 创建的:<br>\n" + + " ${name}待开始<br>\n" + " 请跟进!/${status}<br>\n" + " 点击下面链接进入评审页面进行审核</p>\n" + " <a href=\"${url}/#/track/review/view/${id}\">${url}/#/track/review/view/${id}</a>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator} 创建的:${reviewName}待开始,计划开始时间是:${start}," + - "计划结束时间是:${end}请跟进!/ ${status}!点击下面链接进入测试评审页面${url}/#/track/review/view/${id}", + robotTitle: "${operator} 创建的:${name} 请跟进!/ ${status}!点击下面链接进入测试评审页面${url}/#/track/review/view/${id}", reviewTask: [{ taskType: "reviewTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/track/TrackHomeNotification.vue b/frontend/src/business/components/settings/organization/components/track/TrackHomeNotification.vue index f55581c0e8..ca834601b5 100644 --- a/frontend/src/business/components/settings/organization/components/track/TrackHomeNotification.vue +++ b/frontend/src/business/components/settings/organization/components/track/TrackHomeNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${operator}关闭了定时任务</p>\n" + + " <p>${operator}关闭了定时任务: ${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${operator}发起了一个缺陷:${name},请跟进", + robotTitle: "${operator}关闭了定时任务: ${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/organization/components/track/TrackReportNotification.vue b/frontend/src/business/components/settings/organization/components/track/TrackReportNotification.vue index e16c38eff1..4987b9131a 100644 --- a/frontend/src/business/components/settings/organization/components/track/TrackReportNotification.vue +++ b/frontend/src/business/components/settings/organization/components/track/TrackReportNotification.vue @@ -176,11 +176,11 @@ export default { "</head>\n" + "<body>\n" + "<div>\n" + - " <p>${creator}关闭了定时任务</p>\n" + + " <p>${operator}删除了测试报告${name}</p>\n" + "</div>\n" + "</body>\n" + "</html>", - robotTitle: "【任务通知】:${creator}发起了一个缺陷:${issuesName},请跟进", + robotTitle: "${operator}删除了测试报告${name}", defectTask: [{ taskType: "defectTask", event: "", diff --git a/frontend/src/business/components/settings/project/function/FunctionRun.vue b/frontend/src/business/components/settings/project/function/FunctionRun.vue index cc0fb097ea..db9f87dd7d 100644 --- a/frontend/src/business/components/settings/project/function/FunctionRun.vue +++ b/frontend/src/business/components/settings/project/function/FunctionRun.vue @@ -63,6 +63,9 @@ export default { if (!stepArray[i].clazzName) { stepArray[i].clazzName = TYPE_TO_C.get(stepArray[i].type); } + if (stepArray[i] && stepArray[i].authManager && !stepArray[i].authManager.clazzName) { + stepArray[i].authManager.clazzName = TYPE_TO_C.get(stepArray[i].authManager.type); + } if (stepArray[i].hashTree && stepArray[i].hashTree.length > 0) { this.sort(stepArray[i].hashTree); } diff --git a/frontend/src/business/components/settings/project/function/ScriptNavMenu.vue b/frontend/src/business/components/settings/project/function/ScriptNavMenu.vue index 1d8fcb6b70..7c0f48c6ab 100644 --- a/frontend/src/business/components/settings/project/function/ScriptNavMenu.vue +++ b/frontend/src/business/components/settings/project/function/ScriptNavMenu.vue @@ -1,7 +1,7 @@ <template> <div style="line-height: 20px;"> <div class="template-title"> - <span>{{ $t('api_test.request.processor.code_template') }}</span> + <span class="nav-font">{{ $t('api_test.request.processor.code_template') }}</span> <el-link href="https://jmeter.apache.org/usermanual/component_reference.html#BeanShell_PostProcessor" target="componentReferenceDoc" style="margin-left: 30px; margin-bottom: 3px;" type="primary"><span style="font-size: 13px;">{{ $t('commons.reference_documentation') }}</span> @@ -11,13 +11,13 @@ <span class="link-type"> <i class="icon el-icon-arrow-right" style="font-weight: bold; margin-right: 2px;" @click="active(menu)" :class="{'is-active': menu.open}"></i> - <span @click="active(menu)" class="nav-menu-title">{{menu.title}}</span> + <span @click="active(menu)" class="nav-menu-title nav-font">{{menu.title}}</span> </span> <el-collapse-transition> <div v-if="menu.open"> - <div v-for="(child, key) in menu.children" :key="key"> - <el-link :disabled="child.disabled" @click="handleClick(child)" class="func-link">{{child.title}}</el-link> + <div v-for="(child, key) in menu.children" :key="key" class="func-div"> + <el-link :disabled="child.disabled" @click="handleClick(child)" class="func-link nav-font">{{child.title}}</el-link> </div> </div> </el-collapse-transition> @@ -154,6 +154,14 @@ export default { return; } } else { + // todo 优化 + if (this.language !== 'beanshell' && this.language !== 'groovy') { + if (obj.title === this.$t('api_test.request.processor.code_add_report_length') || + obj.title === this.$t('api_test.request.processor.code_hide_report_length')) { + this.$warning("无对应的 "+ this.language +" 代码模版!"); + return; + } + } code = obj.value; } this.handleCodeTemplate(code); @@ -197,10 +205,17 @@ export default { } .func-link { - color: #935aa1; margin-left: 18px; } +.func-div >>> .func-link { + color: #935aa1; +} + +.func-div >>> .func-link:hover { + color: #935aa1; +} + .link-type { font-weight: bold; font-size: 14px; @@ -213,4 +228,8 @@ export default { .nav-menu-title:hover { cursor: pointer; } + +.nav-font { + font-size: 13px; +} </style> diff --git a/frontend/src/business/components/settings/workspace/template/CustomFiledComponent.vue b/frontend/src/business/components/settings/workspace/template/CustomFiledComponent.vue index 7118ab99f9..06d502d935 100644 --- a/frontend/src/business/components/settings/workspace/template/CustomFiledComponent.vue +++ b/frontend/src/business/components/settings/workspace/template/CustomFiledComponent.vue @@ -83,6 +83,9 @@ </el-option> </el-select> + <ms-input-tag v-else-if="data.type === 'multipleInput'" + :read-only="disabled" :currentScenario="data" :prop="prop"/> + <el-input class="custom-with" @change="handleChange" :disabled="disabled" @@ -95,9 +98,10 @@ <script> import MsTableColumn from "@/business/components/common/components/table/MsTableColumn"; import {getCurrentProjectID, getCurrentWorkspaceId} from "@/common/js/utils"; +import MsInputTag from "@/business/components/api/automation/scenario/MsInputTag"; export default { name: "CustomFiledComponent", - components: {MsTableColumn}, + components: {MsInputTag, MsTableColumn}, props: [ 'data', 'prop', diff --git a/frontend/src/business/components/track/case/components/FormRichTextItem.vue b/frontend/src/business/components/track/case/components/FormRichTextItem.vue index 8371d4379c..e31baaccca 100644 --- a/frontend/src/business/components/track/case/components/FormRichTextItem.vue +++ b/frontend/src/business/components/track/case/components/FormRichTextItem.vue @@ -80,7 +80,12 @@ export default { let el = document.getElementById(this.id); if (el) { el.addEventListener('click', () => { - this.defaultOpen = null; + let imagePreview = el.getElementsByClassName('v-note-img-wrapper'); + if (imagePreview.length > 0) { // 图片预览的时候不切换到编辑模式 + this.defaultOpen = 'preview'; + } else { + this.defaultOpen = null; + } }); let input = el.getElementsByClassName('auto-textarea-input'); input[0].addEventListener('blur', () => { @@ -117,4 +122,8 @@ export default { min-height: 20px; } +/deep/ .v-note-wrapper { + position: initial; +} + </style> diff --git a/frontend/src/business/components/track/case/components/TestCaseEdit.vue b/frontend/src/business/components/track/case/components/TestCaseEdit.vue index 90a5e20e18..4369008c61 100644 --- a/frontend/src/business/components/track/case/components/TestCaseEdit.vue +++ b/frontend/src/business/components/track/case/components/TestCaseEdit.vue @@ -383,7 +383,7 @@ } if (this.type === 'add') { //设置自定义熟悉默认值 - parseCustomField(this.form, this.testCaseTemplate, this.customFieldForm, this.customFieldRules, buildTestCaseOldFields(this.form)); + parseCustomField(this.form, this.testCaseTemplate, this.customFieldForm, this.customFieldRules); this.form.name = this.testCaseTemplate.caseName; this.form.stepDescription = this.testCaseTemplate.stepDescription; this.form.expectedResult = this.testCaseTemplate.expectedResult; @@ -504,7 +504,7 @@ this.form.maintainer = user.id; this.form.tags = []; this.getSelectOptions(); - parseCustomField(this.form, this.testCaseTemplate, this.customFieldForm, this.customFieldRules, buildTestCaseOldFields(this.form)); + parseCustomField(this.form, this.testCaseTemplate, this.customFieldForm, this.customFieldRules); this.reload(); } }, @@ -580,7 +580,7 @@ } this.form.module = testCase.nodeId; //设置自定义熟悉默认值 - parseCustomField(this.form, this.testCaseTemplate, this.customFieldForm, this.customFieldRules, buildTestCaseOldFields(this.form)); + parseCustomField(this.form, this.testCaseTemplate, this.customFieldForm, this.customFieldRules, testCase ? buildTestCaseOldFields(this.form) : null); this.setDefaultValue(); // 重新渲染,显示自定义字段的必填校验 this.reloadForm(); diff --git a/frontend/src/business/components/track/case/components/TestCaseList.vue b/frontend/src/business/components/track/case/components/TestCaseList.vue index db748f4c90..05f9d8ac2a 100644 --- a/frontend/src/business/components/track/case/components/TestCaseList.vue +++ b/frontend/src/business/components/track/case/components/TestCaseList.vue @@ -17,6 +17,8 @@ :remember-order="true" :enable-order-drag="enableOrderDrag" row-key="id" + :row-order-group-id="projectId" + :row-order-func="editTestCaseOrder" @handlePageChange="initTableData" @handleRowClick="handleEdit" :fields.sync="fields" @@ -202,13 +204,12 @@ import ReviewStatus from "@/business/components/track/case/components/ReviewStat import MsTag from "@/business/components/common/components/MsTag"; import { buildBatchParam, - checkTableRowIsSelected, deepClone, getCustomFieldBatchEditOption, getCustomFieldValue, getCustomTableWidth, getLastTableSortField, getPageInfo, - getTableHeaderWithCustomFields, handleRowDrop, + getTableHeaderWithCustomFields, initCondition, } from "@/common/js/tableUtils"; import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate"; @@ -384,6 +385,9 @@ export default { }, systemFiledMap() { return SYSTEM_FIELD_NAME_MAP; + }, + editTestCaseOrder() { + return editTestCaseOrder; } }, created: function () { @@ -579,12 +583,6 @@ export default { let data = response.data; this.page.total = data.itemCount; this.page.data = data.listObject; - if (this.$refs.table) { - this.$refs.table.clear(); - this.$nextTick(() => { - this.$refs.table.doLayout(); - }); - } this.page.data.forEach(item => { if (item.customFields) { item.customFields = JSON.parse(item.customFields); @@ -598,21 +596,9 @@ export default { } }); - checkTableRowIsSelected(this, this.$refs.table); - - this.handleRowDrop(); - }); } }, - handleRowDrop() { - this.$nextTick(() => { - handleRowDrop(this.page.data, (param) => { - param.groupId = this.projectId; - editTestCaseOrder(param); - }); - }); - }, search() { this.initTableData(); }, diff --git a/frontend/src/business/components/track/case/components/TestCaseLoadRelate.vue b/frontend/src/business/components/track/case/components/TestCaseLoadRelate.vue index ef95c84059..57ee28db51 100644 --- a/frontend/src/business/components/track/case/components/TestCaseLoadRelate.vue +++ b/frontend/src/business/components/track/case/components/TestCaseLoadRelate.vue @@ -48,7 +48,6 @@ export default { open() { this.init(); this.$refs.baseRelevance.open(); - this.$refs.apiCaseList.clear(); }, init() { if (this.$refs.apiCaseList) { diff --git a/frontend/src/business/components/track/common/minder/TestCaseMinder.vue b/frontend/src/business/components/track/common/minder/TestCaseMinder.vue index fc7bdaaa12..fb05d4b72e 100644 --- a/frontend/src/business/components/track/common/minder/TestCaseMinder.vue +++ b/frontend/src/business/components/track/common/minder/TestCaseMinder.vue @@ -129,9 +129,9 @@ name: "TestCaseMinder", }, save(data) { let saveCases = []; - let deleteCases = []; + let deleteCases = []; // 包含测试用例和临时节点 let saveExtraNode = {}; - this.buildSaveCase(data.root, saveCases, deleteCases, saveExtraNode, undefined); + this.buildSaveCase(data.root, saveCases, deleteCases, saveExtraNode); let param = { projectId: this.projectId, @@ -166,10 +166,10 @@ name: "TestCaseMinder", this.setIsChange(false); }); }, - buildSaveCase(root, saveCases, deleteCases, saveExtraNode, parent) { + buildSaveCase(root, saveCases, deleteCases, saveExtraNode, parent, preNode, nextNode) { let data = root.data; if (data.resource && data.resource.indexOf(this.$t('api_test.definition.request.case')) > -1) { - this._buildSaveCase(root, saveCases, parent); + this._buildSaveCase(root, saveCases, deleteCases, parent, preNode, nextNode); } else { let deleteChild = data.deleteChild; if (deleteChild && deleteChild.length > 0 @@ -194,17 +194,35 @@ name: "TestCaseMinder", throw new Error(tip); } if (root.children) { - root.children.forEach((childNode) => { - this.buildSaveCase(childNode, saveCases, deleteCases, saveExtraNode, root.data); - }); + for (let i = 0; i < root.children.length; i++) { + let childNode = root.children[i]; + let preNode = null; + let nextNode = null; + if (i != 0) { + preNode = root.children[i - 1]; + } + if (i + 1 < root.children.length) { + nextNode = root.children[i + 1]; + } + this.buildSaveCase(childNode, saveCases, deleteCases, saveExtraNode, root.data, preNode, nextNode); + } } } }, - _buildSaveCase(node, saveCases, parent) { + _buildSaveCase(node, saveCases, deleteCases, parent, preNode, nextNode) { let data = node.data; if (!data.text) { return; } + + if (data.isExtraNode) { + // 如果是临时节点,打上了用例标签,则删除临时节点并新建用例节点 + let deleteData = {}; + Object.assign(deleteData, data); + deleteCases.push(deleteData); + data.id = ""; + } + let isChange = false; let testCase = { id: data.id, @@ -255,7 +273,24 @@ name: "TestCaseMinder", }) } testCase.steps = JSON.stringify(steps); + if (isChange) { + + testCase.targetId = null; // 排序处理 + if (preNode) { + let preId = preNode.data.id; + if (preId && preId.length > 15) { + testCase.targetId = preId; + testCase.moveMode = 'AFTER'; + } + } else if (nextNode) { + let nextId = nextNode.data.id; + if (nextId && nextId.length > 15) { + testCase.targetId = nextId; + testCase.moveMode = 'BEFORE'; + } + } + saveCases.push(testCase); } if (testCase.nodeId !== 'root' && testCase.nodeId.length < 15) { diff --git a/frontend/src/business/components/track/common/minder/minderUtils.js b/frontend/src/business/components/track/common/minder/minderUtils.js index be8beefc2f..35a1ca1644 100644 --- a/frontend/src/business/components/track/common/minder/minderUtils.js +++ b/frontend/src/business/components/track/common/minder/minderUtils.js @@ -71,6 +71,7 @@ export function loadNode(node, param, getCaseFuc, setParamCallback, getExtraNode } } data.loaded = true; + window.minder.execCommand('expand'); } /** @@ -270,6 +271,7 @@ export function appendExtraNodes(parent, nodes) { } function _appendExtraNodes(parent, data) { + data.isExtraNode = true; let node = appendChildNode(parent, data, true); if (data.children && data.children.length > 0) { data.children.forEach(child => { diff --git a/frontend/src/business/components/track/issue/IssueEditDetail.vue b/frontend/src/business/components/track/issue/IssueEditDetail.vue index be4796b50d..09321a4f72 100644 --- a/frontend/src/business/components/track/issue/IssueEditDetail.vue +++ b/frontend/src/business/components/track/issue/IssueEditDetail.vue @@ -72,7 +72,7 @@ import CustomFieldFormList from "@/business/components/settings/workspace/templa import CustomFieldRelateList from "@/business/components/settings/workspace/template/CustomFieldRelateList"; import FormRichTextItem from "@/business/components/track/case/components/FormRichTextItem"; import {SYSTEM_FIELD_NAME_MAP} from "@/common/js/table-constants"; -import {buildCustomFields, getTemplate, parseCustomField} from "@/common/js/custom_field"; +import {buildCustomFields, parseCustomField} from "@/common/js/custom_field"; import CustomFiledComponent from "@/business/components/settings/workspace/template/CustomFiledComponent"; import TestCaseIssueList from "@/business/components/track/issue/TestCaseIssueList"; import IssueEditDetail from "@/business/components/track/issue/IssueEditDetail"; @@ -201,7 +201,7 @@ export default { this.form.creator = getCurrentUserId(); } } - parseCustomField(this.form, this.issueTemplate, this.customFieldForm, this.customFieldRules, null); + parseCustomField(this.form, this.issueTemplate, this.customFieldForm, this.customFieldRules); this.$nextTick(() => { if (this.$refs.testCaseIssueList) { this.$refs.testCaseIssueList.initTableData(); diff --git a/frontend/src/business/components/track/issue/IssueList.vue b/frontend/src/business/components/track/issue/IssueList.vue index b6631db62a..fd57441afc 100644 --- a/frontend/src/business/components/track/issue/IssueList.vue +++ b/frontend/src/business/components/track/issue/IssueList.vue @@ -134,7 +134,7 @@ <ms-table-column v-for="field in issueTemplate.customFields" :key="field.id" :field="item" :fields-width="fieldsWidth" - :label="field.name" + :label="field.system ? $t(systemNameMap[field.name]) :field.name" :prop="field.name"> <template v-slot="scope"> <span v-if="field.name === '状态'"> diff --git a/frontend/src/business/components/track/plan/components/TestPlanEdit.vue b/frontend/src/business/components/track/plan/components/TestPlanEdit.vue index 74a0388a3e..19bff06c69 100644 --- a/frontend/src/business/components/track/plan/components/TestPlanEdit.vue +++ b/frontend/src/business/components/track/plan/components/TestPlanEdit.vue @@ -3,33 +3,36 @@ <div> <el-dialog :close-on-click-modal="false" + :destroy-on-close="true" :title="operationType === 'edit' ? $t('test_track.plan.edit_plan') : $t('test_track.plan.create_plan')" :visible.sync="dialogFormVisible" @close="close" - width="65%"> + top="8vh" + width="60%"> <el-form :model="form" :rules="rules" ref="planFrom" v-if="isStepTableAlive"> - - <el-row> - <el-col :span="8" :offset="1"> + <el-row type="flex" :gutter="20"> + <el-col :span="12"> <el-form-item :label="$t('test_track.plan.plan_name')" :label-width="formLabelWidth" prop="name"> - <el-input v-model="form.name" :placeholder="$t('test_track.plan.input_plan_name')"></el-input> + <el-input v-model="form.name" :placeholder="$t('test_track.plan.input_plan_name')" :size="itemSize"></el-input> </el-form-item> </el-col> - <el-col :span="10" :offset="1"> + <el-col :span="12"> <el-form-item :label="$t('commons.tag')" :label-width="formLabelWidth" prop="tag"> - <ms-input-tag :currentScenario="form" ref="tag"/> + <ms-input-tag :currentScenario="form" ref="tag" :size="itemSize"/> </el-form-item> </el-col> </el-row> - <el-row> - <el-col :span="10" :offset="1"> - <el-form-item :label="$t('test_track.plan.plan_principal')" :label-width="formLabelWidth" prop="principal"> - <el-select v-model="form.principal" :placeholder="$t('test_track.plan.input_plan_principal')" filterable> + <el-row type="flex" :gutter="20"> + <el-col :span="12"> + <el-form-item :label="$t('test_track.plan.plan_principal')" :label-width="formLabelWidth" prop="principals"> + <el-select v-model="form.principals" :placeholder="$t('test_track.plan.input_plan_principal')" + style="width: 100%;" + filterable multiple :size="itemSize"> <el-option v-for="(item) in principalOptions" :key="item.id" @@ -42,7 +45,7 @@ <el-col :span="12"> <el-form-item :label="$t('test_track.plan.plan_stage')" :label-width="formLabelWidth" prop="stage"> - <el-select v-model="form.stage" clearable :placeholder="$t('test_track.plan.input_plan_stage')"> + <el-select v-model="form.stage" clearable :placeholder="$t('test_track.plan.input_plan_stage')" style="width: 100%;" :size="itemSize"> <el-option :label="$t('test_track.plan.smoke_test')" value="smoke"></el-option> <el-option :label="$t('test_track.plan.system_test')" value="system"></el-option> <el-option :label="$t('test_track.plan.regression_test')" value="regression"></el-option> @@ -52,31 +55,31 @@ </el-row> <!--start:xuxm增加自定义‘计划开始’,‘计划结束’时间字段--> - <el-row> - <el-col :span="8" :offset="1"> + <el-row type="flex" :gutter="20"> + <el-col :span="12"> <el-form-item :label="$t('test_track.plan.planned_start_time')" :label-width="formLabelWidth" prop="plannedStartTime"> <el-date-picker :placeholder="$t('test_track.plan.planned_start_time')" v-model="form.plannedStartTime" - type="datetime" value-format="timestamp"></el-date-picker> + type="datetime" value-format="timestamp" style="width: 100%;"></el-date-picker> </el-form-item> </el-col> - <el-col :span="11" :offset="2"> + <el-col :span="12"> <el-form-item :label="$t('test_track.plan.planned_end_time')" :label-width="formLabelWidth" prop="plannedEndTime"> <el-date-picker :placeholder="$t('test_track.plan.planned_end_time')" v-model="form.plannedEndTime" - type="datetime" value-format="timestamp"></el-date-picker> + type="datetime" value-format="timestamp" style="width: 100%;"></el-date-picker> </el-form-item> </el-col> </el-row> <!--end:xuxm增加自定义‘计划开始’,‘计划结束’时间字段--> - <el-row> - <el-col :span="8" :offset="1"> + <el-row type="flex" :gutter="20"> + <el-col :span="12"> <el-form-item :label="$t('自动更新状态')" :label-width="formLabelWidth" @@ -87,8 +90,8 @@ </el-col> </el-row> - <el-row type="flex" justify="left" style="margin-top: 10px;"> - <el-col :span="23" :offset="1"> + <el-row type="flex" justify="left" :gutter="20"> + <el-col :span="24"> <el-form-item :label="$t('commons.description')" :label-width="formLabelWidth" prop="description"> <el-input v-model="form.description" type="textarea" @@ -99,8 +102,8 @@ </el-col> </el-row> - <el-row v-if="operationType === 'edit'" type="flex" justify="left" style="margin-top: 10px;"> - <el-col :span="19" :offset="1"> + <el-row v-if="operationType === 'edit'" type="flex" justify="left" :gutter="20"> + <el-col :span="12"> <el-form-item :label="$t('test_track.plan.plan_status')" :label-width="formLabelWidth" prop="status"> <test-plan-status-button :status="form.status" @statusChange="statusChange"/> </el-form-item> @@ -147,10 +150,11 @@ export default { return { isStepTableAlive: true, dialogFormVisible: false, + itemSize: "medium", form: { name: '', projectIds: [], - principal: '', + principals: [], stage: '', description: '', plannedStartTime: '', @@ -162,7 +166,7 @@ export default { {required: true, message: this.$t('test_track.plan.input_plan_name'), trigger: 'blur'}, {max: 30, message: this.$t('test_track.length_less_than') + '30', trigger: 'blur'} ], - principal: [{required: true, message: this.$t('test_track.plan.input_plan_principal'), trigger: 'change'}], + principals: [{required: true, message: this.$t('test_track.plan.input_plan_principal'), trigger: 'change'}], stage: [{required: true, message: this.$t('test_track.plan.input_plan_stage'), trigger: 'change'}], description: [{max: 200, message: this.$t('test_track.length_less_than') + '200', trigger: 'blur'}] }, @@ -174,8 +178,6 @@ export default { created() { //设置“测试阶段”和“负责人”的默认值 this.form.stage = 'smoke'; - const adminToken = JSON.parse(localStorage.getItem("Admin-Token")); - this.form.principal = adminToken.id; }, methods: { reload() { @@ -280,8 +282,8 @@ export default { this.$refs['planFrom'].resetFields(); this.form.name = ''; this.form.projectIds = []; - const adminToken = JSON.parse(localStorage.getItem("Admin-Token")); - this.form.principal = adminToken.id; + this.form.principals = []; + this.form.automaticStatusUpdate = false; this.form.stage = 'smoke'; this.form.description = ''; this.form.status = null; diff --git a/frontend/src/business/components/track/plan/components/TestPlanList.vue b/frontend/src/business/components/track/plan/components/TestPlanList.vue index a2a1f41d64..d3263cab3a 100644 --- a/frontend/src/business/components/track/plan/components/TestPlanList.vue +++ b/frontend/src/business/components/track/plan/components/TestPlanList.vue @@ -26,7 +26,7 @@ </el-table-column> <el-table-column v-if="item.id == 'userName'" - prop="userName" + prop="principalName" :label="$t('test_track.plan.plan_principal')" show-overflow-tooltip :key="index"> @@ -216,7 +216,7 @@ </template> </el-table-column> </el-table> - <header-custom ref="headerCustom" :initTableData="inite" :optionalFields=headerItems + <header-custom ref="headerCustom" :initTableData="init" :optionalFields=headerItems :type=type></header-custom> @@ -331,7 +331,7 @@ export default { this.initTableData(); }, methods: { - inite() { + init() { this.initTableData(); }, customHeader() { @@ -358,12 +358,23 @@ export default { item.tags = JSON.parse(item.tags); } item.passRate = item.passRate + '%'; - // if (item.creator) { - // this.$get("user/info/" + item.creator, response => { - // let name = response.data.name; - // item.createUser = name; - // }); - // } + this.$get("/test/plan/principal/" + item.id, res => { + let data = res.data; + let principal = ""; + let principalIds = data.map(d => d.id); + if (data) { + data.forEach(d => { + if (principal !== "") { + principal = principal + "、" + d.name; + } else { + principal = principal + d.name; + } + }) + } + this.$set(item, "principalName", principal); + // 编辑时初始化id + this.$set(item, "principals", principalIds); + }) }); }); getLabel(this, TEST_PLAN_LIST); diff --git a/frontend/src/business/components/track/plan/view/comonents/api/TestPlanApiCaseList.vue b/frontend/src/business/components/track/plan/view/comonents/api/TestPlanApiCaseList.vue index ebf2405db2..6fc9ae48dd 100644 --- a/frontend/src/business/components/track/plan/view/comonents/api/TestPlanApiCaseList.vue +++ b/frontend/src/business/components/track/plan/view/comonents/api/TestPlanApiCaseList.vue @@ -25,6 +25,8 @@ :fields.sync="fields" :field-key="tableHeaderKey" @refresh="initTable" + :row-order-group-id="planId" + :row-order-func="editTestPlanApiCaseOrder" :enable-order-drag="enableOrderDrag" row-key="id" ref="table"> @@ -174,7 +176,7 @@ import TestPlanApiCaseResult from "./TestPlanApiCaseResult"; import {TEST_PLAN_API_CASE} from "@/common/js/constants"; import { buildBatchParam, - checkTableRowIsSelect, deepClone, getCustomTableHeader, getCustomTableWidth, handleRowDrop, + checkTableRowIsSelect, deepClone, getCustomTableHeader, getCustomTableWidth, } from "@/common/js/tableUtils"; import HeaderCustom from "@/business/components/common/head/HeaderCustom"; import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate"; @@ -341,6 +343,9 @@ export default { isApiModel() { return this.model === 'api'; }, + editTestPlanApiCaseOrder() { + return editTestPlanApiCaseOrder; + } }, methods: { customHeader() { @@ -386,14 +391,6 @@ export default { item.tags = JSON.parse(item.tags); } }); - - if (this.$refs.table) { - this.$refs.table.clear(); - setTimeout(this.$refs.table.doLayout, 200); - this.$nextTick(() => { - checkTableRowIsSelect(this, this.condition, this.tableData, this.$refs.table, this.$refs.table.selectRows); - }); - } }); } if (this.planId) { @@ -406,25 +403,9 @@ export default { item.tags = JSON.parse(item.tags); } }); - if (this.$refs.table) { - this.$refs.table.clear(); - setTimeout(this.$refs.table.doLayout, 200); - this.$nextTick(() => { - checkTableRowIsSelect(this, this.condition, this.tableData, this.$refs.table, this.$refs.table.selectRows); - }); - } - this.handleRowDrop(); }); } }, - handleRowDrop() { - this.$nextTick(() => { - handleRowDrop(this.tableData, (param) => { - param.groupId = this.planId; - editTestPlanApiCaseOrder(param); - }); - }); - }, search() { this.initTable(); }, diff --git a/frontend/src/business/components/track/plan/view/comonents/api/TestPlanApiScenarioList.vue b/frontend/src/business/components/track/plan/view/comonents/api/TestPlanApiScenarioList.vue index f222bd878f..06875572a4 100644 --- a/frontend/src/business/components/track/plan/view/comonents/api/TestPlanApiScenarioList.vue +++ b/frontend/src/business/components/track/plan/view/comonents/api/TestPlanApiScenarioList.vue @@ -21,6 +21,8 @@ :field-key="tableHeaderKey" :enable-order-drag="enableOrderDrag" row-key="id" + :row-order-func="editTestPlanScenarioCaseOrder" + :row-order-group-id="planId" @refresh="search" ref="table"> <span v-for="(item) in fields" :key="item.key"> @@ -155,7 +157,7 @@ <!-- 执行结果 --> <el-drawer :visible.sync="runVisible" :destroy-on-close="true" direction="ltr" :withHeader="true" :modal="false" size="90%"> - <ms-api-report-detail @refresh="search" :infoDb="infoDb" :report-id="reportId" :currentProjectId="projectId"/> + <ms-api-report-detail @invisible="runVisible = false" @refresh="search" :infoDb="infoDb" :report-id="reportId" :currentProjectId="projectId"/> </el-drawer> </div> </el-card> @@ -180,8 +182,7 @@ import MsTestPlanList from "../../../../../api/automation/scenario/testplan/Test import TestPlanScenarioListHeader from "./TestPlanScenarioListHeader"; import { initCondition, - buildBatchParam, - checkTableRowIsSelect, getCustomTableHeader, getCustomTableWidth, handleRowDrop + buildBatchParam, getCustomTableHeader, getCustomTableWidth } from "../../../../../../../common/js/tableUtils"; import {TEST_PLAN_SCENARIO_CASE} from "@/common/js/constants"; import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate"; @@ -290,6 +291,9 @@ export default { projectId() { return getCurrentProjectID(); }, + editTestPlanScenarioCaseOrder() { + return editTestPlanScenarioCaseOrder; + } }, created() { this.search(); @@ -344,20 +348,6 @@ export default { } }) this.loading = false; - - handleRowDrop(this.tableData, (param) => { - param.groupId = this.planId; - editTestPlanScenarioCaseOrder(param); - }); - - - if (this.$refs.table) { - this.$refs.table.selectRows.clear(); - setTimeout(this.$refs.table.doLayout, 200); - this.$nextTick(() => { - checkTableRowIsSelect(this, this.condition, this.tableData, this.$refs.table, this.$refs.table.selectRows); - }); - } }); } if (this.reviewId) { @@ -373,12 +363,6 @@ export default { } }); this.loading = false; - if (this.$refs.table) { - setTimeout(this.$refs.table.doLayout, 200); - this.$nextTick(() => { - checkTableRowIsSelect(this, this.condition, this.tableData, this.$refs.table, this.$refs.table.selectRows); - }); - } }); } }, diff --git a/frontend/src/business/components/track/plan/view/comonents/functional/FunctionalTestCaseList.vue b/frontend/src/business/components/track/plan/view/comonents/functional/FunctionalTestCaseList.vue index a73b3a4b73..dc2fc14863 100644 --- a/frontend/src/business/components/track/plan/view/comonents/functional/FunctionalTestCaseList.vue +++ b/frontend/src/business/components/track/plan/view/comonents/functional/FunctionalTestCaseList.vue @@ -34,6 +34,8 @@ :fields.sync="fields" :remember-order="true" @refresh="initTableData" + :row-order-group-id="planId" + :row-order-func="editTestPlanTestCaseOrder" :enable-order-drag="enableOrderDrag" row-key="id" ref="table"> @@ -269,9 +271,9 @@ import ClassicEditor from "@ckeditor/ckeditor5-build-classic"; import {hub} from "@/business/components/track/plan/event-bus"; import MsTag from "@/business/components/common/components/MsTag"; import { - buildBatchParam, checkTableRowIsSelected, + buildBatchParam, getCustomFieldValue, getCustomTableWidth, getLastTableSortField, - getTableHeaderWithCustomFields, handleRowDrop, + getTableHeaderWithCustomFields, initCondition, } from "@/common/js/tableUtils"; import MsTable from "@/business/components/common/components/table/MsTable"; @@ -394,6 +396,11 @@ export default { type: Array }, }, + computed: { + editTestPlanTestCaseOrder() { + return editTestPlanTestCaseOrder; + } + }, watch: { planId() { this.refreshTableAndPlan(); @@ -486,17 +493,6 @@ export default { this.$set(this.tableData[i], "issuesContent", JSON.parse(this.tableData[i].issues)); } } - - this.$nextTick(() => { - handleRowDrop(this.tableData, (param) => { - param.groupId = this.planId; - editTestPlanTestCaseOrder(param); - }); - }); - if (this.$refs.table) { - this.$refs.table.clear(); - } - checkTableRowIsSelected(this, this.$refs.table); }); } }, diff --git a/frontend/src/business/components/track/plan/view/comonents/load/TestPlanLoadCaseList.vue b/frontend/src/business/components/track/plan/view/comonents/load/TestPlanLoadCaseList.vue index 24116e5419..9ae014b118 100644 --- a/frontend/src/business/components/track/plan/view/comonents/load/TestPlanLoadCaseList.vue +++ b/frontend/src/business/components/track/plan/view/comonents/load/TestPlanLoadCaseList.vue @@ -23,6 +23,8 @@ :field-key="tableHeaderKey" :enable-order-drag="enableOrderDrag" row-key="id" + :row-order-group-id="planId" + :row-order-func="editTestPlanLoadCaseOrder" @refresh="initTable" ref="table"> <span v-for="(item) in fields" :key="item.key"> @@ -124,11 +126,10 @@ import MsTablePagination from "@/business/components/common/pagination/TablePagi import MsPerformanceTestStatus from "@/business/components/performance/test/PerformanceTestStatus"; import LoadCaseReport from "@/business/components/track/plan/view/comonents/load/LoadCaseReport"; import { - buildBatchParam, - checkTableRowIsSelect, getCustomTableHeader, getCustomTableWidth, handleRowDrop + buildBatchParam, getCustomTableHeader, getCustomTableWidth } from "@/common/js/tableUtils"; import {TEST_PLAN_LOAD_CASE} from "@/common/js/constants"; -import {getCurrentUser} from "@/common/js/utils"; +import {getCurrentProjectID, getCurrentUser, getCurrentUserId} from "@/common/js/utils"; import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOperate"; import MsPlanRunMode from "../../../common/PlanRunMode"; import MsTable from "@/business/components/common/components/table/MsTable"; @@ -223,6 +224,11 @@ export default { reviewId: String, clickType: String, }, + computed: { + editTestPlanLoadCaseOrder() { + return editTestPlanLoadCaseOrder; + } + }, created() { this.initTable(); this.refreshStatus(); @@ -259,7 +265,9 @@ export default { runArr.push({ id: loadCase.loadCaseId, testPlanLoadId: loadCase.id, - triggerMode: 'MANUAL' + userId: getCurrentUserId(), + projectId: getCurrentProjectID(), + triggerMode: 'BATCH' }); }); let obj = {config: config, requests: runArr, userId: getCurrentUser().id}; @@ -273,6 +281,8 @@ export default { runArr.push( { id: loadCase.loadCaseId, testPlanLoadId: loadCase.id, + userId: getCurrentUserId(), + projectId: getCurrentProjectID(), triggerMode: 'MANUAL' }) }); @@ -312,19 +322,6 @@ export default { let {itemCount, listObject} = data; this.total = itemCount; this.tableData = listObject; - - this.$nextTick(() => { - handleRowDrop(this.tableData, (param) => { - param.groupId = this.planId; - editTestPlanLoadCaseOrder(param); - }); - }); - if (this.$refs.table) { - setTimeout(this.$refs.table.doLayout, 200); - this.$nextTick(() => { - checkTableRowIsSelect(this, this.condition, this.tableData, this.$refs.table, this.$refs.table.selectRows); - }); - } }) } if (this.reviewId) { @@ -334,12 +331,6 @@ export default { let {itemCount, listObject} = data; this.total = itemCount; this.tableData = listObject; - if (this.$refs.table) { - setTimeout(this.$refs.table.doLayout, 200); - this.$nextTick(() => { - checkTableRowIsSelect(this, this.condition, this.tableData, this.$refs.table, this.$refs.table.selectRows); - }); - } }) } }, @@ -399,6 +390,8 @@ export default { this.$post('/test/plan/load/case/run', { id: loadCase.loadCaseId, testPlanLoadId: loadCase.id, + userId: getCurrentUserId(), + projectId: getCurrentProjectID(), triggerMode: 'MANUAL' }).then(() => { this.$notify.success({ diff --git a/frontend/src/business/components/track/plan/view/comonents/report/detail/TestPlanReportContent.vue b/frontend/src/business/components/track/plan/view/comonents/report/detail/TestPlanReportContent.vue index 0b35981865..5757d15059 100644 --- a/frontend/src/business/components/track/plan/view/comonents/report/detail/TestPlanReportContent.vue +++ b/frontend/src/business/components/track/plan/view/comonents/report/detail/TestPlanReportContent.vue @@ -238,4 +238,12 @@ export default { /deep/ .padding-col { padding: 5px; } + +/deep/ .el-scrollbar { + height: 100%; +} + +/deep/ .el-card .ms-table { + cursor: pointer; +} </style> diff --git a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiCaseFailureResult.vue b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiCaseFailureResult.vue index cce8981677..009230c9b4 100644 --- a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiCaseFailureResult.vue +++ b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiCaseFailureResult.vue @@ -3,50 +3,52 @@ <el-row class="scenario-info"> <el-col class="padding-col" :span="7"> <el-card> - <ms-table v-loading="result.loading" - :show-select-all="false" - :screen-height="null" - :enable-selection="false" - :highlight-current-row="true" - @refresh="getScenarioApiCase" - @handleRowClick="rowClick" - :data="apiCases"> + <el-scrollbar> + <ms-table v-loading="result.loading" + :show-select-all="false" + :screen-height="null" + :enable-selection="false" + :highlight-current-row="true" + @refresh="getScenarioApiCase" + @handleRowClick="rowClick" + :data="apiCases"> - <ms-table-column - :width="80" - :label="$t('commons.id')" - prop="num"> - </ms-table-column> + <ms-table-column + :width="80" + :label="$t('commons.id')" + prop="num"> + </ms-table-column> - <ms-table-column - :label="$t('commons.name')" - prop="name"> - </ms-table-column> + <ms-table-column + :label="$t('commons.name')" + prop="name"> + </ms-table-column> - <ms-table-column - :label="'创建人'" - prop="creatorName"/> + <ms-table-column + :label="'创建人'" + prop="creatorName"/> - <ms-table-column - :label="$t('test_track.case.priority')" - :width="80" - prop="priority"> - <template v-slot:default="scope"> - <priority-table-item :value="scope.row.priority" ref="priority"/> - </template> - </ms-table-column> + <ms-table-column + :label="$t('test_track.case.priority')" + :width="80" + prop="priority"> + <template v-slot:default="scope"> + <priority-table-item :value="scope.row.priority" ref="priority"/> + </template> + </ms-table-column> - <ms-table-column - :width="80" - :label="'执行结果'" - prop="lastResult"> - <template v-slot:default="scope"> - <status-table-item v-if="scope.row.execResult === 'success'" :value="'Pass'"/> - <status-table-item v-if="scope.row.execResult === 'error'" :value="'Failure'"/> - <status-table-item v-if="scope.row.execResult != 'error' && scope.row.execResult != 'success'" :value="'Prepare'"/> - </template> - </ms-table-column> - </ms-table> + <ms-table-column + :width="80" + :label="'执行结果'" + prop="lastResult"> + <template v-slot:default="scope"> + <status-table-item v-if="scope.row.execResult === 'success'" :value="'Pass'"/> + <status-table-item v-if="scope.row.execResult === 'error'" :value="'Failure'"/> + <status-table-item v-if="scope.row.execResult != 'error' && scope.row.execResult != 'success'" :value="'Prepare'"/> + </template> + </ms-table-column> + </ms-table> + </el-scrollbar> </el-card> </el-col> <el-col class="padding-col" :span="17" v-if="apiCases.length > 0"> @@ -167,4 +169,13 @@ export default { </script> <style scoped> + +.el-card >>> .el-card__body { + height: 600px; +} + +/deep/ .text-container .pane { + height: 550px !important; +} + </style> diff --git a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiScenarioCharResult.vue b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiScenarioCharResult.vue index 020d04870b..f3eb3b94f8 100644 --- a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiScenarioCharResult.vue +++ b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiScenarioCharResult.vue @@ -18,6 +18,24 @@ export default { return { visible: false, options: { + title: { + text: '场景用例数', + subtext: '55', + textAlign:'center', + y: 'center', + padding: 40, + itemGap: 5, + textStyle: { + lineHeight: 30, + fontSize: 16, + fontWeight: 500, + color: 'gray' + }, + subtextStyle: { + height: 30, + fontSize: 18, + } + }, tooltip: { trigger: 'axis', axisPointer: { @@ -38,20 +56,8 @@ export default { data: [], axisLabel: { formatter: function (value) { + return ''; }, - margin: 20, - rich: { - name: { - lineHeight: 30, - fontSize: 16, - align: 'center' - }, - count: { - height: 40, - fontSize: 20, - align: 'center', - }, - } } }, series: [ @@ -110,10 +116,8 @@ export default { this.options.series[0].data = this.data; this.options.series[0].label.formatter = formatterFuc; - let name = this.name; - this.options.yAxis.axisLabel.formatter = function (value) { - return '{name|' + name + '}\n' + '{count| ' + dataCount + '}'; - }; + this.options.title.text = this.name; + this.options.title.subtext = dataCount; this.options.legend.data = this.data.map(i => i.name); this.options.yAxis.data = this.data.map(i => i.name); diff --git a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiScenarioFailureResult.vue b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiScenarioFailureResult.vue index 3b1d1af97a..7b8f131627 100644 --- a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiScenarioFailureResult.vue +++ b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/ApiScenarioFailureResult.vue @@ -2,50 +2,52 @@ <el-row class="scenario-info"> <el-col class="padding-col" :span="8"> <el-card> - <ms-table v-loading="result.loading" - :show-select-all="false" - :screen-height="null" - :enable-selection="false" - :highlight-current-row="true" - @refresh="getScenarioApiCase" - @handleRowClick="rowClick" - :data="scenarioCases"> + <el-scrollbar> + <ms-table v-loading="result.loading" + :show-select-all="false" + :screen-height="null" + :enable-selection="false" + :highlight-current-row="true" + @refresh="getScenarioApiCase" + @handleRowClick="rowClick" + :data="scenarioCases"> - <ms-table-column - :width="80" - :label="$t('commons.id')" - prop="customNum"> - </ms-table-column> - <ms-table-column - :label="$t('commons.name')" - prop="name"> - </ms-table-column> - <ms-table-column - :label="'创建人'" - prop="creatorName"/> - <ms-table-column - :label="$t('test_track.case.priority')" - :width="80"> - <template v-slot:default="scope"> - <priority-table-item :value="scope.row.level" ref="priority"/> - </template> - </ms-table-column> - <ms-table-column - :width="70" - :label="'步骤数'" - prop="stepTotal"> - </ms-table-column> - <ms-table-column - :width="80" - :label="'执行结果'" - prop="lastResult"> - <template v-slot:default="{row}"> - <status-table-item v-if="row.lastResult === 'Success'" :value="'Pass'"/> - <status-table-item v-if="row.lastResult === 'Fail'" :value="'Failure'"/> - <status-table-item v-if="row.lastResult != 'Fail' && row.lastResult != 'Success'" :value="'Prepare'"/> - </template> - </ms-table-column> - </ms-table> + <ms-table-column + :width="80" + :label="$t('commons.id')" + prop="customNum"> + </ms-table-column> + <ms-table-column + :label="$t('commons.name')" + prop="name"> + </ms-table-column> + <ms-table-column + :label="'创建人'" + prop="creatorName"/> + <ms-table-column + :label="$t('test_track.case.priority')" + :width="80"> + <template v-slot:default="scope"> + <priority-table-item :value="scope.row.level" ref="priority"/> + </template> + </ms-table-column> + <ms-table-column + :width="70" + :label="'步骤数'" + prop="stepTotal"> + </ms-table-column> + <ms-table-column + :width="80" + :label="'执行结果'" + prop="lastResult"> + <template v-slot:default="{row}"> + <status-table-item v-if="row.lastResult === 'Success'" :value="'Pass'"/> + <status-table-item v-if="row.lastResult === 'Fail'" :value="'Failure'"/> + <status-table-item v-if="row.lastResult != 'Fail' && row.lastResult != 'Success'" :value="'Prepare'"/> + </template> + </ms-table-column> + </ms-table> + </el-scrollbar> </el-card> </el-col> <el-col :span="16" v-if="scenarioCases && scenarioCases.length > 0"> @@ -156,12 +158,11 @@ export default { padding-right: 0px; } - -.scenario-info { - height: 625px; +.el-card >>> .el-card__body { + height: 600px; } -.ms-main-container { - height: 612px; +/deep/ .ms-main-container { + height: 620px !important; } </style> diff --git a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/LoadAllResult.vue b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/LoadAllResult.vue index 134d40400b..13f5c23305 100644 --- a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/LoadAllResult.vue +++ b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/LoadAllResult.vue @@ -1,16 +1,12 @@ <template> <el-row class="scenario-info"> <el-col :span="7" class="padding-col"> - <el-card> - <load-failure-result :is-db="isDb" @rowClick="getReport" :is-all="true" :share-id="shareId" :is-share="isShare" :is-template="isTemplate" - :report="report" :plan-id="planId" @setSize="setAllSize"/> - </el-card> + <load-failure-result :class="{'init-height': !showResponse}" :is-db="isDb" @rowClick="getReport" :is-all="true" :share-id="shareId" :is-share="isShare" :is-template="isTemplate" + :report="report" :plan-id="planId" @setSize="setAllSize"/> </el-col> <el-col :span="17" class="padding-col"> - <el-card v-show="showResponse"> - <load-case-report-view :is-plan-report="true" :share-id="shareId" :is-share="isShare" + <load-case-report-view :is-plan-report="true" :share-id="shareId" :is-share="isShare" v-show="showResponse" :plan-report-template="response" :report-id="reportId" ref="loadCaseReportView"/> - </el-card> <div class="empty" v-show="!showResponse">内容为空</div> </el-col> </el-row> @@ -90,5 +86,7 @@ export default { </script> <style scoped> - +.init-height >>> .el-card__body { + height: 600px !important; +} </style> diff --git a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/LoadFailureResult.vue b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/LoadFailureResult.vue index 3538da1ef4..9ea85cd2af 100644 --- a/frontend/src/business/components/track/plan/view/comonents/report/detail/component/LoadFailureResult.vue +++ b/frontend/src/business/components/track/plan/view/comonents/report/detail/component/LoadFailureResult.vue @@ -1,47 +1,51 @@ <template> - <el-table - row-key="id" - @row-click="rowClick" - :highlight-current-row="true" - :data="loadTestCases"> - <el-table-column - prop="num" - :label="$t('commons.id')" - show-overflow-tooltip> - <template v-slot:default="{row}"> - {{row.isCustomNum ? row.customNum : row.num }} - </template> - </el-table-column> - <el-table-column - prop="name" - :label="$t('commons.name')" - show-overflow-tooltip> - </el-table-column> + <el-card> + <el-scrollbar> + <el-table + row-key="id" + @row-click="rowClick" + :highlight-current-row="true" + :data="loadTestCases"> + <el-table-column + prop="num" + :label="$t('commons.id')" + show-overflow-tooltip> + <template v-slot:default="{row}"> + {{row.isCustomNum ? row.customNum : row.num }} + </template> + </el-table-column> + <el-table-column + prop="name" + :label="$t('commons.name')" + show-overflow-tooltip> + </el-table-column> - <el-table-column - prop="userName" - :label="$t('commons.create_user')"> - </el-table-column> + <el-table-column + prop="userName" + :label="$t('commons.create_user')"> + </el-table-column> - <el-table-column - prop="status" - column-key="status" - :label="$t('test_track.plan_view.execute_result')"> - <template v-slot:default="{row}"> - <el-tag size="mini" type="danger" v-if="row.status === 'error'"> - {{ row.status }} - </el-tag> - <el-tag size="mini" type="success" v-else-if="row.status === 'success'"> - {{ row.status }} - </el-tag> - <el-tag size="mini" v-else-if="row.status === 'run'"> - {{ row.status }} - </el-tag> - <span v-else>-</span> - </template> - </el-table-column> + <el-table-column + prop="status" + column-key="status" + :label="$t('test_track.plan_view.execute_result')"> + <template v-slot:default="{row}"> + <el-tag size="mini" type="danger" v-if="row.status === 'error'"> + {{ row.status }} + </el-tag> + <el-tag size="mini" type="success" v-else-if="row.status === 'success'"> + {{ row.status }} + </el-tag> + <el-tag size="mini" v-else-if="row.status === 'run'"> + {{ row.status }} + </el-tag> + <span v-else>-</span> + </template> + </el-table-column> - </el-table> + </el-table> + </el-scrollbar> + </el-card> </template> <script> @@ -127,5 +131,7 @@ export default { </script> <style scoped> - +.el-card >>> .el-card__body { + height: 860px; +} </style> diff --git a/frontend/src/business/components/track/report/components/TestPlanReportList.vue b/frontend/src/business/components/track/report/components/TestPlanReportList.vue index 611067ed9f..a72883801f 100644 --- a/frontend/src/business/components/track/report/components/TestPlanReportList.vue +++ b/frontend/src/business/components/track/report/components/TestPlanReportList.vue @@ -86,7 +86,7 @@ import { _filter, _handleSelect, _handleSelectAll, - _sort, checkTableRowIsSelect, + _sort, getSelectDataCounts, initCondition, setUnSelectIds, toggleAllSelection,saveLastTableSortField,getLastTableSortField @@ -173,12 +173,6 @@ export default { let data = response.data; this.total = data.itemCount; this.tableData = data.listObject; - if (this.$refs.testPlanReportTable) { - // setTimeout(this.$refs.testPlanReportTable,200); - } - this.$nextTick(() => { - checkTableRowIsSelect(this,this.condition,this.tableData,this.$refs.testPlanReportTable,this.selectRows); - }); }); }, buildPagePath(path) { diff --git a/frontend/src/business/components/track/review/view/components/TestReviewTestCaseList.vue b/frontend/src/business/components/track/review/view/components/TestReviewTestCaseList.vue index fd50097324..7b8202f564 100644 --- a/frontend/src/business/components/track/review/view/components/TestReviewTestCaseList.vue +++ b/frontend/src/business/components/track/review/view/components/TestReviewTestCaseList.vue @@ -30,6 +30,9 @@ @handleRowClick="showDetail" :fields.sync="fields" :remember-order="true" + :enable-order-drag="enableOrderDrag" + :row-order-func="editTestReviewTestCaseOrder" + :row-order-group-id="reviewId" @refresh="initTableData" ref="table" > @@ -170,7 +173,7 @@ import TestReviewTestCaseEdit from "./TestReviewTestCaseEdit"; import ReviewStatus from "@/business/components/track/case/components/ReviewStatus"; import { _handleSelectAll, - buildBatchParam, checkTableRowIsSelected, deepClone, getCustomTableWidth, getLastTableSortField, + buildBatchParam, deepClone, getCustomTableWidth, getLastTableSortField, getSelectDataCounts, getTableHeaderWithCustomFields, initCondition, toggleAllSelection @@ -181,6 +184,7 @@ import HeaderLabelOperate from "@/business/components/common/head/HeaderLabelOpe import MsTableHeaderSelectPopover from "@/business/components/common/components/table/MsTableHeaderSelectPopover"; import MsTableColumn from "@/business/components/common/components/table/MsTableColumn"; import MsTable from "@/business/components/common/components/table/MsTable"; +import {editTestReviewTestCaseOrder} from "@/network/testCase"; export default { name: "TestReviewTestCaseList", @@ -208,6 +212,7 @@ export default { currentPage: 1, pageSize: 10, total: 0, + enableOrderDrag: true, selectRows: new Set(), testReview: {}, isReadOnly: false, @@ -286,6 +291,9 @@ export default { computed: { selectNodeIds() { return this.$store.state.testReviewSelectNodeIds; + }, + editTestReviewTestCaseOrder() { + return editTestReviewTestCaseOrder; } }, created() { @@ -321,6 +329,8 @@ export default { } this.status = 'all'; } + this.enableOrderDrag = (this.condition.orders && this.condition.orders.length) > 0 ? false : true; + this.condition.nodeIds = this.selectNodeIds; if (this.reviewId) { this.result = this.$post(this.buildPagePath('/test/review/case/list'), this.condition, response => { @@ -328,7 +338,6 @@ export default { this.total = data.itemCount; this.tableData = data.listObject; this.tableClear(); - checkTableRowIsSelected(this, this.$refs.table); }); } diff --git a/frontend/src/business/components/xpack b/frontend/src/business/components/xpack index 183d61c236..71e705f48d 160000 --- a/frontend/src/business/components/xpack +++ b/frontend/src/business/components/xpack @@ -1 +1 @@ -Subproject commit 183d61c23620452e3fe0ba56574adf5ce45207a7 +Subproject commit 71e705f48da10eb37135b189582ca0c917a2ffb3 diff --git a/frontend/src/common/css/main.css b/frontend/src/common/css/main.css index bf2f9d3a22..cde3fe414c 100644 --- a/frontend/src/common/css/main.css +++ b/frontend/src/common/css/main.css @@ -296,3 +296,7 @@ textarea { .ms-full-loading .el-loading-spinner { font-size: 16px; } + +.ace-chrome .ace_print-margin { + width: 0px !important; +} diff --git a/frontend/src/common/js/table-constants.js b/frontend/src/common/js/table-constants.js index f21d51006c..d14c794df7 100644 --- a/frontend/src/common/js/table-constants.js +++ b/frontend/src/common/js/table-constants.js @@ -12,7 +12,8 @@ export const CUSTOM_FIELD_TYPE_OPTION = [ {value: 'multipleMember',text: '多选成员'}, {value: 'data',text: '日期'}, {value: 'int',text: '整型'}, - {value: 'float',text: '浮点型'} + {value: 'float',text: '浮点型'}, + {value: 'multipleInput',text: '多值输入框'} ]; export const CUSTOM_FIELD_SCENE_OPTION = [ @@ -43,7 +44,8 @@ export const FIELD_TYPE_MAP = { multipleMember: '多选成员', data: '日期', int: '整型', - float: '浮点型' + float: '浮点型', + multipleInput: '多值输入框' }; export const SCENE_MAP = { diff --git a/frontend/src/common/js/tableUtils.js b/frontend/src/common/js/tableUtils.js index 8312bfeb75..fed8f15340 100644 --- a/frontend/src/common/js/tableUtils.js +++ b/frontend/src/common/js/tableUtils.js @@ -529,8 +529,13 @@ export function getCustomFieldBatchEditOption(customFields, typeArr, valueArr, m export function handleRowDrop(data, callback) { setTimeout(() => { const tbody = document.querySelector('.el-table__body-wrapper tbody'); + if (!tbody) { + return; + } const dropBars = tbody.getElementsByClassName('table-row-drop-bar'); + const msTable = document.getElementsByClassName('ms-table'); + // 每次调用生成一个class // 避免增删列表数据时,回调函数中的 data 与实际 data 不一致 let dropClass = 'table-row-drop-bar-random' + '_' + getUUID(); @@ -542,6 +547,14 @@ export function handleRowDrop(data, callback) { Sortable.create(tbody, { handle: "." + dropClass, animation: 100, + onStart: function (/**Event*/evt) { + // 解决拖拽时高亮阴影停留在原位置的问题 + if (msTable) { + msTable.forEach(table => { + table.classList.add('disable-hover'); + }); + } + }, onEnd({ newIndex, oldIndex}) { let param = {}; param.moveId = data[oldIndex].id; @@ -566,6 +579,12 @@ export function handleRowDrop(data, callback) { callback(param); } } + + msTable.forEach(table => { + if (msTable) { + table.classList.remove('disable-hover'); + } + }); } }); }, 100); diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index e0e6684b0c..e03bc3969e 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -43,6 +43,7 @@ export default { annotation: 'Annotation', clear: 'Clear', save: 'Save', + save_as: 'Save as', update: 'Update', save_success: 'Saved successfully', delete_success: 'Deleted successfully', diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index 7ad15e8b90..92ce7bcaf8 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -43,6 +43,7 @@ export default { annotation: '注释', clear: '清空', save: '保存', + save_as: '另存为', update: '更新', save_success: '保存成功', delete_success: '删除成功', diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index 83ec904f64..522716d107 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -43,6 +43,7 @@ export default { annotation: '註釋', clear: '清空', save: '保存', + save_as: '另存為', update: '更新', save_success: '保存成功', delete_success: '刪除成功', diff --git a/frontend/src/network/testCase.js b/frontend/src/network/testCase.js index 5d40a732e5..a423a60819 100644 --- a/frontend/src/network/testCase.js +++ b/frontend/src/network/testCase.js @@ -59,3 +59,7 @@ export function getMinderExtraNode(groupId, nodeId, callback) { return baseGet('/minder/extra/node/list/' + groupId + '/' + nodeId, callback); } +export function editTestReviewTestCaseOrder(request, callback) { + return basePost('/test/review/case/edit/order', request, callback); +} +