feat(接口定义): TCP 协议基础完成
This commit is contained in:
parent
84e99e1b68
commit
cbc9c42122
|
@ -3,6 +3,7 @@ package io.metersphere.api.controller;
|
|||
import com.github.pagehelper.Page;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import io.metersphere.api.dto.APIReportResult;
|
||||
import io.metersphere.api.dto.ApiTestImportRequest;
|
||||
import io.metersphere.api.dto.definition.ApiDefinitionRequest;
|
||||
import io.metersphere.api.dto.definition.ApiDefinitionResult;
|
||||
import io.metersphere.api.dto.definition.RunDefinitionRequest;
|
||||
|
@ -81,4 +82,11 @@ public class ApiDefinitionController {
|
|||
return apiDefinitionService.getDbResult(testId);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/import", consumes = {"multipart/form-data"})
|
||||
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
||||
public String testCaseImport(@RequestPart(value = "file", required = false) MultipartFile file, @RequestPart("request") ApiTestImportRequest request) {
|
||||
return apiDefinitionService.apiTestImport(file, request);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import lombok.Setter;
|
|||
@Getter
|
||||
public class ApiTestImportRequest {
|
||||
private String name;
|
||||
private String moduleId;
|
||||
private String modulePath;
|
||||
private String environmentId;
|
||||
private String projectId;
|
||||
private String platform;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package io.metersphere.api.dto.definition.parse;
|
||||
|
||||
import io.metersphere.api.dto.definition.ApiDefinitionResult;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ApiDefinitionImport {
|
||||
private String projectName;
|
||||
private String protocol;
|
||||
private List<ApiDefinitionResult> data;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.metersphere.api.dto.definition.parse.postman;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class PostmanCollection {
|
||||
|
||||
private PostmanCollectionInfo info;
|
||||
private List<PostmanItem> item;
|
||||
private List<PostmanKeyValue> variable;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.metersphere.api.dto.definition.parse.postman;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PostmanCollectionInfo {
|
||||
|
||||
@JSONField(name = "_postman_id")
|
||||
private String postmanId;
|
||||
private String name;
|
||||
private String schema;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package io.metersphere.api.dto.definition.parse.postman;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class PostmanItem {
|
||||
private String name;
|
||||
private PostmanRequest request;
|
||||
private List<PostmanItem> item;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.metersphere.api.dto.definition.parse.postman;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PostmanKeyValue {
|
||||
private String key;
|
||||
private String value;
|
||||
private String type;
|
||||
|
||||
public PostmanKeyValue() {
|
||||
}
|
||||
|
||||
public PostmanKeyValue(String key, String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.metersphere.api.dto.definition.parse.postman;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class PostmanRequest {
|
||||
|
||||
private String method;
|
||||
private String schema;
|
||||
private List<PostmanKeyValue> header;
|
||||
private JSONObject body;
|
||||
private JSONObject auth;
|
||||
private PostmanUrl url;
|
||||
private String description;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package io.metersphere.api.dto.definition.parse.postman;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class PostmanUrl {
|
||||
|
||||
private String raw;
|
||||
private String protocol;
|
||||
private String port;
|
||||
private List<PostmanKeyValue> query;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.metersphere.api.dto.definition.parse.swagger;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class SwaggerApi {
|
||||
private String swagger;
|
||||
private SwaggerInfo info;
|
||||
private String host;
|
||||
private String basePath;
|
||||
private List<String> schemes;
|
||||
private List<SwaggerTag> tags;
|
||||
private JSONObject paths;
|
||||
private JSONObject definitions;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package io.metersphere.api.dto.definition.parse.swagger;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SwaggerInfo {
|
||||
private String version;
|
||||
private String title;
|
||||
private String description;
|
||||
private String termsOfService;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.metersphere.api.dto.definition.parse.swagger;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SwaggerParameter {
|
||||
private String name;
|
||||
private String in;
|
||||
private String description;
|
||||
private Boolean required;
|
||||
private String type;
|
||||
private String format;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package io.metersphere.api.dto.definition.parse.swagger;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class SwaggerRequest {
|
||||
private List<String> tags;
|
||||
private String summary;
|
||||
private String description;
|
||||
private String operationId;
|
||||
private List<String> consumes;
|
||||
private List<String> produces;
|
||||
private List<SwaggerParameter> parameters;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.metersphere.api.dto.definition.parse.swagger;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SwaggerTag {
|
||||
private String name;
|
||||
private String description;
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package io.metersphere.api.parse;
|
||||
|
||||
import io.metersphere.api.dto.ApiTestImportRequest;
|
||||
import io.metersphere.api.dto.definition.parse.ApiDefinitionImport;
|
||||
import io.metersphere.api.dto.parse.ApiImport;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface ApiImportParser {
|
||||
ApiImport parse(InputStream source, ApiTestImportRequest request);
|
||||
ApiDefinitionImport parseApi(InputStream source, ApiTestImportRequest request);
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONArray;
|
|||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.parser.Feature;
|
||||
import io.metersphere.api.dto.ApiTestImportRequest;
|
||||
import io.metersphere.api.dto.definition.parse.ApiDefinitionImport;
|
||||
import io.metersphere.api.dto.parse.ApiImport;
|
||||
import io.metersphere.api.dto.scenario.request.RequestType;
|
||||
import io.metersphere.commons.constants.MsRequestBodyType;
|
||||
|
@ -22,6 +23,13 @@ public class MsParser extends ApiImportAbstractParser {
|
|||
return apiImport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiDefinitionImport parseApi(InputStream source, ApiTestImportRequest request) {
|
||||
String testStr = getApiTestStr(source);
|
||||
ApiDefinitionImport apiImport = JSON.parseObject(testStr, ApiDefinitionImport.class);
|
||||
return apiImport;
|
||||
}
|
||||
|
||||
private String parsePluginFormat(String testStr) {
|
||||
JSONObject testObject = JSONObject.parseObject(testStr, Feature.OrderedField);
|
||||
if (testObject.get("scenarios") != null) {
|
||||
|
|
|
@ -3,6 +3,11 @@ package io.metersphere.api.parse;
|
|||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.metersphere.api.dto.ApiTestImportRequest;
|
||||
import io.metersphere.api.dto.definition.ApiDefinitionResult;
|
||||
import io.metersphere.api.dto.definition.parse.ApiDefinitionImport;
|
||||
import io.metersphere.api.dto.definition.request.MsTestElement;
|
||||
import io.metersphere.api.dto.definition.request.configurations.MsHeaderManager;
|
||||
import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy;
|
||||
import io.metersphere.api.dto.parse.ApiImport;
|
||||
import io.metersphere.api.dto.parse.postman.*;
|
||||
import io.metersphere.api.dto.scenario.Body;
|
||||
|
@ -10,12 +15,17 @@ import io.metersphere.api.dto.scenario.KeyValue;
|
|||
import io.metersphere.api.dto.scenario.Scenario;
|
||||
import io.metersphere.api.dto.scenario.request.HttpRequest;
|
||||
import io.metersphere.api.dto.scenario.request.Request;
|
||||
import io.metersphere.api.dto.scenario.request.RequestType;
|
||||
import io.metersphere.commons.constants.MsRequestBodyType;
|
||||
import io.metersphere.commons.constants.PostmanRequestBodyMode;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jorphan.collections.HashTree;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PostmanParser extends ApiImportAbstractParser {
|
||||
|
||||
|
@ -38,6 +48,67 @@ public class PostmanParser extends ApiImportAbstractParser {
|
|||
return apiImport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiDefinitionImport parseApi(InputStream source, ApiTestImportRequest request) {
|
||||
String testStr = getApiTestStr(source);
|
||||
PostmanCollection postmanCollection = JSON.parseObject(testStr, PostmanCollection.class);
|
||||
List<PostmanKeyValue> variables = postmanCollection.getVariable();
|
||||
ApiDefinitionImport apiImport = new ApiDefinitionImport();
|
||||
List<ApiDefinitionResult> requests = new ArrayList<>();
|
||||
|
||||
parseItem(postmanCollection.getItem(), variables, requests);
|
||||
apiImport.setData(requests);
|
||||
return apiImport;
|
||||
}
|
||||
|
||||
private void parseItem(List<PostmanItem> items, List<PostmanKeyValue> variables, List<ApiDefinitionResult> scenarios) {
|
||||
for (PostmanItem item : items) {
|
||||
List<PostmanItem> childItems = item.getItem();
|
||||
if (childItems != null) {
|
||||
parseItem(childItems, variables, scenarios);
|
||||
} else {
|
||||
ApiDefinitionResult request = parsePostman(item);
|
||||
if (request != null) {
|
||||
scenarios.add(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ApiDefinitionResult parsePostman(PostmanItem requestItem) {
|
||||
PostmanRequest requestDesc = requestItem.getRequest();
|
||||
if (requestDesc == null) {
|
||||
return null;
|
||||
}
|
||||
PostmanUrl url = requestDesc.getUrl();
|
||||
ApiDefinitionResult request = new ApiDefinitionResult();
|
||||
request.setName(requestItem.getName());
|
||||
request.setPath(url.getRaw());
|
||||
request.setMethod(requestDesc.getMethod());
|
||||
request.setProtocol(RequestType.HTTP);
|
||||
MsHTTPSamplerProxy requestElement = new MsHTTPSamplerProxy();
|
||||
requestElement.setName(requestItem.getName() + "Postman MHTTPSamplerProxy");
|
||||
requestElement.setBody(parseBody(requestDesc));
|
||||
requestElement.setArguments(parseKeyValue(url.getQuery()));
|
||||
requestElement.setProtocol(RequestType.HTTP);
|
||||
requestElement.setPath(url.getRaw());
|
||||
requestElement.setMethod(requestDesc.getMethod());
|
||||
requestElement.setId(UUID.randomUUID().toString());
|
||||
requestElement.setRest(new ArrayList<KeyValue>());
|
||||
MsHeaderManager headerManager = new MsHeaderManager();
|
||||
headerManager.setId(UUID.randomUUID().toString());
|
||||
headerManager.setName(requestItem.getName() + "Postman MsHeaderManager");
|
||||
headerManager.setHeaders(parseKeyValue(requestDesc.getHeader()));
|
||||
HashTree tree = new HashTree();
|
||||
tree.add(headerManager);
|
||||
LinkedList<MsTestElement> list = new LinkedList<>();
|
||||
list.add(headerManager);
|
||||
requestElement.setHashTree(list);
|
||||
request.setRequest(JSON.toJSONString(requestElement));
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
private List<KeyValue> parseKeyValue(List<PostmanKeyValue> postmanKeyValues) {
|
||||
if (postmanKeyValues == null) {
|
||||
return null;
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
package io.metersphere.api.parse;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.metersphere.api.dto.ApiTestImportRequest;
|
||||
import io.metersphere.api.dto.definition.ApiDefinitionResult;
|
||||
import io.metersphere.api.dto.definition.parse.ApiDefinitionImport;
|
||||
import io.metersphere.api.dto.definition.request.MsTestElement;
|
||||
import io.metersphere.api.dto.definition.request.configurations.MsHeaderManager;
|
||||
import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy;
|
||||
import io.metersphere.api.dto.parse.ApiImport;
|
||||
import io.metersphere.api.dto.scenario.Body;
|
||||
import io.metersphere.api.dto.scenario.KeyValue;
|
||||
import io.metersphere.api.dto.scenario.Scenario;
|
||||
import io.metersphere.api.dto.scenario.request.HttpRequest;
|
||||
import io.metersphere.api.dto.scenario.request.Request;
|
||||
import io.metersphere.api.dto.scenario.request.RequestType;
|
||||
import io.metersphere.commons.constants.MsRequestBodyType;
|
||||
import io.metersphere.commons.constants.SwaggerParameterType;
|
||||
import io.swagger.models.*;
|
||||
|
@ -19,6 +26,7 @@ import io.swagger.models.properties.Property;
|
|||
import io.swagger.models.properties.RefProperty;
|
||||
import io.swagger.parser.SwaggerParser;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jorphan.collections.HashTree;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
|
@ -39,6 +47,52 @@ public class Swagger2Parser extends ApiImportAbstractParser {
|
|||
return apiImport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiDefinitionImport parseApi(InputStream source, ApiTestImportRequest request) {
|
||||
ApiImport apiImport = this.parse(source, request);
|
||||
ApiDefinitionImport definitionImport = new ApiDefinitionImport();
|
||||
definitionImport.setData(parseSwagger(apiImport));
|
||||
return definitionImport;
|
||||
}
|
||||
|
||||
private List<ApiDefinitionResult> parseSwagger(ApiImport apiImport) {
|
||||
List<ApiDefinitionResult> results = new LinkedList<>();
|
||||
apiImport.getScenarios().forEach(item -> {
|
||||
item.getRequests().forEach(childItem -> {
|
||||
if (childItem instanceof HttpRequest) {
|
||||
HttpRequest res = (HttpRequest) childItem;
|
||||
ApiDefinitionResult request = new ApiDefinitionResult();
|
||||
request.setName(res.getName());
|
||||
request.setPath(res.getPath());
|
||||
request.setMethod(res.getMethod());
|
||||
request.setProtocol(RequestType.HTTP);
|
||||
MsHTTPSamplerProxy requestElement = new MsHTTPSamplerProxy();
|
||||
requestElement.setName(res.getName() + "Postman MHTTPSamplerProxy");
|
||||
requestElement.setBody(res.getBody());
|
||||
requestElement.setArguments(res.getParameters());
|
||||
requestElement.setProtocol(RequestType.HTTP);
|
||||
requestElement.setPath(res.getPath());
|
||||
requestElement.setMethod(res.getMethod());
|
||||
requestElement.setId(UUID.randomUUID().toString());
|
||||
requestElement.setRest(new ArrayList<KeyValue>());
|
||||
MsHeaderManager headerManager = new MsHeaderManager();
|
||||
headerManager.setId(UUID.randomUUID().toString());
|
||||
headerManager.setName(res.getName() + "Postman MsHeaderManager");
|
||||
headerManager.setHeaders(res.getHeaders());
|
||||
HashTree tree = new HashTree();
|
||||
tree.add(headerManager);
|
||||
LinkedList<MsTestElement> list = new LinkedList<>();
|
||||
list.add(headerManager);
|
||||
requestElement.setHashTree(list);
|
||||
request.setRequest(JSON.toJSONString(requestElement));
|
||||
results.add(request);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<Scenario> parseRequests(Swagger swagger) {
|
||||
Map<String, Path> paths = swagger.getPaths();
|
||||
Set<String> pathNames = paths.keySet();
|
||||
|
@ -141,7 +195,7 @@ public class Swagger2Parser extends ApiImportAbstractParser {
|
|||
Model model = definitions.get(simpleRef);
|
||||
HashSet<String> refSet = new HashSet<>();
|
||||
refSet.add(simpleRef);
|
||||
if (model != null ) {
|
||||
if (model != null) {
|
||||
JSONObject bodyParameters = getBodyJSONObjectParameters(model.getProperties(), definitions, refSet);
|
||||
body.setRaw(bodyParameters.toJSONString());
|
||||
}
|
||||
|
|
|
@ -3,10 +3,14 @@ package io.metersphere.api.service;
|
|||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.metersphere.api.dto.APIReportResult;
|
||||
import io.metersphere.api.dto.ApiTestImportRequest;
|
||||
import io.metersphere.api.dto.definition.*;
|
||||
import io.metersphere.api.dto.definition.parse.ApiDefinitionImport;
|
||||
import io.metersphere.api.dto.scenario.request.RequestType;
|
||||
import io.metersphere.api.jmeter.JMeterService;
|
||||
import io.metersphere.api.jmeter.TestResult;
|
||||
import io.metersphere.api.parse.ApiImportParser;
|
||||
import io.metersphere.api.parse.ApiImportParserFactory;
|
||||
import io.metersphere.base.domain.*;
|
||||
import io.metersphere.base.mapper.ApiDefinitionExecResultMapper;
|
||||
import io.metersphere.base.mapper.ApiDefinitionMapper;
|
||||
|
@ -29,10 +33,7 @@ import sun.security.util.Cache;
|
|||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -206,6 +207,39 @@ public class ApiDefinitionService {
|
|||
return test;
|
||||
}
|
||||
|
||||
private ApiDefinition createTest(ApiDefinitionResult request) {
|
||||
SaveApiDefinitionRequest saveReq = new SaveApiDefinitionRequest();
|
||||
saveReq.setId(UUID.randomUUID().toString());
|
||||
saveReq.setName(request.getName());
|
||||
saveReq.setProtocol(request.getProtocol());
|
||||
saveReq.setProjectId(request.getProjectId());
|
||||
saveReq.setPath(request.getPath());
|
||||
checkNameExist(saveReq);
|
||||
final ApiDefinition test = new ApiDefinition();
|
||||
test.setId(request.getId());
|
||||
test.setName(request.getName());
|
||||
test.setProtocol(request.getProtocol());
|
||||
test.setMethod(request.getMethod());
|
||||
test.setPath(request.getPath());
|
||||
test.setModuleId(request.getModuleId());
|
||||
test.setProjectId(request.getProjectId());
|
||||
test.setRequest(request.getRequest());
|
||||
test.setCreateTime(System.currentTimeMillis());
|
||||
test.setUpdateTime(System.currentTimeMillis());
|
||||
test.setStatus(APITestStatus.Underway.name());
|
||||
test.setModulePath(request.getModulePath());
|
||||
test.setResponse(request.getResponse());
|
||||
test.setEnvironmentId(request.getEnvironmentId());
|
||||
if (request.getUserId() == null) {
|
||||
test.setUserId(Objects.requireNonNull(SessionUtils.getUser()).getId());
|
||||
} else {
|
||||
test.setUserId(request.getUserId());
|
||||
}
|
||||
test.setDescription(request.getDescription());
|
||||
apiDefinitionMapper.insert(test);
|
||||
return test;
|
||||
}
|
||||
|
||||
private void deleteFileByTestId(String apiId) {
|
||||
ApiTestFileExample apiTestFileExample = new ApiTestFileExample();
|
||||
apiTestFileExample.createCriteria().andTestIdEqualTo(apiId);
|
||||
|
@ -276,4 +310,31 @@ public class ApiDefinitionService {
|
|||
reportResult.setContent(result.getContent());
|
||||
return reportResult;
|
||||
}
|
||||
|
||||
|
||||
public String apiTestImport(MultipartFile file, ApiTestImportRequest request) {
|
||||
ApiImportParser apiImportParser = ApiImportParserFactory.getApiImportParser(request.getPlatform());
|
||||
ApiDefinitionImport apiImport = null;
|
||||
try {
|
||||
apiImport = Objects.requireNonNull(apiImportParser).parseApi(file == null ? null : file.getInputStream(), request);
|
||||
} catch (Exception e) {
|
||||
LogUtil.error(e.getMessage(), e);
|
||||
MSException.throwException(Translator.get("parse_data_error"));
|
||||
}
|
||||
importApiTest(request, apiImport);
|
||||
return "SUCCESS";
|
||||
}
|
||||
|
||||
private void importApiTest(ApiTestImportRequest importRequest, ApiDefinitionImport apiImport) {
|
||||
apiImport.getData().forEach(item -> {
|
||||
item.setProjectId(importRequest.getProjectId());
|
||||
item.setModuleId(importRequest.getModuleId());
|
||||
item.setModulePath(importRequest.getModulePath());
|
||||
item.setEnvironmentId(importRequest.getEnvironmentId());
|
||||
item.setId(UUID.randomUUID().toString());
|
||||
item.setUserId(null);
|
||||
createTest(item);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<ms-aside-container>
|
||||
<ms-node-tree @selectModule="selectModule" @getApiModuleTree="initTree" @changeProject="changeProject" @changeProtocol="changeProtocol"
|
||||
@refresh="refresh" @saveAsEdit="editApi"/>
|
||||
@refresh="refresh" @saveAsEdit="editApi" @debug="debug" @exportAPI="exportAPI"/>
|
||||
</ms-aside-container>
|
||||
|
||||
<ms-main-container>
|
||||
|
@ -69,7 +69,7 @@
|
|||
import MsApiConfig from "./components/ApiConfig";
|
||||
import MsDebugHttpPage from "./components/debug/DebugHttpPage";
|
||||
import MsRunTestHttpPage from "./components/runtest/RunTestHttpPage";
|
||||
import {getCurrentUser, getUUID} from "../../../../common/js/utils";
|
||||
import {downloadFile, getCurrentUser, getUUID} from "@/common/js/utils";
|
||||
|
||||
export default {
|
||||
name: "TestCase",
|
||||
|
@ -103,6 +103,11 @@
|
|||
}],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentProtocol() {
|
||||
this.handleCommand("closeAll");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCommand(e) {
|
||||
if (e === "add") {
|
||||
|
@ -154,6 +159,9 @@
|
|||
this.apiDefaultTab = newTabName;
|
||||
}
|
||||
},
|
||||
debug() {
|
||||
this.handleTabsEdit(this.$t('api_test.definition.request.fast_debug'), "debug");
|
||||
},
|
||||
editApi(row) {
|
||||
this.currentApi = row;
|
||||
this.handleTabsEdit(row.name, "add");
|
||||
|
@ -168,6 +176,13 @@
|
|||
selectModule(data) {
|
||||
this.currentModule = data;
|
||||
},
|
||||
exportAPI() {
|
||||
if (!this.$refs.apiList[0].tableData) {
|
||||
return;
|
||||
}
|
||||
let obj = {projectName: this.currentProject.name, protocol: this.currentProtocol, data: this.$refs.apiList[0].tableData}
|
||||
downloadFile("导出API.json", JSON.stringify(obj));
|
||||
},
|
||||
refresh(data) {
|
||||
this.$refs.apiList[0].initApiTable(data);
|
||||
},
|
||||
|
|
|
@ -168,10 +168,10 @@
|
|||
<script>
|
||||
import MsTag from "../../../common/components/MsTag";
|
||||
import MsTipButton from "../../../common/components/MsTipButton";
|
||||
import MsApiRequestForm from "./request/ApiRequestForm";
|
||||
import MsApiRequestForm from "./request/http/ApiRequestForm";
|
||||
import {downloadFile, getUUID} from "@/common/js/utils";
|
||||
import {parseEnvironment} from "../model/EnvironmentModel";
|
||||
import ApiEnvironmentConfig from "../../test/components/ApiEnvironmentConfig";
|
||||
import ApiEnvironmentConfig from "./environment/ApiEnvironmentConfig";
|
||||
import {PRIORITY, RESULT_MAP} from "../model/JsonData";
|
||||
import MsApiAssertions from "./assertion/ApiAssertions";
|
||||
import MsRun from "./Run";
|
||||
|
|
|
@ -5,26 +5,33 @@
|
|||
<ms-add-complete-http-api @runTest="runTest" @saveApi="saveApi" :request="request" :headers="headers" :response="response" :basisData="currentApi"
|
||||
:moduleOptions="moduleOptions" :currentProject="currentProject"
|
||||
v-if="currentProtocol === 'HTTP'"/>
|
||||
|
||||
<!-- TCP -->
|
||||
<ms-api-tcp-request-form :request="request" :currentProject="currentProject" :basisData="currentApi" :moduleOptions="moduleOptions" :maintainerOptions="maintainerOptions" v-if="currentProtocol === 'TCP'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsAddCompleteHttpApi from "./complete/AddCompleteHttpApi";
|
||||
import MsApiTcpRequestForm from "./complete/ApiTcpRequestForm";
|
||||
import {ResponseFactory, Body} from "../model/ApiTestModel";
|
||||
import {getUUID} from "@/common/js/utils";
|
||||
import {createComponent, Request} from "./jmeter/components";
|
||||
import Sampler from "./jmeter/components/sampler/sampler";
|
||||
import HeaderManager from "./jmeter/components/configurations/header-manager";
|
||||
import {WORKSPACE_ID} from '@/common/js/constants';
|
||||
|
||||
export default {
|
||||
name: "ApiConfig",
|
||||
components: {MsAddCompleteHttpApi},
|
||||
components: {MsAddCompleteHttpApi, MsApiTcpRequestForm},
|
||||
data() {
|
||||
return {
|
||||
reqUrl: "",
|
||||
request: Sampler,
|
||||
config: {},
|
||||
response: {},
|
||||
headers: [],
|
||||
maintainerOptions: [],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
@ -34,6 +41,7 @@
|
|||
currentProtocol: String,
|
||||
},
|
||||
created() {
|
||||
this.getMaintainerOptions();
|
||||
switch (this.currentProtocol) {
|
||||
case Request.TYPES.SQL:
|
||||
this.request = createComponent("SQL");
|
||||
|
@ -70,6 +78,13 @@
|
|||
this.$emit('runTest', data);
|
||||
});
|
||||
},
|
||||
getMaintainerOptions() {
|
||||
let workspaceId = localStorage.getItem(WORKSPACE_ID);
|
||||
this.$post('/user/ws/member/tester/list', {workspaceId: workspaceId}, response => {
|
||||
this.maintainerOptions = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
createHttp() {
|
||||
if (this.currentApi.request != undefined && this.currentApi.request != null) {
|
||||
this.request = JSON.parse(this.currentApi.request);
|
||||
|
@ -103,32 +118,36 @@
|
|||
data.bodyUploadIds = [];
|
||||
let request = data.request;
|
||||
if (request.body) {
|
||||
request.body.kvs.forEach(param => {
|
||||
if (param.files) {
|
||||
param.files.forEach(item => {
|
||||
if (item.file) {
|
||||
let fileId = getUUID().substring(0, 8);
|
||||
item.name = item.file.name;
|
||||
item.id = fileId;
|
||||
data.bodyUploadIds.push(fileId);
|
||||
bodyUploadFiles.push(item.file);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
request.body.binary.forEach(param => {
|
||||
if (param.files) {
|
||||
param.files.forEach(item => {
|
||||
if (item.file) {
|
||||
let fileId = getUUID().substring(0, 8);
|
||||
item.name = item.file.name;
|
||||
item.id = fileId;
|
||||
data.bodyUploadIds.push(fileId);
|
||||
bodyUploadFiles.push(item.file);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (request.body.kvs) {
|
||||
request.body.kvs.forEach(param => {
|
||||
if (param.files) {
|
||||
param.files.forEach(item => {
|
||||
if (item.file) {
|
||||
let fileId = getUUID().substring(0, 8);
|
||||
item.name = item.file.name;
|
||||
item.id = fileId;
|
||||
data.bodyUploadIds.push(fileId);
|
||||
bodyUploadFiles.push(item.file);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (request.body.binary) {
|
||||
request.body.binary.forEach(param => {
|
||||
if (param.files) {
|
||||
param.files.forEach(item => {
|
||||
if (item.file) {
|
||||
let fileId = getUUID().substring(0, 8);
|
||||
item.name = item.file.name;
|
||||
item.id = fileId;
|
||||
data.bodyUploadIds.push(fileId);
|
||||
bodyUploadFiles.push(item.file);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return bodyUploadFiles;
|
||||
},
|
||||
|
|
|
@ -18,7 +18,20 @@
|
|||
<el-input style="width: 175px; padding-left: 3px" :placeholder="$t('test_track.module.search')" v-model="filterText"
|
||||
size="small">
|
||||
<template v-slot:append>
|
||||
<el-button icon="el-icon-folder-add" @click="addApi"></el-button>
|
||||
<!--
|
||||
<el-button icon="el-icon-folder-add" @click="addApi"></el-button>
|
||||
-->
|
||||
<el-dropdown size="small" split-button type="primary" class="ms-api-buttion" @click="handleCommand('add')"
|
||||
@command="handleCommand">
|
||||
<el-button icon="el-icon-folder-add" @click="addApi"></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="add-api">{{$t('api_test.definition.request.title')}}</el-dropdown-item>
|
||||
<!--<el-dropdown-item command="add-module">{{$t('api_test.definition.request.add_module')}}</el-dropdown-item>-->
|
||||
<el-dropdown-item command="debug">{{$t('api_test.definition.request.fast_debug')}}</el-dropdown-item>
|
||||
<el-dropdown-item command="import">{{$t('api_test.api_import.label')}}</el-dropdown-item>
|
||||
<el-dropdown-item command="export">{{$t('report.export')}}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
|
@ -81,6 +94,7 @@
|
|||
</el-tree>
|
||||
|
||||
<ms-add-basis-http-api :current-protocol="value" ref="httpApi"></ms-add-basis-http-api>
|
||||
<api-import ref="apiImport" @refresh="refresh"/>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -90,12 +104,14 @@
|
|||
import MsAddBasisHttpApi from "./basis/AddBasisApi";
|
||||
import SelectMenu from "../../../track/common/SelectMenu";
|
||||
import {OPTIONS, DEFAULT_DATA} from "../model/JsonData";
|
||||
import ApiImport from "./import/ApiImport";
|
||||
|
||||
export default {
|
||||
name: 'MsApiModule',
|
||||
components: {
|
||||
MsAddBasisHttpApi,
|
||||
SelectMenu
|
||||
SelectMenu,
|
||||
ApiImport
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -140,6 +156,24 @@
|
|||
|
||||
}
|
||||
},
|
||||
handleCommand(e) {
|
||||
switch (e) {
|
||||
case "debug":
|
||||
this.$emit('debug');
|
||||
break;
|
||||
case "add-api":
|
||||
this.addApi();
|
||||
break;
|
||||
case "add-module":
|
||||
break;
|
||||
case "import":
|
||||
this.$refs.apiImport.open(this.currentModule);
|
||||
break;
|
||||
default:
|
||||
this.$emit('exportAPI');
|
||||
break;
|
||||
}
|
||||
},
|
||||
buildNodePath(node, option, moduleOptions) {
|
||||
//递归构建节点路径
|
||||
option.id = node.id;
|
||||
|
@ -382,7 +416,7 @@
|
|||
return data.name.indexOf(value) !== -1;
|
||||
},
|
||||
addApi() {
|
||||
this.$refs.httpApi.open(this.currentModule, this.currentProject.id);
|
||||
this.$refs.httpApi.open(this.currentModule);
|
||||
},
|
||||
// 项目相关方法
|
||||
changeProject(project) {
|
||||
|
@ -460,4 +494,8 @@
|
|||
/deep/ .el-tree-node__content {
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
.ms-api-buttion {
|
||||
width: 30px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiRequestForm from "../request/ApiRequestForm";
|
||||
import MsApiRequestForm from "../request/http/ApiRequestForm";
|
||||
import MsResponseText from "../response/ResponseText";
|
||||
import {WORKSPACE_ID} from '../../../../../../common/js/constants';
|
||||
import {REQ_METHOD, API_STATUS} from "../../model/JsonData";
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
<template xmlns:el-col="http://www.w3.org/1999/html">
|
||||
<!-- 操作按钮 -->
|
||||
<div style="background-color: white;">
|
||||
<el-row>
|
||||
<el-col>
|
||||
<!--操作按钮-->
|
||||
<div style="float: right;margin-right: 20px;margin-top: 20px">
|
||||
<el-button type="primary" size="small" @click="validateApi">{{$t('commons.save')}}</el-button>
|
||||
<el-button type="primary" size="small" @click="runTest">{{$t('commons.test')}}</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 基础信息 -->
|
||||
<p class="tip">{{$t('test_track.plan_view.base_info')}} </p>
|
||||
<br/>
|
||||
<el-row>
|
||||
<el-col>
|
||||
<ms-basis-api :moduleOptions="moduleOptions" :basisData="basisData" ref="basicForm" @callback="saveApi"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 请求参数 -->
|
||||
<p class="tip">{{$t('api_test.definition.request.req_param')}} </p>
|
||||
<el-row>
|
||||
<el-col :span="21" style="padding-bottom: 50px">
|
||||
<el-form class="tcp" :model="request" :rules="rules" ref="request" label-width="auto" :disabled="isReadOnly">
|
||||
|
||||
<el-form-item/>
|
||||
<!-- <el-form-item :label="$t('api_test.request.name')" prop="name">
|
||||
<el-input v-model="request.name" size="small" maxlength="300" show-word-limit/>
|
||||
</el-form-item>-->
|
||||
|
||||
<el-form-item label="TCPClient" prop="classname">
|
||||
<el-select v-model="request.classname" style="width: 100%" size="small">
|
||||
<el-option v-for="c in classes" :key="c" :label="c" :value="c"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="16">
|
||||
<el-form-item :label="$t('api_test.request.tcp.server')" prop="server">
|
||||
<el-input v-model="request.server" maxlength="300" show-word-limit size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item :label="$t('api_test.request.tcp.port')" prop="port" label-width="60px">
|
||||
<el-input-number v-model="request.port" controls-position="right" :min="0" :max="65535" size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('api_test.request.tcp.connect')" prop="ctimeout">
|
||||
<el-input-number v-model="request.ctimeout" controls-position="right" :min="0" size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('api_test.request.tcp.response')" prop="timeout">
|
||||
<el-input-number v-model="request.timeout" controls-position="right" :min="0" size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('api_test.request.tcp.so_linger')" prop="soLinger">
|
||||
<el-input v-model="request.soLinger" size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('api_test.request.tcp.eol_byte')" prop="eolByte">
|
||||
<el-input v-model="request.eolByte" size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('api_test.request.refer_to_environment')">
|
||||
<el-switch
|
||||
v-model="request.useEnvironment"
|
||||
@change="useEnvironmentChange">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('api_test.request.tcp.re_use_connection')">
|
||||
<el-checkbox v-model="request.reUseConnection"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('api_test.request.tcp.close_connection')">
|
||||
<el-checkbox v-model="request.closeConnection"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('api_test.request.tcp.no_delay')">
|
||||
<el-checkbox v-model="request.nodelay"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.request.tcp.username')" prop="username">
|
||||
<el-input v-model="request.username" maxlength="100" show-word-limit size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.request.tcp.password')" prop="password">
|
||||
<el-input v-model="request.password" maxlength="30" show-word-limit show-password
|
||||
autocomplete="new-password" size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item :label="$t('api_test.request.tcp.request')" prop="request">
|
||||
<div class="send-request">
|
||||
<ms-code-edit mode="text" :read-only="isReadOnly" :data.sync="request.request"
|
||||
:modes="['text', 'json', 'xml', 'html']" theme="eclipse"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
|
||||
<div v-for="row in request.hashTree" :key="row.id" v-loading="isReloadData" style="margin-left: 30px">
|
||||
<!-- 前置脚本 -->
|
||||
<ms-jsr233-processor v-if="row.label ==='JSR223 PreProcessor'" @remove="remove" :is-read-only="false" :title="$t('api_test.definition.request.pre_script')" style-type="warning"
|
||||
:jsr223-processor="row"/>
|
||||
<!--后置脚本-->
|
||||
<ms-jsr233-processor v-if="row.label ==='JSR223 PostProcessor'" @remove="remove" :is-read-only="false" :title="$t('api_test.definition.request.post_script')" style-type="success"
|
||||
:jsr223-processor="row"/>
|
||||
<!--断言规则-->
|
||||
<ms-api-assertions v-if="row.type==='Assertions'" @remove="remove" :is-read-only="isReadOnly" :assertions="row"/>
|
||||
<!--提取规则-->
|
||||
<ms-api-extract :is-read-only="isReadOnly" @remove="remove" v-if="row.type==='Extract'" :extract="row"/>
|
||||
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="3" class="ms-left-cell">
|
||||
|
||||
<el-button class="ms-left-buttion" size="small" type="warning" @click="addPre" plain>+{{$t('api_test.definition.request.pre_script')}}</el-button>
|
||||
<br/>
|
||||
<el-button class="ms-left-buttion" size="small" type="success" @click="addPost" plain>+{{$t('api_test.definition.request.post_script')}}</el-button>
|
||||
<br/>
|
||||
<el-button class="ms-left-buttion" size="small" type="danger" @click="addAssertions" plain>+{{$t('api_test.definition.request.assertions_rule')}}</el-button>
|
||||
<br/>
|
||||
<el-button class="ms-left-buttion" size="small" type="info" @click="addExtract" plain>+{{$t('api_test.definition.request.extract_param')}}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiAssertions from "../assertion/ApiAssertions";
|
||||
import MsApiExtract from "../extract/ApiExtract";
|
||||
import MsJsr233Processor from "../processor/Jsr233Processor";
|
||||
import MsCodeEdit from "@/business/components/common/components/MsCodeEdit";
|
||||
import TCPSampler from "../jmeter/components/sampler/tcp-sampler";
|
||||
import {createComponent} from "../jmeter/components";
|
||||
import {Assertions, Extract} from "../../model/ApiTestModel";
|
||||
import {API_STATUS} from "../../model/JsonData";
|
||||
import MsBasisApi from "./BasisApi";
|
||||
|
||||
export default {
|
||||
name: "MsApiTcpRequestForm",
|
||||
components: {MsCodeEdit, MsJsr233Processor, MsApiExtract, MsApiAssertions, MsBasisApi},
|
||||
props: {
|
||||
request: {},
|
||||
basisData: {},
|
||||
currentProject: {},
|
||||
maintainerOptions: Array,
|
||||
moduleOptions: Array,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeName: "assertions",
|
||||
classes: TCPSampler.CLASSES,
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: this.$t('test_track.case.input_name'), trigger: 'blur'},
|
||||
{max: 50, message: this.$t('test_track.length_less_than') + '50', trigger: 'blur'}
|
||||
],
|
||||
},
|
||||
isReloadData: false,
|
||||
options: API_STATUS,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
useEnvironmentChange(value) {
|
||||
if (value && !this.request.environment) {
|
||||
this.$error(this.$t('api_test.request.please_add_environment_to_scenario'), 2000);
|
||||
this.request.useEnvironment = false;
|
||||
}
|
||||
this.$refs["request"].clearValidate();
|
||||
},
|
||||
addPre() {
|
||||
let jsr223PreProcessor = createComponent("JSR223PreProcessor");
|
||||
this.request.hashTree.push(jsr223PreProcessor);
|
||||
this.reload();
|
||||
},
|
||||
addPost() {
|
||||
let jsr223PostProcessor = createComponent("JSR223PostProcessor");
|
||||
this.request.hashTree.push(jsr223PostProcessor);
|
||||
this.reload();
|
||||
},
|
||||
addAssertions() {
|
||||
let assertions = new Assertions();
|
||||
this.request.hashTree.push(assertions);
|
||||
this.reload();
|
||||
},
|
||||
addExtract() {
|
||||
let jsonPostProcessor = new Extract();
|
||||
this.request.hashTree.push(jsonPostProcessor);
|
||||
this.reload();
|
||||
},
|
||||
remove(row) {
|
||||
let index = this.request.hashTree.indexOf(row);
|
||||
this.request.hashTree.splice(index, 1);
|
||||
this.reload();
|
||||
},
|
||||
reload() {
|
||||
this.isReloadData = true
|
||||
this.$nextTick(() => {
|
||||
this.isReloadData = false
|
||||
})
|
||||
},
|
||||
setParameter() {
|
||||
this.request.modulePath = this.getPath(this.basisData.moduleId);
|
||||
this.request.useEnvironment = undefined;
|
||||
},
|
||||
getPath(id) {
|
||||
if (id === null) {
|
||||
return null;
|
||||
}
|
||||
let path = this.moduleOptions.filter(function (item) {
|
||||
return item.id === id ? item.path : "";
|
||||
});
|
||||
return path[0].path;
|
||||
},
|
||||
validateApi() {
|
||||
this.basisData.method = "TCP";
|
||||
if (this.currentProject === null) {
|
||||
this.$error(this.$t('api_test.select_project'), 2000);
|
||||
return;
|
||||
}
|
||||
this.$refs['basicForm'].validate();
|
||||
},
|
||||
saveApi() {
|
||||
this.$emit('saveApi', this.request);
|
||||
},
|
||||
runTest() {
|
||||
alert(444);
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tcp >>> .el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.send-request {
|
||||
padding: 0px 0;
|
||||
height: 300px;
|
||||
border: 1px #DCDFE6 solid;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ms-left-cell {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.ms-left-buttion {
|
||||
margin: 6px 0px 8px 30px;
|
||||
}
|
||||
|
||||
.ms-query {
|
||||
background: #7F7F7F;
|
||||
color: white;
|
||||
height: 18px;
|
||||
border-radius: 42%;
|
||||
}
|
||||
|
||||
.ms-header {
|
||||
background: #783887;
|
||||
color: white;
|
||||
height: 18px;
|
||||
border-radius: 42%;
|
||||
}
|
||||
|
||||
/deep/ .el-form-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
padding: 3px 5px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #783887;
|
||||
margin: 0px 20px 0px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<el-form :model="basicForm" label-position="right" label-width="80px" size="small" :rules="rule" ref="basicForm" style="margin-right: 20px">
|
||||
<!-- 基础信息 -->
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('commons.name')" prop="name">
|
||||
<el-input class="ms-http-input" size="small" v-model="basicForm.name"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('test_track.module.module')" prop="moduleId">
|
||||
<el-select class="ms-http-input" size="small" v-model="basicForm.moduleId" style="width: 100%">
|
||||
<el-option v-for="item in moduleOptions" :key="item.id" :label="item.path" :value="item.id"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('commons.status')" prop="status">
|
||||
<el-select class="ms-http-input" size="small" v-model="basicForm.status" style="width: 100%">
|
||||
<el-option v-for="item in options" :key="item.id" :label="item.label" :value="item.id"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.definition.request.responsible')" prop="userId">
|
||||
<el-select v-model="basicForm.userId"
|
||||
:placeholder="$t('api_test.definition.request.responsible')" filterable size="small"
|
||||
class="ms-http-input" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in maintainerOptions"
|
||||
:key="item.id"
|
||||
:label="item.id + ' (' + item.name + ')'"
|
||||
:value="item.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col>
|
||||
<el-form-item :label="$t('commons.description')" prop="description">
|
||||
<el-input class="ms-http-textarea"
|
||||
v-model="basicForm.description"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 10}"
|
||||
:rows="2" size="small"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {API_STATUS} from "../../model/JsonData";
|
||||
import {WORKSPACE_ID} from '../../../../../../common/js/constants';
|
||||
|
||||
export default {
|
||||
name: "MsBasisApi",
|
||||
components: {},
|
||||
props: {
|
||||
currentProtocol: {
|
||||
type: String,
|
||||
default: "HTTP"
|
||||
},
|
||||
moduleOptions: Array,
|
||||
basisData: {},
|
||||
},
|
||||
created() {
|
||||
this.getMaintainerOptions();
|
||||
this.basicForm = this.basisData;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
basicForm: {},
|
||||
httpVisible: false,
|
||||
currentModule: {},
|
||||
projectId: "",
|
||||
maintainerOptions: [],
|
||||
rule: {
|
||||
name: [
|
||||
{required: true, message: this.$t('test_track.case.input_name'), trigger: 'blur'},
|
||||
{max: 50, message: this.$t('test_track.length_less_than') + '50', trigger: 'blur'}
|
||||
],
|
||||
userId: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],
|
||||
moduleId: [{required: true, message: this.$t('test_track.case.input_module'), trigger: 'change'}],
|
||||
status: [{required: true, message: this.$t('commons.please_select'), trigger: 'change'}],
|
||||
},
|
||||
value: API_STATUS[0].id,
|
||||
options: API_STATUS,
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getMaintainerOptions() {
|
||||
let workspaceId = localStorage.getItem(WORKSPACE_ID);
|
||||
this.$post('/user/ws/member/tester/list', {workspaceId: workspaceId}, response => {
|
||||
this.maintainerOptions = response.data;
|
||||
});
|
||||
},
|
||||
validate() {
|
||||
this.$refs['basicForm'].validate((valid) => {
|
||||
if (valid) {
|
||||
this.$emit('callback');
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -40,7 +40,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiRequestForm from "../request/ApiRequestForm";
|
||||
import MsApiRequestForm from "../request/http/ApiRequestForm";
|
||||
import MsResponseResult from "../response/ResponseResult";
|
||||
import MsRequestMetric from "../response/RequestMetric";
|
||||
import {getUUID, getCurrentUser} from "@/common/js/utils";
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<el-dialog :close-on-click-modal="false" :title="$t('api_test.environment.environment_config')"
|
||||
:visible.sync="visible" class="environment-dialog" width="60%"
|
||||
@close="close" append-to-body ref="environmentConfig">
|
||||
<el-container v-loading="result.loading">
|
||||
<ms-aside-item :enable-aside-hidden="false" :title="$t('api_test.environment.environment_list')"
|
||||
:data="environments" :item-operators="environmentOperators" :add-fuc="addEnvironment"
|
||||
:delete-fuc="deleteEnvironment" @itemSelected="environmentSelected" ref="environmentItems"/>
|
||||
<environment-edit :environment="currentEnvironment" ref="environmentEdit" @close="close"/>
|
||||
</el-container>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiCollapse from "../collapse/ApiCollapse";
|
||||
import MsApiCollapseItem from "../collapse/ApiCollapseItem";
|
||||
import draggable from 'vuedraggable';
|
||||
import MsContainer from "../../../../common/components/MsContainer";
|
||||
import MsAsideContainer from "../../../../common/components/MsAsideContainer";
|
||||
import MsMainContainer from "../../../../common/components/MsMainContainer";
|
||||
import MsAsideItem from "../../../../common/components/MsAsideItem";
|
||||
import EnvironmentEdit from "./EnvironmentEdit";
|
||||
import {deepClone, listenGoBack, removeGoBackListener} from "@/common/js/utils";
|
||||
import {Environment, parseEnvironment} from "../../model/EnvironmentModel";
|
||||
|
||||
export default {
|
||||
name: "ApiEnvironmentConfig",
|
||||
components: {
|
||||
EnvironmentEdit,
|
||||
MsAsideItem,
|
||||
MsMainContainer, MsAsideContainer, MsContainer, MsApiCollapseItem, MsApiCollapse, draggable
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
result: {},
|
||||
visible: false,
|
||||
projectId: '',
|
||||
environments: [],
|
||||
currentEnvironment: new Environment(),
|
||||
environmentOperators: [
|
||||
{
|
||||
icon: 'el-icon-document-copy',
|
||||
func: this.copyEnvironment
|
||||
},
|
||||
{
|
||||
icon: 'el-icon-delete',
|
||||
func: this.deleteEnvironment
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open: function (projectId) {
|
||||
this.visible = true;
|
||||
this.projectId = projectId;
|
||||
this.getEnvironments();
|
||||
listenGoBack(this.close);
|
||||
},
|
||||
deleteEnvironment(environment, index) {
|
||||
if (environment.id) {
|
||||
this.result = this.$get('/api/environment/delete/' + environment.id, () => {
|
||||
this.$success(this.$t('commons.delete_success'));
|
||||
this.getEnvironments();
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.environments.splice(index, 1);
|
||||
}
|
||||
},
|
||||
copyEnvironment(environment) {
|
||||
this.currentEnvironment = environment;
|
||||
if (!environment.id) {
|
||||
this.$warning(this.$t('commons.please_save'));
|
||||
return;
|
||||
}
|
||||
let newEnvironment = {};
|
||||
newEnvironment = new Environment(environment);
|
||||
newEnvironment.id = null;
|
||||
newEnvironment.name = this.getNoRepeatName(newEnvironment.name);
|
||||
if (!this.validateEnvironment(newEnvironment)) {
|
||||
return;
|
||||
}
|
||||
this.$refs.environmentEdit._save(newEnvironment);
|
||||
this.environments.push(newEnvironment);
|
||||
this.$refs.environmentItems.itemSelected(this.environments.length - 1, newEnvironment);
|
||||
},
|
||||
validateEnvironment(environment) {
|
||||
if (!this.$refs.environmentEdit.validate()) {
|
||||
this.$error(this.$t('commons.formatErr'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getNoRepeatName(name) {
|
||||
for (let i in this.environments) {
|
||||
if (this.environments[i].name === name) {
|
||||
return this.getNoRepeatName(name + ' copy');
|
||||
}
|
||||
}
|
||||
return name;
|
||||
},
|
||||
addEnvironment() {
|
||||
let newEnvironment = new Environment({
|
||||
projectId: this.projectId
|
||||
});
|
||||
this.environments.push(newEnvironment);
|
||||
this.$refs.environmentItems.itemSelected(this.environments.length - 1, newEnvironment);
|
||||
},
|
||||
environmentSelected(environment) {
|
||||
this.getEnvironment(environment);
|
||||
},
|
||||
getEnvironments() {
|
||||
if (this.projectId) {
|
||||
this.result = this.$get('/api/environment/list/' + this.projectId, response => {
|
||||
this.environments = response.data;
|
||||
if (this.environments.length > 0) {
|
||||
this.$refs.environmentItems.itemSelected(0, this.environments[0]);
|
||||
} else {
|
||||
let item = new Environment({
|
||||
projectId: this.projectId
|
||||
});
|
||||
this.environments.push(item);
|
||||
this.$refs.environmentItems.itemSelected(0, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
getEnvironment(environment) {
|
||||
parseEnvironment(environment);
|
||||
this.currentEnvironment = environment;
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
this.visible = false;
|
||||
this.$refs.environmentEdit.clearValidate();
|
||||
removeGoBackListener(this.close);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.environment-dialog >>> .el-dialog__body {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.el-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ms-aside-container {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,160 @@
|
|||
<template>
|
||||
<div class="ms-border">
|
||||
<el-table :data="hostTable" style="width: 100%" @cell-dblclick="dblHostTable" class="ht-tb">
|
||||
<el-table-column prop="ip" label="IP">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.status" v-model="scope.row.ip"></el-input>
|
||||
<span v-else>{{scope.row.ip}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="domain" :label="$t('load_test.domain')">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.status" v-model="scope.row.domain"></el-input>
|
||||
<span v-else>{{scope.row.domain}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="annotation" :label="$t('commons.annotation')">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.status" v-model="scope.row.annotation"></el-input>
|
||||
<span v-else>{{scope.row.annotation}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('commons.operating')" width="100">
|
||||
<template v-slot:default="scope">
|
||||
<span>
|
||||
<el-button size="mini" p="$t('commons.remove')" icon="el-icon-close" circle @click="remove(scope.row)"
|
||||
class="ht-btn-remove"/>
|
||||
<el-button size="mini" p="$t('commons.save')" icon="el-icon-check" circle @click="confirm(scope.row)"
|
||||
class="ht-btn-confirm"/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-button class="ht-btn-add" size="mini" p="$t('commons.add')" icon="el-icon-circle-plus-outline" @click="add">添加
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiVariableInput from "../ApiVariableInput";
|
||||
import MsTableOperatorButton from "../../../../common/components/MsTableOperatorButton";
|
||||
|
||||
export default {
|
||||
name: "MsApiHostTable",
|
||||
components: {MsApiVariableInput, MsTableOperatorButton},
|
||||
props: {
|
||||
hostTable: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
total: 0,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
remove: function (row) {
|
||||
this.hostTable.splice(this.hostTable.indexOf(row), 1);
|
||||
},
|
||||
change: function () {
|
||||
this.$emit('change', this.hostTable);
|
||||
},
|
||||
add: function (r) {
|
||||
let row = {
|
||||
ip: '',
|
||||
domain: '',
|
||||
status: 'edit',
|
||||
annotation: '',
|
||||
uuid: this.uuid(),
|
||||
}
|
||||
// 获取上一行的数据
|
||||
for (let i = this.hostTable.length - 1; i >= 0; i--) {
|
||||
if (this.hostTable[i].status === '') {
|
||||
row.ip = this.hostTable[i].ip;
|
||||
row.domain = this.hostTable[i].domain;
|
||||
row.annotation = this.hostTable[i].annotation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.hostTable.push(row);
|
||||
},
|
||||
confirm: function (row) {
|
||||
this.validateIp(row.ip) && this.validateDomain(row.domain) ? row.status = '' : row.status;
|
||||
this.$emit('change', this.hostTable);
|
||||
if (row.status === "") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
init: function () {
|
||||
if (this.hostTable === undefined || this.hostTable.length === 0) {
|
||||
let row = {
|
||||
ip: '',
|
||||
domain: '',
|
||||
status: 'edit',
|
||||
annotation: '',
|
||||
uuid: this.uuid()
|
||||
}
|
||||
this.hostTable.push(row);
|
||||
}
|
||||
},
|
||||
validateIp(ip) {
|
||||
let regexp = /^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}$/;
|
||||
if (regexp.test(ip) == false) {
|
||||
this.$warning(this.$t('load_test.input_ip'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validateDomain(domain) {
|
||||
let strRegex = "^(?=^.{3,255}$)(http(s)?:\\/\\/)?(www\\.)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:\\d+)*(\\/\\w+\\.\\w+)*$";
|
||||
const re = new RegExp(strRegex);
|
||||
if (re.test(domain) && domain.length < 67) {
|
||||
return true;
|
||||
}
|
||||
this.$warning(this.$t('load_test.input_domain'));
|
||||
return false;
|
||||
},
|
||||
dblHostTable: function (row) {
|
||||
row.status = 'edit';
|
||||
},
|
||||
uuid: function () {
|
||||
return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.ht-btn-remove {
|
||||
color: white;
|
||||
background-color: #DCDFE6;
|
||||
}
|
||||
|
||||
.ht-btn-confirm {
|
||||
color: white;
|
||||
background-color: #1483F6;
|
||||
}
|
||||
|
||||
.ht-btn-add {
|
||||
border: 0px;
|
||||
margin-top: 10px;
|
||||
color: #1483F6;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ht-tb {
|
||||
background-color: white;
|
||||
border: 0px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div>
|
||||
<span class="kv-description" v-if="description">
|
||||
{{description}}
|
||||
</span>
|
||||
<div class="kv-row" v-for="(item, index) in items" :key="index">
|
||||
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
|
||||
<el-col class="kv-checkbox">
|
||||
<input type="checkbox" v-if="!isDisable(index)" @change="change" :value="item.uuid" v-model="item.enable"
|
||||
:disabled="isDisable(index) || isReadOnly"/>
|
||||
</el-col>
|
||||
|
||||
<el-col>
|
||||
<ms-api-variable-input :show-copy="showCopy" :show-variable="showVariable" :is-read-only="isReadOnly" v-model="item.name" size="small" maxlength="200" @change="change"
|
||||
:placeholder="$t('api_test.variable_name')" show-word-limit/>
|
||||
</el-col>
|
||||
<el-col>
|
||||
<el-input :disabled="isReadOnly" v-model="item.value" size="small" @change="change"
|
||||
:placeholder="$t('api_test.value')" show-word-limit/>
|
||||
</el-col>
|
||||
<el-col class="kv-delete">
|
||||
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
|
||||
:disabled="isDisable(index) || isReadOnly"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {KeyValue} from "../../model/ApiTestModel";
|
||||
import MsApiVariableInput from "../ApiVariableInput";
|
||||
|
||||
export default {
|
||||
name: "MsApiScenarioVariables",
|
||||
components: {MsApiVariableInput},
|
||||
props: {
|
||||
description: String,
|
||||
items: Array,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showVariable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showCopy: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
remove: function (index) {
|
||||
this.items.splice(index, 1);
|
||||
this.$emit('change', this.items);
|
||||
},
|
||||
change: function () {
|
||||
let isNeedCreate = true;
|
||||
let removeIndex = -1;
|
||||
this.items.forEach((item, index) => {
|
||||
if (!item.name && !item.value) {
|
||||
// 多余的空行
|
||||
if (index !== this.items.length - 1) {
|
||||
removeIndex = index;
|
||||
}
|
||||
// 没有空行,需要创建空行
|
||||
isNeedCreate = false;
|
||||
}
|
||||
});
|
||||
if (isNeedCreate) {
|
||||
this.items.push(new KeyValue({enable: true}));
|
||||
}
|
||||
this.$emit('change', this.items);
|
||||
// TODO 检查key重复
|
||||
},
|
||||
isDisable: function (index) {
|
||||
return this.items.length - 1 === index;
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.items.length === 0) {
|
||||
this.items.push(new KeyValue({enable: true}));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kv-description {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.kv-checkbox {
|
||||
width: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.kv-row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.kv-delete {
|
||||
width: 60px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-form :model="commonConfig" :rules="rules" ref="commonConfig">
|
||||
|
||||
<span>{{$t('api_test.environment.globalVariable')}}</span>
|
||||
<ms-api-scenario-variables :show-copy="false" :items="commonConfig.variables"/>
|
||||
|
||||
<el-form-item>
|
||||
<el-switch v-model="commonConfig.enableHost" active-text="Hosts"/>
|
||||
</el-form-item>
|
||||
<ms-api-host-table v-if="commonConfig.enableHost" :hostTable="commonConfig.hosts" ref="refHostTable"/>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {CommonConfig, Environment} from "../../model/EnvironmentModel";
|
||||
import MsApiScenarioVariables from "./ApiScenarioVariables";
|
||||
import MsApiHostTable from "./ApiHostTable";
|
||||
|
||||
export default {
|
||||
name: "MsEnvironmentCommonConfig",
|
||||
components: {MsApiHostTable, MsApiScenarioVariables},
|
||||
props: {
|
||||
commonConfig: new CommonConfig(),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rules: {
|
||||
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validate() {
|
||||
let isValidate = false;
|
||||
this.$refs['commonConfig'].validate((valid) => {
|
||||
if (valid) {
|
||||
// 校验host列表
|
||||
let valHost = true;
|
||||
if (this.commonConfig.enableHost) {
|
||||
for (let i = 0; i < this.commonConfig.hosts.length; i++) {
|
||||
valHost = this.$refs['refHostTable'].confirm(this.commonConfig.hosts[i]);
|
||||
}
|
||||
}
|
||||
if (valHost) {
|
||||
isValidate = true;
|
||||
} else {
|
||||
isValidate = false;
|
||||
}
|
||||
} else {
|
||||
isValidate = false;
|
||||
}
|
||||
});
|
||||
return isValidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,166 @@
|
|||
<template>
|
||||
<el-main v-loading="result.loading">
|
||||
<el-form :model="environment" :rules="rules" ref="environment">
|
||||
|
||||
<span>{{$t('api_test.environment.name')}}</span>
|
||||
<el-form-item prop="name">
|
||||
<el-input v-model="environment.name" :placeholder="this.$t('commons.input_name')" clearable/>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<el-tabs v-model="activeName">
|
||||
|
||||
<el-tab-pane :label="$t('api_test.environment.common_config')" name="common">
|
||||
<ms-environment-common-config :common-config="environment.config.commonConfig" ref="commonConfig"/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('api_test.environment.http_config')" name="http">
|
||||
<ms-environment-http-config :http-config="environment.config.httpConfig" ref="httpConfig"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.environment.database_config')" name="sql">
|
||||
<ms-database-config :configs="environment.config.databaseConfigs"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.environment.tcp_config')" name="tcp">
|
||||
<ms-tcp-config :config="environment.config.tcpConfig"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div class="environment-footer">
|
||||
<ms-dialog-footer
|
||||
@cancel="cancel"
|
||||
@confirm="save()"/>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiScenarioVariables from "./ApiScenarioVariables";
|
||||
import MsApiKeyValue from "../ApiKeyValue";
|
||||
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
|
||||
import {REQUEST_HEADERS} from "@/common/js/constants";
|
||||
import {Environment} from "../../model/EnvironmentModel";
|
||||
import MsApiHostTable from "./ApiHostTable";
|
||||
import MsDatabaseConfig from "../request/database/DatabaseConfig";
|
||||
import MsEnvironmentHttpConfig from "./EnvironmentHttpConfig";
|
||||
import MsEnvironmentCommonConfig from "./EnvironmentCommonConfig";
|
||||
import MsTcpConfig from "../request/tcp/TcpConfig";
|
||||
|
||||
export default {
|
||||
name: "EnvironmentEdit",
|
||||
components: {
|
||||
MsTcpConfig,
|
||||
MsEnvironmentCommonConfig,
|
||||
MsEnvironmentHttpConfig,
|
||||
MsDatabaseConfig, MsApiHostTable, MsDialogFooter, MsApiKeyValue, MsApiScenarioVariables},
|
||||
props: {
|
||||
environment: new Environment(),
|
||||
},
|
||||
data() {
|
||||
|
||||
return {
|
||||
result: {},
|
||||
envEnable: false,
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
|
||||
{max: 64, message: this.$t('commons.input_limit', [1, 64]), trigger: 'blur'}
|
||||
],
|
||||
},
|
||||
headerSuggestions: REQUEST_HEADERS,
|
||||
activeName: 'common'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
environment: function (o) {
|
||||
this.envEnable = o.enable;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.$refs['environment'].validate((valid) => {
|
||||
if (valid && this.$refs.commonConfig.validate() && this.$refs.httpConfig.validate()) {
|
||||
this._save(this.environment);
|
||||
}
|
||||
});
|
||||
},
|
||||
validate() {
|
||||
let isValidate = false;
|
||||
this.$refs['environment'].validate((valid) => {
|
||||
if (valid && this.$refs.commonConfig.validate() && this.$refs.httpConfig.validate()) {
|
||||
isValidate = true;
|
||||
} else {
|
||||
isValidate = false;
|
||||
}
|
||||
});
|
||||
return isValidate;
|
||||
},
|
||||
_save(environment) {
|
||||
let param = this.buildParam(environment);
|
||||
let url = '/api/environment/add';
|
||||
if (param.id) {
|
||||
url = '/api/environment/update';
|
||||
}
|
||||
this.result = this.$post(url, param, response => {
|
||||
if (!param.id) {
|
||||
environment.id = response.data;
|
||||
}
|
||||
this.$success(this.$t('commons.save_success'));
|
||||
});
|
||||
},
|
||||
buildParam: function (environment) {
|
||||
let param = {};
|
||||
Object.assign(param, environment);
|
||||
let hosts = param.config.commonConfig.hosts;
|
||||
if (hosts != undefined) {
|
||||
let validHosts = [];
|
||||
// 去除掉未确认的host
|
||||
hosts.forEach(host => {
|
||||
if (host.status === '') {
|
||||
validHosts.push(host);
|
||||
}
|
||||
});
|
||||
param.config.commonConfig.hosts = validHosts;
|
||||
}
|
||||
param.config = JSON.stringify(param.config);
|
||||
return param;
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('close');
|
||||
},
|
||||
clearValidate() {
|
||||
this.$refs["environment"].clearValidate();
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.el-main {
|
||||
border: solid 1px #EBEEF5;
|
||||
margin-left: 200px;
|
||||
min-height: 400px;
|
||||
max-height: 700px;
|
||||
|
||||
}
|
||||
|
||||
.el-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.environment-footer {
|
||||
margin-top: 15px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
span:not(:first-child) {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<el-form :model="httpConfig" :rules="rules" ref="httpConfig">
|
||||
<span>{{$t('api_test.environment.socket')}}</span>
|
||||
<el-form-item prop="socket">
|
||||
<el-input v-model="httpConfig.socket" :placeholder="$t('api_test.request.url_description')" clearable>
|
||||
<template v-slot:prepend>
|
||||
<el-select v-model="httpConfig.protocol" class="request-protocol-select">
|
||||
<el-option label="http://" value="http"/>
|
||||
<el-option label="https://" value="https"/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<span>{{$t('api_test.request.headers')}}</span>
|
||||
<ms-api-key-value :items="httpConfig.headers" :isShowEnable="true" :suggestions="headerSuggestions"/>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {HttpConfig} from "../../model/EnvironmentModel";
|
||||
import MsApiKeyValue from "../ApiKeyValue";
|
||||
import {REQUEST_HEADERS} from "../../../../../../common/js/constants";
|
||||
|
||||
export default {
|
||||
name: "MsEnvironmentHttpConfig",
|
||||
components: {MsApiKeyValue},
|
||||
props: {
|
||||
httpConfig: new HttpConfig(),
|
||||
},
|
||||
data() {
|
||||
let socketValidator = (rule, value, callback) => {
|
||||
if (!this.validateSocket(value)) {
|
||||
callback(new Error(this.$t('commons.formatErr')));
|
||||
return false;
|
||||
} else {
|
||||
callback();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return {
|
||||
headerSuggestions: REQUEST_HEADERS,
|
||||
rules: {
|
||||
socket: [{required: false, validator: socketValidator, trigger: 'blur'}],
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateSocket(socket) {
|
||||
if (!socket) return true;
|
||||
let urlStr = this.httpConfig.protocol + '://' + socket;
|
||||
let url = {};
|
||||
try {
|
||||
url = new URL(urlStr);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
this.httpConfig.domain = decodeURIComponent(url.hostname);
|
||||
|
||||
this.httpConfig.port = url.port;
|
||||
let path = url.pathname === '/' ? '' : url.pathname;
|
||||
if (url.port) {
|
||||
this.httpConfig.socket = this.httpConfig.domain + ':' + url.port + path;
|
||||
} else {
|
||||
this.httpConfig.socket = this.httpConfig.domain + path;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validate() {
|
||||
let isValidate = false;
|
||||
this.$refs['httpConfig'].validate((valid) => {
|
||||
isValidate = valid;
|
||||
});
|
||||
return isValidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.request-protocol-select {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,353 @@
|
|||
<template>
|
||||
<el-dialog :close-on-click-modal="false" :title="$t('api_test.api_import.title')" :visible.sync="visible" class="api-import" v-loading="result.loading" @close="close">
|
||||
|
||||
<div class="header-bar">
|
||||
<div>{{$t('api_test.api_import.data_format')}}</div>
|
||||
<el-radio-group v-model="selectedPlatformValue">
|
||||
<el-radio v-for="(item, index) in platforms" :key="index" :label="item.value">{{item.name}}</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<div class="operate-button">
|
||||
<el-button class="save-button" type="primary" plain @click="save">
|
||||
{{$t('commons.save')}}
|
||||
</el-button>
|
||||
<el-button class="cancel-button" type="warning" plain @click="visible = false">
|
||||
{{$t('commons.cancel')}}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form :model="formData" :rules="rules" label-width="100px" v-loading="result.loading" ref="form">
|
||||
<el-row>
|
||||
<el-col :span="11">
|
||||
<el-form-item :label="$t('commons.name')" prop="name">
|
||||
<el-input size="small" class="name-input" v-model="formData.name" clearable show-word-limit/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.project')" prop="projectId">
|
||||
<el-select size="small" v-model="formData.projectId" class="project-select" clearable>
|
||||
<el-option v-for="(project, index) in projects" :key="index" :label="project.name" :value="project.id"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="useEnvironment || selectedPlatformValue == 'Swagger2'" :label="$t('api_test.environment.environment_config')" prop="environmentId">
|
||||
<el-select v-if="showEnvironmentSelect" size="small" v-model="formData.environmentId" class="environment-select" clearable>
|
||||
<el-option v-for="(environment, index) in environments" :key="index" :label="environment.name" :value="environment.id"/>
|
||||
<el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">{{$t('api_test.environment.environment_config')}}</el-button>
|
||||
<template v-slot:empty>
|
||||
<div class="empty-environment">
|
||||
<el-button :disabled="formData.projectId == ''" class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">{{$t('api_test.environment.environment_config')}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="selectedPlatformValue != 'Swagger2'" prop="useEnvironment">
|
||||
<el-checkbox v-model="useEnvironment">{{$t('api_test.environment.config_environment')}}</el-checkbox>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="'Swagger URL'" prop="wgerUrl" v-if="selectedPlatformValue == 'Swagger2' && swaggerUrlEable">
|
||||
<el-input size="small" v-model="formData.swaggerUrl" clearable show-word-limit/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="selectedPlatformValue == 'Swagger2'">
|
||||
<el-switch
|
||||
v-model="swaggerUrlEable"
|
||||
:active-text="$t('api_test.api_import.swagger_url_import')">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="1" v-if="selectedPlatformValue != 'Swagger2' || (selectedPlatformValue == 'Swagger2' && !swaggerUrlEable)">
|
||||
<el-divider direction="vertical"/>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="selectedPlatformValue != 'Swagger2' || (selectedPlatformValue == 'Swagger2' && !swaggerUrlEable)">
|
||||
<el-upload
|
||||
class="api-upload"
|
||||
drag
|
||||
action=""
|
||||
:http-request="upload"
|
||||
:limit="1"
|
||||
:beforeUpload="uploadValidate"
|
||||
:on-remove="handleRemove"
|
||||
:file-list="fileList"
|
||||
:on-exceed="handleExceed"
|
||||
multiple>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text" v-html="$t('load_test.upload_tips')"></div>
|
||||
<div class="el-upload__tip" slot="tip">{{$t('api_test.api_import.file_size_limit')}}</div>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div class="format-tip">
|
||||
<div>
|
||||
<span>{{$t('api_test.api_import.tip')}}:{{selectedPlatform.tip}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{$t('api_test.api_import.export_tip')}}:{{selectedPlatform.exportTip}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<api-environment-config ref="environmentConfig" @close="getEnvironments"/>
|
||||
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
|
||||
import ApiEnvironmentConfig from "../environment/ApiEnvironmentConfig";
|
||||
import {listenGoBack, removeGoBackListener} from "@/common/js/utils";
|
||||
|
||||
export default {
|
||||
name: "ApiImport",
|
||||
components: {ApiEnvironmentConfig, MsDialogFooter},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
swaggerUrlEable: false,
|
||||
showEnvironmentSelect: true,
|
||||
platforms: [
|
||||
{
|
||||
name: 'Metersphere',
|
||||
value: 'Metersphere',
|
||||
tip: this.$t('api_test.api_import.ms_tip'),
|
||||
exportTip: this.$t('api_test.api_import.ms_export_tip'),
|
||||
suffixes: new Set(['json'])
|
||||
},
|
||||
{
|
||||
name: 'Postman',
|
||||
value: 'Postman',
|
||||
tip: this.$t('api_test.api_import.postman_tip'),
|
||||
exportTip: this.$t('api_test.api_import.post_export_tip'),
|
||||
suffixes: new Set(['json'])
|
||||
},
|
||||
{
|
||||
name: 'Swagger',
|
||||
value: 'Swagger2',
|
||||
tip: this.$t('api_test.api_import.swagger_tip'),
|
||||
exportTip: this.$t('api_test.api_import.swagger_export_tip'),
|
||||
suffixes: new Set(['json'])
|
||||
}
|
||||
],
|
||||
selectedPlatform: {},
|
||||
selectedPlatformValue: 'Metersphere',
|
||||
result: {},
|
||||
projects: [],
|
||||
environments: [],
|
||||
useEnvironment: false,
|
||||
formData: {
|
||||
name: '',
|
||||
environmentId: '',
|
||||
projectId: '',
|
||||
file: undefined,
|
||||
swaggerUrl: ''
|
||||
},
|
||||
currentModule: {},
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
|
||||
{max: 60, message: this.$t('commons.input_limit', [1, 60]), trigger: 'blur'}
|
||||
],
|
||||
environmentId: [
|
||||
{required: true, message: this.$t('api_test.environment.select_environment'), trigger: 'blur'},
|
||||
],
|
||||
projectId: [
|
||||
{required: true, message: this.$t('api_test.select_project'), trigger: 'blur'},
|
||||
]
|
||||
},
|
||||
fileList: []
|
||||
}
|
||||
},
|
||||
activated() {
|
||||
this.selectedPlatform = this.platforms[0];
|
||||
this.getProjects();
|
||||
},
|
||||
watch: {
|
||||
selectedPlatformValue() {
|
||||
for (let i in this.platforms) {
|
||||
if (this.platforms[i].value === this.selectedPlatformValue) {
|
||||
this.selectedPlatform = this.platforms[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
'formData.projectId'() {
|
||||
this.getEnvironments();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(module) {
|
||||
this.currentModule = module;
|
||||
this.visible = true;
|
||||
listenGoBack(this.close);
|
||||
},
|
||||
upload(file) {
|
||||
this.formData.file = file.file;
|
||||
},
|
||||
handleExceed(files, fileList) {
|
||||
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
|
||||
},
|
||||
handleRemove(file, fileList) {
|
||||
this.formData.file = undefined;
|
||||
},
|
||||
uploadValidate(file, fileList) {
|
||||
let suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
|
||||
if (!this.selectedPlatform.suffixes.has(suffix)) {
|
||||
this.$warning(this.$t('api_test.api_import.suffixFormatErr'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size / 1024 / 1024 > 20) {
|
||||
this.$warning(this.$t('test_track.case.import.upload_limit_size'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getEnvironments() {
|
||||
if (this.formData.projectId) {
|
||||
this.$get('/api/environment/list/' + this.formData.projectId, response => {
|
||||
this.environments = response.data;
|
||||
let hasEnvironmentId = false;
|
||||
this.environments.forEach(env => {
|
||||
if (env.id === this.formData.environmentId) {
|
||||
hasEnvironmentId = true;
|
||||
}
|
||||
});
|
||||
if (!hasEnvironmentId) {
|
||||
this.formData.environmentId = '';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.environments = [];
|
||||
this.formData.environmentId = '';
|
||||
}
|
||||
},
|
||||
getProjects() {
|
||||
this.result = this.$get("/project/listAll", response => {
|
||||
this.projects = response.data;
|
||||
})
|
||||
},
|
||||
openEnvironmentConfig() {
|
||||
if (!this.formData.projectId) {
|
||||
this.$error(this.$t('api_test.select_project'));
|
||||
return;
|
||||
}
|
||||
this.showEnvironmentSelect = false;
|
||||
this.$refs.environmentConfig.open(this.formData.projectId);
|
||||
this.showEnvironmentSelect = true;
|
||||
},
|
||||
save() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
let param = {};
|
||||
Object.assign(param, this.formData);
|
||||
param.platform = this.selectedPlatformValue;
|
||||
param.useEnvironment = this.useEnvironment;
|
||||
param.moduleId = this.currentModule.id;
|
||||
param.modulePath = this.currentModule.path;
|
||||
if ((this.selectedPlatformValue != 'Swagger2' || (this.selectedPlatformValue == 'Swagger2' && !this.swaggerUrlEable)) && !this.formData.file) {
|
||||
this.$warning(this.$t('commons.please_upload'));
|
||||
return;
|
||||
}
|
||||
if (!this.swaggerUrlEable) {
|
||||
param.swaggerUrl = undefined;
|
||||
}
|
||||
this.result = this.$fileUpload('/api/definition/import', param.file, null, param, response => {
|
||||
let res = response.data;
|
||||
this.$success(this.$t('test_track.case.import.success'));
|
||||
this.visible = false;
|
||||
this.$emit('refresh');
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.formData = {
|
||||
name: '',
|
||||
environmentId: '',
|
||||
projectId: '',
|
||||
file: undefined,
|
||||
swaggerUrl: ''
|
||||
};
|
||||
this.fileList = [];
|
||||
removeGoBackListener(this.close);
|
||||
this.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.format-tip {
|
||||
background: #EDEDED;
|
||||
}
|
||||
|
||||
.api-upload {
|
||||
text-align: center;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.api-upload >>> .el-upload {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.api-upload >>> .el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-radio-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.header-bar, .format-tip, .el-form {
|
||||
border: solid #E1E1E1 1px;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
.api-import >>> .el-dialog__body {
|
||||
padding: 15px 25px;
|
||||
}
|
||||
|
||||
.operate-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.environment-button {
|
||||
margin-left: 20px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.empty-environment {
|
||||
padding: 10px 0px;
|
||||
}
|
||||
|
||||
.el-form {
|
||||
padding: 30px 10px;
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.name-input {
|
||||
max-width: 195px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
float: right;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -12,27 +12,26 @@ const DEFAULT_OPTIONS = {
|
|||
};
|
||||
|
||||
export default class TCPSampler extends Sampler {
|
||||
static CLASSES = ["TCPClientImpl", "BinaryTCPClientImpl", "LengthPrefixedBinaryTCPClientImpl"]
|
||||
|
||||
constructor(options = DEFAULT_OPTIONS) {
|
||||
super(options);
|
||||
this.classname = this.initStringProp("TCPSampler.classname", "TCPClientImpl")
|
||||
this.type = "TCPSampler";
|
||||
this.classname = options.classname || TCPSampler.CLASSES[0];
|
||||
this.server = options.server;
|
||||
this.port = options.port;
|
||||
this.ctimeout = options.ctimeout; // Connect
|
||||
this.timeout = options.timeout; // Response
|
||||
|
||||
this.server = this.initStringProp("TCPSampler.server")
|
||||
this.port = this.initStringProp("TCPSampler.port")
|
||||
|
||||
this.connectTimeout = this.initStringProp("TCPSampler.ctimeout")
|
||||
this.responseTimeout = this.initStringProp("TCPSampler.timeout")
|
||||
|
||||
this.reUseConnection = this.initBoolProp("TCPSampler.reUseConnection", true)
|
||||
this.closeConnection = this.initBoolProp("TCPSampler.closeConnection", false)
|
||||
this.nodelay = this.initBoolProp("TCPSampler.nodelay", false)
|
||||
|
||||
this.soLinger = this.initStringProp("TCPSampler.soLinger")
|
||||
this.eolByte = this.initStringProp("TCPSampler.EolByte")
|
||||
|
||||
this.request = this.initStringProp("TCPSampler.request")
|
||||
this.username = this.initStringProp("ConfigTestElement.username")
|
||||
this.password = this.initStringProp("ConfigTestElement.password")
|
||||
this.reUseConnection = options.reUseConnection === undefined ? true : options.reUseConnection;
|
||||
this.nodelay = options.nodelay === undefined ? false : options.nodelay;
|
||||
this.closeConnection = options.closeConnection === undefined ? false : options.closeConnection;
|
||||
this.soLinger = options.soLinger;
|
||||
this.eolByte = options.eolByte;
|
||||
|
||||
this.username = options.username;
|
||||
this.password = options.password;
|
||||
this.hashTree = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div>
|
||||
<ms-database-from :config="currentConfig" :callback="saveConfig" ref="databaseFrom" :is-read-only="isReadOnly"/>
|
||||
<ms-database-config-list @rowSelect="rowSelect" v-if="configs.length > 0" :table-data="configs"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsDatabaseConfigList from "./DatabaseConfigList";
|
||||
import {DatabaseConfig} from "../../../model/ApiTestModel";
|
||||
import MsDatabaseFrom from "./DatabaseFrom";
|
||||
import {getUUID} from "@/common/js/utils";
|
||||
|
||||
export default {
|
||||
name: "MsDatabaseConfig",
|
||||
components: {MsDatabaseFrom, MsDatabaseConfigList},
|
||||
props: {
|
||||
configs: Array,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drivers: DatabaseConfig.DRIVER_CLASS,
|
||||
currentConfig: new DatabaseConfig()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
configs() {
|
||||
this.currentConfig = new DatabaseConfig();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
saveConfig(config) {
|
||||
for (let item of this.configs) {
|
||||
if (item.name === config.name && item.id !== config.id) {
|
||||
this.$warning(this.$t('commons.already_exists'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (config.id) {
|
||||
this.updateConfig(config);
|
||||
} else {
|
||||
this.addConfig(config);
|
||||
}
|
||||
},
|
||||
addConfig(config) {
|
||||
config.id = getUUID();
|
||||
let item = {};
|
||||
Object.assign(item, config);
|
||||
this.configs.push(item);
|
||||
this.currentConfig = new DatabaseConfig();
|
||||
},
|
||||
updateConfig(config) {
|
||||
Object.assign(this.currentConfig, config);
|
||||
this.currentConfig = new DatabaseConfig();
|
||||
},
|
||||
rowSelect(config) {
|
||||
this.currentConfig = config;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.addButton {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.database-from {
|
||||
padding: 10px;
|
||||
border: #DCDFE6 solid 1px;
|
||||
margin: 5px 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="database-config-list">
|
||||
|
||||
<el-table border :data="tableData"
|
||||
class="adjust-table table-content"
|
||||
highlight-current-row
|
||||
@row-click="handleView">
|
||||
|
||||
<el-table-column prop="name" :label="$t('api_test.request.sql.dataSource')" show-overflow-tooltip/>
|
||||
<el-table-column prop="driver" :label="$t('api_test.request.sql.database_driver')" show-overflow-tooltip/>
|
||||
<el-table-column prop="dbUrl" :label="$t('api_test.request.sql.database_url')" show-overflow-tooltip/>
|
||||
<el-table-column prop="username" :label="$t('api_test.request.sql.username')" show-overflow-tooltip/>
|
||||
<el-table-column prop="poolMax" :label="$t('api_test.request.sql.pool_max')" show-overflow-tooltip/>
|
||||
<el-table-column prop="timeout" :label="$t('api_test.request.sql.query_timeout')" show-overflow-tooltip/>
|
||||
|
||||
<el-table-column :label="$t('commons.operating')" min-width="100">
|
||||
<template v-slot:default="scope">
|
||||
<ms-table-operator-button :is-tester-permission="true" :tip="$t('commons.copy')" icon="el-icon-document-copy" type="success" @exec="handleCopy(scope.$index, scope.row)"/>
|
||||
<ms-table-operator-button :isTesterPermission="true" :tip="$t('commons.delete')" icon="el-icon-delete" type="danger" @exec="handleDelete(scope.$index)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {DatabaseConfig} from "../../../model/ApiTestModel";
|
||||
import MsTableOperator from "../../../../../common/components/MsTableOperator";
|
||||
import MsTableOperatorButton from "../../../../../common/components/MsTableOperatorButton";
|
||||
import {getUUID} from "../../../../../../../common/js/utils";
|
||||
|
||||
export default {
|
||||
name: "MsDatabaseConfigList",
|
||||
components: {MsTableOperatorButton, MsTableOperator},
|
||||
props: {
|
||||
tableData: Array,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drivers: DatabaseConfig.DRIVER_CLASS,
|
||||
result: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleView(row) {
|
||||
this.$emit('rowSelect', row);
|
||||
},
|
||||
handleDelete(index) {
|
||||
this.tableData.splice(index, 1);
|
||||
},
|
||||
handleCopy(index, config) {
|
||||
let copy = {};
|
||||
Object.assign(copy, config);
|
||||
copy.id = getUUID();
|
||||
copy.name = this.getNoRepeatName(copy.name);
|
||||
this.tableData.splice(index + 1, 0, copy);
|
||||
},
|
||||
getNoRepeatName(name) {
|
||||
for (let i in this.tableData) {
|
||||
if (this.tableData[i].name === name) {
|
||||
return this.getNoRepeatName(name + ' copy');
|
||||
}
|
||||
}
|
||||
return name;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.addButton {
|
||||
float: right;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<div class="database-from" v-loading="result.loading">
|
||||
<el-form :model="currentConfig" :rules="rules" label-width="150px" size="small" :disabled="isReadOnly" ref="databaseFrom">
|
||||
|
||||
<el-form-item :label="$t('api_test.request.sql.dataSource')" prop="name">
|
||||
<el-input v-model="currentConfig.name" maxlength="300" show-word-limit
|
||||
:placeholder="$t('commons.input_content')"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('api_test.request.sql.database_url')" prop="dbUrl">
|
||||
<el-input v-model="currentConfig.dbUrl" maxlength="500" show-word-limit
|
||||
:placeholder="$t('commons.input_content')"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('api_test.request.sql.database_driver')" prop="driver">
|
||||
<el-select v-model="currentConfig.driver" class="select-100" clearable>
|
||||
<el-option v-for="p in drivers" :key="p" :label="p" :value="p"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('api_test.request.sql.username')" prop="username">
|
||||
<el-input v-model="currentConfig.username" maxlength="300" show-word-limit
|
||||
:placeholder="$t('commons.input_content')"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('api_test.request.sql.password')" prop="password">
|
||||
<el-input v-model="currentConfig.password" type="password" autocomplete="new-password" maxlength="200"
|
||||
:placeholder="$t('commons.input_content')"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('api_test.request.sql.pool_max')" prop="poolMax">
|
||||
<el-input-number size="small" :disabled="isReadOnly" v-model="currentConfig.poolMax" :placeholder="$t('commons.please_select')" :max="1000*10000000" :min="0"/>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<el-form-item :label="$t('api_test.request.sql.timeout')" prop="timeout">
|
||||
<el-input-number size="small" :disabled="isReadOnly" v-model="currentConfig.timeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="buttons">
|
||||
<el-button type="primary" v-show="currentConfig.id" size="small" @click="validate()">{{$t('commons.validate')}}</el-button>
|
||||
<el-button type="primary" v-show="currentConfig.id" size="small" @click="save('update')">{{$t('commons.update')}}</el-button>
|
||||
<el-button type="primary" size="small" @click="save('add')">{{$t('commons.add')}}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {DatabaseConfig} from "../../../model/ApiTestModel";
|
||||
|
||||
export default {
|
||||
name: "MsDatabaseFrom",
|
||||
components: {},
|
||||
props: {
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default() {
|
||||
return new DatabaseConfig();
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
type: Function
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
config() {
|
||||
Object.assign(this.currentConfig, this.config);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
Object.assign(this.currentConfig, this.config);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drivers: DatabaseConfig.DRIVER_CLASS,
|
||||
result: {},
|
||||
currentConfig: new DatabaseConfig(),
|
||||
rules: {
|
||||
name: [
|
||||
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
|
||||
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
|
||||
],
|
||||
driver: [
|
||||
{required: true, message: this.$t('commons.cannot_be_null'), trigger: 'blur'},
|
||||
],
|
||||
password: [
|
||||
{max: 200, message: this.$t('commons.input_limit', [0, 200]), trigger: 'blur'}
|
||||
],
|
||||
dbUrl: [
|
||||
{required: true, message: this.$t('commons.cannot_be_null'), trigger: 'blur'},
|
||||
{max: 500, message: this.$t('commons.input_limit', [0, 500]), trigger: 'blur'}
|
||||
],
|
||||
username: [
|
||||
{required: true, message: this.$t('commons.cannot_be_null'), trigger: 'blur'},
|
||||
{max: 200, message: this.$t('commons.input_limit', [0, 200]), trigger: 'blur'}
|
||||
],
|
||||
poolMax: [
|
||||
{required: true, message: this.$t('commons.cannot_be_null'), trigger: 'blur'},
|
||||
],
|
||||
timeout: [
|
||||
{required: true, message: this.$t('commons.cannot_be_null'), trigger: 'blur'},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save(type) {
|
||||
this.$refs['databaseFrom'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.callback) {
|
||||
if (type === 'add') {
|
||||
this.currentConfig.id = undefined;
|
||||
}
|
||||
this.callback(this.currentConfig);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
validate() {
|
||||
this.result = this.$post('/api/database/validate', this.currentConfig, () => {
|
||||
this.$success(this.$t('commons.connection_successful'));
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.buttons {
|
||||
float: right;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -87,18 +87,18 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiKeyValue from "../ApiKeyValue";
|
||||
import MsApiBody from "../body/ApiBody";
|
||||
import MsApiAuthConfig from "../auth/ApiAuthConfig";
|
||||
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
|
||||
import MsApiKeyValue from "../../ApiKeyValue";
|
||||
import MsApiBody from "../../body/ApiBody";
|
||||
import MsApiAuthConfig from "../../auth/ApiAuthConfig";
|
||||
import ApiRequestMethodSelect from "../../collapse/ApiRequestMethodSelect";
|
||||
import {REQUEST_HEADERS} from "@/common/js/constants";
|
||||
import MsApiVariable from "../ApiVariable";
|
||||
import MsJsr233Processor from "../processor/Jsr233Processor";
|
||||
import MsApiAdvancedConfig from "../ApiAdvancedConfig";
|
||||
import {createComponent} from "../jmeter/components";
|
||||
import MsApiAssertions from "../assertion/ApiAssertions";
|
||||
import MsApiExtract from "../extract/ApiExtract";
|
||||
import {Assertions, Extract} from "../../model/ApiTestModel";
|
||||
import MsApiVariable from "../../ApiVariable";
|
||||
import MsJsr233Processor from "../../processor/Jsr233Processor";
|
||||
import MsApiAdvancedConfig from "../../ApiAdvancedConfig";
|
||||
import {createComponent} from "../../jmeter/components";
|
||||
import MsApiAssertions from "../../assertion/ApiAssertions";
|
||||
import MsApiExtract from "../../extract/ApiExtract";
|
||||
import {Assertions, Extract} from "../../../model/ApiTestModel";
|
||||
|
||||
export default {
|
||||
name: "MsApiHttpRequestForm",
|
|
@ -6,11 +6,6 @@
|
|||
|
||||
<script>
|
||||
import MsApiHttpRequestForm from "./ApiHttpRequestForm";
|
||||
import {Request} from "../jmeter/components";
|
||||
|
||||
// import MsApiTcpRequestForm from "./ApiTcpRequestForm";
|
||||
// import MsApiDubboRequestForm from "./ApiDubboRequestForm";
|
||||
// import MsApiSqlRequestForm from "./ApiSqlRequestForm";
|
||||
|
||||
export default {
|
||||
name: "MsApiRequestForm",
|
||||
|
@ -29,21 +24,7 @@
|
|||
},
|
||||
computed: {
|
||||
component({request: {type}}) {
|
||||
let name;
|
||||
switch (type) {
|
||||
case Request.TYPES.DUBBO:
|
||||
name = "MsApiDubboRequestForm";
|
||||
break;
|
||||
case Request.TYPES.SQL:
|
||||
name = "MsApiSqlRequestForm";
|
||||
break;
|
||||
case Request.TYPES.TCP:
|
||||
name = "MsApiTcpRequestForm";
|
||||
break;
|
||||
default:
|
||||
name = "MsApiHttpRequestForm";
|
||||
}
|
||||
return name;
|
||||
return "MsApiHttpRequestForm";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<el-form class="tcp" :model="config" :rules="rules" ref="config" label-width="120px" :disabled="isReadOnly"
|
||||
size="small">
|
||||
|
||||
<el-form-item label="TCPClient" prop="classname">
|
||||
<el-select v-model="config.classname" style="width: 100%">
|
||||
<el-option v-for="c in classes" :key="c" :label="c" :value="c"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="16">
|
||||
<el-form-item :label="$t('api_test.request.tcp.server')" prop="server">
|
||||
<el-input v-model="config.server" maxlength="300" show-word-limit/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item :label="$t('api_test.request.tcp.port')" prop="port" label-width="60px">
|
||||
<el-input-number v-model="config.port" controls-position="right" :min="0" :max="65535" :controls="false"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.request.tcp.connect')" prop="ctimeout">
|
||||
<el-input-number v-model="config.ctimeout" controls-position="right" :min="0" :step="1000" :controls="false"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.request.tcp.response')" prop="timeout">
|
||||
<el-input-number v-model="config.timeout" controls-position="right" :min="0" :step="1000" :controls="false"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.request.tcp.so_linger')" prop="soLinger">
|
||||
<el-input v-model="config.soLinger"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.request.tcp.eol_byte')" prop="eolByte">
|
||||
<el-input v-model="config.eolByte"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8">
|
||||
<el-form-item :label="$t('api_test.request.tcp.re_use_connection')">
|
||||
<el-checkbox v-model="config.reUseConnection"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item :label="$t('api_test.request.tcp.close_connection')">
|
||||
<el-checkbox v-model="config.closeConnection"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item :label="$t('api_test.request.tcp.no_delay')">
|
||||
<el-checkbox v-model="config.nodelay"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.request.tcp.username')" prop="username">
|
||||
<el-input v-model="config.username" maxlength="100" show-word-limit/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('api_test.request.tcp.password')" prop="password">
|
||||
<el-input v-model="config.password" maxlength="30" show-word-limit show-password
|
||||
autocomplete="new-password"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {TCPConfig} from "../../../model/ApiTestModel";
|
||||
|
||||
export default {
|
||||
name: "MsTcpConfig",
|
||||
props: {
|
||||
config: TCPConfig,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
classes: TCPConfig.CLASSES,
|
||||
rules: {}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tcp >>> .el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -86,14 +86,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiRequestForm from "../request/ApiRequestForm";
|
||||
import MsApiRequestForm from "../request/http/ApiRequestForm";
|
||||
import {downloadFile, getUUID} from "@/common/js/utils";
|
||||
import MsApiCaseList from "../ApiCaseList";
|
||||
import MsContainer from "../../../../common/components/MsContainer";
|
||||
import MsBottomContainer from "../BottomContainer";
|
||||
import {RequestFactory, Scenario} from "../../model/ApiTestModel";
|
||||
import {parseEnvironment} from "../../model/EnvironmentModel";
|
||||
import ApiEnvironmentConfig from "../../../test/components/ApiEnvironmentConfig";
|
||||
import ApiEnvironmentConfig from "../environment/ApiEnvironmentConfig";
|
||||
import MsRequestResultTail from "../response/RequestResultTail";
|
||||
import MsRun from "../Run";
|
||||
|
||||
|
|
|
@ -525,6 +525,7 @@ export default {
|
|||
pre_script: "Prescript",
|
||||
post_script: "Postscript",
|
||||
extract_param: "Extract parameters",
|
||||
add_module: "Add module",
|
||||
|
||||
}
|
||||
},
|
||||
|
|
|
@ -525,7 +525,9 @@ export default {
|
|||
auth_config_info: "请求需要进行权限校验",
|
||||
pre_script: "前置脚本",
|
||||
post_script: "后置脚本",
|
||||
extract_param:"提取参数",
|
||||
extract_param: "提取参数",
|
||||
add_module: "创建模块",
|
||||
|
||||
}
|
||||
},
|
||||
environment: {
|
||||
|
@ -704,7 +706,7 @@ export default {
|
|||
close_connection: "关闭连接",
|
||||
so_linger: "SO LINGER",
|
||||
eol_byte: "行尾(EOL)字节值",
|
||||
request: "要发送的文本",
|
||||
request: "发送文本",
|
||||
username: "用户名",
|
||||
password: "密码",
|
||||
login: "登录设置",
|
||||
|
|
|
@ -526,7 +526,7 @@ export default {
|
|||
pre_script: "前置腳本",
|
||||
post_script: "後置腳本",
|
||||
extract_param: "提取參數",
|
||||
|
||||
add_module: "創建模塊",
|
||||
}
|
||||
},
|
||||
environment: {
|
||||
|
|
Loading…
Reference in New Issue