Merge branch 'master' into local-api-delimit

This commit is contained in:
fit2-zhao 2020-11-06 18:24:59 +08:00
commit 8404272115
81 changed files with 978 additions and 568 deletions

View File

@ -38,9 +38,21 @@
- [x] 其他:消息通知
- [x] 其他:报告导出
## v1.4 (已发布)
- [x] 测试跟踪模块编辑测试用例支持上传附件
- [x] 支持上传并引用自定义Jar包
- [x] 接口测试支持TCP协议请求
- [x] 全新的消息通知设置,支持企业微信、钉钉机器人通知
## v1.5 (开发中)
- [ ] 性能测试:优化并发数、持续时间等压力配置方式
- [ ] 性能测试:支持使用了额外插件的 JMX 文件
- [ ] 性能测试:自动修改 csv 等数据文件引用路径
- [ ] 性能测试:优化性能测试报告展示
- [ ] 测试跟踪:支持对接禅道同步缺陷
- [ ] 其他Jenkins 插件支持 pipeline 方式调用
## 规划中
- [ ] 接口测试支持添加 TCP 协议请求
- [ ] 接口测试支持添加 WebSocket 协议请求
- [ ] 接口管理功能
- [ ] 集成云平台动态管理测试资源池

View File

@ -250,7 +250,7 @@
<dependency>
<groupId>io.metersphere</groupId>
<artifactId>jmeter-plugins-dubbo</artifactId>
<version>2.7.11</version>
<version>2.7.12</version>
</dependency>
<!-- LDAP Module -->
@ -362,14 +362,6 @@
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<build>

View File

@ -104,9 +104,8 @@ public class APITestController {
@PostMapping("/delete")
public void delete(@RequestBody DeleteAPITestRequest request) {
String testId = request.getId();
checkownerService.checkApiTestOwner(testId);
apiTestService.delete(testId);
checkownerService.checkApiTestOwner(request.getId());
apiTestService.delete(request);
}
@PostMapping(value = "/run")

View File

@ -8,4 +8,8 @@ import lombok.Setter;
public class DeleteAPITestRequest {
private String id;
/**
* 是否强制删除删除项目时不检查关联关系强制删除资源
*/
private boolean forceDelete = false;
}

View File

@ -9,6 +9,7 @@ public class AssertionType {
public final static String JSON_PATH = "JSONPath";
public final static String JSR223 = "JSR223";
public final static String TEXT = "Text";
public final static String XPATH2 = "XPath2";
private String type;
}

View File

@ -0,0 +1,13 @@
package io.metersphere.api.dto.scenario.assertions;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class AssertionXPath2 extends AssertionType {
private String expression;
public AssertionXPath2() {
setType(AssertionType.XPATH2);
}
}

View File

@ -9,5 +9,6 @@ public class Assertions {
private List<AssertionRegex> regex;
private List<AssertionJsonPath> jsonPath;
private List<AssertionJSR223> jsr223;
private List<AssertionXPath2> xpath2;
private AssertionDuration duration;
}

View File

@ -305,9 +305,11 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) {
ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult();
responseAssertionResult.setMessage(assertionResult.getFailureMessage());
responseAssertionResult.setName(assertionResult.getName());
responseAssertionResult.setPass(!assertionResult.isFailure() && !assertionResult.isError());
if (!responseAssertionResult.isPass()) {
responseAssertionResult.setMessage(assertionResult.getFailureMessage());
}
return responseAssertionResult;
}

View File

@ -61,7 +61,7 @@ public class MsParser extends ApiImportAbstractParser {
Object body = requestObject.get("body");
if (body instanceof JSONArray) {
JSONArray bodies = requestObject.getJSONArray("body");
if (StringUtils.equalsIgnoreCase(requestObject.getString("method"), "POST") && bodies != null) {
if (bodies != null) {
StringBuilder bodyStr = new StringBuilder();
for (int i = 0; i < bodies.size(); i++) {
String tmp = bodies.getString(i);
@ -74,7 +74,7 @@ public class MsParser extends ApiImportAbstractParser {
}
} else if (body instanceof JSONObject) {
JSONObject bodyObj = requestObject.getJSONObject("body");
if (StringUtils.equalsIgnoreCase(requestObject.getString("method"), "POST") && bodyObj != null) {
if (bodyObj != null) {
JSONArray kvs = new JSONArray();
bodyObj.keySet().forEach(key -> {
JSONObject kv = new JSONObject();

View File

@ -193,8 +193,11 @@ public class APITestService {
return extApiTestMapper.getApiTestByProjectId(projectId);
}
public void delete(String testId) {
testCaseService.checkIsRelateTest(testId);
public void delete(DeleteAPITestRequest request) {
String testId = request.getId();
if (!request.isForceDelete()) {
testCaseService.checkIsRelateTest(testId);
}
deleteFileByTestId(testId);
apiReportService.deleteByTestId(testId);
scheduleService.deleteByResourceId(testId);

View File

@ -8,7 +8,7 @@ import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface ExtTestPlanMapper {
List<TestPlanDTO> list(@Param("request") QueryTestPlanRequest params);
List<TestPlanDTOWithMetric> list(@Param("request") QueryTestPlanRequest params);
List<TestPlanDTOWithMetric> listRelate(@Param("request") QueryTestPlanRequest params);

View File

@ -96,7 +96,7 @@
</if>
</sql>
<select id="list" resultMap="BaseResultMap"
<select id="list" resultType="io.metersphere.track.dto.TestPlanDTOWithMetric"
parameterType="io.metersphere.track.request.testcase.QueryTestPlanRequest">
select DISTINCT test_plan.*, user.name as user_name from test_plan
LEFT JOIN user ON user.id = test_plan.principal

View File

@ -31,4 +31,11 @@ public interface ExtTestPlanTestCaseMapper {
List<String> getTestPlanTestCaseIds(String testId);
/**
* 根据项目 ids 查询 TestPlanCaseDTO 列表
* @param ids project id list
* @return List<TestPlanCaseDTO>
*/
List<TestPlanCaseDTO> listTestCaseByProjectIds(@Param("ids") List<String> ids);
}

View File

@ -210,6 +210,14 @@
</foreach>
</if>
</select>
<select id="listTestCaseByProjectIds" resultType="io.metersphere.track.dto.TestPlanCaseDTO">
select distinct * from test_plan_test_case, test_case
where test_plan_test_case.case_id = test_case.id
and test_case.project_id in
<foreach collection="ids" item="id" index="index" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
<select id="listByNode" resultType="io.metersphere.track.dto.TestPlanCaseDTO">
select test_plan_test_case.*, test_case.*
from test_plan_test_case

View File

@ -12,4 +12,11 @@ public interface ExtTestReviewCaseMapper {
List<TestReviewCaseDTO> list(@Param("request") QueryCaseReviewRequest request);
List<String> getStatusByReviewId(String reviewId);
List<String> findRelateTestReviewId(@Param("userId") String userId, @Param("workspaceId") String workspaceId);
/**
* 根据项目 ids 查询 TestReviewCaseDTO 列表
* @param ids project id list
* @return List<TestReviewCaseDTO>
*/
List<TestReviewCaseDTO> listTestCaseByProjectIds(@Param("ids") List<String> ids);
}

View File

@ -195,4 +195,12 @@
where test_case_review_test_case.review_id = #{userId}
and project.workspace_id = #{workspaceId}
</select>
<select id="listTestCaseByProjectIds" resultType="io.metersphere.track.dto.TestReviewCaseDTO">
select distinct * from test_case_review_test_case, test_case
where test_case_review_test_case.case_id = test_case.id
and test_case.project_id in
<foreach collection="ids" item="id" index="index" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
</mapper>

View File

@ -3,7 +3,7 @@ package io.metersphere.commons.constants;
public enum FileType {
JMX(".jmx"), CSV(".csv"), JSON(".json"), PDF(".pdf"),
JPG(".jpg"), PNG(".png"), JPEG(".jpeg"), DOC(".doc"),
XLSX(".xlsx"), DOCX(".docx");
XLSX(".xlsx"), DOCX(".docx"), JAR(".jar");
// 保存后缀
private String suffix;

View File

@ -28,7 +28,7 @@ import java.util.Map;
import java.util.Objects;
@Configuration
@ConditionalOnProperty(prefix="sso",name = "mode", havingValue = "local", matchIfMissing = true)
@ConditionalOnProperty(prefix = "sso", name = "mode", havingValue = "local", matchIfMissing = true)
public class ShiroConfig implements EnvironmentAware {
private Environment env;
@ -43,6 +43,8 @@ public class ShiroConfig implements EnvironmentAware {
shiroFilterFactoryBean.getFilters().put("apikey", new ApiKeyFilter());
Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
ShiroUtils.loadBaseFilterChain(filterChainDefinitionMap);
filterChainDefinitionMap.put("/display/info", "anon");
filterChainDefinitionMap.put("/display/file/*", "anon");
filterChainDefinitionMap.put("/**", "apikey, authc");
return shiroFilterFactoryBean;
}

View File

@ -251,7 +251,7 @@ public class UserController {
* 组织成员列表不分页
*/
@PostMapping("/org/member/list/all")
@RequiresRoles(value = {RoleConstants.ORG_ADMIN, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
@RequiresRoles(value = {RoleConstants.ORG_ADMIN, RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public List<User> getOrgMemberList(@RequestBody QueryOrgMemberRequest request) {
return userService.getOrgMemberList(request);
}

View File

@ -6,6 +6,7 @@ import com.dingtalk.api.request.OapiRobotSendRequest;
import com.dingtalk.api.response.OapiRobotSendResponse;
import com.taobao.api.ApiException;
import io.metersphere.commons.constants.NoticeConstants;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.notice.domain.MessageDetail;
import io.metersphere.notice.domain.UserDetail;
import io.metersphere.service.UserService;
@ -62,7 +63,7 @@ public class DingTaskService {
try {
response = client.execute(request);
} catch (ApiException e) {
e.printStackTrace();
LogUtil.error(e);
}
System.out.println(response.getErrcode());
}

View File

@ -1,9 +1,7 @@
package io.metersphere.notice.service;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.base.domain.LoadTestReportWithBLOBs;
import io.metersphere.base.domain.SystemParameter;
import io.metersphere.base.domain.TestCaseWithBLOBs;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.UserMapper;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.NoticeConstants;
import io.metersphere.commons.constants.ParamConstants;
@ -46,6 +44,8 @@ public class MailService {
private UserService userService;
@Resource
private SystemParameterService systemParameterService;
@Resource
private UserMapper userMapper;
//接口和性能测试
public void sendLoadNotification(MessageDetail messageDetail, LoadTestReportWithBLOBs loadTestReport, String eventType) {
@ -144,7 +144,7 @@ public class MailService {
context.put("testCaseName", testCaseWithBLOBs.getName());
context.put("description", request.getDescription());
context.put("url", baseSystemConfigDTO.getUrl());
context.put("id", testCaseWithBLOBs.getId());
context.put("id", request.getReviewId());
try {
String commentTemplate = IOUtils.toString(this.getClass().getResource("/mail/ReviewComments.html"), StandardCharsets.UTF_8);
sendReviewNotice(addresseeIdList(messageDetail, userIds, eventType), context, commentTemplate);
@ -297,7 +297,8 @@ public class MailService {
Map<String, String> context = new HashMap<>();
BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo();
context.put("url", baseSystemConfigDTO.getUrl());
context.put("creator", reviewRequest.getCreator());
User user = userMapper.selectByPrimaryKey(reviewRequest.getCreator());
context.put("creator", user.getName());
context.put("reviewName", reviewRequest.getName());
context.put("start", start);
context.put("end", end);
@ -328,6 +329,8 @@ public class MailService {
context.put("start", start);
context.put("end", end);
context.put("id", testPlan.getId());
User user = userMapper.selectByPrimaryKey(testPlan.getCreator());
context.put("creator", user.getName());
return context;
}

View File

@ -1,6 +1,7 @@
package io.metersphere.notice.service;
import io.metersphere.commons.constants.NoticeConstants;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.notice.domain.MessageDetail;
import io.metersphere.notice.domain.UserDetail;
import io.metersphere.notice.message.TextMessage;
@ -55,7 +56,7 @@ public class WxChatTaskService {
SendResult result = WxChatbotClient.send(Webhook, message);
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
LogUtil.error(e);
}
}

View File

@ -17,6 +17,7 @@ public class EngineContext {
private Map<String, Object> properties = new HashMap<>();
private Map<String, String> testData = new HashMap<>();
private Map<String, String> env = new HashMap<>();
private Map<String, byte[]> testJars = new HashMap<>();
public String getTestId() {
return testId;
@ -126,4 +127,13 @@ public class EngineContext {
public void setResourceIndex(Integer resourceIndex) {
this.resourceIndex = resourceIndex;
}
public Map<String, byte[]> getTestJars() {
return testJars;
}
public void setTestJars(Map<String, byte[]> testJars) {
this.testJars = testJars;
}
}

View File

@ -63,6 +63,7 @@ public class EngineFactory {
});
List<FileMetadata> csvFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.CSV.name())).collect(Collectors.toList());
List<FileMetadata> jarFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.JAR.name())).collect(Collectors.toList());
final FileContent fileContent = fileService.getFileContent(jmxFile.getId());
if (fileContent == null) {
MSException.throwException(Translator.get("run_load_test_file_content_not_found") + loadTest.getId());
@ -125,6 +126,15 @@ public class EngineFactory {
engineContext.setTestData(data);
}
if (CollectionUtils.isNotEmpty(jarFiles)) {
Map<String, byte[]> data = new HashMap<>();
jarFiles.forEach(jf -> {
FileContent content = fileService.getFileContent(jf.getId());
data.put(jf.getName(), content.getFile());
});
engineContext.setTestJars(data);
}
return engineContext;
}

View File

@ -83,6 +83,7 @@ public class DockerTestEngine extends AbstractEngine {
testRequest.setFileString(content);
testRequest.setImage(JMETER_IMAGE);
testRequest.setTestData(context.getTestData());
testRequest.setTestJars(context.getTestJars());
testRequest.setEnv(context.getEnv());

View File

@ -14,4 +14,5 @@ public class TestRequest extends BaseRequest {
private String image;
private Map<String, String> testData = new HashMap<>();
private Map<String, String> env = new HashMap<>();
private Map<String, byte[]> testJars = new HashMap<>();
}

View File

@ -47,27 +47,26 @@ public class PerformanceNoticeTask {
}
public void registerNoticeTask(LoadTestReportWithBLOBs loadTestReport) {
executorService.submit(() -> {
while (isRunning) {
LoadTestReportWithBLOBs loadTestReportFromDatabase = loadTestReportMapper.selectByPrimaryKey(loadTestReport.getId());
if (StringUtils.equals(loadTestReportFromDatabase.getStatus(), PerformanceTestStatus.Completed.name())) {
isRunning = false;
sendSuccessNotice(loadTestReportFromDatabase);
return;
}
if (StringUtils.equals(loadTestReportFromDatabase.getStatus(), PerformanceTestStatus.Error.name())) {
isRunning = false;
sendFailNotice(loadTestReportFromDatabase);
return;
}
try {
Thread.sleep(1000 * 60);// 每分钟检查 loadtest 的状态
} catch (InterruptedException e) {
LogUtil.error(e);
}
int count = 20;
while (count-- > 0) {
LoadTestReportWithBLOBs loadTestReportFromDatabase = loadTestReportMapper.selectByPrimaryKey(loadTestReport.getId());
if (StringUtils.equals(loadTestReportFromDatabase.getStatus(), PerformanceTestStatus.Completed.name())) {
isRunning = false;
sendSuccessNotice(loadTestReportFromDatabase);
return;
}
});
if (StringUtils.equals(loadTestReportFromDatabase.getStatus(), PerformanceTestStatus.Error.name())) {
isRunning = false;
sendFailNotice(loadTestReportFromDatabase);
return;
}
count--;
try {
Thread.sleep(1000 * 4L);// 每分钟检查 loadtest 的状态
} catch (InterruptedException e) {
LogUtil.error(e);
}
}
}
public void sendSuccessNotice(LoadTestReportWithBLOBs loadTestReport) {

View File

@ -19,7 +19,6 @@ import io.metersphere.dto.LoadTestDTO;
import io.metersphere.dto.ScheduleDao;
import io.metersphere.i18n.Translator;
import io.metersphere.job.sechedule.PerformanceTestJob;
import io.metersphere.notice.domain.NoticeDetail;
import io.metersphere.performance.engine.Engine;
import io.metersphere.performance.engine.EngineFactory;
import io.metersphere.performance.notice.PerformanceNoticeTask;
@ -86,7 +85,9 @@ public class PerformanceTestService {
public void delete(DeleteTestPlanRequest request) {
String testId = request.getId();
testCaseService.checkIsRelateTest(testId);
if (!request.isForceDelete()) {
testCaseService.checkIsRelateTest(testId);
}
LoadTestReportExample loadTestReportExample = new LoadTestReportExample();
loadTestReportExample.createCriteria().andTestIdEqualTo(testId);
@ -233,9 +234,6 @@ public class PerformanceTestService {
if (StringUtils.equals(NoticeConstants.API, loadTestReport.getTriggerMode()) || StringUtils.equals(NoticeConstants.SCHEDULE, loadTestReport.getTriggerMode())) {
performanceNoticeTask.registerNoticeTask(loadTestReport);
}
/*if (StringUtils.equals(NoticeConstants.API, loadTestReport.getTriggerMode()) || StringUtils.equals(NoticeConstants.SCHEDULE, "SCHEDULE")) {
performanceNoticeTask.registerNoticeTask(loadTestReport);
}*/
return engine.getReportId();
}
@ -305,15 +303,10 @@ public class PerformanceTestService {
} catch (MSException e) {
// 启动失败之后清理任务
engine.stop();
LogUtil.error(e);
loadTest.setStatus(PerformanceTestStatus.Error.name());
loadTest.setDescription(e.getMessage());
loadTestMapper.updateByPrimaryKeySelective(loadTest);
LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(engine.getReportId());
if (StringUtils.equals(NoticeConstants.API, loadTestReport.getTriggerMode()) || StringUtils.equals(NoticeConstants.SCHEDULE, loadTestReport.getTriggerMode())) {
performanceNoticeTask.registerNoticeTask(loadTestReport);
}
throw e;
}
}
@ -427,29 +420,28 @@ public class PerformanceTestService {
if (forceStop) {
reportService.deleteReport(reportId);
} else {
LoadTestReport loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId);
LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(loadTestReport.getTestId());
final Engine engine = EngineFactory.createEngine(loadTest);
if (engine == null) {
MSException.throwException(String.format("Stop report fail. create engine failreport ID%s", reportId));
}
reportService.stopEngine(loadTest, engine);
stopEngine(reportId);
// 停止测试之后设置报告的状态
reportService.updateStatus(reportId, PerformanceTestStatus.Completed.name());
List<NoticeDetail> noticeList = null;
if (loadTestReport.getTriggerMode().equals("SCHEDULE")) {
/* try {
noticeList = noticeService.queryNotice(loadTest.getId());
mailService.sendPerformanceNotification(noticeList, loadTestReport.getStatus(), loadTest, loadTestReport.getId());
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
}*/
}
}
}
public void stopErrorTest(String reportId) {
stopEngine(reportId);
// 停止测试之后设置报告的状态
reportService.updateStatus(reportId, PerformanceTestStatus.Error.name());
}
private void stopEngine(String reportId) {
LoadTestReport loadTestReport = loadTestReportMapper.selectByPrimaryKey(reportId);
LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(loadTestReport.getTestId());
final Engine engine = EngineFactory.createEngine(loadTest);
if (engine == null) {
MSException.throwException(String.format("Stop report fail. create engine failreport ID%s", reportId));
}
reportService.stopEngine(loadTest, engine);
}
public List<ScheduleDao> listSchedule(QueryScheduleRequest request) {
request.setEnable(true);
List<ScheduleDao> schedules = scheduleService.list(request);

View File

@ -241,7 +241,7 @@ public class ReportService {
}
}
public LoadTestReport getReport(String reportId) {
public LoadTestReportWithBLOBs getReport(String reportId) {
return loadTestReportMapper.selectByPrimaryKey(reportId);
}

View File

@ -8,7 +8,7 @@ import io.metersphere.base.mapper.ext.*;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.dto.LoadTestDTO;
import io.metersphere.i18n.Translator;
import io.metersphere.track.dto.TestPlanDTO;
import io.metersphere.track.dto.TestPlanDTOWithMetric;
import io.metersphere.track.request.testplan.QueryTestPlanRequest;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@ -81,7 +81,7 @@ public class CheckOwnerService {
io.metersphere.track.request.testcase.QueryTestPlanRequest request = new io.metersphere.track.request.testcase.QueryTestPlanRequest();
request.setWorkspaceId(workspaceId);
request.setId(planId);
List<TestPlanDTO> list = extTestPlanMapper.list(request);
List<TestPlanDTOWithMetric> list = extTestPlanMapper.list(request);
if (CollectionUtils.size(list) != 1) {
throw new UnauthorizedException(Translator.get("check_owner_plan"));
}

View File

@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@ -141,4 +142,12 @@ public class FileService {
example.createCriteria().andIdIn(fileIds);
return fileMetadataMapper.selectByExample(example);
}
public void deleteFileById(String fileId) {
deleteFileByIds(Collections.singletonList(fileId));
}
public FileMetadata getFileMetadataById(String fileId) {
return fileMetadataMapper.selectByPrimaryKey(fileId);
}
}

View File

@ -1,5 +1,6 @@
package io.metersphere.service;
import io.metersphere.api.dto.DeleteAPITestRequest;
import io.metersphere.api.dto.QueryAPITestRequest;
import io.metersphere.api.service.APITestService;
import io.metersphere.base.domain.LoadTest;
@ -96,6 +97,7 @@ public class ProjectService {
loadTestIdList.forEach(loadTestId -> {
DeleteTestPlanRequest deleteTestPlanRequest = new DeleteTestPlanRequest();
deleteTestPlanRequest.setId(loadTestId);
deleteTestPlanRequest.setForceDelete(true);
performanceTestService.delete(deleteTestPlanRequest);
});
@ -122,7 +124,10 @@ public class ProjectService {
QueryAPITestRequest request = new QueryAPITestRequest();
request.setProjectId(projectId);
apiTestService.list(request).forEach(test -> {
apiTestService.delete(test.getId());
DeleteAPITestRequest deleteAPITestRequest = new DeleteAPITestRequest();
deleteAPITestRequest.setId(test.getId());
deleteAPITestRequest.setForceDelete(true);
apiTestService.delete(deleteAPITestRequest);
});
}

View File

@ -73,8 +73,14 @@ public class TestResourcePoolService {
LoadTestExample example = new LoadTestExample();
example.createCriteria()
.andTestResourcePoolIdEqualTo(testResourcePoolId);
if (loadTestMapper.countByExample(example) > 0) {
MSException.throwException(Translator.get("test_resource_pool_is_use"));
List<LoadTest> loadTests = loadTestMapper.selectByExample(example);
StringBuilder loadTestNames = new StringBuilder();
if (loadTests.size() > 0) {
for (LoadTest loadTest : loadTests) {
loadTestNames = loadTestNames.append(loadTest.getName()).append(",");
}
String str = loadTestNames.substring(0, loadTestNames.length() - 1);
MSException.throwException(Translator.get("load_test") + " " + str + " " + Translator.get("test_resource_pool_is_use"));
}
}

View File

@ -37,7 +37,7 @@ public class TestPlanController {
CheckOwnerService checkOwnerService;
@PostMapping("/list/{goPage}/{pageSize}")
public Pager<List<TestPlanDTO>> list(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryTestPlanRequest request) {
public Pager<List<TestPlanDTOWithMetric>> list(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryTestPlanRequest request) {
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
request.setWorkspaceId(currentWorkspaceId);
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);

View File

@ -1,4 +1,13 @@
package io.metersphere.track.request.testplan;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class DeleteTestPlanRequest extends TestPlanRequest {
/**
* 是否强制删除删除项目时不检查关联关系强制删除资源
*/
private boolean forceDelete = false;
}

View File

@ -7,5 +7,6 @@ import lombok.Setter;
@Getter
@Setter
public class SaveCommentRequest extends TestCaseComment {
private String reviewId;
}

View File

@ -8,6 +8,7 @@ import io.metersphere.base.mapper.ext.ExtTestCaseReviewMapper;
import io.metersphere.base.mapper.ext.ExtTestReviewCaseMapper;
import io.metersphere.commons.constants.NoticeConstants;
import io.metersphere.commons.constants.TestCaseReviewStatus;
import io.metersphere.commons.constants.TestPlanStatus;
import io.metersphere.commons.constants.TestReviewCaseStatus;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.user.SessionUser;
@ -184,28 +185,31 @@ public class TestCaseReviewService {
testCaseReview.setUpdateTime(System.currentTimeMillis());
checkCaseReviewExist(testCaseReview);
testCaseReviewMapper.updateByPrimaryKeySelective(testCaseReview);
List<String> userIds=new ArrayList<>();
List<String> userIds = new ArrayList<>();
userIds.addAll(testCaseReview.getUserIds());
try {
String context = getReviewContext(testCaseReview, NoticeConstants.CREATE);
MessageSettingDetail messageSettingDetail = noticeService.searchMessage();
List<MessageDetail> taskList = messageSettingDetail.getReviewTask();
taskList.forEach(r -> {
switch (r.getType()) {
case NoticeConstants.NAIL_ROBOT:
dingTaskService.sendNailRobot(r, userIds, context, NoticeConstants.CREATE);
break;
case NoticeConstants.WECHAT_ROBOT:
wxChatTaskService.sendWechatRobot(r, userIds, context, NoticeConstants.CREATE);
break;
case NoticeConstants.EMAIL:
mailService.sendReviewerNotice(r, userIds, testCaseReview, NoticeConstants.CREATE);
break;
}
});
} catch (Exception e) {
LogUtil.error(e);
if (StringUtils.equals(TestPlanStatus.Completed.name(), testCaseReview.getStatus())) {
try {
String context = getReviewContext(testCaseReview, NoticeConstants.UPDATE);
MessageSettingDetail messageSettingDetail = noticeService.searchMessage();
List<MessageDetail> taskList = messageSettingDetail.getReviewTask();
taskList.forEach(r -> {
switch (r.getType()) {
case NoticeConstants.NAIL_ROBOT:
dingTaskService.sendNailRobot(r, userIds, context, NoticeConstants.UPDATE);
break;
case NoticeConstants.WECHAT_ROBOT:
wxChatTaskService.sendWechatRobot(r, userIds, context, NoticeConstants.UPDATE);
break;
case NoticeConstants.EMAIL:
mailService.sendReviewerNotice(r, userIds, testCaseReview, NoticeConstants.UPDATE);
break;
}
});
} catch (Exception e) {
LogUtil.error(e);
}
}
}
private void editCaseReviewer(SaveTestCaseReviewRequest testCaseReview) {
@ -480,7 +484,7 @@ public class TestCaseReviewService {
request.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
request.setReviewIds(extTestReviewCaseMapper.findRelateTestReviewId(user.getId(), SessionUtils.getCurrentWorkspaceId()));
List<String> projectIds = extProjectMapper.getProjectIdByWorkspaceId(SessionUtils.getCurrentOrganizationId());
List<String> projectIds = extProjectMapper.getProjectIdByWorkspaceId(SessionUtils.getCurrentWorkspaceId());
List<TestReviewDTOWithMetric> testReviews = extTestCaseReviewMapper.listRelate(request);
@ -549,14 +553,17 @@ public class TestCaseReviewService {
return name;
}
public List<TestReviewCaseDTO> listTestCaseByProjectIds(List<String> projectIds) {
QueryCaseReviewRequest request = new QueryCaseReviewRequest();
request.setProjectIds(projectIds);
return extTestReviewCaseMapper.list(request);
private List<TestReviewCaseDTO> listTestCaseByProjectIds(List<String> projectIds) {
if (CollectionUtils.isEmpty(projectIds)) {
return new ArrayList<>();
}
return extTestReviewCaseMapper.listTestCaseByProjectIds(projectIds);
}
/*编辑,新建,完成,删除通知内容*/
private static String getReviewContext(SaveTestCaseReviewRequest reviewRequest, String type) {
private String getReviewContext(SaveTestCaseReviewRequest reviewRequest, String type) {
User user = userMapper.selectByPrimaryKey(reviewRequest.getCreator());
Long startTime = reviewRequest.getCreateTime();
Long endTime = reviewRequest.getEndTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@ -572,11 +579,11 @@ public class TestCaseReviewService {
}
String context = "";
if (StringUtils.equals(NoticeConstants.CREATE, type)) {
context = "测试评审任务通知:" + reviewRequest.getCreator() + "发起的" + "'" + reviewRequest.getName() + "'" + "待开始,计划开始时间是" + start + "计划结束时间为" + end + "请跟进";
context = "测试评审任务通知:" + user.getName() + "发起的" + "'" + reviewRequest.getName() + "'" + "待开始,计划开始时间是" + start + "计划结束时间为" + end + "请跟进";
} else if (StringUtils.equals(NoticeConstants.UPDATE, type)) {
context = "测试评审任务通知:" + reviewRequest.getCreator() + "发起的" + "'" + reviewRequest.getName() + "'" + "已完成,计划开始时间是" + start + "计划结束时间为" + end + "已完成";
context = "测试评审任务通知:" + user.getName() + "发起的" + "'" + reviewRequest.getName() + "'" + "已完成,计划开始时间是" + start + "计划结束时间为" + end + "已完成";
} else if (StringUtils.equals(NoticeConstants.DELETE, type)) {
context = "测试评审任务通知:" + reviewRequest.getCreator() + "发起的" + "'" + reviewRequest.getName() + "'" + "计划开始时间是" + start + "计划结束时间为" + end + "已删除";
context = "测试评审任务通知:" + user.getName() + "发起的" + "'" + reviewRequest.getName() + "'" + "计划开始时间是" + start + "计划结束时间为" + end + "已删除";
}
return context;

View File

@ -457,7 +457,7 @@ public class TestCaseService {
if (t.getMethod().equals("manual")) {
String steps = t.getSteps();
String setp = "";
if (steps.contains("null")) {
if (steps.contains("null") && !steps.contains("\"null\"")) {
setp = steps.replace("null", "\"\"");
} else {
setp = steps;
@ -546,7 +546,7 @@ public class TestCaseService {
for (TestCase testCase : testCases) {
caseName = caseName.append(testCase.getName()).append(",");
}
String str = caseName.toString().substring(0, caseName.length() - 1);
String str = caseName.substring(0, caseName.length() - 1);
MSException.throwException(Translator.get("related_case_del_fail_prefix") + " " + str + " " + Translator.get("related_case_del_fail_suffix"));
}
}

View File

@ -93,6 +93,8 @@ public class TestPlanService {
DingTaskService dingTaskService;
@Resource
WxChatTaskService wxChatTaskService;
@Resource
UserMapper userMapper;
public void addTestPlan(AddTestPlanRequest testPlan) {
if (getTestPlanByName(testPlan.getName()).size() > 0) {
@ -165,20 +167,20 @@ public class TestPlanService {
//已完成写入实际完成时间
testPlan.setActualEndTime(System.currentTimeMillis());
try {
BeanUtils.copyBean(testPlans, testPlan);
String context = getTestPlanContext(testPlans, NoticeConstants.CREATE);
BeanUtils.copyBean(testPlans, getTestPlan(testPlan.getId()));
String context = getTestPlanContext(testPlans, NoticeConstants.UPDATE);
MessageSettingDetail messageSettingDetail = noticeService.searchMessage();
List<MessageDetail> taskList = messageSettingDetail.getReviewTask();
List<MessageDetail> taskList = messageSettingDetail.getTestCasePlanTask();
taskList.forEach(r -> {
switch (r.getType()) {
case NoticeConstants.NAIL_ROBOT:
dingTaskService.sendNailRobot(r, userIds, context, NoticeConstants.CREATE);
dingTaskService.sendNailRobot(r, userIds, context, NoticeConstants.UPDATE);
break;
case NoticeConstants.WECHAT_ROBOT:
wxChatTaskService.sendWechatRobot(r, userIds, context, NoticeConstants.CREATE);
wxChatTaskService.sendWechatRobot(r, userIds, context, NoticeConstants.UPDATE);
break;
case NoticeConstants.EMAIL:
mailService.sendTestPlanStartNotice(r, userIds, testPlans, NoticeConstants.CREATE);
mailService.sendTestPlanStartNotice(r, userIds, testPlans, NoticeConstants.UPDATE);
break;
}
});
@ -279,9 +281,46 @@ public class TestPlanService {
testPlanTestCaseMapper.deleteByExample(testPlanTestCaseExample);
}
public List<TestPlanDTO> listTestPlan(QueryTestPlanRequest request) {
private void calcTestPlanRate(List<TestPlanDTOWithMetric> testPlans) {
List<String> projectIds = extProjectMapper.getProjectIdByWorkspaceId(SessionUtils.getCurrentWorkspaceId());
Map<String, List<TestPlanCaseDTO>> testCaseMap = new HashMap<>();
listTestCaseByProjectIds(projectIds).forEach(testCase -> {
List<TestPlanCaseDTO> list = testCaseMap.get(testCase.getPlanId());
if (list == null) {
list = new ArrayList<>();
list.add(testCase);
testCaseMap.put(testCase.getPlanId(), list);
} else {
list.add(testCase);
}
});
testPlans.forEach(testPlan -> {
List<TestPlanCaseDTO> testCases = testCaseMap.get(testPlan.getId());
testPlan.setTested(0);
testPlan.setPassed(0);
testPlan.setTotal(0);
if (testCases != null) {
testPlan.setTotal(testCases.size());
testCases.forEach(testCase -> {
if (!StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Prepare.name())
&& !StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Underway.name())) {
testPlan.setTested(testPlan.getTested() + 1);
if (StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Pass.name())) {
testPlan.setPassed(testPlan.getPassed() + 1);
}
}
});
}
testPlan.setPassRate(MathUtils.getPercentWithDecimal(testPlan.getTested() == 0 ? 0 : testPlan.getPassed() * 1.0 / testPlan.getTested()));
testPlan.setTestRate(MathUtils.getPercentWithDecimal(testPlan.getTotal() == 0 ? 0 : testPlan.getTested() * 1.0 / testPlan.getTotal()));
});
}
public List<TestPlanDTOWithMetric> listTestPlan(QueryTestPlanRequest request) {
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
return extTestPlanMapper.list(request);
List<TestPlanDTOWithMetric> testPlans = extTestPlanMapper.list(request);
calcTestPlanRate(testPlans);
return testPlans;
}
public List<TestPlanDTO> listTestPlanByProject(QueryTestPlanRequest request) {
@ -359,49 +398,12 @@ public class TestPlanService {
public List<TestPlanDTOWithMetric> listRelateAllPlan() {
SessionUser user = SessionUtils.getUser();
QueryTestPlanRequest request = new QueryTestPlanRequest();
request.setPrincipal(user.getId());
request.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
request.setPlanIds(extTestPlanTestCaseMapper.findRelateTestPlanId(user.getId(), SessionUtils.getCurrentWorkspaceId()));
List<String> projectIds = extProjectMapper.getProjectIdByWorkspaceId(SessionUtils.getCurrentOrganizationId());
List<TestPlanDTOWithMetric> testPlans = extTestPlanMapper.listRelate(request);
Map<String, List<TestPlanCaseDTO>> testCaseMap = new HashMap<>();
listTestCaseByProjectIds(projectIds).forEach(testCase -> {
List<TestPlanCaseDTO> list = testCaseMap.get(testCase.getPlanId());
if (list == null) {
list = new ArrayList<>();
list.add(testCase);
testCaseMap.put(testCase.getPlanId(), list);
} else {
list.add(testCase);
}
});
testPlans.forEach(testPlan -> {
List<TestPlanCaseDTO> testCases = testCaseMap.get(testPlan.getId());
testPlan.setTested(0);
testPlan.setPassed(0);
testPlan.setTotal(0);
if (testCases != null) {
testPlan.setTotal(testCases.size());
testCases.forEach(testCase -> {
if (!StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Prepare.name())
&& !StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Underway.name())) {
testPlan.setTested(testPlan.getTested() + 1);
if (StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Pass.name())) {
testPlan.setPassed(testPlan.getPassed() + 1);
}
}
});
}
testPlan.setPassRate(MathUtils.getPercentWithDecimal(testPlan.getTested() == 0 ? 0 : testPlan.getPassed() * 1.0 / testPlan.getTested()));
testPlan.setTestRate(MathUtils.getPercentWithDecimal(testPlan.getTotal() == 0 ? 0 : testPlan.getTested() * 1.0 / testPlan.getTotal()));
});
calcTestPlanRate(testPlans);
return testPlans;
}
@ -411,10 +413,11 @@ public class TestPlanService {
return testPlanTestCaseService.list(request);
}
public List<TestPlanCaseDTO> listTestCaseByProjectIds(List<String> projectIds) {
QueryTestPlanCaseRequest request = new QueryTestPlanCaseRequest();
request.setProjectIds(projectIds);
return extTestPlanTestCaseMapper.list(request);
private List<TestPlanCaseDTO> listTestCaseByProjectIds(List<String> projectIds) {
if (CollectionUtils.isEmpty(projectIds)) {
return new ArrayList<>();
}
return extTestPlanTestCaseMapper.listTestCaseByProjectIds(projectIds);
}
public TestCaseReportMetricDTO getMetric(String planId) {
@ -527,13 +530,14 @@ public class TestPlanService {
for (Project project : projects) {
stringBuilder.append(project.getName()).append("");
}
projectName = stringBuilder.toString().substring(0, stringBuilder.length() - 1);
projectName = stringBuilder.substring(0, stringBuilder.length() - 1);
}
return projectName;
}
private static String getTestPlanContext(AddTestPlanRequest testPlan, String type) {
private String getTestPlanContext(AddTestPlanRequest testPlan, String type) {
User user = userMapper.selectByPrimaryKey(testPlan.getCreator());
Long startTime = testPlan.getPlannedStartTime();
Long endTime = testPlan.getPlannedEndTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@ -553,11 +557,11 @@ public class TestPlanService {
}
String context = "";
if (StringUtils.equals(NoticeConstants.CREATE, type)) {
context = "测试计划任务通知:" + testPlan.getCreator() + "创建的" + "'" + testPlan.getName() + "'" + "待开始,计划开始时间是" + start + "计划结束时间为" + end + "请跟进";
context = "测试计划任务通知:" + user.getName() + "创建的" + "'" + testPlan.getName() + "'" + "待开始,计划开始时间是" + start + "计划结束时间为" + end + "请跟进";
} else if (StringUtils.equals(NoticeConstants.UPDATE, type)) {
context = "测试计划任务通知:" + testPlan.getCreator() + "创建的" + "'" + testPlan.getName() + "'" + "已完成,计划开始时间是" + start + "计划结束时间为" + end + "已完成";
context = "测试计划任务通知:" + user.getName() + "创建的" + "'" + testPlan.getName() + "'" + "已完成,计划开始时间是" + start + "计划结束时间为" + end + "已完成";
} else if (StringUtils.equals(NoticeConstants.DELETE, type)) {
context = "测试计划任务通知:" + testPlan.getCreator() + "创建的" + "'" + testPlan.getName() + "'" + "计划开始时间是" + start + "计划结束时间为" + end + "已删除";
context = "测试计划任务通知:" + user.getName() + "创建的" + "'" + testPlan.getName() + "'" + "计划开始时间是" + start + "计划结束时间为" + end + "已删除";
}
return context;
}

View File

@ -1,8 +1,9 @@
package io.metersphere.websocket;
import io.metersphere.base.domain.LoadTestReport;
import io.metersphere.base.domain.LoadTestReportWithBLOBs;
import io.metersphere.commons.constants.PerformanceTestStatus;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.performance.service.PerformanceTestService;
import io.metersphere.performance.service.ReportService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@ -18,12 +19,18 @@ import java.io.IOException;
public class ReportWebSocket {
private static ReportService reportService;
private static PerformanceTestService performanceTestService;
@Resource
public void setReportService(ReportService reportService) {
ReportWebSocket.reportService = reportService;
}
@Resource
public void setPerformanceTestService(PerformanceTestService performanceTestService) {
ReportWebSocket.performanceTestService = performanceTestService;
}
/**
* 开启连接的操作
*/
@ -78,12 +85,19 @@ public class ReportWebSocket {
public void run() {
while (stopMe) {
try {
LoadTestReport report = reportService.getReport(reportId);
if (report == null || StringUtils.equalsAny(report.getStatus(), PerformanceTestStatus.Completed.name(), PerformanceTestStatus.Error.name())) {
LoadTestReportWithBLOBs report = reportService.getReport(reportId);
if (report == null || StringUtils.equalsAny(report.getStatus(), PerformanceTestStatus.Completed.name())) {
this.stopMe();
session.close();
break;
}
if (StringUtils.equals(report.getStatus(), PerformanceTestStatus.Error.name())) {
this.stopMe();
session.getBasicRemote().sendText("Error: " + report.getDescription());
performanceTestService.stopErrorTest(reportId);
session.close();
break;
}
if (!session.isOpen()) {
return;
}

@ -1 +1 @@
Subproject commit ee74568be0beba46da19616f5832e83f9164c688
Subproject commit 24047fea950a74f7848a9fdaa857a22b884c4ce2

View File

@ -1,2 +1,2 @@
alter table message_task
add create_time bigint(13) null;
ALTER TABLE message_task
ADD create_time bigint(13) DEFAULT 0;

View File

@ -1,3 +0,0 @@
alter table message_task
alter column create_time set default 0;

View File

@ -0,0 +1,10 @@
INSERT INTO system_parameter (param_key, param_value, type, sort)
VALUES ('ui.loginImage', NULL, 'file', 3);
INSERT INTO system_parameter (param_key, param_value, type, sort)
VALUES ('ui.loginLogo', NULL, 'file', 2);
INSERT INTO system_parameter (param_key, param_value, type, sort)
VALUES ('ui.loginTitle', '', 'text', 4);
INSERT INTO system_parameter (param_key, param_value, type, sort)
VALUES ('ui.logo', NULL, 'file', 1);
INSERT INTO system_parameter (param_key, param_value, type, sort)
VALUES ('ui.title', '', 'text', 5);

View File

@ -57,7 +57,8 @@ workspace_not_exists=Workspace is not exists
test_resource_pool_id_is_null=Test Resource Pool ID cannot be null
test_resource_pool_name_is_null=Test Resource Pool name cannot be null
test_resource_pool_name_already_exists=The test resource pool name already exists
test_resource_pool_is_use=The test resource pool is in use and cannot be deleted
load_test=Load Test
test_resource_pool_is_use=This resource pool is in use and cannot be deleted
#project
project_name_is_null=Project name cannot be null
project_name_already_exists=The project name already exists

View File

@ -57,7 +57,8 @@ workspace_not_exists=工作空间不存在
test_resource_pool_id_is_null=资源池ID不能为空
test_resource_pool_name_is_null=资源池名称不能为空
test_resource_pool_name_already_exists=资源池名称已存在
test_resource_pool_is_use=资源池正在使用中,无法删除
load_test=性能测试
test_resource_pool_is_use=正在使用此资源池,无法删除
#project
project_name_is_null=项目名称不能为空
project_name_already_exists=项目名称已存在

View File

@ -57,7 +57,8 @@ workspace_not_exists=工作空間不存在
test_resource_pool_id_is_null=資源池ID不能為空
test_resource_pool_name_is_null=資源池名稱不能為空
test_resource_pool_name_already_exists=資源池名稱已存在
test_resource_pool_is_use=資源池正在使用中,無法刪除
load_test=性能測試
test_resource_pool_is_use=正在使用此資源池,無法刪除
#project
project_name_is_null=項目名稱不能為空
project_name_already_exists=項目名稱已存在

View File

@ -10,7 +10,7 @@
${testCaseName}<br/>
添加评论:${description}<br/>
点击下面链接进入用例评审页面</p>
<a href="${url}/#/track/review/view">${url}/#/track/review/view</a>
<a href="${url}/#/track/review/view/${id}">${url}/#/track/review/view/${id}</a>
</div>
</body>
</html>

View File

@ -1215,7 +1215,7 @@ view.results.tree.renderers_order=.RenderAsText,.RenderAsRegexp,.RenderAsBoundar
# All entries will be added to the class path of the system class loader
# and also to the path of the JMeter internal loader.
# Paths with spaces may cause problems for the JVM
#user.classpath=../classes;../lib
user.classpath=/test/
# List of directories (separated by ;) that JMeter will search for utility
# and plugin dependency classes.

View File

@ -7,7 +7,8 @@
</el-row>
<el-row id="header-top" type="flex" justify="space-between" align="middle">
<el-col :span="12">
<a class="logo"/>
<img v-if="logoId" :src="'/display/file/' + logoId" style="width: 156px;height: 37px;" alt="">
<a v-else class="logo"/>
<ms-top-menus/>
</el-col>
@ -24,107 +25,113 @@
</template>
<script>
import MsTopMenus from "./components/common/head/HeaderTopMenus";
import MsView from "./components/common/router/View";
import MsUser from "./components/common/head/HeaderUser";
import MsHeaderOrgWs from "./components/common/head/HeaderOrgWs";
import MsLanguageSwitch from "./components/common/head/LanguageSwitch";
import {saveLocalStorage} from "../common/js/utils";
import MsTopMenus from "./components/common/head/HeaderTopMenus";
import MsView from "./components/common/router/View";
import MsUser from "./components/common/head/HeaderUser";
import MsHeaderOrgWs from "./components/common/head/HeaderOrgWs";
import MsLanguageSwitch from "./components/common/head/LanguageSwitch";
import {saveLocalStorage} from "@/common/js/utils";
const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/);
const header = requireComponent.keys().length > 0 ? requireComponent("./license/LicenseMessage.vue") : {};
const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/);
const header = requireComponent.keys().length > 0 ? requireComponent("./license/LicenseMessage.vue") : {};
const display = requireComponent.keys().length > 0 ? requireComponent("./display/Display.vue") : {};
export default {
name: 'app',
props: {},
data() {
return {
licenseHeader: null,
auth: false,
header: {},
}
},
beforeCreate() {
this.$get("/isLogin").then(response => {
if (response.data.success) {
this.$setLang(response.data.data.language);
saveLocalStorage(response.data);
this.auth = true;
//
if (header.default !== undefined) {
this.licenseHeader = "LicenseMessage";
}
} else {
window.location.href = "/login"
}
}).catch(() => {
window.location.href = "/login"
});
},
components: {
MsLanguageSwitch,
MsUser,
MsView,
MsTopMenus,
MsHeaderOrgWs,
"LicenseMessage": header.default
export default {
name: 'app',
props: {},
data() {
return {
licenseHeader: null,
auth: false,
header: {},
logoId: '_blank',
}
},
beforeCreate() {
this.$get("/isLogin").then(response => {
if (response.data.success) {
this.$setLang(response.data.data.language);
saveLocalStorage(response.data);
this.auth = true;
//
if (header.default !== undefined) {
this.licenseHeader = "LicenseMessage";
}
//
if (display.default !== undefined) {
display.default.valid(this);
}
} else {
window.location.href = "/login"
}
}).catch(() => {
window.location.href = "/login"
});
},
components: {
MsLanguageSwitch,
MsUser,
MsView,
MsTopMenus,
MsHeaderOrgWs,
"LicenseMessage": header.default
}
}
</script>
<style scoped>
#header-top {
width: 100%;
padding: 0 10px;
background-color: rgb(44, 42, 72);
color: rgb(245, 245, 245);
font-size: 14px;
}
#header-top {
width: 100%;
padding: 0 10px;
background-color: rgb(44, 42, 72);
color: rgb(245, 245, 245);
font-size: 14px;
}
.logo {
width: 156px;
margin-bottom: 0;
border: 0;
margin-right: 20px;
display: inline-block;
line-height: 37px;
background-size: 156px 30px;
box-sizing: border-box;
height: 37px;
background-repeat: no-repeat;
background-position: 50% center;
background-image: url("../assets/logo-light-MeterSphere.svg");
}
.logo {
width: 156px;
margin-bottom: 0;
border: 0;
margin-right: 20px;
display: inline-block;
line-height: 37px;
background-size: 156px 30px;
box-sizing: border-box;
height: 37px;
background-repeat: no-repeat;
background-position: 50% center;
background-image: url("../assets/logo-light-MeterSphere.svg");
}
.menus > * {
color: inherit;
padding: 0;
max-width: 180px;
white-space: pre;
cursor: pointer;
line-height: 40px;
}
.menus > * {
color: inherit;
padding: 0;
max-width: 180px;
white-space: pre;
cursor: pointer;
line-height: 40px;
}
.header-top-menus {
display: inline-block;
border: 0;
}
.header-top-menus {
display: inline-block;
border: 0;
}
.menus > a {
padding-right: 15px;
text-decoration: none;
}
.menus > a {
padding-right: 15px;
text-decoration: none;
}
.align-right {
float: right;
}
.align-right {
float: right;
}
.license-head {
height: 30px;
background: #BA331B;
text-align: center;
line-height: 30px;
color: white;
}
.license-head {
height: 30px;
background: #BA331B;
text-align: center;
line-height: 30px;
color: white;
}
</style>

View File

@ -4,15 +4,25 @@
<el-row :gutter="10" type="flex" align="middle" class="info">
<el-col :span="4">
<div class="method">
{{request.method}}
{{ request.method }}
</div>
</el-col>
<el-col :span="20">
<div class="name">{{request.name}}</div>
<el-col :span="15">
<div class="name">{{ request.name }}</div>
<el-tooltip effect="dark" :content="request.url" placement="bottom" :open-delay="800">
<div class="url">{{request.url}}</div>
<div class="url">{{ request.url }}</div>
</el-tooltip>
</el-col>
<el-col :span="5">
<div class="success">
<el-tag size="mini" type="success" v-if="request.success">
{{ $t('api_report.success') }}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{ $t('api_report.fail') }}
</el-tag>
</div>
</el-col>
</el-row>
</div>
</div>

View File

@ -0,0 +1,72 @@
<template>
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col>
<el-input :disabled="isReadOnly" v-model="xPath2.expression" maxlength="200" size="small" show-word-limit
:placeholder="$t('api_test.request.extract.xpath_expression')"/>
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>
{{ $t('api_test.request.assertions.add') }}
</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import {XPath2} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionXPath2",
props: {
xPath2: {
type: XPath2,
default: () => {
return new XPath2();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
methods: {
add: function () {
this.list.push(this.getXPath2());
this.callback();
},
remove: function () {
this.list.splice(this.index, 1);
},
getXPath2() {
return new XPath2(this.xPath2);
},
}
}
</script>
<style scoped>
.assertion-select {
width: 250px;
}
.assertion-item {
width: 100%;
}
.assertion-btn {
text-align: center;
width: 60px;
}
</style>

View File

@ -8,6 +8,7 @@
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
<el-option :label="'JSONPath'" :value="options.JSON_PATH"/>
<el-option :label="'XPath'" :value="options.XPATH2"/>
<el-option :label="$t('api_test.request.assertions.response_time')" :value="options.DURATION"/>
<el-option :label="$t('api_test.request.assertions.jsr223')" :value="options.JSR223"/>
</el-select>
@ -16,6 +17,7 @@
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/>
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/>
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/>
<ms-api-assertion-x-path2 :is-read-only="isReadOnly" :list="assertions.xpath2" v-if="type === options.XPATH2" :callback="after"/>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
v-if="type === options.DURATION" :callback="after"/>
<ms-api-assertion-jsr223 :is-read-only="isReadOnly" :list="assertions.jsr223" v-if="type === options.JSR223" :callback="after"/>
@ -52,11 +54,13 @@
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223";
import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList";
import MsApiAssertionXPath2 from "./ApiAssertionXPath2";
export default {
name: "MsApiAssertions",
components: {
MsApiAssertionXPath2,
MsApiAssertionJsr223,
MsApiJsonpathSuggestList,
MsApiAssertionJsonPath,

View File

@ -20,6 +20,16 @@
</div>
</div>
<div class="assertion-item-editing x_path" v-if="assertions.xpath2.length > 0">
<div>
{{ 'XPath' }}
</div>
<div class="regex-item" v-for="(xPath, index) in assertions.xpath2" :key="index">
<ms-api-assertion-x-path2 :is-read-only="isReadOnly" :list="assertions.xpath2"
:x-path2="xPath" :edit="true" :index="index"/>
</div>
</div>
<div class="assertion-item-editing jsr223" v-if="assertions.jsr223.length > 0">
<div>
{{ $t("api_test.request.assertions.script") }}
@ -47,11 +57,14 @@ import MsApiAssertionDuration from "./ApiAssertionDuration";
import {Assertions} from "../../model/ScenarioModel";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223";
import MsApiAssertionXPath2 from "./ApiAssertionXPath2";
export default {
name: "MsApiAssertionsEdit",
components: {MsApiAssertionJsr223, MsApiAssertionJsonPath, MsApiAssertionDuration, MsApiAssertionRegex},
components: {
MsApiAssertionXPath2,
MsApiAssertionJsr223, MsApiAssertionJsonPath, MsApiAssertionDuration, MsApiAssertionRegex},
props: {
assertions: Assertions,
@ -92,6 +105,10 @@ export default {
border-left: 2px solid #1FDD02;
}
.assertion-item-editing.x_path {
border-left: 2px solid #fca130;
}
.regex-item {
margin-top: 10px;
}

View File

@ -162,9 +162,11 @@ export default {
break;
}
},
getColor(enable, method) {
if (enable) {
getColor(requestEnable, method) {
if (this.scenario.enable && requestEnable) {
return this.methodColorMap.get(method);
} else {
return '#909399';
}
},
select(request) {

View File

@ -18,7 +18,7 @@
export default {
name: "MsApiRequestForm",
components: {MsApiSqlRequestForm, MsRequestResultTail, MsScenarioResults, MsApiDubboRequestForm, MsApiHttpRequestForm},
components: {MsApiSqlRequestForm, MsRequestResultTail, MsScenarioResults, MsApiDubboRequestForm, MsApiHttpRequestForm, MsApiTcpRequestForm},
props: {
scenario: Scenario,
request: Request,

View File

@ -439,6 +439,16 @@ export class JSONPathAssertion extends DefaultTestElement {
}
}
export class XPath2Assertion extends DefaultTestElement {
constructor(testName, xPath) {
super('XPath2Assertion', 'XPath2AssertionGui', 'XPath2Assertion', testName);
this.xPath = xPath || {};
this.stringProp('XPath.xpath', this.xPath.expression);
this.stringProp('XPath.namespace');
this.boolProp('XPath.negate', false);
}
}
export class ResponseCodeAssertion extends ResponseAssertion {
constructor(testName, type, value, assumeSuccess, message) {
let assertion = {

View File

@ -25,7 +25,7 @@ import {
ThreadGroup,
XPath2Extractor,
IfController as JMXIfController,
ConstantTimer as JMXConstantTimer, TCPSampler, JSR223Assertion,
ConstantTimer as JMXConstantTimer, TCPSampler, JSR223Assertion, XPath2Assertion,
} from "./JMX";
import Mock from "mockjs";
import {funcFilters} from "@/common/js/func-filter";
@ -96,6 +96,7 @@ export const ASSERTION_TYPE = {
JSON_PATH: "JSON",
DURATION: "Duration",
JSR223: "JSR223",
XPATH2: "XPath2",
}
export const ASSERTION_REGEX_SUBJECT = {
@ -741,10 +742,11 @@ export class Assertions extends BaseConfig {
this.regex = [];
this.jsonPath = [];
this.jsr223 = [];
this.xpath2 = [];
this.duration = undefined;
this.set(options);
this.sets({text: Text, regex: Regex, jsonPath: JSONPath, jsr223: AssertionJSR223}, options);
this.sets({text: Text, regex: Regex, jsonPath: JSONPath, jsr223: AssertionJSR223, xpath2: XPath2}, options);
}
initOptions(options) {
@ -826,6 +828,23 @@ export class JSONPath extends AssertionType {
}
}
export class XPath2 extends AssertionType {
constructor(options) {
super(ASSERTION_TYPE.XPATH2);
this.expression = undefined;
this.description = undefined;
this.set(options);
}
// setJSONPathDescription() {
// this.description = this.expression + " expect: " + (this.expect ? this.expect : '');
// }
isValid() {
return !!this.expression;
}
}
export class Duration extends AssertionType {
constructor(options) {
super(ASSERTION_TYPE.DURATION);
@ -1001,7 +1020,8 @@ class JMXHttpRequest {
this.domain = environment.config.httpConfig.domain;
this.port = environment.config.httpConfig.port;
this.protocol = environment.config.httpConfig.protocol;
let envPath = environment.config.httpConfig.protocol + "://" + environment.config.httpConfig.socket;
let url = new URL(environment.config.httpConfig.protocol + "://" + environment.config.httpConfig.socket);
let envPath = url.pathname === '/' ? '' : url.pathname;
this.path = this.getPostQueryParameters(request, decodeURIComponent(envPath + (request.path ? request.path : '')));
}
this.connectTimeout = request.connectTimeout;
@ -1139,17 +1159,18 @@ class JMXGenerator {
sampler = new DubboSample(request.name || "", new JMXDubboRequest(request, scenario.dubboConfig));
} else if (request instanceof HttpRequest) {
sampler = new HTTPSamplerProxy(request.name || "", new JMXHttpRequest(request, scenario.environment));
this.addRequestHeader(sampler, request);
this.addRequestHeader(sampler, request, scenario);
this.addRequestArguments(sampler, request);
this.addRequestBody(sampler, request, testId);
} else if (request instanceof SqlRequest) {
request.dataSource = scenario.databaseConfigMap.get(request.dataSource);
sampler = new JDBCSampler(request.name || "", request);
this.addRequestVariables(sampler, request);
} else if (request instanceof TCPRequest) {
sampler = new TCPSampler(request.name || "", new JMXTCPRequest(request, scenario));
}
this.addRequestVariables(sampler, request, scenario);
this.addDNSCacheManager(sampler, scenario.environment, request.useEnvironment);
this.addRequestExtractor(sampler, request);
@ -1177,29 +1198,31 @@ class JMXGenerator {
}
addEnvironments(environments, target) {
let keys = new Set();
let targetMap = new Map();
target.forEach(item => {
keys.add(item.name);
if (item.name) {
targetMap.set(item.name, item.enable);
}
});
let envArray = environments;
if (!(envArray instanceof Array)) {
envArray = JSON.parse(environments);
}
envArray.forEach(item => {
if (item.name && !keys.has(item.name)) {
let targetItem = targetMap.get(item.name);
let hasItem = undefined;
if (targetItem) {
hasItem = (targetItem.enable === false ? false : true);
} else {
hasItem = false;
}
if (item.enable != false && item.name && !hasItem) {
target.push(new KeyValue({name: item.name, value: item.value}));
}
})
}
addScenarioVariables(threadGroup, scenario) {
if (scenario.environment) {
let config = scenario.environment.config;
if (!(scenario.environment.config instanceof Object)) {
config = JSON.parse(scenario.environment.config);
}
this.addEnvironments(config.commonConfig.variables, scenario.variables)
}
let args = this.filterKV(scenario.variables);
if (args.length > 0) {
let name = scenario.name + " Variables";
@ -1207,11 +1230,23 @@ class JMXGenerator {
}
}
addRequestVariables(httpSamplerProxy, request) {
addRequestVariables(httpSamplerProxy, request, scenario) {
if (request.useEnvironment && scenario.environment) {
let config = scenario.environment.config;
if (!(scenario.environment.config instanceof Object)) {
config = JSON.parse(scenario.environment.config);
}
if (!request.variables) {
request.variables = [];
}
this.addEnvironments(config.commonConfig.variables, request.variables)
}
let name = request.name + " Variables";
let variables = this.filterKV(request.variables);
if (variables && variables.length > 0) {
httpSamplerProxy.put(new Arguments(name, variables));
if (request.variables) {
let variables = this.filterKV(request.variables);
if (variables && variables.length > 0) {
httpSamplerProxy.put(new Arguments(name, variables));
}
}
}
@ -1272,13 +1307,6 @@ class JMXGenerator {
}
addScenarioHeaders(threadGroup, scenario) {
if (scenario.environment) {
let config = scenario.environment.config;
if (!(scenario.environment.config instanceof Object)) {
config = JSON.parse(scenario.environment.config);
}
this.addEnvironments(config.httpConfig.headers, scenario.headers)
}
let headers = this.filterKV(scenario.headers);
if (headers.length > 0) {
let name = scenario.name + " Headers";
@ -1286,7 +1314,17 @@ class JMXGenerator {
}
}
addRequestHeader(httpSamplerProxy, request) {
addRequestHeader(httpSamplerProxy, request, scenario) {
if (request.useEnvironment && scenario.environment) {
let config = scenario.environment.config;
if (!(scenario.environment.config instanceof Object)) {
config = JSON.parse(scenario.environment.config);
}
this.addEnvironments(config.httpConfig.headers, request.headers);
if (request.doMultipartPost) {
this.removeContentType(request);
}
}
let name = request.name + " Headers";
this.addBodyFormat(request);
let headers = this.filterKV(request.headers);
@ -1363,15 +1401,29 @@ class JMXGenerator {
}
addContentType(request, type) {
let hasContentType = false;
for (let index in request.headers) {
if (request.headers.hasOwnProperty(index)) {
if (request.headers[index].name === 'Content-Type') {
if (request.headers[index].name === 'Content-Type' && request.headers[index].enable != false) {
hasContentType = true;
break;
}
}
}
if (!hasContentType) {
request.headers.push(new KeyValue({name: 'Content-Type', value: type}));
}
}
removeContentType(request) {
for (let index in request.headers) {
if (request.headers.hasOwnProperty(index)) {
if (request.headers[index].name === 'Content-Type' && request.headers[index].enable != false) {
request.headers.splice(index, 1);
break;
}
}
}
request.headers.push(new KeyValue({name: 'Content-Type', value: type}));
}
addRequestArguments(httpSamplerProxy, request) {
@ -1387,8 +1439,10 @@ class JMXGenerator {
body = this.filterKV(request.body.kvs);
this.addRequestBodyFile(httpSamplerProxy, request, testId);
} else {
httpSamplerProxy.boolProp('HTTPSampler.postBodyRaw', true);
body.push({name: '', value: request.body.raw, encode: false, enable: true});
if (request.body.raw) {
httpSamplerProxy.boolProp('HTTPSampler.postBodyRaw', true);
body.push({name: '', value: request.body.raw, encode: false, enable: true});
}
}
if (request.method !== 'GET') {
@ -1427,6 +1481,12 @@ class JMXGenerator {
})
}
if (assertions.xpath2.length > 0) {
assertions.xpath2.filter(this.filter).forEach(item => {
httpSamplerProxy.put(this.getXpathAssertion(item));
})
}
if (assertions.jsr223.length > 0) {
assertions.jsr223.filter(this.filter).forEach(item => {
httpSamplerProxy.put(this.getJSR223Assertion(item));
@ -1449,6 +1509,11 @@ class JMXGenerator {
return new JSR223Assertion(name, item);
}
getXpathAssertion(item) {
let name = item.expression;
return new XPath2Assertion(name, item);
}
getResponseAssertion(regex) {
let name = regex.description;
let type = JMX_ASSERTION_CONDITION.CONTAINS; // 固定用Match自己写正则

View File

@ -23,11 +23,11 @@
{{ $t('report.test_execute_again') }}
</el-button>
<el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)">
{{$t('test_track.plan_view.export_report')}}
{{ $t('test_track.plan_view.export_report') }}
</el-button>
<!--<el-button :disabled="isReadOnly" type="warning" plain size="mini">-->
<!--{{$t('report.compare')}}-->
<!--{{$t('report.compare')}}-->
<!--</el-button>-->
</el-row>
</el-col>
@ -65,7 +65,8 @@
</el-tabs>
</div>
<ms-performance-report-export :title="reportName" id="performanceReportExport" v-show="reportExportVisible" :report="report"/>
<ms-performance-report-export :title="reportName" id="performanceReportExport" v-show="reportExportVisible"
:report="report"/>
</el-card>
<el-dialog :title="$t('report.test_stop_now_confirm')" :visible.sync="dialogFormVisible" width="30%">
@ -91,8 +92,7 @@ import MsPerformancePressureConfig from "./components/PerformancePressureConfig"
import MsContainer from "../../common/components/MsContainer";
import MsMainContainer from "../../common/components/MsMainContainer";
import {checkoutTestManagerOrTestUser} from "@/common/js/utils";
import {exportPdf} from "../../../../common/js/utils";
import {checkoutTestManagerOrTestUser, exportPdf} from "@/common/js/utils";
import html2canvas from 'html2canvas';
import MsPerformanceReportExport from "./PerformanceReportExport";
@ -188,7 +188,8 @@ export default {
checkReportStatus(status) {
switch (status) {
case 'Error':
this.$warning(this.$t('report.generation_error'));
// this.$warning(this.$t('report.generation_error'));
this.active = '4';
break;
case 'Starting':
this.$alert(this.$t('report.start_status'));
@ -240,6 +241,11 @@ export default {
},
onMessage(e) {
this.$set(this.report, "refresh", e.data); //
if (e.data.startsWith('Error')) {
this.$set(this.report, "status", 'Error');
this.$warning(e.data);
return;
}
this.$set(this.report, "status", 'Running');
this.status = 'Running';
this.initReportTimeInfo();
@ -264,7 +270,7 @@ export default {
setTimeout(() => {
html2canvas(document.getElementById('performanceReportExport'), {
scale: 2
}).then(function(canvas) {
}).then(function (canvas) {
exportPdf(name, [canvas]);
reset();
});
@ -332,18 +338,18 @@ export default {
<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;
}
.report-export .el-card {
margin-bottom: 15px;
}
.report-export .el-card {
margin-bottom: 15px;
}
</style>

View File

@ -180,10 +180,7 @@ export default {
this.multipleSelection = val;
},
handleEdit(report) {
if (report.status === "Error") {
this.$warning(this.$t('report.generation_error'));
return false
} else if (report.status === "Starting") {
if (report.status === "Starting") {
this.$info(this.$t('report.being_generated'))
return false
}

View File

@ -114,7 +114,7 @@ export default {
this.avgResponseTime = '0';
this.responseTime90 = '0';
this.avgBandwidth = '0';
this.$warning(this.$t('report.generation_error'));
// this.$warning(this.$t('report.generation_error'));
})
this.getLoadChart();
this.getResChart();

View File

@ -1,7 +1,7 @@
<template>
<div v-loading="result.loading">
<el-upload
accept=".jmx,.csv"
accept=".jmx,.csv,.jar"
drag
action=""
:limit="fileNumLimit"
@ -77,7 +77,7 @@ export default {
fileList: [],
tableData: [],
uploadList: [],
fileNumLimit: 5,
fileNumLimit: 10,
};
},
created() {
@ -197,7 +197,7 @@ export default {
return this.fileList;//
},
validConfig() {
let newJmxNum = 0, oldJmxNum = 0, newCsvNum = 0, oldCsvNum = 0;
let newJmxNum = 0, oldJmxNum = 0, newCsvNum = 0, oldCsvNum = 0, newJarNum = 0, oldJarNum = 0;
if (this.uploadList.length > 0) {
this.uploadList.forEach(f => {
if (f.name.toLowerCase().endsWith(".jmx")) {
@ -206,6 +206,9 @@ export default {
if (f.name.toLowerCase().endsWith(".csv")) {
newCsvNum++;
}
if (f.name.toLowerCase().endsWith(".jar")) {
newJarNum++;
}
});
}
if (this.fileList.length > 0) {
@ -216,9 +219,12 @@ export default {
if (f.name.toLowerCase().endsWith(".csv")) {
oldCsvNum++;
}
if (f.name.toLowerCase().endsWith(".jar")) {
oldJarNum++;
}
});
}
if (newCsvNum + oldCsvNum > this.fileNumLimit - 1) {
if (newCsvNum + oldCsvNum + newJarNum + oldJarNum > this.fileNumLimit - 1) {
this.handleExceed();
return false;
}

View File

@ -204,7 +204,11 @@ export default {
})
},
removeRowTask(index, data) { //
data.splice(index, 1)
if (!data[index].identification) {
data.splice(index, 1)
} else {
data[index].isSet = false
}
},
deleteRowTask(index, data) { //
this.result = this.$get("/notice/delete/message/" + data.identification, response => {

View File

@ -205,7 +205,12 @@ export default {
})
},
removeRowTask(index, data) { //
data.splice(index, 1)
if (!data[index].identification) {
data.splice(index, 1)
} else {
data[index].isSet = false
}
},
deleteRowTask(index, data) { //
this.result = this.$get("/notice/delete/message/" + data.identification, response => {

View File

@ -196,7 +196,11 @@ export default {
})
},
removeRowTask(index, data) { //
data.splice(index, 1)
if (!data[index].identification) {
data.splice(index, 1)
} else {
data[index].isSet = false
}
},
deleteRowTask(index, data) { //
this.result = this.$get("/notice/delete/message/" + data.identification, response => {

View File

@ -210,7 +210,11 @@ export default {
})
},
removeRowTask(index, data) { //
data.splice(index, 1)
if (!data[index].identification) {
data.splice(index, 1)
} else {
data[index].isSet = false
}
},
deleteRowTask(index, data) { //
this.result = this.$get("/notice/delete/message/" + data.identification, response => {

View File

@ -211,7 +211,11 @@ export default {
})
},
removeRowTask(index, data) { //
data.splice(index, 1)
if (!data[index].identification) {
data.splice(index, 1)
} else {
data[index].isSet = false
}
},
deleteRowTask(index, data) { //
this.result = this.$get("/notice/delete/message/" + data.identification, response => {

View File

@ -32,7 +32,9 @@ export default {
component: () => import('@/business/components/settings/system/SystemParameterSetting'),
meta: {system: true, title: 'commons.system_parameter_setting'}
},
...requireContext.keys().map(key => requireContext(key).system),...requireContext.keys().map(key => requireContext(key).license),
...requireContext.keys().map(key => requireContext(key).system),
...requireContext.keys().map(key => requireContext(key).license),
...requireContext.keys().map(key => requireContext(key).display),
{
path: 'organizationmember',
component: () => import('@/business/components/settings/organization/OrganizationMember'),

View File

@ -295,7 +295,6 @@ export default {
this.infoList = resources;
},
del(row) {
window.console.log(row);
this.$confirm(this.$t('test_resource_pool.delete_prompt'), this.$t('commons.prompt'), {
confirmButtonText: this.$t('commons.confirm'),
cancelButtonText: this.$t('commons.cancel'),

View File

@ -368,8 +368,9 @@ export default {
}
],
phone: [
{required: true, message: this.$t('user.input_phone'), trigger: 'blur'},
{
required: false,
required: true,
pattern: '^1(3|4|5|7|8)\\d{9}$',
message: this.$t('user.mobile_number_format_is_incorrect'),
trigger: 'blur'

View File

@ -63,6 +63,15 @@
<plan-stage-table-item :stage="scope.row.stage"/>
</template>
</el-table-column>
<el-table-column
prop="projectName"
:label="$t('test_track.home.test_rate')"
min-width="100"
show-overflow-tooltip>
<template v-slot:default="scope">
<el-progress :percentage="scope.row.testRate"></el-progress>
</template>
</el-table-column>
<el-table-column
prop="projectName"
:label="$t('test_track.plan.plan_project')"

View File

@ -286,12 +286,13 @@
},
getProject() {
if (this.planId) {
this.$post("/test/plan/project/", {planId: this.planId}, res => {
this.result = this.$post("/test/plan/project/", {planId: this.planId}, res => {
let data = res.data;
if (data) {
this.projects = data;
this.projectId = data[0].id;
this.projectName = data[0].name;
this.search();
}
})
}

View File

@ -57,24 +57,24 @@
</template>
<script>
import MsMainContainer from "../../../../../common/components/MsMainContainer";
import MsContainer from "../../../../../common/components/MsContainer";
import LogDetails from "../../../../../performance/report/components/LogDetails";
import ErrorLog from "../../../../../performance/report/components/ErrorLog";
import RequestStatistics from "../../../../../performance/report/components/RequestStatistics";
import TestOverview from "../../../../../performance/report/components/TestOverview";
import MsMainContainer from "../../../../../common/components/MsMainContainer";
import MsContainer from "../../../../../common/components/MsContainer";
import LogDetails from "../../../../../performance/report/components/LogDetails";
import ErrorLog from "../../../../../performance/report/components/ErrorLog";
import RequestStatistics from "../../../../../performance/report/components/RequestStatistics";
import TestOverview from "../../../../../performance/report/components/TestOverview";
export default {
name: "PerformanceTestResult",
components: {
TestOverview,
RequestStatistics,
ErrorLog,
LogDetails,
MsContainer,
MsMainContainer
},
data() {
export default {
name: "PerformanceTestResult",
components: {
TestOverview,
RequestStatistics,
ErrorLog,
LogDetails,
MsContainer,
MsMainContainer
},
data() {
return {
result: {},
active: '0',
@ -145,7 +145,7 @@
}
switch (this.report.status) {
case 'Error':
this.$warning(this.$t('report.generation_error'));
// this.$warning(this.$t('report.generation_error'));
break;
case 'Starting':
this.$warning(this.$t('report.start_status'));

View File

@ -40,7 +40,8 @@ export default {
components: {ReviewCommentItem},
props: {
caseId: String,
comments: Array
comments: Array,
reviewId:String,
},
data() {
return {
@ -53,6 +54,7 @@ export default {
let comment = {};
comment.caseId = this.caseId;
comment.description = this.textarea;
comment.reviewId=this.reviewId;
if (!this.textarea) {
this.$warning(this.$t('test_track.comment.description_is_null'));
return;

View File

@ -229,7 +229,7 @@
<i class="el-icon-refresh" @click="getComments(testCase)"
style="margin-left:10px;font-size: 14px; cursor: pointer"/>
</template>
<review-comment :comments="comments" :case-id="testCase.caseId" @getComments="getComments"/>
<review-comment :comments="comments" :case-id="testCase.caseId" :review-id="testCase.reviewId" @getComments="getComments"/>
</el-card>
</el-col>
</div>

View File

@ -300,6 +300,7 @@ export default {
return path + "/" + this.currentPage + "/" + this.pageSize;
},
handleEdit(testCase, index) {
console.log(testCase)
this.isReadOnly = false;
if (!checkoutTestManagerOrTestUser()) {
this.isReadOnly = true;

@ -1 +1 @@
Subproject commit cc38137a69a0f20fadece9c0f9f50a9468c4ace9
Subproject commit eb237fb6bfeba8d99e4db52450ae92f3cdd4ea33

View File

@ -180,6 +180,14 @@ export default {
invalid: 'invalid',
expired: 'expired',
},
display: {
title: 'Theme',
logo: 'System LOGO',
loginLogo: 'Picture on the right side of the login page',
loginImage: 'Login page upper left corner LOGO',
loginTitle: 'Login page prompt information',
pageTitle: 'Page Title',
},
system_config: {
base_config: 'Base Config',
base: {
@ -348,7 +356,7 @@ export default {
test_execute_again: 'Test Execute Again',
export: 'Export',
compare: 'Compare',
generation_error: 'Report generation error, cannot be viewed!',
generation_error: 'Report generation error, unable to view, please check log details!',
being_generated: 'Report is being generated...',
delete_confirm: 'Confirm delete: ',
start_status: 'The test is in the beginning state, we will automatically display it on the page after we generate the report!',
@ -389,7 +397,7 @@ export default {
file_status: 'File Status',
last_modify_time: 'Modify time',
upload_tips: 'Drag files here, or <em> click to upload </em>',
upload_type: 'Only JMX/CSV files can be uploaded',
upload_type: 'Only JMX/CSV/JAR files can be uploaded',
related_file_not_found: "No related test file found!",
delete_file_confirm: 'Confirm delete file:',
file_size_limit: "The number of files exceeds the limit",

View File

@ -180,6 +180,14 @@ export default {
invalid: '无效',
expired: '已过期',
},
display: {
title: '显示设置',
logo: '系统 LOGO',
loginLogo: '登陆页面右侧图片',
loginImage: '登录页左上角 LOGO',
loginTitle: '登陆页面提示信息',
pageTitle: '页面 Title',
},
system_config: {
base_config: '基本配置',
base: {
@ -347,7 +355,7 @@ export default {
test_execute_again: '再次执行',
export: '导出',
compare: '比较',
generation_error: '报告生成错误,无法查看!',
generation_error: '报告生成错误, 无法查看, 请检查日志详情!',
being_generated: '报告正在生成中...',
delete_confirm: '确认删除报告: ',
start_status: '测试处于开始状态, 我们生成报告后会自动展示到页面上!',
@ -387,7 +395,7 @@ export default {
file_status: '文件状态',
last_modify_time: '修改时间',
upload_tips: '将文件拖到此处,或<em>点击上传</em>',
upload_type: '只能上传JMX/CSV文件',
upload_type: '只能上传JMX/CSV/JAR文件',
related_file_not_found: "未找到关联的测试文件!",
delete_file_confirm: '确认删除文件: ',
file_size_limit: "文件个数超出限制!",

View File

@ -180,6 +180,14 @@ export default {
invalid: '無效',
expired: '已過期',
},
display: {
title: '顯示設置',
logo: '系統 LOGO',
loginLogo: '登陸頁面右側圖片',
loginImage: '登錄頁左上角 LOGO',
loginTitle: '登陸頁面提示信息',
pageTitle: '頁面 Title',
},
system_config: {
base_config: '基本配置',
base: {
@ -347,7 +355,7 @@ export default {
test_execute_again: '再次執行',
export: '導出',
compare: '比較',
generation_error: '報告生成錯誤,無法查看!',
generation_error: '報告生成錯誤, 無法查看, 請檢查日誌詳情!',
being_generated: '報告正在生成中...',
delete_confirm: '確認刪除報告: ',
start_status: '測試處於開始狀態, 我們生成報告後會自動展示到頁面上!',
@ -387,7 +395,7 @@ export default {
file_status: '文件狀態',
last_modify_time: '修改時間',
upload_tips: '將文件拖到此處,或<em>點擊上傳</em>',
upload_type: '只能上傳JMX/CSV文件',
upload_type: '只能上傳JMX/CSV/JAR文件',
related_file_not_found: "未找到關聯的測試文件!",
delete_file_confirm: '確認刪除文件: ',
file_size_limit: "文件個數超出限制!",

View File

@ -4,15 +4,16 @@
<el-col :span="12">
<el-form :model="form" :rules="rules" ref="form">
<div class="logo">
<img src="../assets/logo-dark-MeterSphere.svg" style="width: 224px" alt="">
<img v-if="loginLogoId" :src="'/display/file/' + loginLogoId" style="width: 224px;height: 45px;" alt="">
<img v-else src="../assets/logo-dark-MeterSphere.svg" style="width: 224px; " alt="">
</div>
<div class="title">
<span id="s1">{{$t('commons.login')}}</span>
<span id="s1">{{ $t('commons.login') }}</span>
<span id="s2">MeterSphere</span>
</div>
<div class="border"></div>
<div class="welcome">
{{$t('commons.welcome')}}
{{ $t('commons.welcome') }}
</div>
<div class="form">
<el-form-item v-slot:default>
@ -32,241 +33,252 @@
</div>
<div class="btn">
<el-button type="primary" class="submit" @click="submit('form')">
{{$t('commons.login')}}
{{ $t('commons.login') }}
</el-button>
</div>
<div class="msg">
{{msg}}
{{ msg }}
</div>
</el-form>
</el-col>
<el-col :span="12" class="image">
<div></div>
<el-col :span="12">
<img v-if="loginImageId" :src="'/display/file/' + loginImageId" style="height: 560px; width: 100%">
<img v-else src="../assets/info.png" style="height: 560px; width: 100%">
</el-col>
</el-row>
</div>
</template>
<script>
import {saveLocalStorage} from '../common/js/utils';
import {DEFAULT_LANGUAGE} from "../common/js/constants";
import {saveLocalStorage} from '@/common/js/utils';
import {DEFAULT_LANGUAGE} from "@/common/js/constants";
const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/);
const display = requireComponent.keys().length > 0 ? requireComponent("./display/Display.vue") : {};
export default {
name: "Login",
data() {
/*let validateEmail = (rule, value, callback) => {
// eslint-disable-next-line no-useless-escape
let EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!EMAIL_REGEX.test(value)) {
callback(new Error('邮箱格式不正确'));
export default {
name: "Login",
data() {
/*let validateEmail = (rule, value, callback) => {
// eslint-disable-next-line no-useless-escape
let EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (!EMAIL_REGEX.test(value)) {
callback(new Error('邮箱格式不正确'));
} else {
callback();
}
};*/
return {
result: {},
form: {
username: '',
password: '',
authenticate: 'LOCAL'
},
rules: {
username: [
{required: true, message: this.$t('commons.input_login_username'), trigger: 'blur'},
],
password: [
{required: true, message: this.$t('commons.input_password'), trigger: 'blur'},
{min: 6, max: 20, message: this.$t('commons.input_limit', [6, 20]), trigger: 'blur'}
]
},
msg: '',
ready: false,
openLdap: false,
loginLogoId: '_blank',
loginImageId: '_blank',
}
},
beforeCreate() {
this.result = this.$get("/isLogin").then(response => {
if (display.default !== undefined) {
display.default.valid(this);
}
if (!response.data.success) {
if (response.data.message === 'sso') {
window.location.href = "/sso/login"
} else {
callback();
this.ready = true;
}
};*/
return {
result: {},
form: {
username: '',
password: '',
authenticate: 'LOCAL'
},
rules: {
username: [
{required: true, message: this.$t('commons.input_login_username'), trigger: 'blur'},
],
password: [
{required: true, message: this.$t('commons.input_password'), trigger: 'blur'},
{min: 6, max: 20, message: this.$t('commons.input_limit', [6, 20]), trigger: 'blur'}
]
},
msg: '',
ready: false,
openLdap: false
} else {
let user = response.data.data;
saveLocalStorage(response.data);
this.getLanguage(user.language);
window.location.href = "/"
}
});
this.$get("/ldap/open", response => {
this.openLdap = response.data;
})
},
created: function () {
// ,,
document.addEventListener("keydown", this.watchEnter);
},
destroyed() {
//
document.removeEventListener("keydown", this.watchEnter);
},
methods: {
//
watchEnter(e) {
let keyNum = e.which; //
//keycody=13
if (keyNum === 13) {
//
this.submit('form');
}
},
beforeCreate() {
this.$get("/isLogin").then(response => {
if (!response.data.success) {
if (response.data.message === 'sso') {
window.location.href = "/sso/login"
} else {
this.ready = true;
submit(form) {
this.$refs[form].validate((valid) => {
if (valid) {
switch (this.form.authenticate) {
case "LOCAL":
this.normalLogin();
break;
case "LDAP":
this.ldapLogin();
break;
default:
this.normalLogin();
}
} else {
let user = response.data.data;
saveLocalStorage(response.data);
this.getLanguage(user.language);
window.location.href = "/"
return false;
}
});
this.$get("/ldap/open", response => {
this.openLdap = response.data;
})
},
created: function () {
// ,,
document.addEventListener("keydown", this.watchEnter);
normalLogin() {
this.result = this.$post("signin", this.form, response => {
saveLocalStorage(response);
sessionStorage.setItem('loginSuccess', 'true');
this.getLanguage(response.data.language);
});
},
destroyed() {
//
document.removeEventListener("keydown", this.watchEnter);
ldapLogin() {
this.result = this.$post("ldap/signin", this.form, response => {
saveLocalStorage(response);
sessionStorage.setItem('loginSuccess', 'true');
this.getLanguage(response.data.language);
});
},
methods: {
//
watchEnter(e) {
let keyNum = e.which; //
//keycody=13
if (keyNum === 13) {
//
this.submit('form');
}
},
submit(form) {
this.$refs[form].validate((valid) => {
if (valid) {
switch (this.form.authenticate) {
case "LOCAL":
this.normalLogin();
break;
case "LDAP":
this.ldapLogin();
break;
default:
this.normalLogin();
}
} else {
return false;
}
});
},
normalLogin() {
this.result = this.$post("signin", this.form, response => {
saveLocalStorage(response);
sessionStorage.setItem('loginSuccess', 'true');
this.getLanguage(response.data.language);
});
},
ldapLogin() {
this.result = this.$post("ldap/signin", this.form, response => {
saveLocalStorage(response);
sessionStorage.setItem('loginSuccess', 'true');
this.getLanguage(response.data.language);
});
},
getLanguage(language) {
if (!language) {
this.$get("language", response => {
language = response.data;
localStorage.setItem(DEFAULT_LANGUAGE, language);
window.location.href = "/"
})
} else {
getLanguage(language) {
if (!language) {
this.$get("language", response => {
language = response.data;
localStorage.setItem(DEFAULT_LANGUAGE, language);
window.location.href = "/"
}
})
} else {
window.location.href = "/"
}
}
}
}
</script>
<style scoped>
.container {
min-width: 800px;
max-width: 1440px;
height: 560px;
margin: calc((100vh - 560px) / 2) auto 0;
background-color: #FFFFFF;
}
.container {
min-width: 800px;
max-width: 1440px;
height: 560px;
margin: calc((100vh - 560px) / 2) auto 0;
background-color: #FFFFFF;
}
.logo {
margin: 30px 30px 0;
}
.logo {
margin: 30px 30px 0;
}
.title {
margin-top: 50px;
font-size: 32px;
letter-spacing: 0;
text-align: center;
}
.title {
margin-top: 50px;
font-size: 32px;
letter-spacing: 0;
text-align: center;
}
.title > #s1 {
color: #999999;
}
.title > #s1 {
color: #999999;
}
.title > #s2 {
color: #151515;
}
.title > #s2 {
color: #151515;
}
.border {
height: 2px;
margin: 20px auto 20px;
position: relative;
width: 80px;
background: #8B479B;
}
.border {
height: 2px;
margin: 20px auto 20px;
position: relative;
width: 80px;
background: #8B479B;
}
.welcome {
margin-top: 50px;
font-size: 14px;
color: #999999;
letter-spacing: 0;
line-height: 18px;
text-align: center;
}
.welcome {
margin-top: 50px;
font-size: 14px;
color: #999999;
letter-spacing: 0;
line-height: 18px;
text-align: center;
}
.form {
margin-top: 30px;
padding: 0 40px;
}
.form {
margin-top: 30px;
padding: 0 40px;
}
.btn {
margin-top: 40px;
padding: 0 40px;
}
.btn {
margin-top: 40px;
padding: 0 40px;
}
.btn > .submit {
width: 100%;
border-radius: 0;
border-color: #8B479B;
background-color: #8B479B;
}
.btn > .submit {
width: 100%;
border-radius: 0;
border-color: #8B479B;
background-color: #8B479B;
}
.btn > .submit:hover {
border-color: rgba(139, 71, 155, 0.9);
background-color: rgba(139, 71, 155, 0.9);
}
.btn > .submit:hover {
border-color: rgba(139, 71, 155, 0.9);
background-color: rgba(139, 71, 155, 0.9);
}
.btn > .submit:active {
border-color: rgba(139, 71, 155, 0.8);
background-color: rgba(139, 71, 155, 0.8);
}
.btn > .submit:active {
border-color: rgba(139, 71, 155, 0.8);
background-color: rgba(139, 71, 155, 0.8);
}
.msg {
margin-top: 10px;
padding: 0 40px;
color: red;
text-align: center;
}
.msg {
margin-top: 10px;
padding: 0 40px;
color: red;
text-align: center;
}
.image {
background: url(../assets/info.png);
height: 560px;
}
.image {
background: url(../assets/info.png);
height: 560px;
}
</style>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Neue Haas Grotesk Text Pro", "Arial Nova", "Segoe UI", "Helvetica Neue", ".PingFang SC", "PingFang SC", "Source Han Sans SC", "Noto Sans CJK SC", "Source Han Sans CN", "Noto Sans SC", "Source Han Sans TC", "Noto Sans CJK TC", "Hiragino Sans GB", sans-serif;
font-size: 14px;
background-color: #F5F5F5;
line-height: 26px;
color: #2B415C;
-webkit-font-smoothing: antialiased;
margin: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Neue Haas Grotesk Text Pro", "Arial Nova", "Segoe UI", "Helvetica Neue", ".PingFang SC", "PingFang SC", "Source Han Sans SC", "Noto Sans CJK SC", "Source Han Sans CN", "Noto Sans SC", "Source Han Sans TC", "Noto Sans CJK TC", "Hiragino Sans GB", sans-serif;
font-size: 14px;
background-color: #F5F5F5;
line-height: 26px;
color: #2B415C;
-webkit-font-smoothing: antialiased;
margin: 0;
}
.form .el-input > .el-input__inner {
border-radius: 0;
}
.form .el-input > .el-input__inner {
border-radius: 0;
}
</style>