This commit is contained in:
chenjianxing 2020-05-09 19:03:35 +08:00
commit e50241158f
33 changed files with 1209 additions and 528 deletions

View File

@ -43,9 +43,9 @@ public class APIReportController {
return PageUtils.setPageInfo(page, apiReportService.list(request));
}
@GetMapping("/get/{testId}")
public ApiTestReport get(@PathVariable String testId) {
return apiReportService.get(testId);
@GetMapping("/get/{reportId}")
public APIReportResult get(@PathVariable String reportId) {
return apiReportService.get(reportId);
}
@PostMapping("/delete")

View File

@ -9,5 +9,7 @@ import lombok.Setter;
@Getter
public class APIReportResult extends ApiTestReport {
private String testName;
private String projectName;
}

View File

@ -93,7 +93,8 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
}
RequestResult requestResult = getRequestResult(result);
scenarioResult.getRequestResult().add(requestResult);
scenarioResult.getRequestResults().add(requestResult);
scenarioResult.addResponseTime(result.getTime());
testResult.addPassAssertions(requestResult.getPassAssertions());
testResult.addTotalAssertions(requestResult.getTotalAssertions());
@ -110,14 +111,18 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
}
private RequestResult getRequestResult(SampleResult result) {
String body = result.getSamplerData();
String method = StringUtils.substringBefore(body, " ");
RequestResult requestResult = new RequestResult();
requestResult.setName(result.getSampleLabel());
requestResult.setUrl(result.getUrlAsString());
requestResult.setSuccess(result.isSuccessful());
requestResult.setBody(result.getSamplerData());
requestResult.setMethod(method);
requestResult.setBody(body);
requestResult.setHeaders(result.getRequestHeaders());
requestResult.setRequestSize(result.getSentBytes());
requestResult.setTotalAssertions(result.getAssertionResults().length);
requestResult.setSuccess(result.isSuccessful());
ResponseResult responseResult = requestResult.getResponseResult();
responseResult.setBody(result.getResponseDataAsString());

View File

@ -1,21 +1,31 @@
package io.metersphere.api.jmeter;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.config.JmeterProperties;
import io.metersphere.i18n.Translator;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.HashTree;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.lang.reflect.Field;
import javax.annotation.Resource;
@Service
public class JMeterService {
@Resource
private JmeterProperties jmeterProperties;
public void run(InputStream is) {
JMeterUtils.loadJMeterProperties("/Users/q4speed/Downloads/apache-jmeter-5.2.1/bin/jmeter.properties");
JMeterUtils.setJMeterHome("/Users/q4speed/Downloads/apache-jmeter-5.2.1");
String JMETER_HOME = jmeterProperties.getHome();
String JMETER_PROPERTIES = JMETER_HOME + "/bin/jmeter.properties";
JMeterUtils.loadJMeterProperties(JMETER_PROPERTIES);
JMeterUtils.setJMeterHome(JMETER_HOME);
try {
Object scriptWrapper = SaveService.loadElement(is);
HashTree testPlan = getHashTree(scriptWrapper);

View File

@ -9,6 +9,8 @@ public class RequestResult {
private String url;
private String method;
private long requestSize;
private boolean success;

View File

@ -22,7 +22,11 @@ public class ScenarioResult {
private int passAssertions = 0;
private final List<RequestResult> requestResult = new ArrayList<>();
private final List<RequestResult> requestResults = new ArrayList<>();
public void addResponseTime(long time) {
this.responseTime += time;
}
public void addError() {
this.error++;

View File

@ -38,8 +38,8 @@ public class APIReportService {
return extApiTestReportMapper.list(request);
}
public ApiTestReport get(String id) {
return apiTestReportMapper.selectByPrimaryKey(id);
public APIReportResult get(String reportId) {
return extApiTestReportMapper.get(reportId);
}
public List<APIReportResult> listByTestId(String testId) {

View File

@ -13,4 +13,6 @@ public interface ExtApiTestReportMapper {
List<APIReportResult> listByTestId(@Param("testId") String testId);
APIReportResult get(@Param("id") String id);
}

View File

@ -4,12 +4,13 @@
<resultMap id="BaseResultMap" type="io.metersphere.api.dto.APIReportResult"
extends="io.metersphere.base.mapper.ApiTestReportMapper.BaseResultMap">
<result column="test_name" property="testName"/>
<result column="project_name" property="projectName"/>
</resultMap>
<select id="list" resultMap="BaseResultMap">
SELECT t.name, t.description,
r.id, r.test_id, r.create_time, r.update_time, r.status,
SELECT t.name AS test_name,
r.name, r.description, r.id, r.test_id, r.create_time, r.update_time, r.status,
project.name AS project_name
FROM api_test_report r JOIN api_test t ON r.test_id = t.id
JOIN project ON project.id = t.project_id
@ -30,8 +31,8 @@
</select>
<select id="listByTestId" resultMap="BaseResultMap">
SELECT t.name, t.description,
r.id, r.test_id, r.create_time, r.update_time, r.status,
SELECT c,
r.name, r.description, r.id, r.test_id, r.create_time, r.update_time, r.status,
project.name AS project_name
FROM api_test_report r JOIN api_test t ON r.test_id = t.id
JOIN project ON project.id = t.project_id
@ -41,4 +42,14 @@
ORDER BY r.update_time DESC
</select>
<select id="get" resultMap="BaseResultMap">
SELECT r.*, t.name AS test_name, project.name AS project_name
FROM api_test_report r JOIN api_test t ON r.test_id = t.id
JOIN project ON project.id = t.project_id
<where>
r.id = #{id}
</where>
ORDER BY r.update_time DESC
</select>
</mapper>

View File

@ -1,20 +1,19 @@
package io.metersphere.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = JmeterProperties.JMETER_PREFIX)
@Setter
@Getter
public class JmeterProperties {
public static final String JMETER_PREFIX = "jmeter";
private String image;
public String getImage() {
return image;
}
private String home;
public void setImage(String image) {
this.image = image;
}
}

View File

@ -1,8 +1,12 @@
package io.metersphere.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = KafkaProperties.KAFKA_PREFIX)
@Getter
@Setter
public class KafkaProperties {
public static final String KAFKA_PREFIX = "kafka";
@ -22,112 +26,8 @@ public class KafkaProperties {
private KafkaProperties.Ssl ssl = new KafkaProperties.Ssl();
private KafkaProperties.Log log = new KafkaProperties.Log();
public String getAcks() {
return acks;
}
public void setAcks(String acks) {
this.acks = acks;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public String getFields() {
return fields;
}
public void setFields(String fields) {
this.fields = fields;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getBootstrapServers() {
return bootstrapServers;
}
public void setBootstrapServers(String bootstrapServers) {
this.bootstrapServers = bootstrapServers;
}
public String getSampleFilter() {
return sampleFilter;
}
public void setSampleFilter(String sampleFilter) {
this.sampleFilter = sampleFilter;
}
public String getTestMode() {
return testMode;
}
public void setTestMode(String testMode) {
this.testMode = testMode;
}
public String getCompressionType() {
return compressionType;
}
public void setCompressionType(String compressionType) {
this.compressionType = compressionType;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getParseAllReqHeaders() {
return parseAllReqHeaders;
}
public void setParseAllReqHeaders(String parseAllReqHeaders) {
this.parseAllReqHeaders = parseAllReqHeaders;
}
public String getParseAllResHeaders() {
return parseAllResHeaders;
}
public void setParseAllResHeaders(String parseAllResHeaders) {
this.parseAllResHeaders = parseAllResHeaders;
}
public String getBatchSize() {
return batchSize;
}
public void setBatchSize(String batchSize) {
this.batchSize = batchSize;
}
public String getConnectionsMaxIdleMs() {
return connectionsMaxIdleMs;
}
public void setConnectionsMaxIdleMs(String connectionsMaxIdleMs) {
this.connectionsMaxIdleMs = connectionsMaxIdleMs;
}
@Getter
@Setter
public static class Ssl {
private String enabled = "false";
private String keyPassword;
@ -140,124 +40,11 @@ public class KafkaProperties {
private String protocol;
private String enabledProtocols;
private String provider;
public Ssl() {
}
public String getEnabled() {
return enabled;
}
public void setEnabled(String enabled) {
this.enabled = enabled;
}
public String getKeyPassword() {
return keyPassword;
}
public void setKeyPassword(String keyPassword) {
this.keyPassword = keyPassword;
}
public String getKeystoreLocation() {
return keystoreLocation;
}
public void setKeystoreLocation(String keystoreLocation) {
this.keystoreLocation = keystoreLocation;
}
public String getKeystorePassword() {
return keystorePassword;
}
public void setKeystorePassword(String keystorePassword) {
this.keystorePassword = keystorePassword;
}
public String getKeystoreType() {
return keystoreType;
}
public void setKeystoreType(String keystoreType) {
this.keystoreType = keystoreType;
}
public String getTruststoreLocation() {
return truststoreLocation;
}
public void setTruststoreLocation(String truststoreLocation) {
this.truststoreLocation = truststoreLocation;
}
public String getTruststorePassword() {
return truststorePassword;
}
public void setTruststorePassword(String truststorePassword) {
this.truststorePassword = truststorePassword;
}
public String getTruststoreType() {
return truststoreType;
}
public void setTruststoreType(String truststoreType) {
this.truststoreType = truststoreType;
}
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getEnabledProtocols() {
return enabledProtocols;
}
public void setEnabledProtocols(String enabledProtocols) {
this.enabledProtocols = enabledProtocols;
}
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
}
public Ssl getSsl() {
return ssl;
}
public void setSsl(Ssl ssl) {
this.ssl = ssl;
}
@Getter
@Setter
public static class Log {
private String topic;
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
}
public Log getLog() {
return log;
}
public void setLog(Log log) {
this.log = log;
}
}

View File

@ -4,6 +4,7 @@ import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.User;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.controller.request.UserRequest;
@ -17,6 +18,7 @@ import io.metersphere.service.UserService;
import io.metersphere.service.WorkspaceService;
import io.metersphere.user.SessionUser;
import io.metersphere.user.SessionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.BeanUtils;
@ -120,6 +122,9 @@ public class UserController {
@PostMapping("/update/current")
public UserDTO updateCurrentUser(@RequestBody User user) {
UserDTO userDTO = userService.getUserDTO(user.getId());
BeanUtils.copyProperties(user, userDTO);
SessionUtils.putUser(SessionUser.fromUser(userDTO));
userService.updateUser(user);
return SessionUtils.getUser();
}
@ -182,6 +187,10 @@ public class UserController {
@RequiresRoles(value = {RoleConstants.TEST_MANAGER,RoleConstants.ORG_ADMIN}, logical = Logical.OR)
public void deleteMember(@PathVariable String workspaceId, @PathVariable String userId) {
workspaceService.checkWorkspaceOwner(workspaceId);
String currentUserId = SessionUtils.getUser().getId();
if (StringUtils.equals(userId, currentUserId)) {
MSException.throwException("Insufficient permissions!");
}
userService.deleteMember(workspaceId, userId);
}
@ -202,6 +211,10 @@ public class UserController {
@RequiresRoles(RoleConstants.ORG_ADMIN)
public void delOrganizationMember(@PathVariable String organizationId, @PathVariable String userId) {
organizationService.checkOrgOwner(organizationId);
String currentUserId = SessionUtils.getUser().getId();
if (StringUtils.equals(userId, currentUserId)) {
MSException.throwException("Insufficient permissions!");
}
userService.delOrganizationMember(organizationId, userId);
}

View File

@ -11,10 +11,7 @@ import io.metersphere.controller.request.member.AddMemberRequest;
import io.metersphere.controller.request.member.QueryMemberRequest;
import io.metersphere.controller.request.organization.AddOrgMemberRequest;
import io.metersphere.controller.request.organization.QueryOrgMemberRequest;
import io.metersphere.dto.OrganizationMemberDTO;
import io.metersphere.dto.UserDTO;
import io.metersphere.dto.UserRoleDTO;
import io.metersphere.dto.UserRoleHelpDTO;
import io.metersphere.i18n.Translator;
import io.metersphere.user.SessionUser;
import io.metersphere.user.SessionUtils;
@ -23,8 +20,10 @@ import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.*;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@ -70,7 +69,8 @@ public class UserService {
user.setUpdateTime(System.currentTimeMillis());
// 默认1:启用状态
user.setStatus("1");
// 密码使用 MD5
user.setPassword(CodingUtil.md5(user.getPassword()));
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
criteria.andEmailEqualTo(user.getEmail());
@ -124,9 +124,8 @@ public class UserService {
}
public void updateUser(User user) {
UserDTO userDTO = getUserDTO(user.getId());
BeanUtils.copyProperties(user, userDTO);
SessionUtils.putUser(SessionUser.fromUser(userDTO));
// MD5
user.setPassword(CodingUtil.md5(user.getPassword()));
user.setUpdateTime(System.currentTimeMillis());
userMapper.updateByPrimaryKeySelective(user);
}

View File

@ -64,4 +64,5 @@ kafka.ssl.provider=
kafka.ssl.truststore-type=
# jmeter
jmeter.image=registry.fit2cloud.com/metersphere/jmeter-master:0.0.4
jmeter.image=registry.fit2cloud.com/metersphere/jmeter-master:0.0.4
jmeter.home=/opt/jmeter/apache-jmeter-5.2.1

View File

@ -23,8 +23,8 @@
<el-divider/>
<ms-show-all :index="'/api/test/list/all'"/>
<ms-create-button :index="'/api/test/create'" :title="$t('load_test.create')"/>
<!-- <el-menu-item :index="testCaseProjectPath" class="blank_item"></el-menu-item>-->
<!-- <el-menu-item :index="testEditPath" class="blank_item"></el-menu-item>-->
<!-- <el-menu-item :index="testCaseProjectPath" class="blank_item"></el-menu-item>-->
<!-- <el-menu-item :index="testEditPath" class="blank_item"></el-menu-item>-->
</el-submenu>
<el-submenu v-if="isCurrentWorkspaceUser"
@ -33,7 +33,7 @@
<ms-recent-list :options="reportRecent"/>
<el-divider/>
<ms-show-all :index="'/api/report/list/all'"/>
<!-- <el-menu-item :index="reportViewPath" class="blank_item"></el-menu-item>-->
<!-- <el-menu-item :index="reportViewPath" class="blank_item"></el-menu-item>-->
</el-submenu>
</el-menu>
</el-col>
@ -90,7 +90,7 @@
title: this.$t('report.recent'),
url: "/api/report/recent/5",
index: function (item) {
return '/api/report/view?id=' + item.id;
return '/api/report/view/' + item.id;
}
}
}

View File

@ -32,6 +32,38 @@
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column
prop="status"
:label="$t('commons.status')">
<template v-slot:default="{row}">
<el-tag size="mini" type="info" v-if="row.status === 'Saved'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="primary" v-else-if="row.status === 'Starting'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="success" v-else-if="row.status === 'Running'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="warning" v-else-if="row.status === 'Reporting'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="info" v-else-if="row.status === 'Completed'">
{{ row.status }}
</el-tag>
<el-tooltip placement="top" v-else-if="row.status === 'Error'" effect="light">
<template v-slot:content>
<div>{{row.description}}</div>
</template>
<el-tag size="mini" type="danger">
{{ row.status }}
</el-tag>
</el-tooltip>
<span v-else>
{{ row.status }}
</span>
</template>
</el-table-column>
<el-table-column
width="150"
:label="$t('commons.operating')">
@ -101,7 +133,7 @@
})
},
handleDelete(report) {
this.$alert(this.$t('load_test.delete_confirm') + report.name + "", '', {
this.$alert(this.$t('api_report.delete_confirm') + report.name + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {

View File

@ -1,117 +1,113 @@
<template>
<div v-loading="result.loading" class="report-view-container">
<div class="container" v-loading="result.loading">
<div class="main-content">
<el-card>
<el-row>
<el-col :span="16">
<el-row>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">{{report.projectName}}</el-breadcrumb-item>
<el-breadcrumb-item>{{report.testName}}</el-breadcrumb-item>
<el-breadcrumb-item>{{report.name}}</el-breadcrumb-item>
</el-breadcrumb>
</el-row>
<el-row class="ms-report-view-btns">
<el-button type="primary" plain size="mini">立即停止</el-button>
<el-button type="success" plain size="mini">再次执行</el-button>
<el-button type="info" plain size="mini">导出</el-button>
<el-button type="warning" plain size="mini">比较</el-button>
</el-row>
</el-col>
<el-col :span="8">
<span class="ms-report-time-desc">
持续时间 30 分钟
</span>
<span class="ms-report-time-desc">
开始时间 2020-3-10 12:00:00
</span>
<span class="ms-report-time-desc">
结束时间 2020-3-10 12:30:00
</span>
</el-col>
</el-row>
<el-divider></el-divider>
<el-tabs v-model="active" type="border-card" :stretch="true">
<el-tab-pane :label="$t('report.test_details')">
<result-details />
</el-tab-pane>
<el-tab-pane :label="$t('report.test_log_details')">
<ms-report-log-details />
</el-tab-pane>
</el-tabs>
<section class="report-container">
<header class="report-header" v-if="this.report.testId">
<span>{{report.projectName}} / </span>
<router-link :to="path">{{report.testName}}</router-link>
</header>
<main>
<div class="scenario-chart">
<ms-metric-chart :content="content"></ms-metric-chart>
</div>
<el-card>
<div class="scenario-header">
<el-row :gutter="10">
<el-col :span="16">
{{$t('api_report.scenario_name')}}
</el-col>
<el-col :span="2">
{{$t('api_report.response_time')}}
</el-col>
<el-col :span="2">
{{$t('api_report.error')}}
</el-col>
<el-col :span="2">
{{$t('api_report.assertions')}}
</el-col>
<el-col :span="2">
{{$t('api_report.result')}}
</el-col>
</el-row>
</div>
<ms-scenario-result v-for="(scenario, index) in content.scenarios" :key="index" :scenario="scenario"/>
</el-card>
</main>
</section>
</el-card>
</div>
</div>
</template>
<script>
import MsReportLogDetails from './components/LogDetails';
import ResultDetails from './components/ResultDetails';
import MsRequestResult from "./components/RequestResult";
import MsScenarioResult from "./components/ScenarioResult";
import MsMetricChart from "./components/MetricChart";
export default {
name: "ApiReportView",
components: {
MsReportLogDetails,
ResultDetails
},
name: "MsApiReportView",
components: {MsMetricChart, MsScenarioResult, MsRequestResult},
data() {
return {
content: {},
report: {},
result: {},
active: '0',
videoPath: '',
report: {}
}
},
methods: {
initBreadcrumb() {
if(this.reportId){
this.result = this.$get("/api/report/test/pro/info/" + this.reportId, res => {
let data = res.data;
if(data){
this.report = data;
this.report.content = JSON.parse(this.report.content);
}
getReport() {
if (this.reportId) {
let url = "/api/report/get/" + this.reportId;
this.result = this.$get(url, response => {
this.report = response.data || {};
this.content = JSON.parse(this.report.content);
});
}
}
},
mounted() {
this.initBreadcrumb();
watch: {
'$route': 'getReport',
},
created() {
this.getReport();
},
computed: {
reportId: function () {
return this.$route.params.reportId;
},
path() {
return "/api/test/edit?id=" + this.report.testId;
}
}
}
</script>
<style scoped>
.report-view-container {
float: none;
text-align: center;
padding: 15px;
width: 100%;
height: 100%;
box-sizing: border-box;
.report-container {
height: calc(100vh - 150px);
min-height: 600px;
overflow-y: auto;
}
.report-view-container .main-content {
margin: 0 auto;
width: 100%;
max-width: 1200px;
.report-header {
font-size: 15px;
}
.ms-report-view-btns {
margin-top: 15px;
.report-header a {
text-decoration: none;
}
.ms-report-time-desc {
text-align: left;
display: block;
color: #5C7878;
.scenario-header {
border: 1px solid #EBEEF5;
background-color: #F9FCFF;
border-left: 0;
border-right: 0;
padding: 5px 0;
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<el-table :data="assertions" style="width: 100%">
<el-table-column prop="name" :label="$t('api_report.assertions_name')" width="300">
</el-table-column>
<el-table-column prop="message" :label="$t('api_report.assertions_message')">
</el-table-column>
<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">
{{$t('api_report.success')}}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{$t('api_report.fail')}}
</el-tag>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
name: "MsAssertionResults",
props: {
assertions: Array
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,202 @@
<template>
<div class="metric-container">
<el-row type="flex" align="middle">
<div style="width: 50%">
<el-row type="flex" justify="center" align="middle">
<chart id="chart" ref="chart" :options="options" :autoresize="true"></chart>
<el-row type="flex" justify="center" align="middle">
<i class="circle success"/>
<div class="metric-box">
<div class="value">{{content.success}}</div>
<div class="name">{{$t('api_report.success')}}</div>
</div>
<div style="width: 40px"></div>
<i class="circle fail"/>
<div class="metric-box">
<div class="value">{{content.error}}</div>
<div class="name">{{$t('api_report.fail')}}</div>
</div>
</el-row>
</el-row>
</div>
<div class="split"></div>
<div style="width: 50%">
<el-row type="flex" justify="space-around" align="middle">
<div class="metric-icon-box">
<i class="el-icon-warning-outline fail"></i>
<div class="value">{{fail}}</div>
<div class="name">{{$t('api_report.fail')}}</div>
</div>
<div class="metric-icon-box">
<i class="el-icon-document-checked assertions"></i>
<div class="value">{{assertions}}</div>
<div class="name">{{$t('api_report.assertions_pass')}}</div>
</div>
<div class="metric-icon-box">
<i class="el-icon-document-copy total"></i>
<div class="value">{{this.content.total}}</div>
<div class="name">{{$t('api_report.request')}}</div>
</div>
</el-row>
</div>
</el-row>
</div>
</template>
<script>
export default {
name: "MsMetricChart",
props: {
content: Object
},
computed: {
options() {
return {
color: ['#67C23A', '#F56C6C'],
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
title: [{
text: '{value|' + this.content.total + '}\n{name|' + this.$t('api_report.request') + '}',
top: 'center',
left: 'center',
textStyle: {
rich: {
align: 'center',
value: {
fontSize: 32,
fontWeight: 'bold',
padding: [10, 0]
},
name: {
fontSize: 14,
fontWeight: 'normal',
color: '#7F7F7F',
}
}
}
}],
series: [
{
type: 'pie',
radius: ['80%', '90%'],
avoidLabelOverlap: false,
hoverAnimation: false,
itemStyle: {
normal: {
borderColor: "#FFF",
shadowColor: '#E1E1E1',
shadowBlur: 10
}
},
labelLine: {
show: false
},
data: [
{value: this.content.success, name: this.$t('api_report.success')},
{value: this.content.error, name: this.$t('api_report.fail')},
]
}
]
};
},
fail() {
return (this.content.error / this.content.total).toFixed(0) + "%";
},
assertions() {
return this.content.passAssertions + " / " + this.content.totalAssertions;
}
},
}
</script>
<style scoped>
.metric-container {
padding: 20px;
}
.metric-container #chart {
width: 140px;
height: 140px;
margin-right: 40px;
}
.metric-container .split {
margin: 20px;
height: 100px;
border-left: 1px solid #D8DBE1;
}
.metric-container .circle {
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 0 20px 1px rgba(200, 216, 226, .42);
display: inline-block;
margin-right: 10px;
vertical-align: middle;
}
.metric-container .circle.success {
background-color: #67C23A;
}
.metric-container .circle.fail {
background-color: #F56C6C;
}
.metric-box {
display: inline-block;
text-align: center;
}
.metric-box .value {
font-size: 32px;
font-weight: 600;
letter-spacing: -.5px;
}
.metric-box .name {
font-size: 16px;
letter-spacing: -.2px;
color: #404040;
}
.metric-icon-box {
text-align: center;
margin: 0 10px;
}
.metric-icon-box .value {
font-size: 20px;
font-weight: 600;
letter-spacing: -.4px;
line-height: 28px;
vertical-align: middle;
}
.metric-icon-box .name {
font-size: 13px;
letter-spacing: 1px;
color: #BFBFBF;
line-height: 18px;
}
.metric-icon-box .fail {
color: #F56C6C;
font-size: 40px;
}
.metric-icon-box .assertions {
font-size: 40px;
}
.metric-icon-box .total {
font-size: 40px;
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div class="metric-container">
<el-row type="flex">
<div class="metric">
<div class="value">{{request.responseResult.responseTime}} ms</div>
<div class="name">{{$t('api_report.response_time')}}</div>
<br>
<div class="value">{{request.responseResult.latency}} ms</div>
<div class="name">{{$t('api_report.latency')}}</div>
</div>
<div class="metric">
<div class="value">{{request.requestSize}} bytes</div>
<div class="name">{{$t('api_report.request_size')}}</div>
<br>
<div class="value">{{request.responseResult.responseSize}} bytes</div>
<div class="name">{{$t('api_report.response_size')}}</div>
</div>
<div class="metric horizontal">
<el-row type="flex">
<div class="code">
<div class="value" :class="{'error': error}">{{request.responseResult.responseCode}}</div>
<div class="name">{{$t('api_report.response_code')}}</div>
</div>
<div class="split"></div>
<div class="message">
<div class="value">{{request.responseResult.responseMessage}}</div>
<div class="name">{{$t('api_report.response_message')}}</div>
</div>
</el-row>
</div>
</el-row>
</div>
</template>
<script>
export default {
name: "MsRequestMetric",
props: {
request: Object
},
computed: {
error() {
return this.request.responseResult.responseCode >= 400;
}
}
}
</script>
<style scoped>
.metric-container {
padding: 20px;
}
.metric {
padding: 20px;
border: 1px solid #EBEEF5;
min-width: 120px;
height: 114px;
}
.metric + .metric {
margin-left: 20px;
}
.metric .value {
font-size: 16px;
font-weight: 500;
word-break: break-all;
}
.metric .name {
color: #404040;
opacity: 0.5;
padding: 5px 0;
}
.metric.horizontal {
width: 100%;
}
.metric .code {
width: 120px;
}
.metric .code .value {
color: #67C23A;
}
.metric .code .value.error {
color: #F56C6C;
}
.metric .split {
height: 114px;
border-left: 1px solid #EBEEF5;
margin-right: 20px;
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div class="request-result">
<div @click="active">
<el-row :gutter="10" type="flex" align="middle" class="info">
<el-col :span="4">
<div class="method">
{{request.method}}
</div>
</el-col>
<el-col :span="12">
<div class="name">{{request.name}}</div>
<div class="url">{{request.url}}</div>
</el-col>
<el-col :span="2">
<div class="time">
{{request.responseResult.responseTime}}
</div>
</el-col>
<el-col :span="2">
{{error}}
</el-col>
<el-col :span="2">
{{request.passAssertions}} / {{request.totalAssertions}}
</el-col>
<el-col :span="2">
<el-tag size="mini" type="success" v-if="request.success">
{{$t('api_report.success')}}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{$t('api_report.fail')}}
</el-tag>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive">
<ms-request-metric :request="request"/>
<ms-request-text :request="request"/>
<br>
<ms-response-text :response="request.responseResult"/>
</div>
</el-collapse-transition>
</div>
</template>
<script>
import MsRequestMetric from "./RequestMetric";
import MsAssertionResults from "./AssertionResults";
import MsRequestText from "./RequestText";
import MsResponseText from "./ResponseText";
export default {
name: "MsRequestResult",
components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric},
props: {
request: Object
},
data() {
return {
isActive: false,
activeName: "request",
activeName2: "body",
activeName3: "body",
}
},
methods: {
active() {
this.isActive = !this.isActive;
}
},
computed: {
error() {
return this.request.totalAssertions - this.request.passAssertions;
}
}
}
</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 {
/*border-left: 5px solid #1E90FF;*/
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;
overflow: auto;
white-space: normal;
word-wrap: break-word;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="text-container">
<div @click="active" class="collapse">
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{$t('api_report.request')}}
</div>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane label="Body" name="body" class="pane">
<pre>{{request.body}}</pre>
</el-tab-pane>
<el-tab-pane label="Headers" name="headers" class="pane">
<pre>{{request.headers}}</pre>
</el-tab-pane>
<el-tab-pane label="Cookies" name="cookies" class="pane">
<pre>{{request.cookies}}</pre>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</div>
</template>
<script>
export default {
name: "MsRequestText",
props: {
request: Object
},
data() {
return {
isActive: false,
activeName: "body",
}
},
methods: {
active() {
this.isActive = !this.isActive;
}
},
}
</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: #F9F9F9;
padding: 10px;
height: 250px;
overflow-y: auto;
}
pre {
margin: 0;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<div class="text-container">
<div @click="active" class="collapse">
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{$t('api_report.response')}}
</div>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane label="Body" name="body" class="pane">
<div>{{response.body}}</div>
</el-tab-pane>
<el-tab-pane label="Headers" name="headers" class="pane">
<pre>{{response.headers}}</pre>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.assertions')" name="assertions" class="pane">
<ms-assertion-results :assertions="response.assertions"/>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</div>
</template>
<script>
import MsAssertionResults from "./AssertionResults";
export default {
name: "MsResponseText",
components: {MsAssertionResults},
props: {
response: Object
},
data() {
return {
isActive: false,
activeName: "body",
}
},
methods: {
active() {
this.isActive = !this.isActive;
}
},
}
</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: 0 10px;
height: 250px;
overflow-y: auto;
}
pre {
margin: 0;
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<div class="scenario-result">
<div @click="active">
<el-row :gutter="10" type="flex" align="middle" class="info">
<el-col :span="16">
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{scenario.name}}
</el-col>
<el-col :span="2">
{{scenario.responseTime}}
</el-col>
<el-col :span="2">
{{scenario.error}}
</el-col>
<el-col :span="2">
{{assertion}}
</el-col>
<el-col :span="2">
<el-tag size="mini" type="success" v-if="success">
{{$t('api_report.success')}}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{$t('api_report.fail')}}
</el-tag>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive">
<ms-request-result v-for="(request, index) in scenario.requestResults" :key="index" :request="request"/>
</div>
</el-collapse-transition>
</div>
</template>
<script>
import MsRequestResult from "./RequestResult";
export default {
name: "MsScenarioResult",
components: {MsRequestResult},
props: {
scenario: Object
},
data() {
return {
isActive: false
}
},
methods: {
active() {
this.isActive = !this.isActive;
}
},
computed: {
assertion() {
return this.scenario.passAssertions - this.scenario.totalAssertions;
},
success() {
return this.scenario.error === 0;
}
}
}
</script>
<style scoped>
.scenario-result {
width: 100%;
padding: 2px 0;
}
.scenario-result + .scenario-result {
border-top: 1px solid #EBEEF5;
}
.scenario-result .info {
height: 40px;
cursor: pointer;
}
.scenario-result .icon {
padding: 5px;
}
.scenario-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -38,12 +38,43 @@
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
<el-table-column
prop="status"
:label="$t('commons.status')">
<template v-slot:default="{row}">
<el-tag size="mini" type="info" v-if="row.status === 'Saved'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="primary" v-else-if="row.status === 'Starting'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="success" v-else-if="row.status === 'Running'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="warning" v-else-if="row.status === 'Reporting'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="info" v-else-if="row.status === 'Completed'">
{{ row.status }}
</el-tag>
<el-tooltip placement="top" v-else-if="row.status === 'Error'" effect="light">
<template v-slot:content>
<div>{{row.description}}</div>
</template>
<el-tag size="mini" type="danger">
{{ row.status }}
</el-tag>
</el-tooltip>
<span v-else>
{{ row.status }}
</span>
</template>
</el-table-column>
<el-table-column
width="150"
:label="$t('commons.operating')">
<template v-slot:default="scope">
<el-button @click="handleEdit(scope.row)" type="primary" icon="el-icon-edit" size="mini" circle/>
<el-button @click="handleDelete(scope.row)" type="danger" icon="el-icon-delete" size="mini" circle/>
<ms-table-operator @editClick="handleEdit(scope.row)" @deleteClick="handleDelete(scope.row)"/>
</template>
</el-table-column>
</el-table>
@ -57,9 +88,10 @@
<script>
import MsTablePagination from "../../common/pagination/TablePagination";
import MsTableHeader from "../../common/components/MsTableHeader";
import MsTableOperator from "../../common/components/MsTableOperator";
export default {
components: {MsTableHeader, MsTablePagination},
components: {MsTableHeader, MsTablePagination, MsTableOperator},
data() {
return {
result: {},

View File

@ -56,7 +56,6 @@
.logging-content {
white-space: pre-line;
overflow: auto;
height: calc(100vh - 420px);
}
</style>

View File

@ -78,8 +78,7 @@
width="150"
:label="$t('commons.operating')">
<template v-slot:default="scope">
<el-button @click="handleEdit(scope.row)" type="primary" icon="el-icon-edit" size="mini" circle/>
<el-button @click="handleDelete(scope.row)" type="danger" icon="el-icon-delete" size="mini" circle/>
<ms-table-operator @editClick="handleEdit(scope.row)" @deleteClick="handleDelete(scope.row)"/>
</template>
</el-table-column>
</el-table>
@ -92,9 +91,10 @@
<script>
import MsTablePagination from "../../common/pagination/TablePagination";
import MsTableOperator from "../../common/components/MsTableOperator";
export default {
components: {MsTablePagination},
components: {MsTablePagination, MsTableOperator},
data() {
return {
result: {},

View File

@ -3,166 +3,145 @@
<el-row type="flex" justify="start">
<el-col :span="8">
<h3>{{$t('load_test.domain_bind')}}</h3>
<el-button type="el-icon-circle-plus-outline" plain size="mini" @click="add('domains')">{{$t('commons.add')}}</el-button>
<el-button icon="el-icon-circle-plus-outline" plain size="mini" @click="add('domains')">{{$t('commons.add')}}
</el-button>
</el-col>
</el-row>
<!-- -->
<el-row>
<el-col :span="20">
<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="readOnly"
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.enable')"
show-overflow-tooltip>
<template v-slot:default="{row}">
<el-switch
:disabled="!row.edit"
size="mini"
v-model="row.enable"
active-color="#13ce66"
inactive-color="#ff4949"
@click="confirmEdit(row)"
>
</el-switch>
</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="readOnly"
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.operating')">
<template v-slot:default="{row, $index}">
<el-button
type="primary"
icon="el-icon-plus"
circle size="mini"
@click="add('domains')">
</el-button>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
circle
@click="del(row, 'domains', $index)">
</el-button>
</template>
</el-table-column>
</el-table>
<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="readOnly"
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.enable')"
show-overflow-tooltip>
<template v-slot:default="{row}">
<el-switch
:disabled="!row.edit"
size="mini"
v-model="row.enable"
active-color="#13ce66"
inactive-color="#ff4949"
@click="confirmEdit(row)"
>
</el-switch>
</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="readOnly"
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.operating')">
<template v-slot:default="{row, $index}">
<ms-table-operator-button :tip="$t('commons.delete')" icon="el-icon-delete" type="danger"
@exec="del(row, 'domains', $index)"/>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<h3>{{$t('load_test.params')}}</h3>
<el-button type="el-icon-circle-plus-outline" plain size="mini" @click="add('params')">{{$t('commons.add')}}</el-button>
<el-button icon="el-icon-circle-plus-outline" plain size="mini" @click="add('params')">{{$t('commons.add')}}
</el-button>
</el-col>
</el-row>
<!-- -->
<el-row>
<el-col :span="20">
<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="readOnly"
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
align="center"
:label="$t('load_test.enable')"
show-overflow-tooltip>
<template v-slot:default="{row}">
<el-switch
:disabled="!row.edit"
size="mini"
v-model="row.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column
:label="$t('load_test.param_value')"
show-overflow-tooltip align="center">
<template v-slot:default="{row}">
<!-- <template v-if="row.edit">
<el-input v-model="row.value" class="edit-input" size="mini"/>
<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="readOnly"
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>
<span v-else>{{ row.value }}</span>-->
<el-input
size="mini"
v-if="readOnly"
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.operating')">
<template v-slot:default="{row, $index}">
<el-button
type="primary"
size="mini"
icon="el-icon-plus"
circle
@click="add(row)">
</el-button>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
circle
@click="del(row, 'params', $index)">
</el-button>
</template>
</el-table-column>
</el-table>
</el-table-column>
<el-table-column
align="center"
:label="$t('load_test.enable')"
show-overflow-tooltip>
<template v-slot:default="{row}">
<el-switch
:disabled="!row.edit"
size="mini"
v-model="row.enable"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
<el-table-column
:label="$t('load_test.param_value')"
show-overflow-tooltip align="center">
<template v-slot:default="{row}">
<!-- <template v-if="row.edit">
<el-input v-model="row.value" class="edit-input" size="mini"/>
</template>
<span v-else>{{ row.value }}</span>-->
<el-input
size="mini"
v-if="readOnly"
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.operating')">
<template v-slot:default="{row, $index}">
<ms-table-operator-button :tip="$t('commons.delete')" icon="el-icon-delete" type="danger"
@exec="del(row, 'params', $index)"/>
</template>
</el-table-column>
</el-table>
</el-col>
</el-row>
@ -198,8 +177,11 @@
</template>
<script>
import MsTableOperatorButton from "../../../common/components/MsTableOperatorButton";
export default {
name: "PerformanceAdvancedConfig",
components: {MsTableOperatorButton},
data() {
return {
timeout: 100,
@ -233,21 +215,6 @@
}
},
methods: {
revertObject(row) {
Object.keys(row).forEach(function (key) {
row[key] = row[key + 'Origin'];
});
},
saveOriginObject(row) {
Object.keys(row).forEach(function (key) {
row[key + 'Origin'] = row[key];
});
},
delOriginObject(row) {
Object.keys(row).forEach(function (key) {
delete row[key + 'Origin'];
});
},
getAdvancedConfig(testId) {
this.$get('/performance/get-advanced-config/' + testId, (response) => {
if (response.data) {
@ -257,9 +224,8 @@
this.statusCodeStr = this.statusCode.join(',');
this.domains = data.domains || [];
this.params = data.params || [];
this.add('domains');
this.domains.forEach(d => d.edit = false);
this.params.forEach(d => d.edit = false);
/*this.domains.forEach(d => d.edit = false);
this.params.forEach(d => d.edit = false);*/
}
});
},
@ -267,7 +233,7 @@
if (dataName === 'domains') {
this[dataName].push({
domain: 'fit2cloud.com',
enable: false,
enable: true,
ip: '127.0.0.1',
edit: true,
});
@ -282,21 +248,14 @@
}
},
edit(row) {
this.saveOriginObject(row);
row.edit = !row.edit
},
del(row, dataName, index) {
this[dataName].splice(index, 1);
},
cancelEdit(row) {
row.edit = false;
// rollback changes
this.revertObject(row);
},
confirmEdit(row) {
row.edit = false;
row.enable=true,
this.saveOriginObject(row);
row.enable = true;
},
groupBy(data, key) {
return data.reduce((p, c) => {
@ -343,8 +302,6 @@
this.params.forEach(d => d.edit = false);
},
configurations() {
this.domains.forEach(d => this.delOriginObject(d));
this.params.forEach(d => this.delOriginObject(d));
let statusCode = [];
if (this.statusCodeStr) {
statusCode = this.statusCodeStr.split(',');
@ -368,20 +325,25 @@
.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 {
.tb-edit .current-row .el-textarea + span {
display: none;
}
.el-col{
.el-col {
text-align: left;
}
.el-col .el-table{
align:center;
.el-col .el-table {
align: center;
}
</style>

View File

@ -43,6 +43,9 @@
<el-form-item :label="$t('commons.phone')" prop="phone">
<el-input v-model="form.phone" autocomplete="off"/>
</el-form-item>
<el-form-item :label="$t('commons.password')" prop="password">
<el-input v-model="form.password" autocomplete="off" show-password/>
</el-form-item>
</el-form>
<template v-slot:footer>
<ms-dialog-footer
@ -94,6 +97,15 @@
message: this.$t('member.email_format_is_incorrect'),
trigger: 'blur'
}
],
password: [
{required: true, message: this.$t('user.input_password'), trigger: 'blur'},
{
required:true,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/,
message: this.$t('member.password_format_is_incorrect'),
trigger: 'blur'
}
]
}
}

View File

@ -55,6 +55,9 @@
<el-form-item :label="$t('commons.phone')" prop="phone">
<el-input v-model="form.phone" autocomplete="off"/>
</el-form-item>
<el-form-item :label="$t('commons.password')" prop="password">
<el-input v-model="form.password" autocomplete="off" show-password/>
</el-form-item>
</el-form>
<template v-slot:footer>
<ms-dialog-footer
@ -78,6 +81,10 @@
<el-form-item :label="$t('commons.phone')" prop="phone">
<el-input v-model="form.phone" autocomplete="off"/>
</el-form-item>
<el-form-item :label="$t('commons.password')" prop="password">
<el-input v-model="form.password" autocomplete="off" show-password/>
</el-form-item>
<!--<el-input placeholder="请输入密码" v-model="input" show-password></el-input>-->
</el-form>
<template v-slot:footer>
<ms-dialog-footer
@ -101,6 +108,7 @@
components: {MsCreateBox, MsTablePagination, MsTableHeader, MsTableOperator, MsDialogFooter},
data() {
return {
/*input:'',*/
queryPath: '/user/special/list',
deletePath: '/user/special/delete/',
createPath: '/user/special/add',
@ -147,6 +155,15 @@
message: this.$t('user.email_format_is_incorrect'),
trigger: 'blur'
}
],
password: [
{required: true, message: this.$t('user.input_password'), trigger: 'blur'},
{
required:true,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/,
message: this.$t('member.password_format_is_incorrect'),
trigger: 'blur'
}
]
}
}

View File

@ -175,14 +175,11 @@
cancelButtonText: this.$t('commons.cancel'),
type: 'warning'
}).then(() => {
this.loading = true;
this.$get('/user/ws/member/delete/' + this.currentUser().lastWorkspaceId + '/' + row.id).then(() => {
this.result = this.$get('/user/ws/member/delete/' + this.currentUser().lastWorkspaceId + '/' + row.id,() => {
this.$success(this.$t('commons.delete_success'));
this.initTableData();
this.loading = false;
});
this.$success(this.$t('commons.delete_success'));
}).catch(() => {
this.loading = false;
this.$info(this.$t('commons.delete_cancel'));
});
},

View File

@ -94,6 +94,7 @@ export default {
'special_characters_are_not_supported': 'Special characters are not supported',
'mobile_number_format_is_incorrect': 'Mobile number format is incorrect',
'email_format_is_incorrect': 'Email format is incorrect',
'password_format_is_incorrect': 'Password format is incorrect (At least 8-16 characters, at least 1 uppercase letter, 1 lowercase letter and 1 number)',
},
user: {
'create': 'Create',
@ -101,6 +102,7 @@ export default {
'input_name': 'Please enter a user name',
'input_id': 'Please enter a ID',
'input_email': 'Please enter a email',
'input_password': 'Please enter a password',
'special_characters_are_not_supported': 'Special characters are not supported',
'mobile_number_format_is_incorrect': 'Mobile number format is incorrect',
'email_format_is_incorrect': 'Email format is incorrect',
@ -129,8 +131,8 @@ export default {
'being_generated': 'Report is being generated...',
},
load_test: {
'operating':'Operating',
'pressure_prediction_chart':'Pressure Prediction Chart',
'operating': 'Operating',
'pressure_prediction_chart': 'Pressure Prediction Chart',
'recent': 'Recent Tests',
'search_by_name': 'Search by name',
'project_name': 'Project',
@ -186,7 +188,77 @@ export default {
'download_log_file': 'Download',
},
api_test: {
'select_resource_pool': 'Please select resource pool'
save_and_run: "Save and Run",
run: "Run",
running: "Running",
reset: "Rest",
input_name: "Please enter the test name",
select_project: "Please select project",
scenario: {
config: "Scenario Config",
input_name: "Please enter the scenario name",
name: "Scenario Name",
base_url: "Base URL",
base_url_description: "Base URL as URL prefix for all requests",
parameters: "arguments",
headers: "Headers",
kv_description: "Will be used for requests where the item is not set",
},
request: {
input_name: "Please enter the request name",
name: "Request Name",
method: "Method",
url: "url",
url_description: "etc: https://fit2cloud.com",
parameters: "arguments",
parameters_desc: "Parameters will be appended to the URL e.g. https://fit2cloud.com?Name=Value&Name2=Value2",
headers: "Headers",
body: "Body",
body_kv: "Key Value",
body_text: "Raw",
assertions: {
label: "Assertion",
text: "Text",
regex: "Regex",
response_time: "Response Time",
select_type: "Please select a request type",
select_subject: "Please select a request subject",
select_condition: "Please select a condition",
contains: "Contains",
not_contains: "Not contains",
equals: "Equals",
start_with: "Start with",
end_with: "End With",
value: "Value",
expression: "Expression",
response_in_time: "Response in time",
},
extract: "Extract from response",
}
},
api_report: {
request: "Request",
request_body: "Body",
request_headers: "Headers",
request_cookie: "Cookie",
response: "Response",
delete_confirm: 'Confirm delete : ',
scenario_name: "Scenario name",
response_time: "Response time(ms)",
latency: "Latency",
request_size: "Request Size",
response_size: "Response Size",
response_code: "Response Code",
response_message: "Response Message",
error: "Error",
assertions: "Assertions",
assertions_pass: "Passed Assertions",
assertions_name: "Assertion Name",
assertions_message: "Assertion Message",
assertions_is_success: "Fail/Success",
result: "Result",
success: "Success",
fail: "Fail",
},
test_track: {
test_track: "Test Track",

View File

@ -96,6 +96,7 @@ export default {
'special_characters_are_not_supported': '不支持特殊字符',
'mobile_number_format_is_incorrect': '手机号码格式不正确',
'email_format_is_incorrect': '邮箱格式不正确',
'password_format_is_incorrect':'密码格式不正确(至少8-16个字符至少1个大写字母1个小写字母和1个数字)',
},
user: {
'create': '创建用户',
@ -103,6 +104,7 @@ export default {
'input_name': '请输入用户名',
'input_id': '请输入ID',
'input_email': '请输入邮箱',
'input_password':'请输入密码',
'special_characters_are_not_supported': '不支持特殊字符',
'mobile_number_format_is_incorrect': '手机号码格式不正确',
'email_format_is_incorrect': '邮箱格式不正确',
@ -224,7 +226,7 @@ export default {
response_time: "响应时间",
select_type: "请选择类型",
select_subject: "请选择对象",
select_contains: "请选择条件",
select_condition: "请选择条件",
contains: "包含",
not_contains: "不包含",
equals: "等于",
@ -237,6 +239,30 @@ export default {
extract: "提取",
}
},
api_report: {
request: "请求",
request_body: "请求内容",
request_headers: "请求头",
request_cookie: "Cookie",
response: "响应",
delete_confirm: '确认删除报告: ',
scenario_name: "场景名称",
response_time: "响应时间(ms)",
latency: "网络延迟",
request_size: "请求大小",
response_size: "响应大小",
response_code: "状态码",
response_message: "响应报文",
error: "错误",
assertions: "断言",
assertions_pass: "成功断言",
assertions_name: "断言名称",
assertions_message: "断言信息",
assertions_is_success: "是否成功",
result: "结果",
success: "成功",
fail: "失败",
},
test_track: {
test_track: "测试跟踪",
confirm: "确 定",