feat(接口测试): 支持连接Mysql

This commit is contained in:
chenjianxing 2020-09-16 20:45:09 +08:00
parent 14531a571a
commit fccababb39
21 changed files with 726 additions and 445 deletions

View File

@ -1,11 +1,10 @@
package io.metersphere.base.domain;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.io.Serializable;
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@ -14,8 +13,9 @@ public class ApiTestEnvironmentWithBLOBs extends ApiTestEnvironment implements S
private String headers;
private String customData;
private String config;
private String hosts;
private static final long serialVersionUID = 1L;
}

View File

@ -13,7 +13,7 @@
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs">
<result column="variables" jdbcType="LONGVARCHAR" property="variables" />
<result column="headers" jdbcType="LONGVARCHAR" property="headers" />
<result column="custom_data" jdbcType="LONGVARCHAR" property="customData" />
<result column="config" jdbcType="LONGVARCHAR" property="config" />
<result column="hosts" jdbcType="LONGVARCHAR" property="hosts" />
</resultMap>
<sql id="Example_Where_Clause">
@ -78,7 +78,7 @@
id, `name`, project_id, protocol, socket, `domain`, port
</sql>
<sql id="Blob_Column_List">
`variables`, headers, custom_data,hosts
`variables`, headers, config, `hosts`
</sql>
<select id="selectByExampleWithBLOBs" parameterType="io.metersphere.base.domain.ApiTestEnvironmentExample" resultMap="ResultMapWithBLOBs">
select
@ -132,11 +132,11 @@
insert into api_test_environment (id, `name`, project_id,
protocol, socket, `domain`,
port, `variables`, headers,
custom_data,hosts)
config, `hosts`)
values (#{id,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{projectId,jdbcType=VARCHAR},
#{protocol,jdbcType=VARCHAR}, #{socket,jdbcType=VARCHAR}, #{domain,jdbcType=VARCHAR},
#{port,jdbcType=INTEGER}, #{variables,jdbcType=LONGVARCHAR}, #{headers,jdbcType=LONGVARCHAR},
#{customData,jdbcType=LONGVARCHAR},#{hosts,jdbcType=LONGVARCHAR})
#{config,jdbcType=LONGVARCHAR}, #{hosts,jdbcType=LONGVARCHAR})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs">
insert into api_test_environment
@ -168,13 +168,12 @@
<if test="headers != null">
headers,
</if>
<if test="customData != null">
custom_data,
<if test="config != null">
config,
</if>
<if test="hosts != null">
hosts,
`hosts`,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
@ -204,8 +203,8 @@
<if test="headers != null">
#{headers,jdbcType=LONGVARCHAR},
</if>
<if test="customData != null">
#{customData,jdbcType=LONGVARCHAR},
<if test="config != null">
#{config,jdbcType=LONGVARCHAR},
</if>
<if test="hosts != null">
#{hosts,jdbcType=LONGVARCHAR},
@ -248,11 +247,11 @@
<if test="record.headers != null">
headers = #{record.headers,jdbcType=LONGVARCHAR},
</if>
<if test="record.customData != null">
custom_data = #{record.customData,jdbcType=LONGVARCHAR},
<if test="record.config != null">
config = #{record.config,jdbcType=LONGVARCHAR},
</if>
<if test="record.hosts != null">
hosts = #{hosts,jdbcType=LONGVARCHAR},
`hosts` = #{record.hosts,jdbcType=LONGVARCHAR},
</if>
</set>
<if test="_parameter != null">
@ -270,8 +269,8 @@
port = #{record.port,jdbcType=INTEGER},
`variables` = #{record.variables,jdbcType=LONGVARCHAR},
headers = #{record.headers,jdbcType=LONGVARCHAR},
custom_data = #{record.customData,jdbcType=LONGVARCHAR},
hosts = #{hosts,jdbcType=LONGVARCHAR}
config = #{record.config,jdbcType=LONGVARCHAR},
`hosts` = #{record.hosts,jdbcType=LONGVARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
@ -316,11 +315,11 @@
<if test="headers != null">
headers = #{headers,jdbcType=LONGVARCHAR},
</if>
<if test="customData != null">
custom_data = #{customData,jdbcType=LONGVARCHAR},
<if test="config != null">
config = #{config,jdbcType=LONGVARCHAR},
</if>
<if test="hosts != null">
hosts = #{hosts,jdbcType=LONGVARCHAR},
`hosts` = #{hosts,jdbcType=LONGVARCHAR},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
@ -335,8 +334,8 @@
port = #{port,jdbcType=INTEGER},
`variables` = #{variables,jdbcType=LONGVARCHAR},
headers = #{headers,jdbcType=LONGVARCHAR},
custom_data = #{customData,jdbcType=LONGVARCHAR},
hosts = #{hosts,jdbcType=LONGVARCHAR}
config = #{config,jdbcType=LONGVARCHAR},
`hosts` = #{hosts,jdbcType=LONGVARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.base.domain.ApiTestEnvironment">

View File

@ -0,0 +1,4 @@
ALTER TABLE api_test_environment MODIFY COLUMN protocol varchar(20) NULL COMMENT 'Api Test Protocol';
ALTER TABLE api_test_environment MODIFY COLUMN socket varchar(225) NULL COMMENT 'Api Test Socket';
ALTER TABLE api_test_environment MODIFY COLUMN `domain` varchar(225) NULL COMMENT 'Api Test Domain';
ALTER TABLE api_test_environment CHANGE custom_data `config` longtext COMMENT 'Config Data (JSON format)';

View File

@ -21,6 +21,7 @@
import MsAsideItem from "../../../common/components/MsAsideItem";
import EnvironmentEdit from "./environment/EnvironmentEdit";
import {listenGoBack, removeGoBackListener} from "../../../../../common/js/utils";
import {Environment, parseEnvironment} from "../model/EnvironmentModel";
export default {
name: "ApiEnvironmentConfig",
@ -35,7 +36,7 @@
visible: false,
projectId: '',
environments: [],
currentEnvironment: {variables: [{}], headers: [{}], protocol: 'https', projectId: this.projectId, hosts: [{}]},
currentEnvironment: new Environment(),
environmentOperators: [
{
icon: 'el-icon-document-copy',
@ -68,7 +69,7 @@
},
copyEnvironment(environment) {
if (!environment.id) {
this.$warning(this.$t('commons.please_save'))
this.$warning(this.$t('commons.please_save'));
return;
}
let newEnvironment = {};
@ -102,7 +103,9 @@
return name;
},
addEnvironment() {
let newEnvironment = this.getDefaultEnvironment();
let newEnvironment = new Environment({
projectId: this.projectId
});
this.environments.push(newEnvironment);
this.$refs.environmentItems.itemSelected(this.environments.length - 1, newEnvironment);
},
@ -116,7 +119,9 @@
if (this.environments.length > 0) {
this.$refs.environmentItems.itemSelected(0, this.environments[0]);
} else {
let item = this.getDefaultEnvironment();
let item = new Environment({
projectId: this.projectId
});
this.environments.push(item);
this.$refs.environmentItems.itemSelected(0, item);
}
@ -124,25 +129,9 @@
}
},
getEnvironment(environment) {
if (!(environment.variables instanceof Array)) {
environment.variables = JSON.parse(environment.variables);
}
if (!(environment.headers instanceof Array)) {
environment.headers = JSON.parse(environment.headers);
}
if(environment.hosts === undefined || environment.hosts ===null || environment.hosts ===''){
environment.hosts = [];
environment.enable =false;
}
else if (!(environment.hosts instanceof Array)) {
environment.hosts = JSON.parse(environment.hosts);
environment.enable =true;
}
parseEnvironment(environment);
this.currentEnvironment = environment;
},
getDefaultEnvironment() {
return {variables: [{}], headers: [{}], protocol: 'https', projectId: this.projectId, hosts: [{}]};
},
close() {
this.$emit('close');
this.visible = false;

View File

@ -1,6 +1,5 @@
<template>
<div>
<el-card class="table-card">
<div class="ms-border">
<el-table :data="hostTable" style="width: 100%" @cell-dblclick="dblHostTable" class="ht-tb">
<el-table-column prop="ip" label="IP">
<template slot-scope="scope">
@ -36,12 +35,8 @@
</el-table-column>
</el-table>
<el-button class="ht-btn-add" size="mini" p="$t('commons.add')" icon="el-icon-circle-plus-outline" @click="add"
>添加
<el-button class="ht-btn-add" size="mini" p="$t('commons.add')" icon="el-icon-circle-plus-outline" @click="add">添加
</el-button>
</el-card>
</div>
</template>

View File

@ -75,6 +75,7 @@ import MsApiScenarioForm from "./ApiScenarioForm";
import {Request, Scenario} from "../model/ScenarioModel";
import draggable from 'vuedraggable';
import MsApiScenarioSelect from "@/business/components/api/test/components/ApiScenarioSelect";
import {compatibleWithEnvironment, Config, parseEnvironment} from "../model/EnvironmentModel";
export default {
name: "MsApiScenarioConfig",
@ -203,6 +204,7 @@ export default {
let environments = response.data;
let environmentMap = new Map();
environments.forEach(environment => {
parseEnvironment(environment);
environmentMap.set(environment.id, environment);
});
this.scenarios.forEach(scenario => {

View File

@ -37,7 +37,7 @@
:environment="scenario.environment"
:description="$t('api_test.scenario.kv_description')"/>
</el-tab-pane>
<el-tab-pane :label="'数据库配置'" name="database">
<el-tab-pane :label="$t('api_test.environment.database_config')" name="database">
<ms-database-config :configs="scenario.databaseConfigs"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.scenario.dubbo')" name="dubbo">
@ -66,6 +66,7 @@ import MsDubboRegistryCenter from "@/business/components/api/test/components/req
import MsDubboConfigCenter from "@/business/components/api/test/components/request/dubbo/ConfigCenter";
import MsDubboConsumerService from "@/business/components/api/test/components/request/dubbo/ConsumerAndService";
import MsDatabaseConfig from "./request/database/DatabaseConfig";
import {parseEnvironment} from "../model/EnvironmentModel";
export default {
name: "MsApiScenarioForm",
@ -111,6 +112,9 @@ export default {
if (this.projectId) {
this.result = this.$get('/api/environment/list/' + this.projectId, response => {
this.environments = response.data;
this.environments.forEach(environment => {
parseEnvironment(environment);
});
let hasEnvironment = false;
for (let i in this.environments) {
if (this.environments[i].id === this.scenario.environmentId) {

View File

@ -0,0 +1,63 @@
<template>
<div>
<el-form :model="commonConfig" :rules="rules" ref="commonConfig">
<span>{{$t('api_test.environment.globalVariable')}}</span>
<ms-api-scenario-variables :items="commonConfig.variables"/>
<el-form-item>
<el-switch v-model="commonConfig.enableHost" active-text="Hosts"/>
</el-form-item>
<ms-api-host-table v-if="commonConfig.enableHost" :hostTable="commonConfig.hosts" ref="refHostTable"/>
</el-form>
</div>
</template>
<script>
import {CommonConfig, Environment} from "../../model/EnvironmentModel";
import MsApiScenarioVariables from "../ApiScenarioVariables";
import MsApiHostTable from "../ApiHostTable";
export default {
name: "MsEnvironmentCommonConfig",
components: {MsApiHostTable, MsApiScenarioVariables},
props: {
commonConfig: new CommonConfig(),
},
data() {
return {
rules: {
},
}
},
methods: {
validate() {
let isValidate = false;
this.$refs['commonConfig'].validate((valid) => {
if (valid) {
// host
let valHost = true;
if (this.commonConfig.enableHost) {
for (let i = 0; i < this.commonConfig.hosts.length; i++) {
valHost = this.$refs['refHostTable'].confirm(this.commonConfig.hosts[i]);
}
}
if (valHost) {
isValidate = true;
} else {
isValidate = false;
}
} else {
isValidate = false;
}
});
return isValidate;
}
}
}
</script>
<style scoped>
</style>

View File

@ -3,37 +3,24 @@
<el-form :model="environment" :rules="rules" ref="environment">
<span>{{$t('api_test.environment.name')}}</span>
<el-form-item
prop="name">
<el-input v-model="environment.name" :placeholder="this.$t('commons.input_name')" clearable></el-input>
</el-form-item>
<span>{{$t('api_test.environment.socket')}}</span>
<el-form-item
prop="socket">
<el-input v-model="environment.socket" :placeholder="$t('api_test.request.url_description')" clearable>
<template v-slot:prepend>
<el-select v-model="environment.protocol" class="request-protocol-select">
<el-option label="http://" value="http"/>
<el-option label="https://" value="https"/>
</el-select>
</template>
</el-input>
<el-form-item prop="name">
<el-input v-model="environment.name" :placeholder="this.$t('commons.input_name')" clearable/>
</el-form-item>
<el-form-item>
<el-switch
v-model="envEnable"
inactive-text="hosts">
</el-switch>
</el-form-item>
<ms-api-host-table v-if="envEnable" :hostTable="environment.hosts" ref="refHostTable"/>
<el-tabs v-model="activeName">
<span>{{$t('api_test.environment.globalVariable')}}</span>
<ms-api-scenario-variables :show-variable="false" :items="environment.variables"/>
<el-tab-pane :label="$t('api_test.environment.common_config')" name="common">
<ms-environment-common-config :common-config="environment.config.commonConfig" ref="commonConfig"/>
</el-tab-pane>
<span>{{$t('api_test.request.headers')}}</span>
<ms-api-key-value :items="environment.headers" :suggestions="headerSuggestions"/>
<el-tab-pane :label="$t('api_test.environment.http_config')" name="http">
<ms-environment-http-config :http-config="environment.config.httpConfig" ref="httpConfig"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.database_config')" name="sql">
<ms-database-config :configs="environment.config.databaseConfigs"/>
</el-tab-pane>
</el-tabs>
<div class="environment-footer">
<ms-dialog-footer
@ -49,23 +36,23 @@
import MsApiKeyValue from "../ApiKeyValue";
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
import {REQUEST_HEADERS} from "../../../../../../common/js/constants";
import {KeyValue} from "../../model/ScenarioModel";
import {Environment} from "../../model/EnvironmentModel";
import MsApiHostTable from "../ApiHostTable";
import MsDatabaseConfig from "../request/database/DatabaseConfig";
import MsEnvironmentHttpConfig from "./EnvironmentHttpConfig";
import MsEnvironmentCommonConfig from "./EnvironmentCommonConfig";
export default {
name: "EnvironmentEdit",
components: {MsApiHostTable, MsDialogFooter, MsApiKeyValue, MsApiScenarioVariables},
components: {
MsEnvironmentCommonConfig,
MsEnvironmentHttpConfig,
MsDatabaseConfig, MsApiHostTable, MsDialogFooter, MsApiKeyValue, MsApiScenarioVariables},
props: {
environment: Object,
environment: new Environment(),
},
data() {
let socketValidator = (rule, value, callback) => {
if (!this.validateSocket(value)) {
callback(new Error(this.$t('commons.formatErr')));
} else {
callback();
}
}
return {
result: {},
envEnable: false,
@ -74,9 +61,9 @@
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
{max: 64, message: this.$t('commons.input_limit', [1, 64]), trigger: 'blur'}
],
socket: [{required: true, validator: socketValidator, trigger: 'blur'}],
},
headerSuggestions: REQUEST_HEADERS
headerSuggestions: REQUEST_HEADERS,
activeName: 'common'
}
},
watch: {
@ -87,20 +74,10 @@
methods: {
save() {
this.$refs['environment'].validate((valid) => {
// host
let valHost = true;
if (this.envEnable) {
for (let i = 0; i < this.environment.hosts.length; i++) {
valHost = this.$refs['refHostTable'].confirm(this.environment.hosts[i]);
}
}
if (valid && valHost) {
if (valid && this.$refs.commonConfig.validate() && this.$refs.httpConfig.validate()) {
this._save(this.environment);
} else {
return false;
}
});
},
_save(environment) {
let param = this.buildParam(environment);
@ -115,17 +92,11 @@
this.$success(this.$t('commons.save_success'));
});
},
buildParam(environment) {
buildParam: function (environment) {
let param = {};
Object.assign(param, environment);
if (!(environment.variables instanceof String)) {
param.variables = JSON.stringify(environment.variables);
}
if (!(environment.headers instanceof String)) {
param.headers = JSON.stringify(environment.headers);
}
if (environment.hosts != undefined && !(environment.hosts instanceof String)) {
let hosts = JSON.parse(JSON.stringify(environment.hosts));
let hosts = param.config.commonConfig.hosts;
if (hosts != undefined) {
let validHosts = [];
// host
hosts.forEach(host => {
@ -133,33 +104,11 @@
validHosts.push(host);
}
});
environment.hosts = validHosts;
param.hosts = JSON.stringify(validHosts);
}
if (!this.envEnable) {
param.hosts = null;
param.config.commonConfig.hosts = validHosts;
}
param.config = JSON.stringify(param.config);
return param;
},
validateSocket(socket) {
if (!socket) return;
let urlStr = this.environment.protocol + '://' + socket;
let url = {};
try {
url = new URL(urlStr);
} catch (e) {
return false
}
this.environment.port = url.port;
this.environment.domain = decodeURIComponent(url.hostname);
if (url.port) {
this.environment.socket = this.environment.domain + ':' + url.port + url.pathname;
} else {
this.environment.socket = this.environment.domain + url.pathname;
}
return true;
},
cancel() {
this.$emit('close');
},
@ -175,10 +124,9 @@
.el-main {
border: solid 1px #EBEEF5;
margin-left: 200px;
}
min-height: 400px;
max-height: 700px;
.request-protocol-select {
width: 90px;
}
.el-row {

View File

@ -0,0 +1,85 @@
<template>
<el-form :model="httpConfig" :rules="rules" ref="httpConfig">
<span>{{$t('api_test.environment.socket')}}</span>
<el-form-item prop="socket">
<el-input v-model="httpConfig.socket" :placeholder="$t('api_test.request.url_description')" clearable>
<template v-slot:prepend>
<el-select v-model="httpConfig.protocol" class="request-protocol-select">
<el-option label="http://" value="http"/>
<el-option label="https://" value="https"/>
</el-select>
</template>
</el-input>
</el-form-item>
<span>{{$t('api_test.request.headers')}}</span>
<ms-api-key-value :items="httpConfig.headers" :isShowEnable="true" :suggestions="headerSuggestions"/>
</el-form>
</template>
<script>
import {HttpConfig} from "../../model/EnvironmentModel";
import MsApiKeyValue from "../ApiKeyValue";
import {REQUEST_HEADERS} from "../../../../../../common/js/constants";
export default {
name: "MsEnvironmentHttpConfig",
components: {MsApiKeyValue},
props: {
httpConfig: new HttpConfig(),
},
data() {
let socketValidator = (rule, value, callback) => {
if (!this.validateSocket(value)) {
callback(new Error(this.$t('commons.formatErr')));
return false;
} else {
callback();
return true;
}
}
return {
headerSuggestions: REQUEST_HEADERS,
rules: {
socket: [{required: false, validator: socketValidator, trigger: 'blur'}],
},
}
},
methods: {
validateSocket(socket) {
if (!socket) return true;
let urlStr = this.httpConfig.protocol + '://' + socket;
let url = {};
try {
url = new URL(urlStr);
} catch (e) {
return false;
}
this.httpConfig.domain = decodeURIComponent(url.hostname);
this.httpConfig.port = url.port;
if (url.port) {
this.httpConfig.socket = this.httpConfig.domain + ':' + url.port + url.pathname;
} else {
this.httpConfig.socket = this.httpConfig.domain + url.pathname;
}
return true;
},
validate() {
let isValidate = false;
this.$refs['httpConfig'].validate((valid) => {
isValidate = valid;
});
return isValidate;
}
}
}
</script>
<style scoped>
.request-protocol-select {
width: 90px;
}
</style>

View File

@ -5,9 +5,9 @@
<el-input v-model="request.name" maxlength="300" show-word-limit/>
</el-form-item>
<el-form-item :label="'连接池'" prop="dataSource">
<el-form-item :label="$t('api_test.request.sql.dataSource')" prop="dataSource">
<el-select v-model="request.dataSource">
<el-option v-for="(item, index) in scenario.databaseConfigs" :key="index" :value="item.name" :label="item.name"/>
<el-option v-for="(item, index) in databaseConfigsOptions" :key="index" :value="item.id" :label="item.name"/>
</el-select>
</el-form-item>
@ -17,14 +17,21 @@
<!--</el-select>-->
<!--</el-form-item>-->
<el-form-item :label="'超时时间'" prop="queryTimeout">
<el-form-item :label="$t('api_test.request.sql.timeout')" prop="queryTimeout">
<el-input-number :disabled="isReadOnly" size="mini" v-model="request.queryTimeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
</el-form-item>
<el-form-item>
<el-switch
v-model="request.useEnvironment"
:active-text="$t('api_test.request.refer_to_environment')" @change="getDatabaseConfigsOptions">
</el-switch>
</el-form-item>
<el-button :disabled="!request.enable || !scenario.enable || isReadOnly" class="debug-button" size="small" type="primary" @click="runDebug">{{$t('api_test.request.debug')}}</el-button>
<el-tabs v-model="activeName">
<el-tab-pane :label="'sql脚本'" name="sql">
<el-tab-pane :label="$t('api_test.request.sql.sql_script')" name="sql">
<div class="sql-content" >
<ms-code-edit mode="sql" :read-only="isReadOnly" :modes="['sql']" :data.sync="request.query" theme="eclipse" ref="codeEdit"/>
</div>
@ -80,6 +87,7 @@
data() {
return {
activeName: "sql",
databaseConfigsOptions: [],
rules: {
name: [
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
@ -93,19 +101,38 @@
},
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;
getDatabaseConfigsOptions() {
this.databaseConfigsOptions = [];
let names = new Set();
let ids = new Set();
this.scenario.databaseConfigs.forEach(config => {
this.databaseConfigsOptions.push(config);
names.add(config.name);
ids.add(config.id);
});
if (this.request.useEnvironment && this.scenario.environment) {
this.scenario.environment.config.databaseConfigs.forEach(config => {
if (!names.has(config.name)) {
this.databaseConfigsOptions.push(config);
ids.add(config.id);
}
});
}
if (!ids.has(this.request.dataSource)) {
this.request.dataSource = undefined;
}
this.$refs["request"].clearValidate();
},
runDebug() {
this.$emit('runDebug');
}
},
computed: {}
created() {
this.getDatabaseConfigsOptions();
},
activated() {
this.getDatabaseConfigsOptions();
}
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div>
<ms-database-from :config="currentConfig" @save="addConfig" ref="databaseFrom"/>
<ms-database-from :config="currentConfig" :callback="addConfig" ref="databaseFrom"/>
<ms-database-config-list v-if="configs.length > 0" :table-data="configs"/>
</div>
</template>
@ -31,14 +31,16 @@
addConfig(config) {
for (let item of this.configs) {
if (item.name === config.name) {
this.$warning("名称重复");
this.$warning(this.$t('commons.already_exists'));
return;
}
}
config.id = getUUID();
this.configs.push(config);
let item = {};
Object.assign(item, config);
this.configs.push(item);
this.currentConfig = new DatabaseConfig();
}
},
}
}
</script>

View File

@ -1,58 +0,0 @@
<template>
<el-dialog :title="'数据库配置'" :visible.sync="visible">
<ms-database-from :config="config" @save="editConfig"/>
</el-dialog>
</template>
<script>
import MsDatabaseConfigList from "./DatabaseConfigList";
import MsDatabaseFrom from "./DatabaseFrom";
import {DatabaseConfig} from "../../../model/ScenarioModel";
export default {
name: "MsDatabaseConfigDialog",
components: {MsDatabaseFrom, MsDatabaseConfigList},
props: {
configs: Array,
isReadOnly: {
type: Boolean,
default: false
},
},
data() {
return {
visible: false,
config: new DatabaseConfig(),
}
},
methods: {
open(config) {
this.visible = true;
Object.assign(this.config, config);
},
editConfig(config) {
let currentConfig = undefined;
for (let item of this.configs) {
if (item.name === config.name && item.id != config.id) {
this.$warning("名称重复");
return;
}
if (item.id === config.id) {
currentConfig = item;
}
}
if (currentConfig) {
Object.assign(currentConfig, config)
} else {
//copy
this.configs.push(config);
}
this.visible = false;
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,49 +1,44 @@
<template>
<ms-main-container>
<div class="database-config-list">
<el-table border :data="tableData" class="adjust-table table-content"
@row-click="handleView">
<el-table-column prop="name" :label="'连接池名称'" show-overflow-tooltip/>
<el-table-column prop="driver" :label="'数据库驱动'" show-overflow-tooltip/>
<el-table-column prop="dbUrl" :label="'数据库连接URL'" show-overflow-tooltip/>
<el-table-column prop="username" :label="'用户名'" show-overflow-tooltip/>
<el-table-column prop="poolMax" :label="'最大连接数'" show-overflow-tooltip/>
<el-table-column prop="timeout" :label="'最大等待时间'" show-overflow-tooltip/>
<el-table-column
:label="$t('commons.operating')" min-width="100">
<template v-slot:default="scope">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)"
@deleteClick="handleDelete(scope.$index)">
<template v-slot:middle>
<ms-table-operator-button :is-tester-permission="true" :tip="$t('commons.copy')"
icon="el-icon-document-copy"
type="success" @exec="handleCopy(scope.row)"/>
<el-table-column type="expand">
<template slot-scope="props">
<ms-database-from :callback="editConfig" :config="props.row"/>
</template>
</ms-table-operator>
</el-table-column>
<el-table-column prop="name" :label="$t('api_test.request.sql.dataSource')" show-overflow-tooltip/>
<el-table-column prop="driver" :label="$t('api_test.request.sql.database_driver')" show-overflow-tooltip/>
<el-table-column prop="dbUrl" :label="$t('api_test.request.sql.database_url')" show-overflow-tooltip/>
<el-table-column prop="username" :label="$t('api_test.request.sql.username')" show-overflow-tooltip/>
<el-table-column prop="poolMax" :label="$t('api_test.request.sql.pool_max')" show-overflow-tooltip/>
<el-table-column prop="timeout" :label="$t('api_test.request.sql.query_timeout')" show-overflow-tooltip/>
<el-table-column :label="$t('commons.operating')" min-width="100">
<template v-slot:default="scope">
<ms-table-operator-button :is-tester-permission="true" :tip="$t('commons.copy')" icon="el-icon-document-copy" type="success" @exec="handleCopy(scope.$index, scope.row)"/>
<ms-table-operator-button :isTesterPermission="true" :tip="$t('commons.delete')" icon="el-icon-delete" type="danger" @exec="handleDelete(scope.$index)"/>
</template>
</el-table-column>
</el-table>
<ms-database-config-dialog :configs="tableData" ref="databaseConfigEdit"/>
</ms-main-container>
</div>
</template>
<script>
import {DatabaseConfig} from "../../../model/ScenarioModel";
import MsMainContainer from "../../../../../common/components/MsMainContainer";
import MsTableOperator from "../../../../../common/components/MsTableOperator";
import MsTableOperatorButton from "../../../../../common/components/MsTableOperatorButton";
import MsDatabaseConfigDialog from "./DatabaseConfigDialog";
import {getUUID} from "../../../../../../../common/js/utils";
import MsDatabaseFrom from "./DatabaseFrom";
export default {
name: "MsDatabaseConfigList",
components: {MsDatabaseConfigDialog, MsTableOperatorButton, MsTableOperator, MsMainContainer},
components: {MsDatabaseFrom, MsTableOperatorButton, MsTableOperator},
props: {
tableData: Array,
isReadOnly: {
@ -63,16 +58,40 @@
handleEdit(config) {
this.$refs.databaseConfigEdit.open(config);
},
editConfig(config) {
let index = 0;
for (let i in this.tableData) {
let item = this.tableData[i];
if (item.name === config.name && item.id != config.id) {
this.$warning(this.$t('commons.already_exists'));
return;
}
if (item.id === config.id) {
index = i;
}
}
Object.assign(this.tableData[index], config);
this.$success(this.$t('commons.save_success'));
},
handleDelete(index) {
this.tableData.splice(index, 1);
},
handleCopy(config) {
handleCopy(index, config) {
let copy = {};
Object.assign(copy, config);
copy.id = getUUID();
this.$refs.databaseConfigEdit.open(copy);
copy.name = this.getNoRepeatName(copy.name);
this.tableData.splice(index + 1, 0, copy);
},
getNoRepeatName(name) {
for (let i in this.tableData) {
if (this.tableData[i].name === name) {
return this.getNoRepeatName(name + ' copy');
}
}
return name;
},
}
}
</script>
@ -82,11 +101,4 @@
float: right;
}
.database-from {
padding: 10px;
border: #DCDFE6 solid 1px;
margin: 5px 0;
border-radius: 5px;
}
</style>

View File

@ -1,44 +1,44 @@
<template>
<div>
<el-form :model="config" :rules="rules" label-width="150px" size="small" :disabled="isReadOnly" class="database-from" ref="databaseFrom">
<div class="database-from">
<el-form :model="currentConfig" :rules="rules" label-width="150px" size="small" :disabled="isReadOnly" ref="databaseFrom">
<el-form-item :label="'连接池名称'" prop="name">
<el-input v-model="config.name" maxlength="300" show-word-limit
<el-form-item :label="$t('api_test.request.sql.dataSource')" prop="name">
<el-input v-model="currentConfig.name" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item :label="'数据库连接URL'" prop="dbUrl">
<el-input v-model="config.dbUrl" maxlength="500" show-word-limit
<el-form-item :label="$t('api_test.request.sql.database_url')" prop="dbUrl">
<el-input v-model="currentConfig.dbUrl" maxlength="500" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item :label="'数据库驱动'" prop="driver">
<el-select v-model="config.driver" class="select-100" clearable>
<el-form-item :label="$t('api_test.request.sql.database_driver')" prop="driver">
<el-select v-model="currentConfig.driver" class="select-100" clearable>
<el-option v-for="p in drivers" :key="p" :label="p" :value="p"/>
</el-select>
</el-form-item>
<el-form-item :label="'用户名'" prop="username">
<el-input v-model="config.username" maxlength="300" show-word-limit
<el-form-item :label="$t('api_test.request.sql.username')" prop="username">
<el-input v-model="currentConfig.username" maxlength="300" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item :label="'密码'" prop="password">
<el-input v-model="config.password" maxlength="200" show-word-limit
<el-form-item :label="$t('api_test.request.sql.password')" prop="password">
<el-input v-model="currentConfig.password" maxlength="200" show-word-limit
:placeholder="$t('commons.input_content')"/>
</el-form-item>
<el-form-item :label="'最大连接数'" prop="poolMax">
<el-input-number size="small" :disabled="isReadOnly" v-model="config.poolMax" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
<el-form-item :label="$t('api_test.request.sql.pool_max')" prop="poolMax">
<el-input-number size="small" :disabled="isReadOnly" v-model="currentConfig.poolMax" :placeholder="$t('commons.please_select')" :max="1000*10000000" :min="0"/>
</el-form-item>
<el-form-item :label="'最大等待时间(ms)'" prop="timeout">
<el-input-number size="small" :disabled="isReadOnly" v-model="config.timeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
<el-form-item :label="$t('api_test.request.sql.timeout')" prop="timeout">
<el-input-number size="small" :disabled="isReadOnly" v-model="currentConfig.timeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" class="addButton" @click="save">添加</el-button>
<el-button type="primary" size="small" class="addButton" @click="save">{{currentConfig.id ? $t('commons.save') : $t('commons.add')}}</el-button>
</el-form-item>
</el-form>
@ -63,10 +63,22 @@
return new DatabaseConfig();
}
},
callback: {
type: Function
},
},
watch: {
config() {
Object.assign(this.currentConfig, this.config);
}
},
mounted() {
Object.assign(this.currentConfig, this.config);
},
data() {
return {
drivers: DatabaseConfig.DRIVER_CLASS,
currentConfig: new DatabaseConfig(),
rules: {
name: [
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
@ -93,7 +105,9 @@
save() {
this.$refs['databaseFrom'].validate((valid) => {
if (valid) {
this.$emit('save', this.config);
if (this.callback) {
this.callback(this.currentConfig);
}
} else {
return false;
}
@ -109,11 +123,4 @@
float: right;
}
.database-from {
padding: 10px;
border: #DCDFE6 solid 1px;
margin: 5px 0;
border-radius: 5px;
}
</style>

View File

@ -0,0 +1,121 @@
import {BaseConfig, DatabaseConfig, KeyValue} from "./ScenarioModel";
export class Environment extends BaseConfig {
constructor(options = {}) {
super();
this.projectId = undefined;
this.name = undefined;
this.id = undefined;
this.config = options.config || new Config();
this.set(options);
this.sets({}, options);
}
initOptions(options = {}) {
return options;
}
}
export class Config extends BaseConfig {
constructor(options = {}) {
super();
this.commonConfig = options.commonConfig || new CommonConfig();
this.httpConfig = options.httpConfig || new HttpConfig();
this.databaseConfigs = [];
this.set(options);
this.sets({databaseConfigs: DatabaseConfig}, options);
}
initOptions(options = {}) {
options.databaseConfigs = options.databaseConfigs || [];
return options;
}
}
export class CommonConfig extends BaseConfig {
constructor(options = {}) {
super();
this.variables = [];
this.enableHost = false;
this.hosts = [];
this.set(options);
this.sets({variables: KeyValue, hosts: Host}, options);
}
initOptions(options = {}) {
options.variables = options.variables || [new KeyValue()];
options.hosts = options.hosts || [];
return options;
}
}
export class HttpConfig extends BaseConfig {
constructor(options = {}) {
super();
this.socket = undefined;
this.domain = undefined;
this.headers = [];
this.protocol = 'https';
this.port = undefined;
this.set(options);
this.sets({headers: KeyValue}, options);
}
initOptions(options = {}) {
options.headers = options.headers || [new KeyValue()];
return options;
}
}
export class Host extends BaseConfig {
constructor(options = {}) {
super();
this.ip = undefined;
this.domain = undefined;
this.status = undefined;
this.annotation = undefined;
this.uuid = undefined;
this.set(options);
}
}
/* ---------- Functions ------- */
export function compatibleWithEnvironment(environment) {
//兼容旧版本
if (!environment.config) {
let config = new Config();
if (!(environment.variables instanceof Array)) {
config.commonConfig.variables = JSON.parse(environment.variables);
}
if (environment.hosts && !(environment.hosts instanceof Array)) {
config.commonConfig.hosts = JSON.parse(environment.hosts);
config.commonConfig.enableHost = true;
}
if (!(environment.headers instanceof Array)) {
config.httpConfig.headers = JSON.parse(environment.headers);
}
config.httpConfig.port = environment.port;
config.httpConfig.protocol = environment.protocol;
config.httpConfig.domain = environment.domain;
config.httpConfig.socket = environment.socket;
environment.config = JSON.stringify(config);
}
}
export function parseEnvironment(environment) {
compatibleWithEnvironment(environment);
if (!(environment.config instanceof Config)) {
environment.config = new Config(JSON.parse(environment.config));
}
}

View File

@ -469,10 +469,12 @@ export class SqlRequest extends Request {
super(RequestFactory.TYPES.SQL);
this.id = options.id || uuid();
this.name = options.name;
this.useEnvironment = options.useEnvironment;
this.debugReport = undefined;
this.dataSource = options.dataSource;
this.query = options.query;
// this.queryType = options.queryType;
this.queryTimeout = options.queryTimeout;
this.queryTimeout = options.queryTimeout || 60000;
this.enable = options.enable === undefined ? true : options.enable;
this.assertions = new Assertions(options.assertions);
this.extract = new Extract(options.extract);
@ -488,13 +490,13 @@ export class SqlRequest extends Request {
if (!this.name) {
return {
isValid: false,
info: 'name'
info: 'api_test.request.sql.name_cannot_be_empty'
}
}
if (!this.dataSource) {
return {
isValid: false,
info: 'dataSource'
info: 'api_test.request.sql.dataSource_cannot_be_empty'
}
}
}
@ -858,10 +860,10 @@ class JMXHttpRequest {
this.protocol = url.protocol.split(":")[0];
this.path = this.getPostQueryParameters(request, decodeURIComponent(url.pathname));
} else {
this.domain = environment.domain;
this.port = environment.port;
this.protocol = environment.protocol;
let url = new URL(environment.protocol + "://" + environment.socket);
this.domain = environment.config.httpConfig.domain;
this.port = environment.config.httpConfig.port;
this.protocol = environment.config.httpConfig.protocol;
let url = new URL(environment.config.httpConfig.protocol + "://" + environment.config.commonConfig.socket);
this.path = this.getPostQueryParameters(request, decodeURIComponent(url.pathname + (request.path ? request.path : '')));
}
this.connectTimeout = request.connectTimeout;
@ -968,7 +970,7 @@ class JMXGenerator {
// 放在计划或线程组中,不建议放具体某个请求中
this.addDNSCacheManager(threadGroup, scenario.requests[0]);
this.addJDBCDataSource(threadGroup, scenario);
this.addJDBCDataSources(threadGroup, scenario);
scenario.requests.forEach(request => {
if (request.enable) {
@ -986,6 +988,7 @@ class JMXGenerator {
this.addRequestBody(sampler, request, testId);
}
} else if (request instanceof SqlRequest) {
request.dataSource = scenario.databaseConfigMap.get(request.dataSource);
sampler = new JDBCSampler(request.name || "", request);
}
@ -1013,22 +1016,22 @@ class JMXGenerator {
let envArray = environments;
if (!(envArray instanceof Array)) {
envArray = JSON.parse(environments);
}
envArray.forEach(item => {
if (item.name && !keys.has(item.name)) {
target.push(new KeyValue(item.name, item.value));
}
})
}
}
addScenarioVariables(threadGroup, scenario) {
let environment = scenario.environment;
if (environment) {
this.addEnvironments(environment.variables, scenario.variables)
if (scenario.environment) {
let commonConfig = scenario.environment.config.commonConfig;
this.addEnvironments(commonConfig.variables, scenario.variables)
}
let args = this.filterKV(scenario.variables);
if (args.length > 0) {
let name = scenario.name + " Variables"
let name = scenario.name + " Variables";
threadGroup.put(new Arguments(name, args));
}
}
@ -1040,31 +1043,49 @@ class JMXGenerator {
}
addDNSCacheManager(threadGroup, request) {
if (request.environment && request.environment.hosts) {
if (request.environment) {
let commonConfig = request.environment.config.commonConfig;
let hosts = commonConfig.hosts;
if (commonConfig.enableHost) {
let name = request.name + " DNSCacheManager";
let hosts = JSON.parse(request.environment.hosts);
if (hosts.length > 0) {
//let domain = request.environment.protocol + "://" + request.environment.domain;
threadGroup.put(new DNSCacheManager(name, request.environment.domain, hosts));
threadGroup.put(new DNSCacheManager(name, request.environment.config.httpConfig.domain, hosts));
}
}
}
}
addJDBCDataSource(threadGroup, scenario) {
addJDBCDataSources(threadGroup, scenario) {
let names = new Set();
let databaseConfigMap = new Map();
scenario.databaseConfigs.forEach(config => {
let name = config.name + "JDBCDataSource";
threadGroup.put(new JDBCDataSource(name, config));
names.add(name);
databaseConfigMap.set(config.id, config.name);
});
if (scenario.environment) {
let envDatabaseConfigs = scenario.environment.config.databaseConfigs;
envDatabaseConfigs.forEach(config => {
if (!names.has(config.name)) {
let name = config.name + "JDBCDataSource";
threadGroup.put(new JDBCDataSource(name, config));
databaseConfigMap.set(config.id, config.name);
}
});
}
scenario.databaseConfigMap = databaseConfigMap;
}
addScenarioHeaders(threadGroup, scenario) {
let environment = scenario.environment;
if (environment) {
this.addEnvironments(environment.headers, scenario.headers)
if (scenario.environment) {
let httpConfig = scenario.environment.config.httpConfig;
this.addEnvironments(httpConfig.headers, scenario.headers)
}
let headers = this.filterKV(scenario.headers);
if (headers.length > 0) {
let name = scenario.name + " Headers"
let name = scenario.name + " Headers";
threadGroup.put(new HeaderManager(name, headers));
}
}

View File

@ -94,3 +94,10 @@ body {
border: 1px solid #409EFF;
}
/* 表格 input 编辑效果 --> */
.ms-border {
padding: 10px;
border: #DCDFE6 solid 1px;
margin: 5px 0;
border-radius: 5px;
}

View File

@ -114,6 +114,7 @@ export default {
millisecond: 'ms',
please_upload: 'Please upload file',
reference_documentation: "Reference documentation",
already_exists: 'The name already exists',
date: {
select_date: 'Select date',
start_date: 'Start date',
@ -415,6 +416,9 @@ export default {
environment: "Environment",
select_environment: "Please select environment",
please_save_test: "Please Save Test First",
common_config: "Common Config",
http_config: "HTTP Config",
database_config: "Database Config",
},
scenario: {
scenario: "Scenario",
@ -530,6 +534,17 @@ export default {
check_registry_center: "Can't get interface list, please check the registry center",
}
},
sql: {
dataSource: "Data Source",
sql_script: "Sql Script",
timeout: "Timeout(ms)",
database_driver: "Driver",
database_url: "Database URL",
username: "Username",
password: "Password",
pool_max: "Max Number of Configuration",
query_timeout: "Max Wait(ms)",
},
api_import: {
label: "Import",
title: "API test import",

View File

@ -114,6 +114,7 @@ export default {
id: 'ID',
millisecond: '毫秒',
cannot_be_null: '不能为空',
already_exists: '名称不能重复',
date: {
select_date: '选择日期',
start_date: '开始日期',
@ -416,6 +417,9 @@ export default {
environment: "环境",
select_environment: "请选择环境",
please_save_test: "请先保存测试",
common_config: "通用配置",
http_config: "HTTP配置",
database_config: "数据库配置",
},
scenario: {
scenario: "场景",
@ -531,6 +535,20 @@ export default {
check_registry_center: "获取失败请检查Registry Center",
form_description: "如果当前配置项无值,则取场景配置项的值",
},
sql: {
dataSource: "数据源",
sql_script: "SQL脚本",
timeout: "超时时间(ms)",
database_driver: "数据库驱动",
database_url: "数据库连接URL",
username: "用户名",
password: "密码",
pool_max: "最大连接数",
query_timeout: "最大等待时间(ms)",
name_cannot_be_empty: "SQL请求名称不能为空",
dataSource_cannot_be_empty: "SQL请求数据源不能为空",
}
},
api_import: {
label: "导入",

View File

@ -1,7 +1,7 @@
export default {
commons: {
help_documentation: '幫助文檔',
delete_cancelled: '已取消除',
delete_cancelled: '已取消除',
workspace: '工作空間',
organization: '組織',
setting: '設置',
@ -15,7 +15,7 @@ export default {
save: '保存',
save_success: '保存成功',
delete_success: '刪除成功',
copy_success: '複製成功',
copy_success: '復制成功',
modify_success: '修改成功',
delete_cancel: '已取消刪除',
confirm: '確定',
@ -24,10 +24,10 @@ export default {
operating: '操作',
input_limit: '長度在 {0} 到 {1} 個字符',
login: '登錄',
welcome: '歡迎回來,請輸入用戶名和密碼登錄MeterSphere',
username: '用戶名',
welcome: '歡迎回來請輸入用戶名和密碼登錄MeterSphere',
username: '名',
password: '密碼',
input_username: '請輸入用戶名',
input_username: '請輸入用戶名',
input_password: '請輸入密碼',
test: '測試',
create_time: '創建時間',
@ -38,6 +38,8 @@ export default {
phone: '電話',
role: '角色',
personal_info: '個人信息',
api_keys: 'API Keys',
quota: '配額管理',
status: '狀態',
show_all: '顯示全部',
show: '顯示',
@ -45,8 +47,6 @@ export default {
user: '用戶',
system: '系統',
personal_setting: '個人設置',
api_keys: 'API Keys',
quota: '配額管理',
test_resource_pool: '測試資源池',
system_setting: '系統設置',
api: '接口測試',
@ -55,7 +55,7 @@ export default {
input_content: '請輸入內容',
create: '新建',
edit: '編輯',
copy: '複製',
copy: '復制',
refresh: '刷新',
remark: '備註',
delete: '刪除',
@ -69,8 +69,8 @@ export default {
title: '標題',
custom: '自定義',
select_date: '選擇日期',
calendar_heatmap: '測試日',
months_1: '月',
calendar_heatmap: '測試日',
months_1: '月',
months_2: '二月',
months_3: '三月',
months_4: '四月',
@ -80,10 +80,10 @@ export default {
months_8: '八月',
months_9: '九月',
months_10: '十月',
months_11: '十月',
months_11: '十月',
months_12: '十二月',
weeks_0: '周日',
weeks_1: '周',
weeks_1: '周',
weeks_2: '周二',
weeks_3: '周三',
weeks_4: '周四',
@ -95,23 +95,26 @@ export default {
connection_failed: '連接失敗',
save_failed: '保存失敗',
host_cannot_be_empty: '主機不能為空',
port_cannot_be_empty: '號不能為空',
port_cannot_be_empty: '端口號不能為空',
account_cannot_be_empty: '帳戶不能為空',
remove: '移除',
remove_cancel: '移除取消',
remove_success: '移除成功',
tips: '认認證資訊已過期,請重新登入',
tips: '認證信息已過期,請重新登錄',
not_performed_yet: '尚未執行',
incorrect_input: '輸入內容不正確',
delete_confirm: '請輸入以下內容,確認刪除:',
login_username: 'ID 或 郵箱',
input_login_username: '請輸入用戶 ID 或 郵箱',
input_name: '請輸入名稱',
please_upload: '請上傳文件',
formatErr: '格式錯誤',
please_save: '請先保存',
id: 'ID',
cannot_be_null: '不能为空',
millisecond: '毫秒',
reference_documentation: "參考文檔",
please_upload: '請上傳文件',
id: 'ID',
millisecond: '毫秒',
cannot_be_null: '不能為空',
already_exists: '名稱不能重復',
date: {
select_date: '選擇日期',
start_date: '開始日期',
@ -136,7 +139,7 @@ export default {
search: "查詢",
reset: "重置",
and: '所有',
or: '任意個',
or: '任意個',
operators: {
like: "包含",
not_like: "不包含",
@ -160,45 +163,46 @@ export default {
edition: '產品版本',
licenseVersion: '授權版本',
count: '授權數量',
valid_license: '授權验证',
valid_license: '授權驗證',
show_license: '查看授權',
valid_license_error: '授權验证失败',
status: '授權状态',
expired: '已过期',
valid_license_error: '授權驗證失敗',
status: '授權狀態',
valid: '有效',
invalid: '無效',
expired: '已過期',
},
workspace: {
create: '創建工作空間',
update: '修改工作空間',
delete: '刪除工作空間',
delete_confirm: '删除該工作空間會關聯删除該工作空間下的所有資源(如:相關項目,測試用例等),確定要删除嗎?',
delete_confirm: '刪除該工作空間會關聯刪除該工作空間下的所有資源(如:相關項目,測試用例等),確定要刪除嗎?',
add: '添加工作空間',
input_name: '請輸入工作空間名稱',
search_by_name: '根據名稱搜索',
organization_name: '所屬組織',
please_choose_organization: '請選擇組織',
please_select_a_workspace_first: '請先選擇工作空間! ',
please_select_a_workspace_first: '請先選擇工作空間!',
none: '無工作空間',
select: '選擇工作空間',
special_characters_are_not_supported: '格式錯誤(不支持特殊字符,且不能以\'-\'開頭結尾)',
delete_warning: '删除该工作空间将同步删除该工作空间下所有项目,以及项目中的所有用例、接口测试、性能测试等,确定要删除吗?',
delete_warning: '刪除該工作空間將同步刪除該工作空間下所有項目,以及項目中的所有用例、接口測試、性能測試等,確定要刪除嗎?',
},
organization: {
create: '創建組織',
modify: '修改組織',
delete: '刪除組織',
delete_confirm: '删除該組織會關聯删除該組織下的所有資源(如:相關工作空間,項目,測試用例等),確定要删除嗎?',
delete_confirm: '刪除該組織會關聯刪除該組織下的所有資源(如:相關工作空間,項目,測試用例等),確定要刪除嗎?',
input_name: '請輸入組織名稱',
select_organization: '請選擇組織',
search_by_name: '根據名稱搜索',
special_characters_are_not_supported: 'Incorrect format (special characters are not supported and cannot end with \'-\')',
special_characters_are_not_supported: '格式錯誤(不支持特殊字符,且不能以\'-\'開頭結尾)',
none: '無組織',
select: '選擇組織',
delete_warning: '删除该组织将同步删除该组织下所有相关工作空间和相关工作空间下的所有项目,以及项目中的所有用例、接口测试、性能测试等,确定要删除吗?',
delete_warning: '刪除該組織將同步刪除該組織下所有相關工作空間和相關工作空間下的所有項目,以及項目中的所有用例、接口測試、性能測試等,確定要刪除嗎?',
service_integration: '服務集成',
defect_manage: '缺陷管理平',
defect_manage: '缺陷管理平',
integration: {
select_defect_platform: '請選擇要集成的缺陷管理平',
select_defect_platform: '請選擇要集成的缺陷管理平',
basic_auth_info: 'Basic Auth 賬號信息:',
api_account: 'API 賬號',
api_password: 'API 口令',
@ -209,7 +213,7 @@ export default {
input_jira_url: '請輸入Jira地址https://metersphere.atlassian.net/',
input_jira_issuetype: '請輸入問題類型',
use_tip: '使用指引:',
use_tip_tapd: 'Basic Auth 賬號信息在"公司管理-安全與集成-開放平"中查詢',
use_tip_tapd: 'Tapd Basic Auth 賬號信息在"公司管理-安全與集成-開放平"中查詢',
use_tip_jira: 'Jira software server 認證信息為 賬號密碼Jira software cloud 認證信息為 賬號+令牌(賬戶設置-安全-創建API令牌)',
use_tip_two: '保存 Basic Auth 賬號信息後,需要在 Metersphere 項目中手動關聯 ID/key',
link_the_project_now: '馬上關聯項目',
@ -217,8 +221,8 @@ export default {
cancel_integration: '取消集成',
cancel_confirm: '確認取消集成 ',
successful_operation: '操作成功',
not_integrated: '未集成該平',
choose_platform: '請選擇集成的平',
not_integrated: '未集成該平',
choose_platform: '請選擇集成的平',
verified: '驗證通過'
}
},
@ -228,7 +232,7 @@ export default {
edit: '編輯項目',
delete: '刪除項目',
delete_confirm: '確定要刪除這個項目嗎?',
delete_tip: '删除該項目,會删除該項目下所有測試資源,確定要删除嗎?',
delete_tip: '刪除該項目,會刪除該項目下所有測試資源,確定要刪除嗎?',
search_by_name: '根據名稱搜索',
input_name: '請輸入項目名稱',
owning_workspace: '所屬工作空間',
@ -251,11 +255,11 @@ export default {
special_characters_are_not_supported: '不支持特殊字符',
mobile_number_format_is_incorrect: '手機號碼格式不正確',
email_format_is_incorrect: '郵箱格式不正確',
password_format_is_incorrect: '有效密碼8-30 位,英文大小寫字母+數位+特殊字元(可選)',
password_format_is_incorrect: '有效密碼8-30位,英文大小寫字母+數字+特殊字符(可選)',
old_password: '舊密碼',
new_password: '新密碼',
repeat_password: '確認密碼',
inconsistent_passwords: '兩次輸入的密碼不致',
inconsistent_passwords: '兩次輸入的密碼不致',
remove_member: '確定要移除該成員嗎',
input_id_or_email: '請輸入用戶 ID, 或者 用戶郵箱',
no_such_user: '無此用戶信息, 請輸入正確的用戶 ID 或者 用戶郵箱!',
@ -263,7 +267,7 @@ export default {
user: {
create: '創建用戶',
modify: '修改用戶',
input_name: '請輸入用戶名',
input_name: '請輸入用戶名',
input_id: '請輸入ID',
input_email: '請輸入郵箱',
input_password: '請輸入密碼',
@ -286,7 +290,6 @@ export default {
add: '添加角色',
},
report: {
name: '項目名稱',
recent: '最近的報告',
search_by_name: '根據名稱搜索',
test_name: '所屬測試',
@ -321,8 +324,8 @@ export default {
delete_batch_confirm: '確認批量刪除報告',
},
load_test: {
same_project_test: '只能運行同項目內的測試',
run: '一鍵運行',
same_project_test: '只能運行同項目內的測試',
already_exists: '測試名稱不能重復',
operating: '操作',
recent: '最近的測試',
search_by_name: '根據名稱搜索',
@ -335,30 +338,30 @@ export default {
pressure_config: '壓力配置',
advanced_config: '高級配置',
runtime_config: '運行配置',
is_running: '正在運行! ',
test_name_is_null: '測試名稱不能為空! ',
project_is_null: '項目不能為空! ',
jmx_is_null: '必需包含一個JMX文件且只能包含一個JMX文件',
is_running: '正在運行!',
test_name_is_null: '測試名稱不能為空!',
project_is_null: '項目不能為空!',
jmx_is_null: '必需包含壹個JMX文件且只能包含壹個JMX文件',
file_name: '文件名',
file_size: '文件大小',
file_type: '文件類型',
file_status: '文件狀態',
last_modify_time: '修改時間',
upload_tips: '將文件拖到此處,或<em>點擊上傳</em>',
upload_tips: '將文件拖到此處或<em>點擊上傳</em>',
upload_type: '只能上傳JMX/CSV文件',
related_file_not_found: "未找到關聯的測試文件!",
delete_file_confirm: '確認刪除文件: ',
file_size_limit: "文件個數超出限制!",
delete_file: "文件已存在,請先刪除同名文件!",
thread_num: '並用戶數:',
thread_num: '並用戶數:',
input_thread_num: '請輸入線程數',
duration: '壓測時長(分鐘):',
input_duration: '請輸入時長',
rps_limit: 'RPS上限',
input_rps_limit: '請輸入限制',
ramp_up_time_within: '在',
ramp_up_time_minutes: '分鐘內,分',
ramp_up_time_times: '次增加並用戶',
ramp_up_time_minutes: '分鐘內分',
ramp_up_time_times: '次增加並用戶',
advanced_config_error: '高級配置校驗失敗',
domain_bind: '域名綁定',
domain: '域名',
@ -369,14 +372,15 @@ export default {
params: '自定義屬性',
param_name: '屬性名',
param_value: '屬性值',
domain_is_duplicate: '域名不能重',
param_is_duplicate: '參數名不能重',
domain_is_duplicate: '域名不能重',
param_is_duplicate: '參數名不能重',
domain_ip_is_empty: '域名和IP不能為空',
param_name_value_is_empty: '參數名和參數值不能為空',
connect_timeout: '建立連接超時時間',
custom_http_code: '自定義 HTTP 響應成功狀態碼',
separated_by_commas: '按逗號分隔',
create: '創建測試',
run: '壹鍵運行',
select_resource_pool: '請選擇資源池',
resource_pool_is_null: '資源池為空',
download_log_file: '下載完整日誌文件',
@ -394,13 +398,13 @@ export default {
reset: "重置",
input_name: "請輸入測試名稱",
select_project: "請選擇項目",
variable_name: "變名",
variable: "變",
variable_name: "變名",
variable: "變",
copied: "已拷貝",
key: "鍵",
value: "值",
create_performance_test: "創建性能測試",
export_config: "出",
export_config: "出",
enable_validate_tip: "沒有可用請求",
copy: "復制測試",
environment: {
@ -413,21 +417,23 @@ export default {
environment: "環境",
select_environment: "請選擇環境",
please_save_test: "請先保存測試",
common_config: "通用配置",
http_config: "HTTP配置",
database_config: "數據庫配置",
},
scenario: {
scenario: "場景",
dubbo: "Dubbo配寘",
creator: "創建人",
config: "場景配寘",
dubbo: "Dubbo配置",
config: "場景配置",
input_name: "請輸入場景名稱",
name: "場景名稱",
base_url: "基礎URL",
base_url_description: "基礎URL作為所有請求的URL首碼",
variables: "自定義變",
base_url_description: "基礎URL作為所有請求的URL前綴",
variables: "自定義變",
headers: "請求頭",
kv_description: "所有請求可以使用自定義變",
copy: "複製場景",
delete: "除場景",
kv_description: "所有請求可以使用自定義變",
copy: "復制場景",
delete: "除場景",
disable: "禁用",
enable: "啟用",
create_scenario: "創建新場景",
@ -436,13 +442,13 @@ export default {
enable_disable: "啟用/禁用",
test_name: "測試名稱",
reference: "引用",
clone: "複製",
clone: "復制",
cant_reference: '歷史測試文件,重新保存後才可被引用'
},
request: {
debug: "調試",
copy: "複製請求",
delete: "除請求",
copy: "復制請求",
delete: "除請求",
input_name: "請輸入請求名稱",
input_url: "請輸入請求URL",
input_path: "請輸入請求路徑",
@ -460,7 +466,7 @@ export default {
parameters: "請求參數",
jmeter_func: "Jmeter 方法",
parameters_filter_example: "示例",
parameters_filter_tips: "只支持MockJs函數結果預覽",
parameters_filter_tips: "只支持 MockJs 函數結果預覽",
parameters_advance: "高級參數設置",
parameters_preview: "預覽",
parameters_mock_filter_tips: "請輸入關鍵字進行過濾",
@ -470,44 +476,45 @@ export default {
parameters_advance_add_func_limit: "最多支持5個函數",
parameters_advance_add_func_error: "請先選擇函數",
parameters_advance_add_param_error: "請輸入函數參數",
parameters_desc: "參數追加到URL,例如https://fit2cloud.com/entrieskey1=Value1&amp;Key2=Value2",
parameters_desc: "參數追加到URL例如https://fit2cloud.com/entries?key1=Value1&Key2=Value2",
headers: "請求頭",
body: "請求內容",
body_kv: "鍵值對",
body_text: "文",
body_text: "文",
timeout_config: "超時設置",
connect_timeout: "連接超時",
response_timeout: "響應超時",
follow_redirects: "跟隨重定向",
body_upload_limit_size: "上傳文件大小不能超過 500 MB!",
assertions: {
label: "斷言",
text: "文",
text: "文",
regex: "正則",
response_time: "應時間",
response_time: "應時間",
select_type: "請選擇類型",
select_subject: "請選擇對象",
select_condition: "請選擇條件",
contains: "包含",
not_contains: "不包含",
equals: "等於",
start_with: "以開始",
end_with: "以結束",
start_with: "以...開始",
end_with: "以...結束",
value: "值",
expect: "期望值",
expression: "Perl型規則運算式",
response_in_time: "回應時間在…毫秒以內",
expression: "Perl型正則表達式",
response_in_time: "響應時間在...毫秒以內",
},
extract: {
label: "取",
label: "取",
select_type: "請選擇類型",
description: "從響應結果中選取數據並將其存儲在變數中,在後續請求中使用變數。",
description: "從響應結果中提取數據並將其存儲在變量中,在後續請求中使用變量。",
regex: "正則",
regex_expression: "Perl型規則運算式",
json_path_expression: "JSONPath運算式",
xpath_expression: "XPath運算式",
regex_expression: "Perl型正則表達式",
json_path_expression: "JSONPath表達式",
xpath_expression: "XPath表達式",
},
processor: {
pre_exec_script : "預執行腳本",
pre_exec_script: "預執行腳本",
post_exec_script: "後執行腳本",
code_template: "代碼模版",
bean_shell_processor_tip: "僅支持 BeanShell 腳本",
@ -518,14 +525,26 @@ export default {
code_template_get_response_result: "獲取響應結果"
},
dubbo: {
protocol: "協",
protocol: "協",
input_interface: "請輸入Interface",
input_method: "請輸入Method",
input_config_center: "請輸入Config Center",
get_provider_success: "獲取成功",
input_registry_center: "請輸入Registry Center",
input_consumer_service: "請輸入Consumer & Service",
get_provider_success: "獲取成功",
check_registry_center: "獲取失敗請檢查Registry Center",
form_description: "如果當前配置項無值,則取場景配置項的值",
},
sql: {
dataSource: "數據源",
sql_script: "SQL腳本",
timeout: "超時時間(ms)",
database_driver: "數據庫驅動",
database_url: "數據庫連接URL",
username: "用戶名",
password: "密碼",
pool_max: "最大連接數",
query_timeout: "最大等待時間(ms)",
}
},
api_import: {
@ -535,10 +554,10 @@ export default {
file_size_limit: "文件大小不超過 20 M",
tip: "說明",
export_tip: "導出方法",
ms_tip: "支持 MeterSphere json 格式",
ms_export_tip: "通過 MeterSphere Api 測試頁面或者瀏覽器插件導出 json 格式文件",
ms_tip: "支持 Metersphere json 格式",
ms_export_tip: "通過 Metersphere 接口測試頁面或者瀏覽器插件導出 json 格式文件",
postman_tip: "只支持 Postman Collection v2.1 格式的 json 文件",
swagger_tip: "只支持 Swagger2.x 版本的 json 文件",
swagger_tip: "只支持 Swagger 2.x 版本的 json 文件",
post_export_tip: "通過 Postman 導出測試集合",
swagger_export_tip: "通過 Swagger 頁面導出",
suffixFormatErr: "文件格式不符合要求",
@ -552,11 +571,11 @@ export default {
request_headers: "請求頭",
request_cookie: "Cookie",
response: "響應",
delete_confirm: '確認删除報告:',
delete_confirm: '確認刪除報告: ',
delete_batch_confirm: '確認批量刪除報告',
scenario_name: "場景名稱",
response_time: "回應時間ms",
latency: "網延遲",
response_time: "響應時間(ms)",
latency: "網延遲",
request_size: "請求大小",
response_size: "響應大小",
response_code: "狀態碼",
@ -565,7 +584,7 @@ export default {
assertions: "斷言",
assertions_pass: "成功斷言",
assertions_name: "斷言名稱",
assertions_error_message: "錯誤資訊",
assertions_error_message: "錯誤信息",
assertions_is_success: "是否成功",
result: "結果",
success: "成功",
@ -581,7 +600,7 @@ export default {
not_exist: "測試報告不存在",
},
test_track: {
test_track: "測試跟",
test_track: "測試跟",
confirm: "確 定",
cancel: "取 消",
project: "項目",
@ -593,17 +612,18 @@ export default {
pass_rate: "通過率",
execution_result: ": 請選擇執行結果",
actual_result: ": 實際結果為空",
case: {
input_test_case: '請輸入關聯用例名稱',
test_name: '測試名稱',
other: '--其他--',
other: "--其他--",
test_case: "測試用例",
move: "移動用例",
case_list: "用例列表",
create_case: "創建用例",
edit_case: "編輯用例",
view_case: "查看用例",
no_project: "該工作空間下無項目,請先創建項目",
no_project: "該工作空間下無項目請先創建項目",
priority: "用例等級",
type: "類型",
method: "測試方式",
@ -626,14 +646,14 @@ export default {
input_type: "請選擇用例類型",
input_method: "請選擇測試方式",
input_prerequisite: "請輸入前置條件",
delete_confirm: "確認刪除測試用例: ",
delete: "除用例",
delete_confirm: "確認刪除測試用例",
delete: "除用例",
save_create_continue: "保存並繼續創建",
please_create_project: "暫無項目,請先創建項目",
create_module_first: "請先新建模塊",
relate_test: "關聯測試",
relate_test_not_find: '關聯的測試不存在,請檢查用例',
other_relate_test_not_find: '關聯的測試名,請前往協力廠商平臺執行',
other_relate_test_not_find: '關聯的測試名,請前往第三方平臺執行',
batch_handle: '批量處理 (選中{0}項)',
batch_update: '更新{0}個用例的屬性',
select_catalog: '請選擇用例目錄',
@ -651,11 +671,11 @@ export default {
case_import: "導入測試用例",
download_template: "下載模版",
click_upload: "點擊上傳",
upload_limit: "只能上傳xls/xlsx文件,且不超過20M",
upload_limit_count: "一次只能上傳一個文件",
upload_limit: "只能上傳xls/xlsx文件且不超過20M",
upload_limit_other_size: "上傳文件大小不能超過",
upload_limit_count: "壹次只能上傳壹個文件",
upload_limit_format: "上傳文件只能是 xls、xlsx格式!",
upload_limit_size: "上傳文件大小不能超過 20MB!",
upload_limit_other_size: "上傳文件大小不能超過",
success: "導入成功!",
importing: "導入中...",
},
@ -723,13 +743,13 @@ export default {
step_result: "步驟執行結果",
my_case: "我的用例",
all_case: "全部用例",
pre_case: "上條用例",
next_case: "下條用例",
pre_case: "上條用例",
next_case: "下條用例",
change_execution_results: "更改執行結果",
change_executor: "更改執行人",
select_executor: "請選擇執行人",
select_execute_result: "選擇執行結果",
cancel_relevance: "取消關聯",
cancel_relevance: "取消用例關聯",
confirm_cancel_relevance: "確認取消關聯",
select_manipulate: "請選擇需要操作的數據",
select_template: "選擇模版",
@ -741,11 +761,11 @@ export default {
test_result: "測試結果",
result_distribution: "測試結果分布",
custom_component: "自定義模塊",
defect_list: "缺陷列表",
create_report: "創建測試報告",
defect_list:"缺陷清單",
view_report: "查看測試報告",
component_library: "組件庫",
component_library_tip: "拖拽組件庫中組件,添加至右側,預覽報告效果,每個系統組件只能添加壹個。",
component_library_tip: "拖拽組件庫中組件,添加至右側,預覽報告效果,每個系統組件只能添加壹個。",
delete_component_tip: "請至少保留壹個組件",
input_template_name: "輸入模版名稱",
template_special_characters: '模版名稱不支持特殊字符',
@ -757,17 +777,17 @@ export default {
report_template: "測試報告模版",
test_detail: "測試詳情",
failure_case: "失敗用例",
export_report: "出報告"
export_report: "出報告"
},
issue: {
issue: "缺陷",
platform_tip: "在系統設置-組織-服務集成中集成缺陷管理平台可以自動提交缺陷到指定缺陷管理平台",
platform_tip: "在系統設置-組織-服務集成中集成缺陷管理平臺可以自動提交缺陷到指定缺陷管理平臺",
input_title: "請輸入標題",
id: "缺陷ID",
title: "缺陷標題",
description: "缺陷描述",
status: "缺陷狀態",
platform: "平",
platform: "平",
operate: "操作",
close: "關閉缺陷",
title_description_required: "標題和描述必填",
@ -796,17 +816,17 @@ export default {
system_parameter_setting: {
mailbox_service_settings: '郵件設置',
ldap_setting: 'LDAP設置',
test_connection: '測試連',
test_connection: '測試連',
SMTP_host: 'SMTP主機',
SMTP_port: 'SMTP',
SMTP_account: 'SMTP戶',
SMTP_port: 'SMTP端口',
SMTP_account: 'SMTP戶',
SMTP_password: 'SMTP密碼',
SSL: '開啟SSL如果SMTP埠是465通常需要啟用SSL',
TLS: '開啟TLS如果SMTP埠是587通常需要啟用TLS',
SSL: '開啟SSL(如果SMTP端口是465通常需要啟用SSL)',
TLS: '開啟TLS(如果SMTP端口是587通常需要啟用TLS)',
SMTP: '是否匿名 SMTP',
host: '主機號不能為空',
port: '號不能為空',
account: '戶不能為空',
port: '端口號不能為空',
account: '戶不能為空',
},
i18n: {
home: '首頁'
@ -838,23 +858,23 @@ export default {
dn_cannot_be_empty: 'LDAP DN不能為空',
ou_cannot_be_empty: 'LDAP OU不能為空',
filter_cannot_be_empty: 'LDAP 用戶過濾器不能為空',
password_cannot_be_empty: 'LDAP 密碼不能為空',
mapping_cannot_be_empty: 'LDAP 用戶屬性映射不能為空',
password_cannot_be_empty: 'LDAP 密碼不能為空',
},
schedule: {
input_email: "請輸入郵箱號",
input_email: "請輸入郵箱號",
event: "事件",
receiving_mode: "郵箱",
receiver: "接收人",
operation: "操作",
task_notification: "任務通知",
not_set: "未設置",
next_execution_time: "下次執行時間",
edit_timer_task: "編輯定時任務",
test_name: '測試名稱',
running_rule: '運行規則',
job_status: '任務狀態',
running_task: '運行中的任務',
next_execution_time: "下次執行時間",
edit_timer_task: "編輯定時任務",
please_input_cron_expression: "請輸入 Cron 表達式",
generate_expression: "生成表達式",
cron_expression_format_error: "Cron 表達式格式錯誤",