Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
a84edf8cc9
|
@ -6,6 +6,7 @@ import lombok.Getter;
|
|||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
|
@ -34,4 +35,6 @@ public class RunDefinitionRequest {
|
|||
private Response response;
|
||||
|
||||
private List<String> bodyUploadIds;
|
||||
|
||||
private Map<String, String> environmentMap;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import io.metersphere.api.service.ApiTestEnvironmentService;
|
|||
import io.metersphere.base.domain.ApiScenarioWithBLOBs;
|
||||
import io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs;
|
||||
import io.metersphere.commons.utils.CommonBeanFactory;
|
||||
import io.metersphere.commons.utils.SessionUtils;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
|
@ -26,8 +27,10 @@ import org.apache.jmeter.save.SaveService;
|
|||
import org.apache.jmeter.testelement.TestElement;
|
||||
import org.apache.jorphan.collections.HashTree;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Data
|
||||
|
@ -52,6 +55,9 @@ public class MsScenario extends MsTestElement {
|
|||
@JSONField(ordinal = 26)
|
||||
private List<KeyValue> headers;
|
||||
|
||||
@JSONField(ordinal = 27)
|
||||
private Map<String, String> environmentMap;
|
||||
|
||||
private static final String BODY_FILE_DIR = "/opt/metersphere/data/body";
|
||||
|
||||
public MsScenario() {
|
||||
|
@ -101,13 +107,25 @@ public class MsScenario extends MsTestElement {
|
|||
}
|
||||
// 设置共享cookie
|
||||
config.setEnableCookieShare(enableCookieShare);
|
||||
if (StringUtils.isNotEmpty(environmentId)) {
|
||||
ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class);
|
||||
ApiTestEnvironmentWithBLOBs environment = environmentService.get(environmentId);
|
||||
if (environment != null && environment.getConfig() != null) {
|
||||
config.setConfig(JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class));
|
||||
Map<String,EnvironmentConfig> envConfig = new HashMap<>(16);
|
||||
// 兼容历史数据
|
||||
if (environmentMap == null || environmentMap.isEmpty()) {
|
||||
environmentMap = new HashMap<>(16);
|
||||
if (StringUtils.isNotBlank(environmentId)) {
|
||||
environmentMap.put(SessionUtils.getCurrentProjectId(), environmentId);
|
||||
}
|
||||
}
|
||||
if (environmentMap != null && !environmentMap.isEmpty()) {
|
||||
environmentMap.keySet().forEach(projectId -> {
|
||||
ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class);
|
||||
ApiTestEnvironmentWithBLOBs environment = environmentService.get(environmentMap.get(projectId));
|
||||
if (environment != null && environment.getConfig() != null) {
|
||||
EnvironmentConfig env = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class);
|
||||
envConfig.put(projectId, env);
|
||||
}
|
||||
});
|
||||
config.setConfig(envConfig);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(this.getVariables())) {
|
||||
config.setVariables(this.variables);
|
||||
}
|
||||
|
@ -171,9 +189,9 @@ public class MsScenario extends MsTestElement {
|
|||
}
|
||||
});
|
||||
}
|
||||
if (config != null && config.getConfig() != null && config.getConfig().getCommonConfig() != null
|
||||
&& CollectionUtils.isNotEmpty(config.getConfig().getCommonConfig().getVariables())) {
|
||||
config.getConfig().getCommonConfig().getVariables().stream().filter(KeyValue::isValid).filter(KeyValue::isEnable).forEach(keyValue ->
|
||||
if (config != null && config.getConfig() != null && 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(), "=")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import org.apache.jorphan.collections.ListedHashTree;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
|
||||
|
@ -101,6 +102,8 @@ public abstract class MsTestElement {
|
|||
private LinkedList<MsTestElement> hashTree;
|
||||
@JSONField(ordinal = 11)
|
||||
private boolean customizeReq;
|
||||
@JSONField(ordinal = 12)
|
||||
private String projectId;
|
||||
|
||||
private MsTestElement parent;
|
||||
|
||||
|
@ -164,14 +167,14 @@ public abstract class MsTestElement {
|
|||
}
|
||||
|
||||
public Arguments addArguments(ParameterConfig config) {
|
||||
if (config != null && config.getConfig() != null && config.getConfig().getCommonConfig() != null
|
||||
&& CollectionUtils.isNotEmpty(config.getConfig().getCommonConfig().getVariables())) {
|
||||
if (config != null && config.getConfig() != null && config.getConfig().get(this.getProjectId()).getCommonConfig() != null
|
||||
&& CollectionUtils.isNotEmpty(config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables())) {
|
||||
Arguments arguments = new Arguments();
|
||||
arguments.setEnabled(true);
|
||||
arguments.setName(StringUtils.isNoneBlank(this.getName()) ? this.getName() : "Arguments");
|
||||
arguments.setProperty(TestElement.TEST_CLASS, Arguments.class.getName());
|
||||
arguments.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ArgumentsPanel"));
|
||||
config.getConfig().getCommonConfig().getVariables().stream().filter(KeyValue::isValid).filter(KeyValue::isEnable).forEach(keyValue ->
|
||||
config.getConfig().get(this.getProjectId()).getCommonConfig().getVariables().stream().filter(KeyValue::isValid).filter(KeyValue::isEnable).forEach(keyValue ->
|
||||
arguments.addArgument(keyValue.getName(), keyValue.getValue(), "=")
|
||||
);
|
||||
return arguments;
|
||||
|
|
|
@ -5,13 +5,14 @@ import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
|
|||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class ParameterConfig {
|
||||
/**
|
||||
* 环境配置
|
||||
*/
|
||||
private EnvironmentConfig config;
|
||||
private Map<String,EnvironmentConfig> config;
|
||||
/**
|
||||
* 公共场景参数
|
||||
*/
|
||||
|
|
|
@ -116,7 +116,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
|
|||
if (config != null && config.getConfig() != null) {
|
||||
config.setConfig(config.getConfig());
|
||||
} else {
|
||||
config.setConfig(getEnvironmentConfig(useEnvironment));
|
||||
// config.setConfig(getEnvironmentConfig(useEnvironment));
|
||||
}
|
||||
|
||||
// 添加环境中的公共变量
|
||||
|
@ -126,7 +126,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
|
|||
}
|
||||
try {
|
||||
if (config != null && config.getConfig() != null) {
|
||||
String url = config.getConfig().getHttpConfig().getProtocol() + "://" + config.getConfig().getHttpConfig().getSocket();
|
||||
String url = config.getConfig().get(this.getProjectId()).getHttpConfig().getProtocol() + "://" + config.getConfig().get(this.getProjectId()).getHttpConfig().getSocket();
|
||||
// 补充如果是完整URL 则用自身URL
|
||||
boolean isUrl = false;
|
||||
if (StringUtils.isNotEmpty(this.getUrl()) && isURL(this.getUrl())) {
|
||||
|
@ -139,9 +139,9 @@ public class MsHTTPSamplerProxy extends MsTestElement {
|
|||
sampler.setPort(urlObject.getPort());
|
||||
sampler.setProtocol(urlObject.getProtocol());
|
||||
} else {
|
||||
sampler.setDomain(config.getConfig().getHttpConfig().getDomain());
|
||||
sampler.setPort(config.getConfig().getHttpConfig().getPort());
|
||||
sampler.setProtocol(config.getConfig().getHttpConfig().getProtocol());
|
||||
sampler.setDomain(config.getConfig().get(this.getProjectId()).getHttpConfig().getDomain());
|
||||
sampler.setPort(config.getConfig().get(this.getProjectId()).getHttpConfig().getPort());
|
||||
sampler.setProtocol(config.getConfig().get(this.getProjectId()).getHttpConfig().getProtocol());
|
||||
}
|
||||
String envPath = StringUtils.equals(urlObject.getPath(), "/") ? "" : urlObject.getPath();
|
||||
if (StringUtils.isNotBlank(this.getPath()) && !isUrl) {
|
||||
|
@ -214,16 +214,16 @@ public class MsHTTPSamplerProxy extends MsTestElement {
|
|||
}
|
||||
|
||||
// 通用请求Headers
|
||||
if (config != null && config.getConfig() != null && config.getConfig().getHttpConfig() != null
|
||||
&& CollectionUtils.isNotEmpty(config.getConfig().getHttpConfig().getHeaders())) {
|
||||
setHeader(httpSamplerTree, config.getConfig().getHttpConfig().getHeaders());
|
||||
if (config != null && config.getConfig() != null && config.getConfig().get(this.getProjectId()).getHttpConfig() != null
|
||||
&& CollectionUtils.isNotEmpty(config.getConfig().get(this.getProjectId()).getHttpConfig().getHeaders())) {
|
||||
setHeader(httpSamplerTree, config.getConfig().get(this.getProjectId()).getHttpConfig().getHeaders());
|
||||
}
|
||||
|
||||
//判断是否要开启DNS
|
||||
if (config != null && config.getConfig() != null && config.getConfig().getCommonConfig() != null
|
||||
&& config.getConfig().getCommonConfig().isEnableHost()) {
|
||||
MsDNSCacheManager.addEnvironmentVariables(httpSamplerTree, this.getName(), config.getConfig());
|
||||
MsDNSCacheManager.addEnvironmentDNS(httpSamplerTree, this.getName(), config.getConfig());
|
||||
if (config != null && config.getConfig() != null && config.getConfig().get(this.getProjectId()).getCommonConfig() != null
|
||||
&& config.getConfig().get(this.getProjectId()).getCommonConfig().isEnableHost()) {
|
||||
MsDNSCacheManager.addEnvironmentVariables(httpSamplerTree, this.getName(), config.getConfig().get(this.getProjectId()));
|
||||
MsDNSCacheManager.addEnvironmentDNS(httpSamplerTree, this.getName(), config.getConfig().get(this.getProjectId()));
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(hashTree)) {
|
||||
for (MsTestElement el : hashTree) {
|
||||
|
|
|
@ -69,14 +69,16 @@ public class MsTCPSampler extends MsTestElement {
|
|||
private MsJSR223PreProcessor tcpPreProcessor;
|
||||
@JSONField(ordinal = 38)
|
||||
private String protocol = "TCP";
|
||||
@JSONField(ordinal = 39)
|
||||
private String projectId;
|
||||
|
||||
@Override
|
||||
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, ParameterConfig config) {
|
||||
if (this.getReferenced() != null && MsTestElementConstants.REF.name().equals(this.getReferenced())) {
|
||||
this.getRefElement(this);
|
||||
}
|
||||
config.setConfig(getEnvironmentConfig(useEnvironment));
|
||||
parseEnvironment(config.getConfig());
|
||||
// config.setConfig(getEnvironmentConfig(useEnvironment));
|
||||
parseEnvironment(config.getConfig().get(this.projectId));
|
||||
|
||||
// 添加环境中的公共变量
|
||||
Arguments arguments = this.addArguments(config);
|
||||
|
|
|
@ -561,10 +561,14 @@ public class ApiAutomationService {
|
|||
public String debugRun(RunDefinitionRequest request, List<MultipartFile> bodyFiles) {
|
||||
List<String> bodyUploadIds = new ArrayList<>(request.getBodyUploadIds());
|
||||
FileUtils.createBodyFiles(bodyUploadIds, bodyFiles);
|
||||
EnvironmentConfig envConfig = null;
|
||||
if (request.getEnvironmentId() != null) {
|
||||
ApiTestEnvironmentWithBLOBs environment = environmentService.get(request.getEnvironmentId());
|
||||
envConfig = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class);
|
||||
Map<String,EnvironmentConfig> envConfig = new HashMap<>();
|
||||
Map<String, String> map = request.getEnvironmentMap();
|
||||
if (map != null) {
|
||||
map.keySet().forEach(id -> {
|
||||
ApiTestEnvironmentWithBLOBs environment = environmentService.get(map.get(id));
|
||||
EnvironmentConfig env = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class);
|
||||
envConfig.put(id, env);
|
||||
});
|
||||
}
|
||||
ParameterConfig config = new ParameterConfig();
|
||||
config.setConfig(envConfig);
|
||||
|
|
|
@ -513,9 +513,9 @@ public class ApiTestCaseService {
|
|||
ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class);
|
||||
ApiTestEnvironmentWithBLOBs environment = environmentService.get(request.getEnvironmentId());
|
||||
ParameterConfig parameterConfig = new ParameterConfig();
|
||||
if (environment != null && environment.getConfig() != null) {
|
||||
parameterConfig.setConfig(JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class));
|
||||
}
|
||||
// if (environment != null && environment.getConfig() != null) {
|
||||
// parameterConfig.setConfig(JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class));
|
||||
// }
|
||||
testPlan.toHashTree(jmeterHashTree, testPlan.getHashTree(), parameterConfig);
|
||||
return jmeterHashTree;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="环境选择"
|
||||
:visible.sync="dialogVisible"
|
||||
width="30%"
|
||||
:destroy-on-close="true"
|
||||
:before-close="handleClose">
|
||||
|
||||
<div v-for="pe in data" :key="pe.id">
|
||||
<div>
|
||||
{{ getProjectName(pe.id) }}
|
||||
<el-select v-model="pe['selectEnv']" placeholder="请选择环境" style="margin-left:10px; margin-top: 10px;"
|
||||
size="small">
|
||||
<el-option v-for="(environment, index) in pe.envs" :key="index"
|
||||
:label="environment.name + (environment.config.httpConfig.socket ? (': ' + environment.config.httpConfig.protocol + '://' + environment.config.httpConfig.socket) : '')"
|
||||
:value="environment.id"/>
|
||||
<el-button class="ms-scenario-button" size="mini" type="primary" @click="openEnvironmentConfig(pe.id)">
|
||||
{{ $t('api_test.environment.environment_config') }}
|
||||
</el-button>
|
||||
<template v-slot:empty>
|
||||
<div class="empty-environment">
|
||||
<el-button class="ms-scenario-button" size="mini" type="primary" @click="openEnvironmentConfig(pe.id)">
|
||||
{{ $t('api_test.environment.environment_config') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false" size="small">取 消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" size="small">确 定</el-button>
|
||||
</span>
|
||||
<api-environment-config ref="environmentConfig" @close="environmentConfigClose"/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {parseEnvironment} from "@/business/components/api/test/model/EnvironmentModel";
|
||||
import ApiEnvironmentConfig from "@/business/components/api/definition/components/environment/ApiEnvironmentConfig";
|
||||
export default {
|
||||
name: "ApiScenarioEnv",
|
||||
components: {ApiEnvironmentConfig},
|
||||
props: {
|
||||
projectIds: Set,
|
||||
envMap: Map
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
projects: [],
|
||||
data: [],
|
||||
environmentId: '',
|
||||
environments: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getWsProjects();
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
init() {
|
||||
this.projectIds.forEach(id => {
|
||||
this.result = this.$get('/api/environment/list/' + id, res => {
|
||||
let envs = res.data;
|
||||
envs.forEach(environment => {
|
||||
parseEnvironment(environment);
|
||||
});
|
||||
let item = {};
|
||||
item.id = id;
|
||||
item.envs = envs;
|
||||
item.selectEnv = this.envMap.get(id);
|
||||
this.data.push(item)
|
||||
})
|
||||
})
|
||||
},
|
||||
open() {
|
||||
this.data = [];
|
||||
this.dialogVisible = true;
|
||||
if (this.projectIds.size > 0) {
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
getWsProjects() {
|
||||
this.$get("/project/listAll", res => {
|
||||
this.projects = res.data;
|
||||
})
|
||||
},
|
||||
getProjectName(id) {
|
||||
const project = this.projects.find(p => p.id === id);
|
||||
return project ? project.name : "";
|
||||
},
|
||||
openEnvironmentConfig(projectId) {
|
||||
if (!projectId) {
|
||||
this.$error(this.$t('api_test.select_project'));
|
||||
return;
|
||||
}
|
||||
this.$refs.environmentConfig.open(projectId);
|
||||
},
|
||||
handleConfirm() {
|
||||
let map = new Map();
|
||||
let sign = true;
|
||||
this.data.forEach(dt => {
|
||||
if (!dt.selectEnv) {
|
||||
sign = false;
|
||||
return;
|
||||
}
|
||||
map.set(dt.id, dt.selectEnv);
|
||||
})
|
||||
if (!sign) {
|
||||
this.$warning("请为每个项目选择一个运行环境!");
|
||||
return;
|
||||
}
|
||||
this.$emit('setProjectEnvMap', map);
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
checkEnv() {
|
||||
let sign = true;
|
||||
if (this.data.length > 0) {
|
||||
this.data.forEach(dt => {
|
||||
if (!dt.selectEnv) {
|
||||
sign = false;
|
||||
return;
|
||||
}
|
||||
})
|
||||
} else {
|
||||
sign = false;
|
||||
}
|
||||
|
||||
if (!sign) {
|
||||
this.$warning("请为每个项目选择一个运行环境!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
environmentConfigClose(id) {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -360,7 +360,7 @@
|
|||
this.changeSelectDataRangeAll();
|
||||
this.search();
|
||||
},
|
||||
search() {
|
||||
search(projectId) {
|
||||
this.selectRows = new Set();
|
||||
getLabel(this, API_SCENARIO_LIST);
|
||||
this.condition.moduleIds = this.selectNodeIds;
|
||||
|
@ -369,7 +369,10 @@
|
|||
this.condition.moduleIds = [];
|
||||
}
|
||||
|
||||
if (this.projectId != null) {
|
||||
// todo
|
||||
if (projectId != null && typeof projectId === 'string') {
|
||||
this.condition.projectId = projectId;
|
||||
} else if (this.projectId != null) {
|
||||
this.condition.projectId = this.projectId;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
name: 'MsDebugRun',
|
||||
components: {},
|
||||
props: {
|
||||
environment: String,
|
||||
environment: Map,
|
||||
debug: Boolean,
|
||||
reportId: String,
|
||||
runData: Object,
|
||||
|
@ -112,9 +112,12 @@
|
|||
threadGroup.hashTree = [];
|
||||
threadGroup.name = this.reportId;
|
||||
threadGroup.enableCookieShare = this.runData.enableCookieShare;
|
||||
let map = this.environment;
|
||||
this.runData.projectId = getCurrentProjectID();
|
||||
threadGroup.hashTree.push(this.runData);
|
||||
testPlan.hashTree.push(threadGroup);
|
||||
let reqObj = {id: this.reportId, reportId: this.reportId, scenarioName: this.runData.name, scenarioId: this.runData.id, environmentId: this.environment, testElement: testPlan, projectId: getCurrentProjectID()};
|
||||
let reqObj = {id: this.reportId, reportId: this.reportId, scenarioName: this.runData.name,
|
||||
scenarioId: this.runData.id, testElement: testPlan, projectId: getCurrentProjectID(), environmentMap: this.strMapToObj(map)};
|
||||
let bodyFiles = this.getBodyUploadFiles(reqObj);
|
||||
let url = "/api/automation/run/debug";
|
||||
this.$fileUpload(url, null, bodyFiles, reqObj, response => {
|
||||
|
@ -122,6 +125,13 @@
|
|||
this.$emit('runRefresh', {});
|
||||
}, erro => {
|
||||
});
|
||||
},
|
||||
strMapToObj(strMap){
|
||||
let obj= Object.create(null);
|
||||
for (let[k,v] of strMap) {
|
||||
obj[k] = v;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,24 +111,26 @@
|
|||
<el-col :span="3" class="ms-col-one ms-font">
|
||||
<el-checkbox v-model="enableCookieShare">共享cookie</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :span="7" class="ms-font">
|
||||
<el-select v-model="currentEnvironmentId" size="small" class="ms-htt-width"
|
||||
:placeholder="$t('api_test.definition.request.run_env')"
|
||||
clearable>
|
||||
<el-option v-for="(environment, index) in environments" :key="index"
|
||||
:label="environment.name + (environment.config.httpConfig.socket ? (': ' + environment.config.httpConfig.protocol + '://' + environment.config.httpConfig.socket) : '')"
|
||||
:value="environment.id"/>
|
||||
<el-button class="ms-scenario-button" size="mini" type="primary" @click="openEnvironmentConfig">
|
||||
{{ $t('api_test.environment.environment_config') }}
|
||||
</el-button>
|
||||
<template v-slot:empty>
|
||||
<div class="empty-environment">
|
||||
<el-button class="ms-scenario-button" size="mini" type="primary" @click="openEnvironmentConfig">
|
||||
{{ $t('api_test.environment.environment_config') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-select>
|
||||
<el-col :span="7" class="ms-col-one ms-font">
|
||||
<el-link type="primary" @click="handleEnv">环境配置</el-link>
|
||||
<!-- <el-select v-model="currentEnvironmentId" size="small" class="ms-htt-width"-->
|
||||
<!-- :placeholder="$t('api_test.definition.request.run_env')"-->
|
||||
<!-- clearable>-->
|
||||
<!-- <el-option v-for="(environment, index) in environments" :key="index"-->
|
||||
<!-- :label="environment.name + (environment.config.httpConfig.socket ? (': ' + environment.config.httpConfig.protocol + '://' + environment.config.httpConfig.socket) : '')"-->
|
||||
<!-- :value="environment.id"/>-->
|
||||
<!-- <el-button class="ms-scenario-button" size="mini" type="primary" @click="openEnvironmentConfig">-->
|
||||
<!-- {{ $t('api_test.environment.environment_config') }}-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <template v-slot:empty>-->
|
||||
<!-- <div class="empty-environment">-->
|
||||
<!-- <el-button class="ms-scenario-button" size="mini" type="primary" @click="openEnvironmentConfig">-->
|
||||
<!-- {{ $t('api_test.environment.environment_config') }}-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-select>-->
|
||||
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-button :disabled="scenarioDefinition.length < 1" size="small" type="primary" @click="runDebug">{{$t('api_test.request.debug')}}</el-button>
|
||||
|
@ -184,11 +186,13 @@
|
|||
<!--场景导入 -->
|
||||
<scenario-relevance @save="addScenario" ref="scenarioRelevance"/>
|
||||
|
||||
<api-scenario-env :project-ids="projectIds" :env-map="projectEnvMap" ref="apiScenarioEnv" @setProjectEnvMap="setProjectEnvMap"/>
|
||||
|
||||
<!-- 环境 -->
|
||||
<api-environment-config ref="environmentConfig" @close="environmentConfigClose"/>
|
||||
|
||||
<!--执行组件-->
|
||||
<ms-run :debug="true" :environment="currentEnvironmentId" :reportId="reportId" :run-data="debugData"
|
||||
<ms-run :debug="true" :environment="projectEnvMap" :reportId="reportId" :run-data="debugData"
|
||||
@runRefresh="runRefresh" ref="runTest"/>
|
||||
<!-- 调试结果 -->
|
||||
<el-drawer :visible.sync="debugVisible" :destroy-on-close="true" direction="ltr" :withHeader="true" :modal="false" size="90%">
|
||||
|
@ -204,44 +208,47 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {API_STATUS, PRIORITY} from "../../definition/model/JsonData";
|
||||
import {WORKSPACE_ID} from '@/common/js/constants';
|
||||
import {
|
||||
Assertions,
|
||||
ConstantTimer,
|
||||
Extract,
|
||||
IfController,
|
||||
JSR223Processor,
|
||||
LoopController
|
||||
} from "../../definition/model/ApiTestModel";
|
||||
import {parseEnvironment} from "../../definition/model/EnvironmentModel";
|
||||
import {ELEMENT_TYPE, ELEMENTS} from "./Setting";
|
||||
import MsApiCustomize from "./ApiCustomize";
|
||||
import {getCurrentProjectID, getUUID} from "@/common/js/utils";
|
||||
import ApiEnvironmentConfig from "../../definition/components/environment/ApiEnvironmentConfig";
|
||||
import MsInputTag from "./MsInputTag";
|
||||
import MsRun from "./DebugRun";
|
||||
import MsApiReportDetail from "../report/ApiReportDetail";
|
||||
import MsVariableList from "./variable/VariableList";
|
||||
import ApiImport from "../../definition/components/import/ApiImport";
|
||||
import "@/common/css/material-icons.css"
|
||||
import OutsideClick from "@/common/js/outside-click";
|
||||
import ScenarioApiRelevance from "./api/ApiRelevance";
|
||||
import ScenarioRelevance from "./api/ScenarioRelevance";
|
||||
import MsComponentConfig from "./component/ComponentConfig";
|
||||
import {handleCtrlSEvent} from "../../../../../common/js/utils";
|
||||
import {API_STATUS, PRIORITY} from "../../definition/model/JsonData";
|
||||
import {WORKSPACE_ID} from '@/common/js/constants';
|
||||
import {
|
||||
Assertions,
|
||||
ConstantTimer,
|
||||
Extract,
|
||||
IfController,
|
||||
JSR223Processor,
|
||||
LoopController
|
||||
} from "../../definition/model/ApiTestModel";
|
||||
import {parseEnvironment} from "../../definition/model/EnvironmentModel";
|
||||
import {ELEMENT_TYPE, ELEMENTS} from "./Setting";
|
||||
import MsApiCustomize from "./ApiCustomize";
|
||||
import {getCurrentProjectID, getUUID} from "@/common/js/utils";
|
||||
import ApiEnvironmentConfig from "../../definition/components/environment/ApiEnvironmentConfig";
|
||||
import MsInputTag from "./MsInputTag";
|
||||
import MsRun from "./DebugRun";
|
||||
import MsApiReportDetail from "../report/ApiReportDetail";
|
||||
import MsVariableList from "./variable/VariableList";
|
||||
import ApiImport from "../../definition/components/import/ApiImport";
|
||||
import "@/common/css/material-icons.css"
|
||||
import OutsideClick from "@/common/js/outside-click";
|
||||
import ScenarioApiRelevance from "./api/ApiRelevance";
|
||||
import ScenarioRelevance from "./api/ScenarioRelevance";
|
||||
import MsComponentConfig from "./component/ComponentConfig";
|
||||
import {handleCtrlSEvent} from "../../../../../common/js/utils";
|
||||
import {getProject} from "@/business/components/api/automation/scenario/event";
|
||||
import ApiScenarioEnv from "@/business/components/api/automation/scenario/ApiScenarioEnv";
|
||||
|
||||
export default {
|
||||
name: "EditApiScenario",
|
||||
props: {
|
||||
moduleOptions: Array,
|
||||
currentScenario: {},
|
||||
},
|
||||
components: {
|
||||
MsVariableList,
|
||||
ScenarioRelevance,
|
||||
ScenarioApiRelevance,
|
||||
ApiEnvironmentConfig,
|
||||
export default {
|
||||
name: "EditApiScenario",
|
||||
props: {
|
||||
moduleOptions: Array,
|
||||
currentScenario: {},
|
||||
},
|
||||
components: {
|
||||
ApiScenarioEnv,
|
||||
MsVariableList,
|
||||
ScenarioRelevance,
|
||||
ScenarioApiRelevance,
|
||||
ApiEnvironmentConfig,
|
||||
MsApiReportDetail,
|
||||
MsInputTag, MsRun,
|
||||
MsApiCustomize,
|
||||
|
@ -291,7 +298,9 @@
|
|||
globalOptions: {
|
||||
spacing: 30
|
||||
},
|
||||
response: {}
|
||||
response: {},
|
||||
projectIds: new Set,
|
||||
projectEnvMap: new Map
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
@ -304,7 +313,13 @@
|
|||
this.getApiScenario();
|
||||
this.addListener(); // 添加 ctrl s 监听
|
||||
},
|
||||
directives: {OutsideClick},
|
||||
mounted() {
|
||||
getProject.$on('addProjectEnv', (projectId, projectEnv) => {
|
||||
this.projectIds.add(projectId);
|
||||
// this.projectEnvMap.set(projectId, projectEnv);
|
||||
})
|
||||
},
|
||||
directives: {OutsideClick},
|
||||
computed: {
|
||||
buttons() {
|
||||
let buttons = [
|
||||
|
@ -606,6 +621,7 @@
|
|||
request.enable === undefined ? request.enable = true : request.enable;
|
||||
request.active = false;
|
||||
request.resourceId = getUUID();
|
||||
request.projectId = item.projectId;
|
||||
if (!request.url) {
|
||||
request.url = "";
|
||||
}
|
||||
|
@ -647,6 +663,12 @@
|
|||
const parent = node.parent
|
||||
const hashTree = parent.data.hashTree || parent.data;
|
||||
const index = hashTree.findIndex(d => d.resourceId != undefined && row.resourceId != undefined && d.resourceId === row.resourceId)
|
||||
if (hashTree[index] && hashTree[index].projectId) {
|
||||
this.projectIds.delete(hashTree[index].projectId);
|
||||
if (this.projectEnvMap.has(hashTree[index].projectId)) {
|
||||
this.projectEnvMap.delete(hashTree[index].projectId);
|
||||
}
|
||||
}
|
||||
hashTree.splice(index, 1);
|
||||
this.sort();
|
||||
this.reload();
|
||||
|
@ -681,8 +703,12 @@
|
|||
},
|
||||
runDebug() {
|
||||
/*触发执行操作*/
|
||||
if (!this.currentEnvironmentId) {
|
||||
this.$error(this.$t('api_test.environment.select_environment'));
|
||||
// if (!this.currentEnvironmentId) {
|
||||
// this.$error(this.$t('api_test.environment.select_environment'));
|
||||
// return;
|
||||
// }
|
||||
let sign = this.$refs.apiScenarioEnv.checkEnv();
|
||||
if (!sign) {
|
||||
return;
|
||||
}
|
||||
this.$refs['currentScenario'].validate((valid) => {
|
||||
|
@ -696,7 +722,7 @@
|
|||
referenced: 'Created',
|
||||
enableCookieShare: this.enableCookieShare,
|
||||
headers: this.currentScenario.headers,
|
||||
environmentId: this.currentEnvironmentId,
|
||||
environmentMap: this.projectEnvMap,
|
||||
hashTree: this.scenarioDefinition
|
||||
};
|
||||
this.reportId = getUUID().substring(0, 8);
|
||||
|
@ -873,6 +899,14 @@
|
|||
}
|
||||
})
|
||||
},
|
||||
objToStrMap(obj) {
|
||||
let strMap = new Map();
|
||||
for (let k of Object.keys(obj)) {
|
||||
strMap.set(k, obj[k]);
|
||||
}
|
||||
return strMap;
|
||||
},
|
||||
|
||||
getApiScenario() {
|
||||
if (this.currentScenario.tags != undefined && !(this.currentScenario.tags instanceof Array)) {
|
||||
this.currentScenario.tags = JSON.parse(this.currentScenario.tags);
|
||||
|
@ -891,6 +925,12 @@
|
|||
let obj = JSON.parse(response.data.scenarioDefinition);
|
||||
if (obj) {
|
||||
this.currentEnvironmentId = obj.environmentId;
|
||||
if (obj.environmentMap) {
|
||||
this.projectEnvMap = this.objToStrMap(obj.environmentMap);
|
||||
} else {
|
||||
// 兼容历史数据
|
||||
this.projectEnvMap.set(getCurrentProjectID(), obj.environmentId);
|
||||
}
|
||||
this.currentScenario.variables = [];
|
||||
let index = 1;
|
||||
if (obj.variables) {
|
||||
|
@ -922,6 +962,13 @@
|
|||
})
|
||||
}
|
||||
},
|
||||
strMapToObj(strMap){
|
||||
let obj= Object.create(null);
|
||||
for (let[k,v] of strMap) {
|
||||
obj[k] = v;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
setParameter() {
|
||||
this.currentScenario.stepTotal = this.scenarioDefinition.length;
|
||||
this.currentScenario.projectId = getCurrentProjectID();
|
||||
|
@ -935,8 +982,9 @@
|
|||
variables: this.currentScenario.variables,
|
||||
headers: this.currentScenario.headers,
|
||||
referenced: 'Created',
|
||||
environmentId: this.currentEnvironmentId,
|
||||
hashTree: this.scenarioDefinition
|
||||
environmentMap: this.strMapToObj(this.projectEnvMap),
|
||||
hashTree: this.scenarioDefinition,
|
||||
projectId: this.projectId,
|
||||
};
|
||||
this.currentScenario.scenarioDefinition = scenario;
|
||||
if (this.currentScenario.tags instanceof Array) {
|
||||
|
@ -974,6 +1022,15 @@
|
|||
size += this.currentScenario.headers.length - 1;
|
||||
}
|
||||
return size;
|
||||
},
|
||||
beforeDestroy() {
|
||||
getProject.$off('addProjectEnv');
|
||||
},
|
||||
handleEnv() {
|
||||
this.$refs.apiScenarioEnv.open();
|
||||
},
|
||||
setProjectEnvMap(projectEnvMap) {
|
||||
this.projectEnvMap = projectEnvMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<relevance-dialog :title="$t('api_test.automation.scenario_import')" ref="relevanceDialog">
|
||||
|
||||
<test-case-relevance-base
|
||||
@setProject="setProject"
|
||||
ref="baseRelevance">
|
||||
<template v-slot:aside>
|
||||
<ms-api-scenario-module
|
||||
@nodeSelectEvent="nodeChange"
|
||||
|
@ -22,8 +23,7 @@
|
|||
<el-button type="primary" @click="copy" @keydown.enter.native.prevent>{{$t('commons.copy')}}</el-button>
|
||||
<el-button type="primary" @click="reference" @keydown.enter.native.prevent> {{ $t('api_test.scenario.reference') }}</el-button>
|
||||
</template>
|
||||
|
||||
</relevance-dialog>
|
||||
</test-case-relevance-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -37,10 +37,12 @@
|
|||
import MsApiScenarioList from "../ApiScenarioList";
|
||||
import {getUUID} from "../../../../../../common/js/utils";
|
||||
import RelevanceDialog from "../../../../track/plan/view/comonents/base/RelevanceDialog";
|
||||
import TestCaseRelevanceBase from "@/business/components/track/plan/view/comonents/base/TestCaseRelevanceBase";
|
||||
|
||||
export default {
|
||||
name: "ScenarioRelevance",
|
||||
components: {
|
||||
TestCaseRelevanceBase,
|
||||
RelevanceDialog,
|
||||
MsApiScenarioList,
|
||||
MsApiScenarioModule,
|
||||
|
@ -55,6 +57,12 @@
|
|||
isApiListEnable: true,
|
||||
currentScenario: [],
|
||||
currentScenarioIds: [],
|
||||
projectId: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
projectId() {
|
||||
this.$refs.apiScenarioList.search(this.projectId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -65,11 +73,11 @@
|
|||
return;
|
||||
}
|
||||
this.currentScenario.forEach(item => {
|
||||
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'REF', resourceId: getUUID()};
|
||||
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'REF', resourceId: getUUID(), projectId: item.projectId};
|
||||
scenarios.push(obj);
|
||||
});
|
||||
this.$emit('save', scenarios);
|
||||
this.close();
|
||||
this.$refs.baseRelevance.close();
|
||||
},
|
||||
copy() {
|
||||
let scenarios = [];
|
||||
|
@ -82,12 +90,12 @@
|
|||
response.data.forEach(item => {
|
||||
let scenarioDefinition = JSON.parse(item.scenarioDefinition);
|
||||
if (scenarioDefinition && scenarioDefinition.hashTree) {
|
||||
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'Copy', resourceId: getUUID(), hashTree: scenarioDefinition.hashTree};
|
||||
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'Copy', resourceId: getUUID(), hashTree: scenarioDefinition.hashTree, projectId: item.projectId};
|
||||
scenarios.push(obj);
|
||||
}
|
||||
});
|
||||
this.$emit('save', scenarios);
|
||||
this.close();
|
||||
this.$refs.baseRelevance.close();
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -96,9 +104,9 @@
|
|||
this.$refs.relevanceDialog.close();
|
||||
},
|
||||
open() {
|
||||
this.$refs.relevanceDialog.open();
|
||||
this.$refs.baseRelevance.open();
|
||||
if (this.$refs.apiScenarioList) {
|
||||
this.$refs.apiScenarioList.search();
|
||||
this.$refs.apiScenarioList.search(this.projectId);
|
||||
}
|
||||
},
|
||||
nodeChange(node, nodeIds, pNodes) {
|
||||
|
@ -117,6 +125,9 @@
|
|||
this.currentScenario = Array.from(data).map(row => row);
|
||||
this.currentScenarioIds = Array.from(data).map(row => row.id);
|
||||
},
|
||||
setProject(projectId) {
|
||||
this.projectId = projectId;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<el-tag size="mini" style="margin-left: 20px" v-if="request.referenced==='Deleted'" type="danger">{{$t('api_test.automation.reference_deleted')}}</el-tag>
|
||||
<el-tag size="mini" style="margin-left: 20px" v-if="request.referenced==='Copy'">{{ $t('commons.copy') }}</el-tag>
|
||||
<el-tag size="mini" style="margin-left: 20px" v-if="request.referenced ==='REF'">{{ $t('api_test.scenario.reference') }}</el-tag>
|
||||
<span style="margin-left: 20px;">{{getProjectName(request.projectId)}}</span>
|
||||
<ms-run :debug="true" :reportId="reportId" :run-data="runData"
|
||||
@runRefresh="runRefresh" ref="runTest"/>
|
||||
|
||||
|
@ -70,6 +71,7 @@ import {getUUID} from "@/common/js/utils";
|
|||
import ApiBaseComponent from "../common/ApiBaseComponent";
|
||||
import ApiResponseComponent from "./ApiResponseComponent";
|
||||
import CustomizeReqInfo from "@/business/components/api/automation/scenario/common/CustomizeReqInfo";
|
||||
import {getProject} from "@/business/components/api/automation/scenario/event";
|
||||
|
||||
export default {
|
||||
name: "MsApiComponent",
|
||||
|
@ -94,6 +96,7 @@ export default {
|
|||
reportId: "",
|
||||
runData: [],
|
||||
isShowInput: false,
|
||||
projects: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
@ -102,6 +105,7 @@ export default {
|
|||
}
|
||||
// 加载引用对象数据
|
||||
this.getApiInfo();
|
||||
this.getWsProjects();
|
||||
if (this.request.protocol === 'HTTP') {
|
||||
this.setUrl(this.request.url);
|
||||
this.setUrl(this.request.path);
|
||||
|
@ -115,6 +119,7 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
getProject.$emit('addProjectEnv', this.request.projectId, this.currentEnvironmentId);
|
||||
},
|
||||
computed: {
|
||||
displayColor() {
|
||||
|
@ -270,6 +275,18 @@ export default {
|
|||
this.loading = false
|
||||
})
|
||||
},
|
||||
getWsProjects() {
|
||||
this.$get("/project/listAll", res => {
|
||||
this.projects = res.data;
|
||||
})
|
||||
},
|
||||
getProjectName(id) {
|
||||
const project = this.projects.find(p => p.id === id);
|
||||
if (project) {
|
||||
return project.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
<el-tag size="mini" style="margin-left: 20px" v-if="scenario.referenced==='Deleted'" type="danger">{{$t('api_test.automation.reference_deleted')}}</el-tag>
|
||||
<el-tag size="mini" style="margin-left: 20px" v-if="scenario.referenced==='Copy'">{{ $t('commons.copy') }}</el-tag>
|
||||
<el-tag size="mini" style="margin-left: 20px" v-if="scenario.referenced==='REF'">{{ $t('api_test.scenario.reference') }}</el-tag>
|
||||
|
||||
<span style="margin-left: 20px;">{{getProjectName(scenario.projectId)}}</span>
|
||||
</template>
|
||||
|
||||
</api-base-component>
|
||||
|
@ -27,6 +29,7 @@
|
|||
import MsDubboBasisParameters from "../../../definition/components/request/dubbo/BasisParameters";
|
||||
import MsApiRequestForm from "../../../definition/components/request/http/ApiHttpRequestForm";
|
||||
import ApiBaseComponent from "../common/ApiBaseComponent";
|
||||
import {getProject} from "@/business/components/api/automation/scenario/event";
|
||||
|
||||
export default {
|
||||
name: "ApiScenarioComponent",
|
||||
|
@ -37,9 +40,12 @@
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currentEnvironmentId: String,
|
||||
},
|
||||
watch: {},
|
||||
created() {
|
||||
this.getWsProjects();
|
||||
getProject.$emit('addProjectEnv', this.scenario.projectId, this.currentEnvironmentId);
|
||||
if (this.scenario.id && this.scenario.referenced === 'REF' && !this.scenario.loaded) {
|
||||
this.result = this.$get("/api/automation/getApiScenario/" + this.scenario.id, response => {
|
||||
if (response.data) {
|
||||
|
@ -63,7 +69,8 @@
|
|||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
isShowInput: false
|
||||
isShowInput: false,
|
||||
projects: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -109,6 +116,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
getWsProjects() {
|
||||
this.$get("/project/listAll", res => {
|
||||
this.projects = res.data;
|
||||
})
|
||||
},
|
||||
getProjectName(id) {
|
||||
const project = this.projects.find(p => p.id === id);
|
||||
if (project) {
|
||||
return project.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
import Vue from 'vue';
|
||||
export const getProject = new Vue();
|
|
@ -14,11 +14,12 @@
|
|||
<slot></slot>
|
||||
|
||||
<template v-slot:footer>
|
||||
<ms-dialog-footer @cancel="close" @confirm="save"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:footer>
|
||||
<ms-dialog-footer @cancel="close" @confirm="save"/>
|
||||
<div v-if="$slots.footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ms-dialog-footer @cancel="close" @confirm="save"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</relevance-dialog>
|
||||
|
@ -75,17 +76,15 @@
|
|||
},
|
||||
|
||||
getProject() {
|
||||
if (this.planId) {
|
||||
this.result = this.$post("/test/plan/project/", {planId: this.planId}, res => {
|
||||
let data = res.data;
|
||||
if (data) {
|
||||
this.projects = data;
|
||||
this.projectId = data[0].id;
|
||||
this.projectName = data[0].name;
|
||||
this.changeProject(data[0]);
|
||||
}
|
||||
})
|
||||
}
|
||||
this.result = this.$get("/project/listAll", res => {
|
||||
let data = res.data;
|
||||
if (data) {
|
||||
this.projects = data;
|
||||
this.projectId = data[0].id;
|
||||
this.projectName = data[0].name;
|
||||
this.changeProject(data[0]);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
changeProject(project) {
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 4cc8926bad84083f675a262d6766a92bb09a890a
|
||||
Subproject commit 448f6ad1178b1c04df18b386df188974b781a7fd
|
Loading…
Reference in New Issue