This commit is contained in:
chenjianxing 2020-11-13 19:33:58 +08:00
commit 2c47bcc073
61 changed files with 1845 additions and 823 deletions

View File

@ -18,7 +18,8 @@ MeterSphere 是一站式的开源企业级持续测试平台,涵盖测试跟
- 性能测试: 兼容 JMeter支持 Kubernetes 和云环境,轻松支持高并发、分布式的性能测试;
- 团队协作: 两级租户体系,天然支持团队协作。
![产品定位](https://metersphere.io/images/icon/ct-devops.png)
![产品定位](https://metersphere.oss-cn-hangzhou.aliyuncs.com/img/ct-devops.png)
> 如需进一步了解 MeterSphere 开源项目,推荐阅读 [MeterSphere 的初心和使命](https://mp.weixin.qq.com/s/DpCt3BNgBTlV3sJ5qtPmZw)
@ -295,7 +296,7 @@ v1.1.0 是 v1.0.0 之后的功能版本。
## 微信群
![wechat-group](https://metersphere.io/images/contact/wechat-group.png)
![wechat-group](https://metersphere.oss-cn-hangzhou.aliyuncs.com/img/wechat-group.png)
## License & Copyright

View File

@ -39,10 +39,12 @@ public class APIReportController {
return apiReportService.recentTest(request);
}
@GetMapping("/list/{testId}")
public List<APIReportResult> listByTestId(@PathVariable String testId) {
@GetMapping("/list/{testId}/{goPage}/{pageSize}")
public Pager<List<APIReportResult>> listByTestId(@PathVariable String testId, @PathVariable int goPage, @PathVariable int pageSize) {
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}")

View File

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

View File

@ -22,5 +22,7 @@ public class LoadTestReport implements Serializable {
private String triggerMode;
private String fileId;
private static final long serialVersionUID = 1L;
}

View File

@ -643,6 +643,76 @@ public class LoadTestReportExample {
addCriterion("trigger_mode not between", value1, value2, "triggerMode");
return (Criteria) this;
}
public Criteria andFileIdIsNull() {
addCriterion("file_id is null");
return (Criteria) this;
}
public Criteria andFileIdIsNotNull() {
addCriterion("file_id is not null");
return (Criteria) this;
}
public Criteria andFileIdEqualTo(String value) {
addCriterion("file_id =", value, "fileId");
return (Criteria) this;
}
public Criteria andFileIdNotEqualTo(String value) {
addCriterion("file_id <>", value, "fileId");
return (Criteria) this;
}
public Criteria andFileIdGreaterThan(String value) {
addCriterion("file_id >", value, "fileId");
return (Criteria) this;
}
public Criteria andFileIdGreaterThanOrEqualTo(String value) {
addCriterion("file_id >=", value, "fileId");
return (Criteria) this;
}
public Criteria andFileIdLessThan(String value) {
addCriterion("file_id <", value, "fileId");
return (Criteria) this;
}
public Criteria andFileIdLessThanOrEqualTo(String value) {
addCriterion("file_id <=", value, "fileId");
return (Criteria) this;
}
public Criteria andFileIdLike(String value) {
addCriterion("file_id like", value, "fileId");
return (Criteria) this;
}
public Criteria andFileIdNotLike(String value) {
addCriterion("file_id not like", value, "fileId");
return (Criteria) this;
}
public Criteria andFileIdIn(List<String> values) {
addCriterion("file_id in", values, "fileId");
return (Criteria) this;
}
public Criteria andFileIdNotIn(List<String> values) {
addCriterion("file_id not in", values, "fileId");
return (Criteria) this;
}
public Criteria andFileIdBetween(String value1, String value2) {
addCriterion("file_id between", value1, value2, "fileId");
return (Criteria) this;
}
public Criteria andFileIdNotBetween(String value1, String value2) {
addCriterion("file_id not between", value1, value2, "fileId");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {

View File

@ -10,6 +10,7 @@
<result column="status" jdbcType="VARCHAR" property="status" />
<result column="user_id" jdbcType="VARCHAR" property="userId" />
<result column="trigger_mode" jdbcType="VARCHAR" property="triggerMode" />
<result column="file_id" jdbcType="VARCHAR" property="fileId" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.LoadTestReportWithBLOBs">
<result column="description" jdbcType="LONGVARCHAR" property="description" />
@ -74,7 +75,7 @@
</where>
</sql>
<sql id="Base_Column_List">
id, test_id, `name`, create_time, update_time, `status`, user_id, trigger_mode
id, test_id, `name`, create_time, update_time, `status`, user_id, trigger_mode, file_id
</sql>
<sql id="Blob_Column_List">
description, load_configuration
@ -130,12 +131,12 @@
<insert id="insert" parameterType="io.metersphere.base.domain.LoadTestReportWithBLOBs">
INSERT INTO load_test_report (id, test_id, `name`,
create_time, update_time, `status`,
user_id, trigger_mode, description,
load_configuration)
user_id, trigger_mode, file_id,
description, load_configuration)
VALUES (#{id,jdbcType=VARCHAR}, #{testId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR},
#{createTime,jdbcType=BIGINT}, #{updateTime,jdbcType=BIGINT}, #{status,jdbcType=VARCHAR},
#{userId,jdbcType=VARCHAR}, #{triggerMode,jdbcType=VARCHAR}, #{description,jdbcType=LONGVARCHAR},
#{loadConfiguration,jdbcType=LONGVARCHAR})
#{userId,jdbcType=VARCHAR}, #{triggerMode,jdbcType=VARCHAR}, #{fileId,jdbcType=VARCHAR},
#{description,jdbcType=LONGVARCHAR}, #{loadConfiguration,jdbcType=LONGVARCHAR})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.LoadTestReportWithBLOBs">
insert into load_test_report
@ -164,6 +165,9 @@
<if test="triggerMode != null">
trigger_mode,
</if>
<if test="fileId != null">
file_id,
</if>
<if test="description != null">
description,
</if>
@ -196,6 +200,9 @@
<if test="triggerMode != null">
#{triggerMode,jdbcType=VARCHAR},
</if>
<if test="fileId != null">
#{fileId,jdbcType=VARCHAR},
</if>
<if test="description != null">
#{description,jdbcType=LONGVARCHAR},
</if>
@ -237,6 +244,9 @@
<if test="record.triggerMode != null">
trigger_mode = #{record.triggerMode,jdbcType=VARCHAR},
</if>
<if test="record.fileId != null">
file_id = #{record.fileId,jdbcType=VARCHAR},
</if>
<if test="record.description != null">
description = #{record.description,jdbcType=LONGVARCHAR},
</if>
@ -258,6 +268,7 @@
`status` = #{record.status,jdbcType=VARCHAR},
user_id = #{record.userId,jdbcType=VARCHAR},
trigger_mode = #{record.triggerMode,jdbcType=VARCHAR},
file_id = #{record.fileId,jdbcType=VARCHAR},
description = #{record.description,jdbcType=LONGVARCHAR},
load_configuration = #{record.loadConfiguration,jdbcType=LONGVARCHAR}
<if test="_parameter != null">
@ -273,7 +284,8 @@
update_time = #{record.updateTime,jdbcType=BIGINT},
`status` = #{record.status,jdbcType=VARCHAR},
user_id = #{record.userId,jdbcType=VARCHAR},
trigger_mode = #{record.triggerMode,jdbcType=VARCHAR}
trigger_mode = #{record.triggerMode,jdbcType=VARCHAR},
file_id = #{record.fileId,jdbcType=VARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
@ -302,6 +314,9 @@
<if test="triggerMode != null">
trigger_mode = #{triggerMode,jdbcType=VARCHAR},
</if>
<if test="fileId != null">
file_id = #{fileId,jdbcType=VARCHAR},
</if>
<if test="description != null">
description = #{description,jdbcType=LONGVARCHAR},
</if>
@ -312,27 +327,29 @@
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.base.domain.LoadTestReportWithBLOBs">
update load_test_report
set test_id = #{testId,jdbcType=VARCHAR},
UPDATE load_test_report
SET test_id = #{testId,jdbcType=VARCHAR},
`name` = #{name,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=BIGINT},
update_time = #{updateTime,jdbcType=BIGINT},
`status` = #{status,jdbcType=VARCHAR},
user_id = #{userId,jdbcType=VARCHAR},
trigger_mode = #{triggerMode,jdbcType=VARCHAR},
file_id = #{fileId,jdbcType=VARCHAR},
description = #{description,jdbcType=LONGVARCHAR},
load_configuration = #{loadConfiguration,jdbcType=LONGVARCHAR}
where id = #{id,jdbcType=VARCHAR}
WHERE id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.base.domain.LoadTestReport">
update load_test_report
set test_id = #{testId,jdbcType=VARCHAR},
UPDATE load_test_report
SET test_id = #{testId,jdbcType=VARCHAR},
`name` = #{name,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=BIGINT},
update_time = #{updateTime,jdbcType=BIGINT},
`status` = #{status,jdbcType=VARCHAR},
user_id = #{userId,jdbcType=VARCHAR},
trigger_mode = #{triggerMode,jdbcType=VARCHAR}
where id = #{id,jdbcType=VARCHAR}
trigger_mode = #{triggerMode,jdbcType=VARCHAR},
file_id = #{fileId,jdbcType=VARCHAR}
WHERE id = #{id,jdbcType=VARCHAR}
</update>
</mapper>

View File

@ -24,6 +24,7 @@ import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
@ -99,10 +100,10 @@ public class MailService {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom(javaMailSender.getUsername());
if (StringUtils.equals(type, NoticeConstants.API)) {
helper.setSubject("MeterSphere平台" + Translator.get("task_notification"));
helper.setSubject("MeterSphere平台" + Translator.get("task_notification_jenkins"));
}
if (StringUtils.equals(type, NoticeConstants.SCHEDULE)) {
helper.setSubject("MeterSphere平台" + Translator.get("task_notification_"));
helper.setSubject("MeterSphere平台" + Translator.get("task_notification"));
}
String[] users;
List<String> emails = new ArrayList<>();
@ -113,7 +114,11 @@ public class MailService {
users = emails.toArray(new String[0]);
helper.setText(getContent(Template, context), true);
helper.setTo(users);
try {
javaMailSender.send(mimeMessage);
} catch (MailException e) {
LogUtil.error(e);
}
}
//测试评审

View File

@ -16,6 +16,9 @@ import io.metersphere.performance.controller.request.ReportRequest;
import io.metersphere.performance.service.ReportService;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@ -130,4 +133,13 @@ public class PerformanceReportController {
public void deleteReportBatch(@RequestBody DeleteReportRequest reportRequest) {
reportService.deleteReportBatch(reportRequest);
}
@GetMapping("/jtl/download/{reportId}")
public ResponseEntity<byte[]> downloadJtl(@PathVariable String reportId) {
byte[] bytes = reportService.downloadJtl(reportId);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + reportId + ".jtl\"")
.body(bytes);
}
}

View File

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

View File

@ -18,6 +18,7 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public abstract class AbstractEngine implements Engine {
@ -81,9 +82,22 @@ public abstract class AbstractEngine implements Engine {
String loadConfiguration = t.getLoadConfiguration();
JSONArray jsonArray = JSON.parseArray(loadConfiguration);
for (int i = 0; i < jsonArray.size(); i++) {
if (jsonArray.get(i) instanceof Map) {
JSONObject o = jsonArray.getJSONObject(i);
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;

View File

@ -10,7 +10,6 @@ public class EngineContext {
private String fileType;
private String content;
private String resourcePoolId;
private Long threadNum;
private Long startTime;
private String reportId;
private Integer resourceIndex;
@ -95,14 +94,6 @@ public class EngineContext {
this.resourcePoolId = resourcePoolId;
}
public Long getThreadNum() {
return threadNum;
}
public void setThreadNum(Long threadNum) {
this.threadNum = threadNum;
}
public Long getStartTime() {
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.ResourcePoolTypeEnum;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.config.KafkaProperties;
import io.metersphere.i18n.Translator;
import io.metersphere.performance.engine.docker.DockerTestEngine;
@ -22,6 +23,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -52,7 +54,7 @@ public class EngineFactory {
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());
if (org.springframework.util.CollectionUtils.isEmpty(fileMetadataList)) {
MSException.throwException(Translator.get("run_load_test_file_not_found") + loadTest.getId());
@ -73,7 +75,6 @@ public class EngineFactory {
engineContext.setTestName(loadTest.getName());
engineContext.setNamespace(loadTest.getProjectId());
engineContext.setFileType(jmxFile.getType());
engineContext.setThreadNum(threadNum);
engineContext.setResourcePoolId(loadTest.getTestResourcePoolId());
engineContext.setStartTime(startTime);
engineContext.setReportId(reportId);
@ -90,8 +91,34 @@ public class EngineFactory {
final JSONArray jsonArray = JSONObject.parseArray(loadTest.getLoadConfiguration());
for (int i = 0; i < jsonArray.size(); i++) {
final JSONObject jsonObject = jsonArray.getJSONObject(i);
engineContext.addProperty(jsonObject.getString("key"), jsonObject.get("value"));
if (jsonArray.get(i) instanceof Map) {
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);
engineContext.setContent(content);
} catch (MSException e) {
LogUtil.error(e);
throw e;
} catch (Exception e) {
LogUtil.error(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.exception.MSException;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.controller.ResultHolder;
import io.metersphere.dto.NodeDTO;
import io.metersphere.i18n.Translator;
@ -52,19 +53,21 @@ public class DockerTestEngine extends AbstractEngine {
for (int i = 0, size = resourceList.size(); i < size; i++) {
int ratio = resourceRatio.get(i);
double realThreadNum = ((double) ratio / totalThreadNum) * threadNum;
runTest(resourceList.get(i), Math.round(realThreadNum), i);
// double realThreadNum = ((double) ratio / totalThreadNum) * threadNum;
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;
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) {
LogUtil.error(e);
throw e;
} catch (Exception e) {
LogUtil.error(e);
MSException.throwException(e);
}
@ -80,6 +83,7 @@ public class DockerTestEngine extends AbstractEngine {
TestRequest testRequest = new TestRequest();
testRequest.setSize(1);
testRequest.setTestId(testId);
testRequest.setReportId(getReportId());
testRequest.setFileString(content);
testRequest.setImage(JMETER_IMAGE);
testRequest.setTestData(context.getTestData());

View File

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

View File

@ -39,34 +39,36 @@ public class PerformanceNoticeTask {
private LoadTestReportMapper loadTestReportMapper;
private final ExecutorService executorService = Executors.newFixedThreadPool(20);
private boolean isRunning = true;
@PreDestroy
private boolean isRunning=false;
/*@PreDestroy
public void preDestroy() {
isRunning = false;
}
}*/
public void registerNoticeTask(LoadTestReportWithBLOBs loadTestReport) {
int count = 20;
while (count-- > 0) {
isRunning=true;
executorService.submit(() -> {
LogUtil.info("性能测试定时任务");
while (isRunning) {
LoadTestReportWithBLOBs loadTestReportFromDatabase = loadTestReportMapper.selectByPrimaryKey(loadTestReport.getId());
if (StringUtils.equals(loadTestReportFromDatabase.getStatus(), PerformanceTestStatus.Completed.name())) {
isRunning = false;
sendSuccessNotice(loadTestReportFromDatabase);
return;
isRunning=false;
}
if (StringUtils.equals(loadTestReportFromDatabase.getStatus(), PerformanceTestStatus.Error.name())) {
isRunning = false;
sendFailNotice(loadTestReportFromDatabase);
return;
isRunning=false;
}
count--;
try {
Thread.sleep(1000 * 4L);// 每分钟检查 loadtest 的状态
//查询定时任务是否关闭
Thread.sleep(1000 * 30);// 每分钟检查 loadtest 的状态
} catch (InterruptedException e) {
LogUtil.error(e);
}
}
});
}
public void sendSuccessNotice(LoadTestReportWithBLOBs loadTestReport) {

View File

@ -776,15 +776,12 @@ public class JmeterDocumentParser implements DocumentParser {
elementProp.setAttribute("name", "ThreadGroup.main_controller");
elementProp.setAttribute("elementType", "com.blazemeter.jmeter.control.VirtualUserController");
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, "TargetLevel", "2"));
threadGroup.appendChild(createStringProp(document, "RampUp", "12"));
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", ""));
// bzm - Concurrency Thread Group "Thread Iterations Limit:" 设置为空
// threadGroup.appendChild(createStringProp(document, "Iterations", "1"));
@ -803,9 +800,18 @@ public class JmeterDocumentParser implements DocumentParser {
</collectionProp>
</kg.apc.jmeter.timers.VariableThroughputTimer>
*/
if (context.getProperty("rpsLimitEnable") == null || StringUtils.equals(context.getProperty("rpsLimitEnable").toString(), "false")) {
if (context.getProperty("rpsLimitEnable") == null) {
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();
@ -866,11 +872,6 @@ public class JmeterDocumentParser implements DocumentParser {
if (nodeNameEquals(ele, STRING_PROP)) {
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++;
} else {
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));
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) {
if (stringProp.getChildNodes().getLength() > 0 && context.getProperty(stringProp.getAttribute("name")) != null) {
stringProp.getFirstChild().setNodeValue(context.getProperty(stringProp.getAttribute("name")).toString());
Object threadParams = context.getProperty(stringProp.getAttribute("name"));
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

@ -207,6 +207,7 @@ public class PerformanceTestService {
@Transactional(noRollbackFor = MSException.class)// 保存失败的信息
public String run(RunTestPlanRequest request) {
LogUtil.info("性能测试run测试");
final LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(request.getId());
if (request.getUserId() != null) {
loadTest.setUserId(request.getUserId());
@ -345,6 +346,17 @@ public class PerformanceTestService {
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) {
LoadTestExample example = new LoadTestExample();
example.createCriteria().andTestResourcePoolIdEqualTo(resourcePoolId);

View File

@ -13,11 +13,13 @@ import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.controller.request.OrderRequest;
import io.metersphere.dto.LogDetailDTO;
import io.metersphere.dto.ReportDTO;
import io.metersphere.i18n.Translator;
import io.metersphere.performance.base.*;
import io.metersphere.performance.controller.request.DeleteReportRequest;
import io.metersphere.performance.controller.request.ReportRequest;
import io.metersphere.performance.engine.Engine;
import io.metersphere.performance.engine.EngineFactory;
import io.metersphere.service.FileService;
import io.metersphere.service.TestResourceService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@ -47,6 +49,8 @@ public class ReportService {
private TestResourceService testResourceService;
@Resource
private LoadTestReportDetailMapper loadTestReportDetailMapper;
@Resource
private FileService fileService;
public List<ReportDTO> getRecentReportList(ReportRequest request) {
List<OrderRequest> orders = new ArrayList<>();
@ -168,7 +172,10 @@ public class ReportService {
public void checkReportStatus(String reportId) {
LoadTestReport loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId);
String reportStatus = loadTestReport.getStatus();
String reportStatus = "";
if (loadTestReport != null) {
reportStatus = loadTestReport.getStatus();
}
if (StringUtils.equals(PerformanceTestStatus.Error.name(), reportStatus)) {
MSException.throwException("Report generation error!");
}
@ -268,4 +275,12 @@ public class ReportService {
String content = getContent(id, ReportKeys.ResponseCodeChart);
return JSON.parseArray(content, ChartsData.class);
}
public byte[] downloadJtl(String reportId) {
LoadTestReportWithBLOBs report = getReport(reportId);
if (StringUtils.isBlank(report.getFileId())) {
throw new RuntimeException(Translator.get("load_test_report_file_not_exist"));
}
return fileService.loadFileAsBytes(report.getFileId());
}
}

View File

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

@ -1 +1 @@
Subproject commit 24047fea950a74f7848a9fdaa857a22b884c4ce2
Subproject commit 57d6f78efa4b0300be188e8b024511ceef0873ed

View File

@ -0,0 +1,2 @@
ALTER TABLE load_test_report
ADD file_id VARCHAR(50) NULL;

View File

@ -48,6 +48,7 @@ related_case_del_fail_prefix=Connected to
related_case_del_fail_suffix=TestCase, please disassociate first
jmx_content_valid=JMX content is invalid
container_delete_fail=The container failed to stop, please try again
load_test_report_file_not_exist=There is no JTL file in the current report, please execute it again to get it
#workspace
workspace_name_is_null=Workspace name cannot be null
workspace_name_already_exists=The workspace name already exists
@ -167,8 +168,8 @@ check_owner_comment=The current user does not have permission to manipulate this
upload_content_is_null=Imported content is empty
test_plan_notification=Test plan notification
task_defect_notification=Task defect notification
task_notification=Jenkins Task notification
task_notification_=Timing task result notification
task_notification_jenkins=Jenkins Task notification
task_notification=Result notification

View File

@ -48,6 +48,7 @@ related_case_del_fail_prefix=已关联到
related_case_del_fail_suffix=测试用例,请先解除关联
jmx_content_valid=JMX 内容无效,请检查
container_delete_fail=容器由于网络原因停止失败,请重试
load_test_report_file_not_exist=当前报告没有JTL文件请重新执行以便获取
#workspace
workspace_name_is_null=工作空间名不能为空
workspace_name_already_exists=工作空间名已存在
@ -168,5 +169,5 @@ check_owner_comment=当前用户没有操作此评论的权限
upload_content_is_null=导入内容为空
test_plan_notification=测试计划通知
task_defect_notification=缺陷任务通知
task_notification=jenkins任务通知
task_notification_=定时任务结果通知
task_notification_jenkins=jenkins任务通知
task_notification=任务通知

View File

@ -48,6 +48,7 @@ related_case_del_fail_prefix=已關聯到
related_case_del_fail_suffix=測試用例,請先解除關聯
jmx_content_valid=JMX 內容無效,請檢查
container_delete_fail=容器由於網絡原因停止失敗,請重試
load_test_report_file_not_exist=當前報告沒有JTL文件請重新執行以便獲取
#workspace
workspace_name_is_null=工作空間名不能為空
workspace_name_already_exists=工作空間名已存在
@ -169,6 +170,6 @@ check_owner_comment=當前用戶沒有操作此評論的權限
upload_content_is_null=導入內容為空
test_plan_notification=測試計畫通知
task_defect_notification=缺陷任務通知
task_notification=jenkins任務通知
task_notification_=定時任務通知
task_notification_jenkins=jenkins任務通知
task_notification=任務通知

View File

@ -1,3 +1,3 @@
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

View File

@ -37,7 +37,8 @@
"nprogress": "^0.2.0",
"el-table-infinite-scroll": "^1.0.10",
"vue-pdf": "^4.2.0",
"diffable-html": "^4.0.0"
"diffable-html": "^4.0.0",
"xml-js": "^1.6.11"
},
"devDependencies": {
"@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;
this.total = data.itemCount;
this.tableData = data.listObject;
this.selectRows.clear();
});
},
handleSelectionChange(val) {
@ -171,28 +172,13 @@ export default {
this.$set(row, "showMore", true);
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) {
if (selection.length > 0) {
if (selection.length === 1) {
this.selectRows.add(selection[0]);
} else {
this.tableData.forEach(item => {
this.$set(item, "showMore", true);
this.selectRows.add(item);
});
}
} else {
this.selectRows.clear();
this.tableData.forEach(row => {

View File

@ -22,16 +22,19 @@
</template>
</el-table-column>
</el-table>
<ms-table-pagination :change="search" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-dialog>
</template>
<script>
import MsApiReportStatus from "../report/ApiReportStatus";
import MsTablePagination from "@/business/components/common/pagination/TablePagination";
export default {
name: "MsApiReportDialog",
components: {MsApiReportStatus},
components: {MsApiReportStatus, MsTablePagination},
props: ["testId"],
@ -40,7 +43,10 @@
reportVisible: false,
result: {},
tableData: [],
loading: false
loading: false,
currentPage: 1,
pageSize: 5,
total: 0,
}
},
@ -48,10 +54,7 @@
open() {
this.reportVisible = true;
let url = "/api/report/list/" + this.testId;
this.result = this.$get(url, response => {
this.tableData = response.data;
});
this.search();
},
link(row) {
this.reportVisible = false;
@ -59,7 +62,15 @@
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>

View File

@ -305,6 +305,7 @@
},
cancel() {
this.$router.push('/api/test/list/all');
// console.log(this.test.toJMX().xml);
},
handleCommand(command) {
switch (command) {

View File

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

View File

@ -3,7 +3,8 @@
<div class="assertion-add">
<el-row :gutter="10">
<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">
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
@ -14,13 +15,18 @@
</el-select>
</el-col>
<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-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :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-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT"
:callback="after"/>
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX"
: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"
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">
{{ $t('api_test.request.assertions.add') }}
</el-button>
@ -28,7 +34,7 @@
</el-row>
</div>
<div>
<div v-if="!scenario">
<el-row :gutter="10" class="json-path-suggest-button">
<el-button size="small" type="primary" @click="suggestJsonOpen">
{{ $t('api_test.request.assertions.json_path_suggest') }}
@ -39,7 +45,8 @@
</el-row>
</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"/>
</div>
@ -49,7 +56,7 @@
import MsApiAssertionText from "./ApiAssertionText";
import MsApiAssertionRegex from "./ApiAssertionRegex";
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 MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223";
@ -64,11 +71,13 @@
MsApiAssertionJsr223,
MsApiJsonpathSuggestList,
MsApiAssertionJsonPath,
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText},
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText
},
props: {
assertions: Assertions,
request: HttpRequest,
scenario: Scenario,
isReadOnly: {
type: Boolean,
default: false

View File

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

View File

@ -25,6 +25,9 @@
<el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)">
{{ $t('test_track.plan_view.export_report') }}
</el-button>
<el-button :disabled="isReadOnly" type="warning" plain size="mini" @click="downloadJtl()">
{{ $t('report.downloadJtl') }}
</el-button>
<!--<el-button :disabled="isReadOnly" type="warning" plain size="mini">-->
<!--{{$t('report.compare')}}-->
@ -95,6 +98,7 @@ import MsMainContainer from "../../common/components/MsMainContainer";
import {checkoutTestManagerOrTestUser, exportPdf} from "@/common/js/utils";
import html2canvas from 'html2canvas';
import MsPerformanceReportExport from "./PerformanceReportExport";
import {Message} from "element-ui";
export default {
@ -281,6 +285,34 @@ export default {
this.reportExportVisible = false;
this.result.loading = false;
},
downloadJtl() {
let config = {
url: "/performance/report/jtl/download/" + this.reportId,
method: 'get',
responseType: 'blob'
};
this.result = this.$request(config).then(response => {
const content = response.data;
const blob = new Blob([content]);
if ("download" in document.createElement("a")) {
// IE
// chrome/firefox
let aTag = document.createElement('a');
aTag.download = this.reportId + ".jtl";
aTag.href = URL.createObjectURL(blob);
aTag.click();
URL.revokeObjectURL(aTag.href)
} else {
// IE10+
navigator.msSaveBlob(blob, this.filename)
}
}).catch(e => {
let text = e.response.data.text();
text.then((data) => {
Message.error({message: JSON.parse(data).message || e.message, showClose: true});
});
});
}
},
created() {
this.isReadOnly = false;

View File

@ -222,28 +222,13 @@ export default {
this.$set(row, "showMore", true);
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) {
if (selection.length > 0) {
if (selection.length === 1) {
this.selectRows.add(selection[0]);
} else {
this.tableData.forEach(item => {
this.$set(item, "showMore", true);
this.selectRows.add(item);
});
}
} else {
this.selectRows.clear();
this.tableData.forEach(row => {

View File

@ -1,91 +1,73 @@
<template>
<div v-loading="result.loading" class="pressure-config-container">
<el-row>
<el-col>
<ms-chart class="chart-container" ref="chart1" :options="options" :autoresize="true"></ms-chart>
</el-col>
</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>
<div class="config-form-label">{{ $t('load_test.thread_num') }}</div>
</el-form-item>
<el-form-item>
<el-form-item :label="$t('load_test.thread_num')">
<el-input-number
:disabled="true"
:placeholder="$t('load_test.input_thread_num')"
v-model="threadNumber"
@change="calculateChart"
v-model="threadGroup.threadNumber"
: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>
<br>
<el-form-item :label="$t('load_test.duration')">
<el-input-number
:disabled="true"
:placeholder="$t('load_test.duration')"
v-model="duration"
v-model="threadGroup.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>
<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="rpsLimit"
@change="calculateChart"
v-model="threadGroup.rpsLimit"
: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>
<br>
<el-form-item :label="$t('load_test.ramp_up_time_within')">
<el-input-number
:disabled="true"
placeholder=""
:min="1"
:max="duration"
v-model="rampUpTime"
@change="calculateChart"
:max="threadGroup.duration"
v-model="threadGroup.rampUpTime"
size="mini"/>
</el-form-item>
<el-form-item>
<div>{{ $t('load_test.ramp_up_time_minutes') }}</div>
</el-form-item>
<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(threadNumber, rampUpTime)"
v-model="step"
@change="calculateChart"
:max="Math.min(threadGroup.threadNumber, threadGroup.rampUpTime)"
v-model="threadGroup.step"
size="mini"/>
</el-form-item>
<el-form-item>
<div>{{ $t('load_test.ramp_up_time_times') }}</div>
</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" ref="chart1" :options="orgOptions" :autoresize="true"></ms-chart>
<ms-chart class="chart-container" :options="threadGroup.options" :autoresize="true"></ms-chart>
</el-col>
</el-collapse-item>
</el-collapse>
</el-row>
</div>
</template>
@ -93,6 +75,7 @@
<script>
import echarts from "echarts";
import MsChart from "@/business/components/common/chart/MsChart";
import {findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
const TARGET_LEVEL = "TargetLevel";
const RAMP_UP = "RampUp";
@ -100,6 +83,14 @@ const STEPS = "Steps";
const DURATION = "duration";
const RPS_LIMIT = "rpsLimit";
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 {
name: "MsPerformancePressureConfig",
@ -108,62 +99,91 @@ export default {
data() {
return {
result: {},
threadNumber: 10,
duration: 10,
rampUpTime: 10,
step: 10,
rpsLimit: 10,
threadNumber: 0,
duration: 0,
rampUpTime: 0,
step: 0,
rpsLimit: 0,
rpsLimitEnable: false,
orgOptions: {},
options: {},
resourcePool: null,
resourcePools: [],
activeNames: ["0"],
threadGroups: [],
}
},
mounted() {
this.getLoadConfig();
// this.getJmxContent();
},
methods: {
calculateLoadConfiguration: function (data) {
data.forEach(d => {
switch (d.key) {
for (let i = 0; i < data.length; i++) {
let d = data[i];
if (d instanceof Array) {
d.forEach(item => {
switch (item.key) {
case TARGET_LEVEL:
this.threadNumber = d.value;
this.threadGroups[i].threadNumber = item.value;
break;
case RAMP_UP:
this.rampUpTime = d.value;
this.threadGroups[i].rampUpTime = item.value;
break;
case DURATION:
this.duration = d.value;
this.threadGroups[i].duration = item.value;
break;
case STEPS:
this.step = d.value;
this.threadGroups[i].step = item.value;
break;
case RPS_LIMIT:
this.rpsLimit = d.value;
this.threadGroups[i].rpsLimit = item.value;
break;
case RPS_LIMIT_ENABLE:
this.threadGroups[i].rpsLimitEnable = item.value;
break;
default:
break;
}
});
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();
})
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]);
}
}
},
getLoadConfig() {
if (!this.report.id) {
return;
}
this.$get("/performance/report/" + this.report.id, res => {
this.result = this.$get("/performance/report/" + this.report.id, res => {
let data = res.data;
if (data) {
if (data.loadConfiguration) {
let d = JSON.parse(data.loadConfiguration);
this.calculateLoadConfiguration(d);
} else {
this.$get('/performance/get-load-config/' + this.report.testId, (response) => {
this.$get('/performance/get-load-config/' + this.report.id, (response) => {
if (response.data) {
let data = JSON.parse(response.data);
this.calculateLoadConfiguration(data);
@ -175,14 +195,127 @@ export default {
}
});
},
calculateChart() {
if (this.duration < this.rampUpTime) {
this.rampUpTime = this.duration;
getJmxContent() {
console.log(this.report.testId);
if (!this.report.testId) {
return;
}
if (this.rampUpTime < this.step) {
this.step = this.rampUpTime;
this.result = this.$get('/performance/get-jmx-content/' + this.report.testId, (response) => {
if (response.data) {
this.threadGroups = findThreadGroup(response.data);
this.threadGroups.forEach(tg => {
tg.options = {};
});
this.getLoadConfig();
}
this.orgOptions = {
});
},
calculateTotalChart() {
let handler = this;
if (handler.duration < handler.rampUpTime) {
handler.rampUpTime = handler.duration;
}
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: {
type: 'category',
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 threadPeriod = Math.floor(this.threadNumber / this.step);
let threadInc1 = Math.floor(this.threadNumber / this.step);
let threadInc2 = Math.ceil(this.threadNumber / this.step);
let inc2count = this.threadNumber - this.step * threadInc1;
for (let i = 0; i <= this.duration; i++) {
let threadPeriod = Math.floor(handler.threadNumber / handler.step);
let threadInc1 = Math.floor(handler.threadNumber / handler.step);
let threadInc2 = Math.ceil(handler.threadNumber / handler.step);
let inc2count = handler.threadNumber - handler.step * threadInc1;
for (let i = 0; i <= handler.duration; i++) {
// x
this.orgOptions.xAxis.data.push(i);
handler.options.xAxis.data.push(i);
if (i > timePeriod) {
timePeriod += timeInc;
if (inc2count > 0) {
@ -253,25 +386,22 @@ export default {
} else {
threadPeriod = threadPeriod + threadInc1;
}
if (threadPeriod > this.threadNumber) {
threadPeriod = this.threadNumber;
if (threadPeriod > handler.threadNumber) {
threadPeriod = handler.threadNumber;
}
this.orgOptions.series[0].data.push(threadPeriod);
handler.options.series[0].data.push(threadPeriod);
} else {
this.orgOptions.series[0].data.push(threadPeriod);
handler.options.series[0].data.push(threadPeriod);
}
}
this.calculateTotalChart();
},
},
watch: {
report: {
handler(val) {
if (!val.testId) {
return;
}
this.getLoadConfig();
'report.testId': {
handler() {
this.getJmxContent();
},
deep: true
}
}
}
@ -295,6 +425,7 @@ export default {
.chart-container {
width: 100%;
height: 300px;
}
.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 PerformanceTestHome = () => import('@/business/components/performance/home/PerformanceTestHome')
const EditPerformanceTestPlan = () => import('@/business/components/performance/test/EditPerformanceTestPlan')
const PerformanceTestPlan = () => import('@/business/components/performance/test/PerformanceTestPlan')
const EditPerformanceTest = () => import('@/business/components/performance/test/EditPerformanceTest')
const PerformanceTestList = () => import('@/business/components/performance/test/PerformanceTestList')
const PerformanceTestReport = () => import('@/business/components/performance/report/PerformanceTestReport')
const PerformanceChart = () => import('@/business/components/performance/report/components/PerformanceChart')
const PerformanceReportView = () => import('@/business/components/performance/report/PerformanceReportView')
@ -24,12 +24,12 @@ export default {
{
path: 'test/create',
name: "createPerTest",
component: EditPerformanceTestPlan,
component: EditPerformanceTest,
},
{
path: "test/edit/:testId",
name: "editPerTest",
component: EditPerformanceTestPlan,
component: EditPerformanceTest,
props: {
content: (route) => {
return {
@ -41,7 +41,7 @@ export default {
{
path: "test/:projectId",
name: "perPlan",
component: PerformanceTestPlan
component: PerformanceTestList
},
{
path: "project/:type",

View File

@ -4,12 +4,12 @@
<el-card v-loading="result.loading">
<el-row>
<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"
maxlength="30" show-word-limit
>
<template v-slot:prepend>
<el-select filterable v-model="testPlan.projectId"
<el-select filterable v-model="test.projectId"
:placeholder="$t('load_test.select_project')">
<el-option
v-for="item in projects"
@ -29,7 +29,7 @@
<el-button :disabled="isReadOnly" type="warning" plain @click="cancel">{{ $t('commons.cancel') }}
</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"/>
</el-col>
</el-row>
@ -37,10 +37,11 @@
<el-tabs class="testplan-config" v-model="active" type="border-card" :stretch="true">
<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 :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"/>
</el-tab-pane>
<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";
export default {
name: "EditPerformanceTestPlan",
name: "EditPerformanceTest",
components: {
MsScheduleConfig,
PerformancePressureConfig,
@ -75,7 +76,7 @@ export default {
data() {
return {
result: {},
testPlan: {schedule: {}},
test: {schedule: {}},
listProjectPath: "/project/listAll",
savePath: "/performance/save",
editPath: "/performance/edit",
@ -136,8 +137,8 @@ export default {
importAPITest() {
let apiTest = this.$store.state.api.test;
if (apiTest && apiTest.name) {
this.$set(this.testPlan, "projectId", apiTest.projectId);
this.$set(this.testPlan, "name", apiTest.name);
this.$set(this.test, "projectId", apiTest.projectId);
this.$set(this.test, "name", apiTest.name);
let blob = new Blob([apiTest.jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], apiTest.jmx.name);
this.$refs.basicConfig.beforeUpload(file);
@ -151,9 +152,9 @@ export default {
this.testId = testId;
this.result = this.$get('/performance/get/' + testId, response => {
if (response.data) {
this.testPlan = response.data;
if (!this.testPlan.schedule) {
this.testPlan.schedule = {};
this.test = response.data;
if (!this.test.schedule) {
this.test.schedule = {};
}
}
});
@ -165,7 +166,7 @@ export default {
})
},
save() {
if (!this.validTestPlan()) {
if (!this.validTest()) {
return;
}
@ -180,16 +181,16 @@ export default {
});
},
saveAndRun() {
if (!this.validTestPlan()) {
if (!this.validTest()) {
return;
}
let options = this.getSaveOption();
this.result = this.$request(options, (response) => {
this.testPlan.id = response.data;
this.test.id = response.data;
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;
this.$router.push({path: '/performance/report/view/' + reportId})
// 广 head
@ -199,7 +200,7 @@ export default {
},
getSaveOption() {
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) {
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.testPlan.testResourcePoolId = this.$refs.pressureConfig.resourcePool;
this.test.loadConfiguration = JSON.stringify(this.$refs.pressureConfig.convertProperty());
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
let requestJson = JSON.stringify(this.testPlan, function (key, value) {
let requestJson = JSON.stringify(this.test, function (key, value) {
return key === "file" ? undefined : value
});
@ -235,13 +236,13 @@ export default {
cancel() {
this.$router.push({path: '/performance/test/all'})
},
validTestPlan() {
if (!this.testPlan.name) {
validTest() {
if (!this.test.name) {
this.$error(this.$t('load_test.test_name_is_null'));
return false;
}
if (!this.testPlan.projectId) {
if (!this.test.projectId) {
this.$error(this.$t('load_test.project_is_null'));
return false;
}
@ -268,26 +269,26 @@ export default {
});
},
saveCronExpression(cronExpression) {
this.testPlan.schedule.enable = true;
this.testPlan.schedule.value = cronExpression;
this.test.schedule.enable = true;
this.test.schedule.value = cronExpression;
this.saveSchedule();
},
saveSchedule() {
this.checkScheduleEdit();
let param = {};
param = this.testPlan.schedule;
param.resourceId = this.testPlan.id;
param = this.test.schedule;
param.resourceId = this.test.id;
let url = '/performance/schedule/create';
if (param.id) {
url = '/performance/schedule/update';
}
this.$post(url, param, response => {
this.$success(this.$t('commons.save_success'));
this.getTest(this.testPlan.id);
this.getTest(this.test.id);
});
},
checkScheduleEdit() {
if (!this.testPlan.id) {
if (!this.test.id) {
this.$message(this.$t('api_test.environment.please_save_test'));
return false;
}
@ -304,6 +305,18 @@ export default {
return {
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 MsPerformanceTestStatus from "./PerformanceTestStatus";
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 {TEST_CONFIGS} from "../../common/components/search/search-components";
import {LIST_CHANGE, PerformanceEvent} from "@/business/components/common/head/ListEvent";
@ -164,30 +164,30 @@ export default {
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleEdit(testPlan) {
handleEdit(test) {
this.$router.push({
path: '/performance/test/edit/' + testPlan.id,
path: '/performance/test/edit/' + test.id,
})
},
handleCopy(testPlan) {
this.result = this.$post("/performance/copy", {id: testPlan.id}, () => {
handleCopy(test) {
this.result = this.$post("/performance/copy", {id: test.id}, () => {
this.$success(this.$t('commons.copy_success'));
this.search();
});
},
handleDelete(testPlan) {
this.$alert(this.$t('load_test.delete_confirm') + testPlan.name + "", '', {
handleDelete(test) {
this.$alert(this.$t('load_test.delete_confirm') + test.name + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this._handleDelete(testPlan);
this._handleDelete(test);
}
}
});
},
_handleDelete(testPlan) {
_handleDelete(test) {
let data = {
id: testPlan.id
id: test.id
};
this.result = this.$post(this.deletePath, data, () => {

View File

@ -56,11 +56,12 @@
<script>
import {Message} from "element-ui";
import {findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
export default {
name: "PerformanceBasicConfig",
props: {
testPlan: {
test: {
type: Object
},
isReadOnly: {
@ -81,23 +82,36 @@ export default {
};
},
created() {
if (this.testPlan.id) {
this.getFileMetadata(this.testPlan)
if (this.test.id) {
this.getFileMetadata(this.test)
}
},
watch: {
testPlan() {
if (this.testPlan.id) {
this.getFileMetadata(this.testPlan)
test() {
if (this.test.id) {
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: {
getFileMetadata(testPlan) {
getFileMetadata(test) {
this.fileList = [];
this.tableData = [];
this.uploadList = [];
this.result = this.$get(this.getFileMetadataPath + "/" + testPlan.id, response => {
this.result = this.$get(this.getFileMetadataPath + "/" + test.id, response => {
let files = response.data;
if (!files) {

View File

@ -1,91 +1,9 @@
<template>
<div v-loading="result.loading" class="pressure-config-container">
<el-row>
<el-col :span="10">
<el-col>
<el-form :inline="true">
<el-form-item>
<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-form-item :label="$t('load_test.select_resource_pool')">
<el-select v-model="resourcePool" :disabled="isReadOnly" size="mini">
<el-option
v-for="item in resourcePools"
@ -96,11 +14,77 @@
</el-select>
</el-form-item>
</el-form>
<ms-chart class="chart-container" ref="chart1" :options="options" :autoresize="true"></ms-chart>
</el-col>
</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="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" ref="chart1" :options="orgOptions" :autoresize="true"></ms-chart>
<ms-chart class="chart-container" :options="threadGroup.options" :autoresize="true"></ms-chart>
</el-col>
</el-collapse-item>
</el-collapse>
</el-row>
</div>
</template>
@ -108,6 +92,7 @@
<script>
import echarts from "echarts";
import MsChart from "@/business/components/common/chart/MsChart";
import {findTestPlan, findThreadGroup} from "@/business/components/performance/test/model/ThreadGroup";
const TARGET_LEVEL = "TargetLevel";
const RAMP_UP = "RampUp";
@ -115,12 +100,22 @@ const STEPS = "Steps";
const DURATION = "duration";
const RPS_LIMIT = "rpsLimit";
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 {
name: "PerformancePressureConfig",
components: {MsChart},
props: {
testPlan: {
test: {
type: Object
},
testId: {
@ -134,35 +129,38 @@ export default {
data() {
return {
result: {},
threadNumber: 10,
duration: 10,
rampUpTime: 10,
step: 10,
rpsLimit: 10,
threadNumber: 0,
duration: 0,
rampUpTime: 0,
step: 0,
rpsLimit: 0,
rpsLimitEnable: false,
orgOptions: {},
options: {},
resourcePool: null,
resourcePools: [],
activeNames: ["0"],
threadGroups: [],
serializeThreadgroups: false,
}
},
mounted() {
if (this.testId) {
this.getLoadConfig();
this.getJmxContent();
} else {
this.calculateChart();
this.calculateTotalChart();
}
this.resourcePool = this.testPlan.testResourcePoolId;
this.resourcePool = this.test.testResourcePoolId;
this.getResourcePools();
},
watch: {
testPlan(n) {
test(n) {
this.resourcePool = n.testResourcePoolId;
},
testId() {
if (this.testId) {
this.getLoadConfig();
this.getJmxContent();
} else {
this.calculateChart();
this.calculateTotalChart();
}
this.getResourcePools();
},
@ -178,56 +176,191 @@ export default {
})
},
getLoadConfig() {
if (this.testId) {
this.$get('/performance/get-load-config/' + this.testId, (response) => {
if (response.data) {
let data = JSON.parse(response.data);
data.forEach(d => {
switch (d.key) {
for (let i = 0; i < data.length; i++) {
let d = data[i];
if (d instanceof Array) {
d.forEach(item => {
switch (item.key) {
case TARGET_LEVEL:
this.threadNumber = d.value;
this.threadGroups[i].threadNumber = item.value;
break;
case RAMP_UP:
this.rampUpTime = d.value;
this.threadGroups[i].rampUpTime = item.value;
break;
case DURATION:
this.duration = d.value;
this.threadGroups[i].duration = item.value;
break;
case STEPS:
this.step = d.value;
this.threadGroups[i].step = item.value;
break;
case RPS_LIMIT:
this.rpsLimit = d.value;
this.threadGroups[i].rpsLimit = item.value;
break;
case RPS_LIMIT_ENABLE:
this.rpsLimitEnable = d.value;
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.calculateTotalChart();
}
});
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();
},
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.threadGroups.forEach(tg => {
tg.options = {};
});
this.getLoadConfig();
}
});
}
},
calculateChart() {
if (this.duration < this.rampUpTime) {
this.rampUpTime = this.duration;
calculateTotalChart() {
let handler = this;
if (handler.duration < handler.rampUpTime) {
handler.rampUpTime = handler.duration;
}
if (this.rampUpTime < this.step) {
this.step = this.rampUpTime;
if (handler.rampUpTime < handler.step) {
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: {
type: 'category',
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 threadPeriod = Math.floor(this.threadNumber / this.step);
let threadInc1 = Math.floor(this.threadNumber / this.step);
let threadInc2 = Math.ceil(this.threadNumber / this.step);
let inc2count = this.threadNumber - this.step * threadInc1;
for (let i = 0; i <= this.duration; i++) {
let threadPeriod = Math.floor(handler.threadNumber / handler.step);
let threadInc1 = Math.floor(handler.threadNumber / handler.step);
let threadInc2 = Math.ceil(handler.threadNumber / handler.step);
let inc2count = handler.threadNumber - handler.step * threadInc1;
for (let i = 0; i <= handler.duration; i++) {
// x
this.orgOptions.xAxis.data.push(i);
handler.options.xAxis.data.push(i);
if (i > timePeriod) {
timePeriod += timeInc;
if (inc2count > 0) {
@ -298,14 +431,15 @@ export default {
} else {
threadPeriod = threadPeriod + threadInc1;
}
if (threadPeriod > this.threadNumber) {
threadPeriod = this.threadNumber;
if (threadPeriod > handler.threadNumber) {
threadPeriod = handler.threadNumber;
}
this.orgOptions.series[0].data.push(threadPeriod);
handler.options.series[0].data.push(threadPeriod);
} else {
this.orgOptions.series[0].data.push(threadPeriod);
handler.options.series[0].data.push(threadPeriod);
}
}
this.calculateTotalChart();
},
validConfig() {
if (!this.resourcePool) {
@ -315,24 +449,32 @@ export default {
return false;
}
if (!this.threadNumber || !this.duration || !this.rampUpTime || !this.step || !this.rpsLimit) {
for (let i = 0; i < this.threadGroups.length; i++) {
if (!this.threadGroups[i].threadNumber || !this.threadGroups[i].duration
|| !this.threadGroups[i].rampUpTime || !this.threadGroups[i].step || !this.threadGroups[i].rpsLimit) {
this.$warning(this.$t('load_test.pressure_config_params_is_empty'));
this.$emit('changeActive', '1');
return false;
}
}
return true;
},
convertProperty() {
/// todo4jmeter ConcurrencyThreadGroup plugin
return [
{key: TARGET_LEVEL, value: this.threadNumber},
{key: RAMP_UP, value: this.rampUpTime},
{key: STEPS, value: this.step},
{key: DURATION, value: this.duration},
{key: RPS_LIMIT, value: this.rpsLimit},
{key: RPS_LIMIT_ENABLE, value: this.rpsLimitEnable},
];
let result = [];
for (let i = 0; i < this.threadGroups.length; i++) {
result.push([
{key: TARGET_LEVEL, value: this.threadGroups[i].threadNumber},
{key: RAMP_UP, value: this.threadGroups[i].rampUpTime},
{key: STEPS, value: this.threadGroups[i].step},
{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 {
width: 100%;
height: 300px;
}
.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-tabs class="system-setting" v-model="activeName">
<el-tab-pane :label="$t('organization.defect_manage')" name="defect">
<defect-management/>
<bug-management/>
</el-tab-pane>
</el-tabs>
</el-card>
@ -10,12 +10,12 @@
<script>
import DefectManagement from "./IssuesManagement";
import BugManagement from "./BugManagement";
export default {
name: "ServiceIntegration",
components: {
DefectManagement
BugManagement
},
data() {
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>
<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-group>
</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-form-item :label="$t('organization.integration.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-form-item :label="$t('organization.integration.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-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-form-item>
<el-form-item :label="$t('organization.integration.jira_issuetype')" prop="issuetype"
v-if="platform === 'Jira'">
<el-form-item :label="$t('organization.integration.jira_issuetype')" prop="issuetype">
<el-input v-model="form.issuetype" :placeholder="$t('organization.integration.input_jira_issuetype')"/>
</el-form-item>
</el-form>
</div>
<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('form')">{{ $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" @click="cancelIntegration('form')" :disabled="!show">
{{ $t('organization.integration.cancel_integration') }}
</el-button>
</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') }}
1. {{ $t('organization.integration.use_tip_jira') }}
</div>
<div>
2. {{ $t('organization.integration.use_tip_jira') }}
</div>
<div>
3. {{ $t('organization.integration.use_tip_two') }}
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>
@ -63,20 +43,20 @@
</template>
<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 {
name: "IssuesManagement",
name: "JiraSetting",
components: {BugManageBtn},
created() {
this.init();
},
data() {
return {
form: {},
result: {},
platform: '',
orgId: '',
show: true,
showEdit: true,
showSave: false,
showCancel: false,
form: {},
rules: {
account: {
required: true,
@ -101,17 +81,14 @@ export default {
},
}
},
created() {
this.init(this.platform);
},
methods: {
init(platform) {
init() {
const {lastOrganizationId} = getCurrentUser();
let param = {};
param.platform = platform;
param.orgId = getCurrentUser().lastOrganizationId;
this.result = this.$post("service/integration/type", param, response => {
param.platform = JIRA;
param.orgId = lastOrganizationId;
this.$parent.result = this.$post("service/integration/type", param, response => {
let data = response.data;
this.platform = data.platform;
if (data.configuration) {
let config = JSON.parse(data.configuration);
this.$set(this.form, 'account', config.account);
@ -123,54 +100,13 @@ export default {
}
})
},
edit() {
this.show = false;
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 => {
save() {
this.$refs['form'].validate(valid => {
if (valid) {
let formatUrl = this.form.url.trim();
if (!formatUrl.endsWith('/')) {
formatUrl = formatUrl + '/';
}
let param = {};
let auth = {
account: this.form.account,
@ -178,16 +114,16 @@ export default {
url: formatUrl,
issuetype: this.form.issuetype
};
param.organizationId = getCurrentUser().lastOrganizationId;
param.platform = this.platform;
const {lastOrganizationId} = getCurrentUser();
param.organizationId = lastOrganizationId;
param.platform = JIRA;
param.configuration = JSON.stringify(auth);
this.result = this.$post("service/integration/save", param, () => {
this.$parent.result = this.$post("service/integration/save", param, () => {
this.show = true;
this.showEdit = true;
this.showSave = false;
this.showCancel = false;
this.init(this.platform);
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 {
@ -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() {
this.$set(this.form, 'account', '');
this.$set(this.form, 'password', '');
@ -226,24 +141,41 @@ export default {
});
},
testConnection() {
if (this.form.account && this.form.password && this.platform) {
this.result = this.$get("issues/auth/" + this.platform, () => {
if (this.form.account && this.form.password) {
this.$parent.result = this.$get("issues/auth/" + JIRA, () => {
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') + 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>
<style scoped>
.header-title {
padding: 10px 30px;
}
.defect-tip {
background: #EDEDED;
border: solid #E1E1E1 1px;
@ -251,9 +183,4 @@ export default {
padding: 10px;
border-radius: 3px;
}
.platform {
height: 90px;
vertical-align: middle
}
</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>
</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">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)"
@deleteClick="handleDelete(scope.row)">
@ -175,6 +175,7 @@
import StatusTableItem from "@/business/components/track/common/tableItems/planview/StatusTableItem";
import TestCaseDetail from "./TestCaseDetail";
import ReviewStatus from "@/business/components/track/case/components/ReviewStatus";
export default {
name: "TestCaseList",
components: {

View File

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

View File

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

View File

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

View File

@ -19,10 +19,10 @@
<el-tabs class="test-config" v-model="active" type="border-card" :stretch="true">
<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 :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 :label="$t('load_test.advanced_config')" class="advanced-config">
<performance-advanced-config :read-only="true" :test-id="id" ref="advancedConfig"/>
@ -40,6 +40,7 @@
import PerformanceBasicConfig from "../../../../../performance/test/components/PerformanceBasicConfig";
import PerformancePressureConfig from "../../../../../performance/test/components/PerformancePressureConfig";
import PerformanceAdvancedConfig from "../../../../../performance/test/components/PerformanceAdvancedConfig";
export default {
name: "PerformanceTestDetail",
components: {

View File

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

View File

@ -124,6 +124,7 @@
</template>
</el-table-column>
<el-table-column
min-width="100"
:label="$t('commons.operating')">
<template v-slot:default="scope">
<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;
}
.adjust-table th:hover:after {
.adjust-table th:not([class*='el-table-column--selection']):hover:after {
content: '';
position: absolute;
top: 25%;
right: 0;
height: 50%;
width: 3px;
width: 2px;
background-color: #EBEEF5;
}

View File

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

View File

@ -250,15 +250,22 @@ export default {
basic_auth_info: 'Basic Auth account information:',
api_account: 'API account',
api_password: 'API password',
app_name: 'APP name',
app_key: 'APP key',
account: 'Account',
password: 'Password',
jira_url: 'JIRA url',
jira_issuetype: 'JIRA issuetype',
input_api_account: 'please enter account',
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_issuetype: 'Please enter the question type',
use_tip: 'Usage guidelines:',
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_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',
link_the_project_now: 'Link the project now',
cancel_edit: 'Cancel edit',
@ -353,6 +360,7 @@ export default {
test_stop_now_confirm: 'Are you sure you want to stop the current test immediately?',
test_rerun_confirm: 'Are you sure you want to rerun the current test immediately?',
test_stop_success: 'Test stop successfully',
downloadJtl: 'Download JTL',
test_execute_again: 'Test Execute Again',
export: 'Export',
compare: 'Compare',

View File

@ -250,15 +250,22 @@ export default {
basic_auth_info: 'Basic Auth 账号信息:',
api_account: 'API 账号',
api_password: 'API 口令',
app_name: '应用代号',
app_key: '密钥',
account: '账号',
password: '密码',
jira_url: 'JIRA 地址',
jira_issuetype: '问题类型',
input_api_account: '请输入账号',
input_api_password: '请输入口令',
input_app_name: '请输入应用代号',
input_app_key: '请输入密钥',
input_jira_url: '请输入Jira地址https://metersphere.atlassian.net/',
input_jira_issuetype: '请输入问题类型',
use_tip: '使用指引:',
use_tip_tapd: 'Tapd Basic Auth 账号信息在"公司管理-安全与集成-开放平台"中查询',
use_tip_jira: 'Jira software server 认证信息为 账号密码Jira software cloud 认证信息为 账号+令牌(账户设置-安全-创建API令牌)',
use_tip_zentao: '用超级管理员用户登录禅道,进入后台-二次开发-应用,点击【添加应用】新增一个应用',
use_tip_two: '保存 Basic Auth 账号信息后,需要在 Metersphere 项目中手动关联 ID/key',
link_the_project_now: '马上关联项目',
cancel_edit: '取消编辑',
@ -353,6 +360,7 @@ export default {
test_rerun_confirm: '确定要再次执行当前测试吗?',
test_stop_success: '停止成功',
test_execute_again: '再次执行',
downloadJtl: '下载JTL',
export: '导出',
compare: '比较',
generation_error: '报告生成错误, 无法查看, 请检查日志详情!',

View File

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

View File

@ -28,7 +28,7 @@
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" :placeholder="$t('commons.password')" show-password autocomplete="off"
maxlength="20" show-word-limit/>
maxlength="30" show-word-limit/>
</el-form-item>
</div>
<div class="btn">
@ -81,7 +81,7 @@ export default {
],
password: [
{required: true, message: this.$t('commons.input_password'), trigger: 'blur'},
{min: 6, max: 20, message: this.$t('commons.input_limit', [6, 20]), trigger: 'blur'}
{min: 6, max: 30, message: this.$t('commons.input_limit', [6, 30]), trigger: 'blur'}
]
},
msg: '',