feat(测试跟踪): 测试跟踪内的性能测试报告支持查看前三个请求

This commit is contained in:
song-tianyang 2023-10-12 17:56:39 +08:00 committed by 刘瑞斌
parent cf99cf7f66
commit 6b8e1714c9
26 changed files with 1432 additions and 13 deletions

View File

@ -1,6 +1,7 @@
package io.metersphere.dto;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
@ -13,6 +14,10 @@ public class ResourcePoolOperationInfo {
Map<String, NodeOperationInfo> nodeOperationInfos = new HashMap<>();
public void addNodeOperationInfo(String taskResourceId, String ip, String port, String cpuUsage, int runningTask) {
if(StringUtils.isBlank(cpuUsage)) {
//节点下如果获取不到cpu使用率判断为没有查询到该节点的数据
return;
}
NodeOperationInfo nodeOperationInfo = new NodeOperationInfo();
nodeOperationInfo.setIp(ip);
nodeOperationInfo.setPort(port);

View File

@ -84,7 +84,7 @@ public class PrometheusService {
}
String cpuUsage = null;
int runningTask = 0;
int runningTask = -1;
for (TestResource testResource : testResourcePoolDTO.getResources()) {
String config = testResource.getConfiguration();
@ -97,7 +97,9 @@ public class PrometheusService {
String cpuUsageQL = this.generatePromQL(new String[]{"system_cpu_usage"}, nodeId);
LogUtil.debug(host + "/api/v1/query?query=" + cpuUsageQL);
String cpuUsageDouble = this.runPromQL(headers, host, cpuUsageQL);
cpuUsage = decimalFormat.format(Double.parseDouble(cpuUsageDouble) * 100) + "%";
if(StringUtils.isNotBlank(cpuUsageDouble)){
cpuUsage = decimalFormat.format(Double.parseDouble(cpuUsageDouble) * 100) + "%";
}
}
// 查询任务数

View File

@ -132,6 +132,13 @@ public class ShareController {
return performanceReportService.getReportErrorsTOP5(reportId);
}
@GetMapping("/performance/report/content/errors_samples/{shareId}/{reportId}")
public SamplesRecord getErrorSamples(@PathVariable String shareId, @PathVariable String reportId) {
shareInfoService.validateExpired(shareId);
return performanceReportService.getErrorSamples(reportId);
}
@GetMapping("/performance/report/log/resource/{shareId}/{reportId}")
public List<LogDetailDTO> getResourceIds(@PathVariable String shareId, @PathVariable String reportId) {
shareInfoService.validateExpired(shareId);

View File

@ -44,6 +44,7 @@ public class TestPlanLoadCaseDTO extends TestPlanLoadCaseWithBLOBs {
private List<LogDetailDTO> reportLogResource;
private List<Monitor> reportResource;
private List<MetricData> metricData;
private SamplesRecord errorSamples;
private List<TestResourcePoolDTO> resourcePools;
}
}

View File

@ -605,7 +605,8 @@ public class TestPlanLoadCaseService {
response.setReportErrors(reportErrors);
List<ErrorsTop5> reportErrorsTop5 = performanceReportService.getReportErrorsTOP5(reportId);
response.setReportErrorsTop5(reportErrorsTop5);
SamplesRecord samplesRecord = performanceReportService.getErrorSamples(reportId);
response.setErrorSamples(samplesRecord);
// 日志详情
List<LogDetailDTO> reportLogResource = performanceReportService.getReportLogResource(reportId);
if (org.apache.commons.collections.CollectionUtils.isNotEmpty(reportLogResource)) {

View File

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

View File

@ -121,10 +121,17 @@
:plan-report-template="planReportTemplate"
:share-id="shareId" 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" :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"/>
@ -178,14 +185,22 @@ import MonitorCard from "../../../../business/report/components/MonitorCard";
import MsReportTestDetails from '../../../../business/report/components/TestDetails';
import ProjectEnvironmentDialog from "../../../../business/report/components/ProjectEnvironmentDialog";
import MsTag from "metersphere-frontend/src/components/MsTag";
import {getPerformanceReport, getPerformanceReportTime, getSharePerformanceReport, getSharePerformanceReportTime} from "../../../../api/load-test";
import {
getPerformanceReport,
getPerformanceReportErrorSamples,
getPerformanceReportTime,
getSharePerformanceReport, getSharePerformanceReportErrorSamples,
getSharePerformanceReportTime
} from "../../../../api/load-test";
import MsTestConfiguration from "../../../../business/report/components/TestConfiguration";
import {getTestProInfo, stopTest} from "../../../../api/report";
import SamplesTabs from "@/business/report/components/samples/SamplesTabs.vue";
export default {
name: "LoadCaseReportView",
components: {
SamplesTabs,
MsTestConfiguration,
MonitorCard,
MsPerformanceReportExport,
@ -221,8 +236,10 @@ export default {
websocket: null,
dialogFormVisible: false,
reportExportVisible: false,
haveErrorSamples: false,
testPlan: {testResourcePoolId: null},
show: true,
errorSamples: {},
test: {testResourcePoolId: null},
};
},
@ -482,6 +499,7 @@ export default {
.then(({data}) => {
this.handleInit(data);
});
this.checkSampleResults(this.reportId);
} else {
this.loading = getPerformanceReport(this.reportId)
.then(({data}) => {
@ -489,6 +507,17 @@ export default {
});
}
},
checkSampleResults(reportId) {
getSharePerformanceReportErrorSamples(this.shareId,reportId)
.then(res => {
if (res.data) {
this.errorSamples = res.data;
this.haveErrorSamples = true;
} else {
this.haveErrorSamples = false;
}
});
},
handleInit(data) {
if (data) {
this.allProjectEnvMap = data.projectEnvMap;

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

@ -46,6 +46,7 @@ public class TestPlanLoadCaseDTO extends TestPlanLoadCaseWithBLOBs {
private List<LogDetailDTO> reportLogResource;
private List<Monitor> reportResource;
private List<MetricData> metricData;
private SamplesRecord errorSamples;
private List<TestResourcePoolDTO> resourcePools;
}
}

View File

@ -359,7 +359,10 @@ public class TestPlanReportService {
returnDTO.setUiScenarioIdMap(uiScenarioIdMap);
}
if (runInfoDTO != null && testPlanReport == null) {
if (testPlanReport == null) {
if(runInfoDTO == null){
runInfoDTO = new TestPlanReportRunInfoDTO();
}
if (!saveRequest.isApiCaseIsExecuting() && !saveRequest.isScenarioIsExecuting()) {
//如果没有接口用例以及场景运行执行配置中所选的资源池配置置空避免报告显示资源池时给用户造成困扰;
runModeConfigDTO.setResourcePoolId(null);

View File

@ -11,6 +11,11 @@
:label="item.name"
:disabled="!item.performance"
:value="item.id">
<template v-slot>
<node-operation-label
:nodeName="item.name"
:node-operation-info="nodeInfo(item.id)"/>
</template>
</el-option>
</el-select>
</el-form-item>
@ -253,6 +258,8 @@ import {getQuotaValidResourcePools} from "@/api/remote/resource-pool";
import {testPlanLoadCaseGetLoadConfig} from "@/api/remote/plan/test-plan-load-case";
import {loadTestGetJmxContent} from "@/api/remote/load/performance";
import {findThreadGroup} from "@/business/plan/view/comonents/load/ThreadGroup";
import NodeOperationLabel from "metersphere-frontend/src/components/resource-pool/NodeOperationLabel";
import {getNodeOperationInfo} from "@/api/project";
const HANDLER = "handler";
const THREAD_GROUP_TYPE = "tgType";
@ -287,7 +294,7 @@ const hexToRgb = function (hex) {
export default {
name: "PerformanceLoadConfig",
components: {MsChart},
components: {MsChart,NodeOperationLabel},
props: {
test: {
type: Object
@ -323,6 +330,7 @@ export default {
autoStopDelay: 30,
isReadOnly: false,
rampUpTimeVisible: true,
nodeOperationInfo: {},
};
},
computed: {
@ -356,6 +364,20 @@ export default {
},
},
methods: {
refreshNodeOperation() {
let nodeOperationInfoRequest = {nodeIds: []};
this.resourcePools.forEach(item => {
nodeOperationInfoRequest.nodeIds.push(item.id);
});
getNodeOperationInfo(nodeOperationInfoRequest)
.then(response => {
this.parseNodeOperationStatus(response.data);
});
},
nodeInfo(nodeId) {
return this.nodeOperationInfo[nodeId];
},
getResourcePools() {
this.loading = true;
getQuotaValidResourcePools()
@ -366,9 +388,25 @@ export default {
if (response.data.filter(p => p.id === this.resourcePool).length === 0) {
this.resourcePool = null;
}
let nodeOperationInfoRequest = {nodeIds: []};
this.resourcePools.forEach(item => {
nodeOperationInfoRequest.nodeIds.push(item.id);
});
getNodeOperationInfo(nodeOperationInfoRequest)
.then(response => {
this.parseNodeOperationStatus(response.data);
});
this.resourcePoolChange();
});
},
parseNodeOperationStatus(nodeOperationData) {
this.nodeOperationInfo = {};
nodeOperationData.forEach(item => {
this.nodeOperationInfo[item.id] = item;
});
},
getLoadConfig() {
testPlanLoadCaseGetLoadConfig(this.loadCaseId)
.then((response) => {

View File

@ -93,7 +93,6 @@ export default {
},
methods: {
getReport(row) {
if (this.isTemplate) {
if (row.response) {
this.showResponse = true;

View File

@ -64,7 +64,11 @@
:plan-report-template="planReportTemplate"
:share-id="shareId" 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" :is-share="isShare" :plan-report-template="planReportTemplate"
:share-id="shareId" ref="errorLog"/>
</el-tab-pane>
@ -83,9 +87,7 @@
</el-tab-pane>
</el-tabs>
</div>
</el-card>
<project-environment-dialog ref="projectEnvDialog"></project-environment-dialog>
</el-main>
</ms-container>
@ -104,9 +106,9 @@ import MsReportTestDetails from './TestDetails';
import ProjectEnvironmentDialog from "./ProjectEnvironmentDialog";
import MsTag from "metersphere-frontend/src/components/MsTag";
import MsTestConfiguration from "./TestConfiguration";
import SamplesTabs from "./samples/SamplesTabs";
export default {
export default {
name: "LoadCaseReportView",
components: {
MsTestConfiguration,
@ -120,6 +122,7 @@ export default {
MsMainContainer,
ProjectEnvironmentDialog,
MsTag,
SamplesTabs,
},
data() {
return {
@ -145,6 +148,8 @@ export default {
testPlan: {testResourcePoolId: null},
show: true,
test: {testResourcePoolId: null},
haveErrorSamples: false,
errorSamples: {},
};
},
props: {
@ -163,6 +168,16 @@ export default {
this.init();
}
},
created(){
if (this.planReportTemplate) {
if (this.planReportTemplate.errorSamples) {
this.errorSamples = this.planReportTemplate.errorSamples;
this.haveErrorSamples = true;
} else {
this.haveErrorSamples = false;
}
}
},
computed: {
showProjectEnv() {
return this.projectEnvMap && JSON.stringify(this.projectEnvMap) !== '{}';
@ -253,13 +268,21 @@ export default {
});
},
init() {
alert(1);
this.clearData();
if (this.planReportTemplate) {
this.handleInit(this.planReportTemplate);
}
},
handleInit(data) {
if (data) {
if (data.errorSamples) {
this.errorSamples = data.errorSamples;
this.haveErrorSamples = true;
} else {
this.haveErrorSamples = false;
}
this.allProjectEnvMap = data.projectEnvMap;
this.isProjectEnvShowMore(data.projectEnvMap);
this.status = data.status;

View File

@ -0,0 +1,149 @@
<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="error"
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 "./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];
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('plan.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 "./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('plan.error_samples')" name="errorSample">
<error-samples-table :error-samples="errorSamples"/>
</el-tab-pane>
<el-tab-pane :label="$t('plan.all_samples')" name="allSample">
<error-samples-table :error-samples="samples"/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import ErrorSamplesTable from "./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,314 @@
<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();
}, 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

@ -63,6 +63,9 @@ const message = {
}
},
plan: {
error_samples: 'Error samples',
all_samples: 'All samples',
response_3_samples: 'The first three pieces of data',
batch_delete_tip: "Do you want to continue deleting the test plan?",
relevance_case_success: "Relevance success"
},

View File

@ -63,6 +63,9 @@ const message = {
},
},
plan: {
error_samples: '错误请求',
all_samples: '所有请求',
response_3_samples: '默认抽样前3个请求的响应数据',
batch_delete_tip: "批量删除测试计划,是否继续?",
relevance_case_success: "已添加至测试计划"
},

View File

@ -63,6 +63,9 @@ const message = {
}
},
plan: {
error_samples: '錯誤請求',
all_samples: '所有請求',
response_3_samples: '默認抽樣前3個請求的響應數據',
batch_delete_tip: "批量刪除測試計劃,是否繼續?",
relevance_case_success: "已添加至測試計劃"
},