diff --git a/backend/pom.xml b/backend/pom.xml index edd70316fa..e3a2c2754e 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -541,6 +541,11 @@ + + + + + diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.java index bfe5e65b55..17ae2078b3 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.java +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.java @@ -5,6 +5,7 @@ import io.metersphere.controller.request.BaseQueryRequest; import io.metersphere.track.dto.TestCaseDTO; import io.metersphere.track.request.testcase.QueryTestCaseRequest; import io.metersphere.track.request.testcase.TestCaseBatchRequest; +import io.metersphere.track.response.TrackCountResult; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -48,4 +49,32 @@ public interface ExtTestCaseMapper { int checkIsHave(@Param("caseId") String caseId, @Param("workspaceIds") Set workspaceIds); List selectIds(@Param("request") BaseQueryRequest condition); + + /** + * 按照用例等级统计 + * @param projectId 项目ID + * @return 统计结果 + */ + List countPriority(@Param("projectId") String projectId); + + long countCreatedThisWeek(@Param("projectId") String projectId, @Param("firstDayTimestamp") long firstDayTimestamp, @Param("lastDayTimestamp") long lastDayTimestamp); + + List countStatus(@Param("projectId") String projectId); + + List countRelevance(@Param("projectId") String projectId); + + long countRelevanceCreatedThisWeek(@Param("projectId") String projectId,@Param("firstDayTimestamp") long firstDayTimestamp, @Param("lastDayTimestamp") long lastDayTimestamp); + + List countCoverage(@Param("projectId") String projectId); + + List countFuncMaintainer(@Param("projectId") String projectId); + + List countRelevanceMaintainer(@Param("projectId") String projectId); + + int getTestPlanBug(@Param("planId") String planId); + int getTestPlanCase(@Param("planId") String planId); + int getTestPlanPassCase(@Param("planId") String planId); + + + } diff --git a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.xml index 6e746b0b81..72d3a06e38 100644 --- a/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.xml +++ b/backend/src/main/java/io/metersphere/base/mapper/ext/ExtTestCaseMapper.xml @@ -319,4 +319,87 @@ + + SELECT test_case.priority as groupField,count(id) AS countNumber FROM test_case WHERE project_id = #{projectId} GROUP BY test_case.priority + + + + + SELECT count(id) AS countNumber FROM test_case WHERE test_case.project_id = #{projectId} + AND create_time BETWEEN #{firstDayTimestamp} AND #{lastDayTimestamp} + + + + SELECT review_status AS groupField,count(id) AS countNumber FROM test_case WHERE project_id = #{projectId} GROUP BY test_case.review_status + + + + SELECT type AS groupField, count(id) AS countNumber FROM test_case WHERE test_case.project_id = #{projectId} GROUP BY test_case.type + + + SELECT count(id) AS countNumber FROM test_case WHERE test_case.project_id = #{projectId} + AND create_time BETWEEN #{firstDayTimestamp} AND #{lastDayTimestamp} + + + + SELECT count(test_case.id) AS countNumber, + if(test_case.test_id is null,"uncoverage","coverage") AS groupField + FROM test_case WHERE test_case.project_id=#{projectId} and test_case.type != 'functional' GROUP BY groupField + + + select count(tc.id) as countNumber, user.name as groupField from test_case tc right join user on tc.maintainer = user.id + where tc.project_id = #{projectId} + group by tc.maintainer + + + select count(tc.id) as countNumber, user.name as groupField from test_case tc right join user on tc.maintainer = user.id + where tc.project_id = #{projectId} and tc.test_id is not null + group by tc.maintainer + + + select count(tci.issues_id) from test_plan_test_case tptc join test_case_issues tci on tptc.case_id = tci.test_case_id + where tptc.plan_id = #{planId}; + + + select count(s) + from ( + select id as s + from test_plan_test_case tptc + where tptc.plan_id = #{planId} + union all + select id as s + from test_plan_api_scenario tpas + where tpas.test_plan_id = #{planId} + union all + select id as s + from test_plan_api_case tpac + where tpac.test_plan_id = #{planId} + union all + select id as s + from test_plan_load_case tplc + where tplc.test_plan_id = #{planId} + ) as temp + + + select count(s) + from ( + select id as s + from test_plan_test_case tptc + where tptc.plan_id = #{planId} and tptc.status = 'Pass' + union all + select id as s + from test_plan_api_scenario tpas + where tpas.test_plan_id = #{planId} and tpas.last_result = 'Success' + union all + select id as s + from test_plan_api_case tpac + where tpac.test_plan_id = #{planId} and tpac.status = 'success' + union all + select id as s + from test_plan_load_case tplc + where tplc.test_plan_id = #{planId} and tplc.status = 'success' + ) as temp + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/commons/utils/ShiroUtils.java b/backend/src/main/java/io/metersphere/commons/utils/ShiroUtils.java index 90c4e26e92..79ad0b6f70 100644 --- a/backend/src/main/java/io/metersphere/commons/utils/ShiroUtils.java +++ b/backend/src/main/java/io/metersphere/commons/utils/ShiroUtils.java @@ -42,10 +42,13 @@ public class ShiroUtils { //api-对外文档页面提供的查询接口 filterChainDefinitionMap.put("/api/document/**", "anon"); // filterChainDefinitionMap.put("/document/**", "anon"); + filterChainDefinitionMap.put("/system/theme", "anon"); + } public static void ignoreCsrfFilter(Map filterChainDefinitionMap) { filterChainDefinitionMap.put("/", "apikey, authc"); // 跳转到 / 不用校验 csrf + filterChainDefinitionMap.put("/language", "apikey, authc");// 跳转到 /language 不用校验 csrf filterChainDefinitionMap.put("/document", "apikey, authc"); // 跳转到 /document 不用校验 csrf } diff --git a/backend/src/main/java/io/metersphere/performance/parse/xml/reader/jmx/JmeterDocumentParser.java b/backend/src/main/java/io/metersphere/performance/parse/xml/reader/jmx/JmeterDocumentParser.java index 13ffc9a34e..09fbda18a5 100644 --- a/backend/src/main/java/io/metersphere/performance/parse/xml/reader/jmx/JmeterDocumentParser.java +++ b/backend/src/main/java/io/metersphere/performance/parse/xml/reader/jmx/JmeterDocumentParser.java @@ -676,6 +676,13 @@ public class JmeterDocumentParser implements DocumentParser { ((List>) durations).remove(0); duration = o.toString(); } + Object units = context.getProperty("unit"); + String unit = "S"; + if (units instanceof List) { + Object o = ((List>) units).get(0); + ((List>) units).remove(0); + unit = o.toString(); + } Object deleteds = context.getProperty("deleted"); String deleted = "false"; if (deleteds instanceof List) { @@ -691,6 +698,17 @@ public class JmeterDocumentParser implements DocumentParser { enabled = o.toString(); } + switch (unit) { + case "M": + duration = String.valueOf(Long.parseLong(duration) * 60); + break; + case "H": + duration = String.valueOf(Long.parseLong(duration) * 60 * 60); + break; + default: + break; + } + threadGroup.setAttribute("enabled", enabled); if (BooleanUtils.toBoolean(deleted)) { threadGroup.setAttribute("enabled", "false"); @@ -761,6 +779,13 @@ public class JmeterDocumentParser implements DocumentParser { ((List>) holds).remove(0); hold = o.toString(); } + Object units = context.getProperty("unit"); + String unit = "S"; + if (units instanceof List) { + Object o = ((List>) units).get(0); + ((List>) units).remove(0); + unit = o.toString(); + } Object deleteds = context.getProperty("deleted"); String deleted = "false"; if (deleteds instanceof List) { @@ -776,6 +801,17 @@ public class JmeterDocumentParser implements DocumentParser { enabled = o.toString(); } + switch (unit) { + case "M": + hold = String.valueOf(Long.parseLong(hold) * 60); + break; + case "H": + hold = String.valueOf(Long.parseLong(hold) * 60 * 60); + break; + default: + break; + } + threadGroup.setAttribute("enabled", enabled); if (BooleanUtils.toBoolean(deleted)) { threadGroup.setAttribute("enabled", "false"); @@ -928,10 +964,10 @@ public class JmeterDocumentParser implements DocumentParser { } private Element createStringProp(Document document, String name, String value) { - Element unit = document.createElement(STRING_PROP); - unit.setAttribute("name", name); - unit.appendChild(document.createTextNode(value)); - return unit; + Element element = document.createElement(STRING_PROP); + element.setAttribute("name", name); + element.appendChild(document.createTextNode(value)); + return element; } private void processThreadGroupName(Element threadGroup) { diff --git a/backend/src/main/java/io/metersphere/track/controller/TrackController.java b/backend/src/main/java/io/metersphere/track/controller/TrackController.java new file mode 100644 index 0000000000..c010d94bc2 --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/controller/TrackController.java @@ -0,0 +1,93 @@ +package io.metersphere.track.controller; + + +import io.metersphere.commons.constants.RoleConstants; +import io.metersphere.performance.base.ChartsData; +import io.metersphere.track.response.BugStatustics; +import io.metersphere.track.response.TrackCountResult; +import io.metersphere.track.response.TrackStatisticsDTO; +import io.metersphere.track.service.TrackService; +import org.apache.shiro.authz.annotation.Logical; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.text.DecimalFormat; +import java.util.List; + +@RestController +@RequestMapping("/track") +@RequiresRoles(value = {RoleConstants.ADMIN, RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER, RoleConstants.ORG_ADMIN}, logical = Logical.OR) +public class TrackController { + + @Resource + private TrackService trackService; + + @GetMapping("/count/{projectId}") + public TrackStatisticsDTO getTrackCount(@PathVariable String projectId) { + TrackStatisticsDTO statistics = new TrackStatisticsDTO(); + + List priorityResults = trackService.countPriority(projectId); + statistics.countPriority(priorityResults); + + long size = trackService.countCreatedThisWeek(projectId); + statistics.setThisWeekAddedCount(size); + + List statusResults = trackService.countStatus(projectId); + statistics.countStatus(statusResults); + + long total = statistics.getPrepareCount() + statistics.getPassCount() + statistics.getUnPassCount(); + if (total != 0) { + float reviewed = (float) (statistics.getPassCount() + statistics.getUnPassCount()) * 100 / total; + DecimalFormat df = new DecimalFormat("0.0"); + statistics.setReviewRage(df.format(reviewed) + "%"); + } + + statistics.setP0CountStr("P0 " + statistics.getP0CaseCountNumber()); + statistics.setP1CountStr("P1 " + statistics.getP1CaseCountNumber()); + statistics.setP2CountStr("P2 " + statistics.getP2CaseCountNumber()); + statistics.setP3CountStr("P3 " + statistics.getP3CaseCountNumber()); + return statistics; + } + + @GetMapping("/relevance/count/{projectId}") + public TrackStatisticsDTO getRelevanceCount(@PathVariable String projectId) { + TrackStatisticsDTO statistics = new TrackStatisticsDTO(); + + List relevanceResults = trackService.countRelevance(projectId); + statistics.countRelevance(relevanceResults); + + long size = trackService.countRelevanceCreatedThisWeek(projectId); + statistics.setThisWeekAddedCount(size); + + List coverageResults = trackService.countCoverage(projectId); + statistics.countCoverage(coverageResults); + + long total = statistics.getUncoverageCount() + statistics.getCoverageCount(); + + if (total != 0) { + float coverageRageNumber = (float) statistics.getCoverageCount() * 100 / total; + DecimalFormat df = new DecimalFormat("0.0"); + statistics.setCoverageRage(df.format(coverageRageNumber) + "%"); + } + + statistics.setApiCaseCountStr("接口用例 " + statistics.getApiCaseCount()); + statistics.setPerformanceCaseCountStr("性能用例 " + statistics.getPerformanceCaseCount()); + statistics.setScenarioCaseStr("场景用例 " + statistics.getScenarioCaseCount()); + + return statistics; + } + + @GetMapping("/case/bar/{projectId}") + public List getCaseMaintenanceBar(@PathVariable String projectId) { + return trackService.getCaseMaintenanceBar(projectId); + } + + @GetMapping("/bug/count/{projectId}") + public BugStatustics getBugStatistics(@PathVariable String projectId) { + return trackService.getBugStatistics(projectId); + } +} diff --git a/backend/src/main/java/io/metersphere/track/request/testcase/TrackCount.java b/backend/src/main/java/io/metersphere/track/request/testcase/TrackCount.java new file mode 100644 index 0000000000..b795deb9da --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/request/testcase/TrackCount.java @@ -0,0 +1,12 @@ +package io.metersphere.track.request.testcase; + +public class TrackCount { + public static final String P0 = "P0"; + public static final String P1 = "P1"; + public static final String P2 = "P2"; + public static final String P3 = "P3"; + + public static final String API = "api"; + public static final String PERFORMANCE = "performance"; + public static final String AUTOMATION = "automation"; +} diff --git a/backend/src/main/java/io/metersphere/track/response/BugStatustics.java b/backend/src/main/java/io/metersphere/track/response/BugStatustics.java new file mode 100644 index 0000000000..863fa5f0fe --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/response/BugStatustics.java @@ -0,0 +1,16 @@ +package io.metersphere.track.response; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +public class BugStatustics { + + private long bugTotalSize; + private String rage; + private List list = new ArrayList<>(); +} diff --git a/backend/src/main/java/io/metersphere/track/response/TestPlanBugCount.java b/backend/src/main/java/io/metersphere/track/response/TestPlanBugCount.java new file mode 100644 index 0000000000..6802673d16 --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/response/TestPlanBugCount.java @@ -0,0 +1,16 @@ +package io.metersphere.track.response; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TestPlanBugCount { + private int index; + private String planName; + private long createTime; + private String status; + private int caseSize; + private int bugSize; + private String passRage; +} diff --git a/backend/src/main/java/io/metersphere/track/response/TrackCountResult.java b/backend/src/main/java/io/metersphere/track/response/TrackCountResult.java new file mode 100644 index 0000000000..14e2add524 --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/response/TrackCountResult.java @@ -0,0 +1,19 @@ +package io.metersphere.track.response; + + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TrackCountResult { + + /** + * 分组统计字段 + */ + private String groupField; + /** + * 数据统计 + */ + private long countNumber; +} diff --git a/backend/src/main/java/io/metersphere/track/response/TrackStatisticsDTO.java b/backend/src/main/java/io/metersphere/track/response/TrackStatisticsDTO.java new file mode 100644 index 0000000000..264e0cf9cb --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/response/TrackStatisticsDTO.java @@ -0,0 +1,175 @@ +package io.metersphere.track.response; + +import io.metersphere.api.dto.datacount.ApiDataCountResult; +import io.metersphere.commons.constants.TestReviewCaseStatus; +import io.metersphere.track.request.testcase.TrackCount; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * 用例数量统计数据 + */ +@Getter +@Setter +public class TrackStatisticsDTO { + + /** + * 用例总计 + */ + private long allCaseCountNumber = 0; + + /** + * P0登记用例总计 + */ + private long p0CaseCountNumber = 0; + + /** + * P1登记用例总计 + */ + private long p1CaseCountNumber = 0; + + /** + * P2登记用例总计 + */ + private long p2CaseCountNumber = 0; + + /** + * P3登记用例总计 + */ + private long p3CaseCountNumber = 0; + + private String p0CountStr = ""; + private String p1CountStr = ""; + private String p2CountStr = ""; + private String p3CountStr = ""; + + /** + * 关联用例数量统计 + */ + private long allRelevanceCaseCount = 0; + + /** + * 接口用例数量统计 + */ + private long apiCaseCount = 0; + + /** + * 场景用例数量统计 + */ + private long scenarioCaseCount = 0; + + /** + * 性能用例数量统计 + */ + private long performanceCaseCount = 0; + + + private String apiCaseCountStr = ""; + private String scenarioCaseStr = ""; + private String performanceCaseCountStr = ""; + + /** + * 本周新增数量 + */ + private long thisWeekAddedCount = 0; + /** + * 未覆盖 + */ + private long uncoverageCount = 0; + /** + * 已覆盖 + */ + private long coverageCount = 0; + /** + * 覆盖率 + */ + private String coverageRage = " 0%"; + /** + * 评审率 + */ + private String reviewRage = " 0%"; + /** + * 未评审 + */ + private long prepareCount = 0; + /** + * 已通过 + */ + private long passCount = 0; + /** + * 未通过 + */ + private long unPassCount = 0; + + + /** + * 按照 Priority 统计 + * @param trackCountResults 统计结果 + */ + public void countPriority(List trackCountResults) { + for (TrackCountResult countResult : trackCountResults) { + switch (countResult.getGroupField().toUpperCase()){ + case TrackCount.P0: + this.p0CaseCountNumber += countResult.getCountNumber(); + break; + case TrackCount.P1: + this.p1CaseCountNumber += countResult.getCountNumber(); + break; + case TrackCount.P2: + this.p2CaseCountNumber += countResult.getCountNumber(); + break; + case TrackCount.P3: + this.p3CaseCountNumber += countResult.getCountNumber(); + break; + default: + break; + } + this.allCaseCountNumber += countResult.getCountNumber(); + } + } + + public void countStatus(List statusResults) { + for (TrackCountResult countResult : statusResults) { + if(TestReviewCaseStatus.Prepare.name().equals(countResult.getGroupField())){ + this.prepareCount += countResult.getCountNumber(); + }else if(TestReviewCaseStatus.Pass.name().equals(countResult.getGroupField())){ + this.passCount += countResult.getCountNumber(); + }else if(TestReviewCaseStatus.UnPass.name().equals(countResult.getGroupField())){ + this.unPassCount += countResult.getCountNumber(); + } + } + } + + public void countRelevance(List relevanceResults) { + for (TrackCountResult countResult : relevanceResults) { + switch (countResult.getGroupField()){ + case TrackCount.API: + this.apiCaseCount += countResult.getCountNumber(); + this.allRelevanceCaseCount += countResult.getCountNumber(); + break; + case TrackCount.PERFORMANCE: + this.performanceCaseCount += countResult.getCountNumber(); + this.allRelevanceCaseCount += countResult.getCountNumber(); + break; + case TrackCount.AUTOMATION: + this.scenarioCaseCount += countResult.getCountNumber(); + this.allRelevanceCaseCount += countResult.getCountNumber(); + break; + default: + break; + } + } + } + + public void countCoverage(List coverageResults) { + for (TrackCountResult countResult : coverageResults) { + if("coverage".equals(countResult.getGroupField())){ + this.coverageCount+= countResult.getCountNumber(); + }else if("uncoverage".equals(countResult.getGroupField())){ + this.uncoverageCount+= countResult.getCountNumber(); + } + } + } +} diff --git a/backend/src/main/java/io/metersphere/track/service/TrackService.java b/backend/src/main/java/io/metersphere/track/service/TrackService.java new file mode 100644 index 0000000000..3bcdf1cf79 --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/service/TrackService.java @@ -0,0 +1,168 @@ +package io.metersphere.track.service; + +import io.metersphere.base.domain.*; +import io.metersphere.base.mapper.*; +import io.metersphere.base.mapper.ext.ExtTestCaseMapper; +import io.metersphere.commons.utils.DateUtils; +import io.metersphere.performance.base.ChartsData; +import io.metersphere.track.response.BugStatustics; +import io.metersphere.track.response.TestPlanBugCount; +import io.metersphere.track.response.TrackCountResult; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Transactional(rollbackFor = Exception.class) +public class TrackService { + + @Resource + private ExtTestCaseMapper extTestCaseMapper; + @Resource + private TestPlanMapper testPlanMapper; + @Resource + private TestPlanTestCaseMapper testPlanTestCaseMapper; + @Resource + private TestPlanLoadCaseMapper testPlanLoadCaseMapper; + @Resource + private TestPlanApiCaseMapper testPlanApiCaseMapper; + @Resource + private TestPlanApiScenarioMapper testPlanApiScenarioMapper; + + public List countPriority(String projectId) { + return extTestCaseMapper.countPriority(projectId); + } + + public long countCreatedThisWeek(String projectId) { + Map startAndEndDateInWeek = DateUtils.getWeedFirstTimeAndLastTime(new Date()); + + Date firstTime = startAndEndDateInWeek.get("firstTime"); + Date lastTime = startAndEndDateInWeek.get("lastTime"); + + if (firstTime == null || lastTime == null) { + return 0; + } else { + return extTestCaseMapper.countCreatedThisWeek(projectId, firstTime.getTime(), lastTime.getTime()); + } + } + + public List countStatus(String projectId) { + return extTestCaseMapper.countStatus(projectId); + } + + public List countRelevance(String projectId) { + return extTestCaseMapper.countRelevance(projectId); + } + + public long countRelevanceCreatedThisWeek(String projectId) { + Map startAndEndDateInWeek = DateUtils.getWeedFirstTimeAndLastTime(new Date()); + + Date firstTime = startAndEndDateInWeek.get("firstTime"); + Date lastTime = startAndEndDateInWeek.get("lastTime"); + + if (firstTime == null || lastTime == null) { + return 0; + } else { + return extTestCaseMapper.countRelevanceCreatedThisWeek(projectId, firstTime.getTime(), lastTime.getTime()); + } + } + + public List countCoverage(String projectId) { + return extTestCaseMapper.countCoverage(projectId); + } + + public List getCaseMaintenanceBar(String projectId) { + List funcMaintainer = extTestCaseMapper.countFuncMaintainer(projectId); + List relevanceMaintainer = extTestCaseMapper.countRelevanceMaintainer(projectId); + List list = relevanceMaintainer.stream().map(TrackCountResult::getGroupField).collect(Collectors.toList()); + + List charts = new ArrayList<>(); + for (TrackCountResult result : funcMaintainer) { + String groupField = result.getGroupField(); + if (!list.contains(groupField)) { + // 创建了功能用例,但是未关联测试 + TrackCountResult trackCount = new TrackCountResult(); + trackCount.setCountNumber(0); + trackCount.setGroupField(groupField); + relevanceMaintainer.add(trackCount); + } + ChartsData chartsData = new ChartsData(); + chartsData.setxAxis(groupField); + chartsData.setyAxis(BigDecimal.valueOf(result.getCountNumber())); + chartsData.setGroupName("FUNCTIONCASE"); + charts.add(chartsData); + } + + for (TrackCountResult result : relevanceMaintainer) { + ChartsData chartsData = new ChartsData(); + chartsData.setxAxis(result.getGroupField()); + chartsData.setyAxis(BigDecimal.valueOf(result.getCountNumber())); + chartsData.setGroupName("RELEVANCECASE"); + charts.add(chartsData); + } + + return charts; + } + + public BugStatustics getBugStatistics(String projectId) { + TestPlanExample example = new TestPlanExample(); + example.createCriteria().andProjectIdEqualTo(projectId); + List plans = testPlanMapper.selectByExample(example); + List list = new ArrayList<>(); + BugStatustics bugStatustics = new BugStatustics(); + int index = 1; + int totalBugSize = 0; + int totalCaseSize = 0; + for (TestPlan plan : plans) { + TestPlanBugCount testPlanBug = new TestPlanBugCount(); + testPlanBug.setIndex(index++); + testPlanBug.setPlanName(plan.getName()); + testPlanBug.setCreateTime(plan.getCreateTime()); + testPlanBug.setStatus(plan.getStatus()); + + int planCaseSize = getPlanCaseSize(plan.getId()); + testPlanBug.setCaseSize(planCaseSize); + + int planBugSize = getPlanBugSize(plan.getId()); + testPlanBug.setBugSize(planBugSize); + testPlanBug.setPassRage(getPlanPassRage(plan.getId(), planCaseSize)); + list.add(testPlanBug); + + totalBugSize += planBugSize; + totalCaseSize += planCaseSize; + } + + bugStatustics.setList(list); + float rage =totalCaseSize == 0 ? 0 : (float) totalBugSize * 100 / totalCaseSize; + DecimalFormat df = new DecimalFormat("0.0"); + bugStatustics.setRage(df.format(rage) + "%"); + bugStatustics.setBugTotalSize(totalBugSize); + return bugStatustics; + } + + private int getPlanCaseSize(String planId) { + return extTestCaseMapper.getTestPlanCase(planId); + + } + + private int getPlanBugSize(String planId) { + return extTestCaseMapper.getTestPlanBug(planId); + } + + private String getPlanPassRage(String planId, int totalSize) { + if (totalSize == 0) { + return "-"; + } + int passSize = extTestCaseMapper.getTestPlanPassCase(planId); + float rage = (float) passSize * 100 / totalSize; + DecimalFormat df = new DecimalFormat("0.0"); + return df.format(rage) + "%"; + } +} diff --git a/frontend/src/business/App.vue b/frontend/src/business/App.vue index a03bda8a6d..f279581ae6 100644 --- a/frontend/src/business/App.vue +++ b/frontend/src/business/App.vue @@ -31,7 +31,7 @@ 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 {hasLicense, saveLocalStorage, setColor, setOriginColor} from "@/common/js/utils"; +import {hasLicense, saveLocalStorage, setColor, setDefaultTheme} from "@/common/js/utils"; import {registerRequestHeaders} from "@/common/js/ajax"; import {ORIGIN_COLOR} from "@/common/js/constants"; @@ -55,13 +55,14 @@ export default { created() { registerRequestHeaders(); if (!hasLicense()) { - setOriginColor() + setDefaultTheme(); this.color = ORIGIN_COLOR; } else { // this.$get('/system/theme', res => { this.color = res.data ? res.data : ORIGIN_COLOR; - setColor(this.color, this.color, this.color, this.color); + setColor(this.color, this.color, this.color, this.color, this.color); + this.$store.commit('setTheme', res.data); }) } if (localStorage.getItem("store")) { diff --git a/frontend/src/business/components/common/head/HeaderLabelOperate.vue b/frontend/src/business/components/common/head/HeaderLabelOperate.vue index 66c9f275fd..b5764b6ea6 100644 --- a/frontend/src/business/components/common/head/HeaderLabelOperate.vue +++ b/frontend/src/business/components/common/head/HeaderLabelOperate.vue @@ -2,7 +2,7 @@ {{ $t('commons.operating') }} - + @@ -21,5 +21,8 @@ export default { diff --git a/frontend/src/business/components/performance/report/components/PerformancePressureConfig.vue b/frontend/src/business/components/performance/report/components/PerformancePressureConfig.vue index 5068f14929..633b28f122 100644 --- a/frontend/src/business/components/performance/report/components/PerformancePressureConfig.vue +++ b/frontend/src/business/components/performance/report/components/PerformancePressureConfig.vue @@ -37,6 +37,13 @@ @change="calculateChart(threadGroup)" size="mini"/> + + + {{ $t('schedule.cron.seconds') }} + {{ $t('schedule.cron.minutes') }} + {{ $t('schedule.cron.hours') }} + + @@ -59,7 +66,7 @@ @change="calculateChart(threadGroup)" size="mini"/> - + - + @@ -112,7 +119,7 @@ @change="calculateChart(threadGroup)" size="mini"/> - + @@ -137,6 +144,7 @@ const TARGET_LEVEL = "TargetLevel"; const RAMP_UP = "RampUp"; const STEPS = "Steps"; const DURATION = "duration"; +const UNIT = "unit"; const RPS_LIMIT = "rpsLimit"; const RPS_LIMIT_ENABLE = "rpsLimitEnable"; const THREAD_TYPE = "threadType"; @@ -196,11 +204,10 @@ export default { this.threadGroups[i].iterateRampUp = item.value; break; case DURATION: - if (item.unit) { - this.threadGroups[i].duration = item.value; - } else { - this.threadGroups[i].duration = item.value * 60; - } + this.threadGroups[i].duration = item.value; + break; + case UNIT: + this.threadGroups[i].unit = item.value; break; case STEPS: this.threadGroups[i].step = item.value; @@ -364,7 +371,7 @@ export default { if (j === 0) { seriesData.data.push([0, 0]); } - if (j > tg.rampUpTime) { + if (j >= tg.rampUpTime) { xAxis.push(tg.duration); seriesData.data.push([j, tg.threadNumber]); @@ -475,7 +482,7 @@ export default { if (i === 0) { handler.options.series[0].data.push([0, 0]); } - if (i > handler.rampUpTime) { + if (i >= handler.rampUpTime) { handler.options.xAxis.data.push(handler.duration); handler.options.series[0].data.push([i, handler.threadNumber]); @@ -506,6 +513,18 @@ export default { } this.calculateTotalChart(); }, + getUnitLabel(tg) { + if (tg.unit === 'S') { + return this.$t('schedule.cron.seconds'); + } + if (tg.unit === 'M') { + return this.$t('schedule.cron.minutes'); + } + if (tg.unit === 'H') { + return this.$t('schedule.cron.hours'); + } + return this.$t('schedule.cron.seconds'); + }, }, watch: { report: { diff --git a/frontend/src/business/components/performance/report/components/TestOverview.vue b/frontend/src/business/components/performance/report/components/TestOverview.vue index 50574c67d4..f1cd9df364 100644 --- a/frontend/src/business/components/performance/report/components/TestOverview.vue +++ b/frontend/src/business/components/performance/report/components/TestOverview.vue @@ -467,7 +467,8 @@ export default { let items = { name: name, type: 'line', - data: d + data: d, + smooth: true }; let seriesArrayNames = seriesArray.map(m => m.name); if (seriesArrayNames.includes(name)) { diff --git a/frontend/src/business/components/performance/test/components/PerformanceAdvancedConfig.vue b/frontend/src/business/components/performance/test/components/PerformanceAdvancedConfig.vue index 5a835228bd..fcacb7434e 100644 --- a/frontend/src/business/components/performance/test/components/PerformanceAdvancedConfig.vue +++ b/frontend/src/business/components/performance/test/components/PerformanceAdvancedConfig.vue @@ -130,7 +130,7 @@ - {{ scope.row.start }} - {{ scope.row.end }} + {{ scope.row.start }}S - {{ scope.row.end }}S diff --git a/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue b/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue index f7c3e121ee..29df9f4e18 100644 --- a/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue +++ b/frontend/src/business/components/performance/test/components/PerformancePressureConfig.vue @@ -47,10 +47,17 @@ :disabled="isReadOnly" v-model="threadGroup.duration" :min="1" - :max="172800" + :max="99999" @change="calculateChart(threadGroup)" size="mini"/> + + + {{ $t('schedule.cron.seconds') }} + {{ $t('schedule.cron.minutes') }} + {{ $t('schedule.cron.hours') }} + + @@ -74,7 +81,7 @@ @change="calculateChart(threadGroup)" size="mini"/> - + - + @@ -127,7 +135,7 @@ v-model="threadGroup.iterateRampUp" size="mini"/> - + @@ -153,6 +161,7 @@ const RAMP_UP = "RampUp"; const ITERATE_RAMP_UP = "iterateRampUpTime"; const STEPS = "Steps"; const DURATION = "duration"; +const UNIT = "unit"; const RPS_LIMIT = "rpsLimit"; const RPS_LIMIT_ENABLE = "rpsLimitEnable"; const HOLD = "Hold"; @@ -252,11 +261,10 @@ export default { this.threadGroups[i].iterateRampUp = item.value; break; case DURATION: - if (item.unit) { - this.threadGroups[i].duration = item.value; - } else { - this.threadGroups[i].duration = item.value * 60; - } + this.threadGroups[i].duration = item.value; + break; + case UNIT: + this.threadGroups[i].unit = item.value; break; case STEPS: this.threadGroups[i].step = item.value; @@ -289,6 +297,7 @@ export default { break; } // + this.$set(this.threadGroups[i], "unit", this.threadGroups[i].unit || 'S'); this.$set(this.threadGroups[i], "threadType", this.threadGroups[i].threadType || 'DURATION'); this.$set(this.threadGroups[i], "iterateNum", this.threadGroups[i].iterateNum || 1); this.$set(this.threadGroups[i], "iterateRampUp", this.threadGroups[i].iterateRampUp || 10); @@ -402,7 +411,7 @@ export default { if (j === 0) { seriesData.data.push([0, 0]); } - if (j > tg.rampUpTime) { + if (j >= tg.rampUpTime) { xAxis.push(tg.duration); seriesData.data.push([j, tg.threadNumber]); @@ -519,7 +528,7 @@ export default { if (i === 0) { handler.options.series[0].data.push([0, 0]); } - if (i > handler.rampUpTime) { + if (i >= handler.rampUpTime) { handler.options.xAxis.data.push(handler.duration); handler.options.series[0].data.push([i, handler.threadNumber]); @@ -575,6 +584,18 @@ export default { return true; }, + getUnitLabel(tg) { + if (tg.unit === 'S') { + return this.$t('schedule.cron.seconds'); + } + if (tg.unit === 'M') { + return this.$t('schedule.cron.minutes'); + } + if (tg.unit === 'H') { + return this.$t('schedule.cron.hours'); + } + return this.$t('schedule.cron.seconds'); + }, convertProperty() { /// todo:下面4个属性是jmeter ConcurrencyThreadGroup plugin的属性,这种硬编码不太好吧,在哪能转换这种属性? let result = []; @@ -584,7 +605,8 @@ export default { {key: TARGET_LEVEL, value: this.threadGroups[i].threadNumber}, {key: RAMP_UP, value: this.threadGroups[i].rampUpTime}, {key: STEPS, value: this.threadGroups[i].step}, - {key: DURATION, value: this.threadGroups[i].duration, unit: 'S'}, + {key: DURATION, value: this.threadGroups[i].duration, unit: this.threadGroups[i].unit}, + {key: UNIT, value: this.threadGroups[i].unit}, {key: RPS_LIMIT, value: this.threadGroups[i].rpsLimit}, {key: RPS_LIMIT_ENABLE, value: this.threadGroups[i].rpsLimitEnable}, {key: HOLD, value: this.threadGroups[i].duration - this.threadGroups[i].rampUpTime}, diff --git a/frontend/src/business/components/performance/test/model/ThreadGroup.js b/frontend/src/business/components/performance/test/model/ThreadGroup.js index b4656c8af5..2d44f13c5a 100644 --- a/frontend/src/business/components/performance/test/model/ThreadGroup.js +++ b/frontend/src/business/components/performance/test/model/ThreadGroup.js @@ -31,6 +31,7 @@ export function findThreadGroup(jmxContent, handler) { tg.enabled = tg.attributes.enabled; tg.tgType = tg.name; tg.threadType = 'DURATION'; + tg.unit = 'S'; }) return threadGroups; } diff --git a/frontend/src/business/components/track/home/TrackHome.vue b/frontend/src/business/components/track/home/TrackHome.vue index 3061221b30..084aff62dd 100644 --- a/frontend/src/business/components/track/home/TrackHome.vue +++ b/frontend/src/business/components/track/home/TrackHome.vue @@ -1,57 +1,214 @@ - - - - - - - - - + + + + 🤔️ 天凉了,保温杯买了吗? + + + 😔 觉得MeterSphere不好用就来 + https://github.com/metersphere/metersphere/issues + + 吐个槽吧! + + + 😄 觉得MeterSphere好用就来 + https://github.com/metersphere/metersphere + + 点个star吧! + + + 😊 MeterSphere温馨提醒 —— 多喝热水哟! + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/business/components/track/home/components/BugCountCard.vue b/frontend/src/business/components/track/home/components/BugCountCard.vue new file mode 100644 index 0000000000..d0e09fcbe4 --- /dev/null +++ b/frontend/src/business/components/track/home/components/BugCountCard.vue @@ -0,0 +1,130 @@ + + + + + 遗留缺陷统计 + + + + + + + {{ bugTotalSize }} + + + {{ $t('api_test.home_page.unit_of_measurement') }} + + + 占比 + + {{rage}} + + + + + + + + + + {{ scope.row.createTime | timestampFormatDate }} + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/business/components/track/home/components/CaseCountCard.vue b/frontend/src/business/components/track/home/components/CaseCountCard.vue new file mode 100644 index 0000000000..b1caeed243 --- /dev/null +++ b/frontend/src/business/components/track/home/components/CaseCountCard.vue @@ -0,0 +1,191 @@ + + + + + 用例数量统计 + + + + + + + + {{trackCountData.allCaseCountNumber}} + + + {{$t('api_test.home_page.unit_of_measurement')}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{$t('api_test.home_page.api_details_card.this_week_add')}} + {{trackCountData.thisWeekAddedCount}} + + {{$t('api_test.home_page.unit_of_measurement')}} + + + + + + + + + 评审率: + + + + {{trackCountData.reviewRage}} + + + + + + + + + + + {{$t('api_test.home_page.detail_card.running')}} + {{"\xa0\xa0"}} + + {{trackCountData.prepareCount}} + + + + + + {{$t('api_test.home_page.detail_card.not_started')}} + {{"\xa0\xa0"}} + + {{trackCountData.passCount}} + + + + + + {{$t('api_test.home_page.detail_card.finished')}} + {{"\xa0\xa0"}} + + {{trackCountData.unPassCount}} + + + + + + + + + + + + + + + + diff --git a/frontend/src/business/components/track/home/components/CaseMaintenance.vue b/frontend/src/business/components/track/home/components/CaseMaintenance.vue new file mode 100644 index 0000000000..8cbb6f1121 --- /dev/null +++ b/frontend/src/business/components/track/home/components/CaseMaintenance.vue @@ -0,0 +1,41 @@ + + + + + 用例维护人分布 + + + + + + + + + + + diff --git a/frontend/src/business/components/track/home/components/RelevanceCaseCard.vue b/frontend/src/business/components/track/home/components/RelevanceCaseCard.vue new file mode 100644 index 0000000000..d82ef603af --- /dev/null +++ b/frontend/src/business/components/track/home/components/RelevanceCaseCard.vue @@ -0,0 +1,165 @@ + + + + + 关联用例数量统计 + + + + + + + + {{relevanceCountData.allRelevanceCaseCount}} + + + {{$t('api_test.home_page.unit_of_measurement')}} + + + + + + + + + + + + + + + + + + + + + + + + + + {{$t('api_test.home_page.api_details_card.this_week_add')}} + {{relevanceCountData.thisWeekAddedCount}} + + {{$t('api_test.home_page.unit_of_measurement')}} + + + + + + + + + 覆盖率: + + + + {{relevanceCountData.coverageRage}} + + + + + + + + + + + 未覆盖 + {{"\xa0\xa0"}} + + {{relevanceCountData.uncoverageCount}} + + + + + + 已覆盖 + {{"\xa0\xa0"}} + + {{relevanceCountData.coverageCount}} + + + + + + + + + + + + + + + + diff --git a/frontend/src/business/components/track/home/components/ReviewList.vue b/frontend/src/business/components/track/home/components/ReviewList.vue index 5ec3b49d47..b46bb93d01 100644 --- a/frontend/src/business/components/track/home/components/ReviewList.vue +++ b/frontend/src/business/components/track/home/components/ReviewList.vue @@ -1,14 +1,14 @@ - - - - - - - - + + + + 遗留缺陷统计 + + + + - + diff --git a/frontend/src/business/components/xpack b/frontend/src/business/components/xpack index 4c33b9c3b1..b2571e06e8 160000 --- a/frontend/src/business/components/xpack +++ b/frontend/src/business/components/xpack @@ -1 +1 @@ -Subproject commit 4c33b9c3b12a83da6d9bd2740262c6c8baaab819 +Subproject commit b2571e06e8b211821409115cc2c4a7c52cbac1db diff --git a/frontend/src/business/store.js b/frontend/src/business/store.js index c536ea0d38..4fbb5d7f17 100644 --- a/frontend/src/business/store.js +++ b/frontend/src/business/store.js @@ -50,11 +50,23 @@ const IsReadOnly = { } } +const Theme = { + state: { + theme: undefined + }, + mutations: { + setTheme(state, value) { + state.theme = value; + } + } +} + export default new Vuex.Store({ modules: { api: API, common: Common, switch: Switch, isReadOnly: IsReadOnly, + theme: Theme } }) diff --git a/frontend/src/common/css/main.css b/frontend/src/common/css/main.css index 34a336f689..2595486214 100644 --- a/frontend/src/common/css/main.css +++ b/frontend/src/common/css/main.css @@ -17,6 +17,7 @@ body { /*--color: #2c2a48;*/ /*--color_shallow: #595591;*/ --color: ''; + --primary_color: ''; --color_shallow: ''; --count_number: ''; --count_number_shallow: ''; diff --git a/frontend/src/common/js/constants.js b/frontend/src/common/js/constants.js index cbaf36f974..e8c6de79f3 100644 --- a/frontend/src/common/js/constants.js +++ b/frontend/src/common/js/constants.js @@ -173,3 +173,4 @@ export const ORIGIN_COLOR = '#2c2a48'; export const ORIGIN_COLOR_SHALLOW = '#595591'; export const COUNT_NUMBER = '#6C317C'; export const COUNT_NUMBER_SHALLOW = '#CDB9D2'; +export const PRIMARY_COLOR = '#783887'; diff --git a/frontend/src/common/js/utils.js b/frontend/src/common/js/utils.js index 02822ea8e6..2463cec636 100644 --- a/frontend/src/common/js/utils.js +++ b/frontend/src/common/js/utils.js @@ -1,17 +1,17 @@ import { - COUNT_NUMBER, - COUNT_NUMBER_SHALLOW, - LicenseKey, - ORIGIN_COLOR, - ORIGIN_COLOR_SHALLOW, - PROJECT_ID, - REFRESH_SESSION_USER_URL, - ROLE_ADMIN, - ROLE_ORG_ADMIN, - ROLE_TEST_MANAGER, - ROLE_TEST_USER, - ROLE_TEST_VIEWER, - TokenKey + COUNT_NUMBER, + COUNT_NUMBER_SHALLOW, + LicenseKey, + ORIGIN_COLOR, + ORIGIN_COLOR_SHALLOW, PRIMARY_COLOR, + PROJECT_ID, + REFRESH_SESSION_USER_URL, + ROLE_ADMIN, + ROLE_ORG_ADMIN, + ROLE_TEST_MANAGER, + ROLE_TEST_USER, + ROLE_TEST_VIEWER, + TokenKey } from "./constants"; import axios from "axios"; import {jsPDF} from "jspdf"; @@ -354,19 +354,19 @@ export function objToStrMap(obj) { return strMap; } -export function getColor() { - return localStorage.getItem('color'); -} - -export function setColor(a, b, c, d) { +export function setColor(a, b, c, d, e) { + // 顶部菜单背景色 document.body.style.setProperty('--color', a); document.body.style.setProperty('--color_shallow', b); + // 首页颜色 document.body.style.setProperty('--count_number', c); document.body.style.setProperty('--count_number_shallow', d); + // 主颜色 + document.body.style.setProperty('--primary_color', e); } -export function setOriginColor() { - setColor(ORIGIN_COLOR, ORIGIN_COLOR_SHALLOW, COUNT_NUMBER, COUNT_NUMBER_SHALLOW); +export function setDefaultTheme() { + setColor(ORIGIN_COLOR, ORIGIN_COLOR_SHALLOW, COUNT_NUMBER, COUNT_NUMBER_SHALLOW, PRIMARY_COLOR); } export function publicKeyEncrypt(input, publicKey) { diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index 91e69c74ce..2e69834217 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -481,7 +481,7 @@ export default { delete_file: "The file already exists, please delete the file with the same name first!", thread_num: 'Concurrent users:', input_thread_num: 'Please enter the number of threads', - duration: 'Duration time (seconds)', + duration: 'Duration time', granularity: 'Aggregation time (seconds)', input_duration: 'Please enter a duration', rps_limit: 'RPS Limit:', diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index db08f19989..232b611f71 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -478,14 +478,14 @@ export default { delete_file: "文件已存在,请先删除同名文件!", thread_num: '并发用户数:', input_thread_num: '请输入线程数', - duration: '压测时长(秒)', + duration: '压测时长', granularity: '聚合时间(秒)', input_duration: '请输入时长', rps_limit: 'RPS上限:', input_rps_limit: '请输入限制', ramp_up_time_within: '在', - ramp_up_time_minutes: '秒内,分', - ramp_up_time_seconds: '秒内增加并发用户', + ramp_up_time_minutes: '{0}内,分', + ramp_up_time_seconds: '{0}内增加并发用户', iterate_num: '迭代次数 (次): ', by_iteration: '按迭代次数', by_duration: '按持续时间', diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index ccbc5805d6..17cd3fb05e 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -478,7 +478,7 @@ export default { delete_file: "文件已存在,請先刪除同名文件!", thread_num: '並發用戶數:', input_thread_num: '請輸入線程數', - duration: '壓測時長(秒)', + duration: '壓測時長', granularity: '聚合時間(秒)', input_duration: '請輸入時長', rps_limit: 'RPS上限:', diff --git a/frontend/src/login/Login.vue b/frontend/src/login/Login.vue index 8ca8bc7da5..4ef3406a7b 100644 --- a/frontend/src/login/Login.vue +++ b/frontend/src/login/Login.vue @@ -56,7 +56,7 @@