Merge branch 'master' into local-api-delimit

# Conflicts:
#	frontend/package.json
This commit is contained in:
fit2-zhao 2020-11-13 09:43:20 +08:00
commit 5005cbdd85
48 changed files with 1649 additions and 789 deletions

View File

@ -20,6 +20,7 @@ MeterSphere 是一站式的开源企业级持续测试平台,涵盖测试跟
![产品定位](https://metersphere.io/images/icon/ct-devops.png) ![产品定位](https://metersphere.io/images/icon/ct-devops.png)
> 如需进一步了解 MeterSphere 开源项目,推荐阅读 [MeterSphere 的初心和使命](https://mp.weixin.qq.com/s/DpCt3BNgBTlV3sJ5qtPmZw) > 如需进一步了解 MeterSphere 开源项目,推荐阅读 [MeterSphere 的初心和使命](https://mp.weixin.qq.com/s/DpCt3BNgBTlV3sJ5qtPmZw)
## 在线体验 ## 在线体验

View File

@ -39,10 +39,12 @@ public class APIReportController {
return apiReportService.recentTest(request); return apiReportService.recentTest(request);
} }
@GetMapping("/list/{testId}") @GetMapping("/list/{testId}/{goPage}/{pageSize}")
public List<APIReportResult> listByTestId(@PathVariable String testId) { public Pager<List<APIReportResult>> listByTestId(@PathVariable String testId, @PathVariable int goPage, @PathVariable int pageSize) {
checkOwnerService.checkApiTestOwner(testId); checkOwnerService.checkApiTestOwner(testId);
return apiReportService.listByTestId(testId); Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
return PageUtils.setPageInfo(page, apiReportService.listByTestId(testId));
} }
@PostMapping("/list/{goPage}/{pageSize}") @PostMapping("/list/{goPage}/{pageSize}")

View File

@ -1,5 +1,6 @@
package io.metersphere.api.dto.scenario; package io.metersphere.api.dto.scenario;
import io.metersphere.api.dto.scenario.assertions.Assertions;
import io.metersphere.api.dto.scenario.request.Request; import io.metersphere.api.dto.scenario.request.Request;
import lombok.Data; import lombok.Data;
@ -15,6 +16,7 @@ public class Scenario {
private List<KeyValue> variables; private List<KeyValue> variables;
private List<KeyValue> headers; private List<KeyValue> headers;
private List<Request> requests; private List<Request> requests;
private Assertions assertions;
private DubboConfig dubboConfig; private DubboConfig dubboConfig;
private TCPConfig tcpConfig; private TCPConfig tcpConfig;
private List<DatabaseConfig> databaseConfigs; private List<DatabaseConfig> databaseConfigs;

View File

@ -104,6 +104,12 @@ public class PerformanceTestController {
return performanceTestService.getLoadConfiguration(testId); return performanceTestService.getLoadConfiguration(testId);
} }
@GetMapping("/get-jmx-content/{testId}")
public String getJmxContent(@PathVariable String testId) {
checkOwnerService.checkPerformanceTestOwner(testId);
return performanceTestService.getJmxContent(testId);
}
@PostMapping("/delete") @PostMapping("/delete")
public void delete(@RequestBody DeleteTestPlanRequest request) { public void delete(@RequestBody DeleteTestPlanRequest request) {
checkOwnerService.checkPerformanceTestOwner(request.getId()); checkOwnerService.checkPerformanceTestOwner(request.getId());

View File

@ -18,6 +18,7 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public abstract class AbstractEngine implements Engine { public abstract class AbstractEngine implements Engine {
@ -81,9 +82,22 @@ public abstract class AbstractEngine implements Engine {
String loadConfiguration = t.getLoadConfiguration(); String loadConfiguration = t.getLoadConfiguration();
JSONArray jsonArray = JSON.parseArray(loadConfiguration); JSONArray jsonArray = JSON.parseArray(loadConfiguration);
for (int i = 0; i < jsonArray.size(); i++) { for (int i = 0; i < jsonArray.size(); i++) {
JSONObject o = jsonArray.getJSONObject(i); if (jsonArray.get(i) instanceof Map) {
if (StringUtils.equals(o.getString("key"), "TargetLevel")) { JSONObject o = jsonArray.getJSONObject(i);
s = o.getInteger("value"); if (StringUtils.equals(o.getString("key"), "TargetLevel")) {
s = o.getInteger("value");
break;
}
}
if (jsonArray.get(i) instanceof List) {
JSONArray o = jsonArray.getJSONArray(i);
for (int j = 0; j < o.size(); j++) {
JSONObject b = o.getJSONObject(j);
if (StringUtils.equals(b.getString("key"), "TargetLevel")) {
s += b.getInteger("value");
break;
}
}
} }
} }
return s; return s;

View File

@ -10,7 +10,6 @@ public class EngineContext {
private String fileType; private String fileType;
private String content; private String content;
private String resourcePoolId; private String resourcePoolId;
private Long threadNum;
private Long startTime; private Long startTime;
private String reportId; private String reportId;
private Integer resourceIndex; private Integer resourceIndex;
@ -95,14 +94,6 @@ public class EngineContext {
this.resourcePoolId = resourcePoolId; this.resourcePoolId = resourcePoolId;
} }
public Long getThreadNum() {
return threadNum;
}
public void setThreadNum(Long threadNum) {
this.threadNum = threadNum;
}
public Long getStartTime() { public Long getStartTime() {
return startTime; return startTime;
} }

View File

@ -9,6 +9,7 @@ import io.metersphere.base.domain.TestResourcePool;
import io.metersphere.commons.constants.FileType; import io.metersphere.commons.constants.FileType;
import io.metersphere.commons.constants.ResourcePoolTypeEnum; import io.metersphere.commons.constants.ResourcePoolTypeEnum;
import io.metersphere.commons.exception.MSException; import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.config.KafkaProperties; import io.metersphere.config.KafkaProperties;
import io.metersphere.i18n.Translator; import io.metersphere.i18n.Translator;
import io.metersphere.performance.engine.docker.DockerTestEngine; import io.metersphere.performance.engine.docker.DockerTestEngine;
@ -22,6 +23,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -52,7 +54,7 @@ public class EngineFactory {
return null; return null;
} }
public static EngineContext createContext(LoadTestWithBLOBs loadTest, String resourceId, long threadNum, long startTime, String reportId, int resourceIndex) { public static EngineContext createContext(LoadTestWithBLOBs loadTest, String resourceId, double ratio, long startTime, String reportId, int resourceIndex) {
final List<FileMetadata> fileMetadataList = fileService.getFileMetadataByTestId(loadTest.getId()); final List<FileMetadata> fileMetadataList = fileService.getFileMetadataByTestId(loadTest.getId());
if (org.springframework.util.CollectionUtils.isEmpty(fileMetadataList)) { 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") + loadTest.getId());
@ -73,7 +75,6 @@ public class EngineFactory {
engineContext.setTestName(loadTest.getName()); engineContext.setTestName(loadTest.getName());
engineContext.setNamespace(loadTest.getProjectId()); engineContext.setNamespace(loadTest.getProjectId());
engineContext.setFileType(jmxFile.getType()); engineContext.setFileType(jmxFile.getType());
engineContext.setThreadNum(threadNum);
engineContext.setResourcePoolId(loadTest.getTestResourcePoolId()); engineContext.setResourcePoolId(loadTest.getTestResourcePoolId());
engineContext.setStartTime(startTime); engineContext.setStartTime(startTime);
engineContext.setReportId(reportId); engineContext.setReportId(reportId);
@ -90,8 +91,34 @@ public class EngineFactory {
final JSONArray jsonArray = JSONObject.parseArray(loadTest.getLoadConfiguration()); final JSONArray jsonArray = JSONObject.parseArray(loadTest.getLoadConfiguration());
for (int i = 0; i < jsonArray.size(); i++) { for (int i = 0; i < jsonArray.size(); i++) {
final JSONObject jsonObject = jsonArray.getJSONObject(i); if (jsonArray.get(i) instanceof Map) {
engineContext.addProperty(jsonObject.getString("key"), jsonObject.get("value")); JSONObject o = jsonArray.getJSONObject(i);
String key = o.getString("key");
if ("TargetLevel".equals(key)) {
engineContext.addProperty(key, Math.round(((Integer) o.get("value")) * ratio));
} else {
engineContext.addProperty(key, o.get("value"));
}
}
if (jsonArray.get(i) instanceof List) {
JSONArray o = jsonArray.getJSONArray(i);
for (int j = 0; j < o.size(); j++) {
JSONObject b = o.getJSONObject(j);
String key = b.getString("key");
Object values = engineContext.getProperty(key);
if (values == null) {
values = new ArrayList<>();
}
if (values instanceof List) {
Object value = b.get("value");
if ("TargetLevel".equals(key)) {
value = Math.round(((Integer) b.get("value")) * ratio);
}
((List<Object>) values).add(value);
engineContext.addProperty(key, values);
}
}
}
} }
} }
/* /*
@ -112,8 +139,10 @@ public class EngineFactory {
String content = engineSourceParser.parse(engineContext, source); String content = engineSourceParser.parse(engineContext, source);
engineContext.setContent(content); engineContext.setContent(content);
} catch (MSException e) { } catch (MSException e) {
LogUtil.error(e);
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
LogUtil.error(e);
MSException.throwException(e); MSException.throwException(e);
} }

View File

@ -6,6 +6,7 @@ import io.metersphere.base.domain.TestResource;
import io.metersphere.commons.constants.ResourceStatusEnum; import io.metersphere.commons.constants.ResourceStatusEnum;
import io.metersphere.commons.exception.MSException; import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.CommonBeanFactory; import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.controller.ResultHolder; import io.metersphere.controller.ResultHolder;
import io.metersphere.dto.NodeDTO; import io.metersphere.dto.NodeDTO;
import io.metersphere.i18n.Translator; import io.metersphere.i18n.Translator;
@ -52,19 +53,21 @@ public class DockerTestEngine extends AbstractEngine {
for (int i = 0, size = resourceList.size(); i < size; i++) { for (int i = 0, size = resourceList.size(); i < size; i++) {
int ratio = resourceRatio.get(i); int ratio = resourceRatio.get(i);
double realThreadNum = ((double) ratio / totalThreadNum) * threadNum; // double realThreadNum = ((double) ratio / totalThreadNum) * threadNum;
runTest(resourceList.get(i), Math.round(realThreadNum), i); runTest(resourceList.get(i), ((double) ratio / totalThreadNum), i);
} }
} }
private void runTest(TestResource resource, long realThreadNum, int resourceIndex) { private void runTest(TestResource resource, double ratio, int resourceIndex) {
EngineContext context = null; EngineContext context = null;
try { try {
context = EngineFactory.createContext(loadTest, resource.getId(), realThreadNum, this.getStartTime(), this.getReportId(), resourceIndex); context = EngineFactory.createContext(loadTest, resource.getId(), ratio, this.getStartTime(), this.getReportId(), resourceIndex);
} catch (MSException e) { } catch (MSException e) {
LogUtil.error(e);
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
LogUtil.error(e);
MSException.throwException(e); MSException.throwException(e);
} }
@ -80,6 +83,7 @@ public class DockerTestEngine extends AbstractEngine {
TestRequest testRequest = new TestRequest(); TestRequest testRequest = new TestRequest();
testRequest.setSize(1); testRequest.setSize(1);
testRequest.setTestId(testId); testRequest.setTestId(testId);
testRequest.setReportId(getReportId());
testRequest.setFileString(content); testRequest.setFileString(content);
testRequest.setImage(JMETER_IMAGE); testRequest.setImage(JMETER_IMAGE);
testRequest.setTestData(context.getTestData()); testRequest.setTestData(context.getTestData());

View File

@ -7,4 +7,5 @@ import lombok.Setter;
@Setter @Setter
public class BaseRequest { public class BaseRequest {
private String testId; private String testId;
private String reportId;
} }

View File

@ -776,15 +776,12 @@ public class JmeterDocumentParser implements DocumentParser {
elementProp.setAttribute("name", "ThreadGroup.main_controller"); elementProp.setAttribute("name", "ThreadGroup.main_controller");
elementProp.setAttribute("elementType", "com.blazemeter.jmeter.control.VirtualUserController"); elementProp.setAttribute("elementType", "com.blazemeter.jmeter.control.VirtualUserController");
threadGroup.appendChild(elementProp); threadGroup.appendChild(elementProp);
// 持续时长
String duration = context.getProperty("duration").toString();
String rampUp = context.getProperty("RampUp").toString();
int realHold = Integer.parseInt(duration) - Integer.parseInt(rampUp);
threadGroup.appendChild(createStringProp(document, "ThreadGroup.on_sample_error", "continue")); threadGroup.appendChild(createStringProp(document, "ThreadGroup.on_sample_error", "continue"));
threadGroup.appendChild(createStringProp(document, "TargetLevel", "2")); threadGroup.appendChild(createStringProp(document, "TargetLevel", "2"));
threadGroup.appendChild(createStringProp(document, "RampUp", "12")); threadGroup.appendChild(createStringProp(document, "RampUp", "12"));
threadGroup.appendChild(createStringProp(document, "Steps", "2")); threadGroup.appendChild(createStringProp(document, "Steps", "2"));
threadGroup.appendChild(createStringProp(document, "Hold", String.valueOf(realHold))); threadGroup.appendChild(createStringProp(document, "Hold", "1"));
threadGroup.appendChild(createStringProp(document, "LogFilename", "")); threadGroup.appendChild(createStringProp(document, "LogFilename", ""));
// bzm - Concurrency Thread Group "Thread Iterations Limit:" 设置为空 // bzm - Concurrency Thread Group "Thread Iterations Limit:" 设置为空
// threadGroup.appendChild(createStringProp(document, "Iterations", "1")); // threadGroup.appendChild(createStringProp(document, "Iterations", "1"));
@ -803,9 +800,18 @@ public class JmeterDocumentParser implements DocumentParser {
</collectionProp> </collectionProp>
</kg.apc.jmeter.timers.VariableThroughputTimer> </kg.apc.jmeter.timers.VariableThroughputTimer>
*/ */
if (context.getProperty("rpsLimitEnable") == null || StringUtils.equals(context.getProperty("rpsLimitEnable").toString(), "false")) { if (context.getProperty("rpsLimitEnable") == null) {
return; return;
} }
Object rpsLimitEnables = context.getProperty("rpsLimitEnable");
if (rpsLimitEnables instanceof List) {
Object o = ((List<?>) rpsLimitEnables).get(0);
((List<?>) rpsLimitEnables).remove(0);
if (o == null || "false".equals(o.toString())) {
return;
}
}
Document document = element.getOwnerDocument(); Document document = element.getOwnerDocument();
@ -866,11 +872,6 @@ public class JmeterDocumentParser implements DocumentParser {
if (nodeNameEquals(ele, STRING_PROP)) { if (nodeNameEquals(ele, STRING_PROP)) {
parseStringProp(ele); parseStringProp(ele);
} }
// 设置具体的线程数
if (nodeNameEquals(ele, STRING_PROP) && "TargetLevel".equals(ele.getAttribute("name"))) {
ele.getFirstChild().setNodeValue(context.getThreadNum().toString());
}
} }
} }
} }
@ -902,11 +903,28 @@ public class JmeterDocumentParser implements DocumentParser {
stringPropCount++; stringPropCount++;
} else { } else {
stringPropCount = 0; stringPropCount = 0;
Integer duration = (Integer) context.getProperty("duration");// 传入的是分钟数, 需要转化成秒数 Object durations = context.getProperty("duration");// 传入的是分钟数, 需要转化成秒数
Integer duration;
if (durations instanceof List) {
Object o = ((List<?>) durations).get(0);
duration = (Integer) o;
((List<?>) durations).remove(0);
} else {
duration = (Integer) durations;
}
prop.getFirstChild().setNodeValue(String.valueOf(duration * 60)); prop.getFirstChild().setNodeValue(String.valueOf(duration * 60));
continue; continue;
} }
prop.getFirstChild().setNodeValue(context.getProperty("rpsLimit").toString()); Object rpsLimits = context.getProperty("rpsLimit");
String rpsLimit;
if (rpsLimits instanceof List) {
Object o = ((List<?>) rpsLimits).get(0);
((List<?>) rpsLimits).remove(0);
rpsLimit = o.toString();
} else {
rpsLimit = rpsLimits.toString();
}
prop.getFirstChild().setNodeValue(rpsLimit);
} }
} }
} }
@ -920,8 +938,15 @@ public class JmeterDocumentParser implements DocumentParser {
} }
private void parseStringProp(Element stringProp) { private void parseStringProp(Element stringProp) {
if (stringProp.getChildNodes().getLength() > 0 && context.getProperty(stringProp.getAttribute("name")) != null) { Object threadParams = context.getProperty(stringProp.getAttribute("name"));
stringProp.getFirstChild().setNodeValue(context.getProperty(stringProp.getAttribute("name")).toString()); if (stringProp.getChildNodes().getLength() > 0 && threadParams != null) {
if (threadParams instanceof List) {
Object o = ((List<?>) threadParams).get(0);
((List<?>) threadParams).remove(0);
stringProp.getFirstChild().setNodeValue(o.toString());
} else {
stringProp.getFirstChild().setNodeValue(threadParams.toString());
}
} }
} }

View File

@ -345,6 +345,17 @@ public class PerformanceTestService {
return Optional.ofNullable(loadTestWithBLOBs).orElse(new LoadTestWithBLOBs()).getLoadConfiguration(); return Optional.ofNullable(loadTestWithBLOBs).orElse(new LoadTestWithBLOBs()).getLoadConfiguration();
} }
public String getJmxContent(String testId) {
List<FileMetadata> fileMetadataList = fileService.getFileMetadataByTestId(testId);
for (FileMetadata metadata : fileMetadataList) {
if (FileType.JMX.name().equals(metadata.getType())) {
FileContent fileContent = fileService.getFileContent(metadata.getId());
return new String(fileContent.getFile());
}
}
return null;
}
public List<LoadTestWithBLOBs> selectByTestResourcePoolId(String resourcePoolId) { public List<LoadTestWithBLOBs> selectByTestResourcePoolId(String resourcePoolId) {
LoadTestExample example = new LoadTestExample(); LoadTestExample example = new LoadTestExample();
example.createCriteria().andTestResourcePoolIdEqualTo(resourcePoolId); example.createCriteria().andTestResourcePoolIdEqualTo(resourcePoolId);

View File

@ -45,6 +45,10 @@ public class CheckOwnerService {
} }
public void checkApiTestOwner(String testId) { public void checkApiTestOwner(String testId) {
// 关联为其他时
if (StringUtils.equals("other", testId)) {
return;
}
String workspaceId = SessionUtils.getCurrentWorkspaceId(); String workspaceId = SessionUtils.getCurrentWorkspaceId();
QueryAPITestRequest request = new QueryAPITestRequest(); QueryAPITestRequest request = new QueryAPITestRequest();
request.setWorkspaceId(workspaceId); request.setWorkspaceId(workspaceId);

View File

@ -191,17 +191,19 @@ public class TestCaseNodeService {
* 获取当前计划下 * 获取当前计划下
* 有关联数据的节点 * 有关联数据的节点
* *
* @param planId * @param planId plan id
* @return * @return List<TestCaseNodeDTO>
*/ */
public List<TestCaseNodeDTO> getNodeByPlanId(String planId) { public List<TestCaseNodeDTO> getNodeByPlanId(String planId) {
List<TestCaseNodeDTO> list = new ArrayList<>(); List<TestCaseNodeDTO> list = new ArrayList<>();
List<String> projectIds = testPlanProjectService.getProjectIdsByPlanId(planId); List<String> projectIds = testPlanProjectService.getProjectIdsByPlanId(planId);
projectIds.forEach(id -> { projectIds.forEach(id -> {
String name = projectMapper.selectByPrimaryKey(id).getName(); Project project = projectMapper.selectByPrimaryKey(id);
String name = project.getName();
List<TestCaseNodeDTO> nodeList = getNodeDTO(id, planId); List<TestCaseNodeDTO> nodeList = getNodeDTO(id, planId);
TestCaseNodeDTO testCaseNodeDTO = new TestCaseNodeDTO(); TestCaseNodeDTO testCaseNodeDTO = new TestCaseNodeDTO();
testCaseNodeDTO.setId(project.getId());
testCaseNodeDTO.setName(name); testCaseNodeDTO.setName(name);
testCaseNodeDTO.setLabel(name); testCaseNodeDTO.setLabel(name);
testCaseNodeDTO.setChildren(nodeList); testCaseNodeDTO.setChildren(nodeList);

View File

@ -1,3 +1,3 @@
for file in ${TESTS_DIR}/*.jmx; do for file in ${TESTS_DIR}/*.jmx; do
jmeter -n -t ${file} -Jserver.rmi.ssl.disable=${SSL_DISABLED} jmeter -n -t ${file} -Jserver.rmi.ssl.disable=${SSL_DISABLED} -l ${TESTS_DIR}/${REPORT_ID}.jtl
done done

View File

@ -17,28 +17,29 @@
"@fortawesome/vue-fontawesome": "^0.1.9", "@fortawesome/vue-fontawesome": "^0.1.9",
"axios": "^0.19.0", "axios": "^0.19.0",
"core-js": "^3.4.3", "core-js": "^3.4.3",
"diffable-html": "^4.0.0",
"echarts": "^4.6.0", "echarts": "^4.6.0",
"el-table-infinite-scroll": "^1.0.10",
"element-ui": "^2.13.0", "element-ui": "^2.13.0",
"html2canvas": "^1.0.0-rc.7",
"js-base64": "^3.4.4",
"json-bigint": "^1.0.0",
"jsoneditor": "^9.1.2",
"jspdf": "^2.1.1",
"md5": "^2.3.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"sha.js": "^2.4.11",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-calendar-heatmap": "^0.8.4",
"vue-echarts": "^4.1.0", "vue-echarts": "^4.1.0",
"vue-i18n": "^8.15.3", "vue-i18n": "^8.15.3",
"vue-pdf": "^4.2.0",
"vue-router": "^3.1.3", "vue-router": "^3.1.3",
"vuedraggable": "^2.23.2", "vuedraggable": "^2.23.2",
"vuex": "^3.1.2", "vuex": "^3.1.2",
"yan-progress": "^1.0.3" "vue-calendar-heatmap": "^0.8.4",
"mockjs": "^1.1.0",
"md5": "^2.3.0",
"sha.js": "^2.4.11",
"js-base64": "^3.4.4",
"json-bigint": "^1.0.0",
"html2canvas": "^1.0.0-rc.7",
"jspdf": "^2.1.1",
"yan-progress": "^1.0.3",
"nprogress": "^0.2.0",
"el-table-infinite-scroll": "^1.0.10",
"vue-pdf": "^4.2.0",
"diffable-html": "^4.0.0",
"xml-js": "^1.6.11",
"jsoneditor": "^9.1.2",
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0", "@vue/cli-plugin-babel": "^4.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -126,6 +126,7 @@ export default {
let data = response.data; let data = response.data;
this.total = data.itemCount; this.total = data.itemCount;
this.tableData = data.listObject; this.tableData = data.listObject;
this.selectRows.clear();
}); });
}, },
handleSelectionChange(val) { handleSelectionChange(val) {
@ -171,28 +172,13 @@ export default {
this.$set(row, "showMore", true); this.$set(row, "showMore", true);
this.selectRows.add(row); this.selectRows.add(row);
} }
let arr = Array.from(this.selectRows);
// 1
if (this.selectRows.size === 1) {
this.$set(arr[0], "showMore", false);
} else if (this.selectRows.size === 2) {
arr.forEach(row => {
this.$set(row, "showMore", true);
})
}
}, },
handleSelectAll(selection) { handleSelectAll(selection) {
if (selection.length > 0) { if (selection.length > 0) {
if (selection.length === 1) { this.tableData.forEach(item => {
this.selectRows.add(selection[0]); this.$set(item, "showMore", true);
} else { this.selectRows.add(item);
this.tableData.forEach(item => { });
this.$set(item, "showMore", true);
this.selectRows.add(item);
});
}
} else { } else {
this.selectRows.clear(); this.selectRows.clear();
this.tableData.forEach(row => { this.tableData.forEach(row => {

View File

@ -22,46 +22,57 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<ms-table-pagination :change="search" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import MsApiReportStatus from "../report/ApiReportStatus"; import MsApiReportStatus from "../report/ApiReportStatus";
import MsTablePagination from "@/business/components/common/pagination/TablePagination";
export default { export default {
name: "MsApiReportDialog", name: "MsApiReportDialog",
components: {MsApiReportStatus}, components: {MsApiReportStatus, MsTablePagination},
props: ["testId"], props: ["testId"],
data() { data() {
return { return {
reportVisible: false, reportVisible: false,
result: {}, result: {},
tableData: [], tableData: [],
loading: false loading: false,
} currentPage: 1,
pageSize: 5,
total: 0,
}
},
methods: {
open() {
this.reportVisible = true;
this.search();
}, },
link(row) {
this.reportVisible = false;
methods: { this.$router.push({
open() { path: '/api/report/view/' + row.id,
this.reportVisible = true; })
let url = "/api/report/list/" + this.testId;
this.result = this.$get(url, response => {
this.tableData = response.data;
});
},
link(row) {
this.reportVisible = false;
this.$router.push({
path: '/api/report/view/' + row.id,
})
}
}, },
} search() {
let url = "/api/report/list/" + this.testId + "/" + this.currentPage + "/" + this.pageSize;
this.result = this.$get(url, response => {
let data = response.data;
this.total = data.itemCount;
this.tableData = data.listObject;
});
},
},
}
</script> </script>
<style scoped> <style scoped>

View File

@ -76,28 +76,28 @@
</template> </template>
<script> <script>
import MsApiScenarioConfig from "./components/ApiScenarioConfig"; import MsApiScenarioConfig from "./components/ApiScenarioConfig";
import {Scenario, Test} from "./model/ScenarioModel" import {Scenario, Test} from "./model/ScenarioModel"
import MsApiReportStatus from "../report/ApiReportStatus"; import MsApiReportStatus from "../report/ApiReportStatus";
import MsApiReportDialog from "./ApiReportDialog"; import MsApiReportDialog from "./ApiReportDialog";
import {checkoutTestManagerOrTestUser, downloadFile, getUUID} from "@/common/js/utils"; import {checkoutTestManagerOrTestUser, downloadFile, getUUID} from "@/common/js/utils";
import MsScheduleConfig from "../../common/components/MsScheduleConfig"; import MsScheduleConfig from "../../common/components/MsScheduleConfig";
import ApiImport from "./components/import/ApiImport"; import ApiImport from "./components/import/ApiImport";
import {ApiEvent, LIST_CHANGE} from "@/business/components/common/head/ListEvent"; import {ApiEvent, LIST_CHANGE} from "@/business/components/common/head/ListEvent";
import MsContainer from "@/business/components/common/components/MsContainer"; import MsContainer from "@/business/components/common/components/MsContainer";
import MsMainContainer from "@/business/components/common/components/MsMainContainer"; import MsMainContainer from "@/business/components/common/components/MsMainContainer";
import MsJarConfig from "./components/jar/JarConfig"; import MsJarConfig from "./components/jar/JarConfig";
export default { export default {
name: "MsApiTestConfig", name: "MsApiTestConfig",
components: { components: {
MsJarConfig, MsJarConfig,
MsMainContainer, MsMainContainer,
MsContainer, ApiImport, MsScheduleConfig, MsApiReportDialog, MsApiReportStatus, MsApiScenarioConfig MsContainer, ApiImport, MsScheduleConfig, MsApiReportDialog, MsApiReportStatus, MsApiScenarioConfig
}, },
props: ["id"], props: ["id"],
data() { data() {
return { return {
@ -305,6 +305,7 @@
}, },
cancel() { cancel() {
this.$router.push('/api/test/list/all'); this.$router.push('/api/test/list/all');
// console.log(this.test.toJMX().xml);
}, },
handleCommand(command) { handleCommand(command) {
switch (command) { switch (command) {

View File

@ -52,6 +52,9 @@
<el-tab-pane :label="$t('api_test.environment.tcp_config')" name="tcp"> <el-tab-pane :label="$t('api_test.environment.tcp_config')" name="tcp">
<ms-tcp-config :config="scenario.tcpConfig" :is-read-only="isReadOnly"/> <ms-tcp-config :config="scenario.tcpConfig" :is-read-only="isReadOnly"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
<ms-api-assertions :scenario="scenario" :is-read-only="isReadOnly" :assertions="scenario.assertions"/>
</el-tab-pane>
</el-tabs> </el-tabs>
<api-environment-config ref="environmentConfig" @close="environmentConfigClose"/> <api-environment-config ref="environmentConfig" @close="environmentConfigClose"/>
@ -72,6 +75,7 @@ import MsDubboConsumerService from "@/business/components/api/test/components/re
import MsDatabaseConfig from "./request/database/DatabaseConfig"; import MsDatabaseConfig from "./request/database/DatabaseConfig";
import {parseEnvironment} from "../model/EnvironmentModel"; import {parseEnvironment} from "../model/EnvironmentModel";
import MsTcpConfig from "@/business/components/api/test/components/request/tcp/TcpConfig"; import MsTcpConfig from "@/business/components/api/test/components/request/tcp/TcpConfig";
import MsApiAssertions from "@/business/components/api/test/components/assertion/ApiAssertions";
export default { export default {
name: "MsApiScenarioForm", name: "MsApiScenarioForm",
@ -79,7 +83,8 @@ export default {
MsTcpConfig, MsTcpConfig,
MsDatabaseConfig, MsDatabaseConfig,
MsDubboConsumerService, MsDubboConsumerService,
MsDubboConfigCenter, MsDubboRegistryCenter, ApiEnvironmentConfig, MsApiScenarioVariables, MsApiKeyValue MsDubboConfigCenter, MsDubboRegistryCenter, ApiEnvironmentConfig, MsApiScenarioVariables, MsApiKeyValue,
MsApiAssertions
}, },
props: { props: {
scenario: Scenario, scenario: Scenario,

View File

@ -3,7 +3,8 @@
<div class="assertion-add"> <div class="assertion-add">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="4"> <el-col :span="4">
<el-select :disabled="isReadOnly" class="assertion-item" v-model="type" :placeholder="$t('api_test.request.assertions.select_type')" <el-select :disabled="isReadOnly" class="assertion-item" v-model="type"
:placeholder="$t('api_test.request.assertions.select_type')"
size="small"> size="small">
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/> <el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/> <el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
@ -14,13 +15,18 @@
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="20"> <el-col :span="20">
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/> <ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT"
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/> :callback="after"/>
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/> <ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX"
<ms-api-assertion-x-path2 :is-read-only="isReadOnly" :list="assertions.xpath2" v-if="type === options.XPATH2" :callback="after"/> :callback="after"/>
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath"
v-if="type === options.JSON_PATH" :callback="after"/>
<ms-api-assertion-x-path2 :is-read-only="isReadOnly" :list="assertions.xpath2" v-if="type === options.XPATH2"
:callback="after"/>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration" <ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
v-if="type === options.DURATION" :callback="after"/> v-if="type === options.DURATION" :callback="after"/>
<ms-api-assertion-jsr223 :is-read-only="isReadOnly" :list="assertions.jsr223" v-if="type === options.JSR223" :callback="after"/> <ms-api-assertion-jsr223 :is-read-only="isReadOnly" :list="assertions.jsr223" v-if="type === options.JSR223"
:callback="after"/>
<el-button v-if="!type" :disabled="true" type="primary" size="small"> <el-button v-if="!type" :disabled="true" type="primary" size="small">
{{ $t('api_test.request.assertions.add') }} {{ $t('api_test.request.assertions.add') }}
</el-button> </el-button>
@ -28,125 +34,128 @@
</el-row> </el-row>
</div> </div>
<div> <div v-if="!scenario">
<el-row :gutter="10" class="json-path-suggest-button"> <el-row :gutter="10" class="json-path-suggest-button">
<el-button size="small" type="primary" @click="suggestJsonOpen"> <el-button size="small" type="primary" @click="suggestJsonOpen">
{{$t('api_test.request.assertions.json_path_suggest')}} {{ $t('api_test.request.assertions.json_path_suggest') }}
</el-button> </el-button>
<el-button size="small" type="danger" @click="clearJson"> <el-button size="small" type="danger" @click="clearJson">
{{$t('api_test.request.assertions.json_path_clear')}} {{ $t('api_test.request.assertions.json_path_clear') }}
</el-button> </el-button>
</el-row> </el-row>
</div> </div>
<ms-api-jsonpath-suggest-list @addJsonpathSuggest="addJsonpathSuggest" :request="request" ref="jsonpathSuggestList"/> <ms-api-jsonpath-suggest-list @addJsonpathSuggest="addJsonpathSuggest" :request="request"
ref="jsonpathSuggestList"/>
<ms-api-assertions-edit :is-read-only="isReadOnly" :assertions="assertions"/> <ms-api-assertions-edit :is-read-only="isReadOnly" :assertions="assertions"/>
</div> </div>
</template> </template>
<script> <script>
import MsApiAssertionText from "./ApiAssertionText"; import MsApiAssertionText from "./ApiAssertionText";
import MsApiAssertionRegex from "./ApiAssertionRegex"; import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionDuration from "./ApiAssertionDuration"; import MsApiAssertionDuration from "./ApiAssertionDuration";
import {ASSERTION_TYPE, Assertions, HttpRequest, JSONPath} from "../../model/ScenarioModel"; import {ASSERTION_TYPE, Assertions, HttpRequest, JSONPath, Scenario} from "../../model/ScenarioModel";
import MsApiAssertionsEdit from "./ApiAssertionsEdit"; import MsApiAssertionsEdit from "./ApiAssertionsEdit";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath"; import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223"; import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223";
import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList"; import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList";
import MsApiAssertionXPath2 from "./ApiAssertionXPath2"; import MsApiAssertionXPath2 from "./ApiAssertionXPath2";
export default { export default {
name: "MsApiAssertions", name: "MsApiAssertions",
components: { components: {
MsApiAssertionXPath2, MsApiAssertionXPath2,
MsApiAssertionJsr223, MsApiAssertionJsr223,
MsApiJsonpathSuggestList, MsApiJsonpathSuggestList,
MsApiAssertionJsonPath, MsApiAssertionJsonPath,
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText}, MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText
},
props: { props: {
assertions: Assertions, assertions: Assertions,
request: HttpRequest, request: HttpRequest,
isReadOnly: { scenario: Scenario,
type: Boolean, isReadOnly: {
default: false type: Boolean,
} default: false
},
data() {
return {
options: ASSERTION_TYPE,
time: "",
type: "",
}
},
methods: {
after() {
this.type = "";
},
suggestJsonOpen() {
if (!this.request.debugRequestResult) {
this.$message(this.$t('api_test.request.assertions.debug_first'));
return;
}
this.$refs.jsonpathSuggestList.open();
},
addJsonpathSuggest(jsonPathList) {
jsonPathList.forEach(jsonPath => {
let jsonItem = new JSONPath();
jsonItem.expression = jsonPath.json_path;
jsonItem.expect = jsonPath.json_value;
jsonItem.setJSONPathDescription();
this.assertions.jsonPath.push(jsonItem);
});
},
clearJson() {
this.assertions.jsonPath = [];
}
} }
},
data() {
return {
options: ASSERTION_TYPE,
time: "",
type: "",
}
},
methods: {
after() {
this.type = "";
},
suggestJsonOpen() {
if (!this.request.debugRequestResult) {
this.$message(this.$t('api_test.request.assertions.debug_first'));
return;
}
this.$refs.jsonpathSuggestList.open();
},
addJsonpathSuggest(jsonPathList) {
jsonPathList.forEach(jsonPath => {
let jsonItem = new JSONPath();
jsonItem.expression = jsonPath.json_path;
jsonItem.expect = jsonPath.json_value;
jsonItem.setJSONPathDescription();
this.assertions.jsonPath.push(jsonItem);
});
},
clearJson() {
this.assertions.jsonPath = [];
}
} }
}
</script> </script>
<style scoped> <style scoped>
.assertion-item { .assertion-item {
width: 100%; width: 100%;
} }
.assertion-add { .assertion-add {
padding: 10px; padding: 10px;
border: #DCDFE6 solid 1px; border: #DCDFE6 solid 1px;
margin: 5px 0; margin: 5px 0;
border-radius: 5px; border-radius: 5px;
} }
.bg-purple-dark { .bg-purple-dark {
background: #99a9bf; background: #99a9bf;
} }
.bg-purple { .bg-purple {
background: #d3dce6; background: #d3dce6;
} }
.bg-purple-light { .bg-purple-light {
background: #e5e9f2; background: #e5e9f2;
} }
.grid-content { .grid-content {
border-radius: 4px; border-radius: 4px;
min-height: 36px; min-height: 36px;
} }
.row-bg { .row-bg {
padding: 10px 0; padding: 10px 0;
background-color: #f9fafc; background-color: #f9fafc;
} }
.json-path-suggest-button { .json-path-suggest-button {
text-align: right; text-align: right;
} }
</style> </style>

View File

@ -1,5 +1,6 @@
import { import {
Arguments, Arguments,
ConstantTimer as JMXConstantTimer,
CookieManager, CookieManager,
DNSCacheManager, DNSCacheManager,
DubboSample, DubboSample,
@ -10,22 +11,24 @@ import {
HTTPSamplerArguments, HTTPSamplerArguments,
HTTPsamplerFiles, HTTPsamplerFiles,
HTTPSamplerProxy, HTTPSamplerProxy,
IfController as JMXIfController,
JDBCDataSource, JDBCDataSource,
JDBCSampler, JDBCSampler,
JSONPathAssertion, JSONPathAssertion,
JSONPostProcessor, JSONPostProcessor,
JSR223Assertion,
JSR223PostProcessor, JSR223PostProcessor,
JSR223PreProcessor, JSR223PreProcessor,
RegexExtractor, RegexExtractor,
ResponseCodeAssertion, ResponseCodeAssertion,
ResponseDataAssertion, ResponseDataAssertion,
ResponseHeadersAssertion, ResponseHeadersAssertion,
TCPSampler,
TestElement, TestElement,
TestPlan, TestPlan,
ThreadGroup, ThreadGroup,
XPath2Assertion,
XPath2Extractor, XPath2Extractor,
IfController as JMXIfController,
ConstantTimer as JMXConstantTimer, TCPSampler, JSR223Assertion, XPath2Assertion,
} from "./JMX"; } from "./JMX";
import Mock from "mockjs"; import Mock from "mockjs";
import {funcFilters} from "@/common/js/func-filter"; import {funcFilters} from "@/common/js/func-filter";
@ -226,6 +229,7 @@ export class Scenario extends BaseConfig {
this.enable = true; this.enable = true;
this.databaseConfigs = []; this.databaseConfigs = [];
this.tcpConfig = undefined; this.tcpConfig = undefined;
this.assertions = undefined;
this.set(options); this.set(options);
this.sets({ this.sets({
@ -242,6 +246,7 @@ export class Scenario extends BaseConfig {
options.databaseConfigs = options.databaseConfigs || []; options.databaseConfigs = options.databaseConfigs || [];
options.dubboConfig = new DubboConfig(options.dubboConfig); options.dubboConfig = new DubboConfig(options.dubboConfig);
options.tcpConfig = new TCPConfig(options.tcpConfig); options.tcpConfig = new TCPConfig(options.tcpConfig);
options.assertions = new Assertions(options.assertions);
return options; return options;
} }
@ -1151,6 +1156,9 @@ class JMXGenerator {
this.addScenarioCookieManager(threadGroup, scenario); this.addScenarioCookieManager(threadGroup, scenario);
this.addJDBCDataSources(threadGroup, scenario); this.addJDBCDataSources(threadGroup, scenario);
this.addAssertion(threadGroup, scenario);
scenario.requests.forEach(request => { scenario.requests.forEach(request => {
if (request.enable) { if (request.enable) {
if (!request.isValid()) return; if (!request.isValid()) return;
@ -1175,7 +1183,7 @@ class JMXGenerator {
this.addRequestExtractor(sampler, request); this.addRequestExtractor(sampler, request);
this.addRequestAssertion(sampler, request); this.addAssertion(sampler, request);
this.addJSR223PreProcessor(sampler, request); this.addJSR223PreProcessor(sampler, request);
@ -1467,7 +1475,7 @@ class JMXGenerator {
httpSamplerProxy.add(new HTTPsamplerFiles(files)); httpSamplerProxy.add(new HTTPsamplerFiles(files));
} }
addRequestAssertion(httpSamplerProxy, request) { addAssertion(httpSamplerProxy, request) {
let assertions = request.assertions; let assertions = request.assertions;
if (assertions.regex.length > 0) { if (assertions.regex.length > 0) {
assertions.regex.filter(this.filter).forEach(regex => { assertions.regex.filter(this.filter).forEach(regex => {

View File

@ -222,28 +222,13 @@ export default {
this.$set(row, "showMore", true); this.$set(row, "showMore", true);
this.selectRows.add(row); this.selectRows.add(row);
} }
let arr = Array.from(this.selectRows);
// 1
if (this.selectRows.size === 1) {
this.$set(arr[0], "showMore", false);
} else if (this.selectRows.size === 2) {
arr.forEach(row => {
this.$set(row, "showMore", true);
})
}
}, },
handleSelectAll(selection) { handleSelectAll(selection) {
if (selection.length > 0) { if (selection.length > 0) {
if (selection.length === 1) { this.tableData.forEach(item => {
this.selectRows.add(selection[0]); this.$set(item, "showMore", true);
} else { this.selectRows.add(item);
this.tableData.forEach(item => { });
this.$set(item, "showMore", true);
this.selectRows.add(item);
});
}
} else { } else {
this.selectRows.clear(); this.selectRows.clear();
this.tableData.forEach(row => { this.tableData.forEach(row => {

View File

@ -1,98 +1,81 @@
<template> <template>
<div v-loading="result.loading" class="pressure-config-container"> <div v-loading="result.loading" class="pressure-config-container">
<el-row> <el-row>
<el-col :span="10"> <el-col>
<el-form :inline="true"> <ms-chart class="chart-container" ref="chart1" :options="options" :autoresize="true"></ms-chart>
<el-form-item>
<div class="config-form-label">{{ $t('load_test.thread_num') }}</div>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="true"
:placeholder="$t('load_test.input_thread_num')"
v-model="threadNumber"
@change="calculateChart"
:min="1"
size="mini"/>
</el-form-item>
</el-form>
<el-form :inline="true">
<el-form-item>
<div class="config-form-label">{{ $t('load_test.duration') }}</div>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="true"
:placeholder="$t('load_test.duration')"
v-model="duration"
:min="1"
@change="calculateChart"
size="mini"/>
</el-form-item>
</el-form>
<el-form :inline="true">
<el-form-item>
<el-form-item>
<div class="config-form-label">{{ $t('load_test.rps_limit') }}</div>
</el-form-item>
<el-form-item>
<el-switch v-model="rpsLimitEnable" :disabled="true"/>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="true"
:placeholder="$t('load_test.input_rps_limit')"
v-model="rpsLimit"
@change="calculateChart"
:min="1"
size="mini"/>
</el-form-item>
</el-form-item>
</el-form>
<el-form :inline="true" class="input-bottom-border">
<el-form-item>
<div>{{ $t('load_test.ramp_up_time_within') }}</div>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="true"
placeholder=""
:min="1"
:max="duration"
v-model="rampUpTime"
@change="calculateChart"
size="mini"/>
</el-form-item>
<el-form-item>
<div>{{ $t('load_test.ramp_up_time_minutes') }}</div>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="true"
placeholder=""
:min="1"
:max="Math.min(threadNumber, rampUpTime)"
v-model="step"
@change="calculateChart"
size="mini"/>
</el-form-item>
<el-form-item>
<div>{{ $t('load_test.ramp_up_time_times') }}</div>
</el-form-item>
</el-form>
</el-col>
<el-col :span="14">
<div class="title">{{ $t('load_test.pressure_prediction_chart') }}</div>
<ms-chart class="chart-container" ref="chart1" :options="orgOptions" :autoresize="true"></ms-chart>
</el-col> </el-col>
</el-row> </el-row>
<el-row>
<el-collapse v-model="activeNames">
<el-collapse-item :title="threadGroup.attributes.testname" :name="index"
v-for="(threadGroup, index) in threadGroups"
:key="index">
<el-col :span="10">
<el-form :inline="true">
<el-form-item :label="$t('load_test.thread_num')">
<el-input-number
:disabled="true"
:placeholder="$t('load_test.input_thread_num')"
v-model="threadGroup.threadNumber"
:min="1"
size="mini"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.duration')">
<el-input-number
:disabled="true"
:placeholder="$t('load_test.duration')"
v-model="threadGroup.duration"
:min="1"
size="mini"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.rps_limit')">
<el-switch v-model="rpsLimitEnable"/>
&nbsp;
<el-input-number
:disabled="true"
:placeholder="$t('load_test.input_rps_limit')"
v-model="threadGroup.rpsLimit"
:min="1"
size="mini"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.ramp_up_time_within')">
<el-input-number
:disabled="true"
placeholder=""
:min="1"
:max="threadGroup.duration"
v-model="threadGroup.rampUpTime"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_minutes')">
<el-input-number
:disabled="true"
placeholder=""
:min="1"
:max="Math.min(threadGroup.threadNumber, threadGroup.rampUpTime)"
v-model="threadGroup.step"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_times')"/>
</el-form>
</el-col>
<el-col :span="14">
<div class="title">{{ $t('load_test.pressure_prediction_chart') }}</div>
<ms-chart class="chart-container" :options="threadGroup.options" :autoresize="true"></ms-chart>
</el-col>
</el-collapse-item>
</el-collapse>
</el-row>
</div> </div>
</template> </template>
<script> <script>
import echarts from "echarts"; import echarts from "echarts";
import MsChart from "@/business/components/common/chart/MsChart"; import MsChart from "@/business/components/common/chart/MsChart";
import {findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
const TARGET_LEVEL = "TargetLevel"; const TARGET_LEVEL = "TargetLevel";
const RAMP_UP = "RampUp"; const RAMP_UP = "RampUp";
@ -100,6 +83,14 @@ const STEPS = "Steps";
const DURATION = "duration"; const DURATION = "duration";
const RPS_LIMIT = "rpsLimit"; const RPS_LIMIT = "rpsLimit";
const RPS_LIMIT_ENABLE = "rpsLimitEnable"; const RPS_LIMIT_ENABLE = "rpsLimitEnable";
const hexToRgba = function (hex, opacity) {
return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ','
+ parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')';
}
const hexToRgb = function (hex) {
return 'rgb(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5))
+ ',' + parseInt('0x' + hex.slice(5, 7)) + ')';
}
export default { export default {
name: "MsPerformancePressureConfig", name: "MsPerformancePressureConfig",
@ -108,62 +99,91 @@ export default {
data() { data() {
return { return {
result: {}, result: {},
threadNumber: 10, threadNumber: 0,
duration: 10, duration: 0,
rampUpTime: 10, rampUpTime: 0,
step: 10, step: 0,
rpsLimit: 10, rpsLimit: 0,
rpsLimitEnable: false, rpsLimitEnable: false,
orgOptions: {}, options: {},
resourcePool: null,
resourcePools: [],
activeNames: ["0"],
threadGroups: [],
} }
}, },
mounted() { mounted() {
this.getLoadConfig(); // this.getJmxContent();
}, },
methods: { methods: {
calculateLoadConfiguration: function (data) { calculateLoadConfiguration: function (data) {
data.forEach(d => { for (let i = 0; i < data.length; i++) {
switch (d.key) { let d = data[i];
case TARGET_LEVEL: if (d instanceof Array) {
this.threadNumber = d.value; d.forEach(item => {
break; switch (item.key) {
case RAMP_UP: case TARGET_LEVEL:
this.rampUpTime = d.value; this.threadGroups[i].threadNumber = item.value;
break; break;
case DURATION: case RAMP_UP:
this.duration = d.value; this.threadGroups[i].rampUpTime = item.value;
break; break;
case STEPS: case DURATION:
this.step = d.value; this.threadGroups[i].duration = item.value;
break; break;
case RPS_LIMIT: case STEPS:
this.rpsLimit = d.value; this.threadGroups[i].step = item.value;
break; break;
default: case RPS_LIMIT:
break; this.threadGroups[i].rpsLimit = item.value;
break;
case RPS_LIMIT_ENABLE:
this.threadGroups[i].rpsLimitEnable = item.value;
break;
default:
break;
}
})
this.calculateChart(this.threadGroups[i]);
} else {
switch (d.key) {
case TARGET_LEVEL:
this.threadGroups[0].threadNumber = d.value;
break;
case RAMP_UP:
this.threadGroups[0].rampUpTime = d.value;
break;
case DURATION:
this.threadGroups[0].duration = d.value;
break;
case STEPS:
this.threadGroups[0].step = d.value;
break;
case RPS_LIMIT:
this.threadGroups[0].rpsLimit = d.value;
break;
case RPS_LIMIT_ENABLE:
this.threadGroups[0].rpsLimitEnable = d.value;
break;
default:
break;
}
this.calculateChart(this.threadGroups[0]);
} }
}); }
this.threadNumber = this.threadNumber || 10;
this.duration = this.duration || 30;
this.rampUpTime = this.rampUpTime || 12;
this.step = this.step || 3;
this.rpsLimit = this.rpsLimit || 10;
this.calculateChart();
}, },
getLoadConfig() { getLoadConfig() {
if (!this.report.id) { if (!this.report.id) {
return; return;
} }
this.$get("/performance/report/" + this.report.id, res => { this.result = this.$get("/performance/report/" + this.report.id, res => {
let data = res.data; let data = res.data;
if (data) { if (data) {
if (data.loadConfiguration) { if (data.loadConfiguration) {
let d = JSON.parse(data.loadConfiguration); let d = JSON.parse(data.loadConfiguration);
this.calculateLoadConfiguration(d); this.calculateLoadConfiguration(d);
} else { } else {
this.$get('/performance/get-load-config/' + this.report.testId, (response) => { this.$get('/performance/get-load-config/' + this.report.id, (response) => {
if (response.data) { if (response.data) {
let data = JSON.parse(response.data); let data = JSON.parse(response.data);
this.calculateLoadConfiguration(data); this.calculateLoadConfiguration(data);
@ -175,14 +195,127 @@ export default {
} }
}); });
}, },
calculateChart() { getJmxContent() {
if (this.duration < this.rampUpTime) { console.log(this.report.testId);
this.rampUpTime = this.duration; if (!this.report.testId) {
return;
} }
if (this.rampUpTime < this.step) { this.result = this.$get('/performance/get-jmx-content/' + this.report.testId, (response) => {
this.step = this.rampUpTime; if (response.data) {
this.threadGroups = findThreadGroup(response.data);
this.threadGroups.forEach(tg => {
tg.options = {};
});
this.getLoadConfig();
}
});
},
calculateTotalChart() {
let handler = this;
if (handler.duration < handler.rampUpTime) {
handler.rampUpTime = handler.duration;
} }
this.orgOptions = { if (handler.rampUpTime < handler.step) {
handler.step = handler.rampUpTime;
}
handler.options = {
color: ['#60acfc', '#32d3eb', '#5bc49f', '#feb64d', '#ff7c7c', '#9287e7', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'],
xAxis: {
type: 'category',
boundaryGap: false,
data: []
},
yAxis: {
type: 'value'
},
tooltip: {
trigger: 'axis',
},
series: []
};
for (let i = 0; i < handler.threadGroups.length; i++) {
let seriesData = {
name: handler.threadGroups[i].attributes.testname,
data: [],
type: 'line',
step: 'start',
smooth: false,
symbolSize: 5,
showSymbol: false,
lineStyle: {
normal: {
width: 1
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: hexToRgba(handler.options.color[i], 0.3),
}, {
offset: 0.8,
color: hexToRgba(handler.options.color[i], 0),
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: hexToRgb(handler.options.color[i]),
borderColor: 'rgba(137,189,2,0.27)',
borderWidth: 12
}
},
};
let tg = handler.threadGroups[i];
let timePeriod = Math.floor(tg.rampUpTime / tg.step);
let timeInc = timePeriod;
let threadPeriod = Math.floor(tg.threadNumber / tg.step);
let threadInc1 = Math.floor(tg.threadNumber / tg.step);
let threadInc2 = Math.ceil(tg.threadNumber / tg.step);
let inc2count = tg.threadNumber - tg.step * threadInc1;
for (let j = 0; j <= tg.duration; j++) {
if (j > timePeriod) {
timePeriod += timeInc;
if (inc2count > 0) {
threadPeriod = threadPeriod + threadInc2;
inc2count--;
} else {
threadPeriod = threadPeriod + threadInc1;
}
if (threadPeriod > tg.threadNumber) {
threadPeriod = tg.threadNumber;
}
}
// x
let xAxis = handler.options.xAxis.data;
if (xAxis.indexOf(j) < 0) {
xAxis.push(j);
}
seriesData.data.push(threadPeriod);
}
handler.options.series.push(seriesData);
}
},
calculateChart(threadGroup) {
let handler = this;
if (threadGroup) {
handler = threadGroup;
}
if (handler.duration < handler.rampUpTime) {
handler.rampUpTime = handler.duration;
}
if (handler.rampUpTime < handler.step) {
handler.step = handler.rampUpTime;
}
handler.options = {
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
@ -235,16 +368,16 @@ export default {
}, },
}] }]
}; };
let timePeriod = Math.floor(this.rampUpTime / this.step); let timePeriod = Math.floor(handler.rampUpTime / handler.step);
let timeInc = timePeriod; let timeInc = timePeriod;
let threadPeriod = Math.floor(this.threadNumber / this.step); let threadPeriod = Math.floor(handler.threadNumber / handler.step);
let threadInc1 = Math.floor(this.threadNumber / this.step); let threadInc1 = Math.floor(handler.threadNumber / handler.step);
let threadInc2 = Math.ceil(this.threadNumber / this.step); let threadInc2 = Math.ceil(handler.threadNumber / handler.step);
let inc2count = this.threadNumber - this.step * threadInc1; let inc2count = handler.threadNumber - handler.step * threadInc1;
for (let i = 0; i <= this.duration; i++) { for (let i = 0; i <= handler.duration; i++) {
// x // x
this.orgOptions.xAxis.data.push(i); handler.options.xAxis.data.push(i);
if (i > timePeriod) { if (i > timePeriod) {
timePeriod += timeInc; timePeriod += timeInc;
if (inc2count > 0) { if (inc2count > 0) {
@ -253,25 +386,22 @@ export default {
} else { } else {
threadPeriod = threadPeriod + threadInc1; threadPeriod = threadPeriod + threadInc1;
} }
if (threadPeriod > this.threadNumber) { if (threadPeriod > handler.threadNumber) {
threadPeriod = this.threadNumber; threadPeriod = handler.threadNumber;
} }
this.orgOptions.series[0].data.push(threadPeriod); handler.options.series[0].data.push(threadPeriod);
} else { } else {
this.orgOptions.series[0].data.push(threadPeriod); handler.options.series[0].data.push(threadPeriod);
} }
} }
this.calculateTotalChart();
}, },
}, },
watch: { watch: {
report: { 'report.testId': {
handler(val) { handler() {
if (!val.testId) { this.getJmxContent();
return;
}
this.getLoadConfig();
}, },
deep: true
} }
} }
} }
@ -295,6 +425,7 @@ export default {
.chart-container { .chart-container {
width: 100%; width: 100%;
height: 300px;
} }
.el-col .el-form { .el-col .el-form {

View File

@ -2,8 +2,8 @@ import MsProject from "@/business/components/project/MsProject";
const PerformanceTest = () => import('@/business/components/performance/PerformanceTest') const PerformanceTest = () => import('@/business/components/performance/PerformanceTest')
const PerformanceTestHome = () => import('@/business/components/performance/home/PerformanceTestHome') const PerformanceTestHome = () => import('@/business/components/performance/home/PerformanceTestHome')
const EditPerformanceTestPlan = () => import('@/business/components/performance/test/EditPerformanceTestPlan') const EditPerformanceTest = () => import('@/business/components/performance/test/EditPerformanceTest')
const PerformanceTestPlan = () => import('@/business/components/performance/test/PerformanceTestPlan') const PerformanceTestList = () => import('@/business/components/performance/test/PerformanceTestList')
const PerformanceTestReport = () => import('@/business/components/performance/report/PerformanceTestReport') const PerformanceTestReport = () => import('@/business/components/performance/report/PerformanceTestReport')
const PerformanceChart = () => import('@/business/components/performance/report/components/PerformanceChart') const PerformanceChart = () => import('@/business/components/performance/report/components/PerformanceChart')
const PerformanceReportView = () => import('@/business/components/performance/report/PerformanceReportView') const PerformanceReportView = () => import('@/business/components/performance/report/PerformanceReportView')
@ -24,12 +24,12 @@ export default {
{ {
path: 'test/create', path: 'test/create',
name: "createPerTest", name: "createPerTest",
component: EditPerformanceTestPlan, component: EditPerformanceTest,
}, },
{ {
path: "test/edit/:testId", path: "test/edit/:testId",
name: "editPerTest", name: "editPerTest",
component: EditPerformanceTestPlan, component: EditPerformanceTest,
props: { props: {
content: (route) => { content: (route) => {
return { return {
@ -41,7 +41,7 @@ export default {
{ {
path: "test/:projectId", path: "test/:projectId",
name: "perPlan", name: "perPlan",
component: PerformanceTestPlan component: PerformanceTestList
}, },
{ {
path: "project/:type", path: "project/:type",

View File

@ -4,12 +4,12 @@
<el-card v-loading="result.loading"> <el-card v-loading="result.loading">
<el-row> <el-row>
<el-col :span="10"> <el-col :span="10">
<el-input :disabled="isReadOnly" :placeholder="$t('load_test.input_name')" v-model="testPlan.name" <el-input :disabled="isReadOnly" :placeholder="$t('load_test.input_name')" v-model="test.name"
class="input-with-select" class="input-with-select"
maxlength="30" show-word-limit maxlength="30" show-word-limit
> >
<template v-slot:prepend> <template v-slot:prepend>
<el-select filterable v-model="testPlan.projectId" <el-select filterable v-model="test.projectId"
:placeholder="$t('load_test.select_project')"> :placeholder="$t('load_test.select_project')">
<el-option <el-option
v-for="item in projects" v-for="item in projects"
@ -29,7 +29,7 @@
<el-button :disabled="isReadOnly" type="warning" plain @click="cancel">{{ $t('commons.cancel') }} <el-button :disabled="isReadOnly" type="warning" plain @click="cancel">{{ $t('commons.cancel') }}
</el-button> </el-button>
<ms-schedule-config :schedule="testPlan.schedule" :save="saveCronExpression" @scheduleChange="saveSchedule" <ms-schedule-config :schedule="test.schedule" :save="saveCronExpression" @scheduleChange="saveSchedule"
:check-open="checkScheduleEdit" :test-id="testId" :custom-validate="durationValidate"/> :check-open="checkScheduleEdit" :test-id="testId" :custom-validate="durationValidate"/>
</el-col> </el-col>
</el-row> </el-row>
@ -37,10 +37,11 @@
<el-tabs class="testplan-config" v-model="active" type="border-card" :stretch="true"> <el-tabs class="testplan-config" v-model="active" type="border-card" :stretch="true">
<el-tab-pane :label="$t('load_test.basic_config')"> <el-tab-pane :label="$t('load_test.basic_config')">
<performance-basic-config :is-read-only="isReadOnly" :test-plan="testPlan" ref="basicConfig"/> <performance-basic-config :is-read-only="isReadOnly" :test="test" ref="basicConfig"
@fileChange="fileChange"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('load_test.pressure_config')"> <el-tab-pane :label="$t('load_test.pressure_config')">
<performance-pressure-config :is-read-only="isReadOnly" :test-plan="testPlan" :test-id="testId" <performance-pressure-config :is-read-only="isReadOnly" :test="test" :test-id="testId"
ref="pressureConfig" @changeActive="changeTabActive"/> ref="pressureConfig" @changeActive="changeTabActive"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('load_test.advanced_config')" class="advanced-config"> <el-tab-pane :label="$t('load_test.advanced_config')" class="advanced-config">
@ -63,7 +64,7 @@ import MsScheduleConfig from "../../common/components/MsScheduleConfig";
import {LIST_CHANGE, PerformanceEvent} from "@/business/components/common/head/ListEvent"; import {LIST_CHANGE, PerformanceEvent} from "@/business/components/common/head/ListEvent";
export default { export default {
name: "EditPerformanceTestPlan", name: "EditPerformanceTest",
components: { components: {
MsScheduleConfig, MsScheduleConfig,
PerformancePressureConfig, PerformancePressureConfig,
@ -75,7 +76,7 @@ export default {
data() { data() {
return { return {
result: {}, result: {},
testPlan: {schedule: {}}, test: {schedule: {}},
listProjectPath: "/project/listAll", listProjectPath: "/project/listAll",
savePath: "/performance/save", savePath: "/performance/save",
editPath: "/performance/edit", editPath: "/performance/edit",
@ -136,8 +137,8 @@ export default {
importAPITest() { importAPITest() {
let apiTest = this.$store.state.api.test; let apiTest = this.$store.state.api.test;
if (apiTest && apiTest.name) { if (apiTest && apiTest.name) {
this.$set(this.testPlan, "projectId", apiTest.projectId); this.$set(this.test, "projectId", apiTest.projectId);
this.$set(this.testPlan, "name", apiTest.name); this.$set(this.test, "name", apiTest.name);
let blob = new Blob([apiTest.jmx.xml], {type: "application/octet-stream"}); let blob = new Blob([apiTest.jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], apiTest.jmx.name); let file = new File([blob], apiTest.jmx.name);
this.$refs.basicConfig.beforeUpload(file); this.$refs.basicConfig.beforeUpload(file);
@ -151,9 +152,9 @@ export default {
this.testId = testId; this.testId = testId;
this.result = this.$get('/performance/get/' + testId, response => { this.result = this.$get('/performance/get/' + testId, response => {
if (response.data) { if (response.data) {
this.testPlan = response.data; this.test = response.data;
if (!this.testPlan.schedule) { if (!this.test.schedule) {
this.testPlan.schedule = {}; this.test.schedule = {};
} }
} }
}); });
@ -165,7 +166,7 @@ export default {
}) })
}, },
save() { save() {
if (!this.validTestPlan()) { if (!this.validTest()) {
return; return;
} }
@ -180,16 +181,16 @@ export default {
}); });
}, },
saveAndRun() { saveAndRun() {
if (!this.validTestPlan()) { if (!this.validTest()) {
return; return;
} }
let options = this.getSaveOption(); let options = this.getSaveOption();
this.result = this.$request(options, (response) => { this.result = this.$request(options, (response) => {
this.testPlan.id = response.data; this.test.id = response.data;
this.$success(this.$t('commons.save_success')); this.$success(this.$t('commons.save_success'));
this.result = this.$post(this.runPath, {id: this.testPlan.id, triggerMode: 'MANUAL'}, (response) => { this.result = this.$post(this.runPath, {id: this.test.id, triggerMode: 'MANUAL'}, (response) => {
let reportId = response.data; let reportId = response.data;
this.$router.push({path: '/performance/report/view/' + reportId}) this.$router.push({path: '/performance/report/view/' + reportId})
// 广 head // 广 head
@ -199,7 +200,7 @@ export default {
}, },
getSaveOption() { getSaveOption() {
let formData = new FormData(); let formData = new FormData();
let url = this.testPlan.id ? this.editPath : this.savePath; let url = this.test.id ? this.editPath : this.savePath;
if (this.$refs.basicConfig.uploadList.length > 0) { if (this.$refs.basicConfig.uploadList.length > 0) {
this.$refs.basicConfig.uploadList.forEach(f => { this.$refs.basicConfig.uploadList.forEach(f => {
@ -207,15 +208,15 @@ export default {
}); });
} }
// //
this.testPlan.updatedFileList = this.$refs.basicConfig.updatedFileList(); this.test.updatedFileList = this.$refs.basicConfig.updatedFileList();
// //
this.testPlan.loadConfiguration = JSON.stringify(this.$refs.pressureConfig.convertProperty()); this.test.loadConfiguration = JSON.stringify(this.$refs.pressureConfig.convertProperty());
this.testPlan.testResourcePoolId = this.$refs.pressureConfig.resourcePool; this.test.testResourcePoolId = this.$refs.pressureConfig.resourcePool;
// //
this.testPlan.advancedConfiguration = JSON.stringify(this.$refs.advancedConfig.configurations()); this.test.advancedConfiguration = JSON.stringify(this.$refs.advancedConfig.configurations());
// filejson // filejson
let requestJson = JSON.stringify(this.testPlan, function (key, value) { let requestJson = JSON.stringify(this.test, function (key, value) {
return key === "file" ? undefined : value return key === "file" ? undefined : value
}); });
@ -235,13 +236,13 @@ export default {
cancel() { cancel() {
this.$router.push({path: '/performance/test/all'}) this.$router.push({path: '/performance/test/all'})
}, },
validTestPlan() { validTest() {
if (!this.testPlan.name) { if (!this.test.name) {
this.$error(this.$t('load_test.test_name_is_null')); this.$error(this.$t('load_test.test_name_is_null'));
return false; return false;
} }
if (!this.testPlan.projectId) { if (!this.test.projectId) {
this.$error(this.$t('load_test.project_is_null')); this.$error(this.$t('load_test.project_is_null'));
return false; return false;
} }
@ -268,26 +269,26 @@ export default {
}); });
}, },
saveCronExpression(cronExpression) { saveCronExpression(cronExpression) {
this.testPlan.schedule.enable = true; this.test.schedule.enable = true;
this.testPlan.schedule.value = cronExpression; this.test.schedule.value = cronExpression;
this.saveSchedule(); this.saveSchedule();
}, },
saveSchedule() { saveSchedule() {
this.checkScheduleEdit(); this.checkScheduleEdit();
let param = {}; let param = {};
param = this.testPlan.schedule; param = this.test.schedule;
param.resourceId = this.testPlan.id; param.resourceId = this.test.id;
let url = '/performance/schedule/create'; let url = '/performance/schedule/create';
if (param.id) { if (param.id) {
url = '/performance/schedule/update'; url = '/performance/schedule/update';
} }
this.$post(url, param, response => { this.$post(url, param, response => {
this.$success(this.$t('commons.save_success')); this.$success(this.$t('commons.save_success'));
this.getTest(this.testPlan.id); this.getTest(this.test.id);
}); });
}, },
checkScheduleEdit() { checkScheduleEdit() {
if (!this.testPlan.id) { if (!this.test.id) {
this.$message(this.$t('api_test.environment.please_save_test')); this.$message(this.$t('api_test.environment.please_save_test'));
return false; return false;
} }
@ -304,6 +305,18 @@ export default {
return { return {
pass: true pass: true
} }
},
fileChange(threadGroups) {
let handler = this.$refs.pressureConfig;
handler.threadGroups = threadGroups;
threadGroups.forEach(tg => {
tg.threadNumber = tg.threadNumber || 10;
tg.duration = tg.duration || 10;
tg.rampUpTime = tg.rampUpTime || 5;
tg.step = tg.step || 5;
tg.rpsLimit = tg.rpsLimit || 10;
handler.calculateChart(tg);
});
} }
} }
} }

View File

@ -80,7 +80,7 @@ import MsContainer from "../../common/components/MsContainer";
import MsMainContainer from "../../common/components/MsMainContainer"; import MsMainContainer from "../../common/components/MsMainContainer";
import MsPerformanceTestStatus from "./PerformanceTestStatus"; import MsPerformanceTestStatus from "./PerformanceTestStatus";
import MsTableOperators from "../../common/components/MsTableOperators"; import MsTableOperators from "../../common/components/MsTableOperators";
import {_filter, _sort} from "../../../../common/js/utils"; import {_filter, _sort} from "@/common/js/utils";
import MsTableHeader from "../../common/components/MsTableHeader"; import MsTableHeader from "../../common/components/MsTableHeader";
import {TEST_CONFIGS} from "../../common/components/search/search-components"; import {TEST_CONFIGS} from "../../common/components/search/search-components";
import {LIST_CHANGE, PerformanceEvent} from "@/business/components/common/head/ListEvent"; import {LIST_CHANGE, PerformanceEvent} from "@/business/components/common/head/ListEvent";
@ -164,30 +164,30 @@ export default {
handleSelectionChange(val) { handleSelectionChange(val) {
this.multipleSelection = val; this.multipleSelection = val;
}, },
handleEdit(testPlan) { handleEdit(test) {
this.$router.push({ this.$router.push({
path: '/performance/test/edit/' + testPlan.id, path: '/performance/test/edit/' + test.id,
}) })
}, },
handleCopy(testPlan) { handleCopy(test) {
this.result = this.$post("/performance/copy", {id: testPlan.id}, () => { this.result = this.$post("/performance/copy", {id: test.id}, () => {
this.$success(this.$t('commons.copy_success')); this.$success(this.$t('commons.copy_success'));
this.search(); this.search();
}); });
}, },
handleDelete(testPlan) { handleDelete(test) {
this.$alert(this.$t('load_test.delete_confirm') + testPlan.name + "", '', { this.$alert(this.$t('load_test.delete_confirm') + test.name + "", '', {
confirmButtonText: this.$t('commons.confirm'), confirmButtonText: this.$t('commons.confirm'),
callback: (action) => { callback: (action) => {
if (action === 'confirm') { if (action === 'confirm') {
this._handleDelete(testPlan); this._handleDelete(test);
} }
} }
}); });
}, },
_handleDelete(testPlan) { _handleDelete(test) {
let data = { let data = {
id: testPlan.id id: test.id
}; };
this.result = this.$post(this.deletePath, data, () => { this.result = this.$post(this.deletePath, data, () => {

View File

@ -56,11 +56,12 @@
<script> <script>
import {Message} from "element-ui"; import {Message} from "element-ui";
import {findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
export default { export default {
name: "PerformanceBasicConfig", name: "PerformanceBasicConfig",
props: { props: {
testPlan: { test: {
type: Object type: Object
}, },
isReadOnly: { isReadOnly: {
@ -81,23 +82,36 @@ export default {
}; };
}, },
created() { created() {
if (this.testPlan.id) { if (this.test.id) {
this.getFileMetadata(this.testPlan) this.getFileMetadata(this.test)
} }
}, },
watch: { watch: {
testPlan() { test() {
if (this.testPlan.id) { if (this.test.id) {
this.getFileMetadata(this.testPlan) this.getFileMetadata(this.test)
}
},
uploadList() {
let self = this;
let fileList = self.uploadList.filter(f => f.name.endsWith(".jmx"));
if (fileList.length > 0) {
let file = fileList[0];
let jmxReader = new FileReader();
jmxReader.onload = function (event) {
let threadGroups = findThreadGroup(event.target.result);
self.$emit('fileChange', threadGroups);
};
jmxReader.readAsText(file);
} }
} }
}, },
methods: { methods: {
getFileMetadata(testPlan) { getFileMetadata(test) {
this.fileList = []; this.fileList = [];
this.tableData = []; this.tableData = [];
this.uploadList = []; this.uploadList = [];
this.result = this.$get(this.getFileMetadataPath + "/" + testPlan.id, response => { this.result = this.$get(this.getFileMetadataPath + "/" + test.id, response => {
let files = response.data; let files = response.data;
if (!files) { if (!files) {

View File

@ -1,91 +1,9 @@
<template> <template>
<div v-loading="result.loading" class="pressure-config-container"> <div v-loading="result.loading" class="pressure-config-container">
<el-row> <el-row>
<el-col :span="10"> <el-col>
<el-form :inline="true"> <el-form :inline="true">
<el-form-item> <el-form-item :label="$t('load_test.select_resource_pool')">
<div class="config-form-label">{{ $t('load_test.thread_num') }}</div>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="isReadOnly"
:placeholder="$t('load_test.input_thread_num')"
v-model="threadNumber"
@change="calculateChart"
:min="1"
size="mini"/>
</el-form-item>
</el-form>
<el-form :inline="true">
<el-form-item>
<div class="config-form-label">{{ $t('load_test.duration') }}</div>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="isReadOnly"
:placeholder="$t('load_test.duration')"
v-model="duration"
:min="1"
@change="calculateChart"
size="mini"/>
</el-form-item>
</el-form>
<el-form :inline="true">
<el-form-item>
<el-form-item>
<div class="config-form-label">{{ $t('load_test.rps_limit') }}</div>
</el-form-item>
<el-form-item>
<el-switch v-model="rpsLimitEnable"/>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="isReadOnly || !rpsLimitEnable"
:placeholder="$t('load_test.input_rps_limit')"
v-model="rpsLimit"
@change="calculateChart"
:min="1"
size="mini"/>
</el-form-item>
</el-form-item>
</el-form>
<el-form :inline="true" class="input-bottom-border">
<el-form-item>
<div>{{ $t('load_test.ramp_up_time_within') }}</div>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="isReadOnly"
placeholder=""
:min="1"
:max="duration"
v-model="rampUpTime"
@change="calculateChart"
size="mini"/>
</el-form-item>
<el-form-item>
<div>{{ $t('load_test.ramp_up_time_minutes') }}</div>
</el-form-item>
<el-form-item>
<el-input-number
:disabled="isReadOnly"
placeholder=""
:min="1"
:max="Math.min(threadNumber, rampUpTime)"
v-model="step"
@change="calculateChart"
size="mini"/>
</el-form-item>
<el-form-item>
<div>{{ $t('load_test.ramp_up_time_times') }}</div>
</el-form-item>
</el-form>
<el-form :inline="true" class="input-bottom-border">
<el-form-item>
<div>{{ $t('load_test.select_resource_pool') }}</div>
</el-form-item>
<el-form-item>
<el-select v-model="resourcePool" :disabled="isReadOnly" size="mini"> <el-select v-model="resourcePool" :disabled="isReadOnly" size="mini">
<el-option <el-option
v-for="item in resourcePools" v-for="item in resourcePools"
@ -96,11 +14,77 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<ms-chart class="chart-container" ref="chart1" :options="options" :autoresize="true"></ms-chart>
</el-col> </el-col>
<el-col :span="14"> </el-row>
<div class="title">{{ $t('load_test.pressure_prediction_chart') }}</div> <el-row>
<ms-chart class="chart-container" ref="chart1" :options="orgOptions" :autoresize="true"></ms-chart> <el-collapse v-model="activeNames">
</el-col> <el-collapse-item :title="threadGroup.attributes.testname" :name="index"
v-for="(threadGroup, index) in threadGroups"
:key="index">
<el-col :span="10">
<el-form :inline="true">
<el-form-item :label="$t('load_test.thread_num')">
<el-input-number
:disabled="isReadOnly"
:placeholder="$t('load_test.input_thread_num')"
v-model="threadGroup.threadNumber"
@change="calculateChart(threadGroup)"
:min="1"
size="mini"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.duration')">
<el-input-number
:disabled="isReadOnly"
:placeholder="$t('load_test.duration')"
v-model="threadGroup.duration"
:min="1"
@change="calculateChart(threadGroup)"
size="mini"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.rps_limit')">
<el-switch v-model="threadGroup.rpsLimitEnable" @change="calculateTotalChart()"/>
&nbsp;
<el-input-number
:disabled="isReadOnly || !threadGroup.rpsLimitEnable"
:placeholder="$t('load_test.input_rps_limit')"
v-model="threadGroup.rpsLimit"
@change="calculateChart(threadGroup)"
:min="1"
size="mini"/>
</el-form-item>
<br>
<el-form-item :label="$t('load_test.ramp_up_time_within')">
<el-input-number
:disabled="isReadOnly"
placeholder=""
:min="1"
:max="threadGroup.duration"
v-model="threadGroup.rampUpTime"
@change="calculateChart(threadGroup)"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_minutes')">
<el-input-number
:disabled="isReadOnly"
placeholder=""
:min="1"
:max="Math.min(threadGroup.threadNumber, threadGroup.rampUpTime)"
v-model="threadGroup.step"
@change="calculateChart(threadGroup)"
size="mini"/>
</el-form-item>
<el-form-item :label="$t('load_test.ramp_up_time_times')"/>
</el-form>
</el-col>
<el-col :span="14">
<div class="title">{{ $t('load_test.pressure_prediction_chart') }}</div>
<ms-chart class="chart-container" :options="threadGroup.options" :autoresize="true"></ms-chart>
</el-col>
</el-collapse-item>
</el-collapse>
</el-row> </el-row>
</div> </div>
</template> </template>
@ -108,6 +92,7 @@
<script> <script>
import echarts from "echarts"; import echarts from "echarts";
import MsChart from "@/business/components/common/chart/MsChart"; import MsChart from "@/business/components/common/chart/MsChart";
import {findTestPlan, findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
const TARGET_LEVEL = "TargetLevel"; const TARGET_LEVEL = "TargetLevel";
const RAMP_UP = "RampUp"; const RAMP_UP = "RampUp";
@ -115,12 +100,22 @@ const STEPS = "Steps";
const DURATION = "duration"; const DURATION = "duration";
const RPS_LIMIT = "rpsLimit"; const RPS_LIMIT = "rpsLimit";
const RPS_LIMIT_ENABLE = "rpsLimitEnable"; const RPS_LIMIT_ENABLE = "rpsLimitEnable";
const HOLD = "Hold";
const hexToRgba = function (hex, opacity) {
return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ','
+ parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')';
}
const hexToRgb = function (hex) {
return 'rgb(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5))
+ ',' + parseInt('0x' + hex.slice(5, 7)) + ')';
}
export default { export default {
name: "PerformancePressureConfig", name: "PerformancePressureConfig",
components: {MsChart}, components: {MsChart},
props: { props: {
testPlan: { test: {
type: Object type: Object
}, },
testId: { testId: {
@ -134,35 +129,38 @@ export default {
data() { data() {
return { return {
result: {}, result: {},
threadNumber: 10, threadNumber: 0,
duration: 10, duration: 0,
rampUpTime: 10, rampUpTime: 0,
step: 10, step: 0,
rpsLimit: 10, rpsLimit: 0,
rpsLimitEnable: false, rpsLimitEnable: false,
orgOptions: {}, options: {},
resourcePool: null, resourcePool: null,
resourcePools: [], resourcePools: [],
activeNames: ["0"],
threadGroups: [],
serializeThreadgroups: false,
} }
}, },
mounted() { mounted() {
if (this.testId) { if (this.testId) {
this.getLoadConfig(); this.getJmxContent();
} else { } else {
this.calculateChart(); this.calculateTotalChart();
} }
this.resourcePool = this.testPlan.testResourcePoolId; this.resourcePool = this.test.testResourcePoolId;
this.getResourcePools(); this.getResourcePools();
}, },
watch: { watch: {
testPlan(n) { test(n) {
this.resourcePool = n.testResourcePoolId; this.resourcePool = n.testResourcePoolId;
}, },
testId() { testId() {
if (this.testId) { if (this.testId) {
this.getLoadConfig(); this.getJmxContent();
} else { } else {
this.calculateChart(); this.calculateTotalChart();
} }
this.getResourcePools(); this.getResourcePools();
}, },
@ -178,56 +176,191 @@ export default {
}) })
}, },
getLoadConfig() { getLoadConfig() {
if (this.testId) { this.$get('/performance/get-load-config/' + this.testId, (response) => {
if (response.data) {
this.$get('/performance/get-load-config/' + this.testId, (response) => { let data = JSON.parse(response.data);
if (response.data) { for (let i = 0; i < data.length; i++) {
let data = JSON.parse(response.data); let d = data[i];
if (d instanceof Array) {
data.forEach(d => { d.forEach(item => {
switch (item.key) {
case TARGET_LEVEL:
this.threadGroups[i].threadNumber = item.value;
break;
case RAMP_UP:
this.threadGroups[i].rampUpTime = item.value;
break;
case DURATION:
this.threadGroups[i].duration = item.value;
break;
case STEPS:
this.threadGroups[i].step = item.value;
break;
case RPS_LIMIT:
this.threadGroups[i].rpsLimit = item.value;
break;
case RPS_LIMIT_ENABLE:
this.threadGroups[i].rpsLimitEnable = item.value;
break;
default:
break;
}
})
this.calculateChart(this.threadGroups[i]);
} else {
switch (d.key) { switch (d.key) {
case TARGET_LEVEL: case TARGET_LEVEL:
this.threadNumber = d.value; this.threadGroups[0].threadNumber = d.value;
break; break;
case RAMP_UP: case RAMP_UP:
this.rampUpTime = d.value; this.threadGroups[0].rampUpTime = d.value;
break; break;
case DURATION: case DURATION:
this.duration = d.value; this.threadGroups[0].duration = d.value;
break; break;
case STEPS: case STEPS:
this.step = d.value; this.threadGroups[0].step = d.value;
break; break;
case RPS_LIMIT: case RPS_LIMIT:
this.rpsLimit = d.value; this.threadGroups[0].rpsLimit = d.value;
break; break;
case RPS_LIMIT_ENABLE: case RPS_LIMIT_ENABLE:
this.rpsLimitEnable = d.value; this.threadGroups[0].rpsLimitEnable = d.value;
break; break;
default: default:
break; break;
} }
this.calculateChart(this.threadGroups[0]);
}
}
this.calculateTotalChart();
}
});
},
getJmxContent() {
if (this.testId) {
this.$get('/performance/get-jmx-content/' + this.testId, (response) => {
if (response.data) {
let testPlan = findTestPlan(response.data);
testPlan.elements.forEach(e => {
if (e.attributes.name === 'TestPlan.serialize_threadgroups') {
this.serializeThreadgroups = Boolean(e.elements[0].text);
}
}); });
this.threadGroups = findThreadGroup(response.data);
this.threadNumber = this.threadNumber || 10; this.threadGroups.forEach(tg => {
this.duration = this.duration || 30; tg.options = {};
this.rampUpTime = this.rampUpTime || 12; });
this.step = this.step || 3; this.getLoadConfig();
this.rpsLimit = this.rpsLimit || 10;
this.calculateChart();
} }
}); });
} }
}, },
calculateChart() { calculateTotalChart() {
if (this.duration < this.rampUpTime) { let handler = this;
this.rampUpTime = this.duration; if (handler.duration < handler.rampUpTime) {
handler.rampUpTime = handler.duration;
} }
if (this.rampUpTime < this.step) { if (handler.rampUpTime < handler.step) {
this.step = this.rampUpTime; handler.step = handler.rampUpTime;
} }
this.orgOptions = { handler.options = {
color: ['#60acfc', '#32d3eb', '#5bc49f', '#feb64d', '#ff7c7c', '#9287e7', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'],
xAxis: {
type: 'category',
boundaryGap: false,
data: []
},
yAxis: {
type: 'value'
},
tooltip: {
trigger: 'axis',
},
series: []
};
for (let i = 0; i < handler.threadGroups.length; i++) {
let seriesData = {
name: handler.threadGroups[i].attributes.testname,
data: [],
type: 'line',
step: 'start',
smooth: false,
symbolSize: 5,
showSymbol: false,
lineStyle: {
normal: {
width: 1
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: hexToRgba(handler.options.color[i], 0.3),
}, {
offset: 0.8,
color: hexToRgba(handler.options.color[i], 0),
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: hexToRgb(handler.options.color[i]),
borderColor: 'rgba(137,189,2,0.27)',
borderWidth: 12
}
},
};
let tg = handler.threadGroups[i];
let timePeriod = Math.floor(tg.rampUpTime / tg.step);
let timeInc = timePeriod;
let threadPeriod = Math.floor(tg.threadNumber / tg.step);
let threadInc1 = Math.floor(tg.threadNumber / tg.step);
let threadInc2 = Math.ceil(tg.threadNumber / tg.step);
let inc2count = tg.threadNumber - tg.step * threadInc1;
for (let j = 0; j <= tg.duration; j++) {
if (j > timePeriod) {
timePeriod += timeInc;
if (inc2count > 0) {
threadPeriod = threadPeriod + threadInc2;
inc2count--;
} else {
threadPeriod = threadPeriod + threadInc1;
}
if (threadPeriod > tg.threadNumber) {
threadPeriod = tg.threadNumber;
}
}
// x
let xAxis = handler.options.xAxis.data;
if (xAxis.indexOf(j) < 0) {
xAxis.push(j);
}
seriesData.data.push(threadPeriod);
}
handler.options.series.push(seriesData);
}
},
calculateChart(threadGroup) {
let handler = this;
if (threadGroup) {
handler = threadGroup;
}
if (handler.duration < handler.rampUpTime) {
handler.rampUpTime = handler.duration;
}
if (handler.rampUpTime < handler.step) {
handler.step = handler.rampUpTime;
}
handler.options = {
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
@ -280,16 +413,16 @@ export default {
}, },
}] }]
}; };
let timePeriod = Math.floor(this.rampUpTime / this.step); let timePeriod = Math.floor(handler.rampUpTime / handler.step);
let timeInc = timePeriod; let timeInc = timePeriod;
let threadPeriod = Math.floor(this.threadNumber / this.step); let threadPeriod = Math.floor(handler.threadNumber / handler.step);
let threadInc1 = Math.floor(this.threadNumber / this.step); let threadInc1 = Math.floor(handler.threadNumber / handler.step);
let threadInc2 = Math.ceil(this.threadNumber / this.step); let threadInc2 = Math.ceil(handler.threadNumber / handler.step);
let inc2count = this.threadNumber - this.step * threadInc1; let inc2count = handler.threadNumber - handler.step * threadInc1;
for (let i = 0; i <= this.duration; i++) { for (let i = 0; i <= handler.duration; i++) {
// x // x
this.orgOptions.xAxis.data.push(i); handler.options.xAxis.data.push(i);
if (i > timePeriod) { if (i > timePeriod) {
timePeriod += timeInc; timePeriod += timeInc;
if (inc2count > 0) { if (inc2count > 0) {
@ -298,14 +431,15 @@ export default {
} else { } else {
threadPeriod = threadPeriod + threadInc1; threadPeriod = threadPeriod + threadInc1;
} }
if (threadPeriod > this.threadNumber) { if (threadPeriod > handler.threadNumber) {
threadPeriod = this.threadNumber; threadPeriod = handler.threadNumber;
} }
this.orgOptions.series[0].data.push(threadPeriod); handler.options.series[0].data.push(threadPeriod);
} else { } else {
this.orgOptions.series[0].data.push(threadPeriod); handler.options.series[0].data.push(threadPeriod);
} }
} }
this.calculateTotalChart();
}, },
validConfig() { validConfig() {
if (!this.resourcePool) { if (!this.resourcePool) {
@ -315,24 +449,32 @@ export default {
return false; return false;
} }
if (!this.threadNumber || !this.duration || !this.rampUpTime || !this.step || !this.rpsLimit) { for (let i = 0; i < this.threadGroups.length; i++) {
this.$warning(this.$t('load_test.pressure_config_params_is_empty')); if (!this.threadGroups[i].threadNumber || !this.threadGroups[i].duration
this.$emit('changeActive', '1'); || !this.threadGroups[i].rampUpTime || !this.threadGroups[i].step || !this.threadGroups[i].rpsLimit) {
return false; this.$warning(this.$t('load_test.pressure_config_params_is_empty'));
this.$emit('changeActive', '1');
return false;
}
} }
return true; return true;
}, },
convertProperty() { convertProperty() {
/// todo4jmeter ConcurrencyThreadGroup plugin /// todo4jmeter ConcurrencyThreadGroup plugin
return [ let result = [];
{key: TARGET_LEVEL, value: this.threadNumber}, for (let i = 0; i < this.threadGroups.length; i++) {
{key: RAMP_UP, value: this.rampUpTime}, result.push([
{key: STEPS, value: this.step}, {key: TARGET_LEVEL, value: this.threadGroups[i].threadNumber},
{key: DURATION, value: this.duration}, {key: RAMP_UP, value: this.threadGroups[i].rampUpTime},
{key: RPS_LIMIT, value: this.rpsLimit}, {key: STEPS, value: this.threadGroups[i].step},
{key: RPS_LIMIT_ENABLE, value: this.rpsLimitEnable}, {key: DURATION, value: this.threadGroups[i].duration},
]; {key: RPS_LIMIT, value: this.threadGroups[i].rpsLimit},
{key: RPS_LIMIT_ENABLE, value: this.threadGroups[i].rpsLimitEnable},
{key: HOLD, value: this.threadGroups[i].duration - this.threadGroups[i].rampUpTime},
]);
}
return result;
} }
} }
} }
@ -356,6 +498,7 @@ export default {
.chart-container { .chart-container {
width: 100%; width: 100%;
height: 300px;
} }
.el-col .el-form { .el-col .el-form {

View File

@ -0,0 +1,30 @@
import {xml2json} from "xml-js";
let travel = function (elements, threadGroups) {
if (!elements) {
return;
}
for (let element of elements) {
if (element.name === 'ThreadGroup') {
threadGroups.push(element);
}
travel(element.elements, threadGroups)
}
}
export function findThreadGroup(jmxContent) {
let jmxJson = JSON.parse(xml2json(jmxContent));
let threadGroups = [];
travel(jmxJson.elements, threadGroups);
return threadGroups;
}
export function findTestPlan(jmxContent) {
let jmxJson = JSON.parse(xml2json(jmxContent));
for (let element of jmxJson.elements[0].elements[0].elements) {
if (element.name === 'TestPlan') {
return element;
}
}
}

View File

@ -0,0 +1,72 @@
<template>
<div class="header-title" v-loading="result.loading">
<div>
<div>{{ $t('organization.integration.select_defect_platform') }}</div>
<el-radio-group v-model="platform" style="margin-top: 10px" @change="change">
<el-radio label="Tapd">
<img class="platform" src="../../../../assets/tapd.png" alt="Tapd"/>
</el-radio>
<el-radio label="Jira">
<img class="platform" src="../../../../assets/jira.png" alt="Jira"/>
</el-radio>
<el-radio label="Zentao">
<img class="platform" src="../../../../assets/zentao.jpg" alt="Zentao"/>
</el-radio>
</el-radio-group>
</div>
<tapd-setting v-if="tapdEnable" ref="tapdSetting"/>
<jira-setting v-if="jiraEnable" ref="jiraSetting"/>
<zentao-setting v-if="zentaoEnable" ref="zentaoSetting"/>
</div>
</template>
<script>
import TapdSetting from "@/business/components/settings/organization/components/TapdSetting";
import JiraSetting from "@/business/components/settings/organization/components/JiraSetting";
import ZentaoSetting from "@/business/components/settings/organization/components/ZentaoSetting";
import {JIRA, TAPD, ZEN_TAO} from "@/common/js/constants";
export default {
name: "BugManagement",
components: {TapdSetting, JiraSetting, ZentaoSetting},
data() {
return {
tapdEnable: true,
jiraEnable: false,
zentaoEnable: false,
result: {},
platform: TAPD
}
},
methods: {
change(platform) {
if (platform === TAPD) {
this.tapdEnable = true;
this.jiraEnable = false;
this.zentaoEnable = false;
} else if (platform === JIRA) {
this.tapdEnable = false;
this.jiraEnable = true;
this.zentaoEnable = false;
} else if (platform === ZEN_TAO) {
this.tapdEnable = false;
this.jiraEnable = false;
this.zentaoEnable = true;
}
}
}
}
</script>
<style scoped>
.header-title {
padding: 10px 30px;
}
.platform {
height: 90px;
vertical-align: middle
}
</style>

View File

@ -2,7 +2,7 @@
<el-card> <el-card>
<el-tabs class="system-setting" v-model="activeName"> <el-tabs class="system-setting" v-model="activeName">
<el-tab-pane :label="$t('organization.defect_manage')" name="defect"> <el-tab-pane :label="$t('organization.defect_manage')" name="defect">
<defect-management/> <bug-management/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-card> </el-card>
@ -10,12 +10,12 @@
<script> <script>
import DefectManagement from "./IssuesManagement"; import BugManagement from "./BugManagement";
export default { export default {
name: "ServiceIntegration", name: "ServiceIntegration",
components: { components: {
DefectManagement BugManagement
}, },
data() { data() {
return { return {

View File

@ -0,0 +1,71 @@
<template>
<div style="margin-left: 120px">
<el-button type="primary" size="mini" :disabled="!show" @click="testConnection">
{{ $t('ldap.test_connect') }}
</el-button>
<el-button v-if="showEdit" size="mini" @click="edit">
{{ $t('commons.edit') }}
</el-button>
<el-button type="primary" v-if="showSave" size="mini" @click="save">
{{ $t('commons.save') }}
</el-button>
<el-button v-if="showCancel" size="mini" @click="cancelEdit">
{{ $t('organization.integration.cancel_edit') }}
</el-button>
<el-button type="info" size="mini" :disabled="!show" @click="cancelIntegration">
{{ $t('organization.integration.cancel_integration') }}
</el-button>
</div>
</template>
<script>
export default {
name: "BugManageBtn",
data() {
return {
showEdit: true,
showSave: false,
showCancel: false,
}
},
props: {
show: {
type: Boolean,
default: true
},
form: Object,
},
methods: {
testConnection() {
this.$emit("testConnection");
},
edit() {
this.$emit("update:show", false);
this.showEdit = false;
this.showSave = true;
this.showCancel = true;
},
cancelEdit() {
this.showEdit = true;
this.showCancel = false;
this.showSave = false;
this.$emit("update:show", true);
this.init();
},
init() {
this.$emit("init");
},
save() {
this.$emit("save");
},
cancelIntegration() {
this.$emit("cancelIntegration");
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,59 +1,39 @@
<template> <template>
<div class="header-title" v-loading="result.loading"> <div>
<div>
<div>{{ $t('organization.integration.select_defect_platform') }}</div>
<el-radio-group v-model="platform" style="margin-top: 10px" @change="change">
<el-radio label="Tapd">
<img class="platform" src="../../../../assets/tapd.png" alt="Tapd"/>
</el-radio>
<el-radio label="Jira">
<img class="platform" src="../../../../assets/jira.png" alt="Jira"/>
</el-radio>
</el-radio-group>
</div>
<div style="width: 500px"> <div style="width: 500px">
<div style="margin-top: 20px;margin-bottom: 10px">{{ $t('organization.integration.basic_auth_info') }}</div> <div style="margin-top: 20px;margin-bottom: 10px">{{ $t('organization.integration.basic_auth_info') }}</div>
<el-form :model="form" ref="form" label-width="120px" size="small" :disabled="show" :rules="rules"> <el-form :model="form" ref="form" label-width="120px" size="small" :disabled="show" :rules="rules">
<el-form-item :label="$t('organization.integration.api_account')" prop="account"> <el-form-item :label="$t('organization.integration.account')" prop="account">
<el-input v-model="form.account" :placeholder="$t('organization.integration.input_api_account')"/> <el-input v-model="form.account" :placeholder="$t('organization.integration.input_api_account')"/>
</el-form-item> </el-form-item>
<el-form-item :label="$t('organization.integration.api_password')" prop="password"> <el-form-item :label="$t('organization.integration.password')" prop="password">
<el-input v-model="form.password" auto-complete="new-password" <el-input v-model="form.password" auto-complete="new-password"
:placeholder="$t('organization.integration.input_api_password')" show-password/> :placeholder="$t('organization.integration.input_api_password')" show-password/>
</el-form-item> </el-form-item>
<el-form-item :label="$t('organization.integration.jira_url')" prop="url" v-if="platform === 'Jira'"> <el-form-item :label="$t('organization.integration.jira_url')" prop="url">
<el-input v-model="form.url" :placeholder="$t('organization.integration.input_jira_url')"/> <el-input v-model="form.url" :placeholder="$t('organization.integration.input_jira_url')"/>
</el-form-item> </el-form-item>
<el-form-item :label="$t('organization.integration.jira_issuetype')" prop="issuetype" <el-form-item :label="$t('organization.integration.jira_issuetype')" prop="issuetype">
v-if="platform === 'Jira'">
<el-input v-model="form.issuetype" :placeholder="$t('organization.integration.input_jira_issuetype')"/> <el-input v-model="form.issuetype" :placeholder="$t('organization.integration.input_jira_issuetype')"/>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<div style="margin-left: 120px"> <bug-manage-btn @save="save"
<el-button type="primary" size="mini" :disabled="!show" @click="testConnection">{{ $t('ldap.test_connect') }} @init="init"
</el-button> @testConnection="testConnection"
<el-button v-if="showEdit" size="mini" @click="edit">{{ $t('commons.edit') }}</el-button> @cancelIntegration="cancelIntegration"
<el-button type="primary" v-if="showSave" size="mini" @click="save('form')">{{ $t('commons.save') }}</el-button> :form="form"
<el-button v-if="showCancel" size="mini" @click="cancelEdit">{{ $t('organization.integration.cancel_edit') }} :show.sync="show"
</el-button> ref="bugBtn"/>
<el-button type="info" size="mini" @click="cancelIntegration('form')" :disabled="!show">
{{ $t('organization.integration.cancel_integration') }}
</el-button>
</div>
<div class="defect-tip"> <div class="defect-tip">
<div>{{ $t('organization.integration.use_tip') }}</div> <div>{{ $t('organization.integration.use_tip') }}</div>
<div> <div>
1. {{ $t('organization.integration.use_tip_tapd') }} 1. {{ $t('organization.integration.use_tip_jira') }}
</div> </div>
<div> <div>
2. {{ $t('organization.integration.use_tip_jira') }} 2. {{ $t('organization.integration.use_tip_two') }}
</div>
<div>
3. {{ $t('organization.integration.use_tip_two') }}
<router-link to="/track/project/all" style="margin-left: 5px"> <router-link to="/track/project/all" style="margin-left: 5px">
{{ $t('organization.integration.link_the_project_now') }} {{ $t('organization.integration.link_the_project_now') }}
</router-link> </router-link>
@ -63,20 +43,20 @@
</template> </template>
<script> <script>
import {getCurrentUser} from "../../../../common/js/utils"; import BugManageBtn from "@/business/components/settings/organization/components/BugManageBtn";
import {getCurrentUser} from "@/common/js/utils";
import {JIRA} from "@/common/js/constants";
export default { export default {
name: "IssuesManagement", name: "JiraSetting",
components: {BugManageBtn},
created() {
this.init();
},
data() { data() {
return { return {
form: {},
result: {},
platform: '',
orgId: '',
show: true, show: true,
showEdit: true, form: {},
showSave: false,
showCancel: false,
rules: { rules: {
account: { account: {
required: true, required: true,
@ -101,17 +81,14 @@ export default {
}, },
} }
}, },
created() {
this.init(this.platform);
},
methods: { methods: {
init(platform) { init() {
const {lastOrganizationId} = getCurrentUser();
let param = {}; let param = {};
param.platform = platform; param.platform = JIRA;
param.orgId = getCurrentUser().lastOrganizationId; param.orgId = lastOrganizationId;
this.result = this.$post("service/integration/type", param, response => { this.$parent.result = this.$post("service/integration/type", param, response => {
let data = response.data; let data = response.data;
this.platform = data.platform;
if (data.configuration) { if (data.configuration) {
let config = JSON.parse(data.configuration); let config = JSON.parse(data.configuration);
this.$set(this.form, 'account', config.account); this.$set(this.form, 'account', config.account);
@ -123,54 +100,13 @@ export default {
} }
}) })
}, },
edit() { save() {
this.show = false; this.$refs['form'].validate(valid => {
this.showEdit = false;
this.showSave = true;
this.showCancel = true;
},
cancelEdit() {
this.showEdit = true;
this.showCancel = false;
this.showSave = false;
this.show = true;
this.init(this.platform);
},
cancelIntegration() {
if (this.form.account && this.form.password && this.platform) {
this.$alert(this.$t('organization.integration.cancel_confirm') + this.platform + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
let param = {};
param.orgId = getCurrentUser().lastOrganizationId;
param.platform = this.platform;
this.result = this.$post("service/integration/delete", param, () => {
this.$success(this.$t('organization.integration.successful_operation'));
this.init('');
});
}
}
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
}
},
save(form) {
if (!this.platform) {
this.$warning(this.$t('organization.integration.choose_platform'));
return;
}
this.$refs[form].validate(valid => {
if (valid) { if (valid) {
let formatUrl = this.form.url.trim(); let formatUrl = this.form.url.trim();
if (!formatUrl.endsWith('/')) { if (!formatUrl.endsWith('/')) {
formatUrl = formatUrl + '/'; formatUrl = formatUrl + '/';
} }
let param = {}; let param = {};
let auth = { let auth = {
account: this.form.account, account: this.form.account,
@ -178,16 +114,16 @@ export default {
url: formatUrl, url: formatUrl,
issuetype: this.form.issuetype issuetype: this.form.issuetype
}; };
param.organizationId = getCurrentUser().lastOrganizationId; const {lastOrganizationId} = getCurrentUser();
param.platform = this.platform; param.organizationId = lastOrganizationId;
param.platform = JIRA;
param.configuration = JSON.stringify(auth); param.configuration = JSON.stringify(auth);
this.$parent.result = this.$post("service/integration/save", param, () => {
this.result = this.$post("service/integration/save", param, () => {
this.show = true; this.show = true;
this.showEdit = true; this.$refs.bugBtn.showEdit = true;
this.showSave = false; this.$refs.bugBtn.showSave = false;
this.showCancel = false; this.$refs.bugBtn.showCancel = false;
this.init(this.platform); this.init();
this.$success(this.$t('commons.save_success')); this.$success(this.$t('commons.save_success'));
}); });
} else { } else {
@ -195,27 +131,6 @@ export default {
} }
}) })
}, },
change(platform) {
this.show = true;
this.showEdit = true;
this.showCancel = false;
this.showSave = false;
let param = {};
param.orgId = getCurrentUser().lastOrganizationId;
param.platform = platform;
this.result = this.$post("service/integration/type", param, response => {
let data = response.data;
if (data.configuration) {
let config = JSON.parse(data.configuration);
this.$set(this.form, 'account', config.account);
this.$set(this.form, 'password', config.password);
this.$set(this.form, 'url', config.url);
this.$set(this.form, 'issuetype', config.issuetype);
} else {
this.clear();
}
})
},
clear() { clear() {
this.$set(this.form, 'account', ''); this.$set(this.form, 'account', '');
this.$set(this.form, 'password', ''); this.$set(this.form, 'password', '');
@ -226,24 +141,41 @@ export default {
}); });
}, },
testConnection() { testConnection() {
if (this.form.account && this.form.password && this.platform) { if (this.form.account && this.form.password) {
this.result = this.$get("issues/auth/" + this.platform, () => { this.$parent.result = this.$get("issues/auth/" + JIRA, () => {
this.$success(this.$t('organization.integration.verified')); this.$success(this.$t('organization.integration.verified'));
}); });
} else { } else {
this.$warning(this.$t('organization.integration.not_integrated')); this.$warning(this.$t('organization.integration.not_integrated'));
return false; return false;
} }
},
cancelIntegration() {
if (this.form.account && this.form.password) {
this.$alert(this.$t('organization.integration.cancel_confirm') + JIRA + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
const {lastOrganizationId} = getCurrentUser();
let param = {};
param.orgId = lastOrganizationId;
param.platform = JIRA;
this.$parent.result = this.$post("service/integration/delete", param, () => {
this.$success(this.$t('organization.integration.successful_operation'));
this.init('');
});
}
}
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
}
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.header-title {
padding: 10px 30px;
}
.defect-tip { .defect-tip {
background: #EDEDED; background: #EDEDED;
border: solid #E1E1E1 1px; border: solid #E1E1E1 1px;
@ -251,9 +183,4 @@ export default {
padding: 10px; padding: 10px;
border-radius: 3px; border-radius: 3px;
} }
.platform {
height: 90px;
vertical-align: middle
}
</style> </style>

View File

@ -0,0 +1,166 @@
<template>
<div>
<div style="width: 500px">
<div style="margin-top: 20px;margin-bottom: 10px">{{ $t('organization.integration.basic_auth_info') }}</div>
<el-form :model="form" ref="form" label-width="120px" size="small" :disabled="show" :rules="rules">
<el-form-item :label="$t('organization.integration.api_account')" prop="account">
<el-input v-model="form.account" :placeholder="$t('organization.integration.input_api_account')"/>
</el-form-item>
<el-form-item :label="$t('organization.integration.api_password')" prop="password">
<el-input v-model="form.password" auto-complete="new-password"
:placeholder="$t('organization.integration.input_api_password')" show-password/>
</el-form-item>
</el-form>
</div>
<bug-manage-btn @save="save"
@init="init"
@testConnection="testConnection"
@cancelIntegration="cancelIntegration"
:form="form"
:show.sync="show"
ref="bugBtn"/>
<div class="defect-tip">
<div>{{ $t('organization.integration.use_tip') }}</div>
<div>
1. {{ $t('organization.integration.use_tip_tapd') }}
</div>
<div>
2. {{ $t('organization.integration.use_tip_two') }}
<router-link to="/track/project/all" style="margin-left: 5px">
{{ $t('organization.integration.link_the_project_now') }}
</router-link>
</div>
</div>
</div>
</template>
<script>
import BugManageBtn from "@/business/components/settings/organization/components/BugManageBtn";
import {getCurrentUser} from "@/common/js/utils";
import {TAPD} from "@/common/js/constants";
export default {
name: "TapdSetting.vue",
components: {
BugManageBtn
},
created() {
this.init();
},
data() {
return {
show: true,
form: {},
rules: {
account: {
required: true,
message: this.$t('organization.integration.input_api_account'),
trigger: ['change', 'blur']
},
password: {
required: true,
message: this.$t('organization.integration.input_api_password'),
trigger: ['change', 'blur']
}
},
}
},
methods: {
init() {
const {lastOrganizationId} = getCurrentUser();
let param = {};
param.platform = TAPD;
param.orgId = lastOrganizationId;
this.$parent.result = this.$post("service/integration/type", param, response => {
let data = response.data;
if (data.configuration) {
let config = JSON.parse(data.configuration);
this.$set(this.form, 'account', config.account);
this.$set(this.form, 'password', config.password);
} else {
this.clear();
}
})
},
save() {
this.$refs['form'].validate(valid => {
if (valid) {
let param = {};
let auth = {
account: this.form.account,
password: this.form.password,
};
const {lastOrganizationId} = getCurrentUser();
param.organizationId = lastOrganizationId;
param.platform = TAPD;
param.configuration = JSON.stringify(auth);
this.$parent.result = this.$post("service/integration/save", param, () => {
this.show = true;
this.$refs.bugBtn.showEdit = true;
this.$refs.bugBtn.showSave = false;
this.$refs.bugBtn.showCancel = false;
this.init();
this.$success(this.$t('commons.save_success'));
});
} else {
return false;
}
})
},
clear() {
this.$set(this.form, 'account', '');
this.$set(this.form, 'password', '');
this.$nextTick(() => {
this.$refs.form.clearValidate();
});
},
testConnection() {
if (this.form.account && this.form.password) {
this.$parent.result = this.$get("issues/auth/" + TAPD, () => {
this.$success(this.$t('organization.integration.verified'));
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
return false;
}
},
cancelIntegration() {
if (this.form.account && this.form.password) {
this.$alert(this.$t('organization.integration.cancel_confirm') + TAPD + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
const {lastOrganizationId} = getCurrentUser();
let param = {};
param.orgId = lastOrganizationId;
param.platform = TAPD;
this.$parent.result = this.$post("service/integration/delete", param, () => {
this.$success(this.$t('organization.integration.successful_operation'));
this.init('');
});
}
}
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
}
}
}
}
</script>
<style scoped>
.defect-tip {
background: #EDEDED;
border: solid #E1E1E1 1px;
margin: 10px 0;
padding: 10px;
border-radius: 3px;
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<div>
<div style="width: 500px">
<div style="margin-top: 20px;margin-bottom: 10px">{{ $t('organization.integration.basic_auth_info') }}</div>
<el-form :model="form" ref="form" label-width="120px" size="small" :disabled="show" :rules="rules">
<el-form-item :label="$t('organization.integration.app_name')" prop="account">
<el-input v-model="form.account" :placeholder="$t('organization.integration.input_app_name')"/>
</el-form-item>
<el-form-item :label="$t('organization.integration.app_key')" prop="password">
<el-input v-model="form.password" auto-complete="new-password"
:placeholder="$t('organization.integration.input_app_key')" show-password/>
</el-form-item>
</el-form>
</div>
<bug-manage-btn @save="save"
@init="init"
@testConnection="testConnection"
@cancelIntegration="cancelIntegration"
:form="form"
:show.sync="show"
ref="bugBtn"/>
<div class="defect-tip">
<div>{{ $t('organization.integration.use_tip') }}</div>
<div>
1. {{ $t('organization.integration.use_tip_zentao') }}
</div>
<div>
2. {{ $t('organization.integration.use_tip_two') }}
<router-link to="/track/project/all" style="margin-left: 5px">
{{ $t('organization.integration.link_the_project_now') }}
</router-link>
</div>
</div>
</div>
</template>
<script>
import BugManageBtn from "@/business/components/settings/organization/components/BugManageBtn";
import {getCurrentUser} from "@/common/js/utils";
import {ZEN_TAO} from "@/common/js/constants";
export default {
name: "ZentaoSetting",
components: {
BugManageBtn
},
created() {
this.init();
},
data() {
return {
show: true,
form: {},
rules: {
account: {
required: true,
message: this.$t('organization.integration.input_app_name'),
trigger: ['change', 'blur']
},
password: {
required: true,
message: this.$t('organization.integration.input_app_key'),
trigger: ['change', 'blur']
}
},
}
},
methods: {
save() {
this.$refs['form'].validate(valid => {
if (valid) {
const {lastOrganizationId} = getCurrentUser();
let param = {};
let auth = {
account: this.form.account,
password: this.form.password,
};
param.organizationId = lastOrganizationId;
param.platform = ZEN_TAO;
param.configuration = JSON.stringify(auth);
this.$parent.result = this.$post("service/integration/save", param, () => {
this.show = true;
this.$refs.bugBtn.showEdit = true;
this.$refs.bugBtn.showSave = false;
this.$refs.bugBtn.showCancel = false;
this.init();
this.$success(this.$t('commons.save_success'));
});
} else {
return false;
}
})
},
init() {
const {lastOrganizationId} = getCurrentUser();
let param = {};
param.platform = ZEN_TAO;
param.orgId = lastOrganizationId;
this.$parent.result = this.$post("service/integration/type", param, response => {
let data = response.data;
if (data.configuration) {
let config = JSON.parse(data.configuration);
this.$set(this.form, 'account', config.account);
this.$set(this.form, 'password', config.password);
} else {
this.clear();
}
})
},
clear() {
this.$set(this.form, 'account', '');
this.$set(this.form, 'password', '');
this.$nextTick(() => {
this.$refs.form.clearValidate();
});
},
testConnection() {
if (this.form.account && this.form.password) {
this.$parent.result = this.$get("issues/auth/" + ZEN_TAO, () => {
this.$success(this.$t('organization.integration.verified'));
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
return false;
}
},
cancelIntegration() {
if (this.form.account && this.form.password) {
this.$alert(this.$t('organization.integration.cancel_confirm') + ZEN_TAO + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
const {lastOrganizationId} = getCurrentUser();
let param = {};
param.orgId = lastOrganizationId;
param.platform = ZEN_TAO;
this.$parent.result = this.$post("service/integration/delete", param, () => {
this.$success(this.$t('organization.integration.successful_operation'));
this.init('');
});
}
}
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
}
}
}
}
</script>
<style scoped>
.defect-tip {
background: #EDEDED;
border: solid #E1E1E1 1px;
margin: 10px 0;
padding: 10px;
border-radius: 3px;
}
</style>

View File

@ -128,7 +128,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
:label="$t('commons.operating')" min-width="100"> :label="$t('commons.operating')" min-width="150">
<template v-slot:default="scope"> <template v-slot:default="scope">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)" <ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)"
@deleteClick="handleDelete(scope.row)"> @deleteClick="handleDelete(scope.row)">
@ -154,38 +154,39 @@
<script> <script>
import MsCreateBox from '../../../settings/CreateBox'; import MsCreateBox from '../../../settings/CreateBox';
import TestCaseImport from '../components/TestCaseImport'; import TestCaseImport from '../components/TestCaseImport';
import TestCaseExport from '../components/TestCaseExport'; import TestCaseExport from '../components/TestCaseExport';
import MsTablePagination from '../../../../components/common/pagination/TablePagination'; import MsTablePagination from '../../../../components/common/pagination/TablePagination';
import NodeBreadcrumb from '../../common/NodeBreadcrumb'; import NodeBreadcrumb from '../../common/NodeBreadcrumb';
import MsTableHeader from '../../../../components/common/components/MsTableHeader'; import MsTableHeader from '../../../../components/common/components/MsTableHeader';
import PriorityTableItem from "../../common/tableItems/planview/PriorityTableItem"; import PriorityTableItem from "../../common/tableItems/planview/PriorityTableItem";
import TypeTableItem from "../../common/tableItems/planview/TypeTableItem"; import TypeTableItem from "../../common/tableItems/planview/TypeTableItem";
import MethodTableItem from "../../common/tableItems/planview/MethodTableItem"; import MethodTableItem from "../../common/tableItems/planview/MethodTableItem";
import MsTableOperator from "../../../common/components/MsTableOperator"; import MsTableOperator from "../../../common/components/MsTableOperator";
import MsTableOperatorButton from "../../../common/components/MsTableOperatorButton"; import MsTableOperatorButton from "../../../common/components/MsTableOperatorButton";
import MsTableButton from "../../../common/components/MsTableButton"; import MsTableButton from "../../../common/components/MsTableButton";
import {_filter, _sort} from "../../../../../common/js/utils"; import {_filter, _sort} from "../../../../../common/js/utils";
import {TEST_CASE_CONFIGS} from "../../../common/components/search/search-components"; import {TEST_CASE_CONFIGS} from "../../../common/components/search/search-components";
import ShowMoreBtn from "./ShowMoreBtn"; import ShowMoreBtn from "./ShowMoreBtn";
import BatchEdit from "./BatchEdit"; import BatchEdit from "./BatchEdit";
import {WORKSPACE_ID} from "../../../../../common/js/constants"; import {WORKSPACE_ID} from "../../../../../common/js/constants";
import {LIST_CHANGE, TrackEvent} from "@/business/components/common/head/ListEvent"; import {LIST_CHANGE, TrackEvent} from "@/business/components/common/head/ListEvent";
import StatusTableItem from "@/business/components/track/common/tableItems/planview/StatusTableItem"; import StatusTableItem from "@/business/components/track/common/tableItems/planview/StatusTableItem";
import TestCaseDetail from "./TestCaseDetail"; import TestCaseDetail from "./TestCaseDetail";
import ReviewStatus from "@/business/components/track/case/components/ReviewStatus"; import ReviewStatus from "@/business/components/track/case/components/ReviewStatus";
export default {
name: "TestCaseList", export default {
components: { name: "TestCaseList",
MsTableButton, components: {
MsTableOperatorButton, MsTableButton,
MsTableOperator, MsTableOperatorButton,
MethodTableItem, MsTableOperator,
TypeTableItem, MethodTableItem,
PriorityTableItem, TypeTableItem,
MsCreateBox, PriorityTableItem,
TestCaseImport, MsCreateBox,
TestCaseImport,
TestCaseExport, TestCaseExport,
MsTablePagination, MsTablePagination,
NodeBreadcrumb, NodeBreadcrumb,

View File

@ -7,7 +7,7 @@
<related-test-plan-list ref="relatedTestPlanList"/> <related-test-plan-list ref="relatedTestPlanList"/>
</el-row> </el-row>
<el-row> <el-row>
<review-list title="我的评审" ref="caseReviewList"/> <review-list :title="$t('review.my_review')" ref="caseReviewList"/>
</el-row> </el-row>
</el-col> </el-col>
<el-col :span="9"> <el-col :span="9">

View File

@ -114,6 +114,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
min-width="150"
:label="$t('commons.operating')"> :label="$t('commons.operating')">
<template v-slot:default="scope"> <template v-slot:default="scope">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)" <ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)"

View File

@ -183,7 +183,8 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
:label="$t('commons.operating')"> min-width="100"
:label="$t('commons.operating')">
<template v-slot:default="scope"> <template v-slot:default="scope">
<ms-table-operator-button :is-tester-permission="true" :tip="$t('commons.edit')" icon="el-icon-edit" <ms-table-operator-button :is-tester-permission="true" :tip="$t('commons.edit')" icon="el-icon-edit"
@exec="handleEdit(scope.row)"/> @exec="handleEdit(scope.row)"/>

View File

@ -19,10 +19,10 @@
<el-tabs class="test-config" v-model="active" type="border-card" :stretch="true"> <el-tabs class="test-config" v-model="active" type="border-card" :stretch="true">
<el-tab-pane :label="$t('load_test.basic_config')"> <el-tab-pane :label="$t('load_test.basic_config')">
<performance-basic-config :is-read-only="true" :test-plan="test" ref="basicConfig"/> <performance-basic-config :is-read-only="true" :test="test" ref="basicConfig"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('load_test.pressure_config')"> <el-tab-pane :label="$t('load_test.pressure_config')">
<performance-pressure-config :is-read-only="true" :test-plan="test" :test-id="id" ref="pressureConfig"/> <performance-pressure-config :is-read-only="true" :test="test" :test-id="id" ref="pressureConfig"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('load_test.advanced_config')" class="advanced-config"> <el-tab-pane :label="$t('load_test.advanced_config')" class="advanced-config">
<performance-advanced-config :read-only="true" :test-id="id" ref="advancedConfig"/> <performance-advanced-config :read-only="true" :test-id="id" ref="advancedConfig"/>
@ -35,22 +35,23 @@
<script> <script>
import MsContainer from "../../../../../common/components/MsContainer"; import MsContainer from "../../../../../common/components/MsContainer";
import MsMainContainer from "../../../../../common/components/MsMainContainer"; import MsMainContainer from "../../../../../common/components/MsMainContainer";
import PerformanceBasicConfig from "../../../../../performance/test/components/PerformanceBasicConfig"; import PerformanceBasicConfig from "../../../../../performance/test/components/PerformanceBasicConfig";
import PerformancePressureConfig from "../../../../../performance/test/components/PerformancePressureConfig"; import PerformancePressureConfig from "../../../../../performance/test/components/PerformancePressureConfig";
import PerformanceAdvancedConfig from "../../../../../performance/test/components/PerformanceAdvancedConfig"; import PerformanceAdvancedConfig from "../../../../../performance/test/components/PerformanceAdvancedConfig";
export default {
name: "PerformanceTestDetail", export default {
components: { name: "PerformanceTestDetail",
PerformanceAdvancedConfig, components: {
PerformancePressureConfig, PerformanceAdvancedConfig,
PerformanceBasicConfig, PerformancePressureConfig,
MsMainContainer, PerformanceBasicConfig,
MsContainer MsMainContainer,
}, MsContainer
data() { },
return { data() {
return {
result: {}, result: {},
test: {}, test: {},
savePath: "/performance/save", savePath: "/performance/save",

View File

@ -62,6 +62,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
min-width="100"
:label="$t('commons.operating')"> :label="$t('commons.operating')">
<template v-slot:default="scope"> <template v-slot:default="scope">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)" <ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)"

View File

@ -124,6 +124,7 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
min-width="100"
:label="$t('commons.operating')"> :label="$t('commons.operating')">
<template v-slot:default="scope"> <template v-slot:default="scope">
<ms-table-operator-button :is-tester-permission="true" :tip="$t('commons.edit')" icon="el-icon-edit" <ms-table-operator-button :is-tester-permission="true" :tip="$t('commons.edit')" icon="el-icon-edit"

View File

@ -65,13 +65,13 @@ body {
background-color: white; background-color: white;
} }
.adjust-table th:hover:after { .adjust-table th:not([class*='el-table-column--selection']):hover:after {
content: ''; content: '';
position: absolute; position: absolute;
top: 25%; top: 25%;
right: 0; right: 0;
height: 50%; height: 50%;
width: 3px; width: 2px;
background-color: #EBEEF5; background-color: #EBEEF5;
} }

View File

@ -20,6 +20,10 @@ export const ZH_CN = 'zh_CN';
export const ZH_TW = 'zh_TW'; export const ZH_TW = 'zh_TW';
export const EN_US = 'en_US'; export const EN_US = 'en_US';
export const TAPD = 'Tapd';
export const JIRA = 'Jira';
export const ZEN_TAO = 'Zentao';
export const SCHEDULE_TYPE = { export const SCHEDULE_TYPE = {
API_TEST: 'API_TEST', API_TEST: 'API_TEST',
PERFORMANCE_TEST: 'PERFORMANCE_TEST' PERFORMANCE_TEST: 'PERFORMANCE_TEST'

View File

@ -250,15 +250,22 @@ export default {
basic_auth_info: 'Basic Auth account information:', basic_auth_info: 'Basic Auth account information:',
api_account: 'API account', api_account: 'API account',
api_password: 'API password', api_password: 'API password',
app_name: 'APP name',
app_key: 'APP key',
account: 'Account',
password: 'Password',
jira_url: 'JIRA url', jira_url: 'JIRA url',
jira_issuetype: 'JIRA issuetype', jira_issuetype: 'JIRA issuetype',
input_api_account: 'please enter account', input_api_account: 'please enter account',
input_api_password: 'Please enter password', input_api_password: 'Please enter password',
input_app_name: 'Please enter the application code',
input_app_key: 'Please enter the key',
input_jira_url: 'Please enter Jira address, for example: https://metersphere.atlassian.net/', input_jira_url: 'Please enter Jira address, for example: https://metersphere.atlassian.net/',
input_jira_issuetype: 'Please enter the question type', input_jira_issuetype: 'Please enter the question type',
use_tip: 'Usage guidelines:', use_tip: 'Usage guidelines:',
use_tip_tapd: 'Basic Auth account information is queried in "Company Management-Security and Integration-Open Platform"', use_tip_tapd: 'Basic Auth account information is queried in "Company Management-Security and Integration-Open Platform"',
use_tip_jira: 'Jira software server authentication information is account password, Jira software cloud authentication information is account + token (account settings-security-create API token)', use_tip_jira: 'Jira software server authentication information is account password, Jira software cloud authentication information is account + token (account settings-security-create API token)',
use_tip_zentao: 'Log in to ZenTao as a super administrator user, enter the background-secondary development-application, click [Add Application] to add an application',
use_tip_two: 'After saving the Basic Auth account information, you need to manually associate the ID/key in the Metersphere project', use_tip_two: 'After saving the Basic Auth account information, you need to manually associate the ID/key in the Metersphere project',
link_the_project_now: 'Link the project now', link_the_project_now: 'Link the project now',
cancel_edit: 'Cancel edit', cancel_edit: 'Cancel edit',

View File

@ -250,15 +250,22 @@ export default {
basic_auth_info: 'Basic Auth 账号信息:', basic_auth_info: 'Basic Auth 账号信息:',
api_account: 'API 账号', api_account: 'API 账号',
api_password: 'API 口令', api_password: 'API 口令',
app_name: '应用代号',
app_key: '密钥',
account: '账号',
password: '密码',
jira_url: 'JIRA 地址', jira_url: 'JIRA 地址',
jira_issuetype: '问题类型', jira_issuetype: '问题类型',
input_api_account: '请输入账号', input_api_account: '请输入账号',
input_api_password: '请输入口令', input_api_password: '请输入口令',
input_app_name: '请输入应用代号',
input_app_key: '请输入密钥',
input_jira_url: '请输入Jira地址https://metersphere.atlassian.net/', input_jira_url: '请输入Jira地址https://metersphere.atlassian.net/',
input_jira_issuetype: '请输入问题类型', input_jira_issuetype: '请输入问题类型',
use_tip: '使用指引:', use_tip: '使用指引:',
use_tip_tapd: 'Tapd Basic Auth 账号信息在"公司管理-安全与集成-开放平台"中查询', use_tip_tapd: 'Tapd Basic Auth 账号信息在"公司管理-安全与集成-开放平台"中查询',
use_tip_jira: 'Jira software server 认证信息为 账号密码Jira software cloud 认证信息为 账号+令牌(账户设置-安全-创建API令牌)', use_tip_jira: 'Jira software server 认证信息为 账号密码Jira software cloud 认证信息为 账号+令牌(账户设置-安全-创建API令牌)',
use_tip_zentao: '用超级管理员用户登录禅道,进入后台-二次开发-应用,点击【添加应用】新增一个应用',
use_tip_two: '保存 Basic Auth 账号信息后,需要在 Metersphere 项目中手动关联 ID/key', use_tip_two: '保存 Basic Auth 账号信息后,需要在 Metersphere 项目中手动关联 ID/key',
link_the_project_now: '马上关联项目', link_the_project_now: '马上关联项目',
cancel_edit: '取消编辑', cancel_edit: '取消编辑',

View File

@ -250,15 +250,22 @@ export default {
basic_auth_info: 'Basic Auth 賬號信息:', basic_auth_info: 'Basic Auth 賬號信息:',
api_account: 'API 賬號', api_account: 'API 賬號',
api_password: 'API 口令', api_password: 'API 口令',
app_name: '應用代號',
app_key: '密鑰',
account: '賬號',
password: '密碼',
jira_url: 'JIRA 地址', jira_url: 'JIRA 地址',
jira_issuetype: '問題類型', jira_issuetype: '問題類型',
input_api_account: '請輸入賬號', input_api_account: '請輸入賬號',
input_api_password: '請輸入口令', input_api_password: '請輸入口令',
input_app_name: '請輸入應用代號',
input_app_key: '請輸入密鑰',
input_jira_url: '請輸入Jira地址https://metersphere.atlassian.net/', input_jira_url: '請輸入Jira地址https://metersphere.atlassian.net/',
input_jira_issuetype: '請輸入問題類型', input_jira_issuetype: '請輸入問題類型',
use_tip: '使用指引:', use_tip: '使用指引:',
use_tip_tapd: 'Tapd Basic Auth 賬號信息在"公司管理-安全與集成-開放平臺"中查詢', use_tip_tapd: 'Tapd Basic Auth 賬號信息在"公司管理-安全與集成-開放平臺"中查詢',
use_tip_jira: 'Jira software server 認證信息為 賬號密碼Jira software cloud 認證信息為 賬號+令牌(賬戶設置-安全-創建API令牌)', use_tip_jira: 'Jira software server 認證信息為 賬號密碼Jira software cloud 認證信息為 賬號+令牌(賬戶設置-安全-創建API令牌)',
use_tip_zentao: '用超級管理員用戶登錄禪道,進入後台-二次開發-應用,點擊【添加應用】添加一個應用',
use_tip_two: '保存 Basic Auth 賬號信息後,需要在 Metersphere 項目中手動關聯 ID/key', use_tip_two: '保存 Basic Auth 賬號信息後,需要在 Metersphere 項目中手動關聯 ID/key',
link_the_project_now: '馬上關聯項目', link_the_project_now: '馬上關聯項目',
cancel_edit: '取消編輯', cancel_edit: '取消編輯',