Merge branch 'master' of https://github.com/metersphere/metersphere
Conflicts: frontend/src/business/components/api/report/components/ScenarioResults.vue frontend/src/business/components/common/head/ShowAll.vue
This commit is contained in:
commit
a8671ca4f3
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package io.metersphere.commons.constants;
|
||||
|
||||
public enum ApiRunMode {
|
||||
RUN, DEBUG
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE load_test_report
|
||||
ADD load_configuration LONGTEXT NULL;
|
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
||||
|
|
|
@ -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 {
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
|
||||
<el-card>
|
||||
<el-card class="scenario-results">
|
||||
<div class="scenario-header">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="16">
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
}
|
||||
request.dubboConfig = this.scenario.dubboConfig;
|
||||
this.selected = request;
|
||||
this.$emit("select", request);
|
||||
this.$emit("select", request, this.scenario);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
methods: {
|
||||
changeRoute() {
|
||||
// 解决在列表页面点击 显示全部 无效的问题(点击显示全部后改变路由)
|
||||
this.$router.push(this.index + '/all');
|
||||
this.$router.replace({path: this.index, query: {type: 'all'}});
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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}"},
|
||||
]
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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 表达式",
|
||||
|
|
|
@ -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/entries?key1=Value1&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 表達式格式錯誤",
|
||||
|
|
Loading…
Reference in New Issue