Conflicts:
	frontend/src/business/components/api/report/components/ScenarioResults.vue
	frontend/src/business/components/common/head/ShowAll.vue
This commit is contained in:
wenyann 2020-08-11 14:52:02 +08:00
commit a8671ca4f3
57 changed files with 8746 additions and 504 deletions

View File

@ -20,6 +20,7 @@
<jmeter.version>5.2.1</jmeter.version>
<nacos.version>1.1.3</nacos.version>
<dubbo.version>2.7.7</dubbo.version>
<graalvm.version>20.1.0</graalvm.version>
</properties>
<dependencies>
@ -157,6 +158,12 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_functions</artifactId>
<version>${jmeter.version}</version>
</dependency>
<!-- Zookeeper -->
<dependency>
<groupId>org.apache.dubbo</groupId>
@ -233,6 +240,38 @@
<version>1.0.51</version>
</dependency>
<!-- 执行 js 代码依赖 -->
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>${graalvm.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>${graalvm.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>${graalvm.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.graalvm.tools</groupId>
<artifactId>profiler</artifactId>
<version>${graalvm.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.graalvm.tools</groupId>
<artifactId>chromeinspector</artifactId>
<version>${graalvm.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>

View File

@ -49,7 +49,6 @@ public class APITestController {
return apiTestService.getApiTestByProjectId(projectId);
}
@PostMapping(value = "/schedule/update")
public void updateSchedule(@RequestBody Schedule request) {
apiTestService.updateSchedule(request);
@ -61,13 +60,13 @@ public class APITestController {
}
@PostMapping(value = "/create", consumes = {"multipart/form-data"})
public void create(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
apiTestService.create(request, files);
public void create(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "file") MultipartFile file) {
apiTestService.create(request, file);
}
@PostMapping(value = "/update", consumes = {"multipart/form-data"})
public void update(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "files") List<MultipartFile> files) {
apiTestService.update(request, files);
public void update(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "file") MultipartFile file) {
apiTestService.update(request, file);
}
@PostMapping(value = "/copy")
@ -91,6 +90,11 @@ public class APITestController {
return apiTestService.run(request);
}
@PostMapping(value = "/run/debug", consumes = {"multipart/form-data"})
public String runDebug(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "file") MultipartFile file) {
return apiTestService.runDebug(request, file);
}
@PostMapping(value = "/import", consumes = {"multipart/form-data"})
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public ApiTest testCaseImport(@RequestPart(value = "file", required = false) MultipartFile file, @RequestPart("request") ApiTestImportRequest request) {

View File

@ -10,6 +10,7 @@ public class Scenario {
private String name;
private String url;
private String environmentId;
private Boolean enableCookieShare;
private List<KeyValue> variables;
private List<KeyValue> headers;
private List<Request> requests;

View File

@ -2,7 +2,9 @@ package io.metersphere.api.jmeter;
import io.metersphere.api.service.APIReportService;
import io.metersphere.api.service.APITestService;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.lang3.StringUtils;
@ -31,12 +33,16 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
private APIReportService apiReportService;
public String runMode = ApiRunMode.RUN.name();
// 测试ID
private String testId;
private String debugReportId;
@Override
public void setupTest(BackendListenerContext context) throws Exception {
this.testId = context.getParameter(TEST_ID);
setParam(context);
apiTestService = CommonBeanFactory.getBean(APITestService.class);
if (apiTestService == null) {
LogUtil.error("apiTestService is required");
@ -99,8 +105,14 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
testResult.getScenarios().addAll(scenarios.values());
testResult.getScenarios().sort(Comparator.comparing(ScenarioResult::getId));
apiTestService.changeStatus(testId, APITestStatus.Completed);
apiReportService.complete(testResult);
ApiTestReport report = null;
if (StringUtils.equals(this.runMode, ApiRunMode.DEBUG.name())) {
report = apiReportService.get(debugReportId);
} else {
apiTestService.changeStatus(testId, APITestStatus.Completed);
report = apiReportService.getRunningReport(testResult.getTestId());
}
apiReportService.complete(testResult, report);
queue.clear();
super.teardownTest(context);
@ -153,6 +165,15 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
}
}
private void setParam(BackendListenerContext context) {
this.testId = context.getParameter(TEST_ID);
this.runMode = context.getParameter("runMode");
this.debugReportId = context.getParameter("debugReportId");
if (StringUtils.isBlank(this.runMode)) {
this.runMode = ApiRunMode.RUN.name();
}
}
private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) {
ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult();
responseAssertionResult.setMessage(assertionResult.getFailureMessage());

View File

@ -1,9 +1,11 @@
package io.metersphere.api.jmeter;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.config.JmeterProperties;
import io.metersphere.i18n.Translator;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.util.JMeterUtils;
@ -21,7 +23,7 @@ public class JMeterService {
@Resource
private JmeterProperties jmeterProperties;
public void run(String testId, InputStream is) {
public void run(String testId, String debugReportId, InputStream is) {
String JMETER_HOME = jmeterProperties.getHome();
String JMETER_PROPERTIES = JMETER_HOME + "/bin/jmeter.properties";
JMeterUtils.loadJMeterProperties(JMETER_PROPERTIES);
@ -29,7 +31,7 @@ public class JMeterService {
try {
Object scriptWrapper = SaveService.loadElement(is);
HashTree testPlan = getHashTree(scriptWrapper);
addBackendListener(testId, testPlan);
addBackendListener(testId, debugReportId, testPlan);
LocalRunner runner = new LocalRunner(testPlan);
runner.run();
@ -45,11 +47,15 @@ public class JMeterService {
return (HashTree) field.get(scriptWrapper);
}
private void addBackendListener(String testId, HashTree testPlan) {
private void addBackendListener(String testId, String debugReportId, HashTree testPlan) {
BackendListener backendListener = new BackendListener();
backendListener.setName(testId);
Arguments arguments = new Arguments();
arguments.addArgument(APIBackendListenerClient.TEST_ID, testId);
if (StringUtils.isNotBlank(debugReportId)) {
arguments.addArgument("runMode", ApiRunMode.DEBUG.name());
arguments.addArgument("debugReportId", debugReportId);
}
backendListener.setArguments(arguments);
backendListener.setClassname(APIBackendListenerClient.class.getCanonicalName());
testPlan.add(testPlan.getArray()[0], backendListener);

View File

@ -10,10 +10,12 @@ import io.metersphere.base.mapper.ApiTestReportDetailMapper;
import io.metersphere.base.mapper.ApiTestReportMapper;
import io.metersphere.base.mapper.ext.ExtApiTestReportMapper;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.ReportTriggerMode;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.dto.DashboardTestDTO;
import io.metersphere.i18n.Translator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -73,8 +75,7 @@ public class APIReportService {
apiTestReportDetailMapper.deleteByExample(detailExample);
}
public void complete(TestResult result) {
ApiTestReport report = getRunningReport(result.getTestId());
public void complete(TestResult result, ApiTestReport report) {
if (report == null) {
MSException.throwException(Translator.get("api_report_is_null"));
}
@ -87,10 +88,12 @@ public class APIReportService {
// report
report.setUpdateTime(System.currentTimeMillis());
if (result.getError() > 0) {
report.setStatus(APITestStatus.Error.name());
} else {
report.setStatus(APITestStatus.Success.name());
if (!StringUtils.equals(report.getStatus(), APITestStatus.Debug.name())) {
if (result.getError() > 0) {
report.setStatus(APITestStatus.Error.name());
} else {
report.setStatus(APITestStatus.Success.name());
}
}
apiTestReportMapper.updateByPrimaryKeySelective(report);
@ -101,7 +104,18 @@ public class APIReportService {
if (running != null) {
return running.getId();
}
ApiTestReport report = buildReport(test, triggerMode, APITestStatus.Running.name());
apiTestReportMapper.insert(report);
return report.getId();
}
public String createDebugReport(ApiTest test) {
ApiTestReport report = buildReport(test, ReportTriggerMode.MANUAL.name(), APITestStatus.Debug.name());
apiTestReportMapper.insert(report);
return report.getId();
}
public ApiTestReport buildReport(ApiTest test, String triggerMode, String status) {
ApiTestReport report = new ApiTestReport();
report.setId(UUID.randomUUID().toString());
report.setTestId(test.getId());
@ -110,11 +124,9 @@ public class APIReportService {
report.setDescription(test.getDescription());
report.setCreateTime(System.currentTimeMillis());
report.setUpdateTime(System.currentTimeMillis());
report.setStatus(APITestStatus.Running.name());
report.setStatus(status);
report.setUserId(test.getUserId());
apiTestReportMapper.insert(report);
return report.getId();
return report;
}
public ApiTestReport getRunningReport(String testId) {

View File

@ -12,10 +12,7 @@ import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.ApiTestFileMapper;
import io.metersphere.base.mapper.ApiTestMapper;
import io.metersphere.base.mapper.ext.ExtApiTestMapper;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.FileType;
import io.metersphere.commons.constants.ScheduleGroup;
import io.metersphere.commons.constants.ScheduleType;
import io.metersphere.commons.constants.*;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.LogUtil;
@ -37,6 +34,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
@ -72,21 +70,21 @@ public class APITestService {
return extApiTestMapper.list(request);
}
public void create(SaveAPITestRequest request, List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
public void create(SaveAPITestRequest request, MultipartFile file) {
if (file == null) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
ApiTest test = createTest(request);
saveFile(test.getId(), files);
saveFile(test.getId(), file);
}
public void update(SaveAPITestRequest request, List<MultipartFile> files) {
if (files == null || files.isEmpty()) {
public void update(SaveAPITestRequest request, MultipartFile file) {
if (file == null) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
deleteFileByTestId(request.getId());
ApiTest test = updateTest(request);
saveFile(test.getId(), files);
saveFile(test.getId(), file);
}
public void copy(SaveAPITestRequest request) {
@ -156,7 +154,7 @@ public class APITestService {
String reportId = apiReportService.create(apiTest, request.getTriggerMode());
changeStatus(request.getId(), APITestStatus.Running);
jMeterService.run(request.getId(), is);
jMeterService.run(request.getId(), null, is);
return reportId;
}
@ -203,14 +201,12 @@ public class APITestService {
return test;
}
private void saveFile(String testId, List<MultipartFile> files) {
files.forEach(file -> {
final FileMetadata fileMetadata = fileService.saveFile(file);
ApiTestFile apiTestFile = new ApiTestFile();
apiTestFile.setTestId(testId);
apiTestFile.setFileId(fileMetadata.getId());
apiTestFileMapper.insert(apiTestFile);
});
private void saveFile(String testId, MultipartFile file) {
final FileMetadata fileMetadata = fileService.saveFile(file);
ApiTestFile apiTestFile = new ApiTestFile();
apiTestFile.setTestId(testId);
apiTestFile.setFileId(fileMetadata.getId());
apiTestFileMapper.insert(apiTestFile);
}
private void deleteFileByTestId(String testId) {
@ -299,8 +295,8 @@ public class APITestService {
if (info.length > 1) {
provider.setVersion(info[1]);
}
provider.setService(info[0]);
provider.setServiceInterface(p);
provider.setService(p);
provider.setServiceInterface(info[0]);
Map<String, URL> services = providerService.findByService(p);
if (services != null && !services.isEmpty()) {
String[] methods = services.values().stream().findFirst().get().getParameter(CommonConstants.METHODS_KEY).split(",");
@ -314,6 +310,7 @@ public class APITestService {
}
public List<ScheduleDao> listSchedule(QueryScheduleRequest request) {
request.setEnable(true);
List<ScheduleDao> schedules = scheduleService.list(request);
List<String> resourceIds = schedules.stream()
.map(Schedule::getResourceId)
@ -327,4 +324,26 @@ public class APITestService {
}
return schedules;
}
public String runDebug(SaveAPITestRequest request, MultipartFile file) {
if (file == null) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
updateTest(request);
APITestResult apiTest = get(request.getId());
if (SessionUtils.getUser() == null) {
apiTest.setUserId(request.getUserId());
}
String reportId = apiReportService.createDebugReport(apiTest);
InputStream is = null;
try {
is = new ByteArrayInputStream(file.getBytes());
} catch (IOException e) {
LogUtil.error(e);
}
jMeterService.run(request.getId(), reportId, is);
return reportId;
}
}

View File

@ -1,8 +1,9 @@
package io.metersphere.base.domain;
import java.io.Serializable;
import lombok.Data;
import java.io.Serializable;
@Data
public class LoadTestReport implements Serializable {
private String id;
@ -21,7 +22,5 @@ public class LoadTestReport implements Serializable {
private String triggerMode;
private String description;
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,18 @@
package io.metersphere.base.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class LoadTestReportWithBLOBs extends LoadTestReport implements Serializable {
private String description;
private String loadConfiguration;
private static final long serialVersionUID = 1L;
}

View File

@ -2,9 +2,11 @@ package io.metersphere.base.mapper;
import io.metersphere.base.domain.LoadTestReport;
import io.metersphere.base.domain.LoadTestReportExample;
import java.util.List;
import io.metersphere.base.domain.LoadTestReportWithBLOBs;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface LoadTestReportMapper {
long countByExample(LoadTestReportExample example);
@ -12,25 +14,25 @@ public interface LoadTestReportMapper {
int deleteByPrimaryKey(String id);
int insert(LoadTestReport record);
int insert(LoadTestReportWithBLOBs record);
int insertSelective(LoadTestReport record);
int insertSelective(LoadTestReportWithBLOBs record);
List<LoadTestReport> selectByExampleWithBLOBs(LoadTestReportExample example);
List<LoadTestReportWithBLOBs> selectByExampleWithBLOBs(LoadTestReportExample example);
List<LoadTestReport> selectByExample(LoadTestReportExample example);
LoadTestReport selectByPrimaryKey(String id);
LoadTestReportWithBLOBs selectByPrimaryKey(String id);
int updateByExampleSelective(@Param("record") LoadTestReport record, @Param("example") LoadTestReportExample example);
int updateByExampleSelective(@Param("record") LoadTestReportWithBLOBs record, @Param("example") LoadTestReportExample example);
int updateByExampleWithBLOBs(@Param("record") LoadTestReport record, @Param("example") LoadTestReportExample example);
int updateByExampleWithBLOBs(@Param("record") LoadTestReportWithBLOBs record, @Param("example") LoadTestReportExample example);
int updateByExample(@Param("record") LoadTestReport record, @Param("example") LoadTestReportExample example);
int updateByPrimaryKeySelective(LoadTestReport record);
int updateByPrimaryKeySelective(LoadTestReportWithBLOBs record);
int updateByPrimaryKeyWithBLOBs(LoadTestReport record);
int updateByPrimaryKeyWithBLOBs(LoadTestReportWithBLOBs record);
int updateByPrimaryKey(LoadTestReport record);
}

View File

@ -11,8 +11,9 @@
<result column="user_id" jdbcType="VARCHAR" property="userId" />
<result column="trigger_mode" jdbcType="VARCHAR" property="triggerMode" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.LoadTestReport">
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.LoadTestReportWithBLOBs">
<result column="description" jdbcType="LONGVARCHAR" property="description" />
<result column="load_configuration" jdbcType="LONGVARCHAR" property="loadConfiguration" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
@ -76,7 +77,7 @@
id, test_id, `name`, create_time, update_time, `status`, user_id, trigger_mode
</sql>
<sql id="Blob_Column_List">
description
description, load_configuration
</sql>
<select id="selectByExampleWithBLOBs" parameterType="io.metersphere.base.domain.LoadTestReportExample" resultMap="ResultMapWithBLOBs">
select
@ -126,17 +127,17 @@
<include refid="Example_Where_Clause" />
</if>
</delete>
<insert id="insert" parameterType="io.metersphere.base.domain.LoadTestReport">
insert into load_test_report (id, test_id, `name`,
<insert id="insert" parameterType="io.metersphere.base.domain.LoadTestReportWithBLOBs">
INSERT INTO load_test_report (id, test_id, `name`,
create_time, update_time, `status`,
user_id, trigger_mode, description
)
values (#{id,jdbcType=VARCHAR}, #{testId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR},
user_id, trigger_mode, description,
load_configuration)
VALUES (#{id,jdbcType=VARCHAR}, #{testId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR},
#{createTime,jdbcType=BIGINT}, #{updateTime,jdbcType=BIGINT}, #{status,jdbcType=VARCHAR},
#{userId,jdbcType=VARCHAR}, #{triggerMode,jdbcType=VARCHAR}, #{description,jdbcType=LONGVARCHAR}
)
#{userId,jdbcType=VARCHAR}, #{triggerMode,jdbcType=VARCHAR}, #{description,jdbcType=LONGVARCHAR},
#{loadConfiguration,jdbcType=LONGVARCHAR})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.LoadTestReport">
<insert id="insertSelective" parameterType="io.metersphere.base.domain.LoadTestReportWithBLOBs">
insert into load_test_report
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
@ -166,6 +167,9 @@
<if test="description != null">
description,
</if>
<if test="loadConfiguration != null">
load_configuration,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
@ -195,6 +199,9 @@
<if test="description != null">
#{description,jdbcType=LONGVARCHAR},
</if>
<if test="loadConfiguration != null">
#{loadConfiguration,jdbcType=LONGVARCHAR},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.base.domain.LoadTestReportExample" resultType="java.lang.Long">
@ -233,6 +240,9 @@
<if test="record.description != null">
description = #{record.description,jdbcType=LONGVARCHAR},
</if>
<if test="record.loadConfiguration != null">
load_configuration = #{record.loadConfiguration,jdbcType=LONGVARCHAR},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
@ -248,7 +258,8 @@
`status` = #{record.status,jdbcType=VARCHAR},
user_id = #{record.userId,jdbcType=VARCHAR},
trigger_mode = #{record.triggerMode,jdbcType=VARCHAR},
description = #{record.description,jdbcType=LONGVARCHAR}
description = #{record.description,jdbcType=LONGVARCHAR},
load_configuration = #{record.loadConfiguration,jdbcType=LONGVARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
@ -267,7 +278,7 @@
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByPrimaryKeySelective" parameterType="io.metersphere.base.domain.LoadTestReport">
<update id="updateByPrimaryKeySelective" parameterType="io.metersphere.base.domain.LoadTestReportWithBLOBs">
update load_test_report
<set>
<if test="testId != null">
@ -294,10 +305,13 @@
<if test="description != null">
description = #{description,jdbcType=LONGVARCHAR},
</if>
<if test="loadConfiguration != null">
load_configuration = #{loadConfiguration,jdbcType=LONGVARCHAR},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.base.domain.LoadTestReport">
<update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.base.domain.LoadTestReportWithBLOBs">
update load_test_report
set test_id = #{testId,jdbcType=VARCHAR},
`name` = #{name,jdbcType=VARCHAR},
@ -306,7 +320,8 @@
`status` = #{status,jdbcType=VARCHAR},
user_id = #{userId,jdbcType=VARCHAR},
trigger_mode = #{triggerMode,jdbcType=VARCHAR},
description = #{description,jdbcType=LONGVARCHAR}
description = #{description,jdbcType=LONGVARCHAR},
load_configuration = #{loadConfiguration,jdbcType=LONGVARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.base.domain.LoadTestReport">

View File

@ -102,6 +102,7 @@
</if>
</foreach>
</if>
AND r.status != 'Debug'
</where>
<if test="request.orders != null and request.orders.size() > 0">
order by
@ -131,6 +132,7 @@
LEFT JOIN user ON user.id = r.user_id
<where>
r.id = #{id}
AND r.status != 'Debug'
</where>
ORDER BY r.update_time DESC
</select>

View File

@ -1,6 +1,5 @@
package io.metersphere.base.mapper.ext;
import io.metersphere.base.domain.LoadTestReport;
import io.metersphere.dto.DashboardTestDTO;
import io.metersphere.dto.ReportDTO;
import io.metersphere.performance.controller.request.ReportRequest;
@ -14,8 +13,6 @@ public interface ExtLoadTestReportMapper {
ReportDTO getReportTestAndProInfo(@Param("id") String id);
LoadTestReport selectByPrimaryKey(String id);
List<DashboardTestDTO> selectDashboardTests(@Param("workspaceId") String workspaceId, @Param("startTimestamp") long startTimestamp);
List<String> selectResourceId(@Param("reportId") String reportId);

View File

@ -125,13 +125,6 @@
where ltr.id = #{id}
</select>
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM load_test_report
WHERE id = #{id,jdbcType=VARCHAR}
</select>
<select id="selectDashboardTests" resultType="io.metersphere.dto.DashboardTestDTO">
SELECT create_time AS date, count(load_test_report.id) AS count,
date_format(from_unixtime(create_time / 1000), '%Y-%m-%d') AS x

View File

@ -10,6 +10,9 @@
<if test="request.workspaceId != null">
and schedule.workspace_id = #{request.workspaceId}
</if>
<if test="request.enable != null">
and schedule.enable = #{request.enable}
</if>
<if test="request.filters != null and request.filters.size() > 0">
<foreach collection="request.filters.entrySet()" index="key" item="values">
<if test="values != null and values.size() > 0">

View File

@ -154,7 +154,7 @@
</include>
</if>
<if test="request.name != null">
and test_case.name like CONCAT('%', #{request.name},'%')
and (test_case.name like CONCAT('%', #{request.name},'%') or test_case.num like CONCAT('%', #{request.name},'%'))
</if>
<if test="request.nodeIds != null and request.nodeIds.size() > 0">
and test_case.node_id in

View File

@ -126,7 +126,7 @@
</include>
</if>
<if test="request.name != null">
and test_case.name like CONCAT('%', #{request.name},'%')
and (test_case.name like CONCAT('%', #{request.name},'%') or test_case.num like CONCAT('%', #{request.name},'%'))
</if>
<if test="request.id != null">
and test_case.id = #{request.id}
@ -185,7 +185,14 @@
<if test="request.orders != null and request.orders.size() > 0">
order by
<foreach collection="request.orders" separator="," item="order">
test_plan_test_case.${order.name} ${order.type}
<choose>
<when test="order.name == 'num'">
test_case.num ${order.type}
</when>
<otherwise>
test_plan_test_case.${order.name} ${order.type}
</otherwise>
</choose>
</foreach>
</if>
</select>

View File

@ -1,5 +1,5 @@
package io.metersphere.commons.constants;
public enum APITestStatus {
Saved, Starting, Running, Reporting, Completed, Error, Success
Saved, Starting, Running, Reporting, Completed, Debug, Error, Success
}

View File

@ -0,0 +1,5 @@
package io.metersphere.commons.constants;
public enum ApiRunMode {
RUN, DEBUG
}

View File

@ -2,8 +2,8 @@ package io.metersphere.performance.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.LoadTestReport;
import io.metersphere.base.domain.LoadTestReportLog;
import io.metersphere.base.domain.LoadTestReportWithBLOBs;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
@ -94,7 +94,7 @@ public class PerformanceReportController {
}
@GetMapping("/{reportId}")
public LoadTestReport getLoadTestReport(@PathVariable String reportId) {
public LoadTestReportWithBLOBs getLoadTestReport(@PathVariable String reportId) {
return reportService.getLoadTestReport(reportId);
}

View File

@ -226,8 +226,6 @@ public class PerformanceTestService {
startEngine(loadTest, engine, request.getTriggerMode());
// todo通过调用stop方法能够停止正在运行的engine但是如果部署了多个backend实例页面发送的停止请求如何定位到具体的engine
return engine.getReportId();
}
@ -257,7 +255,7 @@ public class PerformanceTestService {
}
private void startEngine(LoadTestWithBLOBs loadTest, Engine engine, String triggerMode) {
LoadTestReport testReport = new LoadTestReport();
LoadTestReportWithBLOBs testReport = new LoadTestReportWithBLOBs();
testReport.setId(engine.getReportId());
testReport.setCreateTime(engine.getStartTime());
testReport.setUpdateTime(engine.getStartTime());
@ -277,6 +275,7 @@ public class PerformanceTestService {
loadTest.setStatus(PerformanceTestStatus.Starting.name());
loadTestMapper.updateByPrimaryKeySelective(loadTest);
// 启动正常插入 report
testReport.setLoadConfiguration(loadTest.getLoadConfiguration());
testReport.setStatus(PerformanceTestStatus.Starting.name());
loadTestReportMapper.insertSelective(testReport);
@ -420,6 +419,7 @@ public class PerformanceTestService {
}
public List<ScheduleDao> listSchedule(QueryScheduleRequest request) {
request.setEnable(true);
List<ScheduleDao> schedules = scheduleService.list(request);
List<String> resourceIds = schedules.stream()
.map(Schedule::getResourceId)

View File

@ -169,8 +169,8 @@ public class ReportService {
}
}
public LoadTestReport getLoadTestReport(String id) {
return extLoadTestReportMapper.selectByPrimaryKey(id);
public LoadTestReportWithBLOBs getLoadTestReport(String id) {
return loadTestReportMapper.selectByPrimaryKey(id);
}
public List<LogDetailDTO> getReportLogResource(String reportId) {
@ -241,7 +241,7 @@ public class ReportService {
}
public void updateStatus(String reportId, String status) {
LoadTestReport report = new LoadTestReport();
LoadTestReportWithBLOBs report = new LoadTestReportWithBLOBs();
report.setId(reportId);
report.setStatus(status);
loadTestReportMapper.updateByPrimaryKeySelective(report);

View File

@ -0,0 +1,2 @@
ALTER TABLE load_test_report
ADD load_configuration LONGTEXT NULL;

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,11 @@
"vue-router": "^3.1.3",
"vuedraggable": "^2.23.2",
"vuex": "^3.1.2",
"vue-calendar-heatmap": "^0.8.4"
"vue-calendar-heatmap": "^0.8.4",
"mockjs": "^1.1.0",
"md5": "^2.3.0",
"sha.js": "^2.4.11",
"js-base64": "^3.4.4"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",

View File

@ -11,7 +11,7 @@
<template v-slot:title>{{$t('commons.project')}}</template>
<ms-recent-list :options="projectRecent"/>
<el-divider class="menu-divider"/>
<ms-show-all :index="'/api/project'"/>
<ms-show-all :index="'/api/project/all'"/>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/api/project/create'"
:title="$t('project.create')"/>
</el-submenu>
@ -20,7 +20,7 @@
<template v-slot:title>{{$t('commons.test')}}</template>
<ms-recent-list :options="testRecent"/>
<el-divider class="menu-divider"/>
<ms-show-all :index="'/api/test/list'"/>
<ms-show-all :index="'/api/test/list/all'"/>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/api/test/create'"
:title="$t('load_test.create')"/>
</el-submenu>
@ -29,7 +29,7 @@
<template v-slot:title>{{$t('commons.report')}}</template>
<ms-recent-list :options="reportRecent"/>
<el-divider class="menu-divider"/>
<ms-show-all :index="'/api/report/list'"/>
<ms-show-all :index="'/api/report/list/all'"/>
</el-submenu>
</el-menu>
</el-col>
@ -60,7 +60,7 @@
title: this.$t('project.recent'),
url: "/project/recent/5",
index: function (item) {
return '/api/' + item.id;
return '/api/test/list/' + item.id;
},
router: function (item) {
return {name: 'ApiTestList', params: {projectId: item.id, projectName: item.name}}

View File

@ -14,7 +14,7 @@
<ms-test-heatmap :values="values"/>
</el-col>
<el-col :span="12">
<ms-api-test-schedule-list :group="'API_TEST'"/>
<ms-schedule-list :group="'API_TEST'"/>
</el-col>
</el-row>
</ms-main-container>
@ -28,13 +28,13 @@
import MsApiTestRecentList from "./ApiTestRecentList";
import MsApiReportRecentList from "./ApiReportRecentList";
import MsTestHeatmap from "../../common/components/MsTestHeatmap";
import MsApiTestScheduleList from "./ApiTestScheduleList";
import MsScheduleList from "./ScheduleList";
export default {
name: "ApiTestHome",
components: {
MsApiTestScheduleList,
MsScheduleList,
MsTestHeatmap, MsApiReportRecentList, MsApiTestRecentList, MsMainContainer, MsContainer
},

View File

@ -1,7 +1,7 @@
<template>
<el-card class="table-card" v-loading="result.loading">
<template v-slot:header>
<span class="title">{{$t('commons.trigger_mode.schedule')}}</span>
<span class="title">{{$t('schedule.running_task')}}</span>
</template>
<el-table height="289" border :data="tableData" class="adjust-table table-content" @row-click="link">
<el-table-column prop="resourceName" :label="$t('schedule.test_name')" width="150" show-overflow-tooltip/>
@ -32,7 +32,7 @@
import {SCHEDULE_TYPE} from "../../../../common/js/constants";
export default {
name: "MsApiTestScheduleList",
name: "MsScheduleList",
components: {CrontabResult},
data() {
return {

View File

@ -1,6 +1,5 @@
<template>
<el-card>
<el-card class="scenario-results">
<div class="scenario-header">
<el-row :gutter="10">
<el-col :span="16">

View File

@ -33,7 +33,7 @@
<el-dropdown trigger="click" @command="handleCommand">
<el-button class="el-dropdown-link more" icon="el-icon-more" plain/>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="report" :disabled="test.status !== 'Completed'">
<el-dropdown-item command="report">
{{$t('api_report.title')}}
</el-dropdown-item>
<el-dropdown-item command="performance" :disabled="create || isReadOnly">
@ -55,7 +55,7 @@
<ms-schedule-config :schedule="test.schedule" :is-read-only="isReadOnly" :save="saveCronExpression" @scheduleChange="saveSchedule" :check-open="checkScheduleEdit"/>
</el-row>
</el-header>
<ms-api-scenario-config :is-read-only="isReadOnly" :scenarios="test.scenarioDefinition" :project-id="test.projectId" ref="config"/>
<ms-api-scenario-config :debug-report-id="debugReportId" @runDebug="runDebug" :is-read-only="isReadOnly" :scenarios="test.scenarioDefinition" :project-id="test.projectId" ref="config"/>
</el-container>
</el-card>
</div>
@ -86,7 +86,8 @@
projects: [],
change: false,
test: new Test(),
isReadOnly: false
isReadOnly: false,
debugReportId: ''
}
},
@ -149,17 +150,22 @@
}
this.change = false;
let url = this.create ? "/api/create" : "/api/update";
this.result = this.$request(this.getOptions(url), () => {
this.create = false;
let jmx = this.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.result = this.$fileUpload(url, file, this.test,response => {
if (callback) callback();
this.create = false;
});
},
saveTest() {
this.save(() => {
this.$success(this.$t('commons.save_success'));
this.$router.push({
path: '/api/test/edit?id=' + this.test.id
})
if (this.create) {
this.$router.push({
path: '/api/test/edit?id=' + this.test.id
})
}
})
},
runTest() {
@ -181,26 +187,6 @@
cancel() {
this.$router.push('/api/test/list/all');
},
getOptions(url) {
let formData = new FormData();
let requestJson = JSON.stringify(this.test);
formData.append('request', new Blob([requestJson], {
type: "application/json"
}));
let jmx = this.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
formData.append("files", new File([blob], jmx.name));
return {
method: 'POST',
url: url,
data: formData,
headers: {
'Content-Type': undefined
}
};
},
handleCommand(command) {
switch (command) {
case "report":
@ -249,6 +235,30 @@
return false;
}
return true;
},
runDebug(scenario) {
if (this.create) {
this.$warning(this.$t('api_test.environment.please_save_test'));
return;
}
let url = "/api/run/debug";
let runningTest = new Test();
Object.assign(runningTest, this.test);
runningTest.scenarioDefinition = [];
runningTest.scenarioDefinition.push(scenario);
let validator = runningTest.isValid();
if (!validator.isValid) {
this.$warning(this.$t(validator.info));
return;
}
let jmx = runningTest.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(url, file, this.test,response => {
this.debugReportId = response.data;
});
}
},

View File

@ -1,7 +1,7 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{description}}
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in items" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
@ -15,8 +15,18 @@
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="item.value" size="small" @change="change"
:placeholder="valueText" show-word-limit/>
<el-autocomplete
:disabled="isReadOnly"
size="small"
class="input-with-autocomplete"
v-model="item.value"
:fetch-suggestions="funcSearch"
:placeholder="valueText"
value-key="name"
highlight-first-item
@select="change">
<i slot="suffix" class="el-input__icon el-icon-edit" style="cursor: pointer;" @click="advanced(item)"></i>
</el-autocomplete>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
@ -24,96 +34,200 @@
</el-col>
</el-row>
</div>
<el-dialog :title="$t('api_test.request.parameters_advance')"
:visible.sync="itemValueVisible"
class="advanced-item-value"
width="50%">
<el-form>
<el-form-item>
<el-input :autosize="{ minRows: 2, maxRows: 4}" type="textarea" :placeholder="valueText"
v-model="itemValue"/>
</el-form-item>
</el-form>
<div>
<el-row type="flex" align="middle">
<el-col :span="6">
<el-button size="small" type="primary" plain @click="saveAdvanced()">
{{ $t('commons.save') }}
</el-button>
<el-button size="small" type="success" plain @click="showPreview(itemValue)">
{{ $t('api_test.request.parameters_preview') }}
</el-button>
</el-col>
<el-col>
<div> {{ itemValuePreview }}</div>
</el-col>
</el-row>
</div>
<div class="format-tip">
<div>
<p>{{ $t('api_test.request.parameters_filter') }}
<el-tag size="mini" v-for="func in funcs" :key="func" @click="appendFunc(func)"
style="margin-left: 2px;cursor: pointer;">
<span>{{ func }}</span>
</el-tag>
</p>
</div>
<div>
<span>{{ $t('api_test.request.parameters_filter_desc') }}
<el-link href="http://mockjs.com/examples.html" target="_blank">http://mockjs.com/examples.html</el-link>
</span>
<p>{{ $t('api_test.request.parameters_filter_example') }}@string(10) | md5 | substr: 1, 3</p>
<p>{{ $t('api_test.request.parameters_filter_example') }}@integer(1, 5) | concat:_metersphere</p>
<p><strong>{{ $t('api_test.request.parameters_filter_tips') }}</strong></p>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import {KeyValue} from "../model/ScenarioModel";
import {KeyValue} from "../model/ScenarioModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
import {calculate} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MsApiKeyValue",
export default {
name: "MsApiKeyValue",
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
items: Array,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
items: Array,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
data() {
return {
itemValueVisible: false,
itemValue: null,
funcs: ["md5", "sha1", "sha224", "sha256", "sha384", "sha512", "base64",
"unbase64", "substr", "concat", "lconcat", "lower", "upper", "length", "number"],
itemValuePreview: null
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
this.items.splice(index, 1);
this.$emit('change', this.items);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.items.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
methods: {
remove: function (index) {
this.items.splice(index, 1);
this.$emit('change', this.items);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.items.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
});
if (isNeedCreate) {
this.items.push(new KeyValue());
//
isNeedCreate = false;
}
this.$emit('change', this.items);
// TODO key
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
},
created() {
if (this.items.length === 0) {
});
if (isNeedCreate) {
this.items.push(new KeyValue());
}
this.$emit('change', this.items);
// TODO key
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
funcSearch(queryString, cb) {
let funcs = MOCKJS_FUNC.concat(JMETER_FUNC);
let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs;
// callback
cb(results);
},
funcFilter(queryString) {
return (func) => {
return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1);
};
},
showPreview(itemValue) {
this.itemValuePreview = calculate(itemValue);
},
appendFunc(func) {
if (this.itemValue) {
this.itemValue += " | " + func;
} else {
this.$warning(this.$t("api_test.request.parameters_preview_warning"));
}
},
advanced(item) {
this.currentItem = item;
this.itemValueVisible = true;
this.itemValue = item.value;
this.itemValuePreview = null;
},
saveAdvanced() {
this.currentItem.value = this.itemValue;
this.itemValueVisible = false;
}
},
created() {
if (this.items.length === 0) {
this.items.push(new KeyValue());
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.kv-delete {
width: 60px;
}
.el-autocomplete {
width: 100%;
}
.el-autocomplete {
width: 100%;
}
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.format-tip {
background: #EDEDED;
}
.format-tip {
border: solid #E1E1E1 1px;
margin: 10px 0;
padding: 10px;
border-radius: 3px;
}
</style>

View File

@ -36,7 +36,7 @@
<el-main class="scenario-main">
<div class="scenario-form">
<ms-api-scenario-form :is-read-only="isReadOnly" :scenario="selected" :project-id="projectId" v-if="isScenario"/>
<ms-api-request-form :is-read-only="isReadOnly" :request="selected" v-if="isRequest"/>
<ms-api-request-form :debug-report-id="debugReportId" @runDebug="runDebug" :is-read-only="isReadOnly" :request="selected" v-if="isRequest"/>
</div>
</el-main>
</el-container>
@ -70,13 +70,15 @@
isReadOnly: {
type: Boolean,
default: false
}
},
debugReportId: String
},
data() {
return {
activeName: 0,
selected: [Scenario, Request]
selected: [Scenario, Request],
currentScenario: {}
}
},
@ -118,9 +120,14 @@
break;
}
},
select: function (obj) {
select: function (obj, scenario) {
this.selected = null;
this.$nextTick(function () {
if (obj instanceof Scenario) {
this.currentScenario = obj;
} else {
this.currentScenario = scenario;
}
this.selected = obj;
});
},
@ -145,6 +152,13 @@
});
});
}
},
runDebug(request) {
let scenario = new Scenario();
Object.assign(scenario, this.currentScenario);
scenario.requests = [];
scenario.requests.push(request);
this.$emit('runDebug', scenario);
}
},

View File

@ -21,6 +21,9 @@
</div>
</template>
</el-select>
<el-form-item class="cookie-item">
<el-checkbox v-model="scenario.enableCookieShare">{{'共享 Cookie'}}</el-checkbox>
</el-form-item>
</el-form-item>
<el-tabs v-model="activeName">
@ -168,4 +171,8 @@
font-weight: 600;
}
.cookie-item {
margin-top: 15px;
}
</style>

View File

@ -11,6 +11,8 @@
</el-select>
</el-form-item>
<el-button class="debug-button" size="small" type="primary" @click="runDebug">{{$t('load_test.save_and_run')}}</el-button>
<el-tabs v-model="activeName">
<el-tab-pane label="Interface" name="interface">
<ms-dubbo-interface :request="request" :is-read-only="isReadOnly"/>
@ -94,6 +96,9 @@
this.request.useEnvironment = false;
}
this.$refs["request"].clearValidate();
},
runDebug() {
this.$emit('runDebug');
}
},

View File

@ -39,17 +39,17 @@
</el-switch>
</el-form-item>
<el-button class="debug-button" size="small" type="primary" @click="runDebug">{{$t('load_test.save_and_run')}}</el-button>
<el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_test.request.parameters')" name="parameters">
<ms-api-key-value :is-read-only="isReadOnly" :items="request.parameters"
<ms-api-body v-if="isNotGet" :is-read-only="isReadOnly" :body="request.body"/>
<ms-api-key-value v-else :is-read-only="isReadOnly" :items="request.parameters"
:description="$t('api_test.request.parameters_desc')"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.headers')" name="headers">
<ms-api-key-value :is-read-only="isReadOnly" :suggestions="headerSuggestions" :items="request.headers"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.body')" name="body" v-if="isNotGet">
<ms-api-body :is-read-only="isReadOnly" :body="request.body"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
<ms-api-assertions :is-read-only="isReadOnly" :assertions="request.assertions"/>
</el-tab-pane>
@ -61,25 +61,24 @@
</template>
<script>
import MsApiKeyValue from "../ApiKeyValue";
import MsApiBody from "../ApiBody";
import MsApiAssertions from "../assertion/ApiAssertions";
import {KeyValue} from "../../model/ScenarioModel";
import MsApiExtract from "../extract/ApiExtract";
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
import {REQUEST_HEADERS} from "@/common/js/constants";
import {HttpRequest} from "../../model/ScenarioModel";
import MsApiKeyValue from "../ApiKeyValue";
import MsApiBody from "../ApiBody";
import MsApiAssertions from "../assertion/ApiAssertions";
import {HttpRequest, KeyValue} from "../../model/ScenarioModel";
import MsApiExtract from "../extract/ApiExtract";
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
import {REQUEST_HEADERS} from "@/common/js/constants";
export default {
name: "MsApiHttpRequestForm",
components: {ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue},
props: {
request: HttpRequest,
isReadOnly: {
type: Boolean,
default: false
}
},
export default {
name: "MsApiHttpRequestForm",
components: {ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue},
props: {
request: HttpRequest,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
let validateURL = (rule, value, callback) => {
@ -154,6 +153,9 @@
}
}
return url;
},
runDebug() {
this.$emit('runDebug');
}
},

View File

@ -112,7 +112,7 @@
}
request.dubboConfig = this.scenario.dubboConfig;
this.selected = request;
this.$emit("select", request);
this.$emit("select", request, this.scenario);
}
},

View File

@ -1,20 +1,33 @@
<template>
<component :is="component" :is-read-only="isReadOnly" :request="request"/>
<div class="request-form">
<component @runDebug="runDebug" :is="component" :is-read-only="isReadOnly" :request="request"/>
<ms-scenario-results v-loading="debugReportLoading" v-if="isCompleted" :scenarios="isCompleted ? request.debugReport.scenarios : []"/>
</div>
</template>
<script>
import {Request, RequestFactory} from "../../model/ScenarioModel";
import MsApiHttpRequestForm from "./ApiHttpRequestForm";
import MsApiDubboRequestForm from "./ApiDubboRequestForm";
import MsScenarioResults from "../../../report/components/ScenarioResults";
export default {
name: "MsApiRequestForm",
components: {MsApiDubboRequestForm, MsApiHttpRequestForm},
components: {MsScenarioResults, MsApiDubboRequestForm, MsApiHttpRequestForm},
props: {
request: Request,
isReadOnly: {
type: Boolean,
default: false
},
debugReportId: String
},
data() {
return {
reportId: "",
content: {scenarios:[]},
debugReportLoading: false,
showDebugReport: false
}
},
computed: {
@ -28,6 +41,51 @@
name = "MsApiHttpRequestForm";
}
return name;
},
isCompleted() {
return !!this.request.debugReport;
}
},
watch: {
debugReportId() {
this.getReport();
}
},
methods: {
getReport() {
if (this.debugReportId) {
this.debugReportLoading = true;
this.showDebugReport = true;
this.request.debugReport = {};
let url = "/api/report/get/" + this.debugReportId;
this.$get(url, response => {
let report = response.data || {};
let res = {};
if (response.data) {
try {
res = JSON.parse(report.content);
} catch (e) {
console.log(report.content)
throw e;
}
if (res) {
this.debugReportLoading = false;
this.request.debugReport = res;
this.deleteReport(this.debugReportId)
} else {
setTimeout(this.getReport, 2000)
}
} else {
this.debugReportLoading = false;
}
});
}
},
deleteReport(reportId) {
this.$post('/api/report/delete', {id: reportId});
},
runDebug() {
this.$emit('runDebug', this.request);
}
}
}
@ -35,4 +93,14 @@
<style scoped>
.scenario-results {
margin-top: 20px;
}
.request-form >>> .debug-button {
margin-left: auto;
display: block;
margin-right: 10px;
}
</style>

View File

@ -275,29 +275,23 @@ export class DubboSample extends DefaultTestElement {
}
export class HTTPSamplerProxy extends DefaultTestElement {
constructor(testName, request) {
constructor(testName, options = {}) {
super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName);
this.request = request || {};
if (request.useEnvironment) {
this.stringProp("HTTPSampler.domain", request.domain);
this.stringProp("HTTPSampler.protocol", request.protocol);
this.stringProp("HTTPSampler.path", this.request.path);
} else {
this.stringProp("HTTPSampler.domain", this.request.hostname);
this.stringProp("HTTPSampler.protocol", this.request.protocol.split(":")[0]);
this.stringProp("HTTPSampler.path", this.request.pathname);
}
this.stringProp("HTTPSampler.method", this.request.method);
this.stringProp("HTTPSampler.contentEncoding", this.request.encoding, "UTF-8");
if (!this.request.port) {
this.stringProp("HTTPSampler.domain", options.domain);
this.stringProp("HTTPSampler.protocol", options.protocol);
this.stringProp("HTTPSampler.path", options.path);
this.stringProp("HTTPSampler.method", options.method);
this.stringProp("HTTPSampler.contentEncoding", options.encoding, "UTF-8");
if (!options.port) {
this.stringProp("HTTPSampler.port", "");
} else {
this.stringProp("HTTPSampler.port", this.request.port);
this.stringProp("HTTPSampler.port", options.port);
}
this.boolProp("HTTPSampler.follow_redirects", this.request.follow, true);
this.boolProp("HTTPSampler.use_keepalive", this.request.keepalive, true);
this.boolProp("HTTPSampler.follow_redirects", options.follow, true);
this.boolProp("HTTPSampler.use_keepalive", options.keepalive, true);
}
}
@ -328,6 +322,15 @@ export class HTTPSamplerArguments extends Element {
}
}
export class CookieManager extends DefaultTestElement {
constructor(testName) {
super('CookieManager', 'CookiePanel', 'CookieManager', testName);
this.collectionProp('CookieManager.cookies');
this.boolProp('CookieManager.clearEachIteration', false, false);
this.boolProp('CookieManager.controlledByThreadGroup', false, false);
}
}
export class DurationAssertion extends DefaultTestElement {
constructor(testName, duration) {
super('DurationAssertion', 'DurationAssertionGui', 'DurationAssertion', testName);

View File

@ -1,5 +1,6 @@
import {
Arguments,
CookieManager,
DubboSample,
DurationAssertion,
Element,
@ -18,6 +19,8 @@ import {
ThreadGroup,
XPath2Extractor,
} from "./JMX";
import Mock from "mockjs";
import {funcFilters} from "@/common/js/func-filter";
export const uuid = function () {
let d = new Date().getTime()
@ -35,6 +38,35 @@ export const uuid = function () {
});
}
export const calculate = function (itemValue) {
if (!itemValue) {
return;
}
try {
if (itemValue.trim().startsWith("${")) {
// jmeter 内置函数不做处理
return itemValue;
}
let funcs = itemValue.split("|");
let value = Mock.mock(funcs[0].trim());
if (funcs.length === 1) {
return value;
}
for (let i = 1; i < funcs.length; i++) {
let func = funcs[i].trim();
let args = func.split(":");
let strings = [];
if (args[1]) {
strings = args[1].split(",");
}
value = funcFilters[args[0].trim()](value, ...strings);
}
return value;
} catch (e) {
return itemValue;
}
}
export const BODY_TYPE = {
KV: "KeyValue",
FORM_DATA: "Form Data",
@ -174,6 +206,7 @@ export class Scenario extends BaseConfig {
this.environmentId = undefined;
this.dubboConfig = undefined;
this.environment = undefined;
this.enableCookieShare = false;
this.set(options);
this.sets({variables: KeyValue, headers: KeyValue, requests: RequestFactory}, options);
@ -268,6 +301,7 @@ export class HttpRequest extends Request {
this.extract = undefined;
this.environment = undefined;
this.useEnvironment = undefined;
this.debugReport = undefined;
this.set(options);
this.sets({parameters: KeyValue, headers: KeyValue}, options);
@ -341,6 +375,7 @@ export class DubboRequest extends Request {
this.extract = new Extract(options.extract);
// Scenario.dubboConfig
this.dubboConfig = undefined;
this.debugReport = undefined;
this.sets({args: KeyValue, attachmentArgs: KeyValue}, options);
}
@ -654,7 +689,7 @@ const JMX_ASSERTION_CONDITION = {
class JMXHttpRequest {
constructor(request, environment) {
if (request && request instanceof HttpRequest && (request.url || request.path)) {
if (request && request instanceof HttpRequest) {
this.useEnvironment = request.useEnvironment;
this.method = request.method;
if (!request.useEnvironment) {
@ -662,14 +697,14 @@ class JMXHttpRequest {
request.url = 'http://' + request.url;
}
let url = new URL(request.url);
this.hostname = decodeURIComponent(url.hostname);
this.domain = decodeURIComponent(url.hostname);
this.port = url.port;
this.protocol = url.protocol.split(":")[0];
this.pathname = this.getPostQueryParameters(request, decodeURIComponent(url.pathname));
this.path = this.getPostQueryParameters(request, decodeURIComponent(url.pathname));
} else {
this.domain = environment.domain;
this.port = environment.port;
this.protocol = environment.protocol;
this.domain = environment.domain;
let url = new URL(environment.protocol + "://" + environment.socket);
this.path = this.getPostQueryParameters(request, decodeURIComponent(url.pathname + (request.path ? request.path : '')));
}
@ -688,7 +723,7 @@ class JMXHttpRequest {
for (let i = 0; i < parameters.length; i++) {
let parameter = parameters[i];
path += (parameter.name + '=' + parameter.value);
if (i != parameters.length - 1) {
if (i !== parameters.length - 1) {
path += '&';
}
}
@ -765,6 +800,8 @@ class JMXGenerator {
this.addScenarioHeaders(threadGroup, scenario);
this.addScenarioCookieManager(threadGroup, scenario);
scenario.requests.forEach(request => {
if (!request.isValid()) return;
let sampler;
@ -822,12 +859,21 @@ class JMXGenerator {
}
}
addScenarioCookieManager(threadGroup, scenario) {
if (scenario.enableCookieShare) {
threadGroup.put(new CookieManager(scenario.name));
}
}
addScenarioHeaders(threadGroup, scenario) {
let environment = scenario.environment;
if (environment) {
this.addEnvironments(environment.headers, scenario.headers)
}
let headers = this.filterKV(scenario.headers);
headers.forEach(h => {
h.value = calculate(h.value);
});
if (headers.length > 0) {
let name = scenario.name + " Headers"
threadGroup.put(new HeaderManager(name, headers));
@ -838,6 +884,9 @@ class JMXGenerator {
let name = request.name + " Headers";
this.addBodyFormat(request);
let headers = this.filterKV(request.headers);
headers.forEach(h => {
h.value = calculate(h.value);
});
if (headers.length > 0) {
httpSamplerProxy.put(new HeaderManager(name, headers));
}
@ -876,6 +925,9 @@ class JMXGenerator {
addRequestArguments(httpSamplerProxy, request) {
let args = this.filterKV(request.parameters);
args.forEach(arg => {
arg.value = calculate(arg.value);
});
if (args.length > 0) {
httpSamplerProxy.add(new HTTPSamplerArguments(args));
}
@ -885,6 +937,9 @@ class JMXGenerator {
let body = [];
if (request.body.isKV()) {
body = this.filterKV(request.body.kvs);
body.forEach(arg => {
arg.value = calculate(arg.value);
});
} else {
httpSamplerProxy.boolProp('HTTPSampler.postBodyRaw', true);
body.push({name: '', value: request.body.raw, encode: false});

View File

@ -7,10 +7,14 @@
</span>
<el-switch :disabled="!schedule.value || isReadOnly" v-model="schedule.enable" @change="scheduleChange"/>
<ms-schedule-edit :is-read-only="isReadOnly" :schedule="schedule" :save="save" :custom-validate="customValidate" ref="scheduleEdit"/>
<crontab-result v-show="false" :ex="schedule.value" ref="crontabResult" @resultListChange="resultListChange"/>
</div>
<div>
<span :class="{'disable-character': !schedule.enable}"> {{$t('schedule.next_execution_time')}}{{this.recentList.length > 0 && schedule.enable ? this.recentList[0] : $t('schedule.not_set')}} </span>
<span>
{{$t('schedule.next_execution_time')}}
<span :class="{'disable-character': !schedule.enable}" v-if="!schedule.enable">{{$t('schedule.not_set')}}</span>
<crontab-result v-if="schedule.enable" :enable-simple-mode="true" :ex="schedule.value" ref="crontabResult"/>
</span>
</div>
</div>
</template>
@ -59,9 +63,6 @@
scheduleChange() {
this.$emit('scheduleChange');
},
resultListChange(resultList) {
this.recentList = resultList;
},
flashResultList() {
this.$refs.crontabResult.expressionChange();
}

View File

@ -1,5 +1,5 @@
<template>
<div>
<span>
<span v-if="enableSimpleMode">{{resultList && resultList.length > 0 ? resultList[0] : ''}}</span>
<div v-if="!enableSimpleMode" class="popup-result">
<p class="title">{{$t('schedule.cron.recent_run_time')}}</p>
@ -9,7 +9,7 @@
</template>
</ul>
</div>
</div>
</span>
</template>
<script>

View File

@ -14,7 +14,7 @@
methods: {
changeRoute() {
//
this.$router.push(this.index + '/all');
this.$router.replace({path: this.index, query: {type: 'all'}});
window.location.reload();
}
}

View File

@ -33,6 +33,7 @@ import TestTrack from "../../track/TestTrack";
import ApiReportList from "../../api/report/ApiReportList";
import axios from "axios";
import ApiKeys from "../../settings/personal/ApiKeys";
import ServiceIntegration from "../../settings/organization/ServiceIntegration";
const requireContext = require.context('@/business/components/xpack/', true, /router\.js$/)
@ -70,6 +71,10 @@ const router = new VueRouter({
path: 'organizationworkspace',
component: OrganizationWorkspace,
},
{
path: 'serviceintegration',
component: ServiceIntegration,
},
{
path: 'personsetting',
component: PersonSetting

View File

@ -12,7 +12,7 @@
<template v-slot:title>{{$t('commons.project')}}</template>
<ms-recent-list :options="projectRecent"/>
<el-divider/>
<ms-show-all :index="'/performance/project'"/>
<ms-show-all :index="'/performance/project/all'"/>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/performance/project/create'" :title="$t('project.create')"/>
</el-submenu>
@ -21,7 +21,7 @@
<template v-slot:title>{{$t('commons.test')}}</template>
<ms-recent-list :options="testRecent"/>
<el-divider/>
<ms-show-all :index="'/performance/test'"/>
<ms-show-all :index="'/performance/test/all'"/>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/performance/test/create'" :title="$t('load_test.create')"/>
</el-submenu>
@ -30,7 +30,7 @@
<template v-slot:title>{{$t('commons.report')}}</template>
<ms-recent-list :options="reportRecent"/>
<el-divider/>
<ms-show-all :index="'/performance/report'"/>
<ms-show-all :index="'/performance/report/all'"/>
</el-submenu>
</el-menu>
</el-col>

View File

@ -14,7 +14,7 @@
<ms-test-heatmap :values="values"/>
</el-col>
<el-col :span="12">
<ms-api-test-schedule-list :group="'PERFORMANCE_TEST'"/>
<ms-schedule-list :group="'PERFORMANCE_TEST'"/>
</el-col>
</el-row>
</ms-main-container>
@ -28,12 +28,12 @@
import MsPerformanceTestRecentList from "./PerformanceTestRecentList"
import MsPerformanceReportRecentList from "./PerformanceReportRecentList"
import MsTestHeatmap from "../../common/components/MsTestHeatmap";
import MsApiTestScheduleList from "../../api/home/ApiTestScheduleList";
import MsScheduleList from "../../api/home/ScheduleList";
export default {
name: "PerformanceTestHome",
components: {
MsApiTestScheduleList,
MsScheduleList,
MsTestHeatmap,
MsMainContainer,
MsContainer,

View File

@ -6,21 +6,21 @@
<el-col :span="16">
<el-row>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/performance/test/' + this.projectId }">{{projectName}}
<el-breadcrumb-item :to="{ path: '/performance/test/' + this.projectId }">{{ projectName }}
</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/performance/test/edit/' + this.testId }">{{testName}}
<el-breadcrumb-item :to="{ path: '/performance/test/edit/' + this.testId }">{{ testName }}
</el-breadcrumb-item>
<el-breadcrumb-item>{{reportName}}</el-breadcrumb-item>
<el-breadcrumb-item>{{ reportName }}</el-breadcrumb-item>
</el-breadcrumb>
</el-row>
<el-row class="ms-report-view-btns">
<el-button :disabled="isReadOnly || report.status !== 'Running'" type="primary" plain size="mini"
@click="dialogFormVisible=true">
{{$t('report.test_stop_now')}}
{{ $t('report.test_stop_now') }}
</el-button>
<el-button :disabled="isReadOnly || report.status !== 'Completed'" type="success" plain size="mini"
@click="rerun(testId)">
{{$t('report.test_execute_again')}}
{{ $t('report.test_execute_again') }}
</el-button>
<!--<el-button :disabled="isReadOnly" type="info" plain size="mini">
{{$t('report.export')}}
@ -32,13 +32,13 @@
</el-col>
<el-col :span="8">
<span class="ms-report-time-desc">
{{$t('report.test_duration', [this.minutes, this.seconds])}}
{{ $t('report.test_duration', [this.minutes, this.seconds]) }}
</span>
<span class="ms-report-time-desc">
{{$t('report.test_start_time')}}{{startTime}}
{{ $t('report.test_start_time') }}{{ startTime }}
</span>
<span class="ms-report-time-desc">
{{$t('report.test_end_time')}}{{endTime}}
{{ $t('report.test_end_time') }}{{ endTime }}
</span>
</el-col>
</el-row>
@ -46,8 +46,10 @@
<el-divider/>
<el-tabs v-model="active" type="border-card" :stretch="true">
<el-tab-pane :label="$t('load_test.pressure_config')">
<ms-performance-pressure-config :is-read-only="true" :report="report"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_overview')">
<!-- <ms-report-test-overview :id="reportId" :status="status"/>-->
<ms-report-test-overview :report="report" ref="testOverview"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_request_statistics')">
@ -66,9 +68,9 @@
<p v-html="$t('report.force_stop_tips')"/>
<p v-html="$t('report.stop_tips')"/>
<div slot="footer" class="dialog-footer">
<el-button type="danger" size="small" @click="stopTest(true)">{{$t('report.force_stop_btn')}}
<el-button type="danger" size="small" @click="stopTest(true)">{{ $t('report.force_stop_btn') }}
</el-button>
<el-button type="primary" size="small" @click="stopTest(false)">{{$t('report.stop_btn')}}
<el-button type="primary" size="small" @click="stopTest(false)">{{ $t('report.stop_btn') }}
</el-button>
</div>
</el-dialog>
@ -77,244 +79,250 @@
</template>
<script>
import MsReportErrorLog from './components/ErrorLog';
import MsReportLogDetails from './components/LogDetails';
import MsReportRequestStatistics from './components/RequestStatistics';
import MsReportTestOverview from './components/TestOverview';
import MsContainer from "../../common/components/MsContainer";
import MsMainContainer from "../../common/components/MsMainContainer";
import {checkoutTestManagerOrTestUser} from "../../../../common/js/utils";
import MsReportErrorLog from './components/ErrorLog';
import MsReportLogDetails from './components/LogDetails';
import MsReportRequestStatistics from './components/RequestStatistics';
import MsReportTestOverview from './components/TestOverview';
import MsPerformancePressureConfig from "./components/PerformancePressureConfig";
import MsContainer from "../../common/components/MsContainer";
import MsMainContainer from "../../common/components/MsMainContainer";
export default {
name: "PerformanceReportView",
components: {
MsReportErrorLog,
MsReportLogDetails,
MsReportRequestStatistics,
MsReportTestOverview,
MsContainer,
MsMainContainer
},
data() {
return {
result: {},
active: '0',
reportId: '',
status: '',
reportName: '',
testId: '',
testName: '',
projectId: '',
projectName: '',
startTime: '0',
endTime: '0',
minutes: '0',
seconds: '0',
title: 'Logging',
report: {},
isReadOnly: false,
websocket: null,
dialogFormVisible: false,
import {checkoutTestManagerOrTestUser} from "@/common/js/utils";
export default {
name: "PerformanceReportView",
components: {
MsReportErrorLog,
MsReportLogDetails,
MsReportRequestStatistics,
MsReportTestOverview,
MsContainer,
MsMainContainer,
MsPerformancePressureConfig
},
data() {
return {
result: {},
active: '1',
reportId: '',
status: '',
reportName: '',
testId: '',
testName: '',
projectId: '',
projectName: '',
startTime: '0',
endTime: '0',
minutes: '0',
seconds: '0',
title: 'Logging',
report: {},
isReadOnly: false,
websocket: null,
dialogFormVisible: false,
testPlan: {testResourcePoolId: null}
}
},
methods: {
initBreadcrumb() {
if (this.reportId) {
this.result = this.$get("/performance/report/test/pro/info/" + this.reportId, res => {
let data = res.data;
if (data) {
this.reportName = data.name;
this.testId = data.testId;
this.testName = data.testName;
this.projectId = data.projectId;
this.projectName = data.projectName;
}
})
}
},
methods: {
initBreadcrumb() {
if (this.reportId) {
this.result = this.$get("/performance/report/test/pro/info/" + this.reportId, res => {
let data = res.data;
initReportTimeInfo() {
if (this.reportId) {
this.result = this.$get("/performance/report/content/report_time/" + this.reportId)
.then(res => {
let data = res.data.data;
if (data) {
this.reportName = data.name;
this.testId = data.testId;
this.testName = data.testName;
this.projectId = data.projectId;
this.projectName = data.projectName;
this.startTime = data.startTime;
this.endTime = data.endTime;
let duration = data.duration;
this.minutes = Math.floor(duration / 60);
this.seconds = duration % 60;
}
}).catch(() => {
this.clearData();
})
}
},
initReportTimeInfo() {
if (this.reportId) {
this.result = this.$get("/performance/report/content/report_time/" + this.reportId)
.then(res => {
let data = res.data.data;
if (data) {
this.startTime = data.startTime;
this.endTime = data.endTime;
let duration = data.duration;
this.minutes = Math.floor(duration / 60);
this.seconds = duration % 60;
}
}).catch(() => {
this.clearData();
})
}
},
initWebSocket() {
let protocol = "ws://";
if (window.location.protocol === 'https:') {
protocol = "wss://";
}
const uri = protocol + window.location.host + "/performance/report/" + this.reportId;
this.websocket = new WebSocket(uri);
this.websocket.onmessage = this.onMessage;
this.websocket.onopen = this.onOpen;
this.websocket.onerror = this.onError;
this.websocket.onclose = this.onClose;
},
checkReportStatus(status) {
switch (status) {
case 'Error':
this.$warning(this.$t('report.generation_error'));
break;
case 'Starting':
this.$alert(this.$t('report.start_status'));
break;
case 'Reporting':
case 'Running':
case 'Completed':
default:
break;
}
},
clearData() {
this.startTime = '0';
this.endTime = '0';
this.minutes = '0';
this.seconds = '0';
},
stopTest(forceStop) {
this.result = this.$get('/performance/stop/' + this.reportId + '/' + forceStop, () => {
this.$success(this.$t('report.test_stop_success'));
if (forceStop) {
this.$router.push('/performance/report/all');
} else {
this.report.status = 'Completed';
}
});
this.dialogFormVisible = false;
},
rerun(testId) {
this.$confirm(this.$t('report.test_rerun_confirm'), '', {
confirmButtonText: this.$t('commons.confirm'),
cancelButtonText: this.$t('commons.cancel'),
type: 'warning'
}).then(() => {
this.result = this.$post('/performance/run', {id: testId, triggerMode: 'MANUAL'}, (response) => {
this.reportId = response.data;
this.$router.push({path: '/performance/report/view/' + this.reportId});
// socket
this.initWebSocket();
})
}).catch(() => {
});
},
onOpen() {
window.console.log("socket opening.");
},
onError(e) {
window.console.error(e)
},
onMessage(e) {
this.$set(this.report, "refresh", e.data); //
this.$set(this.report, "status", 'Running');
this.initReportTimeInfo();
window.console.log('receive a message:', e.data);
},
onClose(e) {
this.$set(this.report, "refresh", Math.random()); //
this.$set(this.report, "status", 'Completed');
this.initReportTimeInfo();
window.console.log("socket closed.");
}
},
created() {
this.isReadOnly = false;
if (!checkoutTestManagerOrTestUser()) {
this.isReadOnly = true;
initWebSocket() {
let protocol = "ws://";
if (window.location.protocol === 'https:') {
protocol = "wss://";
}
this.reportId = this.$route.path.split('/')[4];
this.result = this.$get("/performance/report/" + this.reportId, res => {
let data = res.data;
if (data) {
this.status = data.status;
this.$set(this.report, "id", this.reportId);
this.$set(this.report, "status", data.status);
this.checkReportStatus(data.status);
if (this.status === "Completed" || this.status === "Running") {
this.initReportTimeInfo();
}
this.initBreadcrumb();
this.initWebSocket();
const uri = protocol + window.location.host + "/performance/report/" + this.reportId;
this.websocket = new WebSocket(uri);
this.websocket.onmessage = this.onMessage;
this.websocket.onopen = this.onOpen;
this.websocket.onerror = this.onError;
this.websocket.onclose = this.onClose;
},
checkReportStatus(status) {
switch (status) {
case 'Error':
this.$warning(this.$t('report.generation_error'));
break;
case 'Starting':
this.$alert(this.$t('report.start_status'));
break;
case 'Reporting':
case 'Running':
case 'Completed':
default:
break;
}
},
clearData() {
this.startTime = '0';
this.endTime = '0';
this.minutes = '0';
this.seconds = '0';
},
stopTest(forceStop) {
this.result = this.$get('/performance/stop/' + this.reportId + '/' + forceStop, () => {
this.$success(this.$t('report.test_stop_success'));
if (forceStop) {
this.$router.push('/performance/report/all');
} else {
this.$error(this.$t('report.not_exist'))
this.report.status = 'Completed';
}
});
this.dialogFormVisible = false;
},
beforeDestroy() {
this.websocket.close() //websocket
rerun(testId) {
this.$confirm(this.$t('report.test_rerun_confirm'), '', {
confirmButtonText: this.$t('commons.confirm'),
cancelButtonText: this.$t('commons.cancel'),
type: 'warning'
}).then(() => {
this.result = this.$post('/performance/run', {id: testId, triggerMode: 'MANUAL'}, (response) => {
this.reportId = response.data;
this.$router.push({path: '/performance/report/view/' + this.reportId});
// socket
this.initWebSocket();
})
}).catch(() => {
});
},
watch: {
'$route'(to) {
if (to.name === "perReportView") {
this.isReadOnly = false;
if (!checkoutTestManagerOrTestUser()) {
this.isReadOnly = true;
}
let reportId = to.path.split('/')[4];
this.reportId = reportId;
if (reportId) {
this.$get("/performance/report/test/pro/info/" + reportId, response => {
let data = response.data;
if (data) {
this.status = data.status;
this.reportName = data.name;
this.testName = data.testName;
this.testId = data.testId;
this.projectName = data.projectName;
onOpen() {
window.console.log("socket opening.");
},
onError(e) {
window.console.error(e)
},
onMessage(e) {
this.$set(this.report, "refresh", e.data); //
this.$set(this.report, "status", 'Running');
this.initReportTimeInfo();
window.console.log('receive a message:', e.data);
},
onClose(e) {
this.$set(this.report, "refresh", Math.random()); //
this.$set(this.report, "status", 'Completed');
this.initReportTimeInfo();
window.console.log("socket closed.");
}
},
created() {
this.isReadOnly = false;
if (!checkoutTestManagerOrTestUser()) {
this.isReadOnly = true;
}
this.reportId = this.$route.path.split('/')[4];
this.result = this.$get("/performance/report/" + this.reportId, res => {
let data = res.data;
if (data) {
this.status = data.status;
this.$set(this.report, "id", this.reportId);
this.$set(this.report, "status", data.status);
this.$set(this.report, "testId", data.testId);
this.$set(this.report, "loadConfiguration", data.loadConfiguration);
this.checkReportStatus(data.status);
if (this.status === "Completed" || this.status === "Running") {
this.initReportTimeInfo();
}
this.initBreadcrumb();
this.initWebSocket();
} else {
this.$error(this.$t('report.not_exist'))
}
});
this.$set(this.report, "id", reportId);
this.$set(this.report, "status", data.status);
},
beforeDestroy() {
this.websocket.close() //websocket
},
watch: {
'$route'(to) {
if (to.name === "perReportView") {
this.isReadOnly = false;
if (!checkoutTestManagerOrTestUser()) {
this.isReadOnly = true;
}
let reportId = to.path.split('/')[4];
this.reportId = reportId;
if (reportId) {
this.$get("/performance/report/test/pro/info/" + reportId, response => {
let data = response.data;
if (data) {
this.status = data.status;
this.reportName = data.name;
this.testName = data.testName;
this.testId = data.testId;
this.projectName = data.projectName;
this.checkReportStatus(data.status);
if (this.status === "Completed") {
this.result = this.$get("/performance/report/content/report_time/" + this.reportId).then(res => {
let data = res.data.data;
if (data) {
this.startTime = data.startTime;
this.endTime = data.endTime;
let duration = data.duration;
this.minutes = Math.floor(duration / 60);
this.seconds = duration % 60;
}
}).catch(() => {
this.clearData();
})
} else {
this.$set(this.report, "id", reportId);
this.$set(this.report, "status", data.status);
this.checkReportStatus(data.status);
if (this.status === "Completed") {
this.result = this.$get("/performance/report/content/report_time/" + this.reportId).then(res => {
let data = res.data.data;
if (data) {
this.startTime = data.startTime;
this.endTime = data.endTime;
let duration = data.duration;
this.minutes = Math.floor(duration / 60);
this.seconds = duration % 60;
}
}).catch(() => {
this.clearData();
}
})
} else {
this.$error(this.$t('report.not_exist'));
this.clearData();
}
});
} else {
this.$error(this.$t('report.not_exist'));
}
});
}
}
}
}
}
}
</script>
<style scoped>
.ms-report-view-btns {
margin-top: 15px;
}
.ms-report-view-btns {
margin-top: 15px;
}
.ms-report-time-desc {
text-align: left;
display: block;
color: #5C7878;
}
.ms-report-time-desc {
text-align: left;
display: block;
color: #5C7878;
}
</style>

View File

@ -0,0 +1,311 @@
<template>
<div v-loading="result.loading" class="pressure-config-container">
<el-row>
<el-col :span="10">
<el-form :inline="true">
<el-form-item>
<div class="config-form-label">{{ $t('load_test.thread_num') }}</div>
</el-form-item>
<el-form-item>
<el-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"/>
</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>
<chart class="chart-container" ref="chart1" :options="orgOptions" :autoresize="true"></chart>
</el-col>
</el-row>
</div>
</template>
<script>
import echarts from "echarts";
const TARGET_LEVEL = "TargetLevel";
const RAMP_UP = "RampUp";
const STEPS = "Steps";
const DURATION = "duration";
const RPS_LIMIT = "rpsLimit";
const RPS_LIMIT_ENABLE = "rpsLimitEnable";
export default {
name: "MsPerformancePressureConfig",
props: ['report'],
data() {
return {
result: {},
threadNumber: 10,
duration: 10,
rampUpTime: 10,
step: 10,
rpsLimit: 10,
rpsLimitEnable: false,
orgOptions: {},
}
},
mounted() {
this.getLoadConfig();
},
methods: {
calculateLoadConfiguration: function (data) {
data.forEach(d => {
switch (d.key) {
case TARGET_LEVEL:
this.threadNumber = d.value;
break;
case RAMP_UP:
this.rampUpTime = d.value;
break;
case DURATION:
this.duration = d.value;
break;
case STEPS:
this.step = d.value;
break;
case RPS_LIMIT:
this.rpsLimit = d.value;
break;
default:
break;
}
});
this.threadNumber = this.threadNumber || 10;
this.duration = this.duration || 30;
this.rampUpTime = this.rampUpTime || 12;
this.step = this.step || 3;
this.rpsLimit = this.rpsLimit || 10;
this.calculateChart();
},
getLoadConfig() {
if (!this.report.id) {
return;
}
this.$get("/performance/report/" + this.report.id, res => {
let data = res.data;
if (data) {
if (data.loadConfiguration) {
let d = JSON.parse(data.loadConfiguration);
this.calculateLoadConfiguration(d);
} else {
this.$get('/performance/get-load-config/' + this.report.testId, (response) => {
if (response.data) {
let data = JSON.parse(response.data);
this.calculateLoadConfiguration(data);
}
});
}
} else {
this.$error(this.$t('report.not_exist'))
}
});
},
calculateChart() {
if (this.duration < this.rampUpTime) {
this.rampUpTime = this.duration;
}
if (this.rampUpTime < this.step) {
this.step = this.rampUpTime;
}
this.orgOptions = {
xAxis: {
type: 'category',
boundaryGap: false,
data: []
},
yAxis: {
type: 'value'
},
tooltip: {
trigger: 'axis',
formatter: '{a}: {c0}',
axisPointer: {
lineStyle: {
color: '#57617B'
}
}
},
series: [{
name: 'User',
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: 'rgba(137, 189, 27, 0.3)'
}, {
offset: 0.8,
color: 'rgba(137, 189, 27, 0)'
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: 'rgb(137,189,27)',
borderColor: 'rgba(137,189,2,0.27)',
borderWidth: 12
}
},
}]
};
let timePeriod = Math.floor(this.rampUpTime / this.step);
let timeInc = timePeriod;
let threadPeriod = Math.floor(this.threadNumber / this.step);
let threadInc1 = Math.floor(this.threadNumber / this.step);
let threadInc2 = Math.ceil(this.threadNumber / this.step);
let inc2count = this.threadNumber - this.step * threadInc1;
for (let i = 0; i <= this.duration; i++) {
// x
this.orgOptions.xAxis.data.push(i);
if (i > timePeriod) {
timePeriod += timeInc;
if (inc2count > 0) {
threadPeriod = threadPeriod + threadInc2;
inc2count--;
} else {
threadPeriod = threadPeriod + threadInc1;
}
if (threadPeriod > this.threadNumber) {
threadPeriod = this.threadNumber;
}
this.orgOptions.series[0].data.push(threadPeriod);
} else {
this.orgOptions.series[0].data.push(threadPeriod);
}
}
},
},
watch: {
report: {
handler(val) {
if (!val.testId) {
return;
}
this.getLoadConfig();
},
deep: true
}
}
}
</script>
<style scoped>
.pressure-config-container .el-input {
width: 130px;
}
.pressure-config-container .config-form-label {
width: 130px;
}
.pressure-config-container .input-bottom-border input {
border: 0;
border-bottom: 1px solid #DCDFE6;
}
.chart-container {
width: 100%;
}
.el-col .el-form {
margin-top: 15px;
text-align: left;
}
.el-col {
margin-top: 15px;
text-align: left;
}
.title {
margin-left: 60px;
}
</style>

View File

@ -21,6 +21,8 @@
</el-menu-item>
<el-menu-item index="/setting/organizationworkspace" v-permission="['org_admin']">{{$t('commons.workspace')}}
</el-menu-item>
<el-menu-item index="/setting/serviceintegration" v-permission="['org_admin']">{{$t('organization.service_integration')}}
</el-menu-item>
</el-submenu>
<el-submenu index="3" v-permission="['test_manager']" v-if="isCurrentWorkspaceUser">

View File

@ -0,0 +1,85 @@
<template>
<el-card class="header-title">
<div>
<div>{{$t('organization.select_defect_platform')}}</div>
<el-radio-group v-model="platform" style="margin-top: 10px">
<el-radio v-for="(item, index) in platforms" :key="index" :label="item.value" size="small">
{{item.name}}
</el-radio>
</el-radio-group>
</div>
<div style="width: 500px">
<div style="margin-top: 20px;margin-bottom: 10px">{{$t('organization.basic_auth_info')}}</div>
<el-form :model="form" ref="form" label-width="100px" size="small">
<el-form-item :label="$t('organization.api_account')" prop="account">
<el-input v-model="form.account" :placeholder="$t('organization.input_api_account')"/>
</el-form-item>
<el-form-item :label="$t('organization.api_password')" prop="password">
<el-input v-model="form.password" auto-complete="new-password" :placeholder="$t('organization.input_api_password')" show-password/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" @click="submit('form')" style="width: 400px">
{{$t('commons.save')}}
</el-button>
</el-form-item>
</el-form>
</div>
<div class="defect-tip">
<div>{{$t('organization.use_tip')}}</div>
<div>
1. {{$t('organization.use_tip_one')}}
</div>
<div>
2. {{$t('organization.use_tip_two')}}
<router-link to="/track/project/all" style="margin-left: 5px">{{$t('organization.link_the_project_now')}}</router-link>
</div>
</div>
</el-card>
</template>
<script>
export default {
name: "DefectManagement",
data() {
return {
form: {},
platform: '',
platforms: [
{
name: 'TAPD',
value: 'tapd',
},
{
name: 'JIRA',
value: 'jira',
}
],
rules: {
account: {required: true, message: this.$t('organization.input_api_account'), trigger: ['change', 'blur']},
password: {required: true, message: this.$t('organization.input_api_password'), trigger: ['change', 'blur']}
},
}
},
methods: {
submit(form) {
}
}
}
</script>
<style scoped>
.header-title {
padding: 10px 30px;
}
.defect-tip {
background: #EDEDED;
border: solid #E1E1E1 1px;
margin: 10px 0;
padding: 10px;
border-radius: 3px;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div>
<el-tabs class="system-setting" v-model="activeName">
<el-tab-pane :label="$t('organization.defect_manage')" name="defect">
<defect-management/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import DefectManagement from "./DefectManagement";
export default {
name: "ServiceIntegration",
components: {
DefectManagement
},
data() {
return {
activeName: 'defect'
}
}
}
</script>
<style scoped>
</style>

View File

@ -13,7 +13,7 @@
<template v-slot:title>{{$t('commons.project')}}</template>
<ms-recent-list :options="projectRecent"/>
<el-divider/>
<ms-show-all :index="'/track/project'"/>
<ms-show-all :index="'/track/project/all'"/>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/track/project/create'" :title="$t('project.create')"/>
</el-submenu>
@ -22,7 +22,7 @@
<template v-slot:title>{{$t('test_track.case.test_case')}}</template>
<ms-recent-list :options="caseRecent"/>
<el-divider/>
<ms-show-all :index="'/track/case'"/>
<ms-show-all :index="'/track/case/all'"/>
<el-menu-item :index="testCaseEditPath" class="blank_item"></el-menu-item>
<el-menu-item :index="testCaseProjectPath" class="blank_item"></el-menu-item>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/track/case/create'" :title="$t('test_track.case.create_case')"/>
@ -32,7 +32,7 @@
<template v-slot:title>{{$t('test_track.plan.test_plan')}}</template>
<ms-recent-list :options="planRecent"/>
<el-divider/>
<ms-show-all :index="'/track/plan'"/>
<ms-show-all :index="'/track/plan/all'"/>
<el-menu-item :index="testPlanViewPath" class="blank_item"></el-menu-item>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/track/plan/create'" :title="$t('test_track.plan.create_plan')"/>
</el-submenu>

View File

@ -55,6 +55,7 @@
</el-table-column>
<el-table-column
prop="num"
sortable="custom"
:label="$t('commons.id')"
show-overflow-tooltip>
</el-table-column>
@ -497,6 +498,10 @@
this.initTableData();
},
sort(column) {
//
if (this.condition.orders) {
this.condition.orders = [];
}
_sort(column, this.condition);
this.initTableData();
},

View File

@ -57,3 +57,95 @@ export const REQUEST_HEADERS = [
{value: 'Via'},
{value: 'Warning'}
]
export const MOCKJS_FUNC = [
{name: '@boolean'},
{name: '@natural'},
{name: '@integer'},
{name: '@float'},
{name: '@character'},
{name: '@string'},
{name: '@range'},
{name: '@date'},
{name: '@time'},
{name: '@datetime'},
{name: '@now'},
{name: '@img'},
{name: '@dataImage'},
{name: '@color'},
{name: '@hex'},
{name: '@rgb'},
{name: '@rgba'},
{name: '@hsl'},
{name: '@paragraph'},
{name: '@sentence'},
{name: '@word'},
{name: '@title'},
{name: '@cparagraph'},
{name: '@csentence'},
{name: '@cword'},
{name: '@ctitle'},
{name: '@first'},
{name: '@last'},
{name: '@name'},
{name: '@cfirst'},
{name: '@clast'},
{name: '@cname'},
{name: '@url'},
{name: '@domain'},
{name: '@protocol'},
{name: '@tld'},
{name: '@email'},
{name: '@ip'},
{name: '@region'},
{name: '@province'},
{name: '@city'},
{name: '@county'},
{name: '@zip'},
{name: '@capitalize'},
{name: '@upper'},
{name: '@lower'},
{name: '@pick'},
{name: '@shuffle'},
{name: '@guid'},
{name: '@id'},
{name: '@increment'}
]
export const JMETER_FUNC = [
{name: "${__threadNum}"},
{name: "${__samplerName}"},
{name: "${__machineIP}"},
{name: "${__machineName}"},
{name: "${__time}"},
{name: "${__log}"},
{name: "${__logn}"},
{name: "${__StringFromFile}"},
{name: "${__FileToString}"},
{name: "${__CSVRead}"},
{name: "${__XPath}"},
{name: "${__counter}"},
{name: "${__intSum}"},
{name: "${__longSum}"},
{name: "${__Random}"},
{name: "${__RandomString}"},
{name: "${__UUID}"},
{name: "${__BeanShell}"},
{name: "${__javaScript}"},
{name: "${__jexl}"},
{name: "${__jexl2}"},
{name: "${__property}"},
{name: "${__P}"},
{name: "${__setProperty}"},
{name: "${__split}"},
{name: "${__V}"},
{name: "${__eval}"},
{name: "${__evalVar}"},
{name: "${__regexFunction}"},
{name: "${__escapeOroRegexpChars}"},
{name: "${__char}"},
{name: "${__unescape}"},
{name: "${__unescapeHtml}"},
{name: "${__escapeHtml}"},
{name: "${__TestPlanName}"},
]

View File

@ -0,0 +1,184 @@
const aUniqueVerticalStringNotFoundInData = '___UNIQUE_VERTICAL___';
const aUniqueCommaStringNotFoundInData = '___UNIQUE_COMMA___';
const segmentSeparateChar = '|';
const methodAndArgsSeparateChar = ':';
const argsSeparateChar = ',';
const md5 = require('md5');
const sha = require('sha.js');
const Base64 = require('js-base64').Base64;
export const funcFilters = {
md5: function (str) {
return md5(str);
},
sha: function (str, arg) {
return sha(arg)
.update(str)
.digest('hex');
},
/**
* type: sha1 sha224 sha256 sha384 sha512
*/
sha1: function (str) {
return sha('sha1')
.update(str)
.digest('hex');
},
sha224: function (str) {
return sha('sha224')
.update(str)
.digest('hex');
},
sha256: function (str) {
return sha('sha256')
.update(str)
.digest('hex');
},
sha384: function (str) {
return sha('sha384')
.update(str)
.digest('hex');
},
sha512: function (str) {
return sha('sha512')
.update(str)
.digest('hex');
},
base64: function (str) {
return Base64.encode(str);
},
unbase64: function (str) {
return Base64.decode(str);
},
substr: function (str, ...args) {
return str.substr(...args);
},
concat: function (str, ...args) {
args.forEach(item => {
str += item;
});
return str;
},
lconcat: function (str, ...args) {
args.forEach(item => {
str = item + this._string;
});
return str;
},
lower: function (str) {
return str.toLowerCase();
},
upper: function (str) {
return str.toUpperCase();
},
length: function (str) {
return str.length;
},
number: function (str) {
return !isNaN(str) ? +str : str;
}
};
let handleValue = function (str) {
return str;
};
const _handleValue = function (str) {
if (str[0] === str[str.length - 1] && (str[0] === '"' || str[0] === "'")) {
str = str.substr(1, str.length - 2);
}
return handleValue(
str
.replace(new RegExp(aUniqueVerticalStringNotFoundInData, 'g'), segmentSeparateChar)
.replace(new RegExp(aUniqueCommaStringNotFoundInData, 'g'), argsSeparateChar)
);
};
class PowerString {
constructor(str) {
this._string = str;
}
toString() {
return this._string;
}
}
function addMethod(method, fn) {
PowerString.prototype[method] = function (...args) {
args.unshift(this._string + '');
this._string = fn.apply(this, args);
return this;
};
}
function importMethods(handles) {
for (let method in handles) {
addMethod(method, handles[method]);
}
}
importMethods(funcFilters);
function handleOriginStr(str, handleValueFn) {
if (!str) return str;
if (typeof handleValueFn === 'function') {
handleValue = handleValueFn;
}
str = str
.replace('\\' + segmentSeparateChar, aUniqueVerticalStringNotFoundInData)
.replace('\\' + argsSeparateChar, aUniqueCommaStringNotFoundInData)
.split(segmentSeparateChar)
.map(handleSegment)
.reduce(execute, null)
.toString();
return str;
}
function execute(str, curItem, index) {
if (index === 0) {
return new PowerString(curItem);
}
return str[curItem.method].apply(str, curItem.args);
}
function handleSegment(str, index) {
str = str.trim();
if (index === 0) {
return _handleValue(str);
}
let method,
args = [];
if (str.indexOf(methodAndArgsSeparateChar) > 0) {
str = str.split(methodAndArgsSeparateChar);
method = str[0].trim();
args = str[1].split(argsSeparateChar).map(item => _handleValue(item.trim()));
} else {
method = str;
}
if (typeof funcFilters[method] !== 'function') {
throw new Error(`This method name(${method}) is not exist.`);
}
return {
method,
args
};
}

View File

@ -173,8 +173,18 @@ export default {
special_characters_are_not_supported: 'Incorrect format (special characters are not supported and cannot end with \'-\')',
none: 'None Organization',
select: 'Select Organization',
service_integration: 'Service integration',
defect_manage: 'Defect management platform',
select_defect_platform: 'Please select the defect management platform to be integrated:',
basic_auth_info: 'Basic Auth account information:',
api_account: 'API account',
api_password: 'API password',
input_api_account: 'please enter account',
input_api_password: 'Please enter password',
use_tip: 'Usage guidelines:',
use_tip_one: 'Basic Auth account information is queried in "Company Management-Security and Integration-Open Platform"',
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',
},
project: {
name: 'Project name',
@ -388,6 +398,11 @@ export default {
url_description: "etc: https://fit2cloud.com",
path_description: "etc/login",
parameters: "Query parameters",
parameters_filter_example: "Example",
parameters_filter_tips: "Only support MockJs function result preview",
parameters_advance: "Advanced parameter settings",
parameters_preview: "Preview",
parameters_preview_warning: "Please enter the template first",
parameters_desc: "Parameters will be appended to the URL e.g. https://fit2cloud.com?Name=Value&Name2=Value2",
headers: "Headers",
body: "Body",
@ -495,9 +510,9 @@ export default {
execution_result: ": Please select the execution result",
actual_result: ": The actual result is empty",
case: {
input_test_case:'Please enter the associated case name',
test_name:'TestName',
other:'--Other--',
input_test_case: 'Please enter the associated case name',
test_name: 'TestName',
other: '--Other--',
test_case: "Case",
move: "Move case",
case_list: "Test case list",
@ -715,6 +730,7 @@ export default {
test_name: 'Test Name',
running_rule: 'Rule',
job_status: 'Status',
running_task: 'Running Task',
please_input_cron_expression: "Please Input Cron Expression",
generate_expression: "Generate Expression",
cron_expression_format_error: "Cron Expression Format Error",

View File

@ -174,6 +174,18 @@ export default {
none: '无组织',
select: '选择组织',
delete_warning: '删除该组织将同步删除该组织下所有相关工作空间和相关工作空间下的所有项目,以及项目中的所有用例、接口测试、性能测试等,确定要删除吗?',
service_integration: '服务集成',
defect_manage: '缺陷管理平台',
select_defect_platform: '请选择要集成的缺陷管理平台:',
basic_auth_info: 'Basic Auth 账号信息:',
api_account: 'API 账号',
api_password: 'API 口令',
input_api_account: '请输入账号',
input_api_password: '请输入口令',
use_tip: '使用指引:',
use_tip_one: 'Basic Auth 账号信息在"公司管理-安全与集成-开放平台"中查询',
use_tip_two: '保存 Basic Auth 账号信息后,需要在 Metersphere 项目中手动关联 ID/key',
link_the_project_now: '马上关联项目',
},
project: {
recent: '最近的项目',
@ -387,6 +399,13 @@ export default {
path_description: "例如:/login",
url_invalid: "URL无效",
parameters: "请求参数",
parameters_filter: "内置函数",
parameters_filter_desc: "使用方法",
parameters_filter_example: "示例",
parameters_filter_tips: "只支持 MockJs 函数结果预览",
parameters_advance: "高级参数设置",
parameters_preview: "预览",
parameters_preview_warning: "请先输入模版",
parameters_desc: "参数追加到URL例如https://fit2cloud.com/entries?key1=Value1&Key2=Value2",
headers: "请求头",
body: "请求内容",
@ -496,9 +515,9 @@ export default {
actual_result: ": 实际结果为空",
case: {
input_test_case:'请输入关联用例名称',
test_name:'测试名称',
other:"--其他--",
input_test_case: '请输入关联用例名称',
test_name: '测试名称',
other: "--其他--",
test_case: "测试用例",
move: "移动用例",
case_list: "用例列表",
@ -713,6 +732,7 @@ export default {
test_name: '测试名称',
running_rule: '运行规则',
job_status: '任务状态',
running_task: '运行中的任务',
next_execution_time: "下次执行时间",
edit_timer_task: "编辑定时任务",
please_input_cron_expression: "请输入 Cron 表达式",

View File

@ -172,7 +172,18 @@ export default {
none: '無組織',
select: '選擇組織',
delete_warning: '删除该组织将同步删除该组织下所有相关工作空间和相关工作空间下的所有项目,以及项目中的所有用例、接口测试、性能测试等,确定要删除吗?',
service_integration: '服務集成',
defect_manage: '缺陷管理平台',
select_defect_platform: '請選擇要集成的缺陷管理平台:',
basic_auth_info: 'Basic Auth 賬號信息:',
api_account: 'API 賬號',
api_password: 'API 口令',
input_api_account: '請輸入賬號',
input_api_password: '請輸入口令',
use_tip: '使用指引:',
use_tip_one: 'Basic Auth 賬號信息在"公司管理-安全與集成-開放平台"中查詢',
use_tip_two: '保存 Basic Auth 賬號信息後,需要在 Metersphere 項目中手動關聯 ID/key',
link_the_project_now: '馬上關聯項目',
},
project: {
recent: '最近的項目',
@ -387,6 +398,11 @@ export default {
path_description: "例如:/login",
url_invalid: "URL無效",
parameters: "請求參數",
parameters_filter_example: "示例",
parameters_filter_tips: "只支持MockJs函數結果預覽",
parameters_advance: "高級參數設置",
parameters_preview: "預覽",
parameters_preview_warning: "請先輸入模版",
parameters_desc: "參數追加到URL,例如https://fit2cloud.com/entrieskey1=Value1&amp;Key2=Value2",
headers: "請求頭",
body: "請求內容",
@ -494,9 +510,9 @@ export default {
execution_result: ": 請選擇執行結果",
actual_result: ": 實際結果為空",
case: {
input_test_case:'請輸入關聯用例名稱',
test_name:'測試名稱',
other:'--其他--',
input_test_case: '請輸入關聯用例名稱',
test_name: '測試名稱',
other: '--其他--',
test_case: "測試用例",
move: "移動用例",
case_list: "用例列表",
@ -713,6 +729,7 @@ export default {
test_name: '測試名稱',
running_rule: '運行規則',
job_status: '任務狀態',
running_task: '運行中的任務',
please_input_cron_expression: "請輸入 Cron 表達式",
generate_expression: "生成表達式",
cron_expression_format_error: "Cron 表達式格式錯誤",