feat(接口测试): 增加Dubbo支持

This commit is contained in:
q4speed 2020-07-21 11:34:30 +08:00
parent 55c0a4be65
commit 390384c6a4
35 changed files with 1083 additions and 126 deletions

View File

@ -121,7 +121,7 @@
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId> <artifactId>fastjson</artifactId>
<version>1.2.45</version> <version>1.2.72</version>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -0,0 +1,13 @@
package io.metersphere.api.dto.scenario;
import io.metersphere.api.dto.scenario.request.dubbo.ConfigCenter;
import io.metersphere.api.dto.scenario.request.dubbo.ConsumerAndService;
import io.metersphere.api.dto.scenario.request.dubbo.RegistryCenter;
import lombok.Data;
@Data
public class DubboConfig {
private ConfigCenter configCenter;
private RegistryCenter registryCenter;
private ConsumerAndService consumerAndService;
}

View File

@ -1,21 +0,0 @@
package io.metersphere.api.dto.scenario;
import io.metersphere.api.dto.scenario.assertions.Assertions;
import io.metersphere.api.dto.scenario.extract.Extract;
import lombok.Data;
import java.util.List;
@Data
public class Request {
private String name;
private String url;
private String method;
private Boolean useEnvironment;
private String path;
private List<KeyValue> parameters;
private List<KeyValue> headers;
private Body body;
private Assertions assertions;
private Extract extract;
}

View File

@ -1,5 +1,6 @@
package io.metersphere.api.dto.scenario; package io.metersphere.api.dto.scenario;
import io.metersphere.api.dto.scenario.request.Request;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
@ -12,4 +13,5 @@ public class Scenario {
private List<KeyValue> variables; private List<KeyValue> variables;
private List<KeyValue> headers; private List<KeyValue> headers;
private List<Request> requests; private List<Request> requests;
private DubboConfig dubboConfig;
} }

View File

@ -0,0 +1,46 @@
package io.metersphere.api.dto.scenario.request;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.assertions.Assertions;
import io.metersphere.api.dto.scenario.extract.Extract;
import io.metersphere.api.dto.scenario.request.dubbo.ConfigCenter;
import io.metersphere.api.dto.scenario.request.dubbo.ConsumerAndService;
import io.metersphere.api.dto.scenario.request.dubbo.RegistryCenter;
import lombok.Data;
import java.util.List;
@Data
@JSONType(typeName = RequestType.DUBBO)
public class DubboRequest implements Request {
// type 必须放最前面以便能够转换正确的类
private String type = RequestType.DUBBO;
@JSONField(ordinal = 1)
private String name;
@JSONField(ordinal = 2)
private String protocol;
@JsonProperty(value = "interface")
@JSONField(ordinal = 3, name = "interface")
private String _interface;
@JSONField(ordinal = 4)
private String method;
@JSONField(ordinal = 5)
private ConfigCenter configCenter;
@JSONField(ordinal = 6)
private RegistryCenter registryCenter;
@JSONField(ordinal = 7)
private ConsumerAndService consumerAndService;
@JSONField(ordinal = 8)
private List<KeyValue> args;
@JSONField(ordinal = 9)
private List<KeyValue> attachmentArgs;
@JSONField(ordinal = 10)
private Assertions assertions;
@JSONField(ordinal = 11)
private Extract extract;
}

View File

@ -0,0 +1,38 @@
package io.metersphere.api.dto.scenario.request;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.scenario.Body;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.assertions.Assertions;
import io.metersphere.api.dto.scenario.extract.Extract;
import lombok.Data;
import java.util.List;
@Data
@JSONType(typeName = RequestType.HTTP)
public class HttpRequest implements Request {
// type 必须放最前面以便能够转换正确的类
private String type = RequestType.HTTP;
@JSONField(ordinal = 1)
private String name;
@JSONField(ordinal = 2)
private String url;
@JSONField(ordinal = 3)
private String method;
@JSONField(ordinal = 4)
private String path;
@JSONField(ordinal = 5)
private Boolean useEnvironment;
@JSONField(ordinal = 6)
private List<KeyValue> parameters;
@JSONField(ordinal = 7)
private List<KeyValue> headers;
@JSONField(ordinal = 8)
private Body body;
@JSONField(ordinal = 9)
private Assertions assertions;
@JSONField(ordinal = 10)
private Extract extract;
}

View File

@ -0,0 +1,14 @@
package io.metersphere.api.dto.scenario.request;
import com.alibaba.fastjson.annotation.JSONType;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = HttpRequest.class, name = RequestType.HTTP),
@JsonSubTypes.Type(value = DubboRequest.class, name = RequestType.DUBBO)
})
@JSONType(seeAlso = {HttpRequest.class, DubboRequest.class}, typeKey = "type")
public interface Request {
}

View File

@ -0,0 +1,8 @@
package io.metersphere.api.dto.scenario.request;
public class RequestType {
public static final String HTTP = "HTTP";
public static final String DUBBO = "DUBBO";
}

View File

@ -0,0 +1,14 @@
package io.metersphere.api.dto.scenario.request.dubbo;
import lombok.Data;
@Data
public class ConfigCenter {
private String protocol;
private String group;
private String namespace;
private String username;
private String address;
private String password;
private String timeout;
}

View File

@ -0,0 +1,15 @@
package io.metersphere.api.dto.scenario.request.dubbo;
import lombok.Data;
@Data
public class ConsumerAndService {
private String timeout;
private String version;
private String retries;
private String cluster;
private String group;
private String connections;
private String async;
private String loadBalance;
}

View File

@ -0,0 +1,13 @@
package io.metersphere.api.dto.scenario.request.dubbo;
import lombok.Data;
@Data
public class RegistryCenter {
private String protocol;
private String group;
private String username;
private String address;
private String password;
private String timeout;
}

View File

@ -1,7 +1,7 @@
package io.metersphere.api.parse; package io.metersphere.api.parse;
import io.metersphere.api.dto.scenario.KeyValue; import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.Request; import io.metersphere.api.dto.scenario.request.HttpRequest;
import io.metersphere.commons.exception.MSException; import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil; import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -42,11 +42,11 @@ public abstract class ApiImportAbstractParser implements ApiImportParser {
return testStr.toString(); return testStr.toString();
} }
protected void addContentType(Request request, String contentType) { protected void addContentType(HttpRequest request, String contentType) {
addHeader(request, HttpHeader.CONTENT_TYPE.toString(), contentType); addHeader(request, HttpHeader.CONTENT_TYPE.toString(), contentType);
} }
protected void addCookie(Request request, String key, String value) { protected void addCookie(HttpRequest request, String key, String value) {
List<KeyValue> headers = Optional.ofNullable(request.getHeaders()).orElse(new ArrayList<>()); List<KeyValue> headers = Optional.ofNullable(request.getHeaders()).orElse(new ArrayList<>());
boolean hasCookie = false; boolean hasCookie = false;
for (KeyValue header : headers) { for (KeyValue header : headers) {
@ -61,7 +61,7 @@ public abstract class ApiImportAbstractParser implements ApiImportParser {
} }
} }
protected void addHeader(Request request, String key, String value) { protected void addHeader(HttpRequest request, String key, String value) {
List<KeyValue> headers = Optional.ofNullable(request.getHeaders()).orElse(new ArrayList<>()); List<KeyValue> headers = Optional.ofNullable(request.getHeaders()).orElse(new ArrayList<>());
boolean hasContentType = false; boolean hasContentType = false;
for (KeyValue header : headers) { for (KeyValue header : headers) {

View File

@ -6,8 +6,9 @@ import io.metersphere.api.dto.parse.ApiImport;
import io.metersphere.api.dto.parse.postman.*; import io.metersphere.api.dto.parse.postman.*;
import io.metersphere.api.dto.scenario.Body; import io.metersphere.api.dto.scenario.Body;
import io.metersphere.api.dto.scenario.KeyValue; import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.Request; import io.metersphere.api.dto.scenario.request.HttpRequest;
import io.metersphere.api.dto.scenario.Scenario; import io.metersphere.api.dto.scenario.Scenario;
import io.metersphere.api.dto.scenario.request.Request;
import io.metersphere.commons.constants.MsRequestBodyType; import io.metersphere.commons.constants.MsRequestBodyType;
import io.metersphere.commons.constants.PostmanRequestBodyMode; import io.metersphere.commons.constants.PostmanRequestBodyMode;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -60,7 +61,7 @@ public class PostmanParser extends ApiImportAbstractParser {
List<PostmanItem> item = postmanCollection.getItem(); List<PostmanItem> item = postmanCollection.getItem();
List<Request> requests = new ArrayList<>(); List<Request> requests = new ArrayList<>();
for (PostmanItem requestItem : item) { for (PostmanItem requestItem : item) {
Request request = new Request(); HttpRequest request = new HttpRequest();
PostmanRequest requestDesc = requestItem.getRequest(); PostmanRequest requestDesc = requestItem.getRequest();
PostmanUrl url = requestDesc.getUrl(); PostmanUrl url = requestDesc.getUrl();
request.setName(requestItem.getName()); request.setName(requestItem.getName());
@ -75,7 +76,7 @@ public class PostmanParser extends ApiImportAbstractParser {
return requests; return requests;
} }
private Body parseBody(PostmanRequest requestDesc, Request request) { private Body parseBody(PostmanRequest requestDesc, HttpRequest request) {
Body body = new Body(); Body body = new Body();
JSONObject postmanBody = requestDesc.getBody(); JSONObject postmanBody = requestDesc.getBody();
String bodyMode = postmanBody.getString("mode"); String bodyMode = postmanBody.getString("mode");

View File

@ -5,8 +5,9 @@ import com.alibaba.fastjson.JSONObject;
import io.metersphere.api.dto.parse.ApiImport; import io.metersphere.api.dto.parse.ApiImport;
import io.metersphere.api.dto.scenario.Body; import io.metersphere.api.dto.scenario.Body;
import io.metersphere.api.dto.scenario.KeyValue; import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.Request; import io.metersphere.api.dto.scenario.request.HttpRequest;
import io.metersphere.api.dto.scenario.Scenario; import io.metersphere.api.dto.scenario.Scenario;
import io.metersphere.api.dto.scenario.request.Request;
import io.metersphere.commons.constants.MsRequestBodyType; import io.metersphere.commons.constants.MsRequestBodyType;
import io.metersphere.commons.constants.SwaggerParameterType; import io.metersphere.commons.constants.SwaggerParameterType;
import io.swagger.models.*; import io.swagger.models.*;
@ -43,7 +44,7 @@ public class Swagger2Parser extends ApiImportAbstractParser {
Set<HttpMethod> httpMethods = operationMap.keySet(); Set<HttpMethod> httpMethods = operationMap.keySet();
for (HttpMethod method : httpMethods) { for (HttpMethod method : httpMethods) {
Operation operation = operationMap.get(method); Operation operation = operationMap.get(method);
Request request = new Request(); HttpRequest request = new HttpRequest();
request.setName(operation.getOperationId()); request.setName(operation.getOperationId());
request.setPath(pathName); request.setPath(pathName);
request.setUseEnvironment(true); request.setUseEnvironment(true);
@ -68,13 +69,11 @@ public class Swagger2Parser extends ApiImportAbstractParser {
} }
} }
scenarioMap.values().forEach(scenario -> { scenarios.addAll(scenarioMap.values());
scenarios.add(scenario);
});
return scenarios; return scenarios;
} }
private void parseParameters(Operation operation, Map<String, Model> definitions, Request request) { private void parseParameters(Operation operation, Map<String, Model> definitions, HttpRequest request) {
List<Parameter> parameters = operation.getParameters(); List<Parameter> parameters = operation.getParameters();
@ -105,17 +104,17 @@ public class Swagger2Parser extends ApiImportAbstractParser {
} }
} }
private void parseCookieParameters(Parameter parameter, Request request) { private void parseCookieParameters(Parameter parameter, HttpRequest request) {
CookieParameter cookieParameter = (CookieParameter) parameter; CookieParameter cookieParameter = (CookieParameter) parameter;
addCookie(request, cookieParameter.getName(), cookieParameter.getDescription()); addCookie(request, cookieParameter.getName(), cookieParameter.getDescription());
} }
private void parseHeaderParameters(Parameter parameter, Request request) { private void parseHeaderParameters(Parameter parameter, HttpRequest request) {
HeaderParameter headerParameter = (HeaderParameter) parameter; HeaderParameter headerParameter = (HeaderParameter) parameter;
addHeader(request, headerParameter.getName(), headerParameter.getDescription()); addHeader(request, headerParameter.getName(), headerParameter.getDescription());
} }
private void parseBodyParameters(Parameter parameter, Request request, Map<String, Model> definitions) { private void parseBodyParameters(Parameter parameter, HttpRequest request, Map<String, Model> definitions) {
BodyParameter bodyParameter = (BodyParameter) parameter; BodyParameter bodyParameter = (BodyParameter) parameter;
Body body = Optional.ofNullable(request.getBody()).orElse(new Body()); Body body = Optional.ofNullable(request.getBody()).orElse(new Body());
body.setType(MsRequestBodyType.RAW.value()); body.setType(MsRequestBodyType.RAW.value());
@ -167,7 +166,7 @@ public class Swagger2Parser extends ApiImportAbstractParser {
return jsonObject; return jsonObject;
} }
private void parseFormDataParameters(Parameter parameter, Request request) { private void parseFormDataParameters(Parameter parameter, HttpRequest request) {
Body body = Optional.ofNullable(request.getBody()).orElse(new Body()); Body body = Optional.ofNullable(request.getBody()).orElse(new Body());
body.setType(MsRequestBodyType.FORM_DATA.value()); body.setType(MsRequestBodyType.FORM_DATA.value());
List<KeyValue> keyValues = Optional.ofNullable(body.getKvs()).orElse(new ArrayList<>()); List<KeyValue> keyValues = Optional.ofNullable(body.getKvs()).orElse(new ArrayList<>());
@ -176,7 +175,7 @@ public class Swagger2Parser extends ApiImportAbstractParser {
request.setBody(body); request.setBody(body);
} }
private void parseQueryParameters(Parameter parameter, Request request) { private void parseQueryParameters(Parameter parameter, HttpRequest request) {
QueryParameter queryParameter = (QueryParameter) parameter; QueryParameter queryParameter = (QueryParameter) parameter;
List<KeyValue> parameters = Optional.ofNullable(request.getParameters()).orElse(new ArrayList<>()); List<KeyValue> parameters = Optional.ofNullable(request.getParameters()).orElse(new ArrayList<>());
parameters.add(new KeyValue(queryParameter.getName(), "", queryParameter.getDescription())); parameters.add(new KeyValue(queryParameter.getName(), "", queryParameter.getDescription()));

View File

@ -176,8 +176,8 @@
}) })
}, },
cancel() { cancel() {
// console.log(this.test.toJMX().xml) console.log(this.test.toJMX().xml)
this.$router.push('/api/test/list/all'); // this.$router.push('/api/test/list/all');
}, },
getOptions(url) { getOptions(url) {
let formData = new FormData(); let formData = new FormData();

View File

@ -6,15 +6,17 @@
<div class="kv-row" v-for="(item, index) in items" :key="index"> <div class="kv-row" v-for="(item, index) in items" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle"> <el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col> <el-col>
<el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="100" @change="change" <el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="100"
:placeholder="$t('api_test.key')" show-word-limit/> @change="change"
:placeholder="keyText" show-word-limit/>
<el-autocomplete :maxlength="100" v-if="suggestions" v-model="item.name" size="small" <el-autocomplete :maxlength="100" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="$t('api_test.key')" show-word-limit/> :fetch-suggestions="querySearch" @change="change" :placeholder="keyText"
show-word-limit/>
</el-col> </el-col>
<el-col> <el-col>
<el-input :disabled="isReadOnly" v-model="item.value" size="small" maxlength="500" @change="change" <el-input :disabled="isReadOnly" v-model="item.value" size="small" maxlength="500" @change="change"
:placeholder="$t('api_test.value')" show-word-limit/> :placeholder="valueText" show-word-limit/>
</el-col> </el-col>
<el-col class="kv-delete"> <el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)" <el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
@ -32,6 +34,8 @@
name: "MsApiKeyValue", name: "MsApiKeyValue",
props: { props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String, description: String,
items: Array, items: Array,
isReadOnly: { isReadOnly: {
@ -41,6 +45,15 @@
suggestions: Array suggestions: Array
}, },
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: { methods: {
remove: function (index) { remove: function (index) {
this.items.splice(index, 1); this.items.splice(index, 1);

View File

@ -25,7 +25,7 @@
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</template> </template>
<ms-api-request-config :is-read-only="isReadOnly" :scenario="scenario" :open="select"/> <ms-api-request-config :is-read-only="isReadOnly" :scenario="scenario" @select="select"/>
</ms-api-collapse-item> </ms-api-collapse-item>
</draggable> </draggable>
</ms-api-collapse> </ms-api-collapse>
@ -36,7 +36,7 @@
<el-main class="scenario-main"> <el-main class="scenario-main">
<div class="scenario-form"> <div class="scenario-form">
<ms-api-scenario-form :is-read-only="isReadOnly" :scenario="selected" :project-id="projectId" v-if="isScenario"/> <ms-api-scenario-form :is-read-only="isReadOnly" :scenario="selected" :project-id="projectId" v-if="isScenario"/>
<ms-api-request-form :is-read-only="isReadOnly" :request="selected" :project-id="projectId" v-if="isRequest"/> <ms-api-request-form :is-read-only="isReadOnly" :request="selected" v-if="isRequest"/>
</div> </div>
</el-main> </el-main>
</el-container> </el-container>
@ -46,8 +46,8 @@
import MsApiCollapseItem from "./collapse/ApiCollapseItem"; import MsApiCollapseItem from "./collapse/ApiCollapseItem";
import MsApiCollapse from "./collapse/ApiCollapse"; import MsApiCollapse from "./collapse/ApiCollapse";
import MsApiRequestConfig from "./ApiRequestConfig"; import MsApiRequestConfig from "./request/ApiRequestConfig";
import MsApiRequestForm from "./ApiRequestForm"; import MsApiRequestForm from "./request/ApiRequestForm";
import MsApiScenarioForm from "./ApiScenarioForm"; import MsApiScenarioForm from "./ApiScenarioForm";
import {Scenario, Request} from "../model/ScenarioModel"; import {Scenario, Request} from "../model/ScenarioModel";
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';

View File

@ -4,24 +4,41 @@
<el-input :disabled="isReadOnly" v-model="scenario.name" maxlength="100" show-word-limit/> <el-input :disabled="isReadOnly" v-model="scenario.name" maxlength="100" show-word-limit/>
</el-form-item> </el-form-item>
<el-form-item :label="$t('api_test.environment.environment')"> <el-form-item :label="$t('api_test.environment.environment')">
<el-select :disabled="isReadOnly" v-model="scenario.environmentId" class="environment-select" @change="environmentChange" clearable> <el-select :disabled="isReadOnly" v-model="scenario.environmentId" class="environment-select"
<el-option v-for="(environment, index) in environments" :key="index" :label="environment.name + ': ' + environment.protocol + '://' + environment.socket" :value="environment.id"/> @change="environmentChange" clearable>
<el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">{{$t('api_test.environment.environment_config')}}</el-button> <el-option v-for="(environment, index) in environments" :key="index"
<template v-slot:empty> :label="environment.name + ': ' + environment.protocol + '://' + environment.socket"
<div class="empty-environment"> :value="environment.id"/>
<el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">{{$t('api_test.environment.environment_config')}}</el-button> <el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">
</div> {{$t('api_test.environment.environment_config')}}
</template> </el-button>
</el-select> <template v-slot:empty>
</el-form-item> <div class="empty-environment">
<el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">
{{$t('api_test.environment.environment_config')}}
</el-button>
</div>
</template>
</el-select>
</el-form-item>
<el-tabs v-model="activeName"> <el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_test.scenario.variables')" name="parameters"> <el-tab-pane :label="$t('api_test.scenario.variables')" name="parameters">
<ms-api-scenario-variables :is-read-only="isReadOnly" :items="scenario.variables" :description="$t('api_test.scenario.kv_description')"/> <ms-api-scenario-variables :is-read-only="isReadOnly" :items="scenario.variables"
:description="$t('api_test.scenario.kv_description')"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('api_test.scenario.headers')" name="headers"> <el-tab-pane :label="$t('api_test.scenario.headers')" name="headers">
<ms-api-key-value :is-read-only="isReadOnly" :items="scenario.headers" :suggestions="headerSuggestions" :description="$t('api_test.scenario.kv_description')"/> <ms-api-key-value :is-read-only="isReadOnly" :items="scenario.headers" :suggestions="headerSuggestions"
:description="$t('api_test.scenario.kv_description')"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.scenario.dubbo')" name="dubbo">
<div class="dubbo-config-title">Config Center</div>
<ms-dubbo-config-center :config="scenario.dubboConfig.configCenter"/>
<div class="dubbo-config-title">Registry Center</div>
<ms-dubbo-registry-center :registry="scenario.dubboConfig.registryCenter"/>
<div class="dubbo-config-title">Consumer & Service</div>
<ms-dubbo-consumer-service :consumer="scenario.dubboConfig.consumerAndService"/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@ -36,11 +53,17 @@
import {Scenario} from "../model/ScenarioModel"; import {Scenario} from "../model/ScenarioModel";
import MsApiScenarioVariables from "./ApiScenarioVariables"; import MsApiScenarioVariables from "./ApiScenarioVariables";
import ApiEnvironmentConfig from "./ApiEnvironmentConfig"; import ApiEnvironmentConfig from "./ApiEnvironmentConfig";
import {requestHeaders} from "../../../../../common/js/constants"; import {REQUEST_HEADERS} from "@/common/js/constants";
import MsDubboRegistryCenter from "@/business/components/api/test/components/request/dubbo/RegistryCenter";
import MsDubboConfigCenter from "@/business/components/api/test/components/request/dubbo/ConfigCenter";
import MsDubboConsumerService from "@/business/components/api/test/components/request/dubbo/ConsumerAndService";
export default { export default {
name: "MsApiScenarioForm", name: "MsApiScenarioForm",
components: {ApiEnvironmentConfig, MsApiScenarioVariables, MsApiKeyValue}, components: {
MsDubboConsumerService,
MsDubboConfigCenter, MsDubboRegistryCenter, ApiEnvironmentConfig, MsApiScenarioVariables, MsApiKeyValue
},
props: { props: {
scenario: Scenario, scenario: Scenario,
projectId: String, projectId: String,
@ -65,7 +88,7 @@
{max: 100, message: this.$t('commons.input_limit', [1, 100]), trigger: 'blur'} {max: 100, message: this.$t('commons.input_limit', [1, 100]), trigger: 'blur'}
] ]
}, },
headerSuggestions: requestHeaders headerSuggestions: REQUEST_HEADERS
} }
}, },
watch: { watch: {
@ -143,7 +166,13 @@
} }
.empty-environment { .empty-environment {
padding: 10px 0px; padding: 10px 0;
}
.dubbo-config-title {
margin-bottom: 10px;
font-size: 15px;
font-weight: 600;
} }
</style> </style>

View File

@ -12,17 +12,19 @@
</template> </template>
<script> <script>
export default { export default {
name: "ApiRequestMethodSelect", name: "ApiRequestMethodSelect",
props: ['isReadOnly', 'request'], props: ['isReadOnly', 'request'],
methods: { methods: {
change(value) { change(value) {
this.$emit('change', value); this.$emit('change', value);
}
} }
} }
}
</script> </script>
<style scoped> <style scoped>
.request-method-select {
width: 110px;
}
</style> </style>

View File

@ -41,7 +41,7 @@
import MsApiScenarioVariables from "../ApiScenarioVariables"; import MsApiScenarioVariables from "../ApiScenarioVariables";
import MsApiKeyValue from "../ApiKeyValue"; import MsApiKeyValue from "../ApiKeyValue";
import MsDialogFooter from "../../../../common/components/MsDialogFooter"; import MsDialogFooter from "../../../../common/components/MsDialogFooter";
import {requestHeaders} from "../../../../../../common/js/constants"; import {REQUEST_HEADERS} from "../../../../../../common/js/constants";
export default { export default {
name: "EnvironmentEdit", name: "EnvironmentEdit",
@ -71,7 +71,7 @@
], ],
socket :[{required: true, validator: socketValidator, trigger: 'blur'}], socket :[{required: true, validator: socketValidator, trigger: 'blur'}],
}, },
headerSuggestions: requestHeaders headerSuggestions: REQUEST_HEADERS
} }
}, },
methods: { methods: {

View File

@ -0,0 +1,129 @@
<template>
<el-form :model="request" :rules="rules" ref="request" label-width="100px" :disabled="isReadOnly">
<el-form-item :label="$t('api_test.request.name')" prop="name">
<el-input v-model="request.name" maxlength="300" show-word-limit/>
</el-form-item>
<el-form-item :label="$t('api_test.request.dubbo.protocol')" prop="protocol">
<el-select v-model="request.protocol">
<el-option label="dubbo://" :value="protocols.DUBBO"/>
</el-select>
</el-form-item>
<el-tabs v-model="activeName">
<el-tab-pane label="Interface" name="interface">
<ms-dubbo-interface :request="request"/>
</el-tab-pane>
<el-tab-pane label="Config Center" name="config">
<ms-dubbo-config-center :config="request.configCenter"/>
</el-tab-pane>
<el-tab-pane label="Registry Center" name="registry">
<ms-dubbo-registry-center :registry="request.registryCenter"/>
</el-tab-pane>
<el-tab-pane label="Consumer & Service" name="consumer">
<ms-dubbo-consumer-service :consumer="request.consumerAndService"/>
</el-tab-pane>
</el-tabs>
<el-tabs v-model="activeName2">
<el-tab-pane label="Args" name="args">
<ms-api-key-value :is-read-only="isReadOnly" :items="request.args"
key-placeholder="Param Type" value-placeholder="Param Value"/>
</el-tab-pane>
<el-tab-pane label="Attachment Args" name="attachment">
<ms-api-key-value :is-read-only="isReadOnly" :items="request.attachmentArgs"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
<ms-api-assertions :is-read-only="isReadOnly" :assertions="request.assertions"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract.label')" name="extract">
<ms-api-extract :is-read-only="isReadOnly" :extract="request.extract"/>
</el-tab-pane>
</el-tabs>
</el-form>
</template>
<script>
import MsApiKeyValue from "../ApiKeyValue";
import MsApiBody from "../ApiBody";
import MsApiAssertions from "../assertion/ApiAssertions";
import {DubboRequest} from "../../model/ScenarioModel";
import MsApiExtract from "../extract/ApiExtract";
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
import MsDubboInterface from "@/business/components/api/test/components/request/dubbo/Interface";
import MsDubboRegistryCenter from "@/business/components/api/test/components/request/dubbo/RegistryCenter";
import MsDubboConfigCenter from "@/business/components/api/test/components/request/dubbo/ConfigCenter";
import MsDubboConsumerService from "@/business/components/api/test/components/request/dubbo/ConsumerAndService";
export default {
name: "MsApiDubboRequestForm",
components: {
MsDubboConsumerService,
MsDubboConfigCenter,
MsDubboRegistryCenter,
MsDubboInterface, ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue
},
props: {
request: DubboRequest,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
activeName: "interface",
activeName2: "args",
protocols: DubboRequest.PROTOCOLS,
rules: {
name: [
{max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'}
],
}
}
},
methods: {
useEnvironmentChange(value) {
if (value && !this.request.environment) {
this.$error(this.$t('api_test.request.please_add_environment_to_scenario'), 2000);
this.request.useEnvironment = false;
}
this.$refs["request"].clearValidate();
}
},
computed: {}
}
</script>
<style scoped>
.request-method-select {
width: 110px;
}
.el-tag {
width: 100%;
height: 40px;
line-height: 40px;
}
.environment-display {
font-size: 14px;
}
.environment-name {
font-weight: bold;
font-style: italic;
}
.adjust-margin-bottom {
margin-bottom: 10px;
}
.environment-url-tip {
color: #F56C6C;
}
</style>

View File

@ -5,10 +5,11 @@
<el-input :disabled="isReadOnly" v-model="request.name" maxlength="300" show-word-limit/> <el-input :disabled="isReadOnly" v-model="request.name" maxlength="300" show-word-limit/>
</el-form-item> </el-form-item>
<el-form-item v-if="!request.useEnvironment" :label="$t('api_test.request.url')" prop="url" class="adjust-margin-bottom"> <el-form-item v-if="!request.useEnvironment" :label="$t('api_test.request.url')" prop="url"
class="adjust-margin-bottom">
<el-input :disabled="isReadOnly" v-model="request.url" maxlength="500" <el-input :disabled="isReadOnly" v-model="request.url" maxlength="500"
:placeholder="$t('api_test.request.url_description')" @change="urlChange" clearable> :placeholder="$t('api_test.request.url_description')" @change="urlChange" clearable>
<template v-slot:prepend> <template v-slot:prepend>
<ApiRequestMethodSelect :is-read-only="isReadOnly" :request="request" @change="methodChange"/> <ApiRequestMethodSelect :is-read-only="isReadOnly" :request="request" @change="methodChange"/>
</template> </template>
</el-input> </el-input>
@ -17,7 +18,7 @@
<el-form-item v-if="request.useEnvironment" :label="$t('api_test.request.path')" prop="path"> <el-form-item v-if="request.useEnvironment" :label="$t('api_test.request.path')" prop="path">
<el-input :disabled="isReadOnly" v-model="request.path" maxlength="500" <el-input :disabled="isReadOnly" v-model="request.path" maxlength="500"
:placeholder="$t('api_test.request.path_description')" @change="pathChange" clearable> :placeholder="$t('api_test.request.path_description')" @change="pathChange" clearable>
<template v-slot:prepend> <template v-slot:prepend>
<ApiRequestMethodSelect :is-read-only="isReadOnly" :request="request" @change="methodChange"/> <ApiRequestMethodSelect :is-read-only="isReadOnly" :request="request" @change="methodChange"/>
</template> </template>
</el-input> </el-input>
@ -60,19 +61,20 @@
</template> </template>
<script> <script>
import MsApiKeyValue from "./ApiKeyValue"; import MsApiKeyValue from "../ApiKeyValue";
import MsApiBody from "./ApiBody"; import MsApiBody from "../ApiBody";
import MsApiAssertions from "./assertion/ApiAssertions"; import MsApiAssertions from "../assertion/ApiAssertions";
import {KeyValue, Request} from "../model/ScenarioModel"; import {KeyValue} from "../../model/ScenarioModel";
import MsApiExtract from "./extract/ApiExtract"; import MsApiExtract from "../extract/ApiExtract";
import ApiRequestMethodSelect from "./collapse/ApiRequestMethodSelect"; import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
import {requestHeaders} from "../../../../../common/js/constants"; import {REQUEST_HEADERS} from "@/common/js/constants";
import {HttpRequest} from "../../model/ScenarioModel";
export default { export default {
name: "MsApiRequestForm", name: "MsApiHttpRequestForm",
components: {ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue}, components: {ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue},
props: { props: {
request: Request, request: HttpRequest,
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
default: false default: false
@ -101,7 +103,7 @@
{max: 500, required: true, message: this.$t('commons.input_limit', [1, 500]), trigger: 'blur'}, {max: 500, required: true, message: this.$t('commons.input_limit', [1, 500]), trigger: 'blur'},
] ]
}, },
headerSuggestions: requestHeaders headerSuggestions: REQUEST_HEADERS
} }
}, },
@ -119,7 +121,8 @@
this.request.path = '/' + this.request.path; this.request.path = '/' + this.request.path;
} }
let url = this.getURL(this.displayUrl); let url = this.getURL(this.displayUrl);
this.request.path = decodeURIComponent(url.pathname); this
.request.path = decodeURIComponent(url.pathname);
this.request.urlWirhEnv = decodeURIComponent(url.origin + url.pathname); this.request.urlWirhEnv = decodeURIComponent(url.origin + url.pathname);
}, },
getURL(urlStr) { getURL(urlStr) {
@ -169,10 +172,6 @@
</script> </script>
<style scoped> <style scoped>
.request-method-select {
width: 110px;
}
.el-tag { .el-tag {
width: 100%; width: 100%;
height: 40px; height: 40px;

View File

@ -3,9 +3,12 @@
<draggable :list="this.scenario.requests" group="Request" class="request-draggable" ghost-class="request-ghost"> <draggable :list="this.scenario.requests" group="Request" class="request-draggable" ghost-class="request-ghost">
<div class="request-item" v-for="(request, index) in this.scenario.requests" :key="index" @click="select(request)" <div class="request-item" v-for="(request, index) in this.scenario.requests" :key="index" @click="select(request)"
:class="{'selected': isSelected(request)}"> :class="{'selected': isSelected(request)}">
<el-row type="flex"> <el-row type="flex" align="middle">
<div class="request-type">
{{request.showType()}}
</div>
<div class="request-method"> <div class="request-method">
{{request.method}} {{request.showMethod()}}
</div> </div>
<div class="request-name"> <div class="request-name">
{{request.name}} {{request.name}}
@ -26,12 +29,20 @@
</el-row> </el-row>
</div> </div>
</draggable> </draggable>
<el-button :disabled="isReadOnly" class="request-create" type="primary" size="mini" icon="el-icon-plus" plain @click="createRequest"/> <el-popover placement="top" v-model="visible">
<el-radio-group v-model="type" @change="createRequest">
<el-radio :label="types.HTTP">HTTP</el-radio>
<el-radio :label="types.DUBBO">DUBBO</el-radio>
</el-radio-group>
<el-button slot="reference" :disabled="isReadOnly"
class="request-create" type="primary" size="mini" icon="el-icon-plus" plain/>
</el-popover>
</div> </div>
</template> </template>
<script> <script>
import {Request} from "../model/ScenarioModel"; import {RequestFactory} from "../../model/ScenarioModel";
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
export default { export default {
@ -41,7 +52,6 @@
props: { props: {
scenario: Object, scenario: Object,
open: Function,
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
default: false default: false
@ -51,6 +61,9 @@
data() { data() {
return { return {
selected: 0, selected: 0,
visible: false,
types: RequestFactory.TYPES,
type: ""
} }
}, },
@ -63,13 +76,15 @@
}, },
methods: { methods: {
createRequest: function () { createRequest: function (type) {
let request = new Request(); let request = new RequestFactory({type: type});
this.scenario.requests.push(request); this.scenario.requests.push(request);
this.type = "";
this.visible = false;
}, },
copyRequest: function (index) { copyRequest: function (index) {
let request = this.scenario.requests[index]; let request = this.scenario.requests[index];
this.scenario.requests.push(new Request(request)); this.scenario.requests.push(new RequestFactory(request));
}, },
deleteRequest: function (index) { deleteRequest: function (index) {
this.scenario.requests.splice(index, 1); this.scenario.requests.splice(index, 1);
@ -93,7 +108,7 @@
request.useEnvironment = false; request.useEnvironment = false;
} }
this.selected = request; this.selected = request;
this.open(request); this.$emit("select", request);
} }
}, },
@ -106,7 +121,6 @@
<style scoped> <style scoped>
.request-item { .request-item {
border-left: 5px solid #1E90FF; border-left: 5px solid #1E90FF;
line-height: 40px;
max-height: 40px; max-height: 40px;
border-top: 1px solid #EBEEF5; border-top: 1px solid #EBEEF5;
cursor: pointer; cursor: pointer;
@ -124,6 +138,19 @@
background-color: #F5F5F5; background-color: #F5F5F5;
} }
.request-type {
background-color: #409eff;
color: #fff;
margin-left: 5px;
padding: 4px 8px;
border-radius: 20px;
white-space: nowrap;
font-size: 12px;
display: inline-block;
line-height: 1;
text-align: center;
}
.request-method { .request-method {
padding: 0 5px; padding: 0 5px;
color: #1E90FF; color: #1E90FF;

View File

@ -0,0 +1,38 @@
<template>
<component :is="component" :is-read-only="isReadOnly" :request="request"/>
</template>
<script>
import {Request, RequestFactory} from "../../model/ScenarioModel";
import MsApiHttpRequestForm from "./ApiHttpRequestForm";
import MsApiDubboRequestForm from "./ApiDubboRequestForm";
export default {
name: "MsApiRequestForm",
components: {MsApiDubboRequestForm, MsApiHttpRequestForm},
props: {
request: Request,
isReadOnly: {
type: Boolean,
default: false
}
},
computed: {
component({request: {type}}) {
let name;
switch (type) {
case RequestFactory.TYPES.DUBBO:
name = "MsApiDubboRequestForm";
break;
default:
name = "MsApiHttpRequestForm";
}
return name;
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,72 @@
<template>
<el-form :model="config" :rules="rules" ref="config" label-width="100px" size="small">
<el-form-item label="Protocol" prop="protocol" class="dubbo-form-item">
<el-select v-model="config.protocol" class="select-100">
<el-option v-for="p in protocols" :key="p" :label="p" :value="p"/>
</el-select>
</el-form-item>
<el-form-item label="Group" prop="group" class="dubbo-form-item">
<el-input v-model="config.group" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Namespace" prop="namespace" class="dubbo-form-item">
<el-input v-model="config.namespace" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="UserName" prop="username" class="dubbo-form-item">
<el-input v-model="config.username" maxlength="100" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Address" prop="address" class="dubbo-form-item-long">
<el-input v-model="config.address" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Password" prop="password" class="dubbo-form-item">
<el-input v-model="config.password" maxlength="30" show-word-limit show-password autocomplete="new-password"
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Timeout" prop="timeout" class="dubbo-form-item">
<el-input type="number" v-model="config.timeout" :placeholder="$t('commons.input_content')"/>
</el-form-item>
</el-form>
</template>
<script>
import './dubbo.css'
import {ConfigCenter} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MsDubboConfigCenter",
props: {
config: ConfigCenter
},
data() {
return {
protocols: ConfigCenter.PROTOCOLS,
methods: [],
rules: {
group: [
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
],
namespace: [
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
],
username: [
{max: 100, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
],
password: [
{max: 30, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
],
address: [
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
]
}
}
}
}
</script>

View File

@ -0,0 +1,74 @@
<template>
<el-form :model="consumer" :rules="rules" ref="consumer" label-width="100px" size="small">
<el-form-item label="Timeout" prop="timeout" class="dubbo-form-item">
<el-input type="number" v-model="consumer.timeout" :placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Version" prop="version" class="dubbo-form-item">
<el-input v-model="consumer.version" maxlength="30" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Retries" prop="retries" class="dubbo-form-item">
<el-input type="number" v-model="consumer.retries" :placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Cluster" prop="cluster" class="dubbo-form-item">
<el-input v-model="consumer.cluster" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Group" prop="group" class="dubbo-form-item">
<el-input v-model="consumer.group" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Connections" prop="connections" class="dubbo-form-item">
<el-input type="number" v-model="consumer.connections" :placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Async" prop="async" class="dubbo-form-item">
<el-select v-model="consumer.async" class="select-100">
<el-option v-for="option in asyncOptions" :key="option" :label="option" :value="option"/>
</el-select>
</el-form-item>
<el-form-item label="LoadBalance" prop="loadBalance" class="dubbo-form-item">
<el-select v-model="consumer.loadBalance" class="select-100">
<el-option v-for="option in loadBalances" :key="option" :label="option" :value="option"/>
</el-select>
</el-form-item>
</el-form>
</template>
<script>
import './dubbo.css'
import {ConsumerAndService} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MsDubboConsumerService",
props: {
consumer: ConsumerAndService
},
data() {
return {
asyncOptions: ConsumerAndService.ASYNC_OPTIONS,
loadBalances: ConsumerAndService.LOAD_BALANCE_OPTIONS,
methods: [],
rules: {
version: [
{max: 30, message: this.$t('commons.input_limit', [0, 30]), trigger: 'blur'}
],
cluster: [
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
],
group: [
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
]
}
}
}
}
</script>

View File

@ -0,0 +1,77 @@
<template>
<el-form :model="request" :rules="rules" ref="request" label-width="100px" size="small">
<el-button class="get-provider" type="primary" size="small" @click="getProviderList">Get Provider List</el-button>
<el-row>
<el-col :span="12">
<el-form-item label="Interfaces" prop="interfaces">
<el-select v-model="request.interface" class="select-100">
<el-option v-for="i in interfaces" :key="i.value" :label="i.label" :value="i.value"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Methods" prop="methods">
<el-select v-model="request.method" class="select-100">
<el-option v-for="i in methods" :key="i.value" :label="i.label" :value="i.value"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="Interface" prop="interface">
<el-input v-model="request.interface" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Method" prop="method">
<el-input v-model="request.method" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import {DubboRequest} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MsDubboInterface",
props: {
request: DubboRequest
},
data() {
return {
interfaces: [],
methods: [],
rules: {
interface: [
{max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'}
],
method: [
{max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'}
]
}
}
},
methods: {
getProviderList() {
}
}
}
</script>
<style scoped>
.get-provider {
margin-bottom: 22px;
}
.select-100 {
width: 100%;
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<el-form :model="registry" :rules="rules" ref="registry" label-width="100px" size="small">
<el-form-item label="Protocol" prop="protocol" class="dubbo-form-item">
<el-select v-model="registry.protocol" class="select-100">
<el-option v-for="p in protocols" :key="p" :label="p" :value="p"/>
</el-select>
</el-form-item>
<el-form-item label="Group" prop="group" class="dubbo-form-item">
<el-input v-model="registry.group" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="UserName" prop="username" class="dubbo-form-item">
<el-input v-model="registry.username" maxlength="100" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Password" prop="password" class="dubbo-form-item">
<el-input v-model="registry.password" maxlength="30" show-word-limit show-password autocomplete="new-password"
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Address" prop="address" class="dubbo-form-item-long">
<el-input v-model="registry.address" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item label="Timeout" prop="timeout" class="dubbo-form-item">
<el-input type="number" v-model="registry.timeout" :placeholder="$t('commons.input_content')"/>
</el-form-item>
</el-form>
</template>
<script>
import './dubbo.css'
import {RegistryCenter} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MsDubboRegistryCenter",
props: {
registry: RegistryCenter
},
data() {
return {
protocols: RegistryCenter.PROTOCOLS,
methods: [],
rules: {
group: [
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
],
username: [
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
],
password: [
{max: 30, message: this.$t('commons.input_limit', [0, 30]), trigger: 'blur'}
],
address: [
{max: 300, message: this.$t('commons.input_limit', [0, 300]), trigger: 'blur'}
]
}
}
}
}
</script>

View File

@ -0,0 +1,37 @@
@media only screen and (max-width: 1200px) {
.dubbo-form-item {
width: 50%;
}
.dubbo-form-item-long {
width: 50%;
}
}
@media only screen and (min-width: 1201px) and (max-width: 1600px) {
.dubbo-form-item {
width: 33.33%;
}
.dubbo-form-item-long {
width: 66.67%;
}
}
@media only screen and (min-width: 1601px) {
.dubbo-form-item {
width: 25%;
}
.dubbo-form-item-long {
width: 50%;
}
}
.dubbo-form-item, .dubbo-form-item-long {
display: inline-block;
}
.select-100 {
width: 100%;
}

View File

@ -220,6 +220,60 @@ export class ThreadGroup extends DefaultTestElement {
} }
} }
export class DubboSample extends DefaultTestElement {
constructor(testName, request = {}) {
super('io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample',
'io.github.ningyu.jmeter.plugin.dubbo.gui.DubboSampleGui',
'io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample', testName);
this.request = request;
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_PROTOCOL", this.request.configCenter.protocol);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_GROUP", this.request.configCenter.group);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_NAMESPACE", this.request.configCenter.namespace);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_USER_NAME", this.request.configCenter.username);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_PASSWORD", this.request.configCenter.password);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_ADDRESS", this.request.configCenter.address);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_TIMEOUT", this.request.configCenter.timeout);
this.stringProp("FIELD_DUBBO_REGISTRY_PROTOCOL", this.request.registryCenter.protocol);
this.stringProp("FIELD_DUBBO_REGISTRY_GROUP", this.request.registryCenter.group);
this.stringProp("FIELD_DUBBO_REGISTRY_USER_NAME", this.request.registryCenter.username);
this.stringProp("FIELD_DUBBO_REGISTRY_PASSWORD", this.request.registryCenter.password);
this.stringProp("FIELD_DUBBO_ADDRESS", this.request.registryCenter.address);
this.stringProp("FIELD_DUBBO_REGISTRY_TIMEOUT", this.request.registryCenter.timeout);
this.stringProp("FIELD_DUBBO_TIMEOUT", this.request.consumerAndService.timeout);
this.stringProp("FIELD_DUBBO_VERSION", this.request.consumerAndService.version);
this.stringProp("FIELD_DUBBO_RETRIES", this.request.consumerAndService.retries);
this.stringProp("FIELD_DUBBO_GROUP", this.request.consumerAndService.group);
this.stringProp("FIELD_DUBBO_CONNECTIONS", this.request.consumerAndService.connections);
this.stringProp("FIELD_DUBBO_LOADBALANCE", this.request.consumerAndService.loadBalance);
this.stringProp("FIELD_DUBBO_ASYNC", this.request.consumerAndService.async);
this.stringProp("FIELD_DUBBO_CLUSTER", this.request.consumerAndService.cluster);
this.stringProp("FIELD_DUBBO_RPC_PROTOCOL", this.request.protocol);
this.stringProp("FIELD_DUBBO_INTERFACE", this.request.interface);
this.stringProp("FIELD_DUBBO_METHOD", this.request.method);
this.intProp("FIELD_DUBBO_METHOD_ARGS_SIZE", this.request.args.length);
this.intProp("FIELD_DUBBO_ATTACHMENT_ARGS_SIZE", this.request.attachmentArgs.length);
this.request.args.forEach((arg, i) => {
if (!!arg.name || !!arg.value) {
let index = i + 1;
this.stringProp("FIELD_DUBBO_METHOD_ARGS_PARAM_TYPE" + index, arg.name);
this.stringProp("FIELD_DUBBO_METHOD_ARGS_PARAM_VALUE" + index, arg.value);
}
})
this.request.attachmentArgs.forEach((arg, i) => {
if (!!arg.name || !!arg.value) {
let index = i + 1;
this.stringProp("FIELD_DUBBO_ATTACHMENT_ARGS_KEY" + index, arg.name);
this.stringProp("FIELD_DUBBO_ATTACHMENT_ARGS_VALUE" + index, arg.value);
}
})
}
}
export class HTTPSamplerProxy extends DefaultTestElement { export class HTTPSamplerProxy extends DefaultTestElement {
constructor(testName, request) { constructor(testName, request) {
super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName); super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName);

View File

@ -12,7 +12,7 @@ import {
ResponseCodeAssertion, ResponseCodeAssertion,
ResponseDataAssertion, ResponseDataAssertion,
ResponseHeadersAssertion, ResponseHeadersAssertion,
RegexExtractor, JSONPostProcessor, XPath2Extractor, RegexExtractor, JSONPostProcessor, XPath2Extractor, DubboSample,
} from "./JMX"; } from "./JMX";
export const uuid = function () { export const uuid = function () {
@ -74,7 +74,7 @@ export class BaseConfig {
if (types) { if (types) {
for (let name in types) { for (let name in types) {
if (types.hasOwnProperty(name) && options.hasOwnProperty(name)) { if (types.hasOwnProperty(name) && options.hasOwnProperty(name)) {
options[name].forEach((o) => { options[name].forEach(o => {
this[name].push(new types[name](o)); this[name].push(new types[name](o));
}) })
} }
@ -95,7 +95,7 @@ export class Test extends BaseConfig {
constructor(options) { constructor(options) {
super(); super();
this.type = "MS API CONFIG"; this.type = "MS API CONFIG";
this.version = '1.0.0'; this.version = '1.1.0';
this.id = uuid(); this.id = uuid();
this.name = undefined; this.name = undefined;
this.projectId = undefined; this.projectId = undefined;
@ -140,7 +140,7 @@ export class Test extends BaseConfig {
} }
export class Scenario extends BaseConfig { export class Scenario extends BaseConfig {
constructor(options) { constructor(options = {}) {
super(); super();
this.name = undefined; this.name = undefined;
this.url = undefined; this.url = undefined;
@ -148,14 +148,20 @@ export class Scenario extends BaseConfig {
this.headers = []; this.headers = [];
this.requests = []; this.requests = [];
this.environmentId = undefined; this.environmentId = undefined;
this.dubboConfig = undefined;
this.set(options); this.set(options);
this.sets({variables: KeyValue, headers: KeyValue, requests: Request}, options); this.sets({variables: KeyValue, headers: KeyValue, requests: RequestFactory}, options);
} }
initOptions(options) { initOptions(options) {
options = options || {}; options = options || {};
options.requests = options.requests || [new Request()]; options.requests = options.requests || [new RequestFactory()];
options.dubboConfig = {
configCenter: new ConfigCenter(options.configCenter),
registryCenter: new RegistryCenter(options.configCenter),
consumerAndService: new ConsumerAndService(options.configCenter),
}
return options; return options;
} }
@ -173,9 +179,41 @@ export class Scenario extends BaseConfig {
} }
} }
export class RequestFactory {
static TYPES = {
HTTP: "HTTP",
DUBBO: "DUBBO",
}
constructor(options = {}) {
options.type = options.type || RequestFactory.TYPES.HTTP
switch (options.type) {
case RequestFactory.TYPES.DUBBO:
return new DubboRequest(options);
default:
return new HttpRequest(options);
}
}
}
export class Request extends BaseConfig { export class Request extends BaseConfig {
constructor(options) { constructor(type) {
super(); super();
this.type = type;
}
showType() {
return this.type;
}
showMethod() {
return "";
}
}
export class HttpRequest extends Request {
constructor(options) {
super(RequestFactory.TYPES.HTTP);
this.name = undefined; this.name = undefined;
this.url = undefined; this.url = undefined;
this.path = undefined; this.path = undefined;
@ -204,6 +242,119 @@ export class Request extends BaseConfig {
isValid() { isValid() {
return ((!this.useEnvironment && !!this.url) || (this.useEnvironment && !!this.path && this.environment)) && !!this.method return ((!this.useEnvironment && !!this.url) || (this.useEnvironment && !!this.path && this.environment)) && !!this.method
} }
showType() {
return this.type;
}
showMethod() {
return this.method.toUpperCase();
}
}
export class DubboRequest extends Request {
static PROTOCOLS = {
DUBBO: "dubbo://",
RMI: "rmi://",
}
constructor(options = {}) {
super(RequestFactory.TYPES.DUBBO);
this.name = options.name;
this.protocol = options.protocol || DubboRequest.PROTOCOLS.DUBBO;
this.interface = options.interface;
this.method = options.method;
this.configCenter = new ConfigCenter(options.configCenter);
this.registryCenter = new RegistryCenter(options.registryCenter);
this.consumerAndService = new ConsumerAndService(options.consumerAndService);
this.args = [];
this.attachmentArgs = [];
this.assertions = new Assertions(options.assertions);
this.extract = new Extract(options.extract);
this.sets({args: KeyValue, attachmentArgs: KeyValue}, options);
}
isValid() {
return !!this.name;
}
showType() {
return "RPC";
}
showMethod() {
// dubbo:// -> DUBBO
return this.protocol.substr(0, this.protocol.length - 3).toUpperCase();
}
clone() {
return new DubboRequest(this);
}
}
export class ConfigCenter extends BaseConfig {
static PROTOCOLS = ["zookeeper", "nacos", "apollo"];
constructor(options) {
super();
this.protocol = undefined;
this.group = undefined;
this.namespace = undefined;
this.username = undefined;
this.address = undefined;
this.password = undefined;
this.timeout = undefined;
this.set(options);
}
isValid() {
return !!this.protocol || !!this.group || !!this.namespace || !!this.username || !!this.address || !!this.password || !!this.timeout;
}
}
export class RegistryCenter extends BaseConfig {
static PROTOCOLS = ["none", "zookeeper", "nacos", "apollo", "multicast", "redis", "simple"];
constructor(options) {
super();
this.protocol = undefined;
this.group = undefined;
this.username = undefined;
this.address = undefined;
this.password = undefined;
this.timeout = undefined;
this.set(options);
}
isValid() {
return !!this.protocol || !!this.group || !!this.username || !!this.address || !!this.password || !!this.timeout;
}
}
export class ConsumerAndService extends BaseConfig {
static ASYNC_OPTIONS = ["sync", "async"];
static LOAD_BALANCE_OPTIONS = ["random", "roundrobin", "leastactive", "consistenthash"];
constructor(options) {
super();
this.timeout = "1000";
this.version = "1.0";
this.retries = "0";
this.cluster = "failfast";
this.group = undefined;
this.connections = "100";
this.async = "sync";
this.loadBalance = "random";
this.set(options);
}
isValid() {
return !!this.timeout || !!this.version || !!this.retries || !!this.cluster || !!this.group || !!this.connections || !!this.async || !!this.loadBalance;
}
} }
export class Body extends BaseConfig { export class Body extends BaseConfig {
@ -389,9 +540,9 @@ const JMX_ASSERTION_CONDITION = {
OR: 1 << 5 OR: 1 << 5
} }
class JMXRequest { class JMXHttpRequest {
constructor(request) { constructor(request) {
if (request && request instanceof Request && (request.url || request.path)) { if (request && request instanceof HttpRequest && (request.url || request.path)) {
this.useEnvironment = request.useEnvironment; this.useEnvironment = request.useEnvironment;
this.method = request.method; this.method = request.method;
if (!request.useEnvironment) { if (!request.useEnvironment) {
@ -421,7 +572,38 @@ class JMXRequest {
} }
return path; return path;
} }
}
class JMXDubboRequest {
constructor(request, dubboConfig) {
// Request 复制
let obj = request.clone();
// 去掉无效的kv
obj.args = obj.args.filter(arg => {
return arg.isValid();
});
obj.attachmentArgs = obj.attachmentArgs.filter(arg => {
return arg.isValid();
});
// Scenario DubboConfig复制需要Request中的内容无效才会复制
this.copy(obj.configCenter, dubboConfig.configCenter);
this.copy(obj.registryCenter, dubboConfig.registryCenter);
this.copy(obj.consumerAndService, dubboConfig.consumerAndService);
return obj;
}
copy(source, target) {
if (source.isValid()) return;
let keys = Object.keys(target);
keys.forEach(key => {
if (source[key] === undefined) {
source[key] = target[key];
}
})
}
} }
class JMeterTestPlan extends Element { class JMeterTestPlan extends Element {
@ -463,22 +645,27 @@ class JMXGenerator {
scenario.requests.forEach(request => { scenario.requests.forEach(request => {
if (!request.isValid()) return; if (!request.isValid()) return;
let sampler;
let httpSamplerProxy = new HTTPSamplerProxy(request.name || "", new JMXRequest(request)); if (request instanceof DubboRequest) {
sampler = new DubboSample(request.name || "", new JMXDubboRequest(request, scenario.dubboConfig));
this.addRequestHeader(httpSamplerProxy, request);
if (request.method.toUpperCase() === 'GET') {
this.addRequestArguments(httpSamplerProxy, request);
} else {
this.addRequestBody(httpSamplerProxy, request);
} }
this.addRequestAssertion(httpSamplerProxy, request); if (request instanceof HttpRequest) {
let sampler = new HTTPSamplerProxy(request.name || "", new JMXHttpRequest(request));
this.addRequestHeader(sampler, request);
if (request.method.toUpperCase() === 'GET') {
this.addRequestArguments(sampler, request);
} else {
this.addRequestBody(sampler, request);
}
}
this.addRequestExtractor(httpSamplerProxy, request); this.addRequestAssertion(sampler, request);
threadGroup.put(httpSamplerProxy); this.addRequestExtractor(sampler, request);
threadGroup.put(sampler);
}) })
testPlan.put(threadGroup); testPlan.put(threadGroup);

View File

@ -19,7 +19,7 @@ export const ZH_CN = 'zh_CN';
export const ZH_TW = 'zh_TW'; export const ZH_TW = 'zh_TW';
export const EN_US = 'en_US'; export const EN_US = 'en_US';
export const requestHeaders = [ export const REQUEST_HEADERS = [
{value: 'Accept'}, {value: 'Accept'},
{value: 'Accept-Charset'}, {value: 'Accept-Charset'},
{value: 'Accept-Language'}, {value: 'Accept-Language'},

View File

@ -344,6 +344,7 @@ export default {
please_save_test: "Please Save Test First", please_save_test: "Please Save Test First",
}, },
scenario: { scenario: {
dubbo: "Dubbo Config",
config: "Scenario Config", config: "Scenario Config",
input_name: "Please enter the scenario name", input_name: "Please enter the scenario name",
name: "Scenario Name", name: "Scenario Name",
@ -401,6 +402,9 @@ export default {
regex_expression: "Regular expression", regex_expression: "Regular expression",
json_path_expression: "JSONPath expression", json_path_expression: "JSONPath expression",
xpath_expression: "XPath expression", xpath_expression: "XPath expression",
},
dubbo: {
protocol: "protocol"
} }
}, },
api_import: { api_import: {

View File

@ -343,6 +343,7 @@ export default {
please_save_test: "请先保存测试", please_save_test: "请先保存测试",
}, },
scenario: { scenario: {
dubbo: "Dubbo配置",
config: "场景配置", config: "场景配置",
input_name: "请输入场景名称", input_name: "请输入场景名称",
name: "场景名称", name: "场景名称",
@ -400,6 +401,9 @@ export default {
regex_expression: "Perl型正则表达式", regex_expression: "Perl型正则表达式",
json_path_expression: "JSONPath表达式", json_path_expression: "JSONPath表达式",
xpath_expression: "XPath表达式", xpath_expression: "XPath表达式",
},
dubbo: {
protocol: "协议"
} }
}, },
api_import: { api_import: {

View File

@ -342,6 +342,7 @@ export default {
please_save_test: "請先保存測試", please_save_test: "請先保存測試",
}, },
scenario: { scenario: {
dubbo: "Dubbo配寘",
creator: "創建人", creator: "創建人",
config: "場景配寘", config: "場景配寘",
input_name: "請輸入場景名稱", input_name: "請輸入場景名稱",
@ -400,6 +401,9 @@ export default {
regex_expression: "Perl型規則運算式", regex_expression: "Perl型規則運算式",
json_path_expression: "JSONPath運算式", json_path_expression: "JSONPath運算式",
xpath_expression: "XPath運算式", xpath_expression: "XPath運算式",
},
dubbo: {
protocol: "協定"
} }
}, },
api_import: { api_import: {