This commit is contained in:
shiziyuan9527 2020-11-04 10:49:00 +08:00
commit 7e395a4209
14 changed files with 180 additions and 21 deletions

View File

@ -9,6 +9,7 @@ public class AssertionType {
public final static String JSON_PATH = "JSONPath"; public final static String JSON_PATH = "JSONPath";
public final static String JSR223 = "JSR223"; public final static String JSR223 = "JSR223";
public final static String TEXT = "Text"; public final static String TEXT = "Text";
public final static String XPATH2 = "XPath2";
private String type; private String type;
} }

View File

@ -0,0 +1,13 @@
package io.metersphere.api.dto.scenario.assertions;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class AssertionXPath2 extends AssertionType {
private String expression;
public AssertionXPath2() {
setType(AssertionType.XPATH2);
}
}

View File

@ -9,5 +9,6 @@ public class Assertions {
private List<AssertionRegex> regex; private List<AssertionRegex> regex;
private List<AssertionJsonPath> jsonPath; private List<AssertionJsonPath> jsonPath;
private List<AssertionJSR223> jsr223; private List<AssertionJSR223> jsr223;
private List<AssertionXPath2> xPath2;
private AssertionDuration duration; private AssertionDuration duration;
} }

View File

@ -305,9 +305,11 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) { private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) {
ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult(); ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult();
responseAssertionResult.setMessage(assertionResult.getFailureMessage());
responseAssertionResult.setName(assertionResult.getName()); responseAssertionResult.setName(assertionResult.getName());
responseAssertionResult.setPass(!assertionResult.isFailure() && !assertionResult.isError()); responseAssertionResult.setPass(!assertionResult.isFailure() && !assertionResult.isError());
if (!responseAssertionResult.isPass()) {
responseAssertionResult.setMessage(assertionResult.getFailureMessage());
}
return responseAssertionResult; return responseAssertionResult;
} }

View File

@ -1,9 +1,7 @@
package io.metersphere.notice.service; package io.metersphere.notice.service;
import io.metersphere.base.domain.ApiTestReport; import io.metersphere.base.domain.*;
import io.metersphere.base.domain.LoadTestReportWithBLOBs; import io.metersphere.base.mapper.UserMapper;
import io.metersphere.base.domain.SystemParameter;
import io.metersphere.base.domain.TestCaseWithBLOBs;
import io.metersphere.commons.constants.APITestStatus; import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.NoticeConstants; import io.metersphere.commons.constants.NoticeConstants;
import io.metersphere.commons.constants.ParamConstants; import io.metersphere.commons.constants.ParamConstants;
@ -46,6 +44,8 @@ public class MailService {
private UserService userService; private UserService userService;
@Resource @Resource
private SystemParameterService systemParameterService; private SystemParameterService systemParameterService;
@Resource
private UserMapper userMapper;
//接口和性能测试 //接口和性能测试
public void sendLoadNotification(MessageDetail messageDetail, LoadTestReportWithBLOBs loadTestReport, String eventType) { public void sendLoadNotification(MessageDetail messageDetail, LoadTestReportWithBLOBs loadTestReport, String eventType) {
@ -297,7 +297,8 @@ public class MailService {
Map<String, String> context = new HashMap<>(); Map<String, String> context = new HashMap<>();
BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo(); BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo();
context.put("url", baseSystemConfigDTO.getUrl()); context.put("url", baseSystemConfigDTO.getUrl());
context.put("creator", reviewRequest.getCreator()); User user = userMapper.selectByPrimaryKey(reviewRequest.getCreator());
context.put("creator", user.getName());
context.put("reviewName", reviewRequest.getName()); context.put("reviewName", reviewRequest.getName());
context.put("start", start); context.put("start", start);
context.put("end", end); context.put("end", end);
@ -328,6 +329,8 @@ public class MailService {
context.put("start", start); context.put("start", start);
context.put("end", end); context.put("end", end);
context.put("id", testPlan.getId()); context.put("id", testPlan.getId());
User user = userMapper.selectByPrimaryKey(testPlan.getCreator());
context.put("creator", user.getName());
return context; return context;
} }

View File

@ -229,7 +229,6 @@ public class PerformanceTestService {
startEngine(loadTest, engine, request.getTriggerMode()); startEngine(loadTest, engine, request.getTriggerMode());
LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(engine.getReportId()); LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(engine.getReportId());
loadTestReport.setTriggerMode("API");
if (StringUtils.equals(NoticeConstants.API, loadTestReport.getTriggerMode()) || StringUtils.equals(NoticeConstants.SCHEDULE, loadTestReport.getTriggerMode())) { if (StringUtils.equals(NoticeConstants.API, loadTestReport.getTriggerMode()) || StringUtils.equals(NoticeConstants.SCHEDULE, loadTestReport.getTriggerMode())) {
performanceNoticeTask.registerNoticeTask(loadTestReport); performanceNoticeTask.registerNoticeTask(loadTestReport);
} }

View File

@ -561,7 +561,9 @@ public class TestCaseReviewService {
} }
/*编辑,新建,完成,删除通知内容*/ /*编辑,新建,完成,删除通知内容*/
private static String getReviewContext(SaveTestCaseReviewRequest reviewRequest, String type) { private String getReviewContext(SaveTestCaseReviewRequest reviewRequest, String type) {
User user = userMapper.selectByPrimaryKey(reviewRequest.getCreator());
Long startTime = reviewRequest.getCreateTime(); Long startTime = reviewRequest.getCreateTime();
Long endTime = reviewRequest.getEndTime(); Long endTime = reviewRequest.getEndTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@ -577,11 +579,11 @@ public class TestCaseReviewService {
} }
String context = ""; String context = "";
if (StringUtils.equals(NoticeConstants.CREATE, type)) { if (StringUtils.equals(NoticeConstants.CREATE, type)) {
context = "测试评审任务通知:" + reviewRequest.getCreator() + "发起的" + "'" + reviewRequest.getName() + "'" + "待开始,计划开始时间是" + start + "计划结束时间为" + end + "请跟进"; context = "测试评审任务通知:" + user.getName() + "发起的" + "'" + reviewRequest.getName() + "'" + "待开始,计划开始时间是" + start + "计划结束时间为" + end + "请跟进";
} else if (StringUtils.equals(NoticeConstants.UPDATE, type)) { } else if (StringUtils.equals(NoticeConstants.UPDATE, type)) {
context = "测试评审任务通知:" + reviewRequest.getCreator() + "发起的" + "'" + reviewRequest.getName() + "'" + "已完成,计划开始时间是" + start + "计划结束时间为" + end + "已完成"; context = "测试评审任务通知:" + user.getName() + "发起的" + "'" + reviewRequest.getName() + "'" + "已完成,计划开始时间是" + start + "计划结束时间为" + end + "已完成";
} else if (StringUtils.equals(NoticeConstants.DELETE, type)) { } else if (StringUtils.equals(NoticeConstants.DELETE, type)) {
context = "测试评审任务通知:" + reviewRequest.getCreator() + "发起的" + "'" + reviewRequest.getName() + "'" + "计划开始时间是" + start + "计划结束时间为" + end + "已删除"; context = "测试评审任务通知:" + user.getName() + "发起的" + "'" + reviewRequest.getName() + "'" + "计划开始时间是" + start + "计划结束时间为" + end + "已删除";
} }
return context; return context;

View File

@ -93,6 +93,8 @@ public class TestPlanService {
DingTaskService dingTaskService; DingTaskService dingTaskService;
@Resource @Resource
WxChatTaskService wxChatTaskService; WxChatTaskService wxChatTaskService;
@Resource
UserMapper userMapper;
public void addTestPlan(AddTestPlanRequest testPlan) { public void addTestPlan(AddTestPlanRequest testPlan) {
if (getTestPlanByName(testPlan.getName()).size() > 0) { if (getTestPlanByName(testPlan.getName()).size() > 0) {
@ -534,7 +536,8 @@ public class TestPlanService {
return projectName; return projectName;
} }
private static String getTestPlanContext(AddTestPlanRequest testPlan, String type) { private String getTestPlanContext(AddTestPlanRequest testPlan, String type) {
User user = userMapper.selectByPrimaryKey(testPlan.getCreator());
Long startTime = testPlan.getPlannedStartTime(); Long startTime = testPlan.getPlannedStartTime();
Long endTime = testPlan.getPlannedEndTime(); Long endTime = testPlan.getPlannedEndTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@ -554,11 +557,11 @@ public class TestPlanService {
} }
String context = ""; String context = "";
if (StringUtils.equals(NoticeConstants.CREATE, type)) { if (StringUtils.equals(NoticeConstants.CREATE, type)) {
context = "测试计划任务通知:" + testPlan.getCreator() + "创建的" + "'" + testPlan.getName() + "'" + "待开始,计划开始时间是" + start + "计划结束时间为" + end + "请跟进"; context = "测试计划任务通知:" + user.getName() + "创建的" + "'" + testPlan.getName() + "'" + "待开始,计划开始时间是" + start + "计划结束时间为" + end + "请跟进";
} else if (StringUtils.equals(NoticeConstants.UPDATE, type)) { } else if (StringUtils.equals(NoticeConstants.UPDATE, type)) {
context = "测试计划任务通知:" + testPlan.getCreator() + "创建的" + "'" + testPlan.getName() + "'" + "已完成,计划开始时间是" + start + "计划结束时间为" + end + "已完成"; context = "测试计划任务通知:" + user.getName() + "创建的" + "'" + testPlan.getName() + "'" + "已完成,计划开始时间是" + start + "计划结束时间为" + end + "已完成";
} else if (StringUtils.equals(NoticeConstants.DELETE, type)) { } else if (StringUtils.equals(NoticeConstants.DELETE, type)) {
context = "测试计划任务通知:" + testPlan.getCreator() + "创建的" + "'" + testPlan.getName() + "'" + "计划开始时间是" + start + "计划结束时间为" + end + "已删除"; context = "测试计划任务通知:" + user.getName() + "创建的" + "'" + testPlan.getName() + "'" + "计划开始时间是" + start + "计划结束时间为" + end + "已删除";
} }
return context; return context;
} }

View File

@ -0,0 +1,72 @@
<template>
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col>
<el-input :disabled="isReadOnly" v-model="xPath2.expression" maxlength="200" size="small" show-word-limit
:placeholder="$t('api_test.request.extract.xpath_expression')"/>
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>
{{ $t('api_test.request.assertions.add') }}
</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import {XPath2} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionXPath2",
props: {
xPath2: {
type: XPath2,
default: () => {
return new XPath2();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
methods: {
add: function () {
this.list.push(this.getXPath2());
this.callback();
},
remove: function () {
this.list.splice(this.index, 1);
},
getXPath2() {
return new XPath2(this.xPath2);
},
}
}
</script>
<style scoped>
.assertion-select {
width: 250px;
}
.assertion-item {
width: 100%;
}
.assertion-btn {
text-align: center;
width: 60px;
}
</style>

View File

@ -8,6 +8,7 @@
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/> <el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/> <el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
<el-option :label="'JSONPath'" :value="options.JSON_PATH"/> <el-option :label="'JSONPath'" :value="options.JSON_PATH"/>
<el-option :label="'XPath'" :value="options.XPATH2"/>
<el-option :label="$t('api_test.request.assertions.response_time')" :value="options.DURATION"/> <el-option :label="$t('api_test.request.assertions.response_time')" :value="options.DURATION"/>
<el-option :label="$t('api_test.request.assertions.jsr223')" :value="options.JSR223"/> <el-option :label="$t('api_test.request.assertions.jsr223')" :value="options.JSR223"/>
</el-select> </el-select>
@ -16,6 +17,7 @@
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/> <ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/>
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/> <ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/>
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/> <ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/>
<ms-api-assertion-x-path2 :is-read-only="isReadOnly" :list="assertions.xPath2" v-if="type === options.XPATH2" :callback="after"/>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration" <ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
v-if="type === options.DURATION" :callback="after"/> v-if="type === options.DURATION" :callback="after"/>
<ms-api-assertion-jsr223 :is-read-only="isReadOnly" :list="assertions.jsr223" v-if="type === options.JSR223" :callback="after"/> <ms-api-assertion-jsr223 :is-read-only="isReadOnly" :list="assertions.jsr223" v-if="type === options.JSR223" :callback="after"/>
@ -52,11 +54,13 @@
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath"; import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223"; import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223";
import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList"; import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList";
import MsApiAssertionXPath2 from "./ApiAssertionXPath2";
export default { export default {
name: "MsApiAssertions", name: "MsApiAssertions",
components: { components: {
MsApiAssertionXPath2,
MsApiAssertionJsr223, MsApiAssertionJsr223,
MsApiJsonpathSuggestList, MsApiJsonpathSuggestList,
MsApiAssertionJsonPath, MsApiAssertionJsonPath,

View File

@ -20,6 +20,16 @@
</div> </div>
</div> </div>
<div class="assertion-item-editing x_path" v-if="assertions.xPath2.length > 0">
<div>
{{ 'XPath' }}
</div>
<div class="regex-item" v-for="(xPath, index) in assertions.xPath2" :key="index">
<ms-api-assertion-x-path2 :is-read-only="isReadOnly" :list="assertions.xPath2"
:x-path2="xPath" :edit="true" :index="index"/>
</div>
</div>
<div class="assertion-item-editing jsr223" v-if="assertions.jsr223.length > 0"> <div class="assertion-item-editing jsr223" v-if="assertions.jsr223.length > 0">
<div> <div>
{{ $t("api_test.request.assertions.script") }} {{ $t("api_test.request.assertions.script") }}
@ -47,11 +57,14 @@ import MsApiAssertionDuration from "./ApiAssertionDuration";
import {Assertions} from "../../model/ScenarioModel"; import {Assertions} from "../../model/ScenarioModel";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath"; import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223"; import MsApiAssertionJsr223 from "@/business/components/api/test/components/assertion/ApiAssertionJsr223";
import MsApiAssertionXPath2 from "./ApiAssertionXPath2";
export default { export default {
name: "MsApiAssertionsEdit", name: "MsApiAssertionsEdit",
components: {MsApiAssertionJsr223, MsApiAssertionJsonPath, MsApiAssertionDuration, MsApiAssertionRegex}, components: {
MsApiAssertionXPath2,
MsApiAssertionJsr223, MsApiAssertionJsonPath, MsApiAssertionDuration, MsApiAssertionRegex},
props: { props: {
assertions: Assertions, assertions: Assertions,
@ -92,6 +105,10 @@ export default {
border-left: 2px solid #1FDD02; border-left: 2px solid #1FDD02;
} }
.assertion-item-editing.x_path {
border-left: 2px solid #fca130;
}
.regex-item { .regex-item {
margin-top: 10px; margin-top: 10px;
} }

View File

@ -439,6 +439,16 @@ export class JSONPathAssertion extends DefaultTestElement {
} }
} }
export class XPath2Assertion extends DefaultTestElement {
constructor(testName, xPath) {
super('XPath2Assertion', 'XPath2AssertionGui', 'XPath2Assertion', testName);
this.xPath = xPath || {};
this.stringProp('XPath.xpath', this.xPath.expression);
this.stringProp('XPath.namespace');
this.boolProp('XPath.negate', false);
}
}
export class ResponseCodeAssertion extends ResponseAssertion { export class ResponseCodeAssertion extends ResponseAssertion {
constructor(testName, type, value, assumeSuccess, message) { constructor(testName, type, value, assumeSuccess, message) {
let assertion = { let assertion = {

View File

@ -25,7 +25,7 @@ import {
ThreadGroup, ThreadGroup,
XPath2Extractor, XPath2Extractor,
IfController as JMXIfController, IfController as JMXIfController,
ConstantTimer as JMXConstantTimer, TCPSampler, JSR223Assertion, ConstantTimer as JMXConstantTimer, TCPSampler, JSR223Assertion, XPath2Assertion,
} from "./JMX"; } from "./JMX";
import Mock from "mockjs"; import Mock from "mockjs";
import {funcFilters} from "@/common/js/func-filter"; import {funcFilters} from "@/common/js/func-filter";
@ -96,6 +96,7 @@ export const ASSERTION_TYPE = {
JSON_PATH: "JSON", JSON_PATH: "JSON",
DURATION: "Duration", DURATION: "Duration",
JSR223: "JSR223", JSR223: "JSR223",
XPATH2: "XPath2",
} }
export const ASSERTION_REGEX_SUBJECT = { export const ASSERTION_REGEX_SUBJECT = {
@ -741,10 +742,11 @@ export class Assertions extends BaseConfig {
this.regex = []; this.regex = [];
this.jsonPath = []; this.jsonPath = [];
this.jsr223 = []; this.jsr223 = [];
this.xPath2 = [];
this.duration = undefined; this.duration = undefined;
this.set(options); this.set(options);
this.sets({text: Text, regex: Regex, jsonPath: JSONPath, jsr223: AssertionJSR223}, options); this.sets({text: Text, regex: Regex, jsonPath: JSONPath, jsr223: AssertionJSR223, xPath2: XPath2}, options);
} }
initOptions(options) { initOptions(options) {
@ -826,6 +828,23 @@ export class JSONPath extends AssertionType {
} }
} }
export class XPath2 extends AssertionType {
constructor(options) {
super(ASSERTION_TYPE.XPATH2);
this.expression = undefined;
this.description = undefined;
this.set(options);
}
// setJSONPathDescription() {
// this.description = this.expression + " expect: " + (this.expect ? this.expect : '');
// }
isValid() {
return !!this.expression;
}
}
export class Duration extends AssertionType { export class Duration extends AssertionType {
constructor(options) { constructor(options) {
super(ASSERTION_TYPE.DURATION); super(ASSERTION_TYPE.DURATION);
@ -1001,7 +1020,8 @@ class JMXHttpRequest {
this.domain = environment.config.httpConfig.domain; this.domain = environment.config.httpConfig.domain;
this.port = environment.config.httpConfig.port; this.port = environment.config.httpConfig.port;
this.protocol = environment.config.httpConfig.protocol; this.protocol = environment.config.httpConfig.protocol;
let envPath = environment.config.httpConfig.protocol + "://" + environment.config.httpConfig.socket; let url = new URL(environment.config.httpConfig.protocol + "://" + environment.config.httpConfig.socket);
let envPath = url.pathname === '/' ? '' : url.pathname;
this.path = this.getPostQueryParameters(request, decodeURIComponent(envPath + (request.path ? request.path : ''))); this.path = this.getPostQueryParameters(request, decodeURIComponent(envPath + (request.path ? request.path : '')));
} }
this.connectTimeout = request.connectTimeout; this.connectTimeout = request.connectTimeout;
@ -1397,11 +1417,11 @@ class JMXGenerator {
body = this.filterKV(request.body.kvs); body = this.filterKV(request.body.kvs);
this.addRequestBodyFile(httpSamplerProxy, request, testId); this.addRequestBodyFile(httpSamplerProxy, request, testId);
} else { } else {
httpSamplerProxy.boolProp('HTTPSampler.postBodyRaw', true);
body.push({name: '', value: request.body.raw, encode: false, enable: true}); body.push({name: '', value: request.body.raw, encode: false, enable: true});
} }
if (request.method !== 'GET') { if (request.method !== 'GET') {
httpSamplerProxy.boolProp('HTTPSampler.postBodyRaw', true);
httpSamplerProxy.add(new HTTPSamplerArguments(body)); httpSamplerProxy.add(new HTTPSamplerArguments(body));
} }
} }
@ -1437,6 +1457,12 @@ class JMXGenerator {
}) })
} }
if (assertions.xPath2.length > 0) {
assertions.xPath2.filter(this.filter).forEach(item => {
httpSamplerProxy.put(this.getXpathAssertion(item));
})
}
if (assertions.jsr223.length > 0) { if (assertions.jsr223.length > 0) {
assertions.jsr223.filter(this.filter).forEach(item => { assertions.jsr223.filter(this.filter).forEach(item => {
httpSamplerProxy.put(this.getJSR223Assertion(item)); httpSamplerProxy.put(this.getJSR223Assertion(item));
@ -1459,6 +1485,11 @@ class JMXGenerator {
return new JSR223Assertion(name, item); return new JSR223Assertion(name, item);
} }
getXpathAssertion(item) {
let name = item.expression;
return new XPath2Assertion(name, item);
}
getResponseAssertion(regex) { getResponseAssertion(regex) {
let name = regex.description; let name = regex.description;
let type = JMX_ASSERTION_CONDITION.CONTAINS; // 固定用Match自己写正则 let type = JMX_ASSERTION_CONDITION.CONTAINS; // 固定用Match自己写正则

View File

@ -286,12 +286,13 @@
}, },
getProject() { getProject() {
if (this.planId) { if (this.planId) {
this.$post("/test/plan/project/", {planId: this.planId}, res => { this.result = this.$post("/test/plan/project/", {planId: this.planId}, res => {
let data = res.data; let data = res.data;
if (data) { if (data) {
this.projects = data; this.projects = data;
this.projectId = data[0].id; this.projectId = data[0].id;
this.projectName = data[0].name; this.projectName = data[0].name;
this.search();
} }
}) })
} }