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 和云环境,轻松支持高并发、分布式的性能测试; - 性能测试: 兼容 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) > 如需进一步了解 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 ## License & Copyright

View File

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

View File

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

View File

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

View File

@ -643,6 +643,76 @@ public class LoadTestReportExample {
addCriterion("trigger_mode not between", value1, value2, "triggerMode"); addCriterion("trigger_mode not between", value1, value2, "triggerMode");
return (Criteria) this; 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 { public static class Criteria extends GeneratedCriteria {

View File

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

View File

@ -24,6 +24,7 @@ import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -99,10 +100,10 @@ public class MailService {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom(javaMailSender.getUsername()); helper.setFrom(javaMailSender.getUsername());
if (StringUtils.equals(type, NoticeConstants.API)) { 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)) { if (StringUtils.equals(type, NoticeConstants.SCHEDULE)) {
helper.setSubject("MeterSphere平台" + Translator.get("task_notification_")); helper.setSubject("MeterSphere平台" + Translator.get("task_notification"));
} }
String[] users; String[] users;
List<String> emails = new ArrayList<>(); List<String> emails = new ArrayList<>();
@ -113,7 +114,11 @@ public class MailService {
users = emails.toArray(new String[0]); users = emails.toArray(new String[0]);
helper.setText(getContent(Template, context), true); helper.setText(getContent(Template, context), true);
helper.setTo(users); helper.setTo(users);
javaMailSender.send(mimeMessage); 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 io.metersphere.performance.service.ReportService;
import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles; 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 org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -130,4 +133,13 @@ public class PerformanceReportController {
public void deleteReportBatch(@RequestBody DeleteReportRequest reportRequest) { public void deleteReportBatch(@RequestBody DeleteReportRequest reportRequest) {
reportService.deleteReportBatch(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); return performanceTestService.getLoadConfiguration(testId);
} }
@GetMapping("/get-jmx-content/{testId}")
public String getJmxContent(@PathVariable String testId) {
checkOwnerService.checkPerformanceTestOwner(testId);
return performanceTestService.getJmxContent(testId);
}
@PostMapping("/delete") @PostMapping("/delete")
public void delete(@RequestBody DeleteTestPlanRequest request) { public void delete(@RequestBody DeleteTestPlanRequest request) {
checkOwnerService.checkPerformanceTestOwner(request.getId()); checkOwnerService.checkPerformanceTestOwner(request.getId());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -207,6 +207,7 @@ public class PerformanceTestService {
@Transactional(noRollbackFor = MSException.class)// 保存失败的信息 @Transactional(noRollbackFor = MSException.class)// 保存失败的信息
public String run(RunTestPlanRequest request) { public String run(RunTestPlanRequest request) {
LogUtil.info("性能测试run测试");
final LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(request.getId()); final LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(request.getId());
if (request.getUserId() != null) { if (request.getUserId() != null) {
loadTest.setUserId(request.getUserId()); loadTest.setUserId(request.getUserId());
@ -345,6 +346,17 @@ public class PerformanceTestService {
return Optional.ofNullable(loadTestWithBLOBs).orElse(new LoadTestWithBLOBs()).getLoadConfiguration(); return Optional.ofNullable(loadTestWithBLOBs).orElse(new LoadTestWithBLOBs()).getLoadConfiguration();
} }
public String getJmxContent(String testId) {
List<FileMetadata> fileMetadataList = fileService.getFileMetadataByTestId(testId);
for (FileMetadata metadata : fileMetadataList) {
if (FileType.JMX.name().equals(metadata.getType())) {
FileContent fileContent = fileService.getFileContent(metadata.getId());
return new String(fileContent.getFile());
}
}
return null;
}
public List<LoadTestWithBLOBs> selectByTestResourcePoolId(String resourcePoolId) { public List<LoadTestWithBLOBs> selectByTestResourcePoolId(String resourcePoolId) {
LoadTestExample example = new LoadTestExample(); LoadTestExample example = new LoadTestExample();
example.createCriteria().andTestResourcePoolIdEqualTo(resourcePoolId); example.createCriteria().andTestResourcePoolIdEqualTo(resourcePoolId);

View File

@ -13,11 +13,13 @@ import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.controller.request.OrderRequest; import io.metersphere.controller.request.OrderRequest;
import io.metersphere.dto.LogDetailDTO; import io.metersphere.dto.LogDetailDTO;
import io.metersphere.dto.ReportDTO; import io.metersphere.dto.ReportDTO;
import io.metersphere.i18n.Translator;
import io.metersphere.performance.base.*; import io.metersphere.performance.base.*;
import io.metersphere.performance.controller.request.DeleteReportRequest; import io.metersphere.performance.controller.request.DeleteReportRequest;
import io.metersphere.performance.controller.request.ReportRequest; import io.metersphere.performance.controller.request.ReportRequest;
import io.metersphere.performance.engine.Engine; import io.metersphere.performance.engine.Engine;
import io.metersphere.performance.engine.EngineFactory; import io.metersphere.performance.engine.EngineFactory;
import io.metersphere.service.FileService;
import io.metersphere.service.TestResourceService; import io.metersphere.service.TestResourceService;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -47,6 +49,8 @@ public class ReportService {
private TestResourceService testResourceService; private TestResourceService testResourceService;
@Resource @Resource
private LoadTestReportDetailMapper loadTestReportDetailMapper; private LoadTestReportDetailMapper loadTestReportDetailMapper;
@Resource
private FileService fileService;
public List<ReportDTO> getRecentReportList(ReportRequest request) { public List<ReportDTO> getRecentReportList(ReportRequest request) {
List<OrderRequest> orders = new ArrayList<>(); List<OrderRequest> orders = new ArrayList<>();
@ -168,7 +172,10 @@ public class ReportService {
public void checkReportStatus(String reportId) { public void checkReportStatus(String reportId) {
LoadTestReport loadTestReport = loadTestReportMapper.selectByPrimaryKey(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)) { if (StringUtils.equals(PerformanceTestStatus.Error.name(), reportStatus)) {
MSException.throwException("Report generation error!"); MSException.throwException("Report generation error!");
} }
@ -268,4 +275,12 @@ public class ReportService {
String content = getContent(id, ReportKeys.ResponseCodeChart); String content = getContent(id, ReportKeys.ResponseCodeChart);
return JSON.parseArray(content, ChartsData.class); 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) { public void checkApiTestOwner(String testId) {
// 关联为其他时
if (StringUtils.equals("other", testId)) {
return;
}
String workspaceId = SessionUtils.getCurrentWorkspaceId(); String workspaceId = SessionUtils.getCurrentWorkspaceId();
QueryAPITestRequest request = new QueryAPITestRequest(); QueryAPITestRequest request = new QueryAPITestRequest();
request.setWorkspaceId(workspaceId); request.setWorkspaceId(workspaceId);

@ -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 related_case_del_fail_suffix=TestCase, please disassociate first
jmx_content_valid=JMX content is invalid jmx_content_valid=JMX content is invalid
container_delete_fail=The container failed to stop, please try again 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
workspace_name_is_null=Workspace name cannot be null workspace_name_is_null=Workspace name cannot be null
workspace_name_already_exists=The workspace name already exists 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 upload_content_is_null=Imported content is empty
test_plan_notification=Test plan notification test_plan_notification=Test plan notification
task_defect_notification=Task defect notification task_defect_notification=Task defect notification
task_notification=Jenkins Task notification task_notification_jenkins=Jenkins Task notification
task_notification_=Timing task result notification task_notification=Result notification

View File

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

View File

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

View File

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

View File

@ -37,7 +37,8 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"el-table-infinite-scroll": "^1.0.10", "el-table-infinite-scroll": "^1.0.10",
"vue-pdf": "^4.2.0", "vue-pdf": "^4.2.0",
"diffable-html": "^4.0.0" "diffable-html": "^4.0.0",
"xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0", "@vue/cli-plugin-babel": "^4.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,6 +25,9 @@
<el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)"> <el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)">
{{ $t('test_track.plan_view.export_report') }} {{ $t('test_track.plan_view.export_report') }}
</el-button> </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">--> <!--<el-button :disabled="isReadOnly" type="warning" plain size="mini">-->
<!--{{$t('report.compare')}}--> <!--{{$t('report.compare')}}-->
@ -95,6 +98,7 @@ import MsMainContainer from "../../common/components/MsMainContainer";
import {checkoutTestManagerOrTestUser, exportPdf} from "@/common/js/utils"; import {checkoutTestManagerOrTestUser, exportPdf} from "@/common/js/utils";
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
import MsPerformanceReportExport from "./PerformanceReportExport"; import MsPerformanceReportExport from "./PerformanceReportExport";
import {Message} from "element-ui";
export default { export default {
@ -281,6 +285,34 @@ export default {
this.reportExportVisible = false; this.reportExportVisible = false;
this.result.loading = 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() { created() {
this.isReadOnly = false; this.isReadOnly = false;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -250,15 +250,22 @@ export default {
basic_auth_info: 'Basic Auth account information:', basic_auth_info: 'Basic Auth account information:',
api_account: 'API account', api_account: 'API account',
api_password: 'API password', api_password: 'API password',
app_name: 'APP name',
app_key: 'APP key',
account: 'Account',
password: 'Password',
jira_url: 'JIRA url', jira_url: 'JIRA url',
jira_issuetype: 'JIRA issuetype', jira_issuetype: 'JIRA issuetype',
input_api_account: 'please enter account', input_api_account: 'please enter account',
input_api_password: 'Please enter password', input_api_password: 'Please enter password',
input_app_name: 'Please enter the application code',
input_app_key: 'Please enter the key',
input_jira_url: 'Please enter Jira address, for example: https://metersphere.atlassian.net/', input_jira_url: 'Please enter Jira address, for example: https://metersphere.atlassian.net/',
input_jira_issuetype: 'Please enter the question type', input_jira_issuetype: 'Please enter the question type',
use_tip: 'Usage guidelines:', use_tip: 'Usage guidelines:',
use_tip_tapd: 'Basic Auth account information is queried in "Company Management-Security and Integration-Open Platform"', use_tip_tapd: 'Basic Auth account information is queried in "Company Management-Security and Integration-Open Platform"',
use_tip_jira: 'Jira software server authentication information is account password, Jira software cloud authentication information is account + token (account settings-security-create API token)', use_tip_jira: 'Jira software server authentication information is account password, Jira software cloud authentication information is account + token (account settings-security-create API token)',
use_tip_zentao: 'Log in to ZenTao as a super administrator user, enter the background-secondary development-application, click [Add Application] to add an application',
use_tip_two: 'After saving the Basic Auth account information, you need to manually associate the ID/key in the Metersphere project', use_tip_two: 'After saving the Basic Auth account information, you need to manually associate the ID/key in the Metersphere project',
link_the_project_now: 'Link the project now', link_the_project_now: 'Link the project now',
cancel_edit: 'Cancel edit', cancel_edit: 'Cancel edit',
@ -353,6 +360,7 @@ export default {
test_stop_now_confirm: 'Are you sure you want to stop the current test immediately?', 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_rerun_confirm: 'Are you sure you want to rerun the current test immediately?',
test_stop_success: 'Test stop successfully', test_stop_success: 'Test stop successfully',
downloadJtl: 'Download JTL',
test_execute_again: 'Test Execute Again', test_execute_again: 'Test Execute Again',
export: 'Export', export: 'Export',
compare: 'Compare', compare: 'Compare',

View File

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

View File

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

View File

@ -28,7 +28,7 @@
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input v-model="form.password" :placeholder="$t('commons.password')" show-password autocomplete="off" <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> </el-form-item>
</div> </div>
<div class="btn"> <div class="btn">
@ -81,7 +81,7 @@ export default {
], ],
password: [ password: [
{required: true, message: this.$t('commons.input_password'), trigger: 'blur'}, {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: '', msg: '',