fix(测试跟踪): 测试计划报告导出性能测试报告无法打开

--bug=1018685 --user=陈建星 【测试跟踪】测试计划-测试报告-导出-接口和性能的报告无法打开 https://www.tapd.cn/55049933/s/1273189
This commit is contained in:
chenjianxing 2022-10-23 16:35:13 +08:00 committed by jianxing
parent 31ddfb94fc
commit df34484cc6
21 changed files with 3474 additions and 1683 deletions

View File

@ -1,768 +0,0 @@
<template>
<!-- <div>-->
<!-- &lt;!&ndash; 基本配置 &ndash;&gt;-->
<!-- <el-row>-->
<!-- <el-col :span="6">-->
<!-- <el-form :inline="true" :disabled="isReadOnly">-->
<!-- <el-form-item>-->
<!-- <div>{{ $t('load_test.connect_timeout') }}</div>-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- <el-input-number-->
<!-- size="mini" v-model="timeout"-->
<!-- controls-position="right"-->
<!-- :min="0"/>-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- ms-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- </el-col>-->
<!-- <el-col :span="6">-->
<!-- <el-form :inline="true" :disabled="isReadOnly">-->
<!-- <el-form-item>-->
<!-- <div>{{ $t('load_test.response_timeout') }}</div>-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- <el-input-number-->
<!-- size="mini" :min="0"-->
<!-- controls-position="right"-->
<!-- v-model="responseTimeout"/>-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- ms-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- </el-col>-->
<!-- <el-col :span="6">-->
<!-- <el-form :inline="true" :disabled="isReadOnly">-->
<!-- <el-form-item>-->
<!-- <div>-->
<!-- {{ $t('load_test.granularity') }}-->
<!-- <el-popover-->
<!-- placement="left"-->
<!-- width="300"-->
<!-- trigger="hover">-->
<!-- <el-table :data="granularityData">-->
<!-- <el-table-column property="start" :label="$t('load_test.duration')">-->
<!-- <template v-slot:default="scope">-->
<!-- <span>{{ scope.row.start }}S - {{ scope.row.end }}S</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column property="granularity" :label="$t('load_test.granularity')"/>-->
<!-- </el-table>-->
<!-- <i slot="reference" class="el-icon-info pointer"/>-->
<!-- </el-popover>-->
<!-- </div>-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- <el-select v-model="granularity" :placeholder="$t('commons.please_select')" size="mini"-->
<!-- clearable>-->
<!-- <el-option v-for="op in granularityData" :key="op.granularity" :label="op.granularity"-->
<!-- :value="op.granularity"></el-option>-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- </el-col>-->
<!-- <el-col :span="6">-->
<!-- <el-form :inline="true" :disabled="isReadOnly">-->
<!-- <el-form-item>-->
<!-- <div>{{ $t('load_test.custom_http_code') }}</div>-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- <el-input-->
<!-- size="mini" v-model="statusCodeStr"-->
<!-- :placeholder="$t('load_test.separated_by_commas')"-->
<!-- @input="checkStatusCode"></el-input>-->
<!-- </el-form-item>-->
<!-- </el-form>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- &lt;!&ndash; DNS &ndash;&gt;-->
<!-- <el-row type="flex" justify="start">-->
<!-- <el-col :span="8">-->
<!-- <h3>{{ $t('load_test.domain_bind') }}</h3>-->
<!-- <el-button icon="el-icon-circle-plus-outline"-->
<!-- :disabled="isReadOnly"-->
<!-- plain size="mini" @click="add('domains')">-->
<!-- {{ $t('commons.add') }}-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <el-row>-->
<!-- <el-col :span="24">-->
<!-- <el-table :data="domains" size="mini" class="tb-edit" align="center" border highlight-current-row>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.domain')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-input-->
<!-- size="mini"-->
<!-- v-if="!isReadOnly"-->
<!-- type="textarea"-->
<!-- :rows="1"-->
<!-- class="edit-input"-->
<!-- v-model="row.domain"-->
<!-- :placeholder="$t('load_test.domain')"-->
<!-- clearable>-->
<!-- </el-input>-->
<!-- <span>{{ row.domain }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.ip')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-input-->
<!-- size="mini"-->
<!-- v-if="!isReadOnly"-->
<!-- type="textarea"-->
<!-- class="edit-input"-->
<!-- :rows="1"-->
<!-- v-model="row.ip"-->
<!-- :placeholder="$t('load_test.ip')"-->
<!-- clearable></el-input>-->
<!-- <span>{{ row.ip }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.enable')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-switch-->
<!-- :disabled="isReadOnly"-->
<!-- size="mini"-->
<!-- v-model="row.enable"-->
<!-- inactive-color="#DCDFE6"-->
<!-- >-->
<!-- </el-switch>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column align="center" :label="$t('load_test.operating')">-->
<!-- <template v-slot:default="{row, $index}">-->
<!-- <ms-table-operator-button :tip="$t('commons.delete')" icon="el-icon-delete"-->
<!-- type="danger" :disabled="isReadOnly"-->
<!-- @exec="del(row, 'domains', $index)"/>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- &lt;!&ndash; csv 配置 &ndash;&gt;-->
<!-- <el-row>-->
<!-- <el-col :span="8">-->
<!-- <h3>CSVDataSet</h3>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <el-row>-->
<!-- <el-col :span="24">-->
<!-- <el-table :data="csvFiles" size="mini" class="tb-edit" align="center" border highlight-current-row>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- prop="name"-->
<!-- :label="$t('commons.name')">-->
<!-- </el-table-column>-->
<!-- <el-table-column align="center" prop="csvSplit" :label="$t('load_test.csv_split')">-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-switch :disabled="isReadOnly" v-model="row.csvSplit"/>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column align="center" prop="csvHasHeader" :label="$t('load_test.csv_has_header')">-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-switch :disabled="isReadOnly || !row.csvSplit" v-model="row.csvHasHeader"/>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- &lt;!&ndash; 参数列表 &ndash;&gt;-->
<!-- <el-row>-->
<!-- <el-col :span="8">-->
<!-- <h3>{{ $t('load_test.params') }}</h3>-->
<!-- <el-button icon="el-icon-circle-plus-outline"-->
<!-- :disabled="isReadOnly"-->
<!-- plain size="mini" @click="add('params')">-->
<!-- {{ $t('commons.add') }}-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <el-row>-->
<!-- <el-col :span="24">-->
<!-- <el-table :data="params" size="mini" class="tb-edit" align="center" border highlight-current-row>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.param_name')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-input-->
<!-- size="mini"-->
<!-- v-if="!isReadOnly"-->
<!-- type="textarea"-->
<!-- :rows="1"-->
<!-- class="edit-input"-->
<!-- v-model="row.name"-->
<!-- :placeholder="$t('load_test.param_name')"-->
<!-- clearable>-->
<!-- </el-input>-->
<!-- <span>{{ row.name }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- :label="$t('load_test.param_value')"-->
<!-- show-overflow-tooltip align="center">-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-input-->
<!-- size="mini"-->
<!-- v-if="!isReadOnly"-->
<!-- type="textarea"-->
<!-- class="edit-input"-->
<!-- :rows="1"-->
<!-- v-model="row.value"-->
<!-- :placeholder="$t('load_test.param_value')"-->
<!-- clearable></el-input>-->
<!-- <span>{{ row.value }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.enable')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-switch-->
<!-- :disabled="isReadOnly"-->
<!-- size="mini"-->
<!-- v-model="row.enable"-->
<!-- inactive-color="#DCDFE6">-->
<!-- </el-switch>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column align="center" :label="$t('load_test.operating')">-->
<!-- <template v-slot:default="{row, $index}">-->
<!-- <ms-table-operator-button :tip="$t('commons.delete')" icon="el-icon-delete"-->
<!-- type="danger"-->
<!-- :disabled="isReadOnly"-->
<!-- @exec="del(row, 'params', $index)"/>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- &lt;!&ndash; JMeter Properties &ndash;&gt;-->
<!-- <el-row>-->
<!-- <el-col :span="8">-->
<!-- <h3>JMeter Properties</h3>-->
<!-- <el-button icon="el-icon-circle-plus-outline"-->
<!-- :disabled="isReadOnly"-->
<!-- plain size="mini" @click="add('properties')">-->
<!-- {{ $t('commons.add') }}-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <el-row>-->
<!-- <el-col :span="24">-->
<!-- <el-table :data="properties" size="mini" class="tb-edit" align="center" border highlight-current-row>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.param_name')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-input-->
<!-- size="mini"-->
<!-- v-if="!isReadOnly"-->
<!-- type="textarea"-->
<!-- :rows="1"-->
<!-- class="edit-input"-->
<!-- v-model="row.name"-->
<!-- :placeholder="$t('load_test.param_name')"-->
<!-- clearable>-->
<!-- </el-input>-->
<!-- <span>{{ row.name }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- :label="$t('load_test.param_value')"-->
<!-- show-overflow-tooltip align="center">-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-input-->
<!-- size="mini"-->
<!-- v-if="!isReadOnly"-->
<!-- type="textarea"-->
<!-- class="edit-input"-->
<!-- :rows="1"-->
<!-- v-model="row.value"-->
<!-- :placeholder="$t('load_test.param_value')"-->
<!-- clearable></el-input>-->
<!-- <span>{{ row.value }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.enable')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-switch-->
<!-- :disabled="isReadOnly"-->
<!-- size="mini"-->
<!-- v-model="row.enable"-->
<!-- inactive-color="#DCDFE6">-->
<!-- </el-switch>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column align="center" :label="$t('load_test.operating')">-->
<!-- <template v-slot:default="{row, $index}">-->
<!-- <ms-table-operator-button :tip="$t('commons.delete')" icon="el-icon-delete"-->
<!-- type="danger"-->
<!-- :disabled="isReadOnly"-->
<!-- @exec="del(row, 'properties', $index)"/>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- &lt;!&ndash; System Properties &ndash;&gt;-->
<!-- <el-row>-->
<!-- <el-col :span="8">-->
<!-- <h3>System Properties</h3>-->
<!-- <el-button icon="el-icon-circle-plus-outline"-->
<!-- :disabled="isReadOnly"-->
<!-- plain size="mini" @click="add('systemProperties')">-->
<!-- {{ $t('commons.add') }}-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <el-row>-->
<!-- <el-col :span="24">-->
<!-- <el-table :data="systemProperties" size="mini" class="tb-edit" align="center" border highlight-current-row>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.param_name')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-input-->
<!-- size="mini"-->
<!-- v-if="!isReadOnly"-->
<!-- type="textarea"-->
<!-- :rows="1"-->
<!-- class="edit-input"-->
<!-- v-model="row.name"-->
<!-- :placeholder="$t('load_test.param_name')"-->
<!-- clearable>-->
<!-- </el-input>-->
<!-- <span>{{ row.name }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- :label="$t('load_test.param_value')"-->
<!-- show-overflow-tooltip align="center">-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-input-->
<!-- size="mini"-->
<!-- v-if="!isReadOnly"-->
<!-- type="textarea"-->
<!-- class="edit-input"-->
<!-- :rows="1"-->
<!-- v-model="row.value"-->
<!-- :placeholder="$t('load_test.param_value')"-->
<!-- clearable></el-input>-->
<!-- <span>{{ row.value }}</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- :label="$t('load_test.enable')"-->
<!-- show-overflow-tooltip>-->
<!-- <template v-slot:default="{row}">-->
<!-- <el-switch-->
<!-- :disabled="isReadOnly"-->
<!-- size="mini"-->
<!-- v-model="row.enable"-->
<!-- inactive-color="#DCDFE6">-->
<!-- </el-switch>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column align="center" :label="$t('load_test.operating')">-->
<!-- <template v-slot:default="{row, $index}">-->
<!-- <ms-table-operator-button :tip="$t('commons.delete')" icon="el-icon-delete"-->
<!-- type="danger"-->
<!-- :disabled="isReadOnly"-->
<!-- @exec="del(row, 'systemProperties', $index)"/>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- &lt;!&ndash; 监控配置 &ndash;&gt;-->
<!-- <el-row>-->
<!-- <el-col :span="8">-->
<!-- <h3>{{ $t('commons.monitor') }}</h3>-->
<!-- <el-button icon="el-icon-circle-plus-outline" :disabled="isReadOnly"-->
<!-- plain size="mini" @click="addMonitor">-->
<!-- {{ $t('commons.add') }}-->
<!-- </el-button>-->
<!-- <el-button icon="el-icon-circle-plus-outline" plain size="mini"-->
<!-- :disabled="isReadOnly"-->
<!-- @click="batchAddMonitor">-->
<!-- {{ $t('commons.batch_add') }}-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <el-row>-->
<!-- <el-col :span="24">-->
<!-- <el-table :data="monitorParams" size="mini" class="tb-edit" border highlight-current-row>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- prop="name"-->
<!-- :label="$t('commons.name')">-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- prop="ip"-->
<!-- label="IP">-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- prop="port"-->
<!-- label="Port">-->
<!-- </el-table-column>-->
<!-- <el-table-column-->
<!-- align="center"-->
<!-- prop="description"-->
<!-- :label="$t('commons.description')">-->
<!-- </el-table-column>-->
<!-- <el-table-column align="center" :label="$t('load_test.operating')">-->
<!-- <template v-slot:default="{row, $index}">-->
<!-- <ms-table-operator-button tip="编辑" icon="el-icon-edit"-->
<!-- type="primary"-->
<!-- :disabled="isReadOnly"-->
<!-- @exec="modifyMonitor(row, $index)"/>-->
<!-- <ms-table-operator-button :tip="$t('commons.delete')" icon="el-icon-delete"-->
<!-- :disabled="isReadOnly"-->
<!-- type="danger"-->
<!-- @exec="delMonitor(row, $index)"/>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <edit-monitor ref="monitorDialog" :testId="testId" :list.sync="monitorParams"/>-->
<!-- <batch-add-monitor ref="batchMonitorDialog" @batchSave="batchSave"/>-->
<!-- </div>-->
</template>
<script>
import MsTableOperatorButton from "metersphere-frontend/src/components/MsTableOperatorButton.vue";
// import EditMonitor from "./EditMonitor";
// import BatchAddMonitor from "./BatchAddMonitor";
// import {getAdvancedConfig} from "@/api/performance";
export default {
name: "PerformanceAdvancedConfig",
components: {
// BatchAddMonitor,
// EditMonitor,
MsTableOperatorButton},
data() {
return {
timeout: undefined,
responseTimeout: undefined,
statusCode: [],
domains: [],
params: [],
properties: [],
systemProperties: [],
monitorParams: [],
csvFiles: [],
csvConfig: [],
statusCodeStr: '',
granularity: undefined,
granularityData: [
{start: 0, end: 100, granularity: 1},
{start: 101, end: 500, granularity: 5},
{start: 501, end: 1000, granularity: 10},
{start: 1001, end: 3000, granularity: 30},
{start: 3001, end: 6000, granularity: 60},
{start: 6001, end: 30000, granularity: 300},
{start: 30001, end: 60000, granularity: 600},
{start: 60001, end: 180000, granularity: 1800},
{start: 180001, end: 360000, granularity: 3600},
],
};
},
props: {
testId: String,
reportId: {
type: String
},
isReadOnly: {
type: Boolean,
default() {
return false;
}
},
isShare: Boolean,
shareId: String,
},
mounted() {
if (this.testId) {
this.getAdvancedConfig();
} else if (this.reportId) {
this.getAdvancedConfig('report');
}
},
watch: {
testId() {
if (this.testId) {
this.getAdvancedConfig();
}
},
csvFiles() {
this.refreshCsv();
}
},
methods: {
getAdvancedConfig(type) {
getAdvancedConfig(type, this.testId, this.reportId, this.isShare, this.shareId)
.then(response => {
let data = JSON.parse(response.data);
this.timeout = data.timeout;
this.responseTimeout = data.responseTimeout;
this.statusCode = data.statusCode || [];
this.statusCodeStr = this.statusCode.join(',');
this.domains = data.domains || [];
this.params = data.params || [];
this.granularity = data.granularity;
this.monitorParams = data.monitorParams || [];
this.properties = data.properties || [];
this.systemProperties = data.systemProperties || [];
this.csvConfig = data.csvConfig;
this.refreshCsv();
});
},
refreshCsv() {
if (this.csvConfig && this.csvFiles) {
this.csvFiles.forEach(f => {
f.csvSplit = this.csvConfig[f.name]?.csvSplit;
f.csvHasHeader = this.csvConfig[f.name]?.csvHasHeader;
});
}
},
add(dataName) {
if (dataName === 'domains') {
this[dataName].push({
domain: 'fit2cloud.com',
enable: true,
ip: '127.0.0.1',
edit: true,
});
}
if (dataName === 'params') {
this[dataName].push({
name: 'param1',
enable: true,
value: '0',
edit: true,
});
}
if (dataName === 'properties') {
this[dataName].push({
name: 'prop1',
enable: true,
value: '0',
edit: true,
});
}
if (dataName === 'systemProperties') {
this[dataName].push({
name: 'prop1',
enable: true,
value: '0',
edit: true,
});
}
},
edit(row) {
row.edit = !row.edit;
},
del(row, dataName, index) {
this[dataName].splice(index, 1);
},
confirmEdit(row) {
row.edit = false;
row.enable = true;
},
groupBy(data, key) {
// return data.reduce((p, c) => {
// let name = c[key];
// if (!Object.prototype.hasOwnProperty.call(p, name)) {
// p[name] = 0;
// }
// p[name]++;
// return p;
// }, {});
},
validConfig() {
let counts = this.groupBy(this.domains, 'domain');
for (let c in counts) {
if (counts[c] > 1) {
this.$error(this.$t('load_test.domain_is_duplicate'));
return false;
}
}
counts = this.groupBy(this.params, 'name');
for (let c in counts) {
if (counts[c] > 1) {
this.$error(this.$t('load_test.param_is_duplicate'));
return false;
}
}
counts = this.groupBy(this.properties, 'name');
for (let c in counts) {
if (counts[c] > 1) {
this.$error(this.$t('load_test.param_is_duplicate'));
return false;
}
}
if (this.domains.filter(d => !d.domain || !d.ip).length > 0) {
this.$error(this.$t('load_test.domain_ip_is_empty'));
return false;
}
if (this.params.filter(d => !d.name || !d.value).length > 0) {
this.$error(this.$t('load_test.param_name_value_is_empty'));
return false;
}
return true;
},
checkStatusCode() {
let license_num = this.statusCodeStr;
license_num = license_num.replace(/[^\d,]/g, ''); // .
this.statusCodeStr = license_num;
},
cancelAllEdit() {
this.domains.forEach(d => d.edit = false);
this.params.forEach(d => d.edit = false);
},
configurations() {
let statusCode = [];
if (this.statusCodeStr) {
statusCode = this.statusCodeStr.split(',');
}
return {
timeout: this.timeout,
responseTimeout: this.responseTimeout,
statusCode: statusCode,
params: this.params,
properties: this.properties,
systemProperties: this.systemProperties,
csvConfig: this.csvFiles.reduce((result, curr) => {
result[curr.name] = {csvHasHeader: curr.csvHasHeader, csvSplit: curr.csvSplit};
return result;
}, {}),
domains: this.domains,
granularity: this.granularity,
monitorParams: this.monitorParams
};
},
addMonitor() {
this.$refs.monitorDialog.open();
},
batchAddMonitor() {
this.$refs.batchMonitorDialog.open();
},
batchSave(params) {
let targets = this._handleBatchVars(params);
targets.forEach(row => {
this.monitorParams.push(row);
});
},
_handleBatchVars(data) {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
let line = item.split(/|,/);
if (line.length < 3) {
return;
}
let ipRe = new RegExp("^[0-9a-zA-Z,\.]*$");
if (!ipRe.test(line[1])) {
this.$message.warning("ip" + this.$t("commons.formatErr"));
return;
}
let numRe = new RegExp("^[0-9]*$");
if (!numRe.test(line[2])) {
this.$message.warning("Port" + this.$t("commons.type_of_num"));
return;
}
keyValues.push({
name: line[0],
ip: line[1],
port: line[2],
description: line[3] || '',
});
});
return keyValues;
},
modifyMonitor(row, index) {
this.$refs.monitorDialog.open(row, index);
},
delMonitor(row, index) {
this.monitorParams.splice(index, 1);
},
refreshStatus() {
},
}
};
</script>
<style scoped>
.el-row {
margin-bottom: 10px;
}
.edit-input {
padding-right: 0px;
}
.tb-edit .el-textarea {
display: none;
}
.tb-edit .current-row .el-textarea {
display: block;
}
.tb-edit .current-row .el-textarea + span {
display: none;
}
.el-col {
text-align: left;
}
.el-col .el-table {
align: center;
}
.pointer {
cursor: pointer;
}
.duration-input .el-input-number--mini {
width: 100px;
}
.el-select--mini {
width: 130px;
}
</style>

View File

@ -1,388 +0,0 @@
<template>
<div v-loading="result.loading">
<el-row type="flex" justify="space-between" align="middle">
<h4>{{ $t('load_test.scenario_list') }}</h4>
</el-row>
<el-row type="flex" justify="start" align="middle">
<ms-table-button icon="el-icon-circle-plus-outline"
:disabled="isReadOnly"
:content="$t('load_test.load_exist_jmx')" @click="loadJMX()"/>
<ms-table-button icon="el-icon-share"
:disabled="isReadOnly"
@click="loadApiAutomation()"
:content="$t('load_test.load_api_automation_jmx')"/>
</el-row>
<el-table class="basic-config" :data="threadGroups.filter(tg=>tg.deleted=='false')">
<el-table-column
:label="$t('load_test.scenario_name')">
<template v-slot:default="{row}">
{{ row.attributes.testname }}
</template>
</el-table-column>
<el-table-column
label="Enable/Disable">
<template v-slot:default="{row}">
<el-switch v-model="row.enabled"
inactive-color="#DCDFE6"
active-value="true"
inactive-value="false"
:disabled="isReadOnly || threadGroupDisable(row)"
/>
</template>
</el-table-column>
<el-table-column
label="CSVDataSet">
<template v-slot:default="scope">
<ms-tag v-for="(f, index) in scope.row.csvFiles"
:key="index"
effect="light"
:content="f"/>
</template>
</el-table-column>
<el-table-column
:label="$t('load_test.thread_group')">
<template v-slot:default="{row}">
<span v-if="row.tgType === 'PostThreadGroup' || row.tgType === 'SetupThreadGroup'">
{{ row.tgType }}
</span>
<el-select v-else v-model="row.tgType"
:disabled="isReadOnly"
:placeholder="$t('commons.please_select')" size="small"
@change="tgTypeChange(row)">
<el-option v-for="tg in threadGroupForSelect" :key="tg.tagName" :label="tg.name"
:value="tg.testclass"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column
:label="$t('commons.operating')">
<template v-slot:default="{row}">
<el-button :disabled="isReadOnly || threadGroupDisable(row)"
@click="handleDeleteThreadGroup(row)"
type="danger"
icon="el-icon-delete" size="mini"
circle/>
</template>
</el-table-column>
</el-table>
<el-row type="flex" justify="space-between" align="middle">
<h4>{{ $t('load_test.other_resource') }}</h4>
</el-row>
<el-row type="flex" justify="start" align="middle">
<ms-table-button icon="el-icon-circle-plus-outline"
:disabled="isReadOnly"
:content="$t('load_test.load_exist_file')" @click="loadFile()"/>
</el-row>
<el-table class="basic-config" :data="tableData">
<el-table-column
prop="name"
:label="$t('load_test.file_name')">
</el-table-column>
<el-table-column
prop="size"
:label="$t('load_test.file_size')">
</el-table-column>
<el-table-column
prop="type"
:label="$t('load_test.file_type')">
</el-table-column>
<el-table-column
:label="$t('load_test.last_modify_time')">
<template v-slot:default="scope">
<i v-if="scope.row.updateTime > 0" class="el-icon-time"/>
<span class="last-modified">{{ scope.row.updateTime | datetimeFormat }}</span>
</template>
</el-table-column>
<el-table-column
:label="$t('commons.operating')">
<template v-slot:default="scope">
<el-button @click="handleDownload(scope.row)" :disabled="!scope.row.id || isReadOnly" type="primary"
icon="el-icon-download"
size="mini" circle/>
<el-button :disabled="isReadOnly" @click="handleDelete(scope.row, scope.$index)" type="danger"
icon="el-icon-delete" size="mini"
circle/>
</template>
</el-table-column>
</el-table>
<exist-files ref="existFiles"
@fileChange="fileChange"
:file-list="fileList"
:table-data="tableData"
:upload-list="uploadList"
:is-read-only="isReadOnly"
:scenarios="threadGroups"/>
<exist-scenarios ref="existScenarios"
@fileChange="fileChange"
:file-list="fileList"
:table-data="tableData"
:upload-list="uploadList"
:scenarios="threadGroups"/>
</div>
</template>
<script>
import {Message} from "element-ui";
import MsTableButton from "metersphere-frontend/src/components/MsTableButton";
import MsTablePagination from "metersphere-frontend/src/components/pagination/TablePagination";
import MsTableOperatorButton from "metersphere-frontend/src/components/MsTableOperatorButton";
import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter";
import ExistFiles from "./ExistFiles";
import ExistScenarios from "./ExistScenarios";
// import {findThreadGroup} from "@/business/api/model/ThreadGroup";
import {hasPermission} from "metersphere-frontend/src/utils/permission";
import MsTag from "metersphere-frontend/src/components/MsTag";
import {downloadFile, getFiles, getMetadataById} from "@/api/performance";
export default {
name: "PerformanceBasicConfig",
components: {
MsTag,
ExistScenarios, ExistFiles, MsDialogFooter, MsTableOperatorButton, MsTablePagination, MsTableButton
},
props: {
test: {
type: Object
},
},
data() {
return {
result: {},
isReadOnly: false,
projectLoadingResult: {},
getFileMetadataPath: "/performance/file/metadata",
getFileMetadataById: "/performance/file/getMetadataById",
jmxDownloadPath: '/performance/file/download',
jmxDeletePath: '/performance/file/delete',
fileList: [],
tableData: [],
uploadList: [],
metadataIdList: [],
fileNumLimit: 10,
threadGroups: [],
loadFileVisible: false,
currentPage: 1,
pageSize: 5,
total: 0,
existFiles: [],
apiScenarios: [],
loadApiAutomationVisible: false,
selectIds: new Set(),
threadGroupForSelect: [
{
name: 'ThreadGroup',
tagName: 'ThreadGroup',
testclass: 'ThreadGroup',
guiclass: 'ThreadGroupGui'
},
{
name: 'ConcurrencyThreadGroup',
tagName: 'com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup',
testclass: 'com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup',
guiclass: "com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroupGui"
},
]
};
},
created() {
if (this.test.id) {
this.getFileMetadata(this.test);
}
this.isReadOnly = !hasPermission('PROJECT_PERFORMANCE_TEST:READ+EDIT');
},
watch: {
test() {
if (this.test.id) {
this.getFileMetadata(this.test);
}
}
},
methods: {
getFileMetadata(test) {
this.fileList = [];
this.tableData = [];
this.uploadList = [];
this.metadataIdList = [];
getFiles(test.id)
.then(response => {
let files = response.data;
if (!files) {
Message.error({message: this.$t('load_test.related_file_not_found'), showClose: true});
return;
}
// deep copy
this.fileList = JSON.parse(JSON.stringify(files));
this.tableData = JSON.parse(JSON.stringify(files));
this.tableData.map(f => {
f.size = (f.size / 1024).toFixed(2) + ' KB';
});
});
},
selectAttachFileById(metadataIdArr) {
this.metadataIdList = metadataIdArr;
for (let i = 0; i < metadataIdArr.length; i++) {
let id = metadataIdArr[i];
getMetadataById(id)
.then(response => {
let files = response.data;
if (files) {
this.fileList.push(JSON.parse(JSON.stringify(files)));
this.tableData.push(JSON.parse(JSON.stringify(files)));
this.tableData.map(f => {
f.size = (f.size / 1024).toFixed(2) + ' KB';
});
}
});
}
},
handleDownload(file) {
downloadFile(this.jmxDownloadPath, file)
.then(response => {
const content = response.data;
const blob = new Blob([content]);
if ("download" in document.createElement("a")) {
// IE
// chrome/firefox
let aTag = document.createElement('a');
aTag.download = file.name;
aTag.href = URL.createObjectURL(blob);
aTag.click();
URL.revokeObjectURL(aTag.href);
} else {
// IE10+
navigator.msSaveBlob(blob, this.filename);
}
}).catch(e => {
Message.error({message: e.message, showClose: true});
});
},
handleDelete(file) {
this.$alert(this.$t('load_test.delete_file_confirm') + file.name + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this._handleDelete(file);
}
}
});
},
_handleDelete(file) {
let index = this.fileList.findIndex(f => f.name === file.name);
if (index > -1) {
this.fileList.splice(index, 1);
}
index = this.tableData.findIndex(f => f.name === file.name);
if (index > -1) {
this.tableData.splice(index, 1);
}
//
let i = this.uploadList.findIndex(upLoadFile => upLoadFile.name === file.name);
if (i > -1) {
this.uploadList.splice(i, 1);
}
let jmxIndex = this.threadGroups.findIndex(tg => tg.handler === file.name);
while (jmxIndex !== -1) {
this.threadGroups.splice(jmxIndex, 1);
jmxIndex = this.threadGroups.findIndex(tg => tg.handler === file.name);
}
},
handleDeleteThreadGroup(tg) {
this.$alert(this.$t('load_test.delete_threadgroup_confirm') + tg.attributes.testname + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
tg.deleted = 'true';
}
}
});
},
threadGroupDisable(row) {
return this.threadGroups.filter(tg => tg.enabled == 'true').length === 1 && row.enabled == 'true';
},
tgTypeChange(row) {
this.$emit("tgTypeChange", row);
},
updatedFileList() {
return this.fileList;//
},
conversionMetadataIdList() {
return this.metadataIdList;//
},
fileSorts() {
let fileSorts = {};
this.tableData.forEach((f, index) => {
fileSorts[f.name] = index;
});
return fileSorts;
},
loadJMX() {
this.$refs.existFiles.open('jmx');
},
loadFile() {
this.$refs.existFiles.open('resource');
},
loadApiAutomation() {
this.$refs.existScenarios.open();
},
fileChange(threadGroups) {
this.$emit('fileChange', threadGroups);
},
validConfig() {
if (this.uploadList.length + this.fileList.length > this.fileNumLimit) {
this.$refs.existFiles.handleExceed();
return false;
}
if (this.threadGroups.filter(tg => tg.enabled == 'true').length === 0) {
this.$error(this.$t('load_test.threadgroup_at_least_one'));
return false;
}
return true;
},
importScenario(scenarioId) {
this.$refs.existScenarios.selectIds.add(scenarioId);
},
importCase(caseObj) {
let suffixIndex = caseObj.name.lastIndexOf(".jmx");
let jmxName = caseObj.name.substring(0, suffixIndex) + "_" + new Date().getTime() + ".jmx";
let threadGroups = findThreadGroup(caseObj.xml, jmxName);
threadGroups.forEach(tg => {
tg.options = {};
});
this.fileChange(threadGroups);
let file = new File([caseObj.xml], jmxName);
this.uploadList.push(file);
this.tableData.push({
name: file.name,
size: (file.size / 1024).toFixed(2) + ' KB',
type: 'JMX',
updateTime: file.lastModified,
});
},
handleUpload() {
// api
this.$refs.existScenarios.handleImport();
},
},
};
</script>
<style scoped>
.basic-config {
width: 100%;
}
.last-modified {
margin-left: 5px;
}
.el-dialog :deep( .el-dialog__body ) {
padding: 10px 20px;
}
</style>

View File

@ -246,8 +246,8 @@
<script>
import MsChart from "metersphere-frontend/src/components/chart/MsChart";
// import {findThreadGroup} from "@/business/api/model/ThreadGroup";
import {getJmxContent, getLoadConfig, getResourcePools} from "@/api/performance";
import {findThreadGroup} from "./ThreadGroup";
// import {getJmxContent, getLoadConfig, getResourcePools} from "@/api/load-test";
const HANDLER = "handler";
const THREAD_GROUP_TYPE = "tgType";
@ -390,135 +390,135 @@ export default {
},
methods: {
getResourcePools() {
getResourcePools(this.isShare)
.then(response => {
this.resourcePools = response.data;
// null
if (response.data.filter(p => p.id === this.resourcePool).length === 0) {
this.resourcePool = null;
// IDnull
this.setPoolNull = true;
}
this.resourcePoolChange();
});
// getResourcePools(this.isShare)
// .then(response => {
// this.resourcePools = response.data;
// // null
// if (response.data.filter(p => p.id === this.resourcePool && p.performance).length === 0) {
// this.resourcePool = null;
// // IDnull
// this.setPoolNull = true;
// }
//
// this.resourcePoolChange();
// });
},
getLoadConfig() {
getLoadConfig(this.testId, this.reportId, this.isShare)
.then(response => {
let data = JSON.parse(response.data);
for (let i = 0; i < this.threadGroups.length; i++) {
data[i].forEach(item => {
switch (item.key) {
case TARGET_LEVEL:
this.threadGroups[i].threadNumber = item.value;
break;
case RAMP_UP:
this.threadGroups[i].rampUpTime = item.value;
break;
case ITERATE_RAMP_UP:
this.threadGroups[i].iterateRampUp = item.value;
break;
case DURATION:
this.threadGroups[i].duration = item.value;
break;
case DURATION_HOURS:
this.threadGroups[i].durationHours = item.value;
break;
case DURATION_MINUTES:
this.threadGroups[i].durationMinutes = item.value;
break;
case DURATION_SECONDS:
this.threadGroups[i].durationSeconds = item.value;
break;
case UNIT:
this.threadGroups[i].unit = item.value;
break;
case STEPS:
this.threadGroups[i].step = item.value;
break;
case RPS_LIMIT:
this.threadGroups[i].rpsLimit = item.value;
break;
case RPS_LIMIT_ENABLE:
this.threadGroups[i].rpsLimitEnable = item.value;
break;
case THREAD_TYPE:
this.threadGroups[i].threadType = item.value;
break;
case ITERATE_NUM:
this.threadGroups[i].iterateNum = item.value;
break;
case ENABLED:
this.threadGroups[i].enabled = item.value;
break;
case DELETED:
this.threadGroups[i].deleted = item.value;
break;
case HANDLER:
this.threadGroups[i].handler = item.value;
break;
case THREAD_GROUP_TYPE:
this.threadGroups[i].tgType = item.value;
break;
case ON_SAMPLE_ERROR:
this.threadGroups[i].onSampleError = item.value;
break;
case STRATEGY:
this.threadGroups[i].strategy = item.value;
break;
case RESOURCE_NODE_INDEX:
this.threadGroups[i].resourceNodeIndex = item.value;
break;
case RATIOS:
this.threadGroups[i].ratios = item.value;
break;
case SERIALIZE_THREAD_GROUPS:
this.serializeThreadGroups = item.value;// 线
break;
case AUTO_STOP:
this.autoStop = item.value;// 线
break;
case AUTO_STOP_DELAY:
this.autoStopDelay = item.value;// 线
break;
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);
this.$set(this.threadGroups[i], "enabled", this.threadGroups[i].enabled || 'true');
this.$set(this.threadGroups[i], "deleted", this.threadGroups[i].deleted || 'false');
this.$set(this.threadGroups[i], "onSampleError", this.threadGroups[i].onSampleError || 'continue');
});
}
for (let i = 0; i < this.threadGroups.length; i++) {
let tg = this.threadGroups[i];
tg.durationHours = Math.floor(tg.duration / 3600);
tg.durationMinutes = Math.floor((tg.duration / 60 % 60));
tg.durationSeconds = Math.floor((tg.duration % 60));
}
this.resourcePoolChange();
this.calculateTotalChart();
});
// getLoadConfig(this.testId, this.reportId, this.isShare)
// .then(response => {
// let data = JSON.parse(response.data);
// for (let i = 0; i < this.threadGroups.length; i++) {
// data[i].forEach(item => {
// switch (item.key) {
// case TARGET_LEVEL:
// this.threadGroups[i].threadNumber = item.value;
// break;
// case RAMP_UP:
// this.threadGroups[i].rampUpTime = item.value;
// break;
// case ITERATE_RAMP_UP:
// this.threadGroups[i].iterateRampUp = item.value;
// break;
// case DURATION:
// this.threadGroups[i].duration = item.value;
// break;
// case DURATION_HOURS:
// this.threadGroups[i].durationHours = item.value;
// break;
// case DURATION_MINUTES:
// this.threadGroups[i].durationMinutes = item.value;
// break;
// case DURATION_SECONDS:
// this.threadGroups[i].durationSeconds = item.value;
// break;
// case UNIT:
// this.threadGroups[i].unit = item.value;
// break;
// case STEPS:
// this.threadGroups[i].step = item.value;
// break;
// case RPS_LIMIT:
// this.threadGroups[i].rpsLimit = item.value;
// break;
// case RPS_LIMIT_ENABLE:
// this.threadGroups[i].rpsLimitEnable = item.value;
// break;
// case THREAD_TYPE:
// this.threadGroups[i].threadType = item.value;
// break;
// case ITERATE_NUM:
// this.threadGroups[i].iterateNum = item.value;
// break;
// case ENABLED:
// this.threadGroups[i].enabled = item.value;
// break;
// case DELETED:
// this.threadGroups[i].deleted = item.value;
// break;
// case HANDLER:
// this.threadGroups[i].handler = item.value;
// break;
// case THREAD_GROUP_TYPE:
// this.threadGroups[i].tgType = item.value;
// break;
// case ON_SAMPLE_ERROR:
// this.threadGroups[i].onSampleError = item.value;
// break;
// case STRATEGY:
// this.threadGroups[i].strategy = item.value;
// break;
// case RESOURCE_NODE_INDEX:
// this.threadGroups[i].resourceNodeIndex = item.value;
// break;
// case RATIOS:
// this.threadGroups[i].ratios = item.value;
// break;
// case SERIALIZE_THREAD_GROUPS:
// this.serializeThreadGroups = item.value;// 线
// break;
// case AUTO_STOP:
// this.autoStop = item.value;// 线
// break;
// case AUTO_STOP_DELAY:
// this.autoStopDelay = item.value;// 线
// break;
// 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);
// this.$set(this.threadGroups[i], "enabled", this.threadGroups[i].enabled || 'true');
// this.$set(this.threadGroups[i], "deleted", this.threadGroups[i].deleted || 'false');
// this.$set(this.threadGroups[i], "onSampleError", this.threadGroups[i].onSampleError || 'continue');
// });
// }
// for (let i = 0; i < this.threadGroups.length; i++) {
// let tg = this.threadGroups[i];
// tg.durationHours = Math.floor(tg.duration / 3600);
// tg.durationMinutes = Math.floor((tg.duration / 60 % 60));
// tg.durationSeconds = Math.floor((tg.duration % 60));
// }
// this.resourcePoolChange();
// this.calculateTotalChart();
// });
},
getJmxContent() {
let threadGroups = [];
getJmxContent(this.testId, this.reportId, this.isShare)
.then(response => {
response.data.forEach(d => {
threadGroups = threadGroups.concat(findThreadGroup(d.jmx, d.name));
threadGroups.forEach(tg => {
tg.options = {};
});
});
this.threadGroups = threadGroups;
this.$emit('fileChange', threadGroups);
this.getLoadConfig();
});
// getJmxContent(this.testId, this.reportId, this.isShare)
// .then(response => {
// response.data.forEach(d => {
// threadGroups = threadGroups.concat(findThreadGroup(d.jmx, d.name));
// threadGroups.forEach(tg => {
// tg.options = {};
// });
// });
// this.threadGroups = threadGroups;
// this.$emit('fileChange', threadGroups);
// this.getLoadConfig();
// });
},
resourcePoolChange() {
let result = this.resourcePools.filter(p => p.id === this.resourcePool);

View File

@ -37,8 +37,7 @@
</template>
<script>
//
// import LoadCaseReportView from "../../../../../../../../../../performance-test/frontend/src/template/report/performance/share/LoadCaseReportView";
import LoadCaseReportView from "../load/LoadCaseReportView";
import TypeTableItem from "../../../../../../common/tableItems/planview/TypeTableItem";
import MethodTableItem from "../../../../../../common/tableItems/planview/MethodTableItem";
@ -56,7 +55,7 @@ export default {
MsMainContainer,
MsAsideContainer,
LoadFailureResult, StatusTableItem, MethodTableItem, TypeTableItem,
// LoadCaseReportView,
LoadCaseReportView,
MicroApp
},
props: {

View File

@ -0,0 +1,226 @@
<template>
<div>
<span class="table-title">Errors</span>
<el-table
:data="tableData"
border
stripe
style="width: 100%"
:default-sort="{prop: 'elementLabel'}"
>
<el-table-column
prop="errorType"
label="Type of error"
sortable>
</el-table-column>
<el-table-column
width="200"
prop="errorNumber"
label="Number of errors"
sortable>
</el-table-column>
<el-table-column
width="200"
prop="percentOfErrors"
label="% in errors"
sortable>
</el-table-column>
<el-table-column
width="200"
prop="percentOfAllSamples"
label="% in all samples"
sortable>
</el-table-column>
</el-table>
<span class="table-title">Top 5 Errors</span>
<el-table
:data="errorSummary"
border
stripe
style="width: 100%"
show-summary
>
<el-table-column prop="sample" label="Sample"/>
<el-table-column prop="samples" label="#Samples"/>
<el-table-column prop="errorsAllSize" label="All Errors"/>
</el-table>
<span class="table-title">#1 Error</span>
<el-table
:data="errorTop1"
border
stripe
style="width: 100%"
>
<el-table-column prop="sample" label="Sample"/>
<el-table-column prop="error1" label="#1 Error"/>
<el-table-column prop="error1Size" label="#1 Errors Count" width="200"/>
</el-table>
<span class="table-title">#2 Error</span>
<el-table
:data="errorTop2"
border
stripe
style="width: 100%"
>
<el-table-column prop="sample" label="Sample"/>
<el-table-column prop="error2" label="#2 Error"/>
<el-table-column prop="error2Size" label="#2 Errors Count" width="200"/>
</el-table>
<span class="table-title">#3 Error</span>
<el-table
:data="errorTop3"
border
stripe
style="width: 100%"
>
<el-table-column prop="sample" label="Sample"/>
<el-table-column prop="error3" label="#3 Error"/>
<el-table-column prop="error3Size" label="#3 Errors Count" width="200"/>
</el-table>
<span class="table-title">#4 Error</span>
<el-table
:data="errorTop4"
border
stripe
style="width: 100%"
>
<el-table-column prop="sample" label="Sample"/>
<el-table-column prop="error4" label="#4 Error"/>
<el-table-column prop="error4Size" label="#4 Errors Count" width="200"/>
</el-table>
<span class="table-title">#5 Error</span>
<el-table
:data="errorTop5"
border
stripe
style="width: 100%"
>
<el-table-column prop="sample" label="Sample"/>
<el-table-column prop="error5" label="#5 Error"/>
<el-table-column prop="error5Size" label="#5 Errors Count" width="200"/>
</el-table>
</div>
</template>
<script>
export default {
name: "ErrorLog",
data() {
return {
tableData: [],
errorSummary: [],
errorTop1: [],
errorTop2: [],
errorTop3: [],
errorTop4: [],
errorTop5: [],
id: ''
};
},
props: ['report', 'isShare', 'shareId', 'planReportTemplate'],
methods: {
initTableData() {
if (this.planReportTemplate) {
this.tableData = this.planReportTemplate.reportErrors;
this.handleGetTop5(this.planReportTemplate.reportErrorsTop5);
}
},
handleGetTop5(data) {
if (!data) {
return;
}
this.errorTop1 = data
.map(e => {
return {sample: e.sample, error1: e.error1, error1Size: e.error1Size};
})
.filter(e => e.error1Size > 0);
this.errorTop2 = data
.map(e => {
return {sample: e.sample, error2: e.error2, error2Size: e.error2Size};
})
.filter(e => e.error2Size > 0);
this.errorTop3 = data
.map(e => {
return {sample: e.sample, error3: e.error3, error3Size: e.error3Size};
})
.filter(e => e.error3Size > 0);
this.errorTop4 = data
.map(e => {
return {sample: e.sample, error4: e.error4, error4Size: e.error4Size};
})
.filter(e => e.error4Size > 0);
this.errorTop5 = data
.map(e => {
return {sample: e.sample, error5: e.error5, error5Size: e.error5Size};
})
.filter(e => e.error5Size > 0);
this.errorSummary = data.map(e => {
return {sample: e.sample, samples: e.samples, errorsAllSize: e.errorsAllSize};
});
},
initData() {
this.tableData = [];
this.errorTop1 = [];
this.errorTop2 = [];
this.errorTop3 = [];
this.errorTop4 = [];
this.errorTop5 = [];
this.errorSummary = [];
}
},
watch: {
report: {
handler(val) {
if (!val.status || !val.id) {
return;
}
let status = val.status;
this.id = val.id;
if (status === "Completed" || status === "Running") {
this.initTableData();
} else {
this.tableData = [];
this.errorTop1 = [];
this.errorTop2 = [];
this.errorTop3 = [];
this.errorTop4 = [];
this.errorTop5 = [];
this.errorSummary = [];
}
},
deep: true
},
planReportTemplate: {
handler() {
if (this.planReportTemplate) {
this.initTableData();
}
},
deep: true
}
},
};
</script>
<style scoped>
.table-title {
font-size: 20px;
color: #8492a6;
display: block;
text-align: center;
margin-bottom: 8px;
margin-top: 40px;
}
</style>

View File

@ -0,0 +1,300 @@
<template>
<ms-container>
<el-main>
<el-card v-loading="loading" v-if="show">
<el-row>
<el-col :span="16">
</el-col>
<el-col :span="8">
<div v-if="isPlanReport" style="float: right;margin-right: 10px;">
<div v-if="showProjectEnv" type="flex">
<span> {{ $t('commons.environment') + ':' }} </span>
<div v-for="(values,key) in projectEnvMap" :key="key" style="margin-right: 10px">
{{ key + ":" }}
<ms-tag v-for="(item,index) in values" :key="index" type="success" :content="item"
style="margin-left: 2px"/>
</div>
<div v-show="showMoreProjectEnvMap">
<el-link icon="el-icon-more" @click="showAllProjectInfo"></el-link>
</div>
</div>
</div>
<div style="float: left">
<span class="ms-report-time-desc">
{{
$t('report.test_duration', [templateMinutes ? templateMinutes : minutes,
templateSeconds ? templateSeconds : seconds])
}}
</span>
<span class="ms-report-time-desc" v-if="startTime !== '0'">
{{ $t('report.test_start_time') }}{{ startTime | datetimeFormat }}
</span>
<span class="ms-report-time-desc" v-else-if="planReportTemplate && planReportTemplate.startTime">
{{ $t('report.test_start_time') }}{{ planReportTemplate.startTime | datetimeFormat }}
</span>
<span class="ms-report-time-desc" v-else>
{{ $t('report.test_start_time') }}-
</span>
<span class="ms-report-time-desc" v-if="report.status === 'Completed' && endTime !== '0'">
{{ $t('report.test_end_time') }}{{ endTime | datetimeFormat }}
</span>
<span class="ms-report-time-desc" v-else-if="planReportTemplate && planReportTemplate.endTime">
{{ $t('report.test_end_time') }}{{ planReportTemplate.endTime | datetimeFormat }}
</span>
<span class="ms-report-time-desc" v-else>
{{ $t('report.test_end_time') }}-
</span>
</div>
</el-col>
</el-row>
<el-divider/>
<div ref="resume">
<el-tabs v-model="active">
<el-tab-pane :label="$t('report.test_overview')">
<ms-report-test-overview :report="report" :is-share="isShare" :plan-report-template="planReportTemplate"
:share-id="shareId" ref="testOverview"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_details')">
<ms-report-test-details :report="report" :is-share="isShare" :plan-report-template="planReportTemplate"
:share-id="shareId" ref="testDetails"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_request_statistics')">
<ms-report-request-statistics :report="report" :is-share="isShare"
:plan-report-template="planReportTemplate"
:share-id="shareId" ref="requestStatistics"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_error_log')">
<ms-report-error-log :report="report" :is-share="isShare" :plan-report-template="planReportTemplate"
:share-id="shareId" ref="errorLog"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_log_details')">
<ms-report-log-details :report="report" :is-share="isShare" :plan-report-template="planReportTemplate"
:share-id="shareId"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_monitor_details')">
<monitor-card :report="report" :is-share="isShare" :plan-report-template="planReportTemplate"
:share-id="shareId"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_config')">
<ms-test-configuration :report-id="reportId" :report="report"
:plan-report-template="planReportTemplate"
:is-share="isShare" :share-id="shareId"/>
</el-tab-pane>
</el-tabs>
</div>
</el-card>
<project-environment-dialog ref="projectEnvDialog"></project-environment-dialog>
</el-main>
</ms-container>
</template>
<script>
import MsReportErrorLog from "./ErrorLog";
import MsReportLogDetails from "./LogDetails";
import MsReportRequestStatistics from "./RequestStatistics";
import MsReportTestOverview from "./TestOverview";
import MsContainer from "metersphere-frontend/src/components/MsContainer";
import MsMainContainer from "metersphere-frontend/src/components/MsMainContainer";
import MonitorCard from "./MonitorCard";
import MsReportTestDetails from './TestDetails';
import ProjectEnvironmentDialog from "./ProjectEnvironmentDialog";
import MsTag from "metersphere-frontend/src/components/MsTag";
import MsTestConfiguration from "./TestConfiguration";
export default {
name: "LoadCaseReportView",
components: {
MsTestConfiguration,
MonitorCard,
MsReportErrorLog,
MsReportLogDetails,
MsReportRequestStatistics,
MsReportTestOverview,
MsReportTestDetails,
MsContainer,
MsMainContainer,
ProjectEnvironmentDialog,
MsTag,
},
data() {
return {
loading: false,
active: '0',
status: '',
reportName: '',
testId: '',
testName: '',
projectId: '',
projectName: '',
startTime: '0',
endTime: '0',
minutes: '0',
seconds: '0',
title: 'Logging',
projectEnvMap: null,
showMoreProjectEnvMap: false,
allProjectEnvMap: null,
report: {},
websocket: null,
dialogFormVisible: false,
testPlan: {testResourcePoolId: null},
show: true,
test: {testResourcePoolId: null},
};
},
props: {
reportId: String,
isReadOnly: {
type: Boolean,
default: false
},
isPlanReport: Boolean,
isShare: Boolean,
shareId: String,
planReportTemplate: Object
},
watch: {
reportId() {
this.init();
}
},
computed: {
showProjectEnv() {
return this.projectEnvMap && JSON.stringify(this.projectEnvMap) !== '{}';
},
templateMinutes() {
if (this.planReportTemplate && this.planReportTemplate.duration) {
let duration = this.planReportTemplate.duration;
return Math.floor(duration / 60);
}
return null;
},
templateSeconds() {
if (this.planReportTemplate && this.planReportTemplate.duration) {
let duration = this.planReportTemplate.duration;
return duration % 60;
}
return null;
}
},
methods: {
showAllProjectInfo() {
this.$refs.projectEnvDialog.open(this.allProjectEnvMap);
},
isProjectEnvShowMore(projectEnvMap) {
this.showMoreProjectEnvMap = false;
this.projectEnvMap = {};
if (projectEnvMap) {
let keySize = 0;
for (let key in projectEnvMap) {
keySize++;
if (keySize > 1) {
this.showMoreProjectEnvMap = true;
return;
} else {
this.projectEnvMap = {};
this.$set(this.projectEnvMap, key, projectEnvMap[key]);
}
}
}
},
initBreadcrumb(callback) {
if (this.isPlanReport) {
return;
}
},
initReportTimeInfo() {
if (this.status === 'Starting') {
this.clearData();
return;
}
if (this.planReportTemplate) {
this.handleInitReportTimeInfo(this.planReportTemplate);
}
},
handleInitReportTimeInfo(data) {
if (data) {
this.startTime = data.startTime;
this.endTime = data.endTime;
let duration = data.duration;
this.minutes = Math.floor(duration / 60);
this.seconds = duration % 60;
}
},
checkReportStatus(status) {
switch (status) {
case 'Error':
// this.$warning(this.$t('report.generation_error'));
this.active = '4';
break;
case 'Starting':
this.$alert(this.$t('report.start_status'));
break;
case 'Reporting':
case 'Running':
case 'Completed':
default:
break;
}
},
clearData() {
this.show = false;
this.startTime = '0';
this.endTime = '0';
this.minutes = '0';
this.seconds = '0';
this.$nextTick(() => {
this.show = true;
});
},
init() {
this.clearData();
if (this.planReportTemplate) {
this.handleInit(this.planReportTemplate);
}
},
handleInit(data) {
if (data) {
this.allProjectEnvMap = data.projectEnvMap;
this.isProjectEnvShowMore(data.projectEnvMap);
this.status = data.status;
this.$set(this, "report", data);
this.$set(this.test, "testResourcePoolId", data.testResourcePoolId);
this.checkReportStatus(data.status);
if (this.status === "Completed" || this.status === "Running") {
this.initReportTimeInfo();
}
this.initBreadcrumb();
} else {
this.$error(this.$t('report.not_exist'));
}
}
},
};
</script>
<style scoped>
.ms-report-view-btns {
margin-top: 15px;
}
.ms-report-time-desc {
text-align: left;
display: block;
color: #5C7878;
}
.ms-report-time-desc-share {
text-align: left;
color: #5C7878;
padding-right: 20px;
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<el-card>
<template v-slot:header>
<span class="title">Load</span>
</template>
<div v-for="(option, index) in loadList" :key="index">
<ms-chart ref="chart1" :options="option" :autoresize="true"></ms-chart>
</div>
</el-card>
</template>
<script>
import MsChart from "metersphere-frontend/src/components/chart/MsChart";
import {getPerformanceReportLoadChart} from "@/api/load-test";
export default {
name: "LoadCompareCard",
components: {MsChart},
data() {
return {
loadList: []
}
},
methods: {
initCard() {
this.loadList = [];
this.reportId = this.$route.path.split('/')[4];
this.compareReports = JSON.parse(sessionStorage.getItem("compareReports"));
this.compareReports.forEach(report => {
this.initOverview(report);
})
},
initOverview(report) {
getPerformanceReportLoadChart(report.id)
.then(({data}) => {
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
let yAxis2List = data.filter(m => m.yAxis === -1).map(m => m.yAxis2);
let yAxisListMax = this._getChartMax(yAxisList);
let yAxis2ListMax = this._getChartMax(yAxis2List);
let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName);
yAxisIndex0List = this._unique(yAxisIndex0List);
let yAxisIndex1List = data.filter(m => m.yAxis === -1).map(m => m.groupName);
yAxisIndex1List = this._unique(yAxisIndex1List);
let loadOption = {
title: {
text: report.name + " " + this.$options.filters['datetimeFormat'](report.createTime),
left: 'center',
top: 20,
textStyle: {
color: '#65A2FF'
},
},
tooltip: {
show: true,
trigger: 'axis',
// extraCssText: 'z-index: 999;',
confine: true,
},
legend: {},
xAxis: {},
yAxis: [{
name: 'User',
type: 'value',
min: 0,
max: yAxisListMax,
splitNumber: 5,
interval: yAxisListMax / 5
},
{
name: 'Transactions/s',
type: 'value',
splitNumber: 5,
min: 0,
max: yAxis2ListMax,
interval: yAxis2ListMax / 5
}
],
series: []
};
let setting = {
series: [
{
name: 'users',
color: '#0CA74A',
},
{
name: 'hits',
yAxisIndex: '1',
color: '#65A2FF',
},
{
name: 'errors',
yAxisIndex: '1',
color: '#E6113C',
}
]
}
yAxisIndex0List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'})
})
yAxisIndex1List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'})
})
this.loadList.push(this.generateOption(loadOption, data, setting));
})
.catch(() => {
this.loadList = [];
})
},
generateOption(option, data, setting) {
let chartData = data;
let seriesArray = [];
for (let set in setting) {
if (set === "series") {
seriesArray = setting[set];
continue;
}
this.$set(option, set, setting[set]);
}
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] = []
}
if (item.yAxis === -1) {
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis2.toFixed(2)]);
} else {
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
};
let seriesArrayNames = seriesArray.map(m => m.name);
if (seriesArrayNames.includes(name)) {
for (let j = 0; j < seriesArray.length; j++) {
let seriesObj = seriesArray[j];
if (seriesObj['name'] === name) {
Object.assign(items, seriesObj);
}
}
}
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));
}
}
}
</script>
<style scoped>
.echarts {
width: 100%;
height: 300px;
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<div>
<el-row :gutter="10">
<el-col :span="4">
<el-select v-model="currentInstance" placeholder="" size="small" style="width: 100%"
@change="changeInstance(currentInstance)">
<el-option
v-for="item in resource"
:key="item.resourceId"
:label="item.resourceName"
:value="item.resourceId">
</el-option>
</el-select>
</el-col>
<el-col :span="20">
<div class="logging-content" v-loading="loading">
<ul class="infinite-list">
<li class="infinite-list-item" v-for="(log, index) in logContent"
:key="currentInstance+index">
{{ log.content }}
</li>
</ul>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: "LogDetails",
data() {
return {
resource: [],
logContent: [],
result: {},
id: '',
page: 1,
pageCount: 5,
loading: false,
currentInstance: ''
};
},
props: ['report', 'export', 'isShare', 'shareId', 'planReportTemplate'],
methods: {
getResource() {
if (this.planReportTemplate) {
this.handleGetLogResource(this.planReportTemplate.reportLogResource);
}
},
handleGetLogResource(data) {
this.resource = data;
if (!this.currentInstance) {
this.currentInstance = this.resource[0]?.resourceId;
}
//
if (this.currentInstance) {
this.changeInstance(this.currentInstance);
}
},
load(resourceId) {
if (this.loading || this.page > this.pageCount) {
return;
}
this.loading = true;
if (this.planReportTemplate) {
let {reportLogResource} = this.planReportTemplate;
if (reportLogResource && reportLogResource.length > 0) {
let {reportLogs} = reportLogResource[0];
if (reportLogs) {
this.handleGetPlanTemplateLog(reportLogs);
}
}
}
},
handleGetPlanTemplateLog(data) {
data.forEach(log => {
if (this.logContent) {
this.logContent.push(log);
}
});
this.loading = false;
},
changeInstance(instance) {
this.currentInstance = instance;
this.loading = false;
this.page = 1;
this.logContent = [];
this.load(instance);
},
},
watch: {
'$route'(to) {
if (to.name === "perReportView") {
this.id = to.path.split('/')[4];
this.getResource();
}
},
report: {
handler(val) {
if (!val.status || !val.id) {
return;
}
let status = val.status;
this.id = val.id;
if (status === "Completed" || status === "Running") {
this.getResource();
}
},
deep: true
},
planReportTemplate: {
handler() {
if (this.planReportTemplate) {
this.getResource();
}
},
deep: true
}
},
};
</script>
<style scoped>
.logging-content {
white-space: pre-line;
overflow: auto;
}
.infinite-list {
height: calc(100vh - 205px);
padding: 0;
margin: 0;
list-style: none;
overflow: auto
}
.infinite-list-item {
overflow: hidden;
}
</style>

View File

@ -0,0 +1,348 @@
<template>
<div v-loading="result.loading">
<el-row>
<el-col :span="4">
<div>
<el-select v-model="currentInstance" placeholder="" size="small" style="width: 100%"
@change="getResource(currentInstance)">
<el-option
v-for="item in instances"
:key="item.ip+item.port"
:value="item.ip+':'+item.port">
{{ item.ip }} {{ item.name }}
</el-option>
</el-select>
</div>
<div style="padding-top: 10px">
<el-checkbox-group v-model="checkList"
@change="handleCheckListChange(currentInstance)">
<div v-for="op in checkOptions"
:key="op.key"
:content="op.label">
<el-checkbox :label="op.label"/>
</div>
</el-checkbox-group>
</div>
</el-col>
<el-col :span="20">
<el-row>
<el-col :span="24">
<ms-chart v-if="showChart" ref="chart2" class="chart-config" @datazoom="changeDataZoom"
:options="totalOption"
:autoresize="true"></ms-chart>
</el-col>
</el-row>
<el-row>
<el-col :offset="2" :span="20">
<el-table
:data="tableData"
stripe
border
style="width: 100%">
<el-table-column label="Label" align="center">
<el-table-column
prop="label"
label="Label"
sortable>
</el-table-column>
</el-table-column>
<el-table-column label="Aggregate" align="center">
<el-table-column
prop="avg"
label="Avg."
width="100"
sortable
/>
<el-table-column
prop="min"
label="Min."
width="100"
sortable
/>
<el-table-column
prop="max"
label="Max."
width="100"
sortable
/>
</el-table-column>
<el-table-column label="Range" align="center">
<el-table-column
prop="startTime"
label="Start"
width="160"
/>
<el-table-column
prop="endTime"
label="End"
width="160"
/>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</template>
<script>
import MsChart from "metersphere-frontend/src/components/chart/MsChart";
const color = ['#60acfc', '#32d3eb', '#5bc49f', '#feb64d', '#ff7c7c', '#9287e7', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'];
const checkList = ['CPU', 'Memory', 'Disk', 'Network In', 'Network Out'];
const checkOptions = [
{key: 'cpu', label: 'CPU'},
{key: 'memory', label: 'Memory'},
{key: 'disk', label: 'Disk'},
{key: 'netIn', label: 'Network In'},
{key: 'netOut', label: 'Network Out'}
];
export default {
name: "MonitorCard",
props: ['report', 'export', 'isShare', 'shareId', 'planReportTemplate'],
components: {MsChart},
data() {
return {
activeNames: '0',
result: {},
id: '',
init: false,
loading: false,
currentInstance: '',
instances: [],
data: [],
tableData: [],
checkList: checkList,
checkOptions: checkOptions,
showChart: true,
baseOption: {
color: color,
grid: {
// right: '35%' //
},
title: {},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
},
legend: {
y: 'top'
},
xAxis: {type: 'category'},
yAxis: [{
name: 'Usage(%)',
type: 'value',
min: 0,
max: 100,
}, {
type: 'value',
name: 'kb/s',
min: 0,
}],
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
start: 0,
end: 20
}
],
series: []
},
totalOption: {},
seriesData: [],
};
},
created() {
this.data = [];
this.instances = [];
},
methods: {
getResource(currentInstance) {
// this.init = true;
if (this.planReportTemplate) {
this.instances = this.planReportTemplate.reportResource;
this.currentInstance = currentInstance || this.instances[0].ip + ":" + this.instances[0].port;
this.data = this.planReportTemplate.metricData;
this.totalOption = this.getOption(this.currentInstance);
}
},
handleChecked(id) {
let curr = this.instances.filter(instance => id === instance.ip + ":" + instance.port)[0];
if (curr && curr.monitorConfig) {
this.checkList = [];
this.checkOptions = curr.monitorConfig.filter(mc => mc.value && mc.name)
.map(mc => {
this.checkList.push(mc.name);
return {key: mc.name, label: mc.name,};
});
if (this.checkList.length === 0) {
this.checkList = checkList;
this.checkOptions = checkOptions;
}
} else {
this.checkOptions = checkOptions;
this.checkList = checkList;
}
this.totalOption = {};
this.$nextTick(() => {
this.totalOption = this.getOption(id);
this.changeDataZoom({start: 0, end: 100});
});
},
handleCheckListChange(id) {
this.totalOption = {};
this.showChart = false;
this.$nextTick(() => {
this.showChart = true;
this.totalOption = this.getOption(id);
this.changeDataZoom({start: 0, end: 100});
});
},
getOption(id) {
let legend = [];
let series = [];
for (const name of this.checkList) {
let check = this.checkOptions.filter(op => op.label === name)[0].key;
let yAxisIndex = 1;
if (check === 'cpu' || check === 'memory' || check === 'disk') {
yAxisIndex = 0;
}
this.data.forEach(d => {
if (d.instance === id && d.seriesName === check) {
if (legend.indexOf(name) > -1) {
return;
}
this.baseOption.xAxis.data = d.timestamps;
let yAxis = d.values.map(v => v.toFixed(2));
let data = [];
for (let i = 0; i < d.timestamps.length; i++) {
data.push([d.timestamps[i], yAxis[i]]);
}
legend.push(name);
series.push({
name: name,
data: data,
type: 'line',
yAxisIndex: yAxisIndex,
smooth: true,
sampling: 'lttb',
showSymbol: false,
});
this.seriesData = series;
}
});
}
this.baseOption.legend.data = legend;
this.baseOption.series = series;
return this.baseOption;
},
changeDataZoom(params) {
let start = params.start / 100;
let end = params.end / 100;
if (params.batch) {
start = params.batch[0].start / 100;
end = params.batch[0].end / 100;
}
let tableData = [];
for (let i = 0; i < this.seriesData.length; i++) {
let sub = this.seriesData[i].data, label = this.seriesData[i].name;
let len = 0;
let min, avg, max, sum = 0, startTime, endTime;
for (let j = 0; j < sub.length; j++) {
let time = sub[j][0];
let value = Number.parseFloat(sub[j][1]);
let index = (j / (sub.length - 1)).toFixed(2);
if (index < start) {
continue;
}
if (index >= end) {
endTime = time;
break;
}
if (!startTime) {
startTime = time;
}
if (!min && !max) {
min = max = value;
}
if (min > value) {
min = value;
}
if (max < value) {
max = value;
}
sum += value;
len++; // len
}
avg = (sum / len).toFixed(2);
tableData.push({label, min, max, avg, startTime, endTime});
}
this.tableData = tableData;
},
},
watch: {
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.getResource();
} else {
this.instances = [];
}
},
deep: true
},
planReportTemplate: {
handler() {
if (this.planReportTemplate) {
this.getResource();
}
},
deep: true
}
},
};
</script>
<style scoped>
.chart-config {
width: 100%;
}
.monitor-detail {
height: calc(100vh - 375px);
overflow: auto;
}
:deep(.el-checkbox__label ) {
font-size: 10px !important;
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<el-card class="table-card">
<template v-slot:header>
<span class="title">Overview</span>
</template>
<el-table border :data="overviewList" class="adjust-table test-content">
<el-table-column prop="name" :label="$t('commons.name')"/>
<el-table-column prop="createTime" :label="$t('commons.create_time')">
<template v-slot:default="scope">
<span>{{ scope.row.createTime | datetimeFormat }}</span>
</template>
</el-table-column>
<el-table-column prop="maxUsers" label="Max Users"/>
<el-table-column prop="avgTransactions" label="Avg.Transactions"/>
<el-table-column prop="errors" label="Errors"/>
<el-table-column prop="avgResponseTime" label="Avg.Response Time"/>
<el-table-column prop="responseTime90" label="90% Response Time"/>
<el-table-column prop="avgBandwidth" label="Avg.Bandwidth"/>
</el-table>
</el-card>
</template>
<script>
import {getOverview} from "@/api/report";
export default {
name: "OverviewCompareCard",
data() {
return {
reportId: null,
compareReports: [],
overviewList: [],
};
},
methods: {
initTable() {
this.overviewList = [];
this.reportId = this.$route.path.split('/')[4];
this.compareReports = JSON.parse(sessionStorage.getItem("compareReports"));
this.compareReports.forEach(report => {
this.initOverview(report);
})
},
initOverview(report) {
getOverview(report.id)
.then(({data}) => {
this.overviewList.push({
name: report.name,
createTime: report.createTime,
maxUsers: data.maxUsers,
avgThroughput: data.avgThroughput,
avgTransactions: data.avgTransactions,
errors: data.errors,
avgResponseTime: data.avgResponseTime,
responseTime90: data.responseTime90,
avgBandwidth: data.avgBandwidth,
})
})
.catch(() => {
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,145 @@
<template>
<ms-chart :options="bar"></ms-chart>
</template>
<script>
import echarts from 'echarts'
import MsChart from "metersphere-frontend/src/components/chart/MsChart";
export default {
name: "PerformanceChart",
components: {MsChart},
data() {
return {
bar: {
backgroundColor: '#394056',
title: {
top: 20,
text: 'Requests',
textStyle: {
fontWeight: 'normal',
fontSize: 16,
color: '#F1F1F3'
},
left: '1%'
},
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
color: '#57617B'
}
}
},
legend: {
top: 20,
icon: 'rect',
itemWidth: 14,
itemHeight: 5,
itemGap: 13,
data: ['CMCC', 'CTCC', 'CUCC'],
right: '4%',
textStyle: {
fontSize: 12,
color: '#F1F1F3'
}
},
grid: {
top: 100,
left: '2%',
right: '2%',
bottom: '2%',
containLabel: true
},
xAxis: [{
type: 'category',
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#57617B'
}
},
data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
}],
yAxis: [{
type: 'value',
name: '(%)',
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#57617B'
}
},
axisLabel: {
margin: 10,
textStyle: {
fontSize: 14
}
},
splitLine: {
lineStyle: {
color: '#57617B'
}
}
}],
series: [{
name: 'CMCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
width: 1
},
itemStyle: {
color: 'rgb(137,189,27)',
borderColor: 'rgba(137,189,2,0.27)',
borderWidth: 12
},
data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
}, {
name: 'CTCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
width: 1
},
itemStyle: {
color: 'rgb(0,136,212)',
borderColor: 'rgba(0,136,212,0.2)',
borderWidth: 12
},
data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
}, {
name: 'CUCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
width: 1
},
itemStyle: {
color: 'rgb(219,50,51)',
borderColor: 'rgba(219,50,51,0.2)',
borderWidth: 12
},
data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
}]
},
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,49 @@
<template>
<el-dialog
:title="$t('commons.environment')"
:visible.sync="dialogVisible"
width="30%"
:append-to-body="true"
:destroy-on-close="true"
:before-close="handleClose">
<div>
<div v-if="projectEnvMap" type="flex">
<div v-for="(values,key) in projectEnvMap" :key="key" style="margin-right: 10px">
{{ key + ":" }}
<ms-tag v-for="(item,index) in values" :key="index" type="success" :content="item"
style="margin-left: 2px"/>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import MsTag from "metersphere-frontend/src/components/MsTag";
export default {
name: "ProjectEnvironmentDialog",
components: {MsTag},
data() {
return {
projectEnvMap: {},
dialogVisible: false
}
},
methods: {
handleClose() {
this.dialogVisible = false;
this.projectEnvMap = {};
},
open(projectEnvMap) {
this.dialogVisible = true;
this.projectEnvMap = projectEnvMap;
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,200 @@
<template>
<div>
<el-table
:data="tableData"
stripe
border
height="calc(100vh - 190px)"
style="width: 100%"
>
<el-table-column label="Requests" min-width="150" align="center">
<el-table-column
prop="label"
label="Label"
sortable
min-width="150">
<template v-slot:header="{column}">
<span>Label</span>
<i class="el-icon-search" style="margin-left: 8px;cursor: pointer;font-weight: bold;"
@click="click(column)"></i>
<el-input v-model="searchLabel"
placeholder="请输入 Label 搜索"
size="mini"
class="search_input"
style="width: 100px; margin-left: 5px"
v-if="column.showSearch"
clearable
@clear="filterLabel"
@keyup.enter.native="filterLabel"/>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="Executions" align="center">
<el-table-column
prop="samples"
label="Samples"
sortable
width="110"
/>
<el-table-column
prop="fail"
label="FAIL"
sortable
align="center"
min-width="60"
/>
<el-table-column
prop="error"
label="Error%"
sortable
align="center"
/>
</el-table-column>
<el-table-column label="Response Times(ms)" align="center">
<el-table-column
prop="average"
label="Avg"
sortable
min-width="60"
/>
<el-table-column
prop="min"
label="Min"
sortable
min-width="60"
/>
<el-table-column
prop="max"
label="Max"
sortable
min-width="60"
/>
<el-table-column
prop="median"
label="Med"
sortable
min-width="60"
/>
<el-table-column
prop="tp90"
label="90%"
sortable
min-width="60"
/>
<el-table-column
prop="tp95"
label="95%"
sortable
min-width="60"
/>
<el-table-column
prop="tp99"
label="99%"
sortable
min-width="60"
/>
</el-table-column>
<el-table-column label="Throughput">
<el-table-column
prop="transactions"
label="Trans/s"
sortable
width="100"
/>
</el-table-column>
<el-table-column label="NetWork(KB/sec)" align="center">
<el-table-column
prop="received"
label="Recd"
sortable
align="center"
width="100"
/>
<el-table-column
prop="sent"
label="Sent"
sortable
align="center"
width="100"
/>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: "RequestStatistics",
data() {
return {
tableData: [],
originalData: [],
id: '',
searchLabel: '',
showSearch: false,
showBtn: true,
}
},
props: ['report', 'isShare', 'shareId', 'planReportTemplate'],
methods: {
initTableData() {
if (this.planReportTemplate) {
let data = this.planReportTemplate.reportStatistics;
this.tableData = data;
this.originalData = data;
}
},
click(column) {
this.searchLabel = '';
this.tableData = this.originalData;
this.$set(column, 'showSearch', !column.showSearch);
},
filterLabel() {
this.tableData = this.searchLabel ? this.originalData.filter(this.createFilter(this.searchLabel)) : this.originalData;
},
createFilter(queryString) {
return item => {
return (item.label.toLowerCase().indexOf(queryString.toLowerCase()) !== -1);
};
},
},
watch: {
report: {
handler(val) {
if (!val.status || !val.id) {
return;
}
let status = val.status;
this.id = val.id;
if (status === "Completed" || status === "Running") {
this.initTableData();
} else {
this.tableData = [];
}
},
deep: true
},
planReportTemplate: {
handler() {
if (this.planReportTemplate) {
this.initTableData();
}
},
deep: true
}
}
}
</script>
<style scoped>
.search_input :deep( .el-input__inner ) {
border-radius: 50px;
}
</style>

View File

@ -0,0 +1,190 @@
<template>
<el-card>
<template v-slot:header>
<span class="title">Response Time</span>
</template>
<div v-for="(option, index) in responseTimeList" :key="index">
<ms-chart ref="chart1" :options="option" :autoresize="true"></ms-chart>
</div>
</el-card>
</template>
<script>
import MsChart from "metersphere-frontend/src/components/chart/MsChart";
import {getPerformanceReportResChart} from "@/api/load-test";
export default {
name: "ResponseTimeCompareCard",
components: {MsChart},
data() {
return {
responseTimeList: []
}
},
methods: {
initCard() {
this.responseTimeList = [];
this.reportId = this.$route.path.split('/')[4];
this.compareReports = JSON.parse(sessionStorage.getItem("compareReports"));
this.compareReports.forEach(report => {
this.initOverview(report);
})
},
initOverview(report) {
getPerformanceReportResChart(report.id)
.then(({data}) => {
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
let yAxis2List = data.filter(m => m.yAxis === -1).map(m => m.yAxis2);
let yAxisListMax = this._getChartMax(yAxisList);
let yAxis2ListMax = this._getChartMax(yAxis2List);
let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName);
yAxisIndex0List = this._unique(yAxisIndex0List);
let yAxisIndex1List = data.filter(m => m.yAxis === -1).map(m => m.groupName);
yAxisIndex1List = this._unique(yAxisIndex1List);
let resOption = {
title: {
text: report.name + " " + this.$options.filters['datetimeFormat'](report.createTime),
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: {},
yAxis: [{
name: 'User',
type: 'value',
min: 0,
max: yAxisListMax,
interval: yAxisListMax / 5
},
{
name: 'Response Time',
type: 'value',
min: 0,
max: yAxis2ListMax,
interval: yAxis2ListMax / 5
}
],
series: []
}
let setting = {
series: [
{
name: 'users',
color: '#0CA74A',
}
]
}
yAxisIndex0List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'})
})
yAxisIndex1List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'})
})
this.responseTimeList.push(this.generateOption(resOption, data, setting));
})
.catch(() => {
this.responseTimeList = [];
})
},
generateOption(option, data, setting) {
let chartData = data;
let seriesArray = [];
for (let set in setting) {
if (set === "series") {
seriesArray = setting[set];
continue;
}
this.$set(option, set, setting[set]);
}
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] = []
}
if (item.yAxis === -1) {
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis2.toFixed(2)]);
} else {
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
};
let seriesArrayNames = seriesArray.map(m => m.name);
if (seriesArrayNames.includes(name)) {
for (let j = 0; j < seriesArray.length; j++) {
let seriesObj = seriesArray[j];
if (seriesObj['name'] === name) {
Object.assign(items, seriesObj);
}
}
}
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));
}
}
}
</script>
<style scoped>
.echarts {
width: 100%;
height: 300px;
}
</style>

View File

@ -0,0 +1,128 @@
<template>
<el-dialog :close-on-click-modal="false"
:destroy-on-close="true"
:title="$t('load_test.completed_test_report')" width="60%"
v-loading="loading"
:show-close="false"
:visible.sync="loadReportVisible">
<el-header class="header-btn">
<ms-dialog-header :enable-cancel="true" @cancel="close" @confirm="handleCompare" btn-size="mini">
</ms-dialog-header>
</el-header>
<ms-table
:data="tableData"
:show-select-all="false"
:screen-height="screenHeight"
ref="table"
>
<el-table-column
prop="name"
:label="$t('commons.name')"
show-overflow-tooltip>
<template v-slot:default="scope">
<i v-if="scope.row.id === report.id" class="el-icon-star-on"></i> {{ scope.row.name }}
</template>
</el-table-column>
<el-table-column
prop="userName"
:label="$t('report.user_name')"
show-overflow-tooltip>
</el-table-column>
<el-table-column prop="triggerMode"
:label="$t('test_track.report.list.trigger_mode')">
<template v-slot:default="scope">
<report-trigger-mode-item :trigger-mode="scope.row.triggerMode"/>
</template>
</el-table-column>
<el-table-column
:label="$t('commons.create_time')">
<template v-slot:default="scope">
<i class="el-icon-time"/>
<span class="last-modified">{{ scope.row.createTime | datetimeFormat }}</span>
</template>
</el-table-column>
</ms-table>
<ms-table-pagination :change="getCompareReports" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-dialog>
</template>
<script>
import MsTablePagination from "metersphere-frontend/src/components/pagination/TablePagination";
import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter";
import ReportTriggerModeItem from "metersphere-frontend/src/components/tableItem/ReportTriggerModeItem";
import MsTable from "metersphere-frontend/src/components/table/MsTable";
import MsDialogHeader from "metersphere-frontend/src/components/MsDialogHeader";
import {searchReports} from "@/api/report";
export default {
name: "SameTestReports",
components: {MsDialogHeader, MsTable, ReportTriggerModeItem, MsDialogFooter, MsTablePagination},
data() {
return {
loadReportVisible: false,
loading: false,
tableData: [],
currentPage: 1,
pageSize: 10,
total: 0,
report: {},
compareReports: [],
screenHeight: 'calc(100vh - 400px)',
}
},
methods: {
open(report) {
this.report = report;
this.compareReports = [];
this.getCompareReports(report);
this.compareReports.push(report);
this.loadReportVisible = true;
},
close() {
this.loadReportVisible = false;
},
getCompareReports() {
let condition = {
testId: this.report.testId,
filters: {status: ["Completed"]}
};
this.loading = searchReports(this.currentPage, this.pageSize, condition)
.then(res => {
let data = res.data;
this.total = data.itemCount;
this.tableData = data.listObject;
})
},
handleCompare() {
let reportIds = [...this.$refs.table.selectIds];
this.tableData
.filter(r => reportIds.indexOf(r.id) > -1 && this.report.id !== r.id)
.forEach(r => this.compareReports.push(r));
sessionStorage.setItem("compareReports", JSON.stringify(this.compareReports));
this.close();
this.$router.push({path: '/performance/report/compare/' + reportIds[0]});
},
}
}
</script>
<style scoped>
.header-btn {
position: absolute;
top: 20px;
right: 0;
padding: 0;
background: 0 0;
border: none;
outline: 0;
cursor: pointer;
height: 30px;
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<el-tabs>
<el-tab-pane :label="$t('load_test.pressure_config')">
<performance-pressure-config :is-read-only="true" :test="test" :report="report" :report-id="reportId"
:is-share="isShare" :share-id="shareId" @fileChange="fileChange"/>
</el-tab-pane>
<el-tab-pane :label="$t('load_test.advanced_config')">
<performance-advanced-config :is-read-only="true" :report-id="reportId" :report="report" :is-share="isShare"
:share-id="shareId" ref="advancedConfig"/>
</el-tab-pane>
</el-tabs>
</template>
<script>
import PerformancePressureConfig from "../../../load/PerformancePressureConfig";
import PerformanceAdvancedConfig from "../../../load/PerformanceAdvancedConfig";
export default {
name: "TestConfiguration",
components: {PerformancePressureConfig, PerformanceAdvancedConfig},
props: {
test: Object,
testId: String,
reportId: String,
report: Object,
isShare: Boolean,
shareId: String,
},
methods: {
fileChange(threadGroups) {
let csvSet = new Set;
threadGroups.forEach(tg => {
if (tg.csvFiles) {
tg.csvFiles.map(item => csvSet.add(item));
}
});
let csvFiles = [];
for (const f of csvSet) {
csvFiles.push({name: f, csvSplit: false, csvHasHeader: true});
}
this.$refs.advancedConfig.csvFiles = csvFiles;
},
}
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,728 @@
<template>
<div>
<el-row>
<el-col :span="6">
<div style="padding-bottom: 5px;">
<el-link type="primary" @click="resetDefault()">{{ $t('load_test.report.set_default') }}</el-link>
</div>
<el-collapse v-model="activeNames" class="test-detail">
<el-collapse-item name="users">
<template v-slot:title>
<div style="width: 100%">
<span>{{ $t('load_test.report.ActiveThreadsChart') }}</span>
<span style="float:right;">
<el-link type="primary" @click="selectAll( 'ActiveThreadsChart', $event)">
{{ $t('load_test.report.select_all') }}
</el-link>
/
<el-link type="default" @click="unselectAll('ActiveThreadsChart', $event)">
{{ $t('load_test.report.unselect_all') }}
</el-link>
</span>
</div>
</template>
<el-checkbox-group v-model="checkList['ActiveThreadsChart']"
@change="handleChecked('ActiveThreadsChart')">
<div v-for="name in checkOptions['ActiveThreadsChart']" :key="name">
<el-tooltip class="item" effect="dark"
:content="name"
:disabled="name.length < minLength"
placement="top">
<el-checkbox :label="name"/>
</el-tooltip>
</div>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item name="transactions">
<template v-slot:title>
<div style="width: 100%">
<span>{{ $t('load_test.report.TransactionsChart') }}</span>
<span style="float:right;">
<el-link type="primary" @click="selectAll( 'TransactionsChart', $event)">
{{ $t('load_test.report.select_all') }}
</el-link>
/
<el-link type="default" @click="unselectAll('TransactionsChart', $event)">
{{ $t('load_test.report.unselect_all') }}
</el-link>
</span>
</div>
</template>
<el-checkbox-group v-model="checkList['TransactionsChart']" @change="handleChecked('TransactionsChart')">
<div v-for="name in checkOptions['TransactionsChart']"
:key="name">
<el-tooltip class="item" effect="dark"
:content="name"
:disabled="name.length < minLength"
placement="top">
<el-checkbox :label="name"/>
</el-tooltip>
</div>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item name="responseTime">
<template v-slot:title>
<div style="width: 100%">
<span>{{ $t('load_test.report.ResponseTimeChart') }}</span>
<span style="float:right;">
<el-link type="primary" @click="selectAll( 'ResponseTimeChart', $event)">
{{ $t('load_test.report.select_all') }}
</el-link>
/
<el-link type="default" @click="unselectAll('ResponseTimeChart', $event)">
{{ $t('load_test.report.unselect_all') }}
</el-link>
</span>
</div>
</template>
<el-checkbox-group v-model="checkList['ResponseTimeChart']" @change="handleChecked('ResponseTimeChart')">
<div v-for="name in checkOptions['ResponseTimeChart']"
:key="name">
<el-tooltip class="item" effect="dark"
:content="name"
:disabled="name.length < minLength"
placement="top">
<el-checkbox :label="name"/>
</el-tooltip>
</div>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item name="responseTimePercentiles">
<template v-slot:title>
<div style="width: 100%">
<span>{{ $t('load_test.report.ResponseTimePercentilesChart') }}</span>
<span style="float:right;">
<el-link type="primary" @click="selectAll( 'ResponseTimePercentilesChart', $event)">
{{ $t('load_test.report.select_all') }}
</el-link>
/
<el-link type="default" @click="unselectAll('ResponseTimePercentilesChart', $event)">
{{ $t('load_test.report.unselect_all') }}
</el-link>
</span>
</div>
</template>
<el-checkbox-group v-model="checkList['ResponseTimePercentilesChart']"
@change="handleChecked('ResponseTimePercentilesChart')">
<div v-for="name in checkOptions['ResponseTimePercentilesChart']"
:key="name">
<el-tooltip class="item" effect="dark"
:content="name"
:disabled="name.length < minLength"
placement="top">
<el-checkbox :label="name"/>
</el-tooltip>
</div>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.ResponseCodeChart')" name="responseCode">
<template v-slot:title>
<div style="width: 100%">
<span>{{ $t('load_test.report.ResponseCodeChart') }}</span>
<span style="float:right;">
<el-link type="primary" @click="selectAll( 'ResponseCodeChart', $event)">
{{ $t('load_test.report.select_all') }}
</el-link>
/
<el-link type="default" @click="unselectAll('ResponseCodeChart', $event)">
{{ $t('load_test.report.unselect_all') }}
</el-link>
</span>
</div>
</template>
<el-checkbox-group v-model="checkList['ResponseCodeChart']" @change="handleChecked('ResponseCodeChart')">
<div v-for="name in checkOptions['ResponseCodeChart']"
:key="name">
<el-tooltip class="item" effect="dark"
:content="name"
:disabled="name.length < minLength"
placement="top">
<el-checkbox :label="name"/>
</el-tooltip>
</div>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.LatencyChart')" name="latency">
<template v-slot:title>
<div style="width: 100%">
<span>{{ $t('load_test.report.LatencyChart') }}</span>
<span style="float:right;">
<el-link type="primary" @click="selectAll( 'LatencyChart', $event)">
{{ $t('load_test.report.select_all') }}
</el-link>
/
<el-link type="default" @click="unselectAll('LatencyChart', $event)">
{{ $t('load_test.report.unselect_all') }}
</el-link>
</span>
</div>
</template>
<el-checkbox-group v-model="checkList['LatencyChart']" @change="handleChecked('LatencyChart')">
<div v-for="name in checkOptions['LatencyChart']"
:key="name">
<el-tooltip class="item" effect="dark"
:content="name"
:disabled="name.length < minLength"
placement="top">
<el-checkbox :label="name"/>
</el-tooltip>
</div>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.BytesThroughputChart')" name="bytes">
<template v-slot:title>
<div style="width: 100%">
<span>{{ $t('load_test.report.BytesThroughputChart') }}</span>
<span style="float:right;">
<el-link type="primary" @click="selectAll( 'BytesThroughputChart', $event)">
{{ $t('load_test.report.select_all') }}
</el-link>
/
<el-link type="default" @click="unselectAll('BytesThroughputChart', $event)">
{{ $t('load_test.report.unselect_all') }}
</el-link>
</span>
</div>
</template>
<el-checkbox-group v-model="checkList['BytesThroughputChart']"
@change="handleChecked('BytesThroughputChart')">
<div v-for="name in checkOptions['BytesThroughputChart']"
:key="name">
<el-tooltip class="item" effect="dark"
:content="name"
:disabled="name.length < minLength"
placement="top">
<el-checkbox :label="name"/>
</el-tooltip>
</div>
</el-checkbox-group>
</el-collapse-item>
<el-collapse-item :title="$t('load_test.report.ErrorsChart')" name="errors">
<template v-slot:title>
<div style="width: 100%">
<span>{{ $t('load_test.report.ErrorsChart') }}</span>
<span style="float:right;">
<el-link type="primary" @click="selectAll( 'ErrorsChart', $event)">
{{ $t('load_test.report.select_all') }}
</el-link>
/
<el-link type="default" @click="unselectAll('ErrorsChart', $event)">
{{ $t('load_test.report.unselect_all') }}
</el-link>
</span>
</div>
</template>
<el-checkbox-group v-model="checkList['ErrorsChart']" @change="handleChecked('ErrorsChart')">
<div v-for="name in checkOptions['ErrorsChart']"
:key="name">
<el-tooltip class="item" effect="dark"
:content="name"
:disabled="name.length < minLength"
placement="top">
<el-checkbox :label="name"/>
</el-tooltip>
</div>
</el-checkbox-group>
</el-collapse-item>
</el-collapse>
</el-col>
<el-col :span="18" v-loading="result.loading">
<el-row>
<el-col :span="24">
<ms-chart ref="chart2"
v-if="refresh"
class="chart-config"
:options="totalOption"
@datazoom="changeDataZoom"
:autoresize="true"/>
</el-col>
</el-row>
<el-row>
<el-col :offset="2" :span="20">
<el-table
v-if="refresh"
:data="tableData"
stripe
border
style="width: 100%">
<el-table-column label="Label" align="center">
<el-table-column
prop="label"
label="Label"
sortable>
</el-table-column>
</el-table-column>
<el-table-column label="Aggregate" align="center">
<el-table-column
prop="avg"
label="Avg."
width="100"
sortable
/>
<el-table-column
prop="min"
label="Min."
width="100"
sortable
/>
<el-table-column
prop="max"
label="Max."
width="100"
sortable
/>
</el-table-column>
<el-table-column label="Range" align="center">
<el-table-column
prop="startTime"
label="Start"
width="160"
/>
<el-table-column
prop="endTime"
label="End"
width="160"
/>
</el-table-column>
</el-table>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</template>
<script>
import MsChart from "metersphere-frontend/src/components/chart/MsChart";
const color = ['#60acfc', '#32d3eb', '#5bc49f', '#feb64d', '#ff7c7c', '#9287e7', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'];
const groupBy = function (xs, key) {
return xs.reduce(function (rv, x) {
(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
const CHART_MAP = [
'ActiveThreadsChart',
'TransactionsChart',
'ResponseTimeChart',
'ResponseTimePercentilesChart',
'ResponseCodeChart',
'ErrorsChart',
'LatencyChart',
'BytesThroughputChart',
];
export default {
name: "TestDetails",
components: {MsChart},
props: ['report', 'export', 'isShare', 'shareId', 'planReportTemplate'],
data() {
return {
result: {},
activeNames: 'users',
minLength: 35,
loadOption: {},
resOption: {},
totalOption: {},
responseCodes: [],
checkList: CHART_MAP.reduce((result, curr) => {
result[curr] = [];
return result;
}, {}),
checkOptions: {},
defaultProps: {
children: 'children',
label: 'label'
},
init: false,
refresh: true,
tableData: [],
baseOption: {
color: color,
grid: {
// right: '35%' //
},
title: {},
tooltip: {
show: true,
trigger: 'axis',
axisPointer: {
type: 'cross'
},
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: {
y: 'top',
},
xAxis: {boundaryGap: false},
yAxis: [],
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
start: 0,
end: 20
}
],
series: []
},
seriesData: [],
legend: [],
};
},
methods: {
resetDefault() {
this.checkList['ActiveThreadsChart'] = ['ALL'];
this.checkList['TransactionsChart'] = ['ALL'];
this.checkList['ResponseTimeChart'] = ['ALL'];
//
this.checkList['ResponseTimePercentilesChart'] = [];
this.checkList['ErrorsChart'] = [];
this.checkList['LatencyChart'] = [];
this.checkList['BytesThroughputChart'] = [];
this.getTotalChart();
},
selectAll(name, e) {
if (e) {
e.stopPropagation(); //
}
this.seriesData = [];
this.totalOption = {};
this.baseOption.yAxis = [];
this.legend = [];
this.checkList[name] = this.checkOptions[name];
this.getTotalChart();
},
unselectAll(name, e) {
if (e) {
e.stopPropagation(); //
}
this.seriesData = [];
this.totalOption = {};
this.baseOption.yAxis = [];
this.legend = [];
if (name) {
this.checkList[name] = [];
this.getTotalChart();
return;
}
for (const name in this.checkList) {
this.checkList[name] = [];
}
},
handleChecked(name) {
this.getTotalChart();
this.refresh = false;
this.$nextTick(() => {
this.refresh = true;
});
},
initTableData() {
for (const name of CHART_MAP) {
this.getCheckOptions(name);
}
this.resetDefault();
},
getCheckOptions(reportKey) {
if (this.planReportTemplate) {
let data = this.planReportTemplate.checkOptions[reportKey];
this.handleGetCheckOptions(data, reportKey);
}
},
handleGetCheckOptions(data, reportKey) {
if (!data || data.length === 0) {
this.init = false;
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.result.loading = true;
this.totalOption = {};
this.seriesData = [];
this.baseOption.yAxis = [];
this.legend = [];
let promises = [];
if (this.planReportTemplate) {
let chars = [];
for (let name in this.checkList) {
let data = this.planReportTemplate.checkOptions[name];
chars.push({data, 'reportKey': name});
}
this.handleGetTotalChart(chars);
} else {
for (let name in this.checkList) {
promises.push(this.getChart(name, this.checkList[name]));
}
Promise.all(promises).then((res) => {
this.handleGetTotalChart(res);
}).catch(() => {
this.result.loading = false;
});
}
},
handleGetTotalChart(res) {
res = res.filter(v => !!v);
if (res.length === 0) {
this.refresh = false;
this.result.loading = false;
} else {
this.refresh = true;
}
for (let i = 0; i < res.length; i++) {
if (i === 0) {
this.baseOption.yAxis.push({
name: this.$t('load_test.report.' + res[i].reportKey),
type: 'value',
min: 0,
position: 'left',
boundaryGap: [0, '100%']
});
} else {
this.baseOption.yAxis.push({
name: this.$t('load_test.report.' + res[i].reportKey),
type: 'value',
min: 0,
position: 'right',
nameRotate: 20,
offset: (i - 1) * 50,
boundaryGap: [0, '100%']
});
}
this.totalOption = this.generateOption(this.baseOption, res[i].data, i);
}
this.totalOption.grid.right = (res.length - 1) * 5 + '%';
this.changeDataZoom({start: 0, end: 100});
this.result.loading = false;
},
getChart(reportKey, checkList) {
if (!checkList || checkList.length === 0) {
return;
}
this.totalOption = {};
},
handleGetChart(data, reportKey, checkList) {
let allData = [];
let checkAllOption = checkList.indexOf('ALL') > -1;
if (checkAllOption) {
let avgOpt = [
'ResponseTimeChart',
'ResponseTimePercentilesChart',
'LatencyChart',
];
let result = groupBy(data, 'xAxis');
for (const xAxis in result) {
let yAxis = result[xAxis].map(a => a.yAxis).reduce((a, b) => a + b, 0);
if (avgOpt.indexOf(reportKey) > -1) {
yAxis = yAxis / result[xAxis].length;
}
allData.push({
groupName: 'ALL',
xAxis: xAxis,
yAxis: yAxis
});
}
}
//
data = data.filter(item => {
if (checkList.indexOf(item.groupName) > -1) {
return true;
}
});
// all
data = data.concat(allData);
// prefix
data.forEach(item => {
item.groupName = this.$t('load_test.report.' + reportKey) + ': ' + item.groupName;
});
return {data, reportKey};
},
generateOption(option, data, yAxisIndex) {
let chartData = data;
let series = {}, xAxis = [];
chartData.forEach(item => {
if (!xAxis.includes(item.xAxis)) {
xAxis.push(item.xAxis);
}
xAxis.sort();
let name = item.groupName;
if (!this.legend.includes(name)) {
this.legend.push(name);
series[name] = [];
}
if (series[name]) {
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis.toFixed(2)]);
}
});
this.$set(option.legend, "data", this.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,
yAxisIndex: yAxisIndex,
smooth: true,
sampling: 'lttb',
showSymbol: false,
animation: !this.export,
};
this.seriesData.push(items);
}
this.$set(option, "series", this.seriesData);
return option;
},
changeDataZoom(params) {
let start = params.start / 100;
let end = params.end / 100;
if (params.batch) {
start = params.batch[0].start / 100;
end = params.batch[0].end / 100;
}
let tableData = [];
for (let i = 0; i < this.seriesData.length; i++) {
let sub = this.seriesData[i].data, label = this.seriesData[i].name;
let len = 0;
let min, avg, max, sum = 0, startTime, endTime;
for (let j = 0; j < sub.length; j++) {
let time = sub[j][0];
let value = Number.parseFloat(sub[j][1]);
let index = (j / (sub.length - 1)).toFixed(2);
if (index < start) {
continue;
}
if (index >= end) {
endTime = time;
break;
}
if (!startTime) {
startTime = time;
}
if (!min && !max) {
min = max = value;
}
if (min > value) {
min = value;
}
if (max < value) {
max = value;
}
sum += value;
len++; // len
}
avg = (sum / len).toFixed(2);
tableData.push({label, min, max, avg, startTime, endTime});
}
this.tableData = tableData;
},
_getChartMax(arr) {
const max = Math.max(...arr);
return Math.ceil(max / 4.5) * 5;
},
_unique(arr) {
return Array.from(new Set(arr));
}
},
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 === "Running") {
this.getTotalChart();
} else if (status === "Completed") {
this.initTableData();
}
},
deep: true
},
planReportTemplate: {
handler() {
if (this.planReportTemplate) {
this.initTableData();
// todo
// this.getTotalChart();
}
},
deep: true
}
},
};
</script>
<style scoped>
.chart-config {
width: 100%;
height: 445px;
}
.test-detail {
height: calc(100vh - 215px);
overflow: auto;
}
:deep(.el-checkbox__label ) {
font-size: 10px !important;
}
</style>

View File

@ -0,0 +1,584 @@
<template>
<div>
<el-row :gutter="12">
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-1">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{ maxUsers }}</span>
<span class="ms-card-data-unit"> VU</span>
</span>
<span class="ms-card-desc">{{ $t('load_test.report.ActiveThreadsChart') }}</span>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-2">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{ avgTransactions }}</span>
<span class="ms-card-data-unit"> TPS</span>
</span>
<span class="ms-card-desc">{{ $t('load_test.report.TransactionsChart') }}</span>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-3">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{ errors }}</span>
<span class="ms-card-data-unit"> %</span>
</span>
<span class="ms-card-desc">{{ $t('load_test.report.ErrorsChart') }}</span>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-4">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{ avgResponseTime }}</span>
<span class="ms-card-data-unit"> s</span>
</span>
<span class="ms-card-desc">{{ $t('load_test.report.ResponseTimeChart') }}</span>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-5">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{ responseTime90 }}</span>
<span class="ms-card-data-unit"> s</span>
</span>
<span class="ms-card-desc">90% {{ $t('load_test.report.ResponseTimeChart') }}</span>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-6">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{ avgBandwidth }}</span>
<span class="ms-card-data-unit"> KiB/s</span>
</span>
<span class="ms-card-desc">{{ $t('load_test.report.Network') }}</span>
</el-card>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<ms-chart ref="chart1" :options="loadOption" class="chart-config" :autoresize="true"></ms-chart>
</el-col>
<el-col :span="12">
<ms-chart ref="chart2" :options="resOption" class="chart-config" :autoresize="true"></ms-chart>
</el-col>
</el-row>
</div>
</template>
<script>
import MsChart from "metersphere-frontend/src/components/chart/MsChart";
const color = ['#60acfc', '#32d3eb', '#5bc49f', '#feb64d', '#ff7c7c', '#9287e7', '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3'];
const groupBy = function (xs, key) {
return xs.reduce(function (rv, x) {
(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
export default {
name: "TestOverview",
components: {MsChart},
data() {
return {
maxUsers: "0",
avgThroughput: "0",
avgTransactions: "0",
errors: "0",
avgResponseTime: "0",
responseTime90: "0",
avgBandwidth: "0",
loadOption: {},
resOption: {},
errorOption: {},
resCodeOption: {},
id: ''
};
},
props: ['report', 'export', 'isShare', 'shareId', 'planReportTemplate'],
methods: {
initTableData() {
if (this.planReportTemplate) {
let data = this.planReportTemplate.testOverview;
this.buildInfo(data);
}
this.getLoadChart();
this.getResChart();
},
buildInfo(data) {
this.maxUsers = data ? data.maxUsers : '0';
this.avgThroughput = data ? data.avgThroughput : '0';
this.avgTransactions = data ? data.avgTransactions : '0';
this.errors = data ? data.errors : '0';
this.avgResponseTime = data ? data.avgResponseTime : '0';
this.responseTime90 = data ? data.responseTime90 : '0';
this.avgBandwidth = data ? data.avgBandwidth : '0';
},
getLoadChart() {
if (this.planReportTemplate) {
let data = this.planReportTemplate.loadChartData;
this.handleGetLoadChart(data);
}
},
handleGetLoadChart(data) {
if (!data) {
return;
}
let loadOption = {
color: color,
title: {
text: 'Load',
left: 'center',
top: 20,
textStyle: {
color: '#65A2FF'
},
},
tooltip: {
show: true,
trigger: 'axis',
// extraCssText: 'z-index: 999;',
confine: true,
},
legend: {},
xAxis: {},
series: []
};
let allData = [];
let result = groupBy(data, 'xAxis');
for (const xAxis in result) {
let yAxis1 = result[xAxis].filter(a => a.yAxis2 === -1).map(a => a.yAxis).reduce((a, b) => a + b, 0);
let yAxis2 = result[xAxis].filter(a => a.yAxis === -1).map(a => a.yAxis2).reduce((a, b) => a + b, 0);
allData.push({
groupName: 'users',
xAxis: xAxis,
yAxis: yAxis1,
yAxis2: -1,
yAxisIndex: 0,
}, {
groupName: 'transactions/s',
xAxis: xAxis,
yAxis: -1,
yAxis2: yAxis2,
yAxisIndex: 1,
});
}
let yAxisList = allData.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
let yAxis2List = allData.filter(m => m.yAxis === -1).map(m => m.yAxis2);
let yAxisListMax = this._getChartMax(yAxisList);
let yAxis2ListMax = this._getChartMax(yAxis2List);
loadOption.yAxis = [{
name: 'User',
type: 'value',
min: 0,
max: yAxisListMax,
splitNumber: 5,
interval: yAxisListMax / 5
},
{
name: 'Transactions/s',
type: 'value',
splitNumber: 5,
min: 0,
max: yAxis2ListMax,
interval: yAxis2ListMax / 5
}
];
this.loadOption = this.generateOption(loadOption, allData);
},
getResChart() {
if (this.planReportTemplate) {
let data = this.planReportTemplate.responseTimeChartData;
this.handleGetResChart(data);
}
},
handleGetResChart(data) {
if (!data) {
return;
}
let resOption = {
color: color,
title: {
text: 'Response Time',
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: []
};
let allData = [];
let result = groupBy(data, 'xAxis');
for (const xAxis in result) {
let yAxis1 = result[xAxis].filter(a => a.yAxis2 === -1).map(a => a.yAxis).reduce((a, b) => a + b, 0);
yAxis1 = yAxis1 / result[xAxis].length;
allData.push({
groupName: 'response',
xAxis: xAxis,
yAxis: -1,
yAxis2: yAxis1,
yAxisIndex: 0,
});
}
let yAxisList = allData.filter(m => m.yAxis === -1).map(m => m.yAxis2);
let yAxisListMax = this._getChartMax(yAxisList);
resOption.yAxis = [
{
name: 'Response Time',
type: 'value',
min: 0,
max: yAxisListMax,
interval: yAxisListMax / 5
}
];
this.resOption = this.generateOption(resOption, allData);
},
getErrorChart() {
if (this.planReportTemplate) {
let data = this.planReportTemplate.loadOverviewErrorChart;
this.handleGetErrorChart(data);
}
},
handleGetErrorChart(data) {
if (!data) {
return;
}
let errorOption = {
color: color,
title: {
text: 'Errors',
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: []
};
let allData = [];
let result = groupBy(data, 'xAxis');
for (const xAxis in result) {
let yAxis1 = result[xAxis].filter(a => a.yAxis2 === -1).map(a => a.yAxis).reduce((a, b) => a + b, 0);
allData.push({
groupName: 'errors',
xAxis: xAxis,
yAxis: -1,
yAxis2: yAxis1,
yAxisIndex: 0,
});
}
let yAxisList = allData.filter(m => m.yAxis === -1).map(m => m.yAxis2);
let yAxisListMax = this._getChartMax(yAxisList);
errorOption.yAxis = [
{
name: 'No',
type: 'value',
min: 0,
max: yAxisListMax,
interval: yAxisListMax / 5
}
];
this.errorOption = this.generateOption(errorOption, allData);
},
getResponseCodeChart() {
if (this.planReportTemplate) {
let data = this.planReportTemplate.responseCodeChartData;
this.handleGetResponseCodeChart(data);
}
},
handleGetResponseCodeChart(data) {
if (!data) {
return;
}
let resCodeOption = {
color: color,
title: {
text: 'Response code',
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: []
};
let allData = [];
let result = groupBy(data, 'xAxis');
for (const xAxis in result) {
let yAxis1 = result[xAxis].filter(a => a.yAxis2 === -1).map(a => a.yAxis).reduce((a, b) => a + b, 0);
allData.push({
groupName: 'codes',
xAxis: xAxis,
yAxis: -1,
yAxis2: yAxis1,
yAxisIndex: 0,
});
}
let yAxisList = allData.filter(m => m.yAxis === -1).map(m => m.yAxis2);
let yAxisListMax = this._getChartMax(yAxisList);
resCodeOption.yAxis = [
{
name: 'No',
type: 'value',
min: 0,
max: yAxisListMax,
interval: yAxisListMax / 5
}
];
this.resCodeOption = this.generateOption(resCodeOption, allData);
},
generateOption(option, data) {
let chartData = data;
let legend = [], series = {}, xAxis = [], seriesData = [], yAxisIndex = {};
chartData.forEach(item => {
if (!xAxis.includes(item.xAxis)) {
xAxis.push(item.xAxis);
}
xAxis.sort();
let name = item.groupName;
yAxisIndex[name] = item.yAxisIndex;
if (!legend.includes(name)) {
legend.push(name);
series[name] = [];
}
if (item.yAxis === -1) {
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis2.toFixed(2)]);
} else {
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',
showSymbol: false,
animation: !this.export,
yAxisIndex: yAxisIndex[name]
};
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));
}
},
watch: {
report: {
handler(val) {
if (!val.status || !val.id) {
return;
}
let status = val.status;
this.id = val.id;
if (status === "Completed" || status === "Running") {
this.initTableData();
} else {
this.maxUsers = '0';
this.avgThroughput = '0';
this.avgTransactions = '0';
this.errors = '0';
this.avgResponseTime = '0';
this.responseTime90 = '0';
this.avgBandwidth = '0';
this.loadOption = {};
this.resOption = {};
this.errorOption = {};
this.resCodeOption = {};
}
},
deep: true
},
planReportTemplate: {
handler() {
if (this.planReportTemplate) {
this.initTableData();
}
},
deep: true
}
},
};
</script>
<style scoped>
.ms-card-data {
text-align: left;
display: block;
margin-bottom: 5px;
}
.ms-card-desc {
display: block;
text-align: left;
}
.ms-card-data-digital {
font-size: 21px;
}
.ms-card-data-unit {
color: #8492a6;
font-size: 15px;
}
.ms-card-index-1 .ms-card-data-digital {
color: #44b349;
}
.ms-card-index-1 {
border-left-color: #44b349;
border-left-width: 3px;
}
.ms-card-index-2 .ms-card-data-digital {
color: #65A2FF;
}
.ms-card-index-2 {
border-left-color: #65A2FF;
border-left-width: 3px;
}
.ms-card-index-3 .ms-card-data-digital {
color: #E6113C;
}
.ms-card-index-3 {
border-left-color: #E6113C;
border-left-width: 3px;
}
.ms-card-index-4 .ms-card-data-digital {
color: #99743C;
}
.ms-card-index-4 {
border-left-color: #99743C;
border-left-width: 3px;
}
.ms-card-index-5 .ms-card-data-digital {
color: #99743C;
}
.ms-card-index-5 {
border-left-color: #99743C;
border-left-width: 3px;
}
.ms-card-index-6 .ms-card-data-digital {
color: #3C9899;
}
.ms-card-index-6 {
border-left-color: #3C9899;
border-left-width: 3px;
}
.chart-config {
width: 100%;
}
</style>

View File

@ -1,22 +0,0 @@
<template>
<div>
<span v-if="!reportId">{{$t('commons.not_performed_yet')}}</span>
<ms-api-report-view-detail v-if="reportId" :report-id="reportId"/>
</div>
</template>
<script>
// import MsApiReportViewDetail from "@/business/api/relation/ApiReportViewDetail";
export default {
name: "ApiTestResult",
components: {MsApiReportViewDetail},
props: ['reportId'],
}
</script>
<style>
</style>
<style scoped>
</style>

View File

@ -1,135 +0,0 @@
<template>
<ms-container>
<ms-main-container>
<el-card>
<!--<el-card v-loading="result.loading">-->
<el-row>
<el-col :span="10">
<el-input :disabled="true" :placeholder="$t('load_test.input_name')" v-model="test.name" class="input-with-select">
<template v-slot:prepend>
<el-select :disabled="true" v-model="project.name" :placeholder="$t('load_test.select_project')" slot="prepend">
</el-select>
</template>
</el-input>
</el-col>
<el-col :span="12" :offset="2">
<el-button :disabled="isReadOnly" type="primary" plain @click="runTest">执行</el-button>
</el-col>
</el-row>
<el-tabs class="test-config" v-model="active">
<el-tab-pane :label="$t('load_test.basic_config')">
<performance-basic-config :is-read-only="true" :test="test" ref="basicConfig"/>
</el-tab-pane>
<el-tab-pane :label="$t('load_test.pressure_config')">
<performance-pressure-config :is-read-only="true" :test="test" :test-id="id" ref="pressureConfig"/>
</el-tab-pane>
<el-tab-pane :label="$t('load_test.advanced_config')" class="advanced-config">
<performance-advanced-config :read-only="true" :test-id="id" ref="advancedConfig"/>
</el-tab-pane>
</el-tabs>
</el-card>
</ms-main-container>
</ms-container>
</template>
<script>
import MsContainer from "metersphere-frontend/src/components/MsContainer";
import MsMainContainer from "metersphere-frontend/src/components/MsMainContainer";
// import PerformanceBasicConfig from "../../../../../performance/test/components/PerformanceBasicConfig";
// import PerformancePressureConfig from "../../../../../performance/test/components/PerformancePressureConfig";
// import PerformanceAdvancedConfig from "../../../../../performance/test/components/PerformanceAdvancedConfig";
export default {
name: "PerformanceTestDetail",
components: {
PerformanceAdvancedConfig,
PerformancePressureConfig,
PerformanceBasicConfig,
MsMainContainer,
MsContainer
},
data() {
return {
result: {},
test: {},
savePath: "/performance/save",
editPath: "/performance/edit",
runPath: "/performance/run",
project: {},
projectId: '',
active: '0',
tabs: [{
title: this.$t('load_test.basic_config'),
id: '0',
component: 'PerformanceBasicConfig'
}, {
title: this.$t('load_test.pressure_config'),
id: '1',
component: 'PerformancePressureConfig'
}, {
title: this.$t('load_test.advanced_config'),
id: '2',
component: 'PerformanceAdvancedConfig'
}]
}
},
props: {
id: String,
isReadOnly: {
type: Boolean,
default: false
}
},
methods: {
init() {
this.getTest();
},
getProject(projectId) {
this.$get("/project/get/" + projectId, response => {
this.project = response.data;
});
},
getTest() {
if (this.id) {
this.result = this.$get('/performance/get/' + this.id, response => {
if (response.data) {
this.test = response.data;
this.getProject(this.test.projectId);
} else {
this.test = {};
}
});
}
},
runTest() {
this.result = this.$post(this.runPath, {id: this.test.id, triggerMode: 'MANUAL'}, (response) => {
this.$success(this.$t('load_test.is_running'));
this.$emit('runTest', response.data);
});
}
}
}
</script>
<style scoped>
.test-config {
margin-top: 15px;
text-align: center;
}
.el-select {
min-width: 130px;
}
.edit-test-container .input-with-select .el-input-group__prepend {
background-color: #fff;
}
.advanced-config {
height: calc(100vh - 280px);
overflow: auto;
}
</style>

View File

@ -1,241 +0,0 @@
<template>
<ms-container>
<ms-main-container>
<span v-if="!reportId">{{$t('commons.not_performed_yet')}}</span>
<el-card v-loading="result.loading" v-if="reportId">
<el-row>
<el-col :span="16">
<el-row>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/performance/test/' + this.projectId }">{{projectName}}
</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/performance/test/edit/' + this.testId }">{{testName}}
</el-breadcrumb-item>
<el-breadcrumb-item>{{reportName}}</el-breadcrumb-item>
</el-breadcrumb>
</el-row>
<!--<el-row class="ms-report-view-btns">-->
<!--<el-button :disabled="isReadOnly" type="primary" plain size="mini">{{$t('report.test_stop_now')}}</el-button>-->
<!--<el-button :disabled="isReadOnly" type="success" plain size="mini">{{$t('report.test_execute_again')}}</el-button>-->
<!--<el-button :disabled="isReadOnly" type="info" plain size="mini">{{$t('report.export')}}</el-button>-->
<!--<el-button :disabled="isReadOnly" type="warning" plain size="mini">{{$t('report.compare')}}</el-button>-->
<!--</el-row>-->
</el-col>
<el-col :span="8">
<span class="ms-report-time-desc">
{{$t('report.test_duration', [this.minutes, this.seconds])}}
</span>
<span class="ms-report-time-desc">
{{$t('report.test_start_time')}}{{startTime}}
</span>
<span class="ms-report-time-desc">
{{$t('report.test_end_time')}}{{endTime}}
</span>
</el-col>
</el-row>
<el-divider></el-divider>
<el-tabs v-model="active">
<el-tab-pane :label="$t('report.test_overview')">
<test-overview :report="report"/>
</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')">
<request-statistics :report="report"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_error_log')">
<error-log :report="report"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_log_details')">
<log-details :report="report"/>
</el-tab-pane>
</el-tabs>
</el-card>
</ms-main-container>
</ms-container>
</template>
<script>
import MsMainContainer from "metersphere-frontend/src/components/MsMainContainer";
import MsContainer from "metersphere-frontend/src/components/MsContainer";
// import LogDetails from "metersphere-frontend/src/components/report/LogDetails";
// import ErrorLog from "metersphere-frontend/src/components/report/ErrorLog";
// import RequestStatistics from "metersphere-frontend/src/components/report/RequestStatistics";
// import TestOverview from "metersphere-frontend/src/components/report/TestOverview";
// import MsReportTestDetails from "metersphere-frontend/src/components/report/TestDetails";
export default {
name: "PerformanceTestResult",
components: {
TestOverview,
MsReportTestDetails,
RequestStatistics,
ErrorLog,
LogDetails,
MsContainer,
MsMainContainer
},
data() {
return {
result: {},
active: '0',
status: '',
reportName: '',
testId: '',
testName: '',
projectId: '',
projectName: '',
startTime: '0',
endTime: '0',
minutes: '0',
seconds: '0',
title: 'Logging',
report: {}
}
},
props: {
reportId: String,
isReadOnly: {
type: Boolean,
default: false
},
},
mounted() {
this.init();
},
watch: {
reportId() {
this.init();
}
},
methods: {
initBreadcrumb() {
if (this.reportId) {
this.result = this.$get("/performance/report/test/pro/info/" + this.reportId, res => {
let data = res.data;
if (data) {
this.reportName = data.name;
this.testId = data.testId;
this.testName = data.testName;
this.projectId = data.projectId;
this.projectName = data.projectName;
}
})
}
},
initReportTimeInfo() {
if (this.reportId) {
this.result = this.$get("/performance/report/content/report_time/" + this.reportId)
.then(res => {
let data = res.data.data;
if (data) {
this.startTime = data.startTime;
this.endTime = data.endTime;
let duration = data.duration;
this.minutes = Math.floor(duration / 60);
this.seconds = duration % 60;
}
}).catch(() => {
this.clearData();
})
}
},
checkReportStatus() {
if (!this.report) {
return;
}
switch (this.report.status) {
case 'Error':
// this.$warning(this.$t('report.generation_error'));
break;
case 'Starting':
this.$warning(this.$t('report.start_status'));
break;
case 'Reporting':
case 'Running':
case 'Completed':
default:
break;
}
},
clearData() {
this.startTime = '0';
this.endTime = '0';
this.minutes = '0';
this.seconds = '0';
},
init() {
this.getReport();
this.getReportView();
},
getReportView() {
if (this.reportId) {
this.$get("/performance/report/test/pro/info/" + this.reportId, response => {
let data = response.data;
if (data) {
this.status = data.status;
this.reportName = data.name;
this.testName = data.testName;
this.projectName = data.projectName;
this.$set(this.report, "id", this.reportId);
this.$set(this.report, "status", data.status);
if (this.status === "Completed") {
this.result = this.$get("/performance/report/content/report_time/" + this.reportId).then(res => {
let data = res.data.data;
if (data) {
this.startTime = data.startTime;
this.endTime = data.endTime;
let duration = data.duration;
this.minutes = Math.floor(duration / 60);
this.seconds = duration % 60;
}
}).catch(() => {
this.clearData();
})
} else {
this.clearData();
}
}
});
}
},
getReport() {
if (this.reportId) {
this.result = this.$get("/performance/report/" + this.reportId, res => {
let data = res.data;
if (data) {
this.status = data.status;
this.$set(this.report, "id", this.reportId);
this.$set(this.report, "status", data.status);
if (this.status === "Completed") {
this.initReportTimeInfo();
}
}
});
}
this.initBreadcrumb();
}
}
}
</script>
<style scoped>
.ms-report-view-btns {
margin-top: 15px;
}
.ms-report-time-desc {
text-align: left;
display: block;
color: #5C7878;
}
</style>