Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	frontend/src/business/components/api/head/ApiHeaderMenus.vue
This commit is contained in:
q4speed 2020-04-09 11:28:55 +08:00
commit ba64fa565c
34 changed files with 613 additions and 415 deletions

View File

@ -21,8 +21,8 @@
</select> </select>
<select id="getTestPlanTestCases" resultType="io.metersphere.dto.TestPlanCaseDTO"> <select id="getTestPlanTestCases" resultType="io.metersphere.dto.TestPlanCaseDTO">
select t1.id as id, t1.plan_id as planId, t1.executor as executor, t1.status as status, t1.results as results, t1.remark remard, t1.create_time as createTime, t1.update_time updateTime, select t1.id as id, t1.plan_id as planId, t1.executor as executor, t1.status as status, t1.results as results, t1.create_time as createTime, t1.update_time updateTime,
t2.id as caseId, t2.node_id as nodeId, t2.node_path as nodePath, t2.project_id as projectId, t2.name as name, t2.id as caseId, t2.node_id as nodeId, t2.node_path as nodePath, t2.project_id as projectId, t2.name as name, t2.remark remark, t2.steps steps,
t2.type as type, t2.maintainer as maintainer, t2.priority as priority, t2.method as method, t2.prerequisite as prerequisite t2.type as type, t2.maintainer as maintainer, t2.priority as priority, t2.method as method, t2.prerequisite as prerequisite
from test_plan_test_case as t1 from test_plan_test_case as t1
inner join test_case as t2 on inner join test_case as t2 on

View File

@ -78,12 +78,12 @@ public class ReportController {
} }
@GetMapping("/content/load_chart/{reportId}") @GetMapping("/content/load_chart/{reportId}")
public ChartsData getLoadChartData(@PathVariable String reportId) { public List<ChartsData> getLoadChartData(@PathVariable String reportId) {
return reportService.getLoadChartData(reportId); return reportService.getLoadChartData(reportId);
} }
@GetMapping("/content/res_chart/{reportId}") @GetMapping("/content/res_chart/{reportId}")
public ChartsData getResponseTimeChartData(@PathVariable String reportId) { public List<ChartsData> getResponseTimeChartData(@PathVariable String reportId) {
return reportService.getResponseTimeChartData(reportId); return reportService.getResponseTimeChartData(reportId);
} }

View File

@ -2,10 +2,7 @@ package io.metersphere.controller;
import com.github.pagehelper.Page; import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.LoadTest; import io.metersphere.base.domain.*;
import io.metersphere.base.domain.TestCase;
import io.metersphere.base.domain.TestCaseNode;
import io.metersphere.base.domain.TestCaseWithBLOBs;
import io.metersphere.commons.utils.PageUtils; import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager; import io.metersphere.commons.utils.Pager;
import io.metersphere.controller.request.testcase.QueryTestCaseRequest; import io.metersphere.controller.request.testcase.QueryTestCaseRequest;
@ -39,8 +36,7 @@ public class TestCaseController {
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId(); String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
QueryTestCaseRequest request = new QueryTestCaseRequest(); QueryTestCaseRequest request = new QueryTestCaseRequest();
request.setWorkspaceId(currentWorkspaceId); request.setWorkspaceId(currentWorkspaceId);
PageHelper.startPage(1, count, true); return testCaseService.recentTestPlans(request, count);
return testCaseService.recentTestPlans(request);
} }
@PostMapping("/list") @PostMapping("/list")
@ -58,6 +54,11 @@ public class TestCaseController {
return testCaseService.getTestCase(testCaseId); return testCaseService.getTestCase(testCaseId);
} }
@GetMapping("/project/{testCaseId}")
public Project getProjectByTestCaseId(@PathVariable String testCaseId){
return testCaseService.getProjectByTestCaseId(testCaseId);
}
@PostMapping("/add") @PostMapping("/add")
public void addTestCase(@RequestBody TestCaseWithBLOBs testCase){ public void addTestCase(@RequestBody TestCaseWithBLOBs testCase){
testCaseService.addTestCase(testCase); testCaseService.addTestCase(testCase);

View File

@ -28,6 +28,8 @@ public class TestPlanController {
@PostMapping("/list/{goPage}/{pageSize}") @PostMapping("/list/{goPage}/{pageSize}")
public Pager<List<TestPlanDTO>> list(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryTestPlanRequest request) { public Pager<List<TestPlanDTO>> 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); Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
return PageUtils.setPageInfo(page, testPlanService.listTestPlan(request)); return PageUtils.setPageInfo(page, testPlanService.listTestPlan(request));
} }

View File

@ -46,10 +46,16 @@ public class WorkspaceController {
@PostMapping("special/update") @PostMapping("special/update")
@RequiresRoles(RoleConstants.ADMIN) @RequiresRoles(RoleConstants.ADMIN)
public void updateWorkspacebyAdmin(@RequestBody Workspace workspace) { public void updateWorkspaceByAdmin(@RequestBody Workspace workspace) {
workspaceService.updateWorkspacebyAdmin(workspace); workspaceService.updateWorkspacebyAdmin(workspace);
} }
@GetMapping("special/delete/{workspaceId}")
@RequiresRoles(RoleConstants.ADMIN)
public void deleteWorkspaceByAdmin(@PathVariable String workspaceId) {
workspaceService.deleteWorkspace(workspaceId);
}
@GetMapping("delete/{workspaceId}") @GetMapping("delete/{workspaceId}")
@RequiresRoles(RoleConstants.ORG_ADMIN) @RequiresRoles(RoleConstants.ORG_ADMIN)
public void deleteWorkspace(@PathVariable String workspaceId) { public void deleteWorkspace(@PathVariable String workspaceId) {

View File

@ -1,13 +1,13 @@
package io.metersphere.dto; package io.metersphere.dto;
import io.metersphere.base.domain.TestCase; import io.metersphere.base.domain.TestCase;
import io.metersphere.base.domain.TestCaseWithBLOBs;
public class TestPlanCaseDTO extends TestCase { public class TestPlanCaseDTO extends TestCaseWithBLOBs {
private String executor; private String executor;
private String status; private String status;
private String results; private String results;
private String remark;
public String getExecutor() { public String getExecutor() {
return executor; return executor;
@ -32,12 +32,4 @@ public class TestPlanCaseDTO extends TestCase {
public void setResults(String results) { public void setResults(String results) {
this.results = results; this.results = results;
} }
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
} }

View File

@ -89,8 +89,7 @@ public class JmeterDocumentParser implements DocumentParser {
processCheckoutConfigTestElement(ele); processCheckoutConfigTestElement(ele);
processCheckoutDnsCacheManager(ele); processCheckoutDnsCacheManager(ele);
processCheckoutArguments(ele); processCheckoutArguments(ele);
// TODO: 2020/4/3 使用断言导致backend-listener不可用 processCheckoutResponseAssertion(ele);
// processCheckoutResponseAssertion(ele);
} else if (nodeNameEquals(ele, CONCURRENCY_THREAD_GROUP)) { } else if (nodeNameEquals(ele, CONCURRENCY_THREAD_GROUP)) {
processConcurrencyThreadGroup(ele); processConcurrencyThreadGroup(ele);
processCheckoutTimer(ele); processCheckoutTimer(ele);
@ -112,8 +111,7 @@ public class JmeterDocumentParser implements DocumentParser {
} else if (nodeNameEquals(ele, ARGUMENTS)) { } else if (nodeNameEquals(ele, ARGUMENTS)) {
processArguments(ele); processArguments(ele);
} else if (nodeNameEquals(ele, RESPONSE_ASSERTION)) { } else if (nodeNameEquals(ele, RESPONSE_ASSERTION)) {
// TODO: 2020/4/3 使用断言导致backend-listener不可用 processResponseAssertion(ele);
// processResponseAssertion(ele);
} }
} }
} }
@ -160,7 +158,7 @@ public class JmeterDocumentParser implements DocumentParser {
item.appendChild(collectionProp); item.appendChild(collectionProp);
item.appendChild(createStringProp(document, "Assertion.custom_message", "")); item.appendChild(createStringProp(document, "Assertion.custom_message", ""));
item.appendChild(createStringProp(document, "Assertion.test_field", "Assertion.response_code")); item.appendChild(createStringProp(document, "Assertion.test_field", "Assertion.response_code"));
item.appendChild(createBoolProp(document, "Assertion.assume_success", false)); item.appendChild(createBoolProp(document, "Assertion.assume_success", true));
item.appendChild(createIntProp(document, "Assertion.test_type", 40)); item.appendChild(createIntProp(document, "Assertion.test_type", 40));
return; return;
} }
@ -190,7 +188,7 @@ public class JmeterDocumentParser implements DocumentParser {
responseAssertion.appendChild(collectionProp); responseAssertion.appendChild(collectionProp);
responseAssertion.appendChild(createStringProp(document, "Assertion.custom_message", "")); responseAssertion.appendChild(createStringProp(document, "Assertion.custom_message", ""));
responseAssertion.appendChild(createStringProp(document, "Assertion.test_field", "Assertion.response_code")); responseAssertion.appendChild(createStringProp(document, "Assertion.test_field", "Assertion.response_code"));
responseAssertion.appendChild(createBoolProp(document, "Assertion.assume_success", false)); responseAssertion.appendChild(createBoolProp(document, "Assertion.assume_success", true));
responseAssertion.appendChild(createIntProp(document, "Assertion.test_type", 40)); responseAssertion.appendChild(createIntProp(document, "Assertion.test_type", 40));
hashTree.appendChild(responseAssertion); hashTree.appendChild(responseAssertion);
hashTree.appendChild(document.createElement(HASH_TREE_ELEMENT)); hashTree.appendChild(document.createElement(HASH_TREE_ELEMENT));

View File

@ -1,6 +1,5 @@
package io.metersphere.report; package io.metersphere.report;
import com.alibaba.fastjson.JSONObject;
import com.opencsv.bean.CsvToBean; import com.opencsv.bean.CsvToBean;
import com.opencsv.bean.CsvToBeanBuilder; import com.opencsv.bean.CsvToBeanBuilder;
import com.opencsv.bean.HeaderColumnNameMappingStrategy; import com.opencsv.bean.HeaderColumnNameMappingStrategy;
@ -10,6 +9,7 @@ import io.metersphere.report.dto.RequestStatisticsDTO;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.Reader; import java.io.Reader;
import java.io.StringReader; import java.io.StringReader;
import java.math.BigDecimal;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -312,18 +312,10 @@ public class JtlResolver {
} }
public static ChartsData getLoadChartData(String jtlString) { public static List<ChartsData> getLoadChartData(String jtlString) {
ChartsData data = new ChartsData(); List<ChartsData> chartsDataList = new ArrayList<>();
List<Metric> totalMetricList = JtlResolver.resolver(jtlString); List<Metric> totalMetricList = JtlResolver.resolver(jtlString);
List<String> users = new ArrayList<>();
List<String> hits = new ArrayList<>();
List<String> errors = new ArrayList<>();
List<String> timeList = new ArrayList<>();
Map<String, Object> resultMap = new HashMap<>(5);
DecimalFormat decimalFormat = new DecimalFormat("0.0");
if (totalMetricList != null) { if (totalMetricList != null) {
for (Metric metric : totalMetricList) { for (Metric metric : totalMetricList) {
metric.setTimestamp(stampToDate(metric.getTimestamp())); metric.setTimestamp(stampToDate(metric.getTimestamp()));
@ -331,7 +323,6 @@ public class JtlResolver {
} }
Map<String, List<Metric>> collect = Objects.requireNonNull(totalMetricList).stream().collect(Collectors.groupingBy(Metric::getTimestamp)); Map<String, List<Metric>> collect = Objects.requireNonNull(totalMetricList).stream().collect(Collectors.groupingBy(Metric::getTimestamp));
List<Map.Entry<String, List<Metric>>> entries = new ArrayList<>(collect.entrySet()); List<Map.Entry<String, List<Metric>>> entries = new ArrayList<>(collect.entrySet());
entries.sort(JtlResolver::sortByDate);
for (Map.Entry<String, List<Metric>> entry : entries) { for (Map.Entry<String, List<Metric>> entry : entries) {
int failSize = 0; int failSize = 0;
@ -344,27 +335,39 @@ public class JtlResolver {
failSize++; failSize++;
} }
} }
// todo
timeList.add(entry.getKey());
hits.add(decimalFormat.format(metrics.size() * 1.0 / maxUsers));
users.add(String.valueOf(maxUsers));
errors.add(String.valueOf(failSize));
String timeStamp = "";
try {
timeStamp = formatDate(entry.getKey());
} catch (ParseException e) {
e.printStackTrace();
} }
resultMap.put("users", users);
resultMap.put("hits", hits);
resultMap.put("errors", errors);
JSONObject serices = new JSONObject(resultMap);
data.setxAxis(StringUtils.join(",", timeList));
data.setSerices(serices.toString());
return data;
}
public static ChartsData getResponseTimeChartData(String jtlString) {
ChartsData chartsData = new ChartsData(); ChartsData chartsData = new ChartsData();
chartsData.setxAxis(timeStamp);
chartsData.setGroupName("hits");
chartsData.setyAxis(new BigDecimal(metrics.size() * 1.0 / maxUsers));
chartsDataList.add(chartsData);
chartsData = new ChartsData();
chartsData.setxAxis(timeStamp);
chartsData.setGroupName("users");
chartsData.setyAxis(new BigDecimal(maxUsers));
chartsDataList.add(chartsData);
chartsData = new ChartsData();
chartsData.setxAxis(timeStamp);
chartsData.setGroupName("errors");
chartsData.setyAxis(new BigDecimal(failSize));
chartsDataList.add(chartsData);
}
return chartsDataList;
}
public static List<ChartsData> getResponseTimeChartData(String jtlString) {
List<ChartsData> chartsDataList = new ArrayList<>();
List<Metric> totalMetricList = JtlResolver.resolver(jtlString); List<Metric> totalMetricList = JtlResolver.resolver(jtlString);
totalMetricList.forEach(metric -> { totalMetricList.forEach(metric -> {
@ -373,29 +376,34 @@ public class JtlResolver {
Map<String, List<Metric>> metricMap = totalMetricList.stream().collect(Collectors.groupingBy(Metric::getTimestamp)); Map<String, List<Metric>> metricMap = totalMetricList.stream().collect(Collectors.groupingBy(Metric::getTimestamp));
List<Map.Entry<String, List<Metric>>> entries = new ArrayList<>(metricMap.entrySet()); List<Map.Entry<String, List<Metric>>> entries = new ArrayList<>(metricMap.entrySet());
entries.sort(JtlResolver::sortByDate);
List<String> resTimeList = new ArrayList<>();
List<String> users = new ArrayList<>();
List<String> timestampList = new ArrayList<>();
for (Map.Entry<String, List<Metric>> entry : entries) { for (Map.Entry<String, List<Metric>> entry : entries) {
List<Metric> metricList = entry.getValue(); List<Metric> metricList = entry.getValue();
Map<String, List<Metric>> metricsMap = metricList.stream().collect(Collectors.groupingBy(Metric::getThreadName)); Map<String, List<Metric>> metricsMap = metricList.stream().collect(Collectors.groupingBy(Metric::getThreadName));
int maxUsers = metricsMap.size(); int maxUsers = metricsMap.size();
int sumElapsedTime = metricList.stream().mapToInt(metric -> Integer.parseInt(metric.getElapsed())).sum(); int sumElapsedTime = metricList.stream().mapToInt(metric -> Integer.parseInt(metric.getElapsed())).sum();
timestampList.add(entry.getKey()); String timeStamp = "";
users.add(String.valueOf(maxUsers)); try {
resTimeList.add(String.valueOf(sumElapsedTime / metricList.size())); timeStamp = formatDate(entry.getKey());
} catch (ParseException e) {
e.printStackTrace();
} }
Map<String, Object> resultMap = new HashMap<>(2); ChartsData chartsData = new ChartsData();
resultMap.put("users", users); chartsData.setxAxis(timeStamp);
resultMap.put("resTime", resTimeList); chartsData.setGroupName("users");
JSONObject serices = new JSONObject(resultMap); chartsData.setyAxis(new BigDecimal(maxUsers));
chartsData.setxAxis(StringUtils.join(",", timestampList)); chartsDataList.add(chartsData);
chartsData.setSerices(serices.toString());
return chartsData; ChartsData chartsData2 = new ChartsData();
chartsData2.setxAxis(timeStamp);
chartsData2.setGroupName("responseTime");
chartsData2.setyAxis(new BigDecimal(sumElapsedTime * 1.0 / metricList.size()));
chartsDataList.add(chartsData2);
}
return chartsDataList;
} }
private static String stampToDate(String timeStamp) { private static String stampToDate(String timeStamp) {
@ -405,24 +413,14 @@ public class JtlResolver {
return simpleDateFormat.format(date); return simpleDateFormat.format(date);
} }
private static int sortByDate(Map.Entry<String, List<Metric>> map1, Map.Entry<String, List<Metric>> map2) { /**
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); * @param "yyyy-MM-dd HH:mm:ss"
Date date1 = null, date2 = null; * @return "HH:mm:ss"
try { */
date1 = simpleDateFormat.parse(map1.getKey()); private static String formatDate(String dateString) throws ParseException {
date2 = simpleDateFormat.parse(map2.getKey()); SimpleDateFormat before = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} catch (ParseException e) { SimpleDateFormat after = new SimpleDateFormat("HH:mm:ss");
e.printStackTrace(); return after.format(before.parse(dateString));
}
Long time1 = date1.getTime();
Long time2 = date2.getTime();
if (time1.equals(time2)) {
return 0;
} else {
return time1 > time2 ? 1 : -1;
} }
} }
}

View File

@ -1,9 +1,33 @@
package io.metersphere.report.base; package io.metersphere.report.base;
import java.math.BigDecimal;
public class ChartsData { public class ChartsData {
/**
* X
*/
private String xAxis; private String xAxis;
private String serices;
/**
* Y
*/
private BigDecimal yAxis = BigDecimal.ZERO;
/**
* Y 轴右侧
*/
private BigDecimal yAxis2 = BigDecimal.ZERO;
/**
* series 名称
*/
private String groupName;
/**
* 描述
*/
private String description;
public String getxAxis() { public String getxAxis() {
return xAxis; return xAxis;
@ -13,11 +37,35 @@ public class ChartsData {
this.xAxis = xAxis; this.xAxis = xAxis;
} }
public String getSerices() { public BigDecimal getyAxis() {
return serices; return yAxis;
} }
public void setSerices(String serices) { public void setyAxis(BigDecimal yAxis) {
this.serices=serices; this.yAxis = yAxis;
}
public BigDecimal getyAxis2() {
return yAxis2;
}
public void setyAxis2(BigDecimal yAxis2) {
this.yAxis2 = yAxis2;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
} }
} }

View File

@ -84,20 +84,20 @@ public class ReportService {
return testOverview; return testOverview;
} }
public ChartsData getLoadChartData(String id) { public List<ChartsData> getLoadChartData(String id) {
checkReportStatus(id); checkReportStatus(id);
LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(id); LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(id);
String content = loadTestReport.getContent(); String content = loadTestReport.getContent();
ChartsData chartsData = JtlResolver.getLoadChartData(content); List<ChartsData> chartsDataList = JtlResolver.getLoadChartData(content);
return chartsData; return chartsDataList;
} }
public ChartsData getResponseTimeChartData(String id) { public List<ChartsData> getResponseTimeChartData(String id) {
checkReportStatus(id); checkReportStatus(id);
LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(id); LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(id);
String content = loadTestReport.getContent(); String content = loadTestReport.getContent();
ChartsData chartsData = JtlResolver.getResponseTimeChartData(content); List<ChartsData> chartsDataList = JtlResolver.getResponseTimeChartData(content);
return chartsData; return chartsDataList;
} }
public void checkReportStatus(String reportId) { public void checkReportStatus(String reportId) {

View File

@ -1,6 +1,7 @@
package io.metersphere.service; package io.metersphere.service;
import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.*; import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.ProjectMapper; import io.metersphere.base.mapper.ProjectMapper;
import io.metersphere.base.mapper.TestCaseMapper; import io.metersphere.base.mapper.TestCaseMapper;
@ -114,7 +115,7 @@ public class TestCaseService {
} }
public List<TestCase> recentTestPlans(QueryTestCaseRequest request) { public List<TestCase> recentTestPlans(QueryTestCaseRequest request, int count) {
if (StringUtils.isBlank(request.getWorkspaceId())) { if (StringUtils.isBlank(request.getWorkspaceId())) {
return null; return null;
@ -126,9 +127,23 @@ public class TestCaseService {
List<String> projectIds = projectMapper.selectByExample(projectExample).stream() List<String> projectIds = projectMapper.selectByExample(projectExample).stream()
.map(Project::getId).collect(Collectors.toList()); .map(Project::getId).collect(Collectors.toList());
if (projectIds.isEmpty()) {
return null;
}
PageHelper.startPage(1, count, true);
TestCaseExample testCaseExample = new TestCaseExample(); TestCaseExample testCaseExample = new TestCaseExample();
testCaseExample.createCriteria().andProjectIdIn(projectIds); testCaseExample.createCriteria().andProjectIdIn(projectIds);
testCaseExample.setOrderByClause("update_time desc"); testCaseExample.setOrderByClause("update_time desc");
return testCaseMapper.selectByExample(testCaseExample); return testCaseMapper.selectByExample(testCaseExample);
} }
public Project getProjectByTestCaseId(String testCaseId) {
TestCaseWithBLOBs testCaseWithBLOBs = testCaseMapper.selectByPrimaryKey(testCaseId);
if (testCaseWithBLOBs == null) {
return null;
}
return projectMapper.selectByPrimaryKey(testCaseWithBLOBs.getProjectId());
}
} }

View File

@ -15,6 +15,7 @@ import io.metersphere.controller.request.resourcepool.QueryResourcePoolRequest;
import io.metersphere.dto.NodeDTO; import io.metersphere.dto.NodeDTO;
import io.metersphere.dto.TestResourcePoolDTO; import io.metersphere.dto.TestResourcePoolDTO;
import io.metersphere.engine.kubernetes.provider.KubernetesProvider; import io.metersphere.engine.kubernetes.provider.KubernetesProvider;
import io.metersphere.i18n.Translator;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -83,7 +84,7 @@ public class TestResourcePoolService {
private void validateNodes(TestResourcePoolDTO testResourcePool) { private void validateNodes(TestResourcePoolDTO testResourcePool) {
if (CollectionUtils.isEmpty(testResourcePool.getResources())) { if (CollectionUtils.isEmpty(testResourcePool.getResources())) {
MSException.throwException("没有节点信息"); MSException.throwException(Translator.get("no_nodes_message"));
} }
deleteTestResource(testResourcePool.getId()); deleteTestResource(testResourcePool.getId());
@ -95,7 +96,7 @@ public class TestResourcePoolService {
.distinct() .distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
if (nodeIps.size() < testResourcePool.getResources().size()) { if (nodeIps.size() < testResourcePool.getResources().size()) {
MSException.throwException("节点 IP 重复"); MSException.throwException(Translator.get("duplicate_node_ip"));
} }
for (TestResource resource : testResourcePool.getResources()) { for (TestResource resource : testResourcePool.getResources()) {
NodeDTO nodeDTO = JSON.parseObject(resource.getConfiguration(), NodeDTO.class); NodeDTO nodeDTO = JSON.parseObject(resource.getConfiguration(), NodeDTO.class);
@ -124,7 +125,7 @@ public class TestResourcePoolService {
private void validateK8s(TestResourcePoolDTO testResourcePool) { private void validateK8s(TestResourcePoolDTO testResourcePool) {
if (CollectionUtils.isEmpty(testResourcePool.getResources()) || testResourcePool.getResources().size() != 1) { if (CollectionUtils.isEmpty(testResourcePool.getResources()) || testResourcePool.getResources().size() != 1) {
throw new RuntimeException("只能添加一个 K8s"); throw new RuntimeException(Translator.get("only_one_k8s"));
} }
TestResource testResource = testResourcePool.getResources().get(0); TestResource testResource = testResourcePool.getResources().get(0);

View File

@ -14,5 +14,8 @@
"run_load_test_file_content_not_found": "Cannot run test, cannot get test file content, test ID:", "run_load_test_file_content_not_found": "Cannot run test, cannot get test file content, test ID:",
"run_load_test_file_init_error": "Failed to run test, failed to initialize run environment, test ID:", "run_load_test_file_init_error": "Failed to run test, failed to initialize run environment, test ID:",
"load_test_is_running": "Load test is running, please wait.", "load_test_is_running": "Load test is running, please wait.",
"node_deep_limit": "The node depth does not exceed 5 layers!" "node_deep_limit": "The node depth does not exceed 5 layers!",
"no_nodes_message": "No node message",
"duplicate_node_ip": "Duplicate IPs",
"only_one_k8s": "Only one K8s can be added"
} }

View File

@ -14,5 +14,8 @@
"run_load_test_file_content_not_found": "无法运行测试无法获取测试文件内容测试ID", "run_load_test_file_content_not_found": "无法运行测试无法获取测试文件内容测试ID",
"run_load_test_file_init_error": "无法运行测试初始化运行环境失败测试ID", "run_load_test_file_init_error": "无法运行测试初始化运行环境失败测试ID",
"load_test_is_running": "测试正在运行, 请等待", "load_test_is_running": "测试正在运行, 请等待",
"node_deep_limit": "节点深度不超过5层" "node_deep_limit": "节点深度不超过5层",
"no_nodes_message": "没有节点信息",
"duplicate_node_ip": "节点 IP 重复",
"only_one_k8s": "只能添加一个 K8s"
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<el-col> <el-col>
<header-menus/> <api-header-menus/>
<div> <div>
<transition> <transition>
<keep-alive> <keep-alive>
@ -12,11 +12,11 @@
</template> </template>
<script> <script>
import HeaderMenus from "./head/HeaderMenus"; import ApiHeaderMenus from "./head/ApiHeaderMenus";
export default { export default {
name: "ApiTest", name: "ApiTest",
components: {HeaderMenus}, components: {ApiHeaderMenus},
data() { data() {
return { return {
beaseUrl: "api" beaseUrl: "api"

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="menu-bar"> <div id="menu-bar" v-if="isRouterAlive">
<el-menu class="header-menu" :unique-opened="true" mode="horizontal" router menu-trigger="click" <el-menu class="header-menu" :unique-opened="true" mode="horizontal" router
:default-active='$route.path'> :default-active='$route.path'>
<el-menu-item :index="'/api/home'"> <el-menu-item :index="'/api/home'">
{{ $t("i18n.home") }} {{ $t("i18n.home") }}
@ -24,7 +24,7 @@
<el-submenu v-if="isCurrentWorkspaceUser" <el-submenu v-if="isCurrentWorkspaceUser"
index="4" popper-class="submenu" v-permission="['test_manager', 'test_user']"> index="4" popper-class="submenu" v-permission="['test_manager', 'test_user']">
<template v-slot:title>{{$t('commons.test')}}</template> <template v-slot:title>{{$t('commons.test')}}</template>
<api-recent-test-plan/> <api-recent-test/>
<el-divider/> <el-divider/>
<el-menu-item :index="'/api/test/all'"> <el-menu-item :index="'/api/test/all'">
<font-awesome-icon :icon="['fa', 'list-ul']"/> <font-awesome-icon :icon="['fa', 'list-ul']"/>
@ -33,6 +33,8 @@
<el-menu-item :index="'/api/test/create'"> <el-menu-item :index="'/api/test/create'">
<el-button type="text">{{$t('load_test.create')}}</el-button> <el-button type="text">{{$t('load_test.create')}}</el-button>
</el-menu-item> </el-menu-item>
<el-menu-item :index="testCaseProjectPath" class="blank_item"></el-menu-item>
<el-menu-item :index="testEditPath" class="blank_item"></el-menu-item>
</el-submenu> </el-submenu>
<el-submenu v-if="isCurrentWorkspaceUser" <el-submenu v-if="isCurrentWorkspaceUser"
@ -44,6 +46,7 @@
<font-awesome-icon :icon="['fa', 'list-ul']"/> <font-awesome-icon :icon="['fa', 'list-ul']"/>
<span>{{$t('commons.show_all')}}</span> <span>{{$t('commons.show_all')}}</span>
</el-menu-item> </el-menu-item>
<el-menu-item :index="reportViewPath" class="blank_item"></el-menu-item>
</el-submenu> </el-submenu>
<router-link v-if="isCurrentWorkspaceUser" <router-link v-if="isCurrentWorkspaceUser"
@ -64,15 +67,45 @@
import {checkoutCurrentWorkspace} from "../../../../common/utils"; import {checkoutCurrentWorkspace} from "../../../../common/utils";
export default { export default {
name: "MsMenus", name: "ApiHeaderMenus",
components: {ApiRecentTest, ApiRecentReport, ApiRecentProject}, components: {ApiRecentTest, ApiRecentReport, ApiRecentProject},
data() { data() {
return { return {
isCurrentWorkspaceUser: false, isCurrentWorkspaceUser: false,
testCaseProjectPath: '',
testEditPath: '',
reportViewPath: '',
isRouterAlive: true
}
},
watch: {
'$route'(to, from) {
let path = to.path;
//
if (path.indexOf("/api/test/") >= 0){
this.testCaseProjectPath = '/api/test/' + this.$route.params.projectId;
this.reload();
}
if (path.indexOf("/api/test/edit/") >= 0){
this.testEditPath = '/api/test/edit/' + this.$route.params.testId;
this.reload();
}
if (path.indexOf("/api/report/view/") >= 0){
this.reportViewPath = '/api/report/view/' + this.$route.params.reportId;
this.reload();
}
} }
}, },
mounted() { mounted() {
this.isCurrentWorkspaceUser = checkoutCurrentWorkspace(); this.isCurrentWorkspaceUser = checkoutCurrentWorkspace();
},
methods: {
reload () {
this.isRouterAlive = false;
this.$nextTick(function () {
this.isRouterAlive = true;
})
}
} }
} }
@ -115,4 +148,9 @@
#menu-bar { #menu-bar {
border-bottom: 1px solid #E6E6E6; border-bottom: 1px solid #E6E6E6;
} }
.blank_item {
display: none;
}
</style> </style>

View File

@ -214,7 +214,7 @@ const router = new VueRouter({
component: TestCase, component: TestCase,
}, },
{ {
path: "plan/:projectId", path: "plan/:type",
name: "testPlan", name: "testPlan",
component: TestPlan component: TestPlan
}, },

View File

@ -1,6 +1,6 @@
<template> <template>
<el-col> <el-col>
<header-menus/> <performance-header-menus/>
<div> <div>
<transition> <transition>
<keep-alive> <keep-alive>
@ -13,11 +13,11 @@
<script> <script>
import HeaderMenus from "./head/HeaderMenus"; import PerformanceHeaderMenus from "./head/PerformanceHeaderMenus";
export default { export default {
name: "PerformanceTest", name: "PerformanceTest",
components: {HeaderMenus}, components: {PerformanceHeaderMenus},
data() { data() {
return { return {
beaseUrl: "performance" beaseUrl: "performance"

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="menu-bar"> <div id="menu-bar" v-if="isRouterAlive">
<el-menu class="header-menu" :unique-opened="true" mode="horizontal" router <el-menu class="header-menu" :unique-opened="true" mode="horizontal" router
:default-active='$route.path'> :default-active='$route.path'>
<el-menu-item :index="'/performance/home'"> <el-menu-item :index="'/performance/home'">
@ -33,6 +33,8 @@
<el-menu-item :index="'/performance/test/create'"> <el-menu-item :index="'/performance/test/create'">
<el-button type="text">{{$t('load_test.create')}}</el-button> <el-button type="text">{{$t('load_test.create')}}</el-button>
</el-menu-item> </el-menu-item>
<el-menu-item :index="testCaseProjectPath" class="blank_item"></el-menu-item>
<el-menu-item :index="testEditPath" class="blank_item"></el-menu-item>
</el-submenu> </el-submenu>
<el-submenu v-if="isCurrentWorkspaceUser" <el-submenu v-if="isCurrentWorkspaceUser"
@ -44,6 +46,7 @@
<font-awesome-icon :icon="['fa', 'list-ul']"/> <font-awesome-icon :icon="['fa', 'list-ul']"/>
<span style="padding-left: 5px;">{{$t('commons.show_all')}}</span> <span style="padding-left: 5px;">{{$t('commons.show_all')}}</span>
</el-menu-item> </el-menu-item>
<el-menu-item :index="reportViewPath" class="blank_item"></el-menu-item>
</el-submenu> </el-submenu>
<router-link v-if="isCurrentWorkspaceUser" <router-link v-if="isCurrentWorkspaceUser"
@ -64,20 +67,45 @@
import {checkoutCurrentWorkspace} from "../../../../common/utils"; import {checkoutCurrentWorkspace} from "../../../../common/utils";
export default { export default {
name: "MsMenus", name: "PerformanceHeaderMenus",
components: {PerformanceRecentReport, PerformanceRecentTestPlan, PerformanceRecentProject}, components: {PerformanceRecentReport, PerformanceRecentTestPlan, PerformanceRecentProject},
data() { data() {
return { return {
isCurrentWorkspaceUser: false, isCurrentWorkspaceUser: false,
} testCaseProjectPath: '',
}, testEditPath: '',
props: { reportViewPath: '',
beaseUrl: { isRouterAlive: true
type: String
} }
}, },
mounted() { mounted() {
this.isCurrentWorkspaceUser = checkoutCurrentWorkspace(); this.isCurrentWorkspaceUser = checkoutCurrentWorkspace();
},
watch: {
'$route'(to, from) {
let path = to.path;
//
if (path.indexOf("/performance/test/") >= 0){
this.testCaseProjectPath = '/performance/test/' + this.$route.params.projectId;
this.reload();
}
if (path.indexOf("/performance/test/edit/") >= 0){
this.testEditPath = '/performance/test/edit/' + this.$route.params.testId;
this.reload();
}
if (path.indexOf("/performance/report/view/") >= 0){
this.reportViewPath = '/performance/report/view/' + this.$route.params.reportId;
this.reload();
}
}
},
methods: {
reload () {
this.isRouterAlive = false;
this.$nextTick(function () {
this.isRouterAlive = true;
})
}
} }
} }
@ -111,4 +139,8 @@
#menu-bar { #menu-bar {
border-bottom: 1px solid #E6E6E6; border-bottom: 1px solid #E6E6E6;
} }
.blank_item {
display: none;
}
</style> </style>

View File

@ -70,7 +70,7 @@
width="150" width="150"
:label="$t('commons.operating')"> :label="$t('commons.operating')">
<template v-slot:default="scope"> <template v-slot:default="scope">
<el-button @click="handleEdit(scope.row)" type="primary" icon="el-icon-edit" size="mini" circle/> <el-button @click="handleEdit(scope.row)" type="primary" icon="el-icon-s-data" size="mini" circle/>
<el-button @click="handleDelete(scope.row)" type="danger" icon="el-icon-delete" size="mini" circle/> <el-button @click="handleDelete(scope.row)" type="danger" icon="el-icon-delete" size="mini" circle/>
</template> </template>
</el-table-column> </el-table-column>

View File

@ -105,13 +105,8 @@
color: '#65A2FF' color: '#65A2FF'
}, },
}, },
legend: { legend: {},
bottom: 10, xAxis: {},
data: ['Users', 'Hits/s', 'Error(s)']
},
xAxis: {
type: 'category',
},
yAxis: [{ yAxis: [{
name: 'User', name: 'User',
type: 'value', type: 'value',
@ -128,28 +123,9 @@
// interval: 5 / 5 // interval: 5 / 5
} }
], ],
series: [ series: []
{
name: 'Users',
color: '#0CA74A',
type: 'line',
yAxisIndex: 0
},
{
name: 'Hits/s',
color: '#65A2FF',
type: 'line',
yAxisIndex: 1
},
{
name: 'Error(s)',
color: '#E6113C',
type: 'line',
yAxisIndex: 1
} }
] this.loadOption = this.generateOption(loadOption, data);
}
this.loadOption = this.generateLoadOption(loadOption, data);
}) })
this.$get("/report/content/res_chart/" + this.id, res => { this.$get("/report/content/res_chart/" + this.id, res => {
let data = res.data; let data = res.data;
@ -162,13 +138,8 @@
color: '#99743C' color: '#99743C'
}, },
}, },
legend: { legend: {},
bottom: 10, xAxis: {},
data: ['Users', 'Response Time']
},
xAxis: {
type: 'category'
},
yAxis: [{ yAxis: [{
name: 'User', name: 'User',
type: 'value', type: 'value',
@ -182,77 +153,40 @@
min: 0 min: 0
} }
], ],
series: [ series: []
{ }
name: 'Users', this.resOption = this.generateOption(resOption, data);
color: '#0CA74A', })
},
generateOption(option, data) {
let chartData = data;
let legend = [], series = {}, xAxis = [], seriesData = [];
chartData.forEach(item => {
if (!xAxis.includes(item.xAxis)) {
xAxis.push(item.xAxis);
}
xAxis.sort()
let name = item.groupName
if (!legend.includes(name)) {
legend.push(name)
series[name] = []
}
series[name].splice(xAxis.indexOf(item.xAxis), 0, item.yAxis.toFixed(2));
})
this.$set(option.legend, "data", legend);
this.$set(option.legend, "bottom", 10);
this.$set(option.xAxis, "data", xAxis);
for (let name in series) {
let data = series[name];
let items = {
name: name,
type: 'line', type: 'line',
yAxisIndex: 0 data: data
}, };
{ seriesData.push(items);
name: 'Response Time',
color: '#99743C',
type: 'line',
yAxisIndex: 1
} }
] this.$set(option, "series", seriesData);
} return option;
this.resOption = this.generateResponseOption(resOption, data);
})
},
_objToStrMap(obj){
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k,obj[k]);
}
return strMap;
},
_jsonToMap(jsonStr){
return this._objToStrMap(JSON.parse(jsonStr));
},
generateLoadOption(loadOption, data) {
let map = this._jsonToMap(data.serices);
let xAxis = data.xAxis;
this.$set(loadOption.xAxis, "data", xAxis.split(','));
let user = map.get("users").slice(0);
let hit = map.get("hits").slice(0);
user.sort(function (a,b) {
return parseInt(a) - parseInt(b);
})
hit.sort(function (a,b) {
return parseFloat(a) - parseFloat(b);
})
this.$set(loadOption.yAxis[0], "max",user[user.length-1]);
this.$set(loadOption.yAxis[0], "interval", user[user.length-1]/5);
this.$set(loadOption.yAxis[1], "max", hit[hit.length-1]);
this.$set(loadOption.yAxis[1], "interval", hit[hit.length-1]/5);
this.$set(loadOption.series[0], "data", map.get("users"));
this.$set(loadOption.series[1], "data", map.get("hits"));
this.$set(loadOption.series[2], "data", map.get("errors"));
return loadOption;
},
generateResponseOption(resOption, data) {
let map = this._jsonToMap(data.serices);
let user = map.get("users").slice(0);
let res = map.get("resTime").slice(0);
user.sort(function (a,b) {
return parseInt(a) - parseInt(b);
})
res.sort(function (a,b) {
return parseFloat(a) - parseFloat(b);
})
this.$set(resOption.yAxis[0], "max",user[user.length-1]);
this.$set(resOption.yAxis[0], "interval", user[user.length-1]/5);
this.$set(resOption.yAxis[1], "max", res[res.length-1]);
this.$set(resOption.yAxis[1], "interval", res[res.length-1]/5);
let xAxis = data.xAxis;
this.$set(resOption.xAxis, "data", xAxis.split(','));
this.$set(resOption.series[0], "data", map.get("users"));
this.$set(resOption.series[1], "data", map.get("resTime"));
return resOption;
}, },
}, },
watch: { watch: {

View File

@ -67,7 +67,7 @@
<script> <script>
import MsCreateBox from "../settings/CreateBox"; import MsCreateBox from "../settings/CreateBox";
import {Message} from "element-ui"; import {Message} from "element-ui";
import {CURRENT_PROJECT, TokenKey} from "../../../common/constants"; import {TokenKey} from "../../../common/constants";
export default { export default {
name: "MsProject", name: "MsProject",
@ -168,10 +168,6 @@
this.$get('/project/delete/' + row.id, () => { this.$get('/project/delete/' + row.id, () => {
Message.success(this.$t('commons.delete_success')); Message.success(this.$t('commons.delete_success'));
this.list(); this.list();
let currentProject = JSON.parse(localStorage.getItem(CURRENT_PROJECT));
if(currentProject && row.id === currentProject.id){
localStorage.removeItem(CURRENT_PROJECT);
}
}); });
}).catch(() => { }).catch(() => {
}); });

View File

@ -24,7 +24,8 @@
<el-table-column prop="organizationName" :label="$t('workspace.organization_name')"/> <el-table-column prop="organizationName" :label="$t('workspace.organization_name')"/>
<el-table-column :label="$t('commons.member')"> <el-table-column :label="$t('commons.member')">
<template v-slot:default="scope"> <template v-slot:default="scope">
<el-button type="text" class="member-size" @click="cellClick(scope.row)">{{scope.row.memberSize}}</el-button> <el-button type="text" class="member-size" @click="cellClick(scope.row)">{{scope.row.memberSize}}
</el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column> <el-table-column>
@ -63,7 +64,8 @@
<el-input type="textarea" v-model="form.description"></el-input> <el-input type="textarea" v-model="form.description"></el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('workspace.organization_name')" prop="organizationId"> <el-form-item :label="$t('workspace.organization_name')" prop="organizationId">
<el-select v-model="form.organizationId" :placeholder="$t('organization.select_organization')" class="select-width"> <el-select v-model="form.organizationId" :placeholder="$t('organization.select_organization')"
class="select-width">
<el-option <el-option
v-for="item in form.orgList" v-for="item in form.orgList"
:key="item.id" :key="item.id"
@ -90,7 +92,8 @@
<el-input type="textarea" v-model="form.description"></el-input> <el-input type="textarea" v-model="form.description"></el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('workspace.organization_name')" prop="organizationId"> <el-form-item :label="$t('workspace.organization_name')" prop="organizationId">
<el-select v-model="form.organizationId" :placeholder="$t('organization.select_organization')" class="select-width"> <el-select v-model="form.organizationId" :placeholder="$t('organization.select_organization')"
class="select-width">
<el-option <el-option
v-for="item in form.orgList1" v-for="item in form.orgList1"
:key="item.id" :key="item.id"
@ -102,7 +105,8 @@
</el-form> </el-form>
<template v-slot:footer> <template v-slot:footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="updateWorkspace('updateForm')" size="medium">{{$t('commons.save')}}</el-button> <el-button type="primary" @click="updateWorkspace('updateForm')"
size="medium">{{$t('commons.save')}}</el-button>
</span> </span>
</template> </template>
@ -160,10 +164,13 @@
</el-dialog> </el-dialog>
<!-- add workspace member dialog --> <!-- add workspace member dialog -->
<el-dialog :title="$t('member.create')" :visible.sync="addMemberVisible" width="30%" :destroy-on-close="true" @close="closeFunc"> <el-dialog :title="$t('member.create')" :visible.sync="addMemberVisible" width="30%" :destroy-on-close="true"
<el-form :model="memberForm" ref="form" :rules="wsMemberRule" label-position="right" label-width="100px" size="small"> @close="closeFunc">
<el-form :model="memberForm" ref="form" :rules="wsMemberRule" label-position="right" label-width="100px"
size="small">
<el-form-item :label="$t('commons.member')" prop="userIds"> <el-form-item :label="$t('commons.member')" prop="userIds">
<el-select v-model="memberForm.userIds" multiple :placeholder="$t('member.please_choose_member')" class="select-width"> <el-select v-model="memberForm.userIds" multiple :placeholder="$t('member.please_choose_member')"
class="select-width">
<el-option <el-option
v-for="item in memberForm.userList" v-for="item in memberForm.userList"
:key="item.id" :key="item.id"
@ -175,7 +182,8 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.role')" prop="roleIds"> <el-form-item :label="$t('commons.role')" prop="roleIds">
<el-select v-model="memberForm.roleIds" multiple :placeholder="$t('role.please_choose_role')" class="select-width"> <el-select v-model="memberForm.roleIds" multiple :placeholder="$t('role.please_choose_role')"
class="select-width">
<el-option <el-option
v-for="item in memberForm.roles" v-for="item in memberForm.roles"
:key="item.id" :key="item.id"
@ -193,7 +201,8 @@
</el-dialog> </el-dialog>
<!-- update workspace member dialog --> <!-- update workspace member dialog -->
<el-dialog :title="$t('member.modify')" :visible.sync="updateMemberVisible" width="30%" :destroy-on-close="true" @close="closeFunc"> <el-dialog :title="$t('member.modify')" :visible.sync="updateMemberVisible" width="30%" :destroy-on-close="true"
@close="closeFunc">
<el-form :model="memberForm" label-position="right" label-width="100px" size="small" ref="updateUserForm"> <el-form :model="memberForm" label-position="right" label-width="100px" size="small" ref="updateUserForm">
<el-form-item label="ID" prop="id"> <el-form-item label="ID" prop="id">
<el-input v-model="memberForm.id" autocomplete="off" :disabled="true"/> <el-input v-model="memberForm.id" autocomplete="off" :disabled="true"/>
@ -208,7 +217,8 @@
<el-input v-model="memberForm.phone" autocomplete="off"/> <el-input v-model="memberForm.phone" autocomplete="off"/>
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.role')" prop="roleIds"> <el-form-item :label="$t('commons.role')" prop="roleIds">
<el-select v-model="memberForm.roleIds" multiple :placeholder="$t('role.please_choose_role')" class="select-width"> <el-select v-model="memberForm.roleIds" multiple :placeholder="$t('role.please_choose_role')"
class="select-width">
<el-option <el-option
v-for="item in memberForm.allroles" v-for="item in memberForm.allroles"
:key="item.id" :key="item.id"
@ -220,7 +230,8 @@
</el-form> </el-form>
<template v-slot:footer> <template v-slot:footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button type="primary" @click="updateOrgMember('updateUserForm')" size="medium">{{$t('commons.save')}}</el-button> <el-button type="primary" @click="updateOrgMember('updateUserForm')"
size="medium">{{$t('commons.save')}}</el-button>
</span> </span>
</template> </template>
@ -254,7 +265,7 @@
if (this.form.id) { if (this.form.id) {
saveType = 'update' saveType = 'update'
} }
this.$post("/workspace/" + saveType, this.form, () => { this.result = this.$post("/workspace/" + saveType, this.form, () => {
this.createVisible = false; this.createVisible = false;
this.list(); this.list();
Message.success(this.$t('commons.save_success')); Message.success(this.$t('commons.save_success'));
@ -327,7 +338,7 @@
cancelButtonText: this.$t('commons.cancel'), cancelButtonText: this.$t('commons.cancel'),
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$get('/workspace/delete/' + row.id, () => { this.$get('/workspace/special/delete/' + row.id, () => {
Message.success(this.$t('commons.delete_success')); Message.success(this.$t('commons.delete_success'));
this.list(); this.list();
}); });

View File

@ -1,6 +1,6 @@
<template> <template>
<el-col> <el-col>
<header-menus/> <track-header-menus/>
<div> <div>
<transition> <transition>
<keep-alive> <keep-alive>
@ -13,11 +13,11 @@
<script> <script>
import HeaderMenus from "./head/HeaderMenus"; import TrackHeaderMenus from "./head/TrackHeaderMenus";
export default { export default {
name: "TrackHome", name: "TrackHome",
components: {HeaderMenus}, components: {TrackHeaderMenus},
data() { data() {
return { return {
beaseUrl: "track" beaseUrl: "track"

View File

@ -14,7 +14,7 @@
<label v-for="(item,index) in projects" :key="index"> <label v-for="(item,index) in projects" :key="index">
<el-menu-item @click="changeProject(item)"> <el-menu-item @click="changeProject(item)">
{{item.name}} {{item.name}}
<i class="el-icon-check" v-if="item.id === currentProject.id"></i> <i class="el-icon-check" v-if="currentProject && item.id === currentProject.id"></i>
</el-menu-item> </el-menu-item>
</label> </label>
</el-scrollbar> </el-scrollbar>
@ -22,6 +22,7 @@
</el-submenu> </el-submenu>
</el-menu> </el-menu>
<node-tree class="node_tree" <node-tree class="node_tree"
:current-project="currentProject"
@nodeSelectEvent="refreshTable" @nodeSelectEvent="refreshTable"
@refresh="refreshTable" @refresh="refreshTable"
ref="nodeTree"></node-tree> ref="nodeTree"></node-tree>
@ -30,6 +31,7 @@
<el-main class="main-content"> <el-main class="main-content">
<test-case-list <test-case-list
:current-project="currentProject"
@openTestCaseEditDialog="openTestCaseEditDialog" @openTestCaseEditDialog="openTestCaseEditDialog"
@testCaseEdit="openTestCaseEditDialog" @testCaseEdit="openTestCaseEditDialog"
ref="testCaseList"></test-case-list> ref="testCaseList"></test-case-list>
@ -67,50 +69,56 @@
total: 0, total: 0,
loadingRequire: {project: true, testCase: true}, loadingRequire: {project: true, testCase: true},
projects: [], projects: [],
initProjects: [],
currentProject: null, currentProject: null,
treeNodes: [] treeNodes: []
} }
}, },
created: function () { created() {
this.caseId = this.$route.params.caseId;
this.getProjects(); this.getProjects();
}, },
mounted() {
if (this.$route.path.indexOf("/track/case/edit") >= 0){
this.openRecentTestCaseEditDialog();
}
},
watch: { watch: {
'$route'(to, from) { '$route'(to, from) {
let path = to.path; let path = to.path;
if (path.indexOf("/track/case/all") >= 0){ if (path.indexOf("/track/case/all") >= 0){
this.getProjects();
this.refresh(); this.refresh();
} }
if (path.indexOf("/track/case/edit") >= 0){ if (path.indexOf("/track/case/edit") >= 0){
let caseId = this.$route.params.caseId; this.openRecentTestCaseEditDialog();
this.$get('/test/case/get/' + caseId, response => { this.$router.push('/track/case/all');
console.log(response.data);
this.openTestCaseEditDialog(response.data[0]);
});
} }
} }
}, },
methods: { methods: {
getProjects() { getProjects() {
this.$get("/project/listAll", (response) => { this.$get("/project/listAll", (response) => {
if (response.success) {
this.projects = response.data; this.projects = response.data;
this.initProjects = this.projects.slice(0, 4);
if (localStorage.getItem(CURRENT_PROJECT)) { if (localStorage.getItem(CURRENT_PROJECT)) {
this.currentProject = JSON.parse(localStorage.getItem(CURRENT_PROJECT)); let lastProject = JSON.parse(localStorage.getItem(CURRENT_PROJECT));
} else { let hasCurrentProject = false;
this.currentProject = this.projects[0]; for (let i = 0; i < this.projects.length; i++) {
if (this.projects[i].id == lastProject.id) {
this.currentProject = lastProject;
hasCurrentProject = true;
break;
}
}
if (!hasCurrentProject) {
this.currentProject = null;
}
if(this.projects.length > 0){ if(this.projects.length > 0){
this.currentProject = this.projects[0];
localStorage.setItem(CURRENT_PROJECT, JSON.stringify(this.projects[0])); localStorage.setItem(CURRENT_PROJECT, JSON.stringify(this.projects[0]));
} }
}
} else { } else {
this.$message()({ if(this.projects.length > 0){
type: 'warning', this.currentProject = this.projects[0];
message: response.message localStorage.setItem(CURRENT_PROJECT, JSON.stringify(this.projects[0]));
}); }
} }
this.loadingRequire.project = false; this.loadingRequire.project = false;
// this.checkProject(); // this.checkProject();
@ -164,9 +172,24 @@
this.$refs.testCaseEditDialog.maintainerOptions = response.data; this.$refs.testCaseEditDialog.maintainerOptions = response.data;
}); });
}, },
getProjectByCaseId(caseId) {
return this.$get('/test/case/project/' + caseId, async response => {
localStorage.setItem(CURRENT_PROJECT, JSON.stringify(response.data));
this.refresh();
});
},
refresh() { refresh() {
this.$refs.testCaseList.initTableData(); this.$refs.testCaseList.initTableData();
this.$refs.nodeTree.getNodeTree(); this.$refs.nodeTree.getNodeTree();
this.getProjects();
},
openRecentTestCaseEditDialog() {
let caseId = this.$route.params.caseId;
this.getProjectByCaseId(caseId);
this.refresh();
this.$get('/test/case/get/' + caseId, response => {
this.openTestCaseEditDialog(response.data[0]);
});
} }
} }
} }

View File

@ -91,9 +91,17 @@
defaultKeys: [] defaultKeys: []
}; };
}, },
props: {
currentProject: {
type: Object
}
},
watch: { watch: {
filterText(val) { filterText(val) {
this.$refs.tree.filter(val); this.$refs.tree.filter(val);
},
currentProject() {
this.getNodeTree();
} }
}, },
created() { created() {
@ -163,8 +171,8 @@
this.dialogFormVisible = true; this.dialogFormVisible = true;
}, },
getNodeTree() { getNodeTree() {
if (localStorage.getItem(CURRENT_PROJECT)) { if (this.currentProject) {
let projectId = JSON.parse(localStorage.getItem(CURRENT_PROJECT)).id; let projectId = this.currentProject.id;
this.$get("/case/node/list/" + projectId, response => { this.$get("/case/node/list/" + projectId, response => {
this.treeNodes = response.data; this.treeNodes = response.data;
}); });

View File

@ -103,13 +103,16 @@
:data="form.steps" :data="form.steps"
class="tb-edit" class="tb-edit"
border border
size="mini"
:default-sort = "{prop: 'num', order: 'ascending'}" :default-sort = "{prop: 'num', order: 'ascending'}"
highlight-current-row> highlight-current-row>
<el-table-column :label="$t('test_track.number')" prop="num" min-width="15%"></el-table-column> <el-table-column :label="$t('test_track.number')" prop="num" min-width="15%"></el-table-column>
<el-table-column :label="$t('test_track.step_desc')" prop="desc" min-width="35%"> <el-table-column :label="$t('test_track.step_desc')" prop="desc" min-width="35%">
<template v-slot:default="scope"> <template v-slot:default="scope">
<el-input <el-input
size="small" size="mini"
type="textarea"
:rows="2"
v-model="scope.row.desc" v-model="scope.row.desc"
:placeholder="$t('commons.input_content')" :placeholder="$t('commons.input_content')"
clearable></el-input> clearable></el-input>
@ -119,7 +122,9 @@
<el-table-column :label="$t('test_track.expected_results')" prop="result" min-width="35%"> <el-table-column :label="$t('test_track.expected_results')" prop="result" min-width="35%">
<template v-slot:default="scope"> <template v-slot:default="scope">
<el-input <el-input
size="small" size="mini"
type="textarea"
:rows="2"
v-model="scope.row.result" v-model="scope.row.result"
:placeholder="$t('commons.input_content')" :placeholder="$t('commons.input_content')"
clearable></el-input> clearable></el-input>
@ -304,13 +309,13 @@
<style scoped> <style scoped>
.tb-edit .el-input { .tb-edit .el-textarea {
display: none; display: none;
} }
.tb-edit .current-row .el-input { .tb-edit .current-row .el-textarea {
display: block; display: block;
} }
.tb-edit .current-row .el-input+span { .tb-edit .current-row .el-textarea+span {
display: none; display: none;
} }

View File

@ -139,10 +139,19 @@
testId: null testId: null
} }
}, },
props: {
currentProject: {
type: Object
}
},
created: function () { created: function () {
this.initTableData(); this.initTableData();
}, },
watch: {
currentProject() {
this.initTableData();
}
},
methods: { methods: {
initTableData(nodeIds) { initTableData(nodeIds) {
let param = { let param = {
@ -150,16 +159,15 @@
}; };
param.nodeIds = nodeIds; param.nodeIds = nodeIds;
if(localStorage.getItem(CURRENT_PROJECT)) { if (this.currentProject) {
param.projectId = JSON.parse(localStorage.getItem(CURRENT_PROJECT)).id; param.projectId = this.currentProject.id;
}
this.$post(this.buildPagePath('/test/case/list'), param, response => { this.$post(this.buildPagePath('/test/case/list'), param, response => {
this.loadingRequire.testCase = false; this.loadingRequire.testCase = false;
let data = response.data; let data = response.data;
this.total = data.itemCount; this.total = data.itemCount;
this.tableData = data.listObject; this.tableData = data.listObject;
}); });
}
}, },
search() { search() {
this.initTableData(); this.initTableData();

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="menu-bar"> <div id="menu-bar" v-if="isRouterAlive">
<el-menu class="header-menu" :unique-opened="true" mode="horizontal" router <el-menu class="header-menu" :unique-opened="true" mode="horizontal" router
:default-active='$route.path'> :default-active='$route.path'>
<el-menu-item :index="'/track/home'"> <el-menu-item :index="'/track/home'">
@ -30,9 +30,7 @@
<font-awesome-icon :icon="['fa', 'list-ul']"/> <font-awesome-icon :icon="['fa', 'list-ul']"/>
<span style="padding-left: 5px;">{{$t('test_track.case_list')}}</span> <span style="padding-left: 5px;">{{$t('test_track.case_list')}}</span>
</el-menu-item> </el-menu-item>
<!--<el-menu-item :index="'/' + beaseUrl + '/case/create'">--> <el-menu-item :index="testCaseEditPath" class="blank_item"></el-menu-item>
<!--<el-button type="text">{{$t('test_track.create_case')}}</el-button>-->
<!--</el-menu-item>-->
</el-submenu> </el-submenu>
<el-submenu v-if="isCurrentWorkspaceUser" <el-submenu v-if="isCurrentWorkspaceUser"
@ -40,11 +38,12 @@
<template v-slot:title>{{$t('test_track.test_plan')}}</template> <template v-slot:title>{{$t('test_track.test_plan')}}</template>
<recent-test-plan/> <recent-test-plan/>
<el-divider/> <el-divider/>
<el-menu-item :index="'/track/plan/all'"> <el-menu-item index="/track/plan/all">
<font-awesome-icon :icon="['fa', 'list-ul']"/> <font-awesome-icon :icon="['fa', 'list-ul']"/>
<span style="padding-left: 5px;">{{$t('commons.show_all')}}</span> <span style="padding-left: 5px;">{{$t('commons.show_all')}}</span>
</el-menu-item> </el-menu-item>
<el-menu-item :index="'/track/plan/create'"> <el-menu-item :index="testPlanViewPath" class="blank_item"></el-menu-item>
<el-menu-item index="/track/plan/create">
<el-button type="text">{{$t('test_track.create_plan')}}</el-button> <el-button type="text">{{$t('test_track.create_plan')}}</el-button>
</el-menu-item> </el-menu-item>
</el-submenu> </el-submenu>
@ -53,7 +52,6 @@
</div> </div>
</template> </template>
<script> <script>
import {checkoutCurrentWorkspace} from "../../../../common/utils"; import {checkoutCurrentWorkspace} from "../../../../common/utils";
@ -62,25 +60,45 @@
import RecentTestPlan from "../plan/components/RecentTestPlan"; import RecentTestPlan from "../plan/components/RecentTestPlan";
export default { export default {
name: "MsMenus", name: "TrackHeaderMenus",
components: {RecentTestCase, TrackRecentProject, RecentTestPlan}, components: {RecentTestCase, TrackRecentProject, RecentTestPlan},
data() { data() {
return { return {
isCurrentWorkspaceUser: false, isCurrentWorkspaceUser: false,
testPlanViewPath: '',
isRouterAlive: true,
testCaseEditPath: ''
} }
}, },
props: { watch: {
beaseUrl: { '$route'(to, from) {
type: String let path = to.path;
if (path.indexOf("/track/plan/view") >= 0){
this.testPlanViewPath = '/track/plan/view/' + this.$route.params.planId;
this.reload();
}
if (path.indexOf("/track/case/edit") >= 0){
this.testCaseEditPath = '/track/case/edit/' + this.$route.params.caseId;
this.reload();
}
} }
}, },
mounted() { mounted() {
this.isCurrentWorkspaceUser = checkoutCurrentWorkspace(); this.isCurrentWorkspaceUser = checkoutCurrentWorkspace();
},
methods: {
reload () {
this.isRouterAlive = false;
this.$nextTick(function () {
this.isRouterAlive = true;
})
}
} }
} }
</script> </script>
<style> <style>
.header-menu.el-menu--horizontal > li { .header-menu.el-menu--horizontal > li {
@ -100,6 +118,8 @@
margin-left: 20%; margin-left: 20%;
} }
</style> </style>
<style scoped> <style scoped>
@ -109,4 +129,8 @@
#menu-bar { #menu-bar {
border-bottom: 1px solid #E6E6E6; border-bottom: 1px solid #E6E6E6;
} }
.blank_item {
display: none;
}
</style> </style>

View File

@ -25,6 +25,20 @@
return { return {
} }
}, },
mounted() {
if (this.$route.path.indexOf("/track/plan/create") >= 0){
this.openTestPlanEditDialog();
this.$router.push('/track/plan/all');
}
},
watch: {
'$route'(to, from) {
if (to.path.indexOf("/track/plan/create") >= 0){
this.openTestPlanEditDialog();
this.$router.push('/track/plan/all');
}
}
},
methods: { methods: {
openTestPlanEditDialog(data) { openTestPlanEditDialog(data) {
this.$refs.testPlanEditDialog.openTestPlanEditDialog(data); this.$refs.testPlanEditDialog.openTestPlanEditDialog(data);

View File

@ -13,19 +13,20 @@
<test-case-plan-list <test-case-plan-list
@openTestCaseRelevanceDialog="openTestCaseRelevanceDialog" @openTestCaseRelevanceDialog="openTestCaseRelevanceDialog"
@editTestPlanTestCase="editTestPlanTestCase" @editTestPlanTestCase="editTestPlanTestCase"
@refresh="refresh"
:plan-id="planId" :plan-id="planId"
ref="testCasePlanList"></test-case-plan-list> ref="testCasePlanList"></test-case-plan-list>
</el-main> </el-main>
</el-container> </el-container>
<test-case-relevance <test-case-relevance
@refresh="getPlanCases" @refresh="refresh"
:plan-id="planId" :plan-id="planId"
ref="testCaseRelevance"></test-case-relevance> ref="testCaseRelevance"></test-case-relevance>
<test-plan-test-case-edit <test-plan-test-case-edit
ref="testPlanTestCaseEdit" ref="testPlanTestCaseEdit"
@refresh="getPlanCases"> @refresh="refresh">
</test-plan-test-case-edit> </test-plan-test-case-edit>
@ -65,7 +66,8 @@
}, },
methods: { methods: {
refresh() { refresh() {
this.getPlanCases();
this.getNodeTreeByPlanId();
}, },
getPlanCases(nodeIds) { getPlanCases(nodeIds) {
this.$refs.testCasePlanList.initTableData(nodeIds); this.$refs.testCasePlanList.initTableData(nodeIds);
@ -84,6 +86,16 @@
let item = {}; let item = {};
Object.assign(item, testCase); Object.assign(item, testCase);
item.results = JSON.parse(item.results); item.results = JSON.parse(item.results);
item.steps = JSON.parse(item.steps);
item.steptResults = [];
for (let i = 0; i < item.steps.length; i++){
if(item.results[i]){
item.steps[i].actualResult = item.results[i].actualResult;
item.steps[i].executeResult = item.results[i].executeResult;
}
item.steptResults.push(item.steps[i]);
}
this.$refs.testPlanTestCaseEdit.testCase = item; this.$refs.testPlanTestCaseEdit.testCase = item;
this.$refs.testPlanTestCaseEdit.dialog = true; this.$refs.testPlanTestCaseEdit.dialog = true;
} }

View File

@ -211,6 +211,7 @@
let testCaseId = testCase.id; let testCaseId = testCase.id;
this.$post('/test/plan/case/delete/' + testCaseId, {}, () => { this.$post('/test/plan/case/delete/' + testCaseId, {}, () => {
this.initTableData(); this.initTableData();
this.$emit("refresh");
this.$message({ this.$message({
message: this.$t('commons.delete_success'), message: this.$t('commons.delete_success'),
type: 'success' type: 'success'

View File

@ -237,4 +237,8 @@
margin-right: -9px; margin-right: -9px;
float: right; float: right;
} }
.el-table {
cursor:pointer;
}
</style> </style>

View File

@ -8,8 +8,7 @@
size="100%" size="100%"
ref="drawer"> ref="drawer">
<div> <div class="case_container">
<el-row > <el-row >
<el-col :span="4" :offset="1"> <el-col :span="4" :offset="1">
<span class="cast_label">{{$t('test_track.priority')}}</span> <span class="cast_label">{{$t('test_track.priority')}}</span>
@ -35,6 +34,70 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row>
<el-col :span="20" :offset="1">
<el-table
:data="testCase.steptResults"
class="tb-edit"
size="mini"
height="200px"
:default-sort = "{prop: 'num', order: 'ascending'}"
highlight-current-row>
<el-table-column :label="$t('test_track.number')" prop="num" min-width="5%"></el-table-column>
<el-table-column :label="$t('test_track.step_desc')" prop="desc" min-width="29%">
<template v-slot:default="scope">
<span>{{scope.row.desc}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('test_track.expected_results')" prop="result" min-width="28%">
<template v-slot:default="scope">
<span>{{scope.row.result}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('test_track.actual_result')" min-width="29%">
<template v-slot:default="scope">
<el-input
size="mini"
type="textarea"
:rows="2"
v-model="scope.row.actualResult"
:placeholder="$t('commons.input_content')"
clearable></el-input>
<span>{{scope.row.actualResult}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('test_track.step_result')" min-width="9%">
<template v-slot:default="scope">
<el-select
v-model="scope.row.executeResult"
size="mini">
<el-option :label="$t('test_track.pass')" value="Pass"></el-option>
<el-option :label="$t('test_track.failure')" value="Failure"></el-option>
<el-option :label="$t('test_track.blocking')" value="Blocking"></el-option>
<el-option :label="$t('test_track.skip')" value="Skip"></el-option>
</el-select>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
<el-row >
<el-col :span="15" :offset="1">
<div style="margin-bottom: 5px;">
<span class="cast_label">{{$t('commons.remark')}}</span>
<span v-if="testCase.remark == null || testCase.remark == ''" style="color: darkgrey">{{$t('commons.not_filled')}}</span>
</div>
<div>
<el-input :rows="3"
type="textarea"
v-if="testCase.remark"
disabled
v-model="testCase.remark"></el-input>
</div>
</el-col>
</el-row>
<el-row> <el-row>
<el-col :offset="1" :span="2"> <el-col :offset="1" :span="2">
<el-button type="success" round <el-button type="success" round
@ -58,57 +121,6 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row>
<el-col :span="20" :offset="1">
<el-table
:data="testCase.results"
class="tb-edit"
:default-sort = "{prop: 'num', order: 'ascending'}"
highlight-current-row>
<el-table-column :label="$t('test_track.number')" prop="num" min-width="8%"></el-table-column>
<el-table-column :label="$t('test_track.step_desc')" prop="desc" min-width="30%">
<template v-slot:default="scope">
<span>{{scope.row.desc}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('test_track.expected_results')" prop="result" min-width="30%">
<template v-slot:default="scope">
<span>{{scope.row.result}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('test_track.actual_result')" min-width="30%">
<template v-slot:default="scope">
<el-input
size="small"
v-model="scope.row.actualResult"
:placeholder="$t('commons.input_content')"
clearable></el-input>
<span>{{scope.row.actualResult}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('test_track.step_result')" min-width="15%">
<template v-slot:default="scope">
<el-select v-model="scope.row.stepResult" :placeholder="$t('test_track.select_execute_result')">
<el-option :label="$t('test_track.pass')" value="Pass"></el-option>
<el-option :label="$t('test_track.failure')" value="Failure"></el-option>
<el-option :label="$t('test_track.blocking')" value="Blocking"></el-option>
<el-option :label="$t('test_track.skip')" value="Skip"></el-option>
</el-select>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
<el-row >
<el-col :span="40" :offset="1">
<span>{{$t('commons.remark')}}</span>
<span>{{testCase.remark}}</span>
<span v-if="testCase.remark == null" style="color: gainsboro">{{$t('commons.not_filled')}}</span>
</el-col>
</el-row>
<el-row type="flex" justify="end"> <el-row type="flex" justify="end">
<el-col :span="5"> <el-col :span="5">
<div> <div>
@ -146,7 +158,14 @@
let param = {}; let param = {};
param.id = this.testCase.id; param.id = this.testCase.id;
param.status = this.testCase.status; param.status = this.testCase.status;
param.results = JSON.stringify(this.testCase.results); param.results = [];
this.testCase.steptResults.forEach(item => {
let result = {};
result.actualResult = item.actualResult;
result.executeResult = item.executeResult;
param.results.push(result);
});
param.results = JSON.stringify(param.results);
this.$post('/test/plan/case/edit', param, () => { this.$post('/test/plan/case/edit', param, () => {
this.$refs.drawer.closeDrawer(); this.$refs.drawer.closeDrawer();
this.$message.success("保存成功!"); this.$message.success("保存成功!");
@ -160,20 +179,22 @@
<style scoped> <style scoped>
.tb-edit .el-input { .tb-edit .el-textarea {
display: none; display: none;
color: black;
} }
.tb-edit .current-row .el-input { .tb-edit .current-row .el-textarea {
display: block; display: block;
} }
.tb-edit .current-row .el-input+span { .tb-edit .current-row .el-textarea+span {
display: none; display: none;
} }
.el-row { .el-row {
margin-bottom: 3%; margin-bottom: 2%;
} }
.cast_label {
color: dimgray;
}
</style> </style>