feat(性能测试): 测试报告支持自定义图表
This commit is contained in:
parent
cc24a21e48
commit
051552c0da
|
@ -1,6 +1,24 @@
|
|||
package io.metersphere.commons.constants;
|
||||
|
||||
public enum ReportKeys {
|
||||
LoadChart, ResponseTimeChart, ResponseCodeChart, Errors, ErrorsTop5, RequestStatistics, Overview, TimeInfo, ResultStatus, ErrorsChart
|
||||
|
||||
LoadChart,
|
||||
ResponseTimeChart,
|
||||
ResponseTimePercentilesChart,
|
||||
ConnectTimeChart,
|
||||
BytesThroughputChart,
|
||||
DistributedActiveThreads,
|
||||
HitsChart,
|
||||
LatencyChart,
|
||||
ResponseCodeChart,
|
||||
TransactionsChart,
|
||||
TotalTransactionsChart,
|
||||
ResponseTimeVsRequestChart,
|
||||
LatencyVsRequestChart,
|
||||
ErrorsChart,
|
||||
Errors,
|
||||
ErrorsTop5,
|
||||
RequestStatistics,
|
||||
Overview,
|
||||
TimeInfo,
|
||||
ResultStatus,
|
||||
}
|
||||
|
|
|
@ -68,6 +68,11 @@ public class PerformanceReportController {
|
|||
return performanceReportService.getReportErrors(reportId);
|
||||
}
|
||||
|
||||
@GetMapping("/content/{reportKey}/{reportId}")
|
||||
public List<ChartsData> getReportChart(@PathVariable String reportKey, @PathVariable String reportId) {
|
||||
return performanceReportService.getReportChart(reportKey, reportId);
|
||||
}
|
||||
|
||||
@GetMapping("/content/errors_top5/{reportId}")
|
||||
public List<ErrorsTop5> getReportErrorsTop5(@PathVariable String reportId) {
|
||||
return performanceReportService.getReportErrorsTOP5(reportId);
|
||||
|
|
|
@ -392,4 +392,14 @@ public class PerformanceReportService {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<ChartsData> getReportChart(String reportKey, String reportId) {
|
||||
checkReportStatus(reportId);
|
||||
try {
|
||||
String content = getContent(reportId, ReportKeys.valueOf(reportKey));
|
||||
return JSON.parseArray(content, ChartsData.class);
|
||||
} catch (Exception e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
@click="rerun(testId)">
|
||||
{{ $t('report.test_execute_again') }}
|
||||
</el-button>
|
||||
<el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)" v-permission="['PROJECT_PERFORMANCE_REPORT:READ+EXPORT']">
|
||||
<el-button :disabled="isReadOnly" type="info" plain size="mini" @click="handleExport(reportName)"
|
||||
v-permission="['PROJECT_PERFORMANCE_REPORT:READ+EXPORT']">
|
||||
{{ $t('test_track.plan_view.export_report') }}
|
||||
</el-button>
|
||||
<el-button :disabled="report.status !== 'Completed'" type="default" plain
|
||||
|
@ -84,6 +85,9 @@
|
|||
<el-tab-pane :label="$t('report.test_overview')">
|
||||
<ms-report-test-overview :report="report" ref="testOverview"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('report.test_details')">
|
||||
<ms-report-test-details :report="report" ref="testDetails"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('report.test_request_statistics')">
|
||||
<ms-report-request-statistics :report="report" ref="requestStatistics"/>
|
||||
</el-tab-pane>
|
||||
|
@ -122,6 +126,7 @@
|
|||
import MsReportErrorLog from './components/ErrorLog';
|
||||
import MsReportLogDetails from './components/LogDetails';
|
||||
import MsReportRequestStatistics from './components/RequestStatistics';
|
||||
import MsReportTestDetails from './components/TestDetails';
|
||||
import MsReportTestOverview from './components/TestOverview';
|
||||
import MsPerformancePressureConfig from "./components/PerformancePressureConfig";
|
||||
import MsContainer from "../../common/components/MsContainer";
|
||||
|
@ -147,6 +152,7 @@ export default {
|
|||
MsReportTestOverview,
|
||||
MsContainer,
|
||||
MsMainContainer,
|
||||
MsReportTestDetails,
|
||||
MsPerformancePressureConfig
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -0,0 +1,299 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="6">
|
||||
<el-collapse v-model="activeNames" class="test-detail">
|
||||
<el-collapse-item :title="$t('load_test.report.ActiveThreadsChart')" name="users">
|
||||
<el-checkbox-group v-model="checkList['ActiveThreadsChart']"
|
||||
@change="handleChecked('ActiveThreadsChart')">
|
||||
<el-checkbox v-for="name in checkOptions['ActiveThreadsChart']" :key="name" :label="name"/>
|
||||
</el-checkbox-group>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="$t('load_test.report.TransactionsChart')" name="transactions">
|
||||
<el-checkbox-group v-model="checkList['TransactionsChart']" @change="handleChecked('TransactionsChart')">
|
||||
<el-checkbox v-for="name in checkOptions['TransactionsChart']" :key="name" :label="name"/>
|
||||
</el-checkbox-group>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="$t('load_test.report.ResponseTimeChart')" name="responseTime">
|
||||
<el-checkbox-group v-model="checkList['ResponseTimeChart']" @change="handleChecked('ResponseTimeChart')">
|
||||
<el-checkbox v-for="name in checkOptions['ResponseTimeChart']" :key="name" :label="name"/>
|
||||
</el-checkbox-group>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="$t('load_test.report.ResponseTimePercentilesChart')" name="responseTimePercentiles">
|
||||
<el-checkbox-group v-model="checkList['ResponseTimePercentilesChart']"
|
||||
@change="handleChecked('ResponseTimePercentilesChart')">
|
||||
<el-checkbox v-for="name in checkOptions['ResponseTimePercentilesChart']" :key="name" :label="name"/>
|
||||
</el-checkbox-group>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="$t('load_test.report.ResponseCodeChart')" name="responseCode">
|
||||
<el-checkbox-group v-model="checkList['ResponseCodeChart']" @change="handleChecked('ResponseCodeChart')">
|
||||
<el-checkbox v-for="code in checkOptions['ResponseCodeChart']" :key="code" :label="code"/>
|
||||
</el-checkbox-group>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="$t('load_test.report.LatencyChart')" name="latency">
|
||||
<el-checkbox-group v-model="checkList['LatencyChart']" @change="handleChecked('LatencyChart')">
|
||||
<el-checkbox v-for="name in checkOptions['LatencyChart']" :key="name" :label="name"/>
|
||||
</el-checkbox-group>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="$t('load_test.report.BytesThroughputChart')" name="bytes">
|
||||
<el-checkbox-group v-model="checkList['BytesThroughputChart']"
|
||||
@change="handleChecked('BytesThroughputChart')">
|
||||
<el-checkbox v-for="code in checkOptions['BytesThroughputChart']" :key="code" :label="code"/>
|
||||
</el-checkbox-group>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="$t('load_test.report.ErrorsChart')" name="errors">
|
||||
<el-checkbox-group v-model="checkList['ErrorsChart']" @change="handleChecked('ErrorsChart')">
|
||||
<el-checkbox v-for="name in checkOptions['ErrorsChart']" :key="name" :label="name"/>
|
||||
</el-checkbox-group>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<ms-chart ref="chart2" :options="totalOption" class="chart-config" :autoresize="true"></ms-chart>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsChart from "@/business/components/common/chart/MsChart";
|
||||
|
||||
|
||||
const CHART_MAP = [
|
||||
'ActiveThreadsChart',
|
||||
'TransactionsChart',
|
||||
'ResponseTimePercentilesChart',
|
||||
'ResponseTimeChart',
|
||||
'ResponseCodeChart',
|
||||
'ErrorsChart',
|
||||
'LatencyChart',
|
||||
'BytesThroughputChart',
|
||||
];
|
||||
|
||||
export default {
|
||||
name: "TestDetails",
|
||||
components: {MsChart},
|
||||
props: ['report', 'export'],
|
||||
data() {
|
||||
return {
|
||||
activeNames: 'users',
|
||||
loadOption: {},
|
||||
resOption: {},
|
||||
totalOption: {},
|
||||
responseCodes: [],
|
||||
checkList: CHART_MAP.reduce((result, curr) => {
|
||||
result[curr] = [];
|
||||
return result;
|
||||
}, {}),
|
||||
checkOptions: {},
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
},
|
||||
init: false,
|
||||
baseOption: {
|
||||
title: {
|
||||
text: 'Test Details',
|
||||
left: 'center',
|
||||
top: 20,
|
||||
textStyle: {
|
||||
color: '#99743C'
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
// extraCssText: 'z-index: 999;',
|
||||
confine: true,
|
||||
formatter: function (params, ticket, callback) {
|
||||
let result = "";
|
||||
let name = params[0].name;
|
||||
result += name + "<br/>";
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
let seriesName = params[i].seriesName;
|
||||
if (seriesName.length > 100) {
|
||||
seriesName = seriesName.substring(0, 100);
|
||||
}
|
||||
let value = params[i].value;
|
||||
let marker = params[i].marker;
|
||||
result += marker + seriesName + ": " + value[1] + "<br/>";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {},
|
||||
xAxis: {},
|
||||
|
||||
series: []
|
||||
},
|
||||
chartData: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleChecked(name) {
|
||||
this.getTotalChart();
|
||||
},
|
||||
initTableData() {
|
||||
this.init = true;
|
||||
|
||||
for (const name of CHART_MAP) {
|
||||
this.getCheckOptions(name);
|
||||
}
|
||||
|
||||
this.checkList['ActiveThreadsChart'] = ['ALL'];
|
||||
this.checkList['TransactionsChart'] = ['ALL'];
|
||||
this.checkList['ResponseTimeChart'] = ['ALL'];
|
||||
|
||||
this.getTotalChart();
|
||||
},
|
||||
getCheckOptions(reportKey) {
|
||||
this.$get("/performance/report/content/" + reportKey + "/" + this.id)
|
||||
.then(res => {
|
||||
let data = res.data.data;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName);
|
||||
yAxisIndex0List = this._unique(yAxisIndex0List);
|
||||
this.checkOptions[reportKey] = ['ALL'].concat(yAxisIndex0List);
|
||||
});
|
||||
},
|
||||
getTotalChart() {
|
||||
this.totalOption = {};
|
||||
this.chartData = [];
|
||||
for (let name in this.checkList) {
|
||||
if (this.checkList[name].length > 0) {
|
||||
this.getChart(name, this.checkList[name]);
|
||||
}
|
||||
}
|
||||
},
|
||||
getChart(reportKey, checkList) {
|
||||
this.$get("/performance/report/content/" + reportKey + "/" + this.id)
|
||||
.then(res => {
|
||||
let data = res.data.data;
|
||||
if (!data || data.length === 0) {
|
||||
this.init = false;
|
||||
}
|
||||
if (checkList) {
|
||||
data = data.filter(item => {
|
||||
if (checkList.indexOf('ALL') > -1) {
|
||||
return true;
|
||||
}
|
||||
if (checkList.indexOf(item.groupName) > -1) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// prefix
|
||||
data.forEach(item => {
|
||||
item.groupName = this.$t('load_test.report.' + reportKey) + ': ' + item.groupName;
|
||||
});
|
||||
|
||||
this.chartData = this.chartData.concat(data);
|
||||
|
||||
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
|
||||
let yAxisListMax = this._getChartMax(yAxisList);
|
||||
|
||||
this.baseOption.yAxis = [
|
||||
{
|
||||
name: 'Value',
|
||||
type: 'value',
|
||||
min: 0,
|
||||
}
|
||||
];
|
||||
|
||||
this.totalOption = this.generateOption(this.baseOption, this.chartData);
|
||||
})
|
||||
.catch(() => {
|
||||
this.totalOption = {};
|
||||
});
|
||||
},
|
||||
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.xAxis, item.yAxis.toFixed(2)]);
|
||||
});
|
||||
this.$set(option.legend, "data", legend);
|
||||
this.$set(option.legend, "type", "scroll");
|
||||
this.$set(option.legend, "bottom", "10px");
|
||||
this.$set(option.xAxis, "data", xAxis);
|
||||
for (let name in series) {
|
||||
let d = series[name];
|
||||
d.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
let items = {
|
||||
name: name,
|
||||
type: 'line',
|
||||
data: d,
|
||||
smooth: true,
|
||||
sampling: 'lttb',
|
||||
animation: !this.export,
|
||||
};
|
||||
seriesData.push(items);
|
||||
}
|
||||
this.$set(option, "series", seriesData);
|
||||
return option;
|
||||
},
|
||||
_getChartMax(arr) {
|
||||
const max = Math.max(...arr);
|
||||
return Math.ceil(max / 4.5) * 5;
|
||||
},
|
||||
_unique(arr) {
|
||||
return Array.from(new Set(arr));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.id = this.$route.path.split('/')[4];
|
||||
this.initTableData();
|
||||
},
|
||||
watch: {
|
||||
'$route'(to) {
|
||||
if (to.name === "perReportView") {
|
||||
this.id = to.path.split('/')[4];
|
||||
this.init = false;
|
||||
this.initTableData();
|
||||
}
|
||||
},
|
||||
report: {
|
||||
handler(val) {
|
||||
if (!val.status || !val.id) {
|
||||
return;
|
||||
}
|
||||
let status = val.status;
|
||||
this.id = val.id;
|
||||
if (this.init) {
|
||||
return;
|
||||
}
|
||||
if (status === "Completed" || status === "Running") {
|
||||
this.initTableData();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-config {
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
.test-detail {
|
||||
height: calc(100vh - 320px);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
|
@ -646,7 +646,15 @@ export default {
|
|||
project_file_exist: "The file already exists in the project, please import it directly",
|
||||
project_file_update_type_error: 'Updated file types must be consistent',
|
||||
report: {
|
||||
diff: "Compare"
|
||||
diff: "Compare",
|
||||
ActiveThreadsChart: 'Users',
|
||||
TransactionsChart: 'Requests/Transactions',
|
||||
ErrorsChart: 'Error',
|
||||
ResponseTimeChart: 'Response Time',
|
||||
ResponseTimePercentilesChart: 'Response time percentage',
|
||||
ResponseCodeChart: 'Response Code',
|
||||
LatencyChart: 'Latency',
|
||||
BytesThroughputChart: 'Bytes',
|
||||
},
|
||||
},
|
||||
api_test: {
|
||||
|
@ -847,7 +855,7 @@ export default {
|
|||
wait_controller: "Wait controller",
|
||||
if_controller: "If controller",
|
||||
loop_controller: "Loop Controller",
|
||||
transcation_controller:"Transcation controller",
|
||||
transcation_controller: "Transcation controller",
|
||||
scenario_import: "Scenario import",
|
||||
customize_script: "Customize script",
|
||||
customize_req: "Customize req",
|
||||
|
|
|
@ -644,7 +644,15 @@ export default {
|
|||
load_api_automation_jmx: '引用接口自动化场景',
|
||||
project_file_exist: "项目中已存在该文件,请直接引用",
|
||||
report: {
|
||||
diff: "对比"
|
||||
diff: "对比",
|
||||
ActiveThreadsChart: '用户数',
|
||||
TransactionsChart: '请求/事务数',
|
||||
ErrorsChart: '错误',
|
||||
ResponseTimeChart: '响应时间',
|
||||
ResponseTimePercentilesChart: '响应时间百分比',
|
||||
ResponseCodeChart: '响应码',
|
||||
LatencyChart: '延迟时间',
|
||||
BytesThroughputChart: '字节数',
|
||||
},
|
||||
project_file_update_type_error: '更新的文件类型必须一致',
|
||||
},
|
||||
|
|
|
@ -644,7 +644,15 @@ export default {
|
|||
load_api_automation_jmx: '引用接口自動化場景',
|
||||
project_file_exist: "項目中已存在該文件,請直接引用",
|
||||
report: {
|
||||
diff: "對比"
|
||||
diff: "對比",
|
||||
ActiveThreadsChart: '用戶數',
|
||||
TransactionsChart: '請求/事務數',
|
||||
ErrorsChart: '錯誤',
|
||||
ResponseTimeChart: '響應時間',
|
||||
ResponseTimePercentilesChart: '響應時間百分比',
|
||||
ResponseCodeChart: '響應碼',
|
||||
LatencyChart: '延遲時間',
|
||||
BytesThroughputChart: '字節數',
|
||||
},
|
||||
project_file_update_type_error: '更新的文件類型必須一致',
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue