Merge remote-tracking branch 'origin/master'

This commit is contained in:
wenyann 2020-09-22 15:02:33 +08:00
commit 24bfb51f79
43 changed files with 639 additions and 354 deletions

View File

@ -6,11 +6,11 @@ ARG MS_VERSION=dev
RUN mkdir -p /opt/apps && mkdir -p /opt/jmeter
ADD backend/target/backend-1.1.jar /opt/apps
ADD backend/target/backend-1.3.jar /opt/apps
ADD backend/target/classes/jmeter/ /opt/jmeter/
ENV JAVA_APP_JAR=/opt/apps/backend-1.1.jar
ENV JAVA_APP_JAR=/opt/apps/backend-1.3.jar
ENV AB_OFF=true

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>metersphere-server</artifactId>
<groupId>io.metersphere</groupId>
<version>1.1</version>
<version>1.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -12,6 +12,7 @@ import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.controller.request.QueryScheduleRequest;
import io.metersphere.dto.LicenseDTO;
import io.metersphere.dto.ScheduleDao;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
@ -19,7 +20,6 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
@RestController
@ -127,4 +127,10 @@ public class APITestController {
public List<ScheduleDao> listSchedule(@RequestBody QueryScheduleRequest request) {
return apiTestService.listSchedule(request);
}
@GetMapping("/license/valid")
public LicenseDTO valid() {
return apiTestService.validateLicense();
}
}

View File

@ -12,7 +12,6 @@ import io.metersphere.api.parse.JmeterDocumentParser;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.ApiTestFileMapper;
import io.metersphere.base.mapper.ApiTestMapper;
import io.metersphere.base.mapper.UserMapper;
import io.metersphere.base.mapper.ext.ExtApiTestMapper;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.FileType;
@ -21,15 +20,13 @@ import io.metersphere.commons.constants.ScheduleType;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.*;
import io.metersphere.controller.request.QueryScheduleRequest;
import io.metersphere.dto.LicenseDTO;
import io.metersphere.dto.ScheduleDao;
import io.metersphere.i18n.Translator;
import io.metersphere.job.sechedule.ApiTestJob;
import io.metersphere.notice.service.MailService;
import io.metersphere.notice.service.NoticeService;
import io.metersphere.service.FileService;
import io.metersphere.service.QuotaService;
import io.metersphere.service.ScheduleService;
import io.metersphere.service.UserService;
import io.metersphere.service.*;
import io.metersphere.track.service.TestCaseService;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.constants.CommonConstants;
@ -40,7 +37,6 @@ import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
@ -441,4 +437,13 @@ public class APITestService {
quotaService.checkAPITestQuota();
}
}
public LicenseDTO validateLicense() {
LicenseService licenseService = CommonBeanFactory.getBean(LicenseService.class);
if (licenseService != null) {
return licenseService.valid();
}
return null;
}
}

View File

@ -2,12 +2,16 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="io.metersphere.base.mapper.ext.ExtTestCaseReviewMapper">
<select id="list" resultType="io.metersphere.track.dto.TestCaseReviewDTO" parameterType="io.metersphere.track.request.testreview.QueryCaseReviewRequest">
select distinct test_case_review.*
from test_case_review, project, test_case_review_project
<select id="list" resultType="io.metersphere.track.dto.TestCaseReviewDTO"
parameterType="io.metersphere.track.request.testreview.QueryCaseReviewRequest">
select distinct test_case_review.id, test_case_review.name, user.name as creator, test_case_review.status,
test_case_review.create_time, test_case_review.update_time, test_case_review.end_time,
test_case_review.description
from test_case_review, project, test_case_review_project, user
<where>
test_case_review.id = test_case_review_project.review_id
and test_case_review_project.project_id = project.id
and user.id = test_case_review.creator
<if test="request.name != null">
and test_case_review.name like CONCAT('%', #{request.name},'%')
</if>
@ -21,7 +25,8 @@
</if>
</select>
<select id="listByWorkspaceId" resultType="io.metersphere.track.dto.TestCaseReviewDTO" parameterType="io.metersphere.track.request.testreview.QueryCaseReviewRequest">
<select id="listByWorkspaceId" resultType="io.metersphere.track.dto.TestCaseReviewDTO"
parameterType="io.metersphere.track.request.testreview.QueryCaseReviewRequest">
select distinct test_case_review.*
from test_case_review, project, test_case_review_project
where test_case_review.id = test_case_review_project.review_id

View File

@ -0,0 +1,14 @@
package io.metersphere.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class LicenseDTO implements Serializable {
private String status;
private LicenseInfoDTO license;
}

View File

@ -0,0 +1,21 @@
package io.metersphere.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class LicenseInfoDTO implements Serializable {
// 客户名称
private String corporation;
// 授权截止时间
private String expired;
//产品名称
private String product;
//产品版本
private String edition;
//icense版本
private String licenseVersion;
//授权数量
private int licenseCount;
}

View File

@ -0,0 +1,10 @@
package io.metersphere.service;
import io.metersphere.dto.LicenseDTO;
public interface LicenseService {
public LicenseDTO valid();
public LicenseDTO addValidLicense(String reqLicenseCode);
}

View File

@ -232,6 +232,7 @@ public class TestCaseReviewService {
public void deleteCaseReview(String reviewId) {
deleteCaseReviewProject(reviewId);
deleteCaseReviewUsers(reviewId);
deleteCaseReviewTestCase(reviewId);
testCaseReviewMapper.deleteByPrimaryKey(reviewId);
}
@ -247,6 +248,12 @@ public class TestCaseReviewService {
testCaseReviewUsersMapper.deleteByExample(testCaseReviewUsersExample);
}
private void deleteCaseReviewTestCase(String reviewId) {
TestCaseReviewTestCaseExample testCaseReviewTestCaseExample = new TestCaseReviewTestCaseExample();
testCaseReviewTestCaseExample.createCriteria().andReviewIdEqualTo(reviewId);
testCaseReviewTestCaseMapper.deleteByExample(testCaseReviewTestCaseExample);
}
public List<TestCaseReview> listCaseReviewAll(String currentWorkspaceId) {
ProjectExample projectExample = new ProjectExample();
projectExample.createCriteria().andWorkspaceIdEqualTo(currentWorkspaceId);
@ -390,7 +397,7 @@ public class TestCaseReviewService {
}
return name;
}
public List<TestReviewCaseDTO> listTestCaseByProjectIds(List<String> projectIds) {
QueryCaseReviewRequest request = new QueryCaseReviewRequest();
request.setProjectIds(projectIds);

View File

@ -27,7 +27,7 @@ import io.metersphere.i18n.Translator;
import io.metersphere.track.dto.TestCaseDTO;
import io.metersphere.track.request.testcase.QueryTestCaseRequest;
import io.metersphere.track.request.testcase.TestCaseBatchRequest;
import io.metersphere.xmind.XmindToTestCaseParser;
import io.metersphere.xmind.XmindCaseParser;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
@ -273,13 +273,18 @@ public class TestCaseService {
if (multipartFile.getOriginalFilename().endsWith(".xmind")) {
try {
errList = new ArrayList<>();
String processLog = new XmindToTestCaseParser(this, userId, projectId, testCaseNames).importXmind(multipartFile);
XmindCaseParser xmindParser = new XmindCaseParser(this, userId, projectId, testCaseNames);
String processLog = xmindParser.parse(multipartFile);
if (!StringUtils.isEmpty(processLog)) {
excelResponse.setSuccess(false);
ExcelErrData excelErrData = new ExcelErrData(null, 1, Translator.get("upload_fail")+""+ processLog);
ExcelErrData excelErrData = new ExcelErrData(null, 1, Translator.get("upload_fail") + "" + processLog);
errList.add(excelErrData);
excelResponse.setErrList(errList);
} else {
if (!xmindParser.getTestCase().isEmpty()) {
this.saveImportData(xmindParser.getTestCase(), projectId);
xmindParser.clear();
}
excelResponse.setSuccess(true);
}
} catch (Exception e) {
@ -345,7 +350,7 @@ public class TestCaseService {
// 发送给客户端的数据
byte[] buff = new byte[1024];
try (OutputStream outputStream = res.getOutputStream();
BufferedInputStream bis = new BufferedInputStream(TestCaseService.class.getResourceAsStream("/io/metersphere/xmind/template/testcase.xml"));) {
BufferedInputStream bis = new BufferedInputStream(TestCaseService.class.getResourceAsStream("/io/metersphere/xmind/template/xmind.xml"));) {
int i = bis.read(buff);
while (i != -1) {
outputStream.write(buff, 0, buff.length);

View File

@ -1,6 +1,5 @@
package io.metersphere.xmind;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.base.domain.TestCaseWithBLOBs;
@ -11,51 +10,60 @@ import io.metersphere.excel.domain.TestCaseExcelData;
import io.metersphere.i18n.Translator;
import io.metersphere.track.service.TestCaseService;
import io.metersphere.xmind.parser.XmindParser;
import io.metersphere.xmind.parser.domain.Attached;
import io.metersphere.xmind.parser.domain.JsonRootBean;
import org.springframework.util.StringUtils;
import io.metersphere.xmind.parser.pojo.Attached;
import io.metersphere.xmind.parser.pojo.JsonRootBean;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 数据转换
* 1 解析Xmind文件 XmindParser.parseJson
* 2 解析后的JSON 转成测试用例
* 1 解析Xmind文件 XmindParser.parseObject
* 2 解析后的JSON this.parse 转成测试用例
*/
public class XmindToTestCaseParser {
public class XmindCaseParser {
private TestCaseService testCaseService;
private String maintainer;
private String projectId;
private StringBuffer process; // 过程校验记录
// 已存在用例名称
private Set<String> testCaseNames;
public XmindToTestCaseParser(TestCaseService testCaseService, String userId, String projectId, Set<String> testCaseNames) {
// 转换后的案例信息
private List<TestCaseWithBLOBs> testCases;
// 案例详情重写了hashCode方法去重用
private List<TestCaseExcelData> compartDatas;
public XmindCaseParser(TestCaseService testCaseService, String userId, String projectId, Set<String> testCaseNames) {
this.testCaseService = testCaseService;
this.maintainer = userId;
this.projectId = projectId;
this.testCaseNames = testCaseNames;
testCaseWithBLOBs = new LinkedList<>();
xmindDataList = new ArrayList<>();
testCases = new LinkedList<>();
compartDatas = new ArrayList<>();
process = new StringBuffer();
}
// 案例详情
private List<TestCaseWithBLOBs> testCaseWithBLOBs;
// 用于重复对比
protected List<TestCaseExcelData> xmindDataList;
// 这里清理是为了 加快jvm 回收
public void clear() {
compartDatas.clear();
testCases.clear();
testCaseNames.clear();
}
public List<TestCaseWithBLOBs> getTestCase() {
return this.testCases;
}
// 递归处理案例数据
private void makeXmind(StringBuffer processBuffer, Attached parent, int level, String nodePath, List<Attached> attacheds) {
private void recursion(StringBuffer processBuffer, Attached parent, int level, String nodePath, List<Attached> attacheds) {
for (Attached item : attacheds) {
if (isBlack(item.getTitle(), "(?:tc|tc:|tc)")) { // 用例
if (isAvailable(item.getTitle(), "(?:tc|tc:|tc)")) { // 用例
item.setParent(parent);
this.newTestCase(item.getTitle(), parent.getPath(), item.getChildren() != null ? item.getChildren().getAttached() : null);
} else {
@ -63,14 +71,13 @@ public class XmindToTestCaseParser {
item.setPath(nodePath);
if (item.getChildren() != null && !item.getChildren().getAttached().isEmpty()) {
item.setParent(parent);
makeXmind(processBuffer, item, level + 1, nodePath, item.getChildren().getAttached());
recursion(processBuffer, item, level + 1, nodePath, item.getChildren().getAttached());
}
}
}
}
private boolean isBlack(String str, String regex) {
// regex = "(?:tc:|tc)"
private boolean isAvailable(String str, String regex) {
if (StringUtils.isEmpty(str) || StringUtils.isEmpty(regex))
return false;
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
@ -88,7 +95,7 @@ public class XmindToTestCaseParser {
}
// 获取步骤数据
public String getSteps(List<Attached> attacheds) {
private String getSteps(List<Attached> attacheds) {
JSONArray jsonArray = new JSONArray();
for (int i = 0; i < attacheds.size(); i++) {
// 保持插入顺序判断用例是否有相同的steps
@ -148,9 +155,9 @@ public class XmindToTestCaseParser {
List<Attached> steps = new LinkedList<>();
if (attacheds != null && !attacheds.isEmpty()) {
attacheds.forEach(item -> {
if (isBlack(item.getTitle(), "(?:pc:|pc)")) {
if (isAvailable(item.getTitle(), "(?:pc:|pc)")) {
testCase.setPrerequisite(replace(item.getTitle(), "(?:pc:|pc)"));
} else if (isBlack(item.getTitle(), "(?:rc:|rc)")) {
} else if (isAvailable(item.getTitle(), "(?:rc:|rc)")) {
testCase.setRemark(replace(item.getTitle(), "(?:rc:|rc)"));
} else {
steps.add(item);
@ -171,74 +178,42 @@ public class XmindToTestCaseParser {
}
TestCaseExcelData compartData = new TestCaseExcelData();
BeanUtils.copyBean(compartData, testCase);
if (xmindDataList.contains(compartData)) {
if (compartDatas.contains(compartData)) {
process.append(Translator.get("test_case_already_exists_excel") + "" + testCase.getName() + "; ");
} else if (validate(testCase)) {
testCase.setId(UUID.randomUUID().toString());
testCase.setCreateTime(System.currentTimeMillis());
testCase.setUpdateTime(System.currentTimeMillis());
testCaseWithBLOBs.add(testCase);
testCases.add(testCase);
}
xmindDataList.add(compartData);
compartDatas.add(compartData);
}
//获取流文件
private static void inputStreamToFile(InputStream ins, File file) {
try (OutputStream os = new FileOutputStream(file);) {
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
LogUtil.error(e.getMessage());
}
}
/**
* MultipartFile File
*
* @param file
* @throws Exception
*/
private File multipartFileToFile(MultipartFile file) throws Exception {
if (file != null && file.getSize() > 0) {
try (InputStream ins = file.getInputStream();) {
File toFile = new File(file.getOriginalFilename());
inputStreamToFile(ins, toFile);
return toFile;
}
}
return null;
}
public boolean validate(TestCaseWithBLOBs data) {
// 验证合法性
private boolean validate(TestCaseWithBLOBs data) {
String nodePath = data.getNodePath();
StringBuilder stringBuilder = new StringBuilder();
if (nodePath != null) {
if (!StringUtils.isEmpty(nodePath)) {
String[] nodes = nodePath.split("/");
if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) {
stringBuilder.append(Translator.get("test_case_node_level_tip") +
TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level") + "; ");
}
for (int i = 0; i < nodes.length; i++) {
if (i != 0 && org.apache.commons.lang3.StringUtils.equals(nodes[i].trim(), "")) {
if (i != 0 && StringUtils.equals(nodes[i].trim(), "")) {
stringBuilder.append(Translator.get("module_not_null") + "; ");
break;
}
}
}
if (org.apache.commons.lang3.StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && org.apache.commons.lang3.StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) {
if (StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) {
stringBuilder.append(Translator.get("functional_method_tip") + "; ");
}
if (testCaseNames.contains(data.getName())) {
boolean dbExist = testCaseService.exist(data);
boolean excelExist = false;
if (dbExist) {
// db exist
stringBuilder.append(Translator.get("test_case_already_exists_excel") + "" + data.getName() + "; ");
@ -255,49 +230,29 @@ public class XmindToTestCaseParser {
}
// 导入思维导图处理
public String importXmind(MultipartFile multipartFile) {
public String parse(MultipartFile multipartFile) {
StringBuffer processBuffer = new StringBuffer();
File file = null;
try {
file = multipartFileToFile(multipartFile);
if (file == null || !file.exists())
return Translator.get("incorrect_format");
// 获取思维导图内容
String content = XmindParser.parseJson(file);
if (StringUtils.isEmpty(content) || content.split("(?:tc:|tc|TC:|TC|tc|TC)").length == 1) {
return Translator.get("import_xmind_not_found");
}
if (!StringUtils.isEmpty(content) && content.split("(?:tc:|tc|TC:|TC|tc|TC)").length > 500) {
return Translator.get("import_xmind_count_error");
}
JsonRootBean root = JSON.parseObject(content, JsonRootBean.class);
JsonRootBean root = XmindParser.parseObject(multipartFile);
if (root != null && root.getRootTopic() != null && root.getRootTopic().getChildren() != null) {
// 判断是模块还是用例
for (Attached item : root.getRootTopic().getChildren().getAttached()) {
if (isBlack(item.getTitle(), "(?:tc:|tc|tc)")) { // 用例
if (isAvailable(item.getTitle(), "(?:tc:|tc|tc)")) { // 用例
return replace(item.getTitle(), "(?:tc:|tc|tc)") + "" + Translator.get("test_case_create_module_fail");
} else {
item.setPath(item.getTitle());
if (item.getChildren() != null && !item.getChildren().getAttached().isEmpty()) {
item.setPath(item.getTitle());
makeXmind(processBuffer, item, 1, item.getPath(), item.getChildren().getAttached());
recursion(processBuffer, item, 1, item.getPath(), item.getChildren().getAttached());
}
}
}
}
if (StringUtils.isEmpty(process.toString()) && !testCaseWithBLOBs.isEmpty()) {
testCaseService.saveImportData(testCaseWithBLOBs, projectId);
}
} catch (Exception ex) {
processBuffer.append(Translator.get("incorrect_format"));
LogUtil.error(ex.getMessage());
ex.printStackTrace();
} finally {
if (file != null)
file.delete();
testCaseWithBLOBs.clear();
return ex.getMessage();
}
return process.toString();
}

View File

@ -1,9 +1,14 @@
package io.metersphere.xmind.parser;
import com.alibaba.fastjson.JSON;
import io.metersphere.xmind.parser.domain.JsonRootBean;
import io.metersphere.commons.exception.MSException;
import io.metersphere.i18n.Translator;
import io.metersphere.xmind.parser.pojo.JsonRootBean;
import io.metersphere.xmind.utils.FileUtil;
import org.apache.commons.compress.archivers.ArchiveException;
import org.dom4j.DocumentException;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@ -16,101 +21,98 @@ import java.util.Objects;
* @Description 解析主体
*/
public class XmindParser {
public static final String xmindZenJson = "content.json";
public static final String xmindLegacyContent = "content.xml";
public static final String xmindLegacyComments = "comments.xml";
public static final String xmindZenJson = "content.json";
public static final String xmindLegacyContent = "content.xml";
public static final String xmindLegacyComments = "comments.xml";
/**
* 解析脑图文件返回content整合后的内容
*
* @param file
* @return
* @throws IOException
* @throws ArchiveException
* @throws DocumentException
*/
public static String parseJson(File file) throws IOException, ArchiveException, DocumentException {
String res = ZipUtils.extract(file);
/**
* 解析脑图文件返回content整合后的内容
*
* @param multipartFile
* @return
* @throws IOException
* @throws ArchiveException
* @throws DocumentException
*/
public static String parseJson(MultipartFile multipartFile) throws IOException, ArchiveException, DocumentException {
String content = null;
if (isXmindZen(res, file)) {
content = getXmindZenContent(file, res);
} else {
content = getXmindLegacyContent(file, res);
}
File file = FileUtil.multipartFileToFile(multipartFile);
if (file == null || !file.exists())
MSException.throwException(Translator.get("incorrect_format"));
// 删除生成的文件夹
File dir = new File(res);
boolean flag = deleteDir(dir);
if (flag) {
// do something
}
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
return (JSON.toJSONString(jsonRootBean, false));
}
String res = ZipUtils.extract(file);
String content = null;
if (isXmindZen(res, file)) {
content = getXmindZenContent(file, res);
} else {
content = getXmindLegacyContent(file, res);
}
public static JsonRootBean parseObject(File file) throws DocumentException, ArchiveException, IOException {
String content = parseJson(file);
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
return jsonRootBean;
}
// 删除生成的文件夹
File dir = new File(res);
FileUtil.deleteDir(dir);
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
// 删除零时文件
if (file != null)
file.delete();
String json = (JSON.toJSONString(jsonRootBean, false));
public static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
// 递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空可以删除
return dir.delete();
}
if (StringUtils.isEmpty(content) || content.split("(?:tc:|tc|TC:|TC|tc|TC)").length == 1) {
MSException.throwException(Translator.get("import_xmind_not_found"));
}
if (!StringUtils.isEmpty(content) && content.split("(?:tc:|tc|TC:|TC|tc|TC)").length > 500) {
MSException.throwException(Translator.get("import_xmind_count_error"));
}
return json;
}
/**
* @return
*/
public static String getXmindZenContent(File file, String extractFileDir)
throws IOException, ArchiveException {
List<String> keys = new ArrayList<>();
keys.add(xmindZenJson);
Map<String, String> map = ZipUtils.getContents(keys, file, extractFileDir);
String content = map.get(xmindZenJson);
content = XmindZen.getContent(content);
return content;
}
public static JsonRootBean parseObject(MultipartFile multipartFile) throws DocumentException, ArchiveException, IOException {
String content = parseJson(multipartFile);
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
return jsonRootBean;
}
/**
* @return
*/
public static String getXmindLegacyContent(File file, String extractFileDir)
throws IOException, ArchiveException, DocumentException {
List<String> keys = new ArrayList<>();
keys.add(xmindLegacyContent);
keys.add(xmindLegacyComments);
Map<String, String> map = ZipUtils.getContents(keys, file, extractFileDir);
/**
* @return
*/
public static String getXmindZenContent(File file, String extractFileDir)
throws IOException, ArchiveException {
List<String> keys = new ArrayList<>();
keys.add(xmindZenJson);
Map<String, String> map = ZipUtils.getContents(keys, file, extractFileDir);
String content = map.get(xmindZenJson);
content = XmindZen.getContent(content);
return content;
}
String contentXml = map.get(xmindLegacyContent);
String commentsXml = map.get(xmindLegacyComments);
String xmlContent = XmindLegacy.getContent(contentXml, commentsXml);
/**
* @return
*/
public static String getXmindLegacyContent(File file, String extractFileDir)
throws IOException, ArchiveException, DocumentException {
List<String> keys = new ArrayList<>();
keys.add(xmindLegacyContent);
keys.add(xmindLegacyComments);
Map<String, String> map = ZipUtils.getContents(keys, file, extractFileDir);
return xmlContent;
}
String contentXml = map.get(xmindLegacyContent);
String commentsXml = map.get(xmindLegacyComments);
String xmlContent = XmindLegacy.getContent(contentXml, commentsXml);
private static boolean isXmindZen(String res, File file) throws IOException, ArchiveException {
// 解压
File parent = new File(res);
if (parent.isDirectory()) {
String[] files = parent.list(new ZipUtils.FileFilter());
for (int i = 0; i < Objects.requireNonNull(files).length; i++) {
if (files[i].equals(xmindZenJson)) {
return true;
}
}
}
return false;
}
return xmlContent;
}
private static boolean isXmindZen(String res, File file) throws IOException, ArchiveException {
// 解压
File parent = new File(res);
if (parent.isDirectory()) {
String[] files = parent.list(new ZipUtils.FileFilter());
for (int i = 0; i < Objects.requireNonNull(files).length; i++) {
if (files[i].equals(xmindZenJson)) {
return true;
}
}
}
return false;
}
}

View File

@ -1,5 +1,5 @@
package io.metersphere.xmind.parser.domain;
package io.metersphere.xmind.parser.pojo;
import lombok.Data;

View File

@ -1,4 +1,4 @@
package io.metersphere.xmind.parser.domain;
package io.metersphere.xmind.parser.pojo;
import lombok.Data;

View File

@ -1,4 +1,4 @@
package io.metersphere.xmind.parser.domain;
package io.metersphere.xmind.parser.pojo;
import lombok.Data;

View File

@ -1,4 +1,4 @@
package io.metersphere.xmind.parser.domain;
package io.metersphere.xmind.parser.pojo;
import lombok.Data;

View File

@ -1,4 +1,4 @@
package io.metersphere.xmind.parser.domain;
package io.metersphere.xmind.parser.pojo;
import lombok.Data;

View File

@ -1,4 +1,4 @@
package io.metersphere.xmind.parser.domain;
package io.metersphere.xmind.parser.pojo;
import lombok.Data;

View File

@ -0,0 +1,61 @@
package io.metersphere.xmind.utils;
import io.metersphere.commons.utils.LogUtil;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class FileUtil {
//获取流文件
private static void inputStreamToFile(InputStream ins, File file) {
try (OutputStream os = new FileOutputStream(file);) {
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
LogUtil.error(e.getMessage());
}
}
/**
* MultipartFile File
*
* @param file
* @throws Exception
*/
public static File multipartFileToFile(MultipartFile file) {
if (file != null && file.getSize() > 0) {
try (InputStream ins = file.getInputStream();) {
File toFile = new File(file.getOriginalFilename());
inputStreamToFile(ins, toFile);
return toFile;
} catch (Exception e) {
LogUtil.error(e.getMessage());
}
}
return null;
}
public static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
// 递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空可以删除
return dir.delete();
}
}

@ -1 +1 @@
Subproject commit 321c869938357e8c2253e5bd86c963828664ae23
Subproject commit d5b4969642fd8d10cc2f949d7377e0a0e5217a3a

View File

@ -30,7 +30,9 @@
"md5": "^2.3.0",
"sha.js": "^2.4.11",
"js-base64": "^3.4.4",
"json-bigint": "^1.0.0"
"json-bigint": "^1.0.0",
"html2canvas": "^1.0.0-rc.7",
"jspdf": "^2.1.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>metersphere-server</artifactId>
<groupId>io.metersphere</groupId>
<version>1.1</version>
<version>1.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -2,7 +2,7 @@
<el-col v-if="auth">
<el-row id="header-top1" type="flex" justify="space-between" align="middle">
<el-col>
<div class="license-head" v-if="valid === true && validData.status == 'expired'">License has expired since
<div class="license-head" v-if="validData.status == 'expired'">License has expired since
{{(validData!= undefined && validData.license!= undefined) ? validData.license.expired:''}},please
update license.
</div>
@ -40,15 +40,7 @@
export default {
name: 'app',
data() {
let xpack = false;
Setting.children.forEach(child => {
if (child.path === "license") {
xpack = true;
return;
}
})
return {
valid: xpack,
validData: {},
auth: false
}
@ -67,13 +59,14 @@
});
},
beforeMount() {
if (this.valid === true) {
// license
this.result = this.$get("/license/valid", response => {
// license
this.result = this.$get("/api/license/valid", response => {
let data = response.data;
if (data != undefined && data != null) {
this.validData = response.data;
saveLicense(response.data);
});
}
}
});
},
components: {MsLanguageSwitch, MsUser, MsView, MsTopMenus, MsHeaderOrgWs},
methods: {}

View File

@ -20,7 +20,7 @@
import MsMainContainer from "../../../common/components/MsMainContainer";
import MsAsideItem from "../../../common/components/MsAsideItem";
import EnvironmentEdit from "./environment/EnvironmentEdit";
import {listenGoBack, removeGoBackListener} from "../../../../../common/js/utils";
import {deepClone, listenGoBack, removeGoBackListener} from "../../../../../common/js/utils";
import {Environment, parseEnvironment} from "../model/EnvironmentModel";
export default {
@ -68,12 +68,13 @@
}
},
copyEnvironment(environment) {
this.currentEnvironment = environment;
if (!environment.id) {
this.$warning(this.$t('commons.please_save'));
return;
}
let newEnvironment = {};
Object.assign(newEnvironment, environment);
newEnvironment = new Environment(environment);
newEnvironment.id = null;
newEnvironment.name = this.getNoRepeatName(newEnvironment.name);
if (!this.validateEnvironment(newEnvironment)) {
@ -84,11 +85,7 @@
this.$refs.environmentItems.itemSelected(this.environments.length - 1, newEnvironment);
},
validateEnvironment(environment) {
if (!environment.name || !!environment.name && environment.name.length > 64) {
this.$error(this.$t('commons.input_limit', [1, 64]));
return false;
}
if (!this.$refs.environmentEdit.validateSocket(environment.socket)) {
if (!this.$refs.environmentEdit.validate()) {
this.$error(this.$t('commons.formatErr'));
return false;
}

View File

@ -79,6 +79,17 @@
}
});
},
validate() {
let isValidate = false;
this.$refs['environment'].validate((valid) => {
if (valid && this.$refs.commonConfig.validate() && this.$refs.httpConfig.validate()) {
isValidate = true;
} else {
isValidate = false;
}
});
return isValidate;
},
_save(environment) {
let param = this.buildParam(environment);
let url = '/api/environment/add';

View File

@ -8,13 +8,14 @@ export class Environment extends BaseConfig {
this.projectId = undefined;
this.name = undefined;
this.id = undefined;
this.config = options.config || new Config();
this.config = undefined;
this.set(options);
this.sets({}, options);
}
initOptions(options = {}) {
this.config = new Config(options.config);
return options;
}
}
@ -22,14 +23,16 @@ export class Environment extends BaseConfig {
export class Config extends BaseConfig {
constructor(options = {}) {
super();
this.commonConfig = options.commonConfig || new CommonConfig();
this.httpConfig = options.httpConfig || new HttpConfig();
this.commonConfig = undefined;
this.httpConfig = undefined;
this.databaseConfigs = [];
this.set(options);
this.sets({databaseConfigs: DatabaseConfig}, options);
}
initOptions(options = {}) {
this.commonConfig = new CommonConfig(options.commonConfig);
this.httpConfig = new HttpConfig(options.httpConfig);
options.databaseConfigs = options.databaseConfigs || [];
return options;
}

View File

@ -22,13 +22,13 @@
@click="rerun(testId)">
{{ $t('report.test_execute_again') }}
</el-button>
<!-- <el-button :disabled="isReadOnly" type="info" plain size="mini" @click="exports(reportName)">
<el-button :disabled="isReadOnly" type="info" plain size="mini" @click="exportReport(reportName)">
{{$t('report.export')}}
</el-button>-->
<!--
<el-button :disabled="isReadOnly" type="warning" plain size="mini">
{{$t('report.compare')}}
</el-button>-->
</el-button>
<!--<el-button :disabled="isReadOnly" type="warning" plain size="mini">-->
<!--{{$t('report.compare')}}-->
<!--</el-button>-->
</el-row>
</el-col>
<el-col :span="8">
@ -54,10 +54,10 @@
<ms-report-test-overview :report="report" ref="testOverview"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_request_statistics')">
<ms-report-request-statistics :report="report"/>
<ms-report-request-statistics :report="report" ref="requestStatistics"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_error_log')">
<ms-report-error-log :report="report"/>
<ms-report-error-log :report="report" ref="errorLog"/>
</el-tab-pane>
<el-tab-pane :label="$t('report.test_log_details')">
<ms-report-log-details :report="report"/>
@ -65,6 +65,33 @@
</el-tabs>
</div>
<div class="report-export" v-show="reportExportVisible">
<!--<div class="report-export">-->
<el-card id="testOverview">
<template v-slot:header >
<slot name="header">
<span class="title">{{$t('report.test_overview')}}</span>
</slot>
</template>
<ms-report-test-overview :report="report" ref="testOverview"/>
</el-card>
<el-card id="requestStatistics" title="'requestStatistics'">
<template v-slot:header >
<slot name="header">
<span class="title">{{$t('report.test_request_statistics')}}</span>
</slot>
</template>
<ms-report-request-statistics :report="report" ref="requestStatistics"/>
</el-card>
<el-card id="errorLog" title="'errorLog'">
<template v-slot:header >
<slot name="header">
<span class="title">{{$t('report.test_error_log')}}</span>
</slot>
</template>
<ms-report-error-log :report="report" ref="errorLog"/>
</el-card>
</div>
</el-card>
<el-dialog :title="$t('report.test_stop_now_confirm')" :visible.sync="dialogFormVisible" width="30%">
@ -91,6 +118,9 @@ import MsContainer from "../../common/components/MsContainer";
import MsMainContainer from "../../common/components/MsMainContainer";
import {checkoutTestManagerOrTestUser} from "@/common/js/utils";
import {exportPdf} from "../../../../common/js/utils";
import html2canvas from 'html2canvas';
export default {
name: "PerformanceReportView",
@ -123,6 +153,8 @@ export default {
isReadOnly: false,
websocket: null,
dialogFormVisible: false,
reportExportVisible: false,
isShow: true,
testPlan: {testResourcePoolId: null}
}
},
@ -247,7 +279,43 @@ export default {
this.$set(this.report, "status", 'Completed');
this.initReportTimeInfo();
window.console.log("socket closed.");
}
},
exportReport(name) {
this.result = {loading: true};
let result = this.result;
result.loading = true;
this.reportExportVisible = true;
let promises = [];
let canvasList = new Array(3);
let reset = this.exportReportReset;
this.$nextTick(function () {
setTimeout(() => {
promises.push(this.getCanvasPromise('testOverview', 0, canvasList));
promises.push(this.getCanvasPromise('requestStatistics', 1, canvasList));
promises.push(this.getCanvasPromise('errorLog', 2, canvasList));
Promise.all(promises).then(function (info) {
exportPdf(name, canvasList);
result.loading = false;
reset();
});
}, 1000);
})
},
exportReportReset() {
this.reportExportVisible = false;
this.isShow = true;
},
getCanvasPromise(id, index, canvasList) {
return new Promise(function(resolve, reject) {
html2canvas(document.getElementById(id), {
scale: 2
}).then(function(canvas) {
canvasList[index] = canvas;
resolve('success');
});
});
},
},
created() {
this.isReadOnly = false;
@ -305,14 +373,18 @@ export default {
<style scoped>
.ms-report-view-btns {
margin-top: 15px;
}
.ms-report-view-btns {
margin-top: 15px;
}
.ms-report-time-desc {
text-align: left;
display: block;
color: #5C7878;
}
.ms-report-time-desc {
text-align: left;
display: block;
color: #5C7878;
}
.report-export .el-card {
margin-bottom: 15px;
}
</style>

View File

@ -96,8 +96,7 @@
<div>{{ $t('load_test.response_timeout') }}</div>
</el-form-item>
<el-form-item>
<el-input-number :disabled="readOnly" size="mini" v-model="responseTimeout" :min="10"
:max="100000"></el-input-number>
<el-input-number :disabled="readOnly" size="mini" v-model="responseTimeout"></el-input-number>
</el-form-item>
<el-form-item>
ms

View File

@ -32,12 +32,12 @@
<el-submenu v-permission="['test_manager','test_user','test_viewer']"
index="8" popper-class="submenu">
<template v-slot:title>用例评审</template>
<template v-slot:title>{{$t('test_track.review.test_review')}}</template>
<ms-recent-list ref="reviewRecent" :options="reviewRecent"/>
<el-divider/>
<ms-show-all :index="'/track/review/all'"/>
<el-menu-item :index="testCaseReviewEditPath" class="blank_item"/>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/track/review/create'" title="创建用例评审"/>
<ms-create-button v-permission="['test_manager','test_user']" :index="'/track/review/create'" :title="$t('test_track.review.create_review')"/>
</el-submenu>
<el-submenu v-permission="['test_manager','test_user','test_viewer']" index="7" popper-class="submenu">
@ -94,7 +94,7 @@ export default {
}
},
reviewRecent: {
title: "最近的评审",
title: this.$t('test_track.recent_review'),
url: "/test/case/review/recent/5",
index: function (item) {
return '/track/review/view/' + item.id;

View File

@ -13,12 +13,12 @@
<!--报告-->
<div v-if="metric">
<base-info-component :report-info="metric" v-if="preview.id == 1"/>
<test-result-component :test-results="metric.moduleExecuteResult" v-if="preview.id == 2"/>
<test-result-chart-component :execute-result="metric.executeResult" v-if="preview.id == 3"/>
<failure-result-component :failure-test-cases="metric.failureTestCases" v-if="preview.id == 4"/>
<defect-list-component :defect-list="metric.issues" v-if="preview.id == 5"/>
<rich-text-component :is-report-view="isReportView" :preview="preview" v-if="preview.type != 'system'"/>
<base-info-component id="baseInfoComponent" :report-info="metric" v-if="preview.id == 1"/>
<test-result-component id="testResultComponent" :test-results="metric.moduleExecuteResult" v-if="preview.id == 2"/>
<test-result-chart-component id="resultChartComponent" :execute-result="metric.executeResult" v-if="preview.id == 3"/>
<failure-result-component id="failureResultComponent" :failure-test-cases="metric.failureTestCases" v-if="preview.id == 4"/>
<defect-list-component id="defectListComponent" :defect-list="metric.issues" v-if="preview.id == 5"/>
<rich-text-component id="richTextComponent" :is-report-view="isReportView" :preview="preview" v-if="preview.type != 'system'"/>
</div>
</div>
@ -31,6 +31,8 @@
import RichTextComponent from "./RichTextComponent";
import FailureResultComponent from "./FailureResultComponent";
import DefectListComponent from "./DefectListComponent";
import html2canvas from 'html2canvas';
export default {
name: "TemplateComponent",
components: {
@ -51,6 +53,41 @@
type: Boolean,
default: true
},
index: {
type: Number,
default: 0
},
},
methods: {
getCanvas(canvasList) {
let index = this.index;
let componentId = this.getComponentId();
return new Promise(function(resolve, reject) {
html2canvas(document.getElementById(componentId), {
scale: 2
}).then(function(canvas) {
//
canvasList[index] = canvas;
resolve('success');
});
});
},
getComponentId() {
switch (this.preview.id) {
case 1:
return "baseInfoComponent";
case 2:
return "testResultComponent";
case 3:
return "resultChartComponent";
case 4:
return "failureResultComponent";
case 5:
return "defectListComponent";
default:
return "richTextComponent";
}
},
}
}
</script>

View File

@ -25,38 +25,33 @@
<el-button :disabled="!isTestManagerOrTestUser" plain size="mini" @click="handleEdit">
{{$t('test_track.plan_view.edit_component')}}
</el-button>
<!--<el-button :disabled="!isTestManagerOrTestUser" plain size="mini" @click="handleExport(report.name)">
<el-button :disabled="!isTestManagerOrTestUser" plain size="mini" @click="handleExport(report.name)">
{{$t('test_track.plan_view.export_report')}}
</el-button>-->
</el-button>
</el-col>
</el-row>
<div class="container" ref="resume" id="app">
<el-main>
<div class="preview" v-for="item in previews" :key="item.id">
<template-component :isReportView="true" :metric="metric" :preview="item"/>
<div v-for="(item, index) in previews" :key="item.id" id="reportViewpp">
<template-component :isReportView="true" :metric="metric" :preview="item" :index="index" ref="templateComponent"/>
</div>
</el-main>
</div>
</template>
</el-drawer>
<test-case-report-template-edit :metric="metric" ref="templateEdit" @refresh="getReport"/>
<!-- <script>
</script>-->
</div>
</template>
<script>
import {checkoutTestManagerOrTestUser, jsonToMap, mapToJson} from "../../../../../../../common/js/utils";
import {checkoutTestManagerOrTestUser, exportPdf, jsonToMap, mapToJson} from "../../../../../../../common/js/utils";
import BaseInfoComponent from "./TemplateComponent/BaseInfoComponent";
import TestResultChartComponent from "./TemplateComponent/TestResultChartComponent";
import TestResultComponent from "./TemplateComponent/TestResultComponent";
import RichTextComponent from "./TemplateComponent/RichTextComponent";
import TestCaseReportTemplateEdit from "./TestCaseReportTemplateEdit";
import TemplateComponent from "./TemplateComponent/TemplateComponent";
import writer from 'file-writer'
import ReportStyle from "../../../../../../../common/css/report.css.js";
export default {
name: "TestCaseReportView",
@ -178,7 +173,7 @@
},
getMetric() {
this.result = this.$get('/test/plan/get/metric/' + this.planId, response => {
this.metric = response.data
this.metric = response.data;
if (!this.metric.failureTestCases) {
this.metric.failureTestCases = [];
@ -205,31 +200,22 @@
},
/*导出报告*/
handleExport(name) {
let html = this.getHtml();
writer(`${name}.html`, html, 'utf-8');
console.log(html)
},
getHtml() {
let template = this.$refs.resume.innerHTML;
let html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>html</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<style>${ReportStyle}</style>
</head>
<body>
<div style="margin:0 auto;width:1200px">
${template}
</div>
<script src="https://cdn.bootcss.com/element-ui/2.4.11/index.js"/>
</body>
</html>`
return html
},
let result = this.result;
result.loading = true;
let promises = [];
let canvasList = new Array(this.previews.length);
for (let item of this.$refs.templateComponent) {
promises.push(item.getCanvas(canvasList));
}
Promise.all(promises).then(function (info) {
exportPdf(name, canvasList);
result.loading = false;
});
},
}
}
</script>

View File

@ -3,7 +3,7 @@
<div>
<el-dialog :close-on-click-modal="false"
:title="operationType === 'edit' ? '编辑用例评审' : '创建用例评审'"
:title="operationType === 'edit' ? $t('test_track.review.edit_review') : $t('test_track.review.create_review')"
:visible.sync="dialogFormVisible"
@close="close"
v-loading="result.loading"
@ -14,8 +14,8 @@
<el-row>
<el-col :span="8" :offset="1">
<el-form-item
placeholder="请输入评审标题"
label="评审标题"
:placeholder="$t('test_track.review.input_review_name')"
:label="$t('test_track.review.review_name')"
:label-width="formLabelWidth"
prop="name">
<el-input v-model="form.name"/>
@ -23,10 +23,10 @@
</el-col>
<el-col :span="11" :offset="2">
<el-form-item :label="$t('test_track.plan.plan_project')" :label-width="formLabelWidth" prop="projectIds">
<el-form-item :label="$t('test_track.review.review_project')" :label-width="formLabelWidth" prop="projectIds">
<el-select
v-model="form.projectIds"
:placeholder="$t('test_track.plan.input_plan_project')"
:placeholder="$t('test_track.review.input_review_project')"
multiple
style="width: 100%"
collapse-tags
@ -44,10 +44,10 @@
<el-row>
<el-col :span="10" :offset="1">
<el-form-item label="评审人" :label-width="formLabelWidth" prop="principal">
<el-form-item :label="$t('test_track.review.reviewer')" :label-width="formLabelWidth" prop="userIds">
<el-select
v-model="form.userIds"
placeholder="请选择评审人"
:placeholder="$t('test_track.review.input_reviewer')"
filterable multiple
collapse-tags
>
@ -62,7 +62,7 @@
</el-col>
<el-col :span="10">
<el-form-item label="截止时间" :label-width="formLabelWidth" prop="endTime">
<el-form-item :label="$t('test_track.review.end_time')" :label-width="formLabelWidth" prop="endTime">
<el-date-picker @change="endTimeChange" type="datetime" :placeholder="$t('commons.select_date')"
v-model="form.endTime"/>
</el-form-item>
@ -84,7 +84,7 @@
<el-row v-if="operationType == 'edit'" type="flex" justify="left" style="margin-top: 10px;">
<el-col :span="19" :offset="1">
<el-form-item label="当前状态" :label-width="formLabelWidth" prop="status">
<el-form-item :label="$t('test_track.review.review_status')" :label-width="formLabelWidth" prop="status">
<test-plan-status-button :status="form.status" @statusChange="statusChange"/>
</el-form-item>
</el-col>

View File

@ -3,8 +3,8 @@
<template v-slot:header>
<ms-table-header :is-tester-permission="true" :condition.sync="condition"
@search="initTableData" @create="testCaseReviewCreate"
create-tip="创建用例评审"
title="用例评审"/>
:create-tip="$t('test_track.review.create_review')"
:title="$t('test_track.review.test_review')"/>
</template>
<el-table
@ -16,28 +16,28 @@
@row-click="intoReview">
<el-table-column
prop="name"
label="评审名称"
:label="$t('test_track.review.review_name')"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="reviewer"
label="评审人"
:label="$t('test_track.review.reviewer')"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="projectName"
label="所属项目"
:label="$t('test_track.review.review_project')"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="creator"
label="发起人"
:label="$t('test_track.review.review_creator')"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="status"
column-key="status"
:label="$t('test_track.plan.plan_status')"
:label="$t('test_track.review.review_status')"
show-overflow-tooltip>
<template v-slot:default="scope">
<span class="el-dropdown-link">
@ -55,7 +55,7 @@
</el-table-column>
<el-table-column
prop="endTime"
label="截止时间"
:label="$t('test_track.review.end_time')"
show-overflow-tooltip>
<template v-slot:default="scope">
<span>{{ scope.row.endTime | timestampFormatDate }}</span>
@ -77,7 +77,7 @@
<ms-table-pagination :change="initTableData" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
<ms-delete-confirm title="取消用例关联" @delete="_handleDelete" ref="deleteConfirm"/>
<ms-delete-confirm :title="$t('test_track.review.delete')" @delete="_handleDelete" ref="deleteConfirm"/>
</el-card>
</template>

View File

@ -5,7 +5,7 @@
<select-menu
:data="testReviews"
:current-data="currentReview"
title="评审"
:title="$t('test_track.review_view.review')"
@dataChange="changeReview"/>
<node-tree class="node-tree"
v-loading="result.loading"

View File

@ -2,7 +2,7 @@
<div>
<el-dialog title="关联测试评审"
<el-dialog :title="$t('test_track.review_view.relevance_case')"
:visible.sync="dialogFormVisible"
@close="close"
width="60%" v-loading="result.loading"

View File

@ -5,13 +5,13 @@
<ms-table-header :is-tester-permission="true" :condition.sync="condition" @search="initTableData"
:show-create="false" :tip="$t('commons.search_by_name_or_id')">
<template v-slot:title>
<node-breadcrumb class="table-title" :nodes="selectParentNodes" @refresh="refresh" title="全部评审"/>
<node-breadcrumb class="table-title" :nodes="selectParentNodes" @refresh="refresh" :title="$t('test_track.review_view.all_review')"/>
</template>
<template v-slot:button>
<ms-table-button :is-tester-permission="true" icon="el-icon-video-play"
content="开始用例评审" @click="startReview"/>
:content="$t('test_track.review_view.start_review')" @click="startReview"/>
<ms-table-button :is-tester-permission="true" icon="el-icon-connection"
content="关联用例评审"
:content="$t('test_track.review_view.relevance_case')"
@click="$emit('openTestReviewRelevanceDialog')"/>
</template>
</ms-table-header>
@ -91,13 +91,13 @@
<el-table-column
prop="projectName"
:label="$t('test_track.plan.plan_project')"
:label="$t('test_track.review.review_project')"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="reviewerName"
label="评审人"
:label="$t('test_track.review.review_creator')"
show-overflow-tooltip
>
</el-table-column>
@ -106,7 +106,7 @@
prop="status"
:filters="statusFilters"
column-key="status"
:label="$t('test_track.plan_view.execute_result')">
:label="$t('test_track.review_view.execute_result')">
<template v-slot:default="scope">
<span class="el-dropdown-link">
<status-table-item :value="scope.row.status"/>
@ -348,41 +348,10 @@ export default {
});
},
handleSelectAll(selection) {
if (selection.length > 0) {
if (selection.length === 1) {
this.selectRows.add(selection[0]);
} else {
this.tableData.forEach(item => {
this.$set(item, "showMore", true);
this.selectRows.add(item);
});
}
} else {
this.selectRows.clear();
this.tableData.forEach(row => {
this.$set(row, "showMore", false);
})
}
},
handleSelectionChange(selection, row) {
if (this.selectRows.has(row)) {
this.$set(row, "showMore", false);
this.selectRows.delete(row);
} else {
this.$set(row, "showMore", true);
this.selectRows.add(row);
}
let arr = Array.from(this.selectRows);
// 1
if (this.selectRows.size === 1) {
this.$set(arr[0], "showMore", false);
} else if (this.selectRows.size === 2) {
arr.forEach(row => {
this.$set(row, "showMore", true);
})
}
},
handleBatch(type) {
if (this.selectRows.size < 1) {

@ -1 +1 @@
Subproject commit f2d5a342c82e629f510550d5778d752bb73bf5e7
Subproject commit 0a375848d034d20eaf05caf11769e1c75c39235c

View File

@ -9,6 +9,7 @@ import {
LicenseKey
} from "./constants";
import axios from "axios";
import {jsPDF} from "jspdf";
export function hasRole(role) {
let user = getCurrentUser();
@ -203,3 +204,58 @@ export function getUUID() {
}
export function exportPdf(name, canvasList) {
let pdf = new jsPDF('', 'pt', 'a4');
// 当前页面的当前高度
let currentHeight = 0;
for (let canvas of canvasList) {
if (canvas) {
let contentWidth = canvas.width;
let contentHeight = canvas.height;
//a4纸的尺寸[595.28,841.89]
let a4Width = 592.28;
let a4Height = 841.89;
// html页面生成的canvas在pdf中图片的宽高
let imgWidth = a4Width;
let imgHeight = a4Width/contentWidth * contentHeight;
let pageData = canvas.toDataURL('image/jpeg', 1.0);
// 当前图片的剩余高度
let leftHeight = imgHeight;
// 当前页面的剩余高度
let blankHeight = a4Height - currentHeight;
if (leftHeight > blankHeight) {
//页面偏移
let position = 0;
while(leftHeight > 0) {
// 本次添加占用的高度
let occupation = a4Height - currentHeight;
pdf.addImage(pageData, 'JPEG', 0, position + currentHeight, imgWidth, imgHeight);
currentHeight = leftHeight;
leftHeight -= occupation;
position -= occupation;
//避免添加空白页
if(leftHeight > 0) {
pdf.addPage();
currentHeight = 0;
}
}
} else {
pdf.addImage(pageData, 'JPEG', 0, currentHeight, imgWidth, imgHeight);
currentHeight += imgHeight;
}
}
}
pdf.save(name);
}

View File

@ -617,6 +617,7 @@ export default {
length_less_than: "The length less than",
recent_plan: "Recent plan",
recent_case: "Recent case",
recent_review: "Recent review",
pass_rate: "Pass rate",
execution_result: ": Please select the execution result",
actual_result: ": The actual result is empty",
@ -722,6 +723,28 @@ export default {
plan_delete_confirm: "All use cases under this plan will be deleted,confirm delete test plan: ",
plan_delete: "Delete test plan",
},
review: {
test_review: "Test Review",
create_review: "Create Review",
edit_review: "Edit Review",
review_name: "Name",
reviewer: "Reviewer",
review_project: "Project",
review_creator: "Creator",
review_status: "Status",
end_time: "EndTime",
delete: "Delete",
input_review_name: "Please enter the name of the review",
input_review_project: "Please select the project",
input_reviewer: "Please select reviewer",
},
review_view: {
review: "Review",
all_review: "All Review",
start_review: "Start Review",
relevance_case: "Relevance Case",
execute_result: "Result",
},
module: {
search: "Search module",
rename: "Rename",

View File

@ -619,6 +619,7 @@ export default {
length_less_than: "长度必须小于",
recent_plan: "最近的计划",
recent_case: "最近的用例",
recent_review: "最近的评审",
pass_rate: "通过率",
execution_result: ": 请选择执行结果",
actual_result: ": 实际结果为空",
@ -725,6 +726,28 @@ export default {
plan_delete_confirm: "将删除该测试计划下所有用例,确认删除测试计划: ",
plan_delete: "删除计划",
},
review: {
test_review: "用例评审",
create_review: "创建用例评审",
edit_review: "编辑用例评审",
review_name: "评审名称",
reviewer: "评审人",
review_project: "所属项目",
review_creator: "发起人",
review_status: "当前状态",
end_time: "截止时间",
delete: "删除评审",
input_review_name: "请输入评审名称",
input_review_project: "请选择所属项目",
input_reviewer: "请选择评审人",
},
review_view: {
review: "评审",
all_review: "全部评审",
start_review: "开始评审",
relevance_case: "关联用例",
execute_result: "执行结果",
},
module: {
search: "搜索模块",
rename: "重命名",

View File

@ -619,6 +619,7 @@ export default {
length_less_than: "長度必須小於",
recent_plan: "最近的計劃",
recent_case: "最近的用例",
recent_review: "最近的評審",
pass_rate: "通過率",
execution_result: ": 請選擇執行結果",
actual_result: ": 實際結果為空",
@ -725,6 +726,28 @@ export default {
plan_delete_confirm: "將刪除該測試計劃下所有用例,確認刪除測試計劃: ",
plan_delete: "刪除計劃",
},
review: {
test_review: "用例評審",
create_review: "創建用例評審",
edit_review: "編輯用例評審",
review_name: "評審名稱",
reviewer: "評審人",
review_project: "所屬項目",
review_creator: "發起人",
review_status: "當前狀態",
end_time: "截止時間",
delete: "刪除評審",
input_review_name: "請輸入評審名稱",
input_review_project: "請選擇所屬項目",
input_reviewer: "請選擇評審人",
},
review_view: {
review: "評審",
all_review: "全部評審",
start_review: "開始評審",
relevance_case: "關聯用例",
execute_result: "執行結果",
},
module: {
search: "搜索模塊",
rename: "重命名",

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.metersphere</groupId>
<artifactId>metersphere-server</artifactId>
<version>1.1</version>
<version>1.3</version>
<packaging>pom</packaging>
<parent>