feat(接口测试): 环境变量增加导入导出 (#17018)

--story=1008922 --user=王孝刚 【接口测试】环境参数的查询功能,以及场景变量和环境参数的导入导出功能;
https://www.tapd.cn/55049933/s/1220274

Co-authored-by: wxg0103 <727495428@qq.com>
This commit is contained in:
MeterSphere Bot 2022-08-18 10:27:53 +08:00 committed by GitHub
parent 07ea672ff9
commit 81955c649a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2405 additions and 600 deletions

View File

@ -65,16 +65,18 @@ public class ApiTestEnvironmentController {
@PostMapping("/add")
@MsAuditLog(module = OperLogModule.PROJECT_ENVIRONMENT_SETTING, type = OperLogConstants.CREATE, content = "#msClass.getLogDetails(#apiTestEnvironmentWithBLOBs.id)", msClass = ApiTestEnvironmentService.class)
public String create(@RequestPart("request") ApiTestEnvironmentDTO apiTestEnvironmentWithBLOBs, @RequestPart(value = "files", required = false) List<MultipartFile> sslFiles) {
public String create(@RequestPart("request") ApiTestEnvironmentDTO apiTestEnvironmentWithBLOBs, @RequestPart(value = "files", required = false) List<MultipartFile> sslFiles,
@RequestPart(value = "variablesFiles", required = false) List<MultipartFile> variableFile) {
checkParams(apiTestEnvironmentWithBLOBs);
return apiTestEnvironmentService.add(apiTestEnvironmentWithBLOBs, sslFiles);
return apiTestEnvironmentService.add(apiTestEnvironmentWithBLOBs, sslFiles, variableFile);
}
@PostMapping(value = "/update")
@MsAuditLog(module = OperLogModule.PROJECT_ENVIRONMENT_SETTING, type = OperLogConstants.UPDATE, beforeEvent = "#msClass.getLogDetails(#apiTestEnvironment.id)", content = "#msClass.getLogDetails(#apiTestEnvironment.id)", msClass = ApiTestEnvironmentService.class)
public void update(@RequestPart("request") ApiTestEnvironmentDTO apiTestEnvironment, @RequestPart(value = "files", required = false) List<MultipartFile> sslFiles) {
public void update(@RequestPart("request") ApiTestEnvironmentDTO apiTestEnvironment, @RequestPart(value = "files", required = false) List<MultipartFile> sslFiles,
@RequestPart(value = "variablesFiles", required = false) List<MultipartFile> variableFile) {
checkParams(apiTestEnvironment);
apiTestEnvironmentService.update(apiTestEnvironment, sslFiles);
apiTestEnvironmentService.update(apiTestEnvironment, sslFiles, variableFile);
}
private void checkParams(ApiTestEnvironmentDTO apiTestEnvironment) {

View File

@ -8,4 +8,5 @@ import java.util.List;
@Data
public class ApiTestEnvironmentDTO extends ApiTestEnvironmentWithBLOBs {
private List<String> uploadIds;
private List<String> variablesFilesIds;
}

View File

@ -11,7 +11,6 @@ import io.metersphere.api.dto.EnvironmentType;
import io.metersphere.api.dto.definition.request.controller.MsLoopController;
import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy;
import io.metersphere.api.dto.definition.request.variable.ScenarioVariable;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import io.metersphere.api.dto.scenario.request.BodyFile;
import io.metersphere.api.service.ApiTestEnvironmentService;
@ -69,7 +68,7 @@ public class ElementUtil {
arguments.setName(StringUtils.isNoneBlank(name) ? name : "Arguments");
arguments.setProperty(TestElement.TEST_CLASS, Arguments.class.getName());
arguments.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ArgumentsPanel"));
config.getConfig().get(projectId).getCommonConfig().getVariables().stream().filter(KeyValue::isValid).filter(KeyValue::isEnable).forEach(keyValue ->
config.getConfig().get(projectId).getCommonConfig().getVariables().stream().filter(ScenarioVariable::isConstantValid).filter(ScenarioVariable::isEnable).forEach(keyValue ->
arguments.addArgument(keyValue.getName(), keyValue.getValue(), "=")
);
if (arguments.getArguments().size() > 0) {
@ -101,9 +100,9 @@ public class ElementUtil {
public static void addCsvDataSet(HashTree tree, List<ScenarioVariable> variables, ParameterConfig config, String shareMode) {
if (CollectionUtils.isNotEmpty(variables)) {
List<ScenarioVariable> list = variables.stream().filter(ScenarioVariable::isCSVValid).collect(Collectors.toList());
List<ScenarioVariable> list = variables.stream().filter(ScenarioVariable::isCSVValid).filter(ScenarioVariable::isEnable).collect(Collectors.toList());
if (CollectionUtils.isEmpty(list) && CollectionUtils.isNotEmpty(config.getTransferVariables())) {
list = config.getTransferVariables().stream().filter(ScenarioVariable::isCSVValid).collect(Collectors.toList());
list = config.getTransferVariables().stream().filter(ScenarioVariable::isCSVValid).filter(ScenarioVariable::isEnable).collect(Collectors.toList());
}
if (CollectionUtils.isNotEmpty(list)) {
list.forEach(item -> {
@ -140,7 +139,7 @@ public class ElementUtil {
public static void addCounter(HashTree tree, List<ScenarioVariable> variables, boolean isInternal) {
if (CollectionUtils.isNotEmpty(variables)) {
List<ScenarioVariable> list = variables.stream().filter(ScenarioVariable::isCounterValid).collect(Collectors.toList());
List<ScenarioVariable> list = variables.stream().filter(ScenarioVariable::isCounterValid).filter(ScenarioVariable::isEnable).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(list)) {
list.forEach(item -> {
CounterConfig counterConfig = new CounterConfig();
@ -166,7 +165,7 @@ public class ElementUtil {
public static void addRandom(HashTree tree, List<ScenarioVariable> variables) {
if (CollectionUtils.isNotEmpty(variables)) {
List<ScenarioVariable> list = variables.stream().filter(ScenarioVariable::isRandom).collect(Collectors.toList());
List<ScenarioVariable> list = variables.stream().filter(ScenarioVariable::isRandom).filter(ScenarioVariable::isEnable).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(list)) {
list.forEach(item -> {
RandomVariableConfig randomVariableConfig = new RandomVariableConfig();
@ -794,4 +793,62 @@ public class ElementUtil {
return evlValue;
}
}
public static Arguments getConfigArguments(ParameterConfig config, String name, String projectId, List<ScenarioVariable> variables) {
Arguments arguments = new Arguments();
arguments.setEnabled(true);
arguments.setName(StringUtils.isNotEmpty(name) ? name : "Arguments");
arguments.setProperty(TestElement.TEST_CLASS, Arguments.class.getName());
arguments.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ArgumentsPanel"));
// 场景变量
if (CollectionUtils.isNotEmpty(variables)) {
variables.stream().filter(ScenarioVariable::isConstantValid).forEach(keyValue ->
arguments.addArgument(keyValue.getName(), keyValue.getValue(), "=")
);
List<ScenarioVariable> variableList = variables.stream().filter(ScenarioVariable::isListValid).collect(Collectors.toList());
variableList.forEach(item -> {
String[] arrays = item.getValue().split(",");
for (int i = 0; i < arrays.length; i++) {
arguments.addArgument(item.getName() + "_" + (i + 1), arrays[i], "=");
}
});
}
// 环境通用变量
if (config.isEffective(projectId) && config.getConfig().get(projectId).getCommonConfig() != null
&& CollectionUtils.isNotEmpty(config.getConfig().get(projectId).getCommonConfig().getVariables())) {
//常量
List<ScenarioVariable> constants = config.getConfig().get(projectId).getCommonConfig().getVariables().stream().filter(ScenarioVariable::isConstantValid).filter(ScenarioVariable::isEnable).collect(Collectors.toList());
constants.forEach(keyValue ->
arguments.addArgument(keyValue.getName(), keyValue.getValue(), "=")
);
// List类型的变量
List<ScenarioVariable> variableList = config.getConfig().get(projectId).getCommonConfig().getVariables().stream().filter(ScenarioVariable::isListValid).filter(ScenarioVariable::isEnable).collect(Collectors.toList());
variableList.forEach(item -> {
String[] arrays = item.getValue().split(",");
for (int i = 0; i < arrays.length; i++) {
arguments.addArgument(item.getName() + "_" + (i + 1), arrays[i], "=");
}
});
// 清空变量防止重复添加
config.getConfig().get(projectId).getCommonConfig().getVariables().remove(constants);
config.getConfig().get(projectId).getCommonConfig().getVariables().remove(variableList);
}
if (arguments.getArguments() != null && arguments.getArguments().size() > 0) {
return arguments;
}
return null;
}
public static void addOtherVariables(ParameterConfig config, HashTree httpSamplerTree, String projectId) {
if (config.isEffective(projectId) && config.getConfig().get(projectId).getCommonConfig() != null
&& CollectionUtils.isNotEmpty(config.getConfig().get(projectId).getCommonConfig().getVariables())) {
ElementUtil.addCsvDataSet(httpSamplerTree, config.getConfig().get(projectId).getCommonConfig().getVariables(), config, "shareMode.group");
ElementUtil.addCounter(httpSamplerTree, config.getConfig().get(projectId).getCommonConfig().getVariables(), false);
ElementUtil.addRandom(httpSamplerTree, config.getConfig().get(projectId).getCommonConfig().getVariables());
}
}
}

View File

@ -41,7 +41,6 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Data
@EqualsAndHashCode(callSuper = true)
@ -145,7 +144,8 @@ public class MsScenario extends MsTestElement {
scenarioTree = MsCriticalSectionController.createHashTree(tree, this.getName(), this.isEnable());
}
// 环境变量
Arguments arguments = arguments(this.isEnvironmentEnable() ? newConfig : config);
Arguments arguments = ElementUtil.getConfigArguments(this.isEnvironmentEnable() ? newConfig : config, this.getName(), this.getProjectId(), this.getVariables());
if (arguments != null && (this.variableEnable == null || this.variableEnable)) {
Arguments valueSupposeMock = ParameterConfig.valueSupposeMock(arguments);
// 这里加入自定义变量解决ForEach循环控制器取值问题循环控制器无法从vars中取值
@ -309,41 +309,6 @@ public class MsScenario extends MsTestElement {
}
}
private Arguments arguments(ParameterConfig config) {
Arguments arguments = new Arguments();
arguments.setEnabled(true);
arguments.setName(StringUtils.isNotEmpty(this.getName()) ? this.getName() : "Arguments");
arguments.setProperty(TestElement.TEST_CLASS, Arguments.class.getName());
arguments.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ArgumentsPanel"));
// 场景变量
if (CollectionUtils.isNotEmpty(this.getVariables())) {
this.getVariables().stream().filter(ScenarioVariable::isConstantValid).forEach(keyValue ->
arguments.addArgument(keyValue.getName(), keyValue.getValue(), "=")
);
List<ScenarioVariable> variableList = this.getVariables().stream().filter(ScenarioVariable::isListValid).collect(Collectors.toList());
variableList.forEach(item -> {
String[] arrays = item.getValue().split(",");
for (int i = 0; i < arrays.length; i++) {
arguments.addArgument(item.getName() + "_" + (i + 1), arrays[i], "=");
}
});
}
// 环境通用变量
if (config.isEffective(this.getProjectId()) && config.getConfig().get(this.getProjectId()).getCommonConfig() != null
&& CollectionUtils.isNotEmpty(config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables())) {
config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables().stream().filter(KeyValue::isValid).filter(KeyValue::isEnable).forEach(keyValue ->
arguments.addArgument(keyValue.getName(), keyValue.getValue(), "=")
);
// 清空变量防止重复添加
config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables().clear();
}
if (arguments.getArguments() != null && arguments.getArguments().size() > 0) {
return arguments;
}
return null;
}
private void setEnv(Map<String, String> environmentMap, Map<String, EnvironmentConfig> envConfig) {
for (String projectId : environmentMap.keySet()) {
ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class);

View File

@ -2,8 +2,8 @@ package io.metersphere.api.dto.definition.request.dns;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.ParameterConfig;
import io.metersphere.api.dto.definition.request.variable.ScenarioVariable;
import io.metersphere.api.dto.scenario.HttpConfig;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import io.metersphere.api.dto.scenario.environment.Host;
import io.metersphere.plugin.core.MsParameter;
@ -65,14 +65,14 @@ public class MsDNSCacheManager extends MsTestElement {
}
}
private static Arguments arguments(String name, List<KeyValue> variables) {
private static Arguments arguments(String name, List<ScenarioVariable> variables) {
Arguments arguments = new Arguments();
arguments.setEnabled(true);
arguments.setName(name);
arguments.setProperty(TestElement.TEST_CLASS, Arguments.class.getName());
arguments.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ArgumentsPanel"));
variables.stream().filter(KeyValue::isValid).filter(KeyValue::isEnable).forEach(keyValue ->
arguments.addArgument(keyValue.getName(), keyValue.getValue(), "=")
variables.stream().filter(ScenarioVariable::isConstantValid).filter(ScenarioVariable::isEnable).forEach(ScenarioVariable ->
arguments.addArgument(ScenarioVariable.getName(), ScenarioVariable.getValue(), "=")
);
return arguments;
}

View File

@ -226,10 +226,12 @@ public class MsHTTPSamplerProxy extends MsTestElement {
setHeader(httpSamplerTree, config.getHeaders());
}
// 环境通用请求头
Arguments arguments = getConfigArguments(config);
Arguments arguments = ElementUtil.getConfigArguments(config, this.getName(), this.getProjectId(), null);
if (arguments != null) {
httpSamplerTree.add(arguments);
}
//添加csv
ElementUtil.addOtherVariables(config, httpSamplerTree, this.getProjectId());
//判断是否要开启DNS
if (config.isEffective(this.getProjectId()) && config.getConfig().get(this.getProjectId()).getCommonConfig() != null
&& config.getConfig().get(this.getProjectId()).getCommonConfig().isEnableHost()) {
@ -353,6 +355,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
}
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
}
}
if (StringUtils.isNotEmpty(useEvnId) && !StringUtils.equals(useEvnId, this.getEnvironmentId())) {
@ -682,7 +685,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
arguments.addArgument(httpArgument);
}
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
}
}
);
@ -728,30 +731,6 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
}
/**
* 环境通用变量这里只适用用接口定义和用例场景自动化会加到场景中
*/
private Arguments getConfigArguments(ParameterConfig config) {
Arguments arguments = new Arguments();
arguments.setEnabled(true);
arguments.setName(StringUtils.isNotEmpty(this.getName()) ? this.getName() : "Arguments");
arguments.setProperty(TestElement.TEST_CLASS, Arguments.class.getName());
arguments.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ArgumentsPanel"));
// 环境通用变量
if (config.isEffective(this.getProjectId()) && config.getConfig().get(this.getProjectId()).getCommonConfig() != null
&& CollectionUtils.isNotEmpty(config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables())) {
config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables().stream().filter(KeyValue::isValid).filter(KeyValue::isEnable).forEach(keyValue ->
arguments.addArgument(keyValue.getName(), keyValue.getValue(), "=")
);
// 清空变量防止重复添加
config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables().clear();
}
if (arguments.getArguments() != null && arguments.getArguments().size() > 0) {
return arguments;
}
return null;
}
private void addArguments(HashTree tree, String key, String value) {
Arguments arguments = new Arguments();
arguments.setEnabled(true);

View File

@ -165,11 +165,12 @@ public class MsJDBCSampler extends MsTestElement {
tree.add(arguments);
}
// 环境通用请求头
Arguments envArguments = getConfigArguments(config);
Arguments envArguments = ElementUtil.getConfigArguments(config, this.getName(), this.getProjectId(), null);
if (envArguments != null) {
tree.add(envArguments);
}
//添加csv
ElementUtil.addOtherVariables(config, tree, this.getProjectId());
//增加误报全局断言
HashTreeUtil.addPositive(envConfig, samplerHashTree, config, this.getProjectId());
@ -192,30 +193,6 @@ public class MsJDBCSampler extends MsTestElement {
}
/**
* 环境通用变量
*/
private Arguments getConfigArguments(ParameterConfig config) {
Arguments arguments = new Arguments();
arguments.setEnabled(true);
arguments.setName(StringUtils.isNotEmpty(this.getName()) ? this.getName() : "Arguments");
arguments.setProperty(TestElement.TEST_CLASS, Arguments.class.getName());
arguments.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ArgumentsPanel"));
// 环境通用变量
if (config.isEffective(this.getProjectId()) && config.getConfig().get(this.getProjectId()).getCommonConfig() != null
&& CollectionUtils.isNotEmpty(config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables())) {
config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables().stream().filter(KeyValue::isValid).filter(KeyValue::isEnable).forEach(keyValue ->
arguments.addArgument(keyValue.getName(), ElementUtil.getEvlValue(keyValue.getValue()), "=")
);
// 清空变量防止重复添加
config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables().clear();
}
if (arguments.getArguments() != null && arguments.getArguments().size() > 0) {
return arguments;
}
return null;
}
private boolean isDataSource(List<DatabaseConfig> databaseConfigs) {
List<String> ids = databaseConfigs.stream().map(DatabaseConfig::getId).collect(Collectors.toList());
if (StringUtils.isNotEmpty(this.dataSourceId) && ids.contains(this.dataSourceId)) {

View File

@ -171,11 +171,12 @@ public class MsTCPSampler extends MsTestElement {
}
// 添加环境中的公共变量
Arguments arguments = ElementUtil.addArguments(config, this.getProjectId(), this.getName());
Arguments arguments = ElementUtil.getConfigArguments(config, this.getName(), this.getProjectId(), null);
if (arguments != null) {
tree.add(arguments);
}
//添加csv
ElementUtil.addOtherVariables(config, tree, this.getProjectId());
final HashTree samplerHashTree = new ListedHashTree();
samplerHashTree.add(tcpConfig());
tree.set(tcpSampler(config), samplerHashTree);

View File

@ -13,7 +13,7 @@ public class ScenarioVariable {
/**
* CONSTANT LIST CSV COUNTER RANDOM
*/
private String type;
private String type = VariableTypeConstants.CONSTANT.name();
private String id;
private String name;
@ -41,6 +41,8 @@ public class ScenarioVariable {
private String minNumber;
private String maxNumber;
private boolean enable = true;
public ScenarioVariable() {
}

View File

@ -1,13 +1,13 @@
package io.metersphere.api.dto.scenario.environment;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.definition.request.variable.ScenarioVariable;
import lombok.Data;
import java.util.List;
@Data
public class CommonConfig {
private List<KeyValue> variables;
private List<ScenarioVariable> variables;
private boolean enableHost;
private List<Host> hosts;
private int requestTimeout;

View File

@ -8,9 +8,12 @@ import io.metersphere.api.dto.mockconfig.MockConfigStaticData;
import io.metersphere.api.tcp.TCPPool;
import io.metersphere.base.domain.ApiTestEnvironmentExample;
import io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs;
import io.metersphere.base.domain.FileAssociationExample;
import io.metersphere.base.domain.Project;
import io.metersphere.base.mapper.ApiTestEnvironmentMapper;
import io.metersphere.base.mapper.FileAssociationMapper;
import io.metersphere.base.mapper.ext.ExtApiTestEnvironmentMapper;
import io.metersphere.commons.constants.FileAssociationType;
import io.metersphere.commons.constants.ProjectApplicationType;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.CommonBeanFactory;
@ -25,6 +28,7 @@ import io.metersphere.log.utils.ReflexObjectUtil;
import io.metersphere.log.vo.DetailColumn;
import io.metersphere.log.vo.OperatingLogDetails;
import io.metersphere.log.vo.system.SystemReference;
import io.metersphere.metadata.service.FileAssociationService;
import io.metersphere.service.EnvironmentGroupProjectService;
import io.metersphere.service.ProjectApplicationService;
import io.metersphere.service.ProjectService;
@ -50,6 +54,10 @@ public class ApiTestEnvironmentService {
private ProjectApplicationService projectApplicationService;
@Resource
private ExtApiTestEnvironmentMapper extApiTestEnvironmentMapper;
@Resource
private FileAssociationService fileAssociationService;
@Resource
private FileAssociationMapper fileAssociationMapper;
public List<ApiTestEnvironmentWithBLOBs> list(String projectId) {
ApiTestEnvironmentExample example = new ApiTestEnvironmentExample();
@ -85,6 +93,9 @@ public class ApiTestEnvironmentService {
public void delete(String id) {
apiTestEnvironmentMapper.deleteByPrimaryKey(id);
environmentGroupProjectService.deleteRelateEnv(id);
FileAssociationExample associationExample = new FileAssociationExample();
associationExample.createCriteria().andSourceIdEqualTo(id);
fileAssociationMapper.deleteByExample(associationExample);
}
public void update(ApiTestEnvironmentWithBLOBs apiTestEnvironment) {
@ -99,16 +110,19 @@ public class ApiTestEnvironmentService {
return apiTestEnvironmentWithBLOBs.getId();
}
public String add(ApiTestEnvironmentDTO request, List<MultipartFile> sslFiles) {
public String add(ApiTestEnvironmentDTO request, List<MultipartFile> sslFiles, List<MultipartFile> variableFile) {
request.setId(UUID.randomUUID().toString());
request.setCreateUser(SessionUtils.getUserId());
checkEnvironmentExist(request);
FileUtils.createFiles(request.getUploadIds(), sslFiles, FileUtils.BODY_FILE_DIR + "/ssl");
FileUtils.createBodyFiles(request.getVariablesFilesIds(), variableFile);
//检查Config判断isMock参数是否给True
request = this.updateConfig(request, false);
request.setCreateTime(System.currentTimeMillis());
request.setUpdateTime(System.currentTimeMillis());
apiTestEnvironmentMapper.insert(request);
// 存储附件关系
fileAssociationService.saveEnvironment(request.getId(), request.getConfig(), FileAssociationType.ENVIRONMENT.name());
return request.getId();
}
@ -128,10 +142,13 @@ public class ApiTestEnvironmentService {
return request;
}
public void update(ApiTestEnvironmentDTO apiTestEnvironment, List<MultipartFile> sslFiles) {
public void update(ApiTestEnvironmentDTO apiTestEnvironment, List<MultipartFile> sslFiles, List<MultipartFile> variablesFiles) {
checkEnvironmentExist(apiTestEnvironment);
FileUtils.createFiles(apiTestEnvironment.getUploadIds(), sslFiles, FileUtils.BODY_FILE_DIR + "/ssl");
FileUtils.createBodyFiles(apiTestEnvironment.getVariablesFilesIds(), variablesFiles);
apiTestEnvironment.setUpdateTime(System.currentTimeMillis());
// 存储附件关系
fileAssociationService.saveEnvironment(apiTestEnvironment.getId(), apiTestEnvironment.getConfig(), FileAssociationType.ENVIRONMENT.name());
apiTestEnvironmentMapper.updateByPrimaryKeyWithBLOBs(apiTestEnvironment);
}

View File

@ -1,5 +1,5 @@
package io.metersphere.commons.constants;
public enum FileAssociationType {
API, CASE, SCENARIO, UI
API, CASE, SCENARIO, UI, ENVIRONMENT
}

View File

@ -1,5 +1,6 @@
package io.metersphere.metadata.service;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.api.dto.definition.request.MsScenario;
import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy;
import io.metersphere.api.dto.definition.request.variable.ScenarioVariable;
@ -104,7 +105,7 @@ public class FileAssociationService {
}
}
public void saveApi(String id, MsTestElement request,String type) {
public void saveApi(String id, MsTestElement request, String type) {
this.deleteByResourceId(id);
if (StringUtils.isNotEmpty(id) && request != null && StringUtils.equalsIgnoreCase(request.getType(), HTTPSamplerProxy.class.getSimpleName())) {
MsHTTPSamplerProxy samplerProxy = (MsHTTPSamplerProxy) request;
@ -134,6 +135,22 @@ public class FileAssociationService {
}
}
public void saveEnvironment(String id, String config, String type) {
this.deleteByResourceId(id);
List<BodyFile> files = new ArrayList<>();
if (StringUtils.isNotEmpty(config)) {
JSONObject commonConfig = JSONObject.parseObject(config).getJSONObject("commonConfig");
List<ScenarioVariable> list = JSONObject.parseArray(commonConfig.getString("variables"), ScenarioVariable.class);
list.stream().filter(ScenarioVariable::isCSVValid).forEach(keyValue -> {
files.addAll(keyValue.getFiles().stream().filter(BodyFile::isRef).collect(Collectors.toList()));
});
}
if (!CollectionUtils.isEmpty(files)) {
List<BodyFile> list = files.stream().distinct().collect(Collectors.toList());
this.save(list, type, id);
}
}
private void getHashTree(List<MsTestElement> testElements, List<BodyFile> files) {
testElements.forEach(item -> {
if (StringUtils.equalsIgnoreCase(item.getType(), HTTPSamplerProxy.class.getSimpleName())) {

View File

@ -129,11 +129,11 @@ export default {
},
download() {
//
if (this.parameter.files && this.parameter.files.length > 0 && this.parameter.files[0].file) {
if (this.parameter.files && this.parameter.files.length > 0 && this.parameter.files[0].file && this.parameter.files[0].file.name) {
downloadFile(this.parameter.files[0].file.name, this.parameter.files[0].file);
}
//
if (this.parameter.files && this.parameter.files.length > 0 && !this.parameter.files[0].file) {
if (this.parameter.files && this.parameter.files.length > 0 && (!this.parameter.files[0].file || !this.parameter.files[0].file.name)) {
let file = this.parameter.files[0];
let conf = {
url: "/api/automation/file/download",
@ -157,15 +157,15 @@ export default {
}
},
handleRemove(file) {
let fileName = file.file ? file.file.name : file.name
this.$alert('是否确认删除CSV文件:【 ' + fileName + " 】?", '', {
let fileName = file.name ? file.name : file.file.name
this.$alert(this.$t('api_test.environment.csv_delete') + ':【 ' + fileName + " 】?", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this.$refs.upload.handleRemove(file);
for (let i = 0; i < this.parameter.files.length; i++) {
let paramFileName = this.parameter.files[i].file ?
this.parameter.files[i].file.name : this.parameter.files[i].name;
let paramFileName = this.parameter.files[i].name ?
this.parameter.files[i].name : this.parameter.files[i].file.name;
if (fileName === paramFileName) {
this.parameter.files.splice(i, 1);
this.$refs.upload.handleRemove(file);
@ -180,6 +180,7 @@ export default {
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
},
upload(file) {
this.parameter.files = [];
this.parameter.files.push(file);
},
uploadValidate(file) {

View File

@ -17,7 +17,7 @@
<el-tab-pane :label="$t('variables.config')" name="config">
<el-row>
<el-col :span="5" style="margin-top: 5px">
<span>{{$t('variables.add_file')}}</span>
<span>{{ $t('variables.add_file') }}</span>
</el-col>
<el-col :span="19">
<ms-csv-file-upload :parameter="editData"/>
@ -40,7 +40,7 @@
</el-row>
<el-row style="margin-top: 10px">
<el-col :span="5" style="margin-top: 5px">
<span>{{$t('variables.delimiter')}}</span>
<span>{{ $t('variables.delimiter') }}</span>
</el-col>
<el-col :span="19">
<el-input v-model="editData.delimiter" size="small" :disabled="disabled"/>
@ -48,7 +48,7 @@
</el-row>
<el-row style="margin-top: 10px">
<el-col :span="5" style="margin-top: 5px">
<span>{{$t('variables.quoted_data')}}</span>
<span>{{ $t('variables.quoted_data') }}</span>
</el-col>
<el-col :span="19">
<el-select v-model="editData.quotedData" size="small" :disabled="disabled">
@ -66,10 +66,11 @@
height="200px"
v-loading="loading">
<!-- 自定义列的遍历-->
<el-table-column v-for="(item, index) in columns" :key="index" :label="columns[index]" align="left" width="180">
<el-table-column v-for="(item, index) in columns" :key="index" :label="columns[index]" align="left"
width="180">
<!-- 数据的遍历 scope.row就代表数据的每一个对象-->
<template slot-scope="scope">
<span>{{scope.row[index]}}</span>
<span>{{ scope.row[index] }}</span>
</template>
</el-table-column>
</el-table>
@ -79,9 +80,9 @@
</template>
<script>
import MsCsvFileUpload from "./CsvFileUpload";
import MsCsvFileUpload from "./CsvFileUpload";
export default {
export default {
name: "MsEditCsv",
components: {
MsCsvFileUpload
@ -152,12 +153,12 @@
};
this.allData = [];
//
if (this.editData.files && this.editData.files.length > 0 && this.editData.files[0].file) {
if (this.editData.files && this.editData.files.length > 0 && this.editData.files[0].file && this.editData.files[0].file.name) {
this.loading = true;
this.$papa.parse(this.editData.files[0].file, config);
}
//
if (this.editData.files && this.editData.files.length > 0 && !this.editData.files[0].file) {
if (this.editData.files && this.editData.files.length > 0 && (!this.editData.files[0].file || !this.editData.files[0].file.name)) {
let file = this.editData.files[0];
let conf = {
url: "/api/automation/file/download",
@ -187,18 +188,18 @@
},
querySearch(queryString, cb) {
let restaurants = [{value: "UTF-8"}, {value: "UTF-16"},{value: "GB2312"}, {value: "ISO-8859-15"}, {value: "US-ASCll"}];
let restaurants = [{value: "UTF-8"}, {value: "UTF-16"}, {value: "GB2312"}, {value: "ISO-8859-15"}, {value: "US-ASCll"}];
let results = queryString ? restaurants.filter(this.createFilter(queryString)) : restaurants;
// callback
cb(results);
},
}
}
}
</script>
<style scoped>
ms-is-leaf >>> .is-leaf {
ms-is-leaf >>> .is-leaf {
color: red;
}
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<el-dialog :visible="dialogVisible" :title="dialogTitle"
@close="close" :close-on-click-modal="false" append-to-body
width="35%">
<el-form :rules="rules" label-width="80px" v-model="modeId">
<el-form-item prop="modeId" :label="$t('commons.import_mode')">
<el-select size="small" v-model="modeId">
<el-option v-for="item in modeOptions" :key="item.id" :label="item.name" :value="item.id"/>
</el-select>
</el-form-item>
<el-form-item>
<el-upload
class="api-upload" drag action="alert"
:on-change="handleFileChange"
:limit="1" :file-list="uploadFiles"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:auto-upload="false" accept=".json">
<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') }}
{{ '' + $t('api_test.api_import.ms_env_import_file_limit') }}
</div>
</el-upload>
</el-form-item>
</el-form>
<template v-slot:footer>
<el-button type="primary" @click="save">
{{ $t('commons.confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: "VariableImport",
props: {
projectList: {
type: Array,
default() {
return [];
}
},
toImportProjectId: {
type: String,
default() {
return "";
}
}
},
data() {
return {
currentProjectId: '', //id
uploadFiles: [],
dialogTitle: this.$t('commons.import_variable'),
dialogVisible: false,
modeOptions: [
{
id: 'fullCoverage',
name: this.$t('commons.cover')
},
{
id: 'incrementalMerge',
name: this.$t('commons.not_cover')
}
],
modeId: 'fullCoverage',
rules: {
modeId: [
{required: true, message: "", trigger: 'blur'},
],
},
}
},
watch: {
//
dialogVisible(val, oldVal) {
if (oldVal === false) {
this.currentProjectId = '';
this.uploadFiles = [];
}
}
},
methods: {
handleFileChange(file, uploadFiles) {
this.uploadFiles = uploadFiles;
},
save() {
if (this.uploadFiles.length > 0) {
for (let i = 0; i < this.uploadFiles.length; i++) {
this.uploadValidate(this.uploadFiles[i]);
let file = this.uploadFiles[i];
if (!file) {
continue;
}
let reader = new FileReader();
reader.readAsText(file.raw)
reader.onload = (e) => {
let fileString = e.target.result;
let messages = '';
try {
JSON.parse(fileString).map(env => {
if (!env.name) {
messages = this.$t('api_test.automation.variable_warning')
}
})
if (messages !== '') {
this.$warning(messages);
return;
}
this.$emit("mergeData", fileString, this.modeId);
this.dialogVisible = false;
this.$success(this.$t('commons.save_success'));
} catch (exception) {
this.$warning(this.$t('api_test.api_import.ms_env_import_file_limit'));
}
}
}
} else {
this.$warning(this.$t('test_track.case.import.import_file_tips'));
}
},
handleExceed() {
this.$warning(this.$t('api_test.api_import.file_exceed_limit'));
},
handleRemove() {
},
uploadValidate(file) { //.json20M
const extension = file.name.substring(file.name.lastIndexOf('.') + 1);
if (!(extension === 'json')) {
this.$warning(this.$t('api_test.api_import.ms_env_import_file_limit'));
}
if (file.size / 1024 / 1024 > 20) {
this.$warning(this.$t('api_test.api_import.file_size_limit'));
}
},
open() {
this.dialogVisible = true;
},
close() {
this.dialogVisible = false;
}
},
}
</script>
<style scoped>
.project-item {
padding-left: 20px;
padding-right: 20px;
}
</style>

View File

@ -37,9 +37,23 @@
<el-button size="small" style="margin-left: 10px" type="primary" @click="addVariable">
{{ $t('commons.add') }}
</el-button>
<el-link @click="batchAddParameter" type="primary" :disabled="disabled" style="margin-left: 10px">
<el-dropdown style="margin-left: 10px">
<el-button size="small">
<span class="tip-font">{{ $t('commons.more_operator') }}</span>
<i class="el-icon-arrow-down el-icon--right"/>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native.stop="importVariable" @mergeData="mergeData" :disabled="disabled">
{{ $t("commons.import_variable") }}
</el-dropdown-item>
<el-dropdown-item @click.native.stop="exportVariable" :disabled="disabled">
{{ $t("commons.export_variable") }}
</el-dropdown-item>
<el-dropdown-item @click.native.stop="batchAddParameter" :disabled="disabled">
{{ $t("commons.batch_add") }}
</el-link>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-row>
<el-row>
@ -94,6 +108,13 @@
:label="$t('api_test.value')"
sortable>
</ms-table-column>
<ms-table-column
prop="description"
:field="item"
:fields-width="fieldsWidth"
:label="$t('commons.description')"
sortable>
</ms-table-column>
</span>
</ms-table>
<batch-add-parameter @batchSave="batchSaveParameter" ref="batchAddParameter"/>
@ -149,6 +170,7 @@
</template>
</el-collapse-transition>
</fieldset>
<variable-import ref="variableImport"></variable-import>
</el-dialog>
</template>
@ -161,7 +183,7 @@ import MsEditCounter from "./EditCounter";
import MsEditRandom from "./EditRandom";
import MsEditListValue from "./EditListValue";
import MsEditCsv from "./EditCsv";
import {getUUID} from "@/common/js/utils";
import {downloadFile, getUUID} from "@/common/js/utils";
import MsApiKeyValue from "../../../definition/components/ApiKeyValue";
import BatchAddParameter from "../../../definition/components/basis/BatchAddParameter";
import {KeyValue} from "../../../definition/model/ApiTestModel";
@ -170,6 +192,7 @@ import {REQUEST_HEADERS} from "@/common/js/constants";
import MsTable from "@/business/components/common/components/table/MsTable";
import MsTableColumn from "@/business/components/common/components/table/MsTableColumn";
import {getCustomTableHeader, getCustomTableWidth} from "@/common/js/tableUtils";
import VariableImport from "@/business/components/api/automation/scenario/variable/VariableImport";
const jsondiffpatch = require('jsondiffpatch');
@ -188,6 +211,7 @@ export default {
BatchAddParameter,
MsTableColumn,
MsTable,
VariableImport
},
data() {
return {
@ -230,6 +254,46 @@ export default {
};
},
methods: {
importVariable() {
this.$refs.variableImport.open();
},
mergeData(data, modeId) {
JSON.parse(data).map(importData => {
importData.id = getUUID();
importData.enable = true;
importData.showMore = false;
let sameNameIndex = this.variables.findIndex(d => d.name === importData.name);
if (sameNameIndex !== -1) {
if (modeId === 'fullCoverage') {
this.variables.splice(sameNameIndex, 1, importData);
}
} else {
this.variables.splice(this.variables.length - 1, 0, importData);
}
})
},
exportVariable() {
if (this.$refs.variableTable.selectIds.length < 1) {
this.$warning(this.$t('api_test.environment.select_environment'));
return;
}
let variablesJson = [];
let messages = '';
let rows = this.$refs.variableTable.selectRows;
rows.forEach(row => {
if (row.type === 'CSV') {
messages = this.$t('variables.csv_download')
}
if (row.name) {
variablesJson.push(row);
}
})
if (messages !== '') {
this.$warning(messages);
return;
}
downloadFile('MS_' + variablesJson.length + '_Environments_variables.json', JSON.stringify(variablesJson));
},
batchAddParameter() {
this.$refs.batchAddParameter.open();
},
@ -241,20 +305,21 @@ export default {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
if (item) {
let line = item.split(/|:/);
let required = false;
keyValues.unshift(new KeyValue({
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: line[1],
description: line[2],
type: "text",
type: "CONSTANT",
valid: false,
file: false,
encode: true,
enable: true,
contentType: "text/plain"
}));
}
})
return keyValues;
}
@ -278,7 +343,7 @@ export default {
}
}
if (isAdd) {
this.headers.unshift(obj);
this.headers.splice(this.headers.indexOf(h => !h.name), 0, obj);
}
}
},
@ -346,6 +411,18 @@ export default {
this.disabled = disabled;
this.$nextTick(() => {
this.$refs.variableTable.doLayout();
let variable = [];
let messages = '';
this.variables.forEach(item => {
if (variable.indexOf(item.name) !== -1) {
messages += item.name + ' ,';
} else {
variable.push(item.name);
}
})
if (messages !== '') {
this.$alert(this.$t('api_test.scenario.variables') + "【" + messages.substr(0, messages.length - 1) + "】" + this.$t('load_test.param_is_duplicate'));
}
});
},
save() {
@ -382,6 +459,18 @@ export default {
this.$warning(this.$t('api_test.automation.variable_warning'));
return;
}
let repeatKey = "";
if (!this.showDelete) {
this.variables.forEach((item, index) => {
if (item.name === this.editData.name) {
repeatKey = item.name;
}
});
}
if (repeatKey !== "") {
this.$warning(this.$t('api_test.scenario.variables') + "【" + repeatKey + "】" + this.$t('load_test.param_is_duplicate'));
return;
}
if (this.editData.type === 'CSV' && this.$refs.csv) {
if (this.editData.files.length === 0) {
this.$warning(this.$t('api_test.automation.csv_warning'));
@ -407,7 +496,7 @@ export default {
deleteVariable() {
let ids = [this.editData.id];
if (ids.length == 0) {
this.$warning("请选择一条数据删除");
this.$warning(this.$t('api_test.environment.delete_info'));
return;
}
let message = "";
@ -419,7 +508,7 @@ export default {
});
if (message !== "") {
message = message.substr(0, message.length - 1);
this.$alert('是否确认删除变量:【 ' + message + " 】?", '', {
this.$alert(this.$t('api_test.environment.variables_delete_info') + ':【 ' + message + " 】?", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
@ -442,7 +531,7 @@ export default {
}
},
handleDeleteBatch() {
this.$alert("是否确认删除所选变量" + ' ' + " ", '', {
this.$alert(this.$t('api_test.environment.variables_delete_info') + ' ' + " ", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {

View File

@ -8,12 +8,15 @@
<div v-html="$t('api_test.batch_add_parameter')"/>
</el-col>
<el-col :span="10" class="buttons">
<el-button size="mini" @click="handleClose">{{$t('commons.cancel')}}</el-button>
<el-button type="primary" size="mini" @click="confirm" @keydown.enter.native.prevent>{{$t('commons.confirm')}}</el-button>
<el-button size="mini" @click="handleClose">{{ $t('commons.cancel') }}</el-button>
<el-button type="primary" size="mini" @click="confirm" @keydown.enter.native.prevent>
{{ $t('commons.confirm') }}
</el-button>
</el-col>
</el-row>
<div class="ms-code">
<ms-code-edit class="ms-code" :enable-format="false" mode="text" :data.sync="parameters" theme="eclipse" :modes="['text']"
<ms-code-edit class="ms-code" :enable-format="false" mode="text" :data.sync="parameters" theme="eclipse"
:modes="['text']"
ref="codeEdit"/>
</div>
</div>
@ -22,12 +25,12 @@
</template>
<script>
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
import {listenGoBack, removeGoBackListener} from "@/common/js/utils";
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsDrawer from "../../../../common/components/MsDrawer";
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
import {listenGoBack, removeGoBackListener} from "@/common/js/utils";
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsDrawer from "../../../../common/components/MsDrawer";
export default {
export default {
name: "BatchAddParameter",
components: {
MsDrawer,
@ -52,29 +55,45 @@
removeGoBackListener(this.handleClose);
},
confirm() {
let params = this.parameters.split("\n");
let index = 1;
let isNormal = true;
params.forEach(item => {
if (item) {
let line = item.split(/|:/);
if (!line[0]) {
isNormal = false;
this.$warning(this.$t('api_test.params_format_warning', [index]) + " :" + this.$t('api_test.automation.variable_warning'));
return;
}
index++;
}
});
if (isNormal) {
this.dialogVisible = false;
this.$emit("batchSave", this.parameters);
this.parameters = "";
}
}
}
}
</script>
<style scoped>
.ms-drawer {
.ms-drawer {
padding: 10px 13px;
}
}
.ms-code {
.ms-code {
height: calc(100vh);
}
}
.buttons .el-button {
.buttons .el-button {
float: right;
}
}
.buttons .el-button:nth-child(2) {
.buttons .el-button:nth-child(2) {
margin-right: 15px;
}
}
</style>

View File

@ -302,7 +302,7 @@ export default {
}
}
if (isAdd) {
this.body.kvs.unshift(obj);
this.body.kvs.splice(this.body.kvs.indexOf(kv => !kv.name), 0, obj);
}
}
},
@ -311,11 +311,12 @@ export default {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
if (item) {
let line = [];
line[0] = item.substring(0, item.indexOf(":"));
line[1] = item.substring(item.indexOf(":") + 1, item.length);
let required = false;
keyValues.unshift(new KeyValue({
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: line[1],
@ -327,6 +328,7 @@ export default {
enable: true,
contentType: "text/plain"
}));
}
})
keyValues.forEach(item => {
this.format(this.body.kvs, item);

View File

@ -2,19 +2,22 @@
<el-main v-loading="result.loading">
<el-form :model="environment" :rules="rules" ref="environment">
<span>{{$t('api_test.environment.name')}}</span>
<span>{{ $t('api_test.environment.name') }}</span>
<el-form-item prop="name">
<el-input v-model="environment.name" :disabled="isReadOnly" :placeholder="this.$t('commons.input_name')" clearable/>
<el-input v-model="environment.name" :disabled="isReadOnly" :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" :is-read-only="isReadOnly"/>
<ms-environment-common-config :common-config="environment.config.commonConfig" ref="commonConfig"
:is-read-only="isReadOnly"/>
</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" :is-read-only="isReadOnly"/>
<ms-environment-http-config :http-config="environment.config.httpConfig" ref="httpConfig"
:is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.database_config')" name="sql">
<ms-database-config :configs="environment.config.databaseConfigs" :is-read-only="isReadOnly"/>
@ -34,24 +37,27 @@
</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 "../../../test/components/environment/EnvironmentHttpConfig";
import MsEnvironmentCommonConfig from "./EnvironmentCommonConfig";
import EnvironmentTcpConfig from "./EnvironmentTcpConfig";
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 "../../../test/components/environment/EnvironmentHttpConfig";
import MsEnvironmentCommonConfig from "./EnvironmentCommonConfig";
import EnvironmentTcpConfig from "./EnvironmentTcpConfig";
import {getUploadConfig, request} from "@/common/js/ajax";
import {getUUID} from "@/common/js/utils";
export default {
export default {
name: "EnvironmentEdit",
components: {
EnvironmentTcpConfig,
MsEnvironmentCommonConfig,
MsEnvironmentHttpConfig,
MsDatabaseConfig, MsApiHostTable, MsDialogFooter, MsApiKeyValue, MsApiScenarioVariables},
MsDatabaseConfig, MsApiHostTable, MsDialogFooter, MsApiKeyValue, MsApiScenarioVariables
},
props: {
environment: new Environment(),
isReadOnly: {
@ -99,17 +105,115 @@
return isValidate;
},
_save(environment) {
if (!this.projectId) {
this.$warning(this.$t('api_test.select_project'));
return;
}
if (environment && environment.config && environment.config.commonConfig && environment.config.commonConfig.variables) {
let repeatKey = this.check(environment.config.commonConfig && environment.config.commonConfig.variables);
if (repeatKey !== "") {
this.$warning(this.$t('api_test.environment.common_config') + "【" + repeatKey + "】" + this.$t('load_test.param_is_duplicate'));
return;
}
}
let message = '';
if (environment && environment.config && environment.config.httpConfig && environment.config.httpConfig.conditions) {
environment.config.httpConfig.conditions.forEach(env => {
if (env.type === "MODULE" && env.details.length === 0) {
message += this.$t('load_test.domain') + ":" + env.socket + ":" + this.$t('api_test.environment.module_warning');
return;
}
if (env.type === "PATH" && env.details) {
env.details.forEach(item => {
if (!item.name) {
message += this.$t('load_test.domain') + ":" + env.socket + ":" + this.$t('api_test.environment.path_warning');
return;
}
})
}
})
}
environment.config.commonConfig.variables.forEach(variable => {
if (variable.type === 'CSV' && variable.files.length === 0) {
message = this.$t('api_test.automation.csv_warning');
return;
}
})
if (message) {
this.$warning(message);
return;
}
let bodyFiles = this.geFiles(environment);
let variablesFiles = this.getVariablesFiles(environment);
let formData = new FormData();
if (bodyFiles) {
bodyFiles.forEach(f => {
formData.append("files", f);
})
}
if (variablesFiles) {
variablesFiles.forEach(f => {
formData.append("variablesFiles", f);
})
}
let param = this.buildParam(environment);
let url = '/api/environment/add';
if (param.id) {
url = '/api/environment/update';
}
this.result = this.$fileUpload(url, null, [], param, response => {
if (!param.id) {
environment.id = response.data;
}
formData.append('request', new Blob([JSON.stringify(param)], {type: "application/json"}));
let axiosRequestConfig = getUploadConfig(url, formData);
request(axiosRequestConfig, (response) => {
if (response.success) {
this.$success(this.$t('commons.save_success'));
this.$emit('refreshAfterSave'); //EnvironmentList.vue使
this.cancel()
}
}, error => {
this.$emit('errorRefresh', error);
});
},
geFiles(obj) {
let uploadFiles = [];
obj.uploadIds = [];
if (obj.config && obj.config.sslConfig && obj.config.sslConfig.files) {
obj.config.sslConfig.files.forEach(item => {
if (item.file && item.file.size > 0) {
if (!item.id) {
item.name = item.file.name;
item.id = getUUID();
}
obj.uploadIds.push(item.id);
uploadFiles.push(item.file);
}
})
}
return uploadFiles;
},
getVariablesFiles(obj) {
let variablesFiles = [];
obj.variablesFilesIds = [];
// csv
if (obj.config.commonConfig.variables) {
obj.config.commonConfig.variables.forEach(param => {
if (param.type === 'CSV' && param.files) {
param.files.forEach(item => {
if (item.file && item.file.name) {
if (!item.id) {
let fileId = getUUID().substring(0, 12);
item.name = item.file.name;
item.id = fileId;
}
obj.variablesFilesIds.push(item.id);
variablesFiles.push(item.file);
}
})
}
});
}
return variablesFiles;
},
buildParam: function (environment) {
let param = {};
@ -135,35 +239,35 @@
this.$refs["environment"].clearValidate();
},
},
}
}
</script>
<style scoped>
.el-main {
.el-main {
border: solid 1px #EBEEF5;
margin-left: 200px;
min-height: 400px;
max-height: 700px;
}
}
.el-row {
.el-row {
margin-bottom: 15px;
}
}
.environment-footer {
.environment-footer {
margin-top: 15px;
float: right;
}
}
span {
span {
display: block;
margin-bottom: 15px;
}
}
span:not(:first-child) {
span:not(:first-child) {
margin-top: 15px;
}
}
</style>

View File

@ -274,11 +274,12 @@ export default {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
if (item) {
let line = [];
line[0] = item.substring(0,item.indexOf(":"));
line[1] = item.substring(item.indexOf(":")+1,item.length);
line[0] = item.substring(0, item.indexOf(":"));
line[1] = item.substring(item.indexOf(":") + 1, item.length);
let required = false;
keyValues.unshift(new KeyValue({
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: line[1],
@ -290,6 +291,7 @@ export default {
enable: true,
contentType: "text/plain"
}));
}
})
keyValues.forEach(item => {
this.format(this.body.kvs, item);

View File

@ -309,11 +309,12 @@ export default {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
if (item) {
let line = [];
line[0] = item.substring(0, item.indexOf(":"));
line[1] = item.substring(item.indexOf(":") + 1, item.length);
let required = false;
keyValues.unshift(new KeyValue({
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: line[1],
@ -325,6 +326,7 @@ export default {
enable: true,
contentType: "text/plain"
}));
}
})
keyValues.forEach(item => {
this.format(this.body.kvs, item);

View File

@ -424,13 +424,13 @@ export default {
if (isAdd) {
switch (this.activeName) {
case "parameters":
this.request.arguments.unshift(obj);
this.request.arguments.splice(this.request.arguments.indexOf(h => !h.name), 0, obj);
break;
case "rest":
this.request.rest.unshift(obj);
this.request.rest.splice(this.request.rest.indexOf(h => !h.name), 0, obj);
break;
case "headers":
this.request.headers.unshift(obj);
this.request.headers.splice(this.request.headers.indexOf(h => !h.name), 0, obj);
break;
default:
break;
@ -443,10 +443,11 @@ export default {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
if (item) {
let line = item.split(/|:/);
let values = item.split(line[0] + ":");
let required = false;
keyValues.unshift(new KeyValue({
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: values[1],
@ -457,6 +458,7 @@ export default {
enable: true,
contentType: "text/plain"
}));
}
})
keyValues.forEach(item => {

View File

@ -8,9 +8,9 @@
:data="environments" :item-operators="environmentOperators" :add-fuc="addEnvironment"
:env-add-permission="ENV_CREATE"
:delete-fuc="openDelEnv" @itemSelected="environmentSelected" ref="environmentItems"/>
<environment-edit :if-create="ifCreate" :project-id="projectId" :environment="currentEnvironment"
<environment-edit :if-create="ifCreate" :environment="currentEnvironment"
ref="environmentEdit" :is-read-only="isReadOnly"
@confirm="save"
@confirm="save" :is-project="true"
@close="close"/>
</el-container>
</el-dialog>

View File

@ -112,7 +112,7 @@ export default {
let line = item.split(/|:/);
let values = item.split(line[0] + ":");
let required = false;
keyValues.unshift(new KeyValue({
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: values[1],

View File

@ -0,0 +1,399 @@
<template>
<div>
<div>
<div style="padding-bottom: 10px;float: left">
<el-input :placeholder="$t('commons.search_by_name')" size="mini" v-model="selectVariable"
@change="filter"
@keyup.enter="filter">
</el-input>
</div>
<div style="padding-bottom: 10px; float: right;">
<ms-table-button v-permission="['PROJECT_ENVIRONMENT:READ+IMPORT']" icon="el-icon-box"
:content="$t('commons.import')" @click="importJSON"/>
<ms-table-button v-permission="['PROJECT_ENVIRONMENT:READ+EXPORT']" icon="el-icon-box"
:content="$t('commons.export')" @click="exportJSON"/>
<el-link style="margin-left: 10px" @click="batchAdd" type="primary" :disabled="isReadOnly">
{{ $t("commons.batch_add") }}
</el-link>
</div>
</div>
<div
style="border:1px #DCDFE6 solid; min-height: 300px;border-radius: 4px ;width: 99% ;margin-top: 10px; clear: both">
<ms-table
v-loading="loading"
row-key="id"
:data="variables"
:total="items.length"
:screen-height="screenHeight"
:batch-operators="batchButtons"
:remember-order="true"
:highlightCurrentRow="true"
@refresh="onChange"
ref="variableTable">
<ms-table-column
prop="num"
sortable
label="ID"
min-width="60">
</ms-table-column>
<ms-table-column prop="name" :label="$t('api_test.variable_name')" min-width="200" sortable>
<template slot-scope="scope">
<el-input
v-model="scope.row.name" size="mini"
maxlength="200" :placeholder="$t('api_test.variable_name')" show-word-limit
@change="change"/>
</template>
</ms-table-column>
<ms-table-column prop="type" :label="$t('test_track.case.type')" min-width="140" sortable>
<template slot-scope="scope">
<el-select v-model="scope.row.type" :placeholder="$t('commons.please_select')" size="mini"
@change="changeType(scope.row)">
<el-option v-for="item in typeSelectOptions " :key="item.value" :label="item.label" :value="item.value"/>
</el-select>
</template>
</ms-table-column>
<ms-table-column prop="value" :label="$t('api_test.value')"
min-width="200px" sortable>
<template slot-scope="scope">
<el-input v-model="scope.row.value" size="mini" v-if="scope.row.type !=='CSV'"
:placeholder="valueText(scope.row)"
:disabled="scope.row.type === 'COUNTER' || scope.row.type === 'RANDOM'"/>
<csv-file-upload :parameter="scope.row" v-if="scope.row.type ==='CSV'"/>
</template>
</ms-table-column>
<ms-table-column prop="description" :label="$t('commons.remark')"
min-width="160" sortable>
<template slot-scope="scope">
<el-input v-model="scope.row.description" size="mini"/>
</template>
</ms-table-column>
<ms-table-column :label="$t('commons.operating')" width="150" fixed="right">
<template v-slot:default="scope">
<span>
<el-switch v-model="scope.row.enable" size="mini"/>
<el-tooltip effect="dark" :content="$t('commons.remove')" placement="top-start">
<el-button icon="el-icon-delete" type="danger" circle size="mini" style="margin-left: 10px"
@click="remove(scope.row)" v-if="scope.row.name"/>
</el-tooltip>
<el-tooltip effect="dark" :content="$t('schema.adv_setting')" placement="top-start">
<el-button icon="el-icon-setting" circle size="mini" style="margin-left: 10px"
@click="openSetting(scope.row)" v-if="scope.row.type !=='LIST'"/>
</el-tooltip>
</span>
</template>
</ms-table-column>
</ms-table>
</div>
<batch-add-parameter @batchSave="batchSave" ref="batchAdd"/>
<api-variable-setting ref="apiVariableSetting"></api-variable-setting>
<variable-import ref="variableImport" @mergeData="mergeData"></variable-import>
</div>
</template>
<script>
import {KeyValue} from "@/business/components/api/test/model/ScenarioModel";
import MsApiVariableInput from "@/business/components/api/automation/scenario/ApiVariableInput";
import BatchAddParameter from "@/business/components/api/definition/components/basis/BatchAddParameter";
import MsTableButton from "@/business/components/common/components/MsTableButton";
import MsTable from "@/business/components/common/components/table/MsTable";
import MsTableColumn from "@/business/components/common/components/table/MsTableColumn";
import ApiVariableSetting from "@/business/components/api/test/components/environment/ApiVariableSetting";
import CsvFileUpload from "@/business/components/api/test/components/environment/CsvFileUpload";
import {downloadFile, getUUID} from "@/common/js/utils";
import VariableImport from "@/business/components/api/test/components/environment/VariableImport";
export default {
name: "MsApiScenarioVariables",
components: {
BatchAddParameter,
MsApiVariableInput,
MsTableButton,
MsTable,
MsTableColumn,
ApiVariableSetting,
CsvFileUpload,
VariableImport
},
props: {
description: String,
items: Array,
isReadOnly: {
type: Boolean,
default: false
},
showVariable: {
type: Boolean,
default: true
},
showCopy: {
type: Boolean,
default: true
},
},
data() {
return {
loading: false,
screenHeight: '400px',
batchButtons: [
{
name: this.$t('api_test.definition.request.batch_delete'),
handleClick: this.handleDeleteBatch,
},
],
typeSelectOptions: [
{value: 'CONSTANT', label: this.$t('api_test.automation.constant')},
{value: 'LIST', label: this.$t('test_track.case.list')},
{value: 'CSV', label: 'CSV'},
{value: 'COUNTER', label: this.$t('api_test.automation.counter')},
{value: 'RANDOM', label: this.$t('api_test.automation.random')},
],
variables: {},
selectVariable: '',
editData: {},
}
},
watch: {
items: {
handler(v) {
this.variables = v;
this.sortParameters();
},
immediate: true,
deep: true
}
},
methods: {
remove: function (index) {
const dataIndex = this.variables.findIndex(d => d.name === index.name);
this.variables.splice(dataIndex, 1);
this.$emit('change', this.variables);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
let repeatKey = "";
this.variables.forEach((item, index) => {
this.variables.forEach((row, rowIndex) => {
if (item.name === row.name && index !== rowIndex) {
repeatKey = item.name;
}
});
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (repeatKey !== "") {
this.$warning(this.$t('api_test.environment.common_config') + "【" + repeatKey + "】" + this.$t('load_test.param_is_duplicate'));
}
if (isNeedCreate && !repeatKey) {
this.variables.push(new KeyValue({enable: true, id: getUUID(), type: 'CONSTANT'}));
}
this.$emit('change', this.variables);
// TODO key
},
changeType(data) {
if (!data.delimiter || (!data.files && data.files.length === 0) || !data.quotedData) {
data.delimiter = ',';
data.files = [];
data.quotedData = 'false';
}
},
valueText(data) {
switch (data.type) {
case 'LIST':
return this.$t('api_test.environment.list_info');
case 'CONSTANT':
return this.$t('api_test.value');
case 'COUNTER':
case 'RANDOM':
return this.$t('api_test.environment.advanced_setting');
default:
return this.$t('api_test.value');
}
},
querySearch(queryString, cb) {
let restaurants = [{value: "UTF-8"}, {value: "UTF-16"}, {value: "GB2312"}, {value: "ISO-8859-15"}, {value: "US-ASCll"}];
let results = queryString ? restaurants.filter(this.createFilter(queryString)) : restaurants;
// callback
cb(results);
},
sortParameters() {
let index = 1;
this.variables.forEach(item => {
item.num = index;
if (!item.type || item.type === 'text') {
item.type = 'CONSTANT';
}
if (!item.id) {
item.id = getUUID();
}
if (item.remark) {
item.description = item.remark;
}
index++;
});
},
handleDeleteBatch() {
this.$alert(this.$t('api_test.environment.variables_delete_info') + ' ' + " ", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
let ids = this.$refs.variableTable.selectRows;
ids.forEach(row => {
if (row.name) {
const index = this.variables.findIndex(d => d.name === row.name);
this.variables.splice(index, 1);
}
});
this.sortParameters();
this.$refs.variableTable.cancelCurrentRow();
this.$refs.variableTable.clear();
}
}
});
},
filter() {
let datas = [];
this.variables.forEach(item => {
if (this.selectVariable && this.selectVariable != "" && item.name) {
if (item.name.toLowerCase().indexOf(this.selectVariable.toLowerCase()) == -1) {
item.hidden = true;
} else {
item.hidden = undefined;
}
} else {
item.hidden = undefined;
}
datas.push(item);
});
this.variables = datas;
},
openSetting(data) {
this.$refs.apiVariableSetting.open(data);
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
_handleBatchVars(data) {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
if (item) {
let line = item.split(/|:/);
let values = item.split(line[0] + ":");
let required = false;
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: values[1],
type: 'CONSTANT',
valid: false,
file: false,
encode: true,
enable: true,
description: undefined
}));
}
});
return keyValues;
},
batchAdd() {
this.$refs.batchAdd.open();
},
batchSave(data) {
if (data) {
let keyValues = this._handleBatchVars(data);
keyValues.forEach(keyValue => {
let isAdd = true;
this.variables.forEach(item => {
if (item.name === keyValue.name) {
item.value = keyValue.value;
isAdd = false;
}
})
if (isAdd) {
this.variables.splice(this.variables.indexOf(i => !i.name), 0, keyValue);
}
})
}
},
onChange() {
this.sortParameters();
},
exportJSON() {
if (this.$refs.variableTable.selectIds.length < 1) {
this.$warning(this.$t('api_test.environment.select_variable'));
return;
}
let variablesJson = [];
let messages = '';
let rows = this.$refs.variableTable.selectRows;
rows.forEach(row => {
if (row.type === 'CSV') {
messages = this.$t('variables.csv_download')
}
if (row.name) {
variablesJson.push(row);
}
})
if (messages !== '') {
this.$warning(messages);
return;
}
downloadFile('MS_' + variablesJson.length + '_Environments_variables.json', JSON.stringify(variablesJson));
},
importJSON() {
this.$refs.variableImport.open();
},
mergeData(data, modeId) {
JSON.parse(data).map(importData => {
importData.id = getUUID();
importData.enable = true;
importData.showMore = false;
let sameNameIndex = this.variables.findIndex(d => d.name === importData.name);
if (sameNameIndex !== -1) {
if (modeId === 'fullCoverage') {
this.variables.splice(sameNameIndex, 1, importData);
}
} else {
this.variables.splice(this.variables.length - 1, 0, importData);
}
})
}
},
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>

View File

@ -0,0 +1,306 @@
<template>
<div>
<el-tabs tab-position="top" @tab-click="selectTab">
<el-tab-pane :label="$t('api_test.request.parameters_advance_mock')">
<el-row type="flex" :gutter="20">
<el-col :span="6" class="col-height">
<div>
<el-input size="small" v-model="filterText"
:placeholder="$t('api_test.request.parameters_mock_filter_tips')"/>
<el-tree class="filter-tree" ref="tree" :data="mockFuncs" :props="treeProps"
default-expand-all @node-click="selectVariable"
:filter-node-method="filterNode"></el-tree>
</div>
</el-col>
<el-col :span="6" v-for="(itemFunc, itemIndex) in mockVariableFuncs" :key="itemIndex">
<div v-for="(func, funcIndex) in funcs"
:key="`${itemIndex}-${funcIndex}`">
<el-row>
<el-col :span="12">
<el-radio size="mini" v-model="itemFunc.name" :label="func.name"
@change="methodChange(itemFunc, func)" @click.native.prevent="radioClick(itemFunc, func)"/>
</el-col>
<el-col :span="12" v-if="itemFunc.name === func.name">
<div v-for="(p, pIndex) in itemFunc.params" :key="`${itemIndex}-${funcIndex}-${pIndex}`">
<el-input :placeholder="p.name" size="mini" v-model="p.value" @change="showPreview"/>
</div>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.variable')">
<el-row>
<el-col :span="6" class="col-height">
<div v-if="environment">
<p>{{ $t('api_test.environment.environment') }}</p>
<el-tree :data="environmentParams" :props="treeProps" @node-click="selectVariable"></el-tree>
</div>
<div v-if="scenario">
<p>{{ $t('api_test.scenario.scenario') }}</p>
<el-tree :data="scenarioParams" :props="treeProps" @node-click="selectVariable"></el-tree>
</div>
<div v-if="preRequestParams">
<p>{{ $t('api_test.request.parameters_pre_request') }}</p>
<el-tree :data="preRequestParams" :props="treeProps" @node-click="selectVariable"></el-tree>
</div>
</el-col>
<el-col :span="18" class="col-height">
<div>
<h1>{{ $t('api_test.request.jmeter_func') }}</h1>
<el-table border :data="jmeterFuncs" class="adjust-table table-content" height="400">
<el-table-column prop="type" label="Type" width="150"/>
<el-table-column prop="name" label="Functions" width="250"/>
<el-table-column prop="description" label="Description"/>
</el-table>
</div>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
<el-form>
<el-form-item>
<el-input :placeholder="valueText" size="small"
v-model="itemValue"/>
</el-form-item>
</el-form>
<div style="padding-top: 10px;">
<el-row type="flex" align="middle">
<el-col :span="12">
<el-button size="small" type="info" plain @click="addFunc()" v-if="currentTab === 0">
{{ $t('api_test.request.parameters_advance_add_func') }}
</el-button>
<el-button size="small" type="success" plain @click="showPreview()" v-if="currentTab === 0">
{{ $t('api_test.request.parameters_preview') }}
</el-button>
</el-col>
<el-col>
<div> {{ itemValuePreview }}</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import {calculate, Scenario} from "@/business/components/api/test/model/ScenarioModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
export default {
name: "MsApiVariableAdvance",
props: {
parameters: Array,
environment: Object,
scenario: Scenario,
currentItem: Object,
appendToBody: {
type: Boolean,
default() {
return false;
}
},
},
data() {
return {
itemValueVisible: false,
filterText: '',
environmentParams: [],
scenarioParams: [],
preRequests: [],
preRequestParams: [],
treeProps: {children: 'children', label: 'name'},
currentTab: 0,
itemValue: null,
itemValuePreview: null,
funcs: [
{name: "md5"},
{name: "base64"},
{name: "unbase64"},
{
name: "substr",
params: [{name: "start"}, {name: "length"}]
},
{
name: "concat",
params: [{name: "suffix"}]
},
{name: "lconcat", params: [{name: "prefix"}]},
{name: "sha1"},
{name: "sha224"},
{name: "sha256"},
{name: "sha384"},
{name: "sha512"},
{name: "lower"},
{name: "upper"},
{name: "length"},
{name: "number"}
],
mockFuncs: MOCKJS_FUNC.map(f => {
return {name: f.name, value: f.name}
}),
jmeterFuncs: JMETER_FUNC,
mockVariableFuncs: [],
jmeterVariableFuncs: [],
}
},
computed: {
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
mounted() {
this.prepareData();
},
watch: {
filterText(val) {
this.$refs.tree.filter(val);
},
},
methods: {
open() {
this.itemValueVisible = true;
},
prepareData() {
if (this.scenario) {
let variables = this.scenario.variables;
this.scenarioParams = [
{
name: this.scenario.name,
children: variables.filter(v => v.name).map(v => {
return {name: v.name, value: '${' + v.name + '}'}
}),
}
];
if (this.environment) {
let variables = this.environment.config.commonConfig.variables;
this.environmentParams = [
{
name: this.environment.name,
children: variables.filter(v => v.name).map(v => {
return {name: v.name, value: '${' + v.name + '}'}
}),
}
];
}
let i = this.scenario.requests.indexOf(this.request);
this.preRequests = this.scenario.requests.slice(0, i);
this.preRequests.forEach(r => {
let js = r.extract.json.map(v => {
return {name: v.variable, value: v.value}
});
let xs = r.extract.xpath.map(v => {
return {name: v.variable, value: v.value}
});
let rx = r.extract.regex.map(v => {
return {name: v.variable, value: v.value}
});
let vs = [...js, ...xs, ...rx];
if (vs.length > 0) {
this.preRequestParams.push({name: r.name, children: vs});
}
});
}
},
filterNode(value, data) {
if (!value) {
return true;
}
return data.name.indexOf(value) !== -1;
},
selectVariable(node) {
this.itemValue = node.value;
},
selectTab(tab) {
this.currentTab = +tab.index;
this.itemValue = null;
this.itemValuePreview = null;
},
showPreview() {
//
if (!this.itemValue) {
return;
}
let index = this.itemValue.indexOf("|");
if (index > -1) {
this.itemValue = this.itemValue.substring(0, index).trim();
}
this.mockVariableFuncs.forEach(f => {
if (!f.name) {
return;
}
this.itemValue += "|" + f.name;
if (f.params) {
this.itemValue += ":" + f.params.map(p => p.value).join(",");
}
});
this.itemValuePreview = calculate(this.itemValue);
},
methodChange(itemFunc, func) {
let index = this.mockVariableFuncs.indexOf(itemFunc);
this.mockVariableFuncs = this.mockVariableFuncs.slice(0, index);
// deep copy
this.mockVariableFuncs.push(JSON.parse(JSON.stringify(func)));
this.showPreview();
},
radioClick(itemFunc, func) {
if (itemFunc.name === func.name) {
let index = this.mockVariableFuncs.indexOf(itemFunc);
this.mockVariableFuncs = this.mockVariableFuncs.slice(0, index);
this.mockVariableFuncs.push({name: '', params: []});
let valindex = this.itemValue.indexOf('|' + func.name);
this.itemValue = this.itemValue.slice(0, valindex);
} else {
this.methodChange(itemFunc, func);
}
},
addFunc() {
if (this.itemValue.indexOf('@') == -1) {
this.itemValue = '@' + this.itemValue;
} else {
this.itemValue = this.itemValue;
}
if (this.mockVariableFuncs.length > 4) {
this.$info(this.$t('api_test.request.parameters_advance_add_func_limit'));
return;
}
if (this.mockVariableFuncs.length > 0) {
let func = this.mockVariableFuncs[this.mockVariableFuncs.length - 1];
if (!func.name) {
this.$warning(this.$t('api_test.request.parameters_advance_add_func_error'));
return;
}
if (func.params) {
for (let j = 0; j < func.params.length; j++) {
if (!func.params[j].value) {
this.$warning(this.$t('api_test.request.parameters_advance_add_param_error'));
return;
}
}
}
}
this.mockVariableFuncs.push({name: '', params: []});
},
saveAdvanced() {
if (this.itemValue && this.itemValue.indexOf('@') == -1) {
this.currentItem.value = '@' + this.itemValue;
} else {
this.currentItem.value = this.itemValue;
}
this.itemValueVisible = false;
this.mockVariableFuncs = [];
this.$emit('advancedRefresh', this.currentItem.value);
}
}
}
</script>
<style scoped>
.col-height {
height: 40vh;
overflow: auto;
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<ms-edit-dialog
:visible.sync="visible"
width="700px"
:title="$t('schema.adv_setting')"
:with-footer="false"
append-to-body
:close-on-click-modal="true">
<ms-api-variable-advance v-if="editData.type=='CONSTANT'" ref="variableAdvance" :current-item="editData"
@advancedRefresh="reload"/>
<ms-edit-counter v-if="editData.type=='COUNTER'" ref="counter" :editData.sync="editData"/>
<ms-edit-random v-if="editData.type=='RANDOM'" ref="random" :editData.sync="editData"/>
<ms-edit-csv v-if="editData.type=='CSV'" ref="csv" :editData.sync="editData"/>
<template v-slot:footer>
<ms-dialog-footer
@cancel="handleCancel"
@confirm="handleConfirm"
/>
</template>
</ms-edit-dialog>
</template>
<script>
import MsEditDialog from "@/business/components/common/components/MsEditDialog";
import MsEditConstant from "@/business/components/api/automation/scenario/variable/EditConstant";
import MsEditCounter from "@/business/components/api/automation/scenario/variable/EditCounter";
import MsEditRandom from "@/business/components/api/automation/scenario/variable/EditRandom";
import MsEditListValue from "@/business/components/api/automation/scenario/variable/EditListValue";
import MsEditCsv from "@/business/components/api/automation/scenario/variable/EditCsv";
import MsApiVariableAdvance from "@/business/components/api/test/components/environment/ApiVariableAdvance";
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
export default {
name: "ApiVariableSetting",
components: {
MsEditDialog,
MsEditConstant,
MsEditCounter,
MsEditRandom,
MsEditListValue,
MsEditCsv,
MsApiVariableAdvance,
MsDialogFooter
},
data() {
return {
visible: false,
data: {},
editData: {}
}
},
methods: {
open(item) {
this.visible = true;
this.editData = item;
},
handleConfirm() {
if (this.editData.type === 'CONSTANT') {
this.$refs.variableAdvance.saveAdvanced();
}
if (this.editData.type === 'CSV' && this.$refs.csv) {
if (this.editData.files.length === 0) {
this.$warning(this.$t('api_test.automation.csv_warning'));
return;
}
}
this.visible = false;
},
handleCancel() {
this.visible = false;
},
reload() {
this.isActive = false;
this.$nextTick(() => {
this.isActive = true;
});
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,273 @@
<template>
<span>
<el-row>
<el-col :span="18">
<el-upload
action="#"
class="api-body-upload"
list-type="picture-card"
:file-list="parameter.files"
:beforeUpload="uploadValidate"
:on-exceed="exceed"
:limit="1"
ref="upload">
<div class="upload-default" @click.stop>
<el-popover
placement="right"
trigger="hover">
<div>
<el-upload
action="#"
class="ms-body-upload"
:http-request="upload"
:limit="1"
:on-exceed="exceed"
:beforeUpload="uploadValidate"
ref="uploadLocal">
<el-button type="text"> {{ $t('permission.project_file.local_upload') }}</el-button>
<span slot="file"/>
</el-upload>
</div>
<el-button type="text" @click="associationFile">{{
$t('permission.project_file.associated_files')
}}</el-button>
<i class="el-icon-plus" slot="reference"/>
</el-popover>
</div>
<div class="upload-item" slot="file" slot-scope="{file}">
<span>{{ file.file && file.file.name ? file.file.name : file.name }}</span>
<span class="el-upload-list__item-actions" v-if="file.storage === 'FILE_REF'">
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleUnlock(file)">
<i class="el-icon-unlock"/>
</span>
</span>
<span class="el-upload-list__item-actions" v-else>
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleUpload(file)">
<i class="el-icon-upload" style="font-size: 23px"/>
</span>
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
<i class="el-icon-delete"/>
</span>
</span>
</div>
</el-upload>
</el-col>
</el-row>
<ms-file-batch-move ref="module" @setModuleId="setModuleId"/>
<ms-file-metadata-list ref="metadataList" @checkRows="checkRows"/>
</span>
</template>
<script>
import {downloadFile} from "@/common/js/utils";
import MsFileBatchMove from "@/business/components/project/menu/file/module/FileBatchMove";
import MsFileMetadataList from "@/business/components/project/menu/file/quote/QuoteFileList";
import {getCurrentProjectID, getUUID} from "@/common/js/utils";
export default {
name: "MsCsvFileUpload",
data() {
return {
disabled: false,
};
},
components: {
MsFileBatchMove,
MsFileMetadataList
},
props: {
parameter: Object,
default() {
return {}
}
},
methods: {
setModuleId(moduleId) {
let files = [];
if (this.file && this.file.file) {
files.push(this.file.file);
}
let request = {
id: getUUID(),
resourceId: this.id,
moduleId: moduleId,
projectId: getCurrentProjectID(),
fileName: this.file.name
};
this.$fileUpload("/file/metadata/api/upload", null, files, request, (response) => {
this.$success(this.$t("organization.integration.successful_operation"));
});
},
handleUpload(file) {
this.$refs.module.init();
this.file = file;
},
associationFile() {
this.$refs.metadataList.open();
},
checkRows(rows) {
if (rows && rows.size !== 1 || this.parameter.files.length > 0) {
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
return;
}
rows.forEach(item => {
if (!item.type || item.type.toLowerCase() !== "csv") {
this.$warning(this.$t('variables.cvs_info'));
return;
}
let file = {
name: item.name,
id: getUUID(),
fileId: item.id,
storage: "FILE_REF",
projectId: item.projectId,
fileType: item.type
};
this.parameter.files.push(file);
})
},
handleUnlock(file) {
for (let i = 0; i < this.parameter.files.length; i++) {
let fileName = file.file ? file.file.name : file.name;
let paramFileName = this.parameter.files[i].file ?
this.parameter.files[i].file.name : this.parameter.files[i].name;
if (fileName === paramFileName) {
this.parameter.files.splice(i, 1);
this.$refs.upload.handleRemove(file);
break;
}
}
},
download() {
//
if (this.parameter.files && this.parameter.files.length > 0 && this.parameter.files[0].file) {
downloadFile(this.parameter.files[0].file.name, this.parameter.files[0].file);
}
//
if (this.parameter.files && this.parameter.files.length > 0 && !this.parameter.files[0].file) {
let file = this.parameter.files[0];
let conf = {
url: "/api/automation/file/download",
method: 'post',
data: file,
responseType: 'blob',
};
if (file.storage === "FILE_REF") {
conf = {
url: "/file/metadata/download/" + file.fileId,
method: 'get',
responseType: 'blob',
};
}
this.result = this.$request(conf).then(response => {
const content = response.data;
if (content && this.parameter.files[0]) {
downloadFile(this.parameter.files[0].name, content);
}
});
}
},
handleRemove(file) {
let fileName = file.file.name ? file.file.name : file.name
this.$alert(this.$t('api_test.environment.csv_delete') + ':【 ' + fileName + " 】?", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
this.$refs.upload.handleRemove(file);
for (let i = 0; i < this.parameter.files.length; i++) {
let paramFileName = this.parameter.files[i].name ?
this.parameter.files[i].name : this.parameter.files[i].file.name;
if (fileName === paramFileName) {
this.parameter.files.splice(i, 1);
this.$refs.upload.handleRemove(file);
break;
}
}
}
}
});
},
exceed() {
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
},
upload(file) {
this.parameter.files = [];
this.parameter.files.push(file);
},
uploadValidate(file) {
if (this.parameter.files.length > 0) {
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
return false;
}
if (file.size / 1024 / 1024 > 500) {
this.$warning(this.$t('api_test.request.body_upload_limit_size'));
return false;
}
if (!file.name.endsWith(".csv")) {
this.$warning(this.$t('variables.cvs_info'));
return false;
}
return true;
},
},
created() {
if (!this.parameter.files) {
this.parameter.files = [];
}
}
}
</script>
<style scoped>
.el-upload {
background-color: black;
}
.api-body-upload >>> .el-upload {
height: 30px;
width: 32px;
}
.upload-default {
min-height: 30px;
width: 32px;
line-height: 32px;
}
.el-icon-plus {
font-size: 16px;
}
.api-body-upload >>> .el-upload-list__item {
height: 30px;
width: auto;
padding: 2px 5px;
margin-bottom: 0px;
}
.api-body-upload >>> .el-upload-list--picture-card {
}
.api-body-upload {
min-height: 30px;
border: 1px solid #EBEEF5;
padding: 2px;
border-radius: 4px;
}
.ms-body-upload {
min-height: 0px;
height: 30px;
border: 0px;
padding: 0px;
border-radius: 0px;
}
.upload-item {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
</style>

View File

@ -30,7 +30,7 @@
<script>
import {CommonConfig} from "../../model/EnvironmentModel";
import MsApiScenarioVariables from "../ApiScenarioVariables";
import MsApiScenarioVariables from "@/business/components/api/test/components/environment/ApiScenarioVariables";
import MsApiHostTable from "../ApiHostTable";
export default {

View File

@ -2,19 +2,27 @@
<el-main v-loading="result.loading" class="environment-edit" style="margin-left: 0px">
<el-form :model="environment" :rules="rules" ref="environment" label-width="80px">
<el-row>
<el-col :span="20">
<el-form-item prop="name" :label="$t('api_test.environment.name')">
<el-input v-model="environment.name" :disabled="isReadOnly" :placeholder="this.$t('commons.input_name')"
clearable/>
<el-col :span="10" v-if="!isProject">
<el-form-item class="project-item" prop="currentProjectId" :label="$t('project.select')">
<el-select @change="handleProjectChange" v-model="environment.currentProjectId" filterable clearable
size="small">
<el-option v-for="item in projectList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4" v-if="!hideButton">
<el-col :span="10">
<el-form-item prop="name" :label="$t('api_test.environment.name')">
<el-input v-model="environment.name" :disabled="isReadOnly" :placeholder="this.$t('commons.input_name')"
clearable size="small"/>
</el-form-item>
</el-col>
<el-col :span="4" v-if="!hideButton" :offset="isProject ? 10 : 0">
<div style="float: right;width: fit-content;">
<div style="float: left; margin-right: 8px;">
<slot name="other"></slot>
</div>
<div class="ms_btn">
<el-button type="primary" @click="confirm" @keydown.enter.native.prevent>
<el-button type="primary" @click="confirm" @keydown.enter.native.prevent size="small">
{{ $t('commons.confirm') }}
</el-button>
</div>
@ -30,7 +38,7 @@
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.http_config')" name="http">
<ms-environment-http-config :project-id="projectId" :http-config="environment.config.httpConfig"
<ms-environment-http-config :project-id="environment.projectId" :http-config="environment.config.httpConfig"
ref="httpConfig" :is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.database_config')" name="sql">
@ -40,7 +48,7 @@
<ms-tcp-config :config="environment.config.tcpConfig" :is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('commons.ssl.config')" name="ssl">
<ms-environment-s-s-l-config :project-id="projectId" :ssl-config="environment.config.sslConfig"
<ms-environment-s-s-l-config :project-id="environment.projectId" :ssl-config="environment.config.sslConfig"
:is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.all_pre_script')" name="prescript">
@ -97,7 +105,8 @@
<el-form-item style="margin-bottom: 0px;"
:label="$t('error_report_library.use_error_report')"
prop="status">
<el-switch v-model="environment.config.useErrorCode" style="margin-right: 10px" :disabled="isReadOnly"/>
<el-switch v-model="environment.config.useErrorCode" style="margin-right: 10px"
:disabled="isReadOnly"/>
</el-form-item>
</el-col>
</el-row>
@ -128,11 +137,7 @@
:is-show-json-path-suggest="false"/>
</el-tab-pane>
</el-tabs>
<!-- <div class="environment-footer">-->
<!-- <ms-dialog-footer-->
<!-- @cancel="cancel"-->
<!-- @confirm="save()"/>-->
<!-- </div>-->
</el-form>
<ms-change-history ref="changeHistory"/>
</el-main>
@ -158,6 +163,7 @@ import EnvironmentGlobalScript from "@/business/components/api/test/components/e
import GlobalAssertions from "@/business/components/api/definition/components/assertion/GlobalAssertions";
import MsChangeHistory from "../../../../history/ChangeHistory";
import MsDialogHeader from "../../../../common/components/MsDialogHeader";
import {getUploadConfig, request} from "@/common/js/ajax";
export default {
name: "EnvironmentEdit",
@ -185,6 +191,13 @@ export default {
type: Boolean,
default: false
},
projectList: {
type: Array,
default() {
return [];
}
},
isProject: Boolean
},
data() {
return {
@ -196,6 +209,9 @@ export default {
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
{max: 64, message: this.$t('commons.input_limit', [1, 64]), trigger: 'blur'}
],
currentProjectId: [
{required: true, message: "", trigger: 'blur'},
],
},
headerSuggestions: REQUEST_HEADERS,
activeName: 'common'
@ -295,6 +311,15 @@ export default {
this.isRefresh = true;
});
this.envEnable = o.enable;
},
//projectId
'environment.currentProjectId'() {
// el-select''''projectId使
if (!this.environment.currentProjectId) {
this.environment.projectId = null;
} else {
this.environment.projectId = this.environment.currentProjectId;
}
}
},
computed: {
@ -361,6 +386,29 @@ export default {
}
return uploadFiles;
},
getVariablesFiles(obj) {
let variablesFiles = [];
obj.variablesFilesIds = [];
// csv
if (obj.config.commonConfig.variables) {
obj.config.commonConfig.variables.forEach(param => {
if (param.type === 'CSV' && param.files) {
param.files.forEach(item => {
if (item.file && item.file.name) {
if (!item.id) {
let fileId = getUUID().substring(0, 12);
item.name = item.file.name;
item.id = fileId;
}
obj.variablesFilesIds.push(item.id);
variablesFiles.push(item.file);
}
})
}
});
}
return variablesFiles;
},
check(items) {
let repeatKey = "";
items.forEach((item, index) => {
@ -373,7 +421,7 @@ export default {
return repeatKey;
},
_save(environment) {
if (!this.projectId) {
if (!environment.projectId) {
this.$warning(this.$t('api_test.select_project'));
return;
}
@ -401,25 +449,47 @@ export default {
}
})
}
environment.config.commonConfig.variables.forEach(variable => {
if (variable.type === 'CSV' && variable.files.length === 0) {
message = this.$t('api_test.automation.csv_warning');
return;
}
})
if (message) {
this.$warning(message);
return;
}
let bodyFiles = this.geFiles(environment);
let variablesFiles = this.getVariablesFiles(environment);
let formData = new FormData();
if (bodyFiles) {
bodyFiles.forEach(f => {
formData.append("files", f);
})
}
if (variablesFiles) {
variablesFiles.forEach(f => {
formData.append("variablesFiles", f);
})
}
let param = this.buildParam(environment);
let url = '/api/environment/add';
if (param.id) {
url = '/api/environment/update';
}
this.$fileUpload(url, null, bodyFiles, param, response => {
//this.result = this.$post(url, param, response => {
if (!param.id) {
environment.id = response.data;
}
formData.append('request', new Blob([JSON.stringify(param)], {type: "application/json"}));
let axiosRequestConfig = getUploadConfig(url, formData);
request(axiosRequestConfig, (response) => {
if (response.success) {
this.$success(this.$t('commons.save_success'));
this.$emit('refreshAfterSave'); //EnvironmentList.vue使
this.cancel();
this.cancel()
}
}, error => {
this.$emit('errorRefresh', error);
});
},
buildParam: function (environment) {
let param = {};
@ -447,6 +517,9 @@ export default {
clearValidate() {
this.$refs["environment"].clearValidate();
},
handleProjectChange() { //,
this.environment.config.httpConfig.conditions = [];
},
},
}
</script>
@ -468,10 +541,11 @@ span:not(:first-child) {
margin-top: 15px;
}
.errorReportConfigSwitch /deep/ .el-switch__label{
.errorReportConfigSwitch /deep/ .el-switch__label {
color: #D8DAE2;
}
.errorReportConfigSwitch /deep/ .is-active{
.errorReportConfigSwitch /deep/ .is-active {
color: var(--count_number);
}
</style>

View File

@ -6,7 +6,8 @@
<el-row type="flex" justify="space-between">
<el-col :span="14">
<span class="ms-env-span" style="line-height: 30px;">{{ $t('api_test.environment.socket') }}</span>
<el-input v-model="condition.socket" style="width: 85%" :placeholder="$t('api_test.request.url_description')" clearable size="small">
<el-input v-model="condition.socket" style="width: 85%"
:placeholder="$t('api_test.request.url_description')" clearable size="small">
<template slot="prepend">
<el-select v-model="condition.protocol" class="request-protocol-select" size="small">
<el-option label="http://" value="http"/>
@ -17,7 +18,8 @@
</el-col>
<el-col :span="10">
<span style="margin-right: 12px; line-height: 30px;">{{ $t('commons.description') }}</span>
<el-input v-model="condition.description" maxlength="200" :show-word-limit="true" size="small" style="width: 70%;"/>
<el-input v-model="condition.description" maxlength="200" :show-word-limit="true" size="small"
style="width: 70%;"/>
</el-col>
</el-row>
</el-form-item>
@ -29,7 +31,8 @@
<el-radio label="PATH">{{ $t('api_test.definition.api_path') }}</el-radio>
</el-radio-group>
<div v-if="condition.type === 'MODULE'" style="margin-top: 6px">
<ms-select-tree size="small" :data="moduleOptions" :default-key="condition.ids" @getValue="setModule" :obj="moduleObj" clearable :checkStrictly="true" multiple v-if="!loading"/>
<ms-select-tree size="small" :data="moduleOptions" :default-key="condition.ids" @getValue="setModule"
:obj="moduleObj" clearable :checkStrictly="true" multiple v-if="!loading"/>
</div>
<div v-if="condition.type === 'PATH'" style="margin-top: 6px">
<el-input v-model="pathDetails.name" :placeholder="$t('api_test.value')" clearable size="small">
@ -70,7 +73,8 @@
{{ getUrl(row) }}
</template>
</el-table-column>
<el-table-column prop="type" :label="$t('api_test.environment.condition_enable')" show-overflow-tooltip min-width="100px">
<el-table-column prop="type" :label="$t('api_test.environment.condition_enable')" show-overflow-tooltip
min-width="100px">
<template v-slot:default="{row}">
{{ getName(row) }}
</template>
@ -153,7 +157,15 @@ export default {
},
loading: false,
pathDetails: new KeyValue({name: "", value: "contains"}),
condition: {type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "http", socket: "", domain: "", port: 0, headers: [new KeyValue()]},
condition: {
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
port: 0,
headers: [new KeyValue()]
},
beforeCondition: {}
};
},
@ -174,7 +186,16 @@ export default {
this.condition.description = this.httpConfig.description;
this.add();
}
this.condition = {id: undefined, type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "http", socket: "", domain: "", port: 0, headers: [new KeyValue()]};
this.condition = {
id: undefined,
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
port: 0,
headers: [new KeyValue()]
};
},
},
methods: {
@ -216,7 +237,15 @@ export default {
}
},
selectRow(row) {
this.condition = {type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "http", socket: "", domain: "", port: 0, headers: [new KeyValue()]};
this.condition = {
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
port: 0,
headers: [new KeyValue()]
};
if (row) {
this.httpConfig.socket = row.socket;
this.httpConfig.protocol = row.protocol;
@ -309,7 +338,14 @@ export default {
this.$refs.envTable.setCurrentRow(0);
},
clear() {
this.condition = {type: "NONE", details: [new KeyValue({name: "", value: "contains"})], protocol: "http", socket: "", domain: "", headers: [new KeyValue()]};
this.condition = {
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
headers: [new KeyValue()]
};
this.$refs.envTable.setCurrentRow(0);
},
reload() {
@ -336,8 +372,15 @@ export default {
}
this.validateSocket();
let obj = {
id: getUUID(), type: this.condition.type, socket: this.condition.socket, protocol: this.condition.protocol, headers: this.condition.headers,
domain: this.condition.domain, port: this.condition.port, time: new Date().getTime(), description: this.condition.description
id: getUUID(),
type: this.condition.type,
socket: this.condition.socket,
protocol: this.condition.protocol,
headers: this.condition.headers,
domain: this.condition.domain,
port: this.condition.port,
time: new Date().getTime(),
description: this.condition.description
};
if (this.condition.type === "PATH") {
obj.details = [JSON.parse(JSON.stringify(this.pathDetails))];
@ -358,7 +401,16 @@ export default {
return;
}
const index = this.httpConfig.conditions.findIndex((d) => d.id === row.id);
let obj = {id: getUUID(), type: row.type, socket: row.socket, details: row.details, protocol: row.protocol, headers: JSON.parse(JSON.stringify(row.headers)), domain: row.domain, time: new Date().getTime()};
let obj = {
id: getUUID(),
type: row.type,
socket: row.socket,
details: row.details,
protocol: row.protocol,
headers: JSON.parse(JSON.stringify(row.headers)),
domain: row.domain,
time: new Date().getTime()
};
if (index != -1) {
this.httpConfig.conditions.splice(index, 0, obj);
} else {
@ -402,7 +454,7 @@ export default {
let line = item.split(/|:/);
let values = item.split(line[0] + ":");
let required = false;
keyValues.unshift(new KeyValue({
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: values[1],
@ -429,7 +481,7 @@ export default {
}
}
if (isAdd) {
this.condition.headers.unshift(keyValue);
this.condition.headers.splice(this.condition.headers.indexOf(h => !h.name), 0, keyValue);
}
})
}

View File

@ -0,0 +1,162 @@
<template>
<el-dialog :visible="dialogVisible" :title="dialogTitle"
@close="close" :close-on-click-modal="false" append-to-body
width="35%">
<el-form>
<el-form :rules="rules" label-width="80px" v-model="modeId">
<el-form-item prop="modeId" :label="$t('commons.import_mode')">
<el-select size="small" v-model="modeId">
<el-option v-for="item in modeOptions" :key="item.id" :label="item.name" :value="item.id"/>
</el-select>
</el-form-item>
<el-form-item>
<el-upload
class="api-upload" drag action="alert"
:on-change="handleFileChange"
:limit="1" :file-list="uploadFiles"
:on-remove="handleRemove"
:on-exceed="handleExceed"
:auto-upload="false" accept=".json">
<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') }}
{{ '' + $t('api_test.api_import.ms_env_import_file_limit') }}
</div>
</el-upload>
</el-form-item>
</el-form>
</el-form>
<template v-slot:footer>
<el-button type="primary" @click="save">
{{ $t('commons.confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: "VariableImport",
props: {
projectList: {
type: Array,
default() {
return [];
}
},
toImportProjectId: {
type: String,
default() {
return "";
}
}
},
data() {
return {
currentProjectId: '', //id
uploadFiles: [],
dialogTitle: this.$t('commons.import_variable'),
dialogVisible: false,
modeOptions: [
{
id: 'fullCoverage',
name: this.$t('commons.cover')
},
{
id: 'incrementalMerge',
name: this.$t('commons.not_cover')
}
],
modeId: 'fullCoverage',
rules: {
modeId: [
{required: true, message: "", trigger: 'blur'},
],
},
}
},
watch: {
//
dialogVisible(val, oldVal) {
if (oldVal === false) {
this.currentProjectId = '';
this.uploadFiles = [];
}
}
},
methods: {
handleFileChange(file, uploadFiles) {
this.uploadFiles = uploadFiles;
},
save() {
if (this.uploadFiles.length > 0) {
for (let i = 0; i < this.uploadFiles.length; i++) {
this.uploadValidate(this.uploadFiles[i]);
let file = this.uploadFiles[i];
if (!file) {
continue;
}
let reader = new FileReader();
reader.readAsText(file.raw)
reader.onload = (e) => {
let fileString = e.target.result;
let messages = '';
try {
JSON.parse(fileString).map(env => {
if (!env.name) {
messages = this.$t('api_test.automation.variable_warning')
}
})
if (messages !== '') {
this.$warning(messages);
return;
}
this.$emit("mergeData", fileString, this.modeId);
this.dialogVisible = false;
this.$success(this.$t('commons.save_success'));
} catch (exception) {
this.$warning(this.$t('api_test.api_import.ms_env_import_file_limit'));
}
}
}
} else {
this.$warning(this.$t('test_track.case.import.import_file_tips'));
}
},
handleExceed() {
this.$warning(this.$t('api_test.api_import.file_exceed_limit'));
},
handleRemove() {
},
uploadValidate(file) { //.json20M
const extension = file.name.substring(file.name.lastIndexOf('.') + 1);
if (!(extension === 'json')) {
this.$warning(this.$t('api_test.api_import.ms_env_import_file_limit'));
}
if (file.size / 1024 / 1024 > 20) {
this.$warning(this.$t('api_test.api_import.file_size_limit'));
}
},
open() {
this.dialogVisible = true;
},
close() {
this.dialogVisible = false;
}
},
}
</script>
<style scoped>
.project-item {
padding-left: 20px;
padding-right: 20px;
}
</style>

View File

@ -2,7 +2,12 @@
<div class="msDialogHeader">
<span style="float: left;font-size: 18px;color: #303133;">{{ title }}</span>
<div style="float: right; margin-right: 30px">
<el-tooltip effect="dark" :content="$t('commons.full_screen_editing')"
placement="top-start">
<font-awesome-icon class="alt-ico" :icon="['fa', 'expand-alt']" size="lg" @click="fullScreen"/>
</el-tooltip>
</div>
<div v-if="!hideButton" style="float: right;width: fit-content;">
<div style="float: left; margin-right: 8px;">
<slot name="other"></slot>
@ -39,6 +44,9 @@ export default {
},
confirm() {
this.$emit("confirm");
},
fullScreen() {
this.$emit("fullScreen");
}
}
}
@ -53,4 +61,9 @@ export default {
.msDialogHeader {
margin-bottom: 5px;
}
.alt-ico {
font-size: 13px;
color: #8c939d;
}
</style>

View File

@ -32,7 +32,9 @@
<el-table-column :label="$t('api_test.environment.socket')" show-overflow-tooltip>
<template v-slot="scope">
<span v-if="parseDomainName(scope.row)!='SHOW_INFO'">{{ parseDomainName(scope.row) }}</span>
<el-button size="mini" icon="el-icon-s-data" @click="showInfo(scope.row)" v-else>{{ $t('workspace.env_group.view_details') }}</el-button>
<el-button size="mini" icon="el-icon-s-data" @click="showInfo(scope.row)" v-else>
{{ $t('workspace.env_group.view_details') }}
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('commons.operating')">
@ -57,18 +59,20 @@
</el-card>
<!-- 创建编辑复制环境时的对话框 -->
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="66%" top="50px">
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="66%" top="50px"
:fullscreen="isFullScreen">
<template #title>
<ms-dialog-header :title="dialogTitle" :hide-button="true"
@cancel="dialogVisible = false"
@confirm="save"/>
@confirm="save" @fullScreen="fullScreen"/>
</template>
<environment-edit :if-create="ifCreate" :environment="currentEnvironment" ref="environmentEdit" @close="close"
@confirm="save"
:project-id="currentProjectId" @refreshAfterSave="refresh">
@confirm="save" :is-project="true"
:project-list="projectList" @refreshAfterSave="refresh">
</environment-edit>
</el-dialog>
<environment-import :project-list="projectList" :to-import-project-id="currentProjectId" @refresh="refresh" ref="envImport"></environment-import>
<environment-import :project-list="projectList" :to-import-project-id="currentProjectId" @refresh="refresh"
ref="envImport"></environment-import>
<el-dialog title="域名列表" :visible.sync="domainVisible">
<el-table :data="conditions">
@ -94,12 +98,14 @@
{{ row.conditionType ? "-" : getDetails(row) }}
</template>
</el-table-column>
<el-table-column prop="description" show-overflow-tooltip min-width="120px" :label="$t('commons.description')">
<el-table-column prop="description" show-overflow-tooltip min-width="120px"
:label="$t('commons.description')">
<template v-slot:default="{row}">
<span>{{ row.description ? row.description : "-" }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" show-overflow-tooltip min-width="120px" :label="$t('commons.create_time')">
<el-table-column prop="createTime" show-overflow-tooltip min-width="120px"
:label="$t('commons.create_time')">
<template v-slot:default="{row}">
<span v-if="!row.conditionType">{{ row.time | timestampFormatDate }}</span>
<span v-else>-</span>
@ -170,6 +176,7 @@ export default {
projectFilters: [],
screenHeight: 'calc(100vh - 155px)',
ifCreate: false, //
isFullScreen: false //
}
},
created() {
@ -180,6 +187,9 @@ export default {
},
methods: {
fullScreen() {
this.isFullScreen = !this.isFullScreen;
},
showInfo(row) {
const config = JSON.parse(row.config);
this.conditions = config.httpConfig.conditions;
@ -193,7 +203,7 @@ export default {
}
this.domainVisible = true;
},
save(){
save() {
this.$refs.environmentEdit.save();
},
getName(row) {
@ -387,8 +397,7 @@ export default {
} else { //config,protocoldomain
return environment.protocol + '://' + environment.domain;
}
}
},
},

View File

@ -38,7 +38,9 @@
<el-table-column :label="$t('api_test.environment.socket')" show-overflow-tooltip>
<template v-slot="scope">
<span v-if="parseDomainName(scope.row)!='SHOW_INFO'">{{ parseDomainName(scope.row) }}</span>
<el-button size="mini" icon="el-icon-s-data" @click="showInfo(scope.row)" v-else>{{ $t('workspace.env_group.view_details') }}</el-button>
<el-button size="mini" icon="el-icon-s-data" @click="showInfo(scope.row)" v-else>
{{ $t('workspace.env_group.view_details') }}
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('commons.operating')">
@ -62,26 +64,16 @@
</el-card>
<!-- 创建编辑复制环境时的对话框 -->
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" top="50px" width="66%">
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" top="50px" width="66%"
:fullscreen="isFullScreen">
<template #title>
<ms-dialog-header :title="dialogTitle"
@cancel="dialogVisible = false"
@confirm="save"/>
@cancel="dialogVisible = false" :hide-button="true"
@confirm="save" @fullScreen="fullScreen"/>
</template>
<el-row>
<el-col :span="20">
<el-form label-width="80px" :rules="rules" style="display: flow-root">
<el-form-item class="project-item" prop="currentProjectId" :label="$t('project.select')">
<el-select @change="handleProjectChange" v-model="currentProjectId" filterable clearable>
<el-option v-for="item in projectList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
</el-col>
</el-row>
<environment-edit :if-create="ifCreate" :environment="currentEnvironment" ref="environmentEdit" @close="close"
:hide-button="true"
:project-id="currentProjectId" @refreshAfterSave="refresh">
:project-list="projectList" @confirm="save" :is-project="false"
@refreshAfterSave="refresh">
</environment-edit>
</el-dialog>
<environment-import :project-list="projectList" @refresh="refresh" ref="envImport"></environment-import>
@ -203,6 +195,7 @@ export default {
},
],
ifCreate: false, //
isFullScreen: false
};
},
created() {
@ -227,6 +220,9 @@ export default {
},
methods: {
fullScreen() {
this.isFullScreen = !this.isFullScreen;
},
showInfo(row) {
const config = JSON.parse(row.config);
this.conditions = config.httpConfig.conditions;
@ -316,6 +312,7 @@ export default {
editEnv(environment) {
this.dialogTitle = this.$t('api_test.environment.config_environment');
this.currentProjectId = environment.projectId;
environment.currentProjectId = environment.projectId;
const temEnv = {};
Object.assign(temEnv, environment);
parseEnvironment(temEnv); //parseEnvironment
@ -323,11 +320,12 @@ export default {
this.dialogVisible = true;
this.ifCreate = false;
},
save(){
save() {
this.$refs.environmentEdit.save();
},
copyEnv(environment) {
this.currentProjectId = environment.projectId; //
environment.currentProjectId = environment.projectId;
this.dialogTitle = this.$t('api_test.environment.copy_environment');
const temEnv = {};
Object.assign(temEnv, environment);
@ -458,7 +456,7 @@ export default {
this.selectRow.forEach(row => {
map.set(row.projectId, row.id);
})
this.$post("/environment/group/batch/add", {map: strMapToObj(map), groupIds:value}, () => {
this.$post("/environment/group/batch/add", {map: strMapToObj(map), groupIds: value}, () => {
this.$success(this.$t('commons.save_success'));
this.getEnvironments();
})

View File

@ -246,6 +246,7 @@ export let CUSTOM_TABLE_HEADER = {
{id: 'name', key: '2', label: 'api_test.variable_name'},
{id: 'type', key: '3', label: 'test_track.case.type'},
{id: 'value', key: '4', label: 'api_test.value'},
{id: 'description', key: '5', label: 'commons.description'}
],
//缺陷列表

View File

@ -211,6 +211,8 @@ export default {
modifier: 'Modifier',
validate: "Validate",
batch_add: "Batch add",
import_variable: "Import variable",
export_variable: "Export variable",
batch_restore: "Batch restore",
batch_gc: "Batch gc",
check_project_tip: "Create or select the project first",
@ -1193,7 +1195,8 @@ export default {
copy: "Copy Test",
please_select_case: "Please select case",
fail_to_stop: "Fail to stop",
batch_add_parameter: "Format: parameter name: parameter value <br/> likeAccept-Encoding:utf-8",
batch_add_parameter: "Format: parameter name: parameter value <br/> likeAccept-Encoding:utf-8 <br/> Note: The parameter names in batch addition are repeated, and the last data is the latest data by default",
params_format_warning: "Incorrect data format at line {0}",
create_performance_test_tips: 'This operation cannot be completed without permission to create performance tests',
jar_config: {
title: "Upload jar package",
@ -1510,9 +1513,15 @@ export default {
environment_json: "Environment Config",
environment_group_id: "Environment Group ID",
select_environment: "Please select environment",
select_variable: "Please select variable",
please_save_test: "Please Save Test First",
common_config: "Common Config",
http_config: "HTTP Config",
advanced_setting: "Click Advanced settings to add variable values",
variables_delete_info: "Are you sure you want to delete the selected variable",
csv_delete: "Are you sure you want to delete the CSV file",
delete_info: "Please select a data deletion",
list_info: "List data is separated by ,",
database_config: "Database Config",
tcp_config: "TCP Config",
import: "Import Environment",
@ -2792,6 +2801,7 @@ export default {
delimiter: "Delimiter",
format: "Output format",
quoted_data: "Whether to allow quotes",
csv_download: "CSV file does not support exporting"
},
auth_source: {
delete_prompt: 'This operation will delete the authentication source, do you want to continue? ',

View File

@ -212,6 +212,8 @@ export default {
modifier: '修改人',
validate: "校验",
batch_add: "批量添加",
import_variable: "导入变量",
export_variable: "导出变量",
batch_restore: "批量恢复",
batch_gc: "批量回收",
check_project_tip: "请先创建或选择项目",
@ -1203,7 +1205,8 @@ export default {
copy: "复制测试",
please_select_case: "请选择用例",
fail_to_stop: "失败停止",
batch_add_parameter: "格式:参数名:参数值 <br/> 如Accept-Encoding:utf-8",
batch_add_parameter: "格式:参数名:参数值 <br/> 如Accept-Encoding:utf-8 <br/> 注:批量添加里的参数名重复,默认以最后一条数据为最新数据",
params_format_warning: "第{0}行数据格式有误",
create_performance_test_tips: '没有创建性能测试的权限,无法完成此操作',
jar_config: {
title: "上传jar包",
@ -1519,8 +1522,14 @@ export default {
environment_json: "环境配置",
environment_group_id: "环境组ID",
select_environment: "请选择环境",
select_variable: "请选择变量",
please_save_test: "请先保存测试",
common_config: "通用配置",
list_info: "列表数据用,分隔",
advanced_setting: "点击高级设置,添加变量值",
variables_delete_info: "是否确认删除所选变量",
csv_delete: "是否确认删除CSV文件",
delete_info: "请选择一条数据删除",
http_config: "HTTP配置",
database_config: "数据库配置",
tcp_config: "TCP配置",
@ -2801,6 +2810,7 @@ export default {
delimiter: "分隔符",
format: "输出格式",
quoted_data: "是否允许带引号",
csv_download: "CSV文件暂不支持导出"
},
auth_source: {
delete_prompt: '此操作会删除认证源,是否继续?',

View File

@ -212,6 +212,8 @@ export default {
modifier: '修改人',
validate: "校驗",
batch_add: "批量添加",
import_variable: "導出變量",
export_variable: "导出变量",
batch_restore: "批量恢復",
batch_gc: "批量回收",
check_project_tip: "請先創建或選擇項目",
@ -1200,7 +1202,8 @@ export default {
copy: "復製測試",
please_select_case: "請選擇用例",
fail_to_stop: "失敗停止",
batch_add_parameter: "格式:參數名:參數值 <br/> 如Accept-Encoding:utf-8",
batch_add_parameter: "格式:參數名:參數值 <br/> 如Accept-Encoding:utf-8 <br/> 注:批量添加里的參數名重複,默認以最後一條數據為最新數據",
params_format_warning: "第{0}行數據格式有誤",
create_performance_test_tips: '沒有創建性能測試的權限,無法完成此操作',
jar_config: {
title: "上傳jar包",
@ -1516,9 +1519,15 @@ export default {
environment_json: "環境配置",
environment_group_id: "環境組ID",
select_environment: "請選擇環境",
select_variable: "請選擇变量",
please_save_test: "請先保存測試",
common_config: "通用配置",
http_config: "HTTP配置",
list_info: "清單數據用,分隔",
advanced_setting: "點擊高級設定,添加變數值",
variables_delete_info: "是否確認刪除所選變量",
csv_delete: "是否確認删除CSV檔案",
delete_info: "請選擇一條數據刪除",
database_config: "數據庫配置",
tcp_config: "TCP配置",
import: "導入環境",
@ -2795,6 +2804,7 @@ export default {
delimiter: "分隔符",
format: "輸出格式",
quoted_data: "是否允許帶引號",
csv_download: "CSV文件暫不支持導出"
},
auth_source: {
delete_prompt: '此操作會刪除認證源,是否繼續?',