feat(项目报告定时任务): 项目报告定时任务可以实时查询图表

项目报告定时任务可以实时查询图表
This commit is contained in:
song-tianyang 2021-12-14 14:58:23 +08:00 committed by song-tianyang
parent 217c30be67
commit f0e6bed4d9
20 changed files with 454 additions and 48 deletions

View File

@ -440,6 +440,11 @@
<artifactId>xmindjbehaveplugin</artifactId>
<version>0.8</version>
</dependency>
<!-- selenium包 -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</dependency>
<!-- 基础包 -->
<dependency>
<groupId>io.metersphere</groupId>

View File

@ -7,8 +7,11 @@ import io.metersphere.api.dto.share.ShareInfoDTO;
import io.metersphere.api.service.ApiDefinitionService;
import io.metersphere.api.service.ShareInfoService;
import io.metersphere.base.domain.ApiDefinitionWithBLOBs;
import io.metersphere.base.domain.ReportStatisticsWithBLOBs;
import io.metersphere.base.domain.ShareInfo;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.reportstatistics.dto.ReportStatisticsSaveRequest;
import io.metersphere.reportstatistics.service.ReportStatisticsService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@ -29,6 +32,8 @@ public class ShareInfoController {
ShareInfoService shareInfoService;
@Resource
ApiDefinitionService apiDefinitionService;
@Resource
ReportStatisticsService reportStatisticsService;
@PostMapping("/selectApiSimpleInfo")
public List<ApiDocumentInfoDTO> list(@RequestBody ApiDocumentRequest request) {
@ -88,4 +93,9 @@ public class ShareInfoController {
ShareInfoDTO returnDTO = shareInfoService.conversionShareInfoToDTO(apiShare);
return returnDTO;
}
@PostMapping("/selectHistoryReportById")
public ReportStatisticsWithBLOBs selectById(@RequestBody ReportStatisticsSaveRequest request) {
return reportStatisticsService.selectById(request.getId());
}
}

View File

@ -176,4 +176,5 @@ public class PermissionConstants {
public static final String PROJECT_ENTERPRISE_REPORT_EDIT = "PROJECT_ENTERPRISE_REPORT:READ+EDIT";
public static final String PROJECT_ENTERPRISE_REPORT_DELETE = "PROJECT_ENTERPRISE_REPORT:READ+DELETE";
public static final String PROJECT_ENTERPRISE_REPORT_COPY = "PROJECT_ENTERPRISE_REPORT:READ+COPY";
public static final String PROJECT_ENTERPRISE_REPORT_SCHEDULE = "PROJECT_ENTERPRISE_REPORT:READ+SCHEDULE";
}

View File

@ -1,5 +1,6 @@
package io.metersphere.commons.constants;
public enum ScheduleGroup {
API_TEST, PERFORMANCE_TEST, API_SCENARIO_TEST, TEST_PLAN_TEST, SWAGGER_IMPORT, ISSUE_SYNC
API_TEST, PERFORMANCE_TEST, API_SCENARIO_TEST, TEST_PLAN_TEST, SWAGGER_IMPORT, ISSUE_SYNC,
SCHEDULE_SEND_REPORT
}

View File

@ -53,9 +53,11 @@ public class ShiroUtils {
//分享相关接口
filterChainDefinitionMap.put("/share/info/generateShareInfoWithExpired", "anon");
filterChainDefinitionMap.put("/share/info/selectApiInfoByParam", "anon");
filterChainDefinitionMap.put("/share/info/selectHistoryReportById", "anon");
filterChainDefinitionMap.put("/share/get/**", "anon");
filterChainDefinitionMap.put("/share/info", "apikey, csrf, authc"); // 需要认证
filterChainDefinitionMap.put("/document/**", "anon");
filterChainDefinitionMap.put("/echartPic/**", "anon");
filterChainDefinitionMap.put("/share/**", "anon");
filterChainDefinitionMap.put("/sharePlanReport", "anon");

View File

@ -32,6 +32,11 @@ public class IndexController {
return "document.html";
}
@GetMapping(value = "/echartPic")
public String echartPic() {
return "share-enterprise-report.html";
}
@GetMapping(value = "/sharePlanReport")
public String shareRedirect() {
return "share-plan-report.html";

View File

@ -0,0 +1,11 @@
package io.metersphere.reportstatistics.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class HeadlessRequest {
public String url;
public String driverPath;
}

View File

@ -6,16 +6,20 @@ import io.metersphere.base.domain.ReportStatistics;
import io.metersphere.base.domain.ReportStatisticsExample;
import io.metersphere.base.domain.ReportStatisticsWithBLOBs;
import io.metersphere.base.mapper.ReportStatisticsMapper;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.reportstatistics.dto.ReportStatisticsSaveRequest;
import io.metersphere.reportstatistics.dto.ReportStatisticsType;
import io.metersphere.reportstatistics.dto.TestCaseCountTableDTO;
import io.metersphere.dto.BaseSystemConfigDTO;
import io.metersphere.reportstatistics.dto.*;
import io.metersphere.reportstatistics.dto.charts.Series;
import io.metersphere.reportstatistics.dto.table.TestCaseCountTableDataDTO;
import io.metersphere.reportstatistics.dto.table.TestCaseCountTableItemDataDTO;
import io.metersphere.reportstatistics.dto.table.TestCaseCountTableRowDTO;
import io.metersphere.reportstatistics.utils.ChromeUtils;
import io.metersphere.service.SystemParameterService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
@ -31,6 +35,8 @@ import java.util.UUID;
public class ReportStatisticsService {
@Resource
private ReportStatisticsMapper reportStatisticsMapper;
@Resource
private TestCaseCountService testCaseCountService;
public ReportStatisticsWithBLOBs saveByRequest(ReportStatisticsSaveRequest request) {
ReportStatisticsWithBLOBs model = new ReportStatisticsWithBLOBs();
@ -67,8 +73,17 @@ public class ReportStatisticsService {
public ReportStatisticsWithBLOBs selectById(String id) {
ReportStatisticsWithBLOBs blob = reportStatisticsMapper.selectByPrimaryKey(id);
JSONObject dataOption = JSONObject.parseObject(blob.getDataOption());
JSONObject selectOption = JSONObject.parseObject(blob.getSelectOption());
JSONObject dataOption = JSONObject.parseObject(blob.getDataOption());
boolean isReportNeedUpdate = this.isReportNeedUpdate(blob);
if (isReportNeedUpdate) {
TestCaseCountRequest countRequest = JSONObject.parseObject(blob.getSelectOption(), TestCaseCountRequest.class);
JSONObject returnDataOption = this.reloadDatas(countRequest, dataOption.getString("chartType"));
if (returnDataOption != null) {
dataOption = returnDataOption;
}
}
if (!dataOption.containsKey("showTable")) {
List<TestCaseCountTableDTO> dtos = JSONArray.parseArray(dataOption.getString("tableData"), TestCaseCountTableDTO.class);
TestCaseCountTableDataDTO showTable = this.countShowTable(selectOption.getString("xaxis"), JSONArray.parseArray(selectOption.getString("yaxis"), String.class), dtos);
@ -77,6 +92,55 @@ public class ReportStatisticsService {
}
return blob;
}
private JSONObject reloadDatas(TestCaseCountRequest request, String chartType) {
if (StringUtils.isEmpty(chartType)) {
chartType = "bar";
}
JSONObject returnObject = new JSONObject();
TestCaseCountResponse testCaseCountResponse = testCaseCountService.getReport(request);
if (testCaseCountResponse.getBarChartDTO() != null) {
JSONObject loadOptionObject = new JSONObject();
loadOptionObject.put("legend", JSONObject.toJSON(testCaseCountResponse.getBarChartDTO().getLegend()));
loadOptionObject.put("xAxis", JSONObject.toJSON(testCaseCountResponse.getBarChartDTO().getXAxis()));
loadOptionObject.put("xaxis", JSONObject.toJSON(testCaseCountResponse.getBarChartDTO().getXAxis()));
loadOptionObject.put("yAxis", JSONObject.toJSON(testCaseCountResponse.getBarChartDTO().getYAxis()));
loadOptionObject.put("tooltip", new JSONObject());
loadOptionObject.put("lable", new JSONObject());
if (!CollectionUtils.isEmpty(testCaseCountResponse.getBarChartDTO().getSeries())) {
List<Series> list = testCaseCountResponse.getBarChartDTO().getSeries();
for (Series model : list) {
model.setType(chartType);
}
loadOptionObject.put("series", JSONArray.toJSON(list));
}
loadOptionObject.put("grid", new JSONObject().put("bottom", "75px"));
returnObject.put("loadOption", loadOptionObject);
}
if (testCaseCountResponse.getPieChartDTO() != null) {
JSONObject pieOptionObject = new JSONObject();
pieOptionObject.put("title", JSONObject.toJSON(testCaseCountResponse.getPieChartDTO().getTitle()));
pieOptionObject.put("xAxis", JSONObject.toJSON(testCaseCountResponse.getPieChartDTO().getXAxis()));
if (!CollectionUtils.isEmpty(testCaseCountResponse.getPieChartDTO().getSeries())) {
List<Series> list = testCaseCountResponse.getPieChartDTO().getSeries();
for (Series model : list) {
model.setType(chartType);
}
pieOptionObject.put("series", JSONArray.toJSON(list));
}
pieOptionObject.put("grid", new JSONObject().put("bottom", "75px"));
if (testCaseCountResponse.getPieChartDTO().getWidth() > 0) {
pieOptionObject.put("width", testCaseCountResponse.getPieChartDTO().getWidth());
}
returnObject.put("pieOption", pieOptionObject);
}
if (testCaseCountResponse.getTableDTOs() != null) {
returnObject.put("tableData", JSONArray.toJSON(testCaseCountResponse.getTableDTOs()));
}
returnObject.put("chartType", chartType);
return returnObject;
}
private TestCaseCountTableDataDTO countShowTable(String groupName, List<String> yaxis, List<TestCaseCountTableDTO> dtos) {
if (yaxis == null) {
yaxis = new ArrayList<>();
@ -185,4 +249,26 @@ public class ReportStatisticsService {
example.setOrderByClause("create_time DESC");
return reportStatisticsMapper.selectByExample(example);
}
public String getImageContentById(ReportStatisticsWithBLOBs reportRecordId) {
ChromeUtils chromeUtils = new ChromeUtils();
HeadlessRequest headlessRequest = new HeadlessRequest();
BaseSystemConfigDTO baseInfo = CommonBeanFactory.getBean(SystemParameterService.class).getBaseInfo();
// 占位符
String platformUrl = "http://localhost:8081";
if (baseInfo != null) {
platformUrl = baseInfo.getUrl();
}
platformUrl += "/echartPic?shareId=" + reportRecordId.getId();
headlessRequest.setUrl(platformUrl);
String imageData = chromeUtils.getImageInfo(headlessRequest);
return imageData;
}
public boolean isReportNeedUpdate(ReportStatisticsWithBLOBs model) {
JSONObject selectOption = JSONObject.parseObject(model.getSelectOption());
return selectOption.containsKey("timeType") && StringUtils.equalsIgnoreCase("dynamicTime", selectOption.getString("timeType"));
}
}

View File

@ -496,6 +496,13 @@ public class TestCaseCountService {
this.add("50%");
}});
Map<String,Object> labelMap = new HashMap<>();
Map<String,Object> normalMap = new HashMap<>();
normalMap.put("show",true);
normalMap.put("formatter","{b}: {c}({d}%)");
labelMap.put("normal",normalMap);
series.setLabel(labelMap);
Title title = new Title();
title.setSubtext(summary.groupName);
title.setLeft(leftPxStr);

View File

@ -0,0 +1,75 @@
package io.metersphere.reportstatistics.utils;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.reportstatistics.dto.HeadlessRequest;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
public class ChromeUtils {
private static final String DEFAULT_DRIVERPATH = "/Users/handsomesong/chromeDriver/chromedriver_mac64_m1/chromedriver";
private WebDriver genWebDriver(HeadlessRequest headlessRequest) {
String driverPath = headlessRequest.driverPath;
if (StringUtils.isEmpty(driverPath)) {
driverPath = DEFAULT_DRIVERPATH;
}
//初始化一个chrome浏览器实例driver
ChromeOptions options = new ChromeOptions();
options.addArguments("headless");
options.addArguments("no-sandbox");
options.addArguments("disable-gpu");
options.addArguments("disable-features=NetworkService");
options.addArguments("ignore-certificate-errors");
options.addArguments("silent-launch");
options.addArguments("disable-application-cache");
options.addArguments("disable-web-security");
options.addArguments("no-proxy-server");
options.addArguments("disable-dev-shm-usage");
options.addArguments("lang=zh_CN.UTF-8");
WebDriver driver = null;
try {
System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, driverPath);
driver = new ChromeDriver(options);
driver.get(headlessRequest.url);
driver.manage().window().fullscreen();
}catch (Exception e){
if(driver != null){
driver.quit();
driver = null;
}
LogUtil.error(e);
}
return driver;
}
public String getImageInfo(HeadlessRequest request){
WebDriver driver = this.genWebDriver(request);
String files = null;
if(driver != null){
try {
//预留echart动画的加载时间
Thread.sleep(3 * 1000);
String js = "var chartsCanvas = document.getElementById('picChart').getElementsByTagName('canvas')[0];" +
"var imageUrl = null;" +
"if (chartsCanvas!= null) {" +
" imageUrl = chartsCanvas && chartsCanvas.toDataURL('image/png');"+
"return imageUrl;" +
"}";
files = ((JavascriptExecutor)driver).executeScript(js).toString();
}catch (Exception e){
LogUtil.error(e);
}finally {
driver.quit();
}
}
if(StringUtils.isNotEmpty(files)){
return files;
}else {
return null;
}
}
}

@ -1 +1 @@
Subproject commit a41ba52f5495bc68f84a07643442d2b0e9524786
Subproject commit 9dd0111ebafcdab7153a97ab3a3af39f5c69dac5

View File

@ -873,6 +873,12 @@
"resourceId": "PROJECT_ENTERPRISE_REPORT",
"license": true
},
{
"id": "PROJECT_ENTERPRISE_REPORT:READ+SCHEDULE",
"name": "定时发送",
"resourceId": "PROJECT_ENTERPRISE_REPORT",
"license": true
},
{
"id": "PROJECT_ENTERPRISE_REPORT:READ+EDIT",
"name": "修改报告",

View File

@ -165,6 +165,9 @@ export default {
if (this.paramRow.redirectFrom == 'testPlan') {
paramTestId = this.paramRow.id;
this.scheduleTaskType = "TEST_PLAN_TEST";
} else if (this.paramRow.redirectFrom == 'enterpriseReport') {
paramTestId = this.paramRow.id;
this.scheduleTaskType = "ENTERPRISE_REPORT";
} else {
paramTestId = this.paramRow.id;
this.scheduleTaskType = "API_SCENARIO_TEST";
@ -192,6 +195,9 @@ export default {
if (row.redirectFrom == 'testPlan') {
paramTestId = row.id;
this.scheduleTaskType = "TEST_PLAN_TEST";
} else if (row.redirectFrom == 'enterpriseReport') {
paramTestId = row.id;
this.scheduleTaskType = "ENTERPRISE_REPORT";
} else {
paramTestId = row.id;
this.scheduleTaskType = "API_SCENARIO_TEST";
@ -263,6 +269,13 @@ export default {
if (param.id) {
url = '/schedule/update';
}
} else if (this.scheduleTaskType == 'ENTERPRISE_REPORT') {
param.scheduleFrom = "enterpriseReport";
//
url = '/schedule/create';
if (param.id) {
url = '/schedule/update';
}
} else {
param.scheduleFrom = "scenario";
if (param.id) {

View File

@ -94,6 +94,7 @@ export default {
let data = response.data.barChartDTO;
let pieData = response.data.pieChartDTO;
let selectTableData = response.data.tableDTOs;
console.info(response.data);
this.initPic(data, pieData, selectTableData);
}, error => {
this.loading = false;
@ -101,6 +102,7 @@ export default {
},
initPic(barData, pieData, selectTableData) {
this.loading = true;
this.resetOptions();
if (barData) {
this.loadOption.legend = barData.legend;
this.loadOption.xAxis = barData.xaxis;
@ -207,6 +209,24 @@ export default {
return "";
}
},
resetOptions() {
this.loadOption = {
legend: {},
xAxis: {},
yAxis: {},
label: {},
tooltip: {},
series: []
};
this.pieOption = {
legend: {},
label: {},
tooltip: {},
series: [],
title: [],
};
this.tableData = [];
},
selectAndSaveReport(reportName) {
let opt = this.$refs.countFilter.getOption();
this.options = opt;

View File

@ -269,9 +269,6 @@ export default {
this.loading = true;
this.option = opt;
this.$nextTick(() => {
if(this.option.timeType === "dynamicTime"){
this.init();
}
this.loading = false;
});
},

@ -1 +1 @@
Subproject commit dd73a29ed22916b8d7ff70d1062604a17177d3a5
Subproject commit 249ac53686faf82906dcfabf76d66bdc8fda39db

View File

@ -0,0 +1,109 @@
<template>
<report-chart v-if="!needReloading" :read-only="true" :need-full-screen="false" :chart-type="dataOption.chartType"
ref="analysisChart" :load-option="dataOption.loadOption" :pie-option="dataOption.pieOption"/>
</template>
<script>
import ReportChart from "@/business/components/xpack/reportstatistics/projectreport/components/chart/ReportChart";
import {getShareId} from "@/common/js/utils";
export default {
name: "ShareEnterpriseReportTemplate",
components: {ReportChart},
data() {
return {
needReloading: false,
shareId: '',
dataOption: {
chartType: '',
loadOption: {
legend: {},
xAxis: {},
yAxis: {},
label: {},
tooltip: {},
series: []
},
pieOption: {
legend: {},
label: {},
tooltip: {},
series: [],
title: [],
},
},
}
},
created() {
this.initEchartData();
},
methods: {
initEchartData() {
this.shareId = getShareId();
let paramObj = {
id: this.shareId
};
this.resetOptions();
this.$post('/share/info/selectHistoryReportById', paramObj, response => {
let reportData = response.data;
if (reportData) {
let selectOption = JSON.parse(reportData.selectOption);
let data = JSON.parse(reportData.dataOption);
data.selectOption = selectOption;
this.dataOption = data;
console.info(this.dataOption);
this.reloadChart();
}
}, (error) => {
this.$error(this.$t('查找报告失败!'));
return false;
});
},
initPic(loadOptionParam, tableData) {
this.loading = true;
if (loadOptionParam) {
this.loadOption.legend = loadOptionParam.legend;
this.loadOption.xAxis = loadOptionParam.xaxis;
this.loadOption.series = loadOptionParam.series;
this.loadOption.grid = {
bottom: '75px',//
}
this.loadOption.series.forEach(item => {
item.type = this.$refs.analysisChart.chartType;
})
}
if (tableData) {
this.tableData = tableData;
}
this.loading = false;
},
reloadChart() {
console.info("load data over, reload compnents.");
this.$refs.analysisChart.reload();
},
resetOptions() {
this.dataOption = {
chartType: '',
loadOption: {
legend: {},
xAxis: {},
yAxis: {},
label: {},
tooltip: {},
series: []
},
pieOption: {
legend: {},
label: {},
tooltip: {},
series: [],
title: [],
},
};
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="shortcut icon" href="<%= BASE_URL %>favicon.ico">
<title>Enterprise Report</title>
</head>
<body>
<div id="shareEnterpriseReport"></div>
</body>
</html>

View File

@ -0,0 +1,40 @@
import ShareEnterpriseReportTemplate from "./ShareEnterpriseReportTemplate";
import Vue from 'vue';
import ElementUI, {Button, Col, Form, FormItem, Input, Row, Main, Card, Table, TableColumn} from 'element-ui';
import '@/assets/theme/index.css';
import '@/common/css/menu-header.css';
import '@/common/css/main.css';
import i18n from "@/i18n/i18n";
import chart from "@/common/js/chart";
import filters from "@/common/js/filter";
import icon from "@/common/js/icon";
import message from "@/common/js/message";
import Ajax from "@/common/js/ajax";
Vue.use(ElementUI, {
i18n: (key, value) => i18n.t(key, value)
});
Vue.use(Row);
Vue.use(Col);
Vue.use(Form);
Vue.use(FormItem);
Vue.use(Input);
Vue.use(Button);
Vue.use(chart);
Vue.use(Main);
Vue.use(Card);
Vue.use(Ajax)
Vue.use(TableColumn);
Vue.use(Table);
Vue.use(filters);
Vue.use(icon);
Vue.use(message);
new Vue({
el: '#shareEnterpriseReport',
i18n,
render: h => h(ShareEnterpriseReportTemplate)
});

View File

@ -44,6 +44,11 @@ module.exports = {
template: "src/template/report/plan/plan-report.html",
filename: "plan-report.html",
},
enterpriseReport: {
entry: "src/template/enterprise/share/share-enterprise-report.js",
template: "src/template/enterprise/share/share-enterprise-report.html",
filename: "share-enterprise-report.html",
},
},
configureWebpack: {
devtool: 'source-map',
@ -63,7 +68,7 @@ module.exports = {
config.plugin('inline-source-html-planReport')
.after('html-planReport')
.use(HtmlWebpackInlineSourcePlugin);
config.plugins.delete('prefetch');
}
};