fix(性能测试): 测试报告中的错误记录支持查看请求返回详情

--story=1013022 --user=宋天阳 【性能测试】测试报告中的错误记录支持查看请求返回详情
https://www.tapd.cn/55049933/s/1422637
This commit is contained in:
song-tianyang 2023-10-07 17:02:25 +08:00 committed by 刘瑞斌
parent 46e7557bef
commit d9c52b4404
19 changed files with 1305 additions and 19 deletions

View File

@ -17,6 +17,7 @@ public enum ReportKeys {
ErrorsChart,
Errors,
ErrorsTop5,
ErrorSamples,
RequestStatistics,
Overview,
TimeInfo,

View File

@ -10,22 +10,19 @@ import io.metersphere.commons.constants.OperLogModule;
import io.metersphere.commons.constants.PermissionConstants;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.dto.LogDetailDTO;
import io.metersphere.dto.ReportDTO;
import io.metersphere.dto.*;
import io.metersphere.log.annotation.MsAuditLog;
import io.metersphere.notice.annotation.SendNotice;
import io.metersphere.dto.*;
import io.metersphere.request.DeleteReportRequest;
import io.metersphere.request.RenameReportRequest;
import io.metersphere.request.ReportRequest;
import io.metersphere.dto.LoadTestExportJmx;
import io.metersphere.service.PerformanceReportService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@RestController
@ -86,6 +83,11 @@ public class PerformanceReportController {
return performanceReportService.getReportErrorsTOP5(reportId);
}
@GetMapping("/content/errors_samples/{reportId}")
public SamplesRecord getErrorSamples(@PathVariable String reportId) {
return performanceReportService.getErrorSamples(reportId);
}
@GetMapping("/content/testoverview/{reportId}")
public TestOverview getTestOverview(@PathVariable String reportId) {
return performanceReportService.getTestOverview(reportId);

View File

@ -0,0 +1,13 @@
package io.metersphere.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class SamplesRecord {
//请求名称 - 错误类型 - 错误请求
private Map<String, Map<String, List<RequestResult>>> samples;
private Map<String, Map<String, Long>> sampleCount;
}

View File

@ -2,21 +2,18 @@ package io.metersphere.service;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtFileContentMapper;
import io.metersphere.base.mapper.ext.ExtLoadTestReportMapper;
import io.metersphere.base.mapper.ext.ExtTestPlanLoadCaseMapper;
import io.metersphere.commons.constants.PerformanceTestStatus;
import io.metersphere.commons.constants.ReportKeys;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.JSON;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.dto.*;
import io.metersphere.engine.Engine;
import io.metersphere.engine.EngineFactory;
import io.metersphere.environment.service.BaseEnvironmentService;
import io.metersphere.i18n.Translator;
import io.metersphere.log.utils.ReflexObjectUtil;
import io.metersphere.log.vo.DetailColumn;
import io.metersphere.log.vo.OperatingLogDetails;
@ -27,20 +24,17 @@ import io.metersphere.request.DeleteReportRequest;
import io.metersphere.request.OrderRequest;
import io.metersphere.request.RenameReportRequest;
import io.metersphere.request.ReportRequest;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
@ -273,6 +267,14 @@ public class PerformanceReportService {
return JSON.parseArray(content, ErrorsTop5.class);
}
public SamplesRecord getErrorSamples(String id) {
String content = getContent(id, ReportKeys.ErrorSamples);
if (StringUtils.isEmpty(content)) {
return null;
}
return JSON.parseObject(content, SamplesRecord.class);
}
public TestOverview getTestOverview(String id) {
if (isReportError(id)) {
return new TestOverview();

View File

@ -108,6 +108,10 @@ export function getPerformanceReportErrorsTop5(reportId) {
return get('/performance/report/content/errors_top5/' + reportId);
}
export function getPerformanceReportErrorSamples(reportId) {
return get('/performance/report/content/errors_samples/' + reportId);
}
export function getSharePerformanceReportErrorsTop5(shareId, reportId) {
return get('/share/performance/report/content/errors_top5/' + shareId + '/' + reportId);
}

View File

@ -102,9 +102,15 @@
<el-tab-pane :label="$t('report.test_request_statistics')">
<ms-report-request-statistics :report="report" ref="requestStatistics"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_error_log')">
<el-tab-pane v-if="haveErrorSamples" :label="$t('report.test_error_log')">
<samples-tabs :samples="errorSamples" ref="errorSamples"/>
</el-tab-pane>
<el-tab-pane v-else :label="$t('report.test_error_log')">
<ms-report-error-log :report="report" ref="errorLog"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_log_details')">
<ms-report-log-details :report="report"/>
</el-tab-pane>
@ -157,6 +163,7 @@ import MonitorCard from "./components/MonitorCard";
import MsTestConfiguration from "./components/TestConfiguration";
import {generateShareInfoWithExpired, getShareRedirectUrl} from "@/api/share";
import ProjectEnvironmentDialog from "./components/ProjectEnvironmentDialog";
import SamplesTabs from "@/business/report/components/samples/SamplesTabs.vue";
import {
downloadZip,
getProjectApplication,
@ -167,6 +174,7 @@ import {
stopTest
} from "@/api/report";
import {getTest, runTest} from "@/api/performance";
import {getPerformanceReportErrorSamples} from "@/api/load-test";
export default {
@ -184,7 +192,8 @@ export default {
MsMainContainer,
MsReportTestDetails,
MsTag,
ProjectEnvironmentDialog
ProjectEnvironmentDialog,
SamplesTabs
},
props: {},
inject: [
@ -229,6 +238,8 @@ export default {
],
testDeleted: false,
shareUrl: "",
haveErrorSamples: false,
errorSamples: {},
application: {}
};
},
@ -461,6 +472,17 @@ export default {
}
}
},
checkSampleResults(reportId) {
getPerformanceReportErrorSamples(reportId)
.then(res => {
if (res.data) {
this.errorSamples = res.data;
this.haveErrorSamples = true;
} else {
this.haveErrorSamples = false;
}
});
},
getReport(reportId) {
this.loading = getReport(reportId)
.then(res => {
@ -523,6 +545,7 @@ export default {
this.reportId = this.perReportId;
}
this.getReport(this.reportId);
this.checkSampleResults(this.reportId);
this.$EventBus.$on('projectChange', this.handleProjectChange);
this.getProjectApplication();
},

View File

@ -0,0 +1,150 @@
<template>
<div>
<el-table
ref="samplesTable"
:data="tableData"
border
header-cell-class-name="sample-table-header"
style="width: 100%">
<el-table-column
prop="name"
label="Sample"
min-width="180">
</el-table-column>
<el-table-column
prop="count"
label="Samples"
min-width="180">
</el-table-column>
<el-table-column
prop="errors"
label="Errors"
min-width="180">
</el-table-column>
<el-table-column
prop="percentOfErrors"
label="% In Errors"
min-width="180">
</el-table-column>
<el-table-column
prop="percentOfSamples"
label="% In Samples"
min-width="180">
</el-table-column>
<el-table-column
prop="code"
label="Response Code"
min-width="180">
<template v-slot:default="scope">
<span v-if="scope.row.code === '200' " style="color: #44b349">{{ scope.row.code }}</span>
<span v-else style="color: #E6113C">{{ scope.row.code }}</span>
</template>
</el-table-column>
<el-table-column
:label="$t('commons.operating')"
min-width="180">
<template v-slot="scope">
<el-link @click="openRecord(scope.row)">{{ $t('operating_log.info') }}</el-link>
</template>
</el-table-column>
</el-table>
<samples-drawer ref="sampleDrawer" :samples="samplesRecord"/>
</div>
</template>
<script>
import SamplesDrawer from "@/business/report/components/samples/SamplesDrawer.vue";
export default {
name: "ErrorSamplesTable",
components: {SamplesDrawer},
data() {
return {
id: '',
drawer: false,
tableData: [],
samplesRecord: [],
sampleRows: {},
};
},
comments: {
SamplesDrawer
},
props: ['errorSamples'],
created() {
this.initTableData();
},
methods: {
initTableData() {
if (this.errorSamples && this.errorSamples.sampleCount) {
let allSampleCount = 0;
let errorCount = 0;
for (let sampleName in this.errorSamples.sampleCount) {
let sampleCountObj = this.errorSamples.sampleCount[sampleName];
let index = 0;
for (let code in sampleCountObj) {
let codeCount = sampleCountObj[code];
let sampleTableData = {};
sampleTableData.name = sampleName;
sampleTableData.code = code;
sampleTableData.count = codeCount;
this.tableData.push(sampleTableData);
index++;
if (code !== '200') {
errorCount += codeCount;
sampleTableData.error = codeCount;
} else {
sampleTableData.error = 0;
}
allSampleCount += codeCount;
}
this.sampleRows[sampleName] = index;
}
this.tableData.forEach(item => {
item.percentOfErrors = (item.error / errorCount * 100).toFixed(2) + '%';
item.percentOfSamples = (item.count / allSampleCount * 100).toFixed(2) + '%';
});
} else {
this.tableData = [];
}
this.$nextTick(() => {
this.$refs.samplesTable.doLayout();
}, 500)
},
objectSpanMethod({row, column, rowIndex, columnIndex}) {
if (columnIndex === 0) {
let rowspan = this.sampleRows[row.name];
if (rowspan != 0) {
this.sampleRows[row.name] = 0;
return {
rowspan: rowspan,
colspan: 1,
};
} else {
return {
rowspan: 0,
colspan: 1,
};
}
}
},
handleClose(done) {
done();
},
openRecord(row) {
let drawerSamples = this.errorSamples.samples[row.name][row.code];
console.info(drawerSamples);
this.$refs.sampleDrawer.openRecord(drawerSamples);
},
},
};
</script>
<style scoped>
.el-table :deep(.sample-table-header) {
color: #1a1a1a;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div>
<el-drawer
:visible.sync="drawer"
direction="rtl"
custom-class="sample-drawer"
:size="820"
:before-close="handleClose">
<template v-slot:title>
<span style="color: #1a1a1a; font-size: large;">
{{ $t('performance_test.response_3_samples') }}
</span>
</template>
<div style="margin: 0 10px 0 10px ">
<el-collapse v-model="activeName" accordion>
<el-collapse-item
v-for="(sample, index) in sampleRecord" :key="index" :name="index">
<template v-slot:title>
<div style="font-size: 16px;color: #783887">
<span> {{ sample.name }}</span>
</div>
</template>
<request-result-tail :report-id="sample.id" :response="sample" ref="debugResult"/>
</el-collapse-item>
</el-collapse>
</div>
</el-drawer>
</div>
</template>
<script>
import RequestResultTail from "@/business/report/components/samples/compnent/RequestResultTail";
import {datetimeFormat} from "fit2cloud-ui/src/filters/time";
export default {
name: "ErrorSamplesTable",
components: {RequestResultTail},
data() {
return {
activeName: '1',
sampleRecord: [],
drawer: false,
};
},
props: ['samples'],
created() {
},
methods: {
datetimeFormat,
handleClose(done) {
done();
},
openRecord(samples) {
this.sampleRecord = [];
samples.forEach(sample => {
this.sampleRecord.push(sample);
});
this.drawer = true;
},
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,68 @@
<template>
<div style=" height:calc(100vh - 190px)">
<el-tabs v-model="activeName" type="card" class="sample-tabs">
<el-tab-pane :label="$t('performance_test.error_samples')" name="errorSample">
<error-samples-table :error-samples="errorSamples"/>
</el-tab-pane>
<el-tab-pane :label="$t('performance_test.all_samples')" name="allSample">
<error-samples-table :error-samples="samples"/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import ErrorSamplesTable from "@/business/report/components/samples/ErrorSamplesTable.vue";
export default {
name: "SamplesTabs",
components: {ErrorSamplesTable},
data() {
return {
activeName: 'errorSample',
errorSamples: {
sampleCount: {},
samples: {}
},
};
},
comments: {
ErrorSamplesTable
},
props: ['samples'],
created() {
this.initErrorSamples();
},
activated() {
this.initErrorSamples();
},
methods: {
initErrorSamples() {
this.errorSamples = {
sampleCount: {},
samples: {}
};
if (this.samples && this.samples.sampleCount) {
for (let sampleName in this.samples.sampleCount) {
let sampleCountObj = this.samples.sampleCount[sampleName];
for (let code in sampleCountObj) {
if (code !== '200') {
if (!this.errorSamples.sampleCount[sampleName]) {
this.errorSamples.sampleCount[sampleName] = {};
this.errorSamples.samples[sampleName] = {};
}
this.errorSamples.sampleCount[sampleName][code] = this.samples.sampleCount[sampleName][code] || {};
this.errorSamples.samples[sampleName][code] = this.samples.samples[sampleName][code] || [];
}
}
}
}
},
},
};
</script>
<style scoped>
.sample-tabs :deep(.el-tabs__nav) {
float: right;
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div>
<el-table :data="assertions" :row-style="getRowStyle" :header-cell-style="getRowStyle">
<el-table-column prop="name" :label="$t('api_report.assertions_name')" width="150" show-overflow-tooltip>
<template v-slot:default="scope">
<span>{{ !scope.row.name || scope.row.name === 'null' ? '' : scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column
prop="content"
v-if="showContent"
:label="$t('api_report.assertions_content')"
width="300"
show-overflow-tooltip />
<el-table-column prop="message" :label="$t('api_report.assertions_error_message')" />
<el-table-column prop="pass" :label="$t('api_report.assertions_is_success')" width="180">
<template v-slot:default="{ row }">
<el-tag size="mini" type="success" v-if="row.pass"> Success </el-tag>
<el-tag size="mini" type="danger" v-else> Error </el-tag>
</template>
</el-table-column>
<el-table-column prop="script">
<template v-slot:default="{ row }">
<div class="assertion-item btn circle" v-if="row.script">
<i
class="el-icon-view el-button el-button--primary el-button--mini is-circle"
circle
@click="showPage(row.script)" />
</div>
</template>
</el-table-column>
</el-table>
<el-dialog
:title="$t('api_test.request.assertions.script')"
:visible.sync="visible"
width="900px"
modal-append-to-body
append-to-body>
<el-row type="flex" justify="space-between" align="middle" class="quick-script-block">
<el-col :span="codeSpan" class="script-content">
<ms-code-edit
v-if="isCodeEditAlive"
:read-only="disabled"
:data.sync="scriptContent"
theme="eclipse"
:modes="['java', 'python']"
ref="codeEdit" />
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script>
import MsCodeEdit from 'metersphere-frontend/src/components/MsCodeEdit';
export default {
name: 'MsAssertionResults',
components: { MsCodeEdit },
props: {
assertions: Array,
showContent: {
type: Boolean,
default: true,
},
},
data() {
return {
visible: false,
disabled: false,
codeSpan: 20,
isCodeEditAlive: true,
scriptContent: '',
};
},
methods: {
getRowStyle() {
return { backgroundColor: '#F5F5F5' };
},
showPage(script) {
this.disabled = true;
this.visible = true;
this.scriptContent = script;
this.reload();
},
reload() {
this.isCodeEditAlive = false;
this.$nextTick(() => (this.isCodeEditAlive = true));
},
},
};
</script>
<style scoped>
.assertion-item.btn.circle {
text-align: right;
min-width: 80px;
}
.script-content {
height: calc(100vh - 570px);
min-height: 440px;
}
.quick-script-block {
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<editor
v-model="formatData"
:lang="mode"
@init="editorInit"
:theme="theme"
:height="height"
:key="readOnly"
ref="msEditor" />
</template>
<script>
import { formatJson, formatXml } from 'metersphere-frontend/src/utils/format-utils';
import toDiffableHtml from 'diffable-html';
import editor from 'vue2-ace-editor';
import 'brace/ext/language_tools'; //language extension prerequisite...
import 'brace/mode/text';
import 'brace/mode/json';
import 'brace/mode/xml';
import 'brace/mode/html';
import 'brace/mode/java';
import 'brace/mode/python';
import 'brace/mode/sql';
import 'brace/mode/javascript';
import 'brace/mode/yaml';
import 'brace/theme/chrome';
import 'brace/theme/eclipse';
import 'brace/snippets/javascript'; //snippet
export default {
name: 'MsCodeEdit',
components: { editor },
data() {
return {
formatData: '',
};
},
props: {
height: [String, Number],
data: {
type: String,
default() {
return '';
},
},
theme: {
type: String,
default() {
return 'chrome';
},
},
init: {
type: Function,
},
readOnly: {
type: Boolean,
default() {
return false;
},
},
mode: {
type: String,
default() {
return 'text';
},
},
modes: {
type: Array,
default() {
return ['text', 'json', 'xml', 'html'];
},
},
},
mounted() {
this.format();
},
watch: {
formatData() {
this.$emit('update:data', this.formatData);
},
mode() {
this.format();
},
data() {
this.formatData = this.data;
},
},
methods: {
insert(code) {
if (this.$refs.msEditor.editor) {
this.$refs.msEditor.editor.insert(code);
}
},
editorInit: function (editor) {
// require('brace/ext/language_tools'); //language extension prerequisite...
// require('brace/mode/text');
// require('brace/mode/json');
// require('brace/mode/xml');
// require('brace/mode/html');
// require('brace/mode/java');
// require('brace/mode/python');
// require('brace/mode/sql');
// require('brace/mode/javascript');
// require('brace/mode/yaml');
// // this.modes.forEach((mode) => {
// // require('brace/mode/' + mode); //language
// // });
// require('brace/theme/' + this.theme);
// require('brace/snippets/javascript'); //snippet
if (this.readOnly) {
editor.setReadOnly(true);
}
if (this.init) {
this.init(editor);
}
},
format() {
switch (this.mode) {
case 'json':
this.formatData = formatJson(this.data);
break;
case 'html':
this.formatData = toDiffableHtml(this.data);
break;
case 'xml':
this.formatData = formatXml(this.data);
break;
default:
if (this.data) {
this.formatData = this.data;
}
}
},
},
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,58 @@
<template>
<el-dropdown @command="handleCommand" class="ms-dropdown">
<slot>
<span class="el-dropdown-link">
{{ currentCommand }}
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
</slot>
<el-dropdown-menu slot="dropdown" chang>
<el-dropdown-item v-for="(command, index) in commands" :key="index" :command="command">
{{ command }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
name: 'MsDropdown',
data() {
return {
currentCommand: '',
};
},
props: {
commands: {
type: Array,
},
defaultCommand: {
type: String,
},
},
created() {
if (this.defaultCommand) {
this.currentCommand = this.defaultCommand;
} else if (this.commands && this.commands.length > 0) {
this.currentCommand = this.commands[0];
}
},
methods: {
handleCommand(command) {
this.currentCommand = command;
this.$emit('command', command);
},
},
};
</script>
<style scoped>
.el-dropdown-link {
cursor: pointer;
color: #409eff;
}
.el-icon-arrow-down {
font-size: 12px;
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<div class="metric-container">
<el-row type="flex">
<el-col>
<div style="font-size: 14px; color: #aaaaaa; float: left">{{ $t('api_report.response_code') }} :</div>
<el-tooltip v-if="responseResult.responseCode" :content="responseResult.responseCode" placement="top">
<div
v-if="
response.attachInfoMap &&
response.attachInfoMap.FAKE_ERROR &&
response.attachInfoMap.status === 'FAKE_ERROR'
"
class="node-title"
:class="'ms-req-error-report-result'">
{{ responseResult && responseResult.responseCode ? responseResult.responseCode : '0' }}
</div>
<div v-else class="node-title" :class="response && response.success ? 'ms-req-success' : 'ms-req-error'">
{{ responseResult && responseResult.responseCode ? responseResult.responseCode : '0' }}
</div>
</el-tooltip>
<div v-else class="node-title" :class="response && response.success ? 'ms-req-success' : 'ms-req-error'">
{{ responseResult && responseResult.responseCode ? responseResult.responseCode : '0' }}
</div>
<div v-if="response && response.attachInfoMap && response.attachInfoMap.FAKE_ERROR">
<div
class="node-title ms-req-error-report-result"
v-if="response.attachInfoMap.status === 'FAKE_ERROR'"
style="margin-left: 0px; padding-left: 0px">
{{ response.attachInfoMap.FAKE_ERROR }}
</div>
<div
class="node-title ms-req-success"
v-else-if="response.success"
style="margin-left: 0px; padding-left: 0px">
{{ response.attachInfoMap.FAKE_ERROR }}
</div>
<div class="node-title ms-req-error" v-else style="margin-left: 0px; padding-left: 0px">
{{ response.attachInfoMap.FAKE_ERROR }}
</div>
</div>
</el-col>
<el-col>
<div style="font-size: 14px; color: #aaaaaa; float: left">{{ $t('api_report.response_time') }} :</div>
<div style="font-size: 14px; color: #61c550; margin-top: 2px; margin-left: 10px; float: left">
{{ responseResult && responseResult.responseTime ? responseResult.responseTime : 0 }}
ms
</div>
</el-col>
<el-col>
<div style="font-size: 14px; color: #aaaaaa; float: left">{{ $t('api_report.response_size') }} :</div>
<div style="font-size: 14px; color: #61c550; margin-top: 2px; margin-left: 10px; float: left">
{{ responseResult && responseResult.responseSize ? responseResult.responseSize : 0 }}
bytes
</div>
</el-col>
</el-row>
<el-row type="flex" style="margin-top: 5px">
<el-col v-if="response && response.poolName">
<div style="font-size: 14px; color: #aaaaaa; float: left">
<span> {{ $t('load_test.select_resource_pool') + ':' }} </span>
</div>
<div style="font-size: 14px; color: #61c550; margin-left: 10px; float: left">
{{ response.poolName }}
</div>
</el-col>
<el-col type="flex" v-if="response && response.envName">
<div style="font-size: 14px; color: #aaaaaa; float: left">
<span> {{ $t('commons.environment') + ':' }} </span>
</div>
<div style="font-size: 14px; color: #61c550; margin-left: 10px; float: left">
{{ response.envName }}
</div>
</el-col>
<el-col></el-col>
</el-row>
</div>
</template>
<script>
export default {
name: 'MsRequestMetric',
props: {
response: {
type: Object,
default() {
return {};
},
},
},
computed: {
responseResult() {
return this.response && this.response.responseResult ? this.response.responseResult : {};
},
error() {
return this.response && this.response.responseCode && this.response.responseCode >= 400;
},
},
};
</script>
<style scoped>
.metric-container {
padding-bottom: 10px;
}
.node-title {
/*width: 150px;*/
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0px 5px;
overflow: hidden;
font-size: 14px;
color: #61c550;
margin-top: 2px;
margin-left: 10px;
margin-right: 10px;
float: left;
}
.ms-req-error {
color: #f56c6c;
}
.ms-req-error-report-result {
color: #f6972a;
}
.ms-req-success {
color: #67c23a;
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<div>
<div style="background-color: #fafafa;margin: 0 0 5px 0">
<el-tag
size="medium"
:style="{
'background-color': getColor(true, response.method),
border: getColor(true, response.method),
borderRadius: '0px',
marginRight: '20px',
color: 'white',
}">
{{ response.method }}
</el-tag>
{{ response.url }}
</div>
<div class="request-result">
<ms-request-metric v-if="showMetric" :response="response"/>
<ms-response-result
:currentProtocol="currentProtocol"
:response="response"
:isTestPlan="isTestPlan"/>
</div>
</div>
</template>
<script>
import MsResponseResult from './ResponseResult';
import MsRequestMetric from './RequestMetric';
export default {
name: 'MsRequestResultTail',
components: {MsRequestMetric, MsResponseResult},
props: {
response: Object,
currentProtocol: String,
reportId: String,
showMetric: {
type: Boolean,
default() {
return true;
},
},
isTestPlan: {
type: Boolean,
default() {
return false;
},
},
},
data() {
return {
loading: false,
report: {},
apiMethodColor: {
'GET': '#61AFFE',
'POST': '#49CC90',
'PUT': '#fca130',
'PATCH': '#E2EE11',
'DELETE': '#f93e3d',
'OPTIONS': '#0EF5DA',
'HEAD': '#8E58E7',
'CONNECT': '#90AFAE',
'DUBBO': '#C36EEF',
'dubbo://': '#C36EEF',
'SQL': '#0AEAD4',
'TCP': '#0A52DF',
},
};
},
methods: {
getColor(enable, method) {
return this.apiMethodColor[method];
},
},
};
</script>
<style scoped>
.request-result {
width: 100%;
min-height: 40px;
padding: 2px 0;
}
.request-result .info {
background-color: #f9f9f9;
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1e90ff;
font-size: 14px;
font-weight: 500;
line-height: 40px;
padding-left: 5px;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #fff;
}
.sub-result .method {
border-left: 5px solid #1e90ff;
padding-left: 20px;
}
.sub-result:last-child {
border-bottom: 1px solid #ebeef5;
}
.request-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,260 @@
<template>
<div class="text-container" v-if="responseResult">
<el-tabs v-model="activeName" v-show="isActive" class="response-result">
<el-tab-pane :label="$t('api_test.definition.request.response_body')" name="body" class="pane">
<ms-sql-result-table v-if="isSqlType && activeName === 'body'" :body="responseResult.body"/>
<ms-code-edit
v-if="!isSqlType && isMsCodeEditShow && activeName === 'body'"
:mode="mode"
:read-only="true"
:modes="modes"
:data.sync="responseResult.body"
height="250px"
ref="codeEdit"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.response_header')" name="headers" class="pane">
<ms-code-edit
:mode="'text'"
:read-only="true"
:data.sync="responseResult.headers"
ref="codeEdit"
v-if="activeName === 'headers'"/>
</el-tab-pane>
<el-tab-pane v-if="responseResult.console" :label="$t('api_test.definition.request.console')" name="console"
class="pane">
<ms-code-edit
:mode="'text'"
:read-only="true"
:data.sync="responseResult.console"
ref="codeEdit"
v-if="activeName === 'console'"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.assertions')" name="assertions" class="pane assertions">
<ms-assertion-results :assertions="responseResult.assertions" v-if="activeName === 'assertions'"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract.label')" name="label" class="pane">
<ms-code-edit
:mode="'text'"
:read-only="true"
:data.sync="responseResult.vars"
v-if="activeName === 'label'"
ref="codeEdit"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.request_body')" name="request_body" class="pane">
<ms-code-edit
:mode="'text'"
:read-only="true"
:data.sync="reqMessages"
v-if="activeName === 'request_body'"
ref="codeEdit"/>
</el-tab-pane>
<el-tab-pane v-if="activeName == 'body'" :disabled="true" name="mode" class="pane cookie">
<template v-slot:label>
<ms-dropdown
v-if="currentProtocol === 'SQL'"
:commands="sqlModes"
:default-command="mode"
@command="sqlModeChange"/>
<ms-dropdown v-else :commands="modes" :default-command="mode" @command="modeChange" ref="modeDropdown"/>
</template>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import MsAssertionResults from './AssertionResults';
import MsCodeEdit from './MsCodeEdit';
import MsDropdown from './MsDropdown';
import MsSqlResultTable from './SqlResultTable';
export default {
name: 'MsResponseResult',
components: {
MsDropdown,
MsCodeEdit,
MsAssertionResults,
MsSqlResultTable,
},
props: {
response: Object,
currentProtocol: String,
},
data() {
return {
isActive: true,
activeName: 'body',
modes: ['text', 'json', 'xml', 'html'],
sqlModes: ['text', 'table'],
bodyFormat: {
TEXT: 'text',
JSON: 'json',
XML: 'xml',
HTML: 'html',
},
mode: 'text',
isMsCodeEditShow: true,
reqMessages: '',
};
},
watch: {
response() {
this.setBodyType();
this.setReqMessage();
},
activeName: {
handler() {
setTimeout(() => {
// 300ms 使
this.$refs.codeEdit?.$el.querySelector('.ace_text-input')?.focus();
this.$refs.codeEdit?.$parent?.$parent?.$parent?.$parent?.$parent?.$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
}, 300);
},
immediate: true,
},
},
methods: {
modeChange(mode) {
this.mode = mode;
},
sqlModeChange(mode) {
this.mode = mode;
},
setBodyType() {
if (
this.response &&
this.response.responseResult &&
this.response.responseResult.headers &&
this.response.responseResult.headers.indexOf('Content-Type: application/json') > 0
) {
this.mode = this.bodyFormat.JSON;
this.$nextTick(() => {
if (this.$refs.modeDropdown) {
this.$refs.modeDropdown.handleCommand(this.bodyFormat.JSON);
this.msCodeReload();
}
});
}
},
msCodeReload() {
this.isMsCodeEditShow = false;
this.$nextTick(() => {
this.isMsCodeEditShow = true;
});
},
setReqMessage() {
if (this.response) {
if (!this.response.url) {
this.response.url = '';
}
if (!this.response.headers) {
this.response.headers = '';
}
if (!this.response.cookies) {
this.response.cookies = '';
}
if (!this.response.body) {
this.response.body = '';
}
if (!this.response.responseResult) {
this.response.responseResult = {};
}
if (!this.response.responseResult.vars) {
this.response.responseResult.vars = '';
}
this.reqMessages = '';
if (this.response.url) {
this.reqMessages += this.$t('api_test.request.address') + ':\n' + this.response.url + '\n';
}
if (this.response.headers) {
this.reqMessages += this.$t('api_test.scenario.headers') + ':\n' + this.response.headers + '\n';
}
if (this.response.cookies) {
this.reqMessages += 'Cookie:' + this.response.cookies + '\n';
}
this.reqMessages += 'Body:' + '\n' + this.response.body;
if (this.mode === this.bodyFormat.JSON) {
this.msCodeReload();
}
}
},
},
mounted() {
this.setBodyType();
this.setReqMessage();
},
computed: {
isSqlType() {
return (
this.currentProtocol === 'SQL' &&
this.response &&
this.response.responseResult &&
this.response.responseResult.responseCode === '200' &&
this.mode === 'table'
);
},
responseResult() {
return this.response && this.response.responseResult ? this.response.responseResult : {};
},
},
};
</script>
<style scoped>
.text-container .icon {
padding: 5px;
}
.text-container .collapse {
cursor: pointer;
}
.text-container .collapse:hover {
opacity: 0.8;
}
.text-container .icon.is-active {
transform: rotate(90deg);
}
.text-container .pane {
background-color: #f5f5f5;
padding: 1px 0;
height: 250px;
overflow-y: auto;
}
.text-container .pane.cookie {
padding: 0;
}
:deep(.el-tabs__nav-wrap::after) {
height: 0px;
}
.ms-div {
margin-top: 20px;
}
pre {
margin: 0;
}
.response-result :deep(.el-tabs__nav) {
float: left !important;
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<div>
<el-table
v-for="(table, index) in tables"
:key="index"
:data="table.tableData"
border
size="mini"
highlight-current-row>
<el-table-column v-for="(title, index) in table.titles" :key="index" :label="title" min-width="150px">
<template v-slot:default="scope">
<el-popover placement="top" trigger="click">
<el-container>
<div>{{ scope.row[title] }}</div>
</el-container>
<span class="table-content" slot="reference">{{ scope.row[title] }}</span>
</el-popover>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'MsSqlResultTable',
data() {
return {
tables: [],
titles: [],
};
},
props: {
body: String,
},
created() {
this.init();
},
watch: {
body() {
this.init();
},
},
methods: {
init() {
if (!this.body) {
return;
}
this.tables = [];
this.titles = [];
let rowArray = this.body.split('\n');
//
if (rowArray.length > 100) {
rowArray = rowArray.slice(0, 100);
}
this.getTableData(rowArray);
},
getTableData(rowArray) {
let titles;
let result = [];
for (let i = 0; i < rowArray.length; i++) {
let colArray = rowArray[i].split('\t');
if (i === 0) {
titles = colArray;
} else {
if (colArray.length != titles.length) {
//
if (colArray.length === 1 && colArray[0] === '') {
this.getTableData(rowArray.slice(i + 1));
} else {
this.getTableData(rowArray.slice(i));
}
break;
} else {
let item = {};
for (let j = 0; j < colArray.length; j++) {
item[titles[j]] = colArray[j] ? colArray[j] : '';
}
//
if (result.length < 100) {
result.push(item);
}
}
}
}
this.tables.splice(0, 0, {
titles: titles,
tableData: result,
});
},
},
};
</script>
<style scoped>
.el-table {
margin-bottom: 20px;
}
.el-table :deep(.cell) {
white-space: nowrap;
}
.table-content {
cursor: pointer;
}
.el-container {
overflow: auto;
max-height: 500px;
}
</style>

View File

@ -11,7 +11,10 @@ const message = {
max_current_threads_tips: 'Exceeded the maximum concurrent number of this node {0}',
sync_scenario_no_permission_tips: 'No permission to create the scenario cannot perform synchronization',
basic_config_file_limit_tip: 'Note: The maximum number of resource files is limited to 10',
edit_performance_test_tips: 'No permission to edit test, please check it before operation'
edit_performance_test_tips: 'No permission to edit test, please check it before operation',
error_samples: 'Error samples',
all_samples: 'All samples',
response_3_samples: 'The first three pieces of data',
}
}
export default {

View File

@ -11,7 +11,10 @@ const message = {
max_current_threads_tips: '超出此节点{0}最大并发数',
sync_scenario_no_permission_tips: '没有创建接口的权限无法执行同步',
basic_config_file_limit_tip: '注资源文件数最大限制为10个',
edit_performance_test_tips: '没有编辑性能测试的权限,请勾选后再操作'
edit_performance_test_tips: '没有编辑性能测试的权限,请勾选后再操作',
error_samples: '错误请求',
all_samples: '所有请求',
response_3_samples: '默认抽样前3个请求的响应数据',
}
}

View File

@ -11,7 +11,10 @@ const message = {
max_current_threads_tips: '超出此節點{0}最大並發數',
sync_scenario_no_permission_tips: '沒有创建接口的權限無法執行同步',
basic_config_file_limit_tip: '注資源文件數最大限制為10個',
edit_performance_test_tips: '沒有編輯性能測試的權限,請勾選後再操作'
edit_performance_test_tips: '沒有編輯性能測試的權限,請勾選後再操作',
error_samples: '錯誤請求',
all_samples: '所有請求',
response_3_samples: '默认抽样前3个请求的响应数据',
}
}