This commit is contained in:
liqiang-fit2cloud 2022-12-11 16:07:11 +08:00
commit e812265f40
290 changed files with 23430 additions and 2605 deletions

4
Jenkinsfile vendored
View File

@ -97,7 +97,7 @@ pipeline {
LOCAL_REPOSITORY=$(./mvnw help:evaluate -Dexpression=settings.localRepository --settings ./settings.xml -q -DforceStdout)
libraries=('api-test' 'performance-test' 'project-management' 'system-setting' 'test-track' 'report-stat')
libraries=('api-test' 'performance-test' 'project-management' 'system-setting' 'test-track' 'report-stat' 'workstation')
for library in "${libraries[@]}";
do
mkdir -p $library/backend/target/dependency && (cd $library/backend/target/dependency; jar -xf ../*.jar; cp $LOCAL_REPOSITORY/io/metersphere/metersphere-xpack/${REVISION}/metersphere-xpack-${REVISION}.jar ./BOOT-INF/lib/)
@ -119,7 +119,7 @@ pipeline {
try {
sh '''#!/bin/bash -xe
cd ${WORKSPACE}
libraries=('framework/eureka' 'framework/gateway' 'api-test' 'performance-test' 'project-management' 'report-stat' 'system-setting' 'test-track')
libraries=('framework/eureka' 'framework/gateway' 'api-test' 'performance-test' 'project-management' 'report-stat' 'system-setting' 'test-track' 'workstation')
for library in "${libraries[@]}";
do
IMAGE_NAME=${library#*/}

View File

@ -110,13 +110,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>

View File

@ -554,8 +554,9 @@ public class MsHTTPSamplerProxy extends MsTestElement {
try {
String value = keyValue.getValue() != null && keyValue.getValue().startsWith("@") ?
ScriptEngineUtils.buildFunctionCallString(keyValue.getValue()) : keyValue.getValue();
value = keyValue.isUrlEncode() ? "${__urlencode(" + value + ")}" : value;
keyValueMap.put(keyValue.getName(), value);
value = keyValue.isUrlEncode() ? StringUtils.join(StringUtils.join("${__urlencode(", value), ")}") : value;
String key = keyValue.isUrlEncode() ? StringUtils.join(StringUtils.join("${__urlencode(", keyValue.getName()), ")}") : keyValue.getName();
keyValueMap.put(key, value);
} catch (Exception e) {
LogUtil.error(e);
}
@ -585,11 +586,11 @@ public class MsHTTPSamplerProxy extends MsTestElement {
stringBuffer.append("?");
}
this.getArguments().stream().filter(KeyValue::isEnable).filter(KeyValue::isValid).forEach(keyValue -> {
stringBuffer.append(keyValue.getName());
stringBuffer.append(keyValue.isUrlEncode() ? StringUtils.join(StringUtils.join("${__urlencode(", keyValue.getName()), ")}") : keyValue.getName());
if (keyValue.getValue() != null) {
try {
String value = keyValue.getValue().startsWith("@") ? ScriptEngineUtils.buildFunctionCallString(keyValue.getValue()) : keyValue.getValue();
value = keyValue.isUrlEncode() ? "${__urlencode(" + value + ")}" : value;
value = keyValue.isUrlEncode() ? StringUtils.join(StringUtils.join("${__urlencode(", value), ")}") : value;
if (StringUtils.isNotEmpty(value) && value.contains(StringUtils.CR)) {
value = value.replaceAll(StringUtils.CR, StringUtils.EMPTY);
}

View File

@ -2,12 +2,14 @@ package io.metersphere.api.exec.engine;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.metersphere.api.dto.MsgDTO;
import io.metersphere.base.domain.TestResource;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.constants.ExtendedParameter;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.JSON;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.WebSocketUtil;
import io.metersphere.dto.JmeterRunRequestDTO;
import io.metersphere.utils.LoggerUtil;
import io.metersphere.xpack.resourcepool.engine.provider.ClientCredential;
@ -21,6 +23,7 @@ import java.util.Set;
public class KubernetesTestEngine extends AbstractEngine {
private JmeterRunRequestDTO runRequest;
private final String DEBUG_ERROR = "DEBUG_ERROR";
// 初始化API调用
public KubernetesTestEngine(JmeterRunRequestDTO runRequest) {
@ -77,6 +80,14 @@ public class KubernetesTestEngine extends AbstractEngine {
command.append(StringUtils.SPACE).append("http://127.0.0.1:8082/jmeter/").append(path);
KubernetesApiExec.newExecWatch(client, clientCredential.getNamespace(), pod.getMetadata().getName(), command.toString());
} catch (Exception e) {
MsgDTO dto = new MsgDTO();
dto.setExecEnd(false);
dto.setContent(DEBUG_ERROR);
dto.setReportId("send." + runRequest.getReportId());
dto.setToReport(runRequest.getReportId());
LoggerUtil.debug("send. " + runRequest.getReportId());
WebSocketUtil.sendMessageSingle(dto);
WebSocketUtil.onClose(runRequest.getReportId());
LoggerUtil.error("当前报告:【" + runRequest.getReportId() + "】资源:【" + runRequest.getTestId() + "】CURL失败", e);
MSException.throwException(e);
}

View File

@ -333,10 +333,14 @@ public class ApiTestCaseService {
extApiDefinitionExecResultMapper.deleteByResourceId(testId);
apiTestCaseMapper.deleteByPrimaryKey(testId);
esbApiParamService.deleteByResourceId(testId);
deleteBodyFiles(testId);
// 删除附件关系
extFileAssociationService.deleteByResourceId(testId);
deleteFollows(testId);
ApiTestCase apiTestCase = apiTestCaseMapper.selectByPrimaryKey(testId);
if (apiTestCase != null) {
String filePath = StringUtils.join(BODY_FILE_DIR, File.separator, apiTestCase.getId());
FileUtil.deleteContents(new File(filePath));
}
}
private void deleteFollows(String testId) {
@ -362,14 +366,6 @@ public class ApiTestCaseService {
}
}
public void deleteBodyFiles(String testId) {
File file = new File(BODY_FILE_DIR + "/" + testId);
FileUtil.deleteContents(file);
if (file.exists()) {
file.delete();
}
}
public void checkNameExist(SaveApiTestCaseRequest request) {
if (hasSameCase(request)) {
MSException.throwException(Translator.get("case_name_is_already_exist") + ": " + request.getName());

View File

@ -503,6 +503,14 @@ export default {
this.runningEvaluation(e.data);
this.sort(this.fullTreeNodes);
}
if (e && e.data === 'DEBUG_ERROR') {
this.$error(this.$t('api_definition.debug_pool_warning'));
this.messageWebSocket.close();
this.cleanHeartBeat();
this.$EventBus.$emit('hide', this.scenarioId);
this.$emit('refresh', this.debugResult);
}
if (e.data && e.data.indexOf('MS_TEST_END') !== -1) {
this.getReport();
this.messageWebSocket.close();

View File

@ -40,7 +40,7 @@
<pre>{{ request.headers }}</pre>
</div>
<div class="ms-div" v-if="request.url && request.url !== ''">
Cookies :
Cookie :
<pre>{{ request.cookies }}</pre>
</div>
<div class="ms-div">

View File

@ -3,7 +3,6 @@
<slot name="version"></slot>
<ms-search :condition.sync="condition" :base-search-tip="$t('commons.search_by_id_name_tag')" @search="search">
</ms-search>
<ms-table
:data="tableData"
:screen-height="isRelate ? 'calc(100vh - 400px)' : screenHeight"

View File

@ -1233,6 +1233,11 @@ export default {
if (e && e.data === 'CONN_SUCCEEDED') {
this.run();
}
if (e && e.data === 'DEBUG_ERROR') {
this.$error(this.$t('api_definition.debug_pool_warning'));
this.messageWebSocket.close();
this.errorRefresh();
}
if (e.data && e.data.startsWith('result_')) {
let data = JSON.parse(e.data.substring(7));
this.reqTotal += 1;

View File

@ -292,6 +292,13 @@ export default {
if (e && e.data === 'CONN_SUCCEEDED') {
this.runDebug();
}
if (e && e.data === 'DEBUG_ERROR') {
this.$error(this.$t('api_definition.debug_pool_warning'));
this.loading = false;
this.node.expanded = true;
this.messageWebSocket.close();
this.reload();
}
if (e.data && e.data.startsWith('result_')) {
let data = JSON.parse(e.data.substring(7));
this.debugCode(data);

View File

@ -31,6 +31,7 @@
v-if="type === 'body'"
:disabled="isReadOnly"
v-model="scope.row.type"
size="mini"
@change="typeChange(item)">
<el-option value="text" />
<el-option value="file" />
@ -150,8 +151,16 @@
circle
@click="remove(scope.$index)"
:disabled="isDisable(scope.$index) || isReadOnly" />
<i class="el-icon-top" style="cursor: pointer" @click="moveTop(scope.$index)" />
<i class="el-icon-bottom" style="cursor: pointer" @click="moveBottom(scope.$index)" />
<i
class="el-icon-top"
v-show="!(isDisable(scope.$index) || isReadOnly)"
style="cursor: pointer"
@click="moveTop(scope.$index)" />
<i
class="el-icon-bottom"
v-show="!(isDisable(scope.$index) || isReadOnly)"
style="cursor: pointer"
@click="moveBottom(scope.$index)" />
</template>
</el-table-column>
</el-table>

View File

@ -62,7 +62,11 @@ export default {
if (e && e.data === 'CONN_SUCCEEDED') {
this.run();
}
if (e && e.data === 'DEBUG_ERROR') {
this.$error(this.$t('api_definition.debug_pool_warning'));
this.websocket.close();
this.$emit('errorRefresh', '');
}
if (e.data && e.data.startsWith('result_')) {
try {
let data = e.data.substring(7);

View File

@ -53,6 +53,7 @@
:isStop="isStop"
:run-data="runData"
@runRefresh="runRefresh"
@errorRefresh="errorRefresh"
ref="runTest" />
</el-card>
<div v-if="scenario">
@ -188,11 +189,14 @@ export default {
this.reportId = getUUID().substring(0, 8);
},
runRefresh(data) {
this.responseData = data;
this.responseData = data || { type: 'DUBBO', responseResult: {}, subRequestResults: [] };
this.loading = false;
this.isStop = false;
this.$refs.debugResult.reload();
},
errorRefresh() {
this.runRefresh();
},
saveAsApi() {
let obj = { request: this.request };
obj.request.id = getUUID();

View File

@ -74,6 +74,7 @@
:isStop="isStop"
:run-data="runData"
@runRefresh="runRefresh"
@errorRefresh="errorRefresh"
ref="runTest" />
</div>
</el-card>
@ -240,13 +241,16 @@ export default {
this.$emit('refreshModule');
},
runRefresh(data) {
this.responseData = data;
this.responseData = data || { type: 'HTTP', responseResult: {}, subRequestResults: [] };
this.loading = false;
this.isStop = false;
if (this.$refs.debugResult) {
this.$refs.debugResult.reload();
}
},
errorRefresh() {
this.runRefresh();
},
saveAsApi() {
this.$refs['debugForm'].validate((valid) => {
if (valid) {

View File

@ -54,6 +54,7 @@
:isStop="isStop"
:run-data="runData"
@runRefresh="runRefresh"
@errorRefresh="errorRefresh"
ref="runTest" />
</el-card>
<div v-if="scenario">
@ -194,13 +195,16 @@ export default {
this.reportId = getUUID().substring(0, 8);
},
runRefresh(data) {
this.responseData = data;
this.responseData = data || { type: 'JDBC', responseResult: {}, subRequestResults: [] };
this.loading = false;
this.isStop = false;
if (this.$refs.debugResult) {
this.$refs.debugResult.reload();
}
},
errorRefresh() {
this.runRefresh();
},
saveAsApi() {
let obj = { request: this.request };
obj.request.id = getUUID();

View File

@ -56,6 +56,7 @@
:isStop="isStop"
:run-data="runData"
@runRefresh="runRefresh"
@errorRefresh="errorRefresh"
ref="runTest" />
</el-card>
<div v-if="scenario">
@ -193,13 +194,16 @@ export default {
this.reportId = getUUID().substring(0, 8);
},
runRefresh(data) {
this.responseData = data;
this.responseData = data || { type: 'TCP', responseResult: {}, subRequestResults: [] };
this.loading = false;
this.isStop = false;
if (this.$refs.debugResult) {
this.$refs.debugResult.reload();
}
},
errorRefresh() {
this.runRefresh();
},
saveAsApi() {
let obj = { request: this.request };
obj.request.id = getUUID();

View File

@ -7,6 +7,7 @@
<api-params-config
v-if="apiParamsConfigFields"
@refresh="refreshApiParamsField"
:storage-key="apiParamStorageKey"
:api-params-config-fields="apiParamsConfigFields" />
</div>
</el-row>
@ -69,40 +70,17 @@ export default {
apiInfo: Object,
},
activated() {
this.formatTableData();
this.initTableColumn();
},
created: function () {
this.formatTableData();
this.initTableColumn();
},
mounted() {
this.formatTableData();
this.initTableColumn();
},
computed: {},
watch: {},
methods: {
formatTableData() {
if (this.tableData) {
this.tableData.forEach((item) => {
if (item.urlEncode !== null && item.urlEncode !== undefined) {
if (item.urlEncode === true) {
item.urlEncode = this.$t('commons.yes');
} else {
item.urlEncode = this.$t('commons.no');
}
}
if (item.enable !== null && item.enable !== undefined) {
if (item.enable === true) {
item.enable = this.$t('commons.yes');
} else {
item.enable = this.$t('commons.no');
}
}
});
}
},
refreshApiParamsField() {
this.initTableColumn();
this.reloadedApiVariable = false;
@ -176,6 +154,24 @@ export default {
returnJsonArr.push(item);
}
}
returnJsonArr.forEach((item) => {
if (item.urlEncode !== null && item.urlEncode !== undefined) {
if (item.urlEncode === true) {
item.urlEncode = this.$t('commons.yes');
} else {
item.urlEncode = this.$t('commons.no');
}
}
if (item.enable !== null && item.enable !== undefined) {
if (item.enable === true) {
item.enable = this.$t('commons.yes');
} else {
item.enable = this.$t('commons.no');
}
}
});
return returnJsonArr;
},
formatBoolean(row, column, cellValue) {

View File

@ -214,7 +214,7 @@ export default {
':\n' +
this.response.headers +
'\n' +
'Cookies :\n' +
'Cookie :' +
this.response.cookies +
'\n' +
'Body:' +

View File

@ -234,7 +234,7 @@ export default {
':\n' +
this.response.headers +
'\n' +
'Cookies :\n' +
'Cookie :' +
this.response.cookies +
'\n' +
'Body:' +

View File

@ -166,7 +166,7 @@ export default {
}
if (this.response.cookies) {
this.reqMessages += 'Cookies :\n' + this.response.cookies + '\n';
this.reqMessages += 'Cookie :' + this.response.cookies + '\n';
}
this.reqMessages += 'Body:' + '\n' + this.response.body;
}

View File

@ -453,13 +453,24 @@ export default {
this.runLoading = false;
this.checkVersionEnable();
},
margeFiles(targetFiles, sourceFiles) {
targetFiles.forEach((target) => {
sourceFiles.forEach((source) => {
if (target.uuid === source.uuid) {
source.file = target.file;
}
});
});
},
initLocalFile() {
if (this.apiData.request && this.apiData.request.body) {
if (this.apiData.request.body.binary && this.apiData.request.body.binary.length > 0) {
this.apiData.request.body.binary.forEach((item) => {
this.api.request.body.binary.forEach((api) => {
if (item.uuid && api.uuid && item.uuid === api.uuid) {
api = item;
api.files = item.files;
} else if (item.files && api.files) {
this.margeFiles(item.files, api.files);
}
});
});
@ -469,6 +480,8 @@ export default {
this.api.request.body.kvs.forEach((api) => {
if (item.uuid && api.uuid && item.uuid === api.uuid && item.files && api.files) {
api.files = item.files;
} else if (item.files && api.files) {
this.margeFiles(item.files, api.files);
}
});
});
@ -476,6 +489,7 @@ export default {
}
},
},
created() {
this.init();
},

View File

@ -95,10 +95,10 @@ export default {
borderWidth = 3;
dataIsNotEmpty = true;
protocolData = [
{ value: this.apiData.httpCount, name: 'HTTP' },
{ value: this.apiData.rpcCount, name: 'RPC' },
{ value: this.apiData.tcpCount, name: 'TCP' },
{ value: this.apiData.sqlCount, name: 'SQL' },
{ value: this.apiData.httpCount > 0 ? this.apiData.httpCount : '-', name: 'HTTP' },
{ value: this.apiData.rpcCount > 0 ? this.apiData.rpcCount : '-', name: 'RPC' },
{ value: this.apiData.tcpCount > 0 ? this.apiData.tcpCount : '-', name: 'TCP' },
{ value: this.apiData.sqlCount > 0 ? this.apiData.sqlCount : '-', name: 'SQL' },
];
}
let optionData = {
@ -109,6 +109,7 @@ export default {
legend: {
orient: 'vertical',
icon: 'rect',
inactiveBorderWidth: 0.1,
selectedMode: dataIsNotEmpty,
itemGap: 16,
left: '50%',
@ -141,12 +142,14 @@ export default {
},
data: protocolData,
formatter: function (name) {
//name
let singleData = protocolData.filter(function (item) {
return item.name === name;
});
let value = singleData[0].value;
return [`{protocol|${name}}`, `{num|${value}}`].join('');
let showValue = singleData[0].value;
if (showValue === '-') {
showValue = 0;
}
return [`{protocol|${name}}`, `{num|${showValue}}`].join('');
},
},
title: {

View File

@ -7,6 +7,8 @@ const message = {
please_add_api_case: 'Please add api case',
},
api_definition: {
debug_pool_warning:
'Failed to call the resource pool, please check whether the configuration of the resource pool is normal',
document: {
name: 'name',
value: 'value',

View File

@ -7,6 +7,7 @@ const message = {
please_add_api_case: '请先添加接口用例',
},
api_definition: {
debug_pool_warning: '调用资源池执行失败,请检查资源池是否配置正常',
document: {
name: '名称',
value: '值',

View File

@ -7,6 +7,7 @@ const message = {
please_add_api_case: '请先添加接口用例',
},
api_definition: {
debug_pool_warning: '調用資源池執行失敗,請檢查資源池是否配置正常',
document: {
name: '名稱',
value: '值',

View File

@ -284,13 +284,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -21,7 +21,7 @@ public class SessionFilter implements WebFilter {
private static final String[] TO_SUB_SERVICE = new String[]{"/license", "/system", "/resource", "/sso/callback/logout", "/sso/callback/cas/logout"};
private static final String PERFORMANCE_DOWNLOAD_PREFIX = "/jmeter/";
private static final String API_DOWNLOAD_PREFIX = "/api/jmeter/";
private static final String TRACK_IMAGE_PREFIX = "/resource/md/get/url";
private static final String TRACK_IMAGE_PREFIX = "/resource/md/get/path";
@Resource
private DiscoveryClient discoveryClient;

View File

@ -48,6 +48,8 @@ import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
@ -278,6 +280,7 @@ public class SSOService {
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(userInfoUrl, HttpMethod.GET, httpEntity, String.class);
resultObj = JSON.parseObject(response.getBody(), new TypeReference<HashMap<String, Object>>() {});
LogUtil.info("user info: " + response.getBody());
} catch (Exception e) {
LogUtil.error("fail to get user info", e);
MSException.throwException("fail to get user info!");
@ -293,6 +296,12 @@ public class SSOService {
if (StringUtils.isBlank(userid)) {
MSException.throwException("userid is empty!");
}
Pattern p = Pattern.compile("[\u4e00-\u9fa5]");
Matcher m = p.matcher(userid);
if (m.find()) {
MSException.throwException("userid cannot contain Chinese characters!");
}
if (StringUtils.isBlank(username)) {
username = userid;
}

View File

@ -2,10 +2,8 @@ package io.metersphere.gateway.service;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*;
import io.metersphere.commons.constants.SessionConstants;
import io.metersphere.commons.constants.UserGroupType;
import io.metersphere.commons.constants.UserSource;
import io.metersphere.commons.constants.UserStatus;
import io.metersphere.base.mapper.ext.BaseProjectMapper;
import io.metersphere.commons.constants.*;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.user.SessionUser;
import io.metersphere.commons.utils.CodingUtil;
@ -39,6 +37,8 @@ public class UserLoginService {
private UserGroupPermissionMapper userGroupPermissionMapper;
@Resource
private ProjectMapper projectMapper;
@Resource
private BaseProjectMapper baseProjectMapper;
public Optional<SessionUser> login(LoginRequest request, WebSession session, Locale locale) {
UserDTO userDTO;
@ -173,31 +173,55 @@ public class UserLoginService {
private void checkNewWorkspaceAndProject(WebSession session, UserDTO user) {
List<UserGroup> userGroups = user.getUserGroups();
List<String> projectGroupIds = user.getGroups()
.stream().filter(ug -> StringUtils.equals(ug.getType(), UserGroupType.PROJECT))
List<Group> groups = user.getGroups();
List<String> projectGroupIds = groups
.stream()
.filter(ug -> StringUtils.equals(ug.getType(), UserGroupType.PROJECT))
.map(Group::getId)
.collect(Collectors.toList());
List<UserGroup> project = userGroups.stream().filter(ug -> projectGroupIds.contains(ug.getGroupId()))
List<UserGroup> projects = userGroups
.stream()
.filter(ug -> projectGroupIds.contains(ug.getGroupId()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(project)) {
List<String> workspaceIds = user.getGroups()
if (CollectionUtils.isEmpty(projects)) {
List<String> workspaceIds = groups
.stream()
.filter(ug -> StringUtils.equals(ug.getType(), UserGroupType.WORKSPACE))
.map(Group::getId)
.collect(Collectors.toList());
List<UserGroup> workspaces = userGroups.stream().filter(ug -> workspaceIds.contains(ug.getGroupId()))
List<UserGroup> workspaces = userGroups
.stream()
.filter(ug -> workspaceIds.contains(ug.getGroupId()))
.collect(Collectors.toList());
if (workspaces.size() > 0) {
String wsId = workspaces.get(0).getSourceId();
switchUserResource(session, "workspace", wsId, user);
} else {
// 用户登录之后没有项目和工作空间的权限就把值清空
user.setLastWorkspaceId("");
user.setLastProjectId("");
updateUser(user);
List<String> superGroupIds = groups
.stream()
.map(Group::getId)
.filter(id -> StringUtils.equals(id, UserGroupConstants.SUPER_GROUP))
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(superGroupIds)) {
Project p = baseProjectMapper.selectOne();
if (p != null) {
switchSuperUserResource(session, p.getId(), p.getWorkspaceId(), user);
}
} else {
// 用户登录之后没有项目和工作空间的权限就把值清空
user.setLastWorkspaceId(StringUtils.EMPTY);
user.setLastProjectId(StringUtils.EMPTY);
updateUser(user);
}
}
} else {
UserGroup userGroup = project.stream().filter(p -> StringUtils.isNotBlank(p.getSourceId()))
UserGroup userGroup = projects.stream()
.filter(p -> StringUtils.isNotBlank(p.getSourceId()))
.collect(Collectors.toList()).get(0);
String projectId = userGroup.getSourceId();
Project p = projectMapper.selectByPrimaryKey(projectId);
@ -231,6 +255,20 @@ public class UserLoginService {
userMapper.updateByPrimaryKeySelective(newUser);
}
private void switchSuperUserResource(WebSession session, String projectId, String workspaceId, UserDTO sessionUser) {
// 获取最新UserDTO
UserDTO user = getUserDTO(sessionUser.getId());
User newUser = new User();
user.setLastWorkspaceId(workspaceId);
sessionUser.setLastWorkspaceId(workspaceId);
user.setLastProjectId(projectId);
BeanUtils.copyProperties(user, newUser);
// 切换工作空间或组织之后更新 session 里的 user
session.getAttributes().put(SessionConstants.ATTR_USER, SessionUser.fromUser(user, session.getId()));
session.getAttributes().put(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, sessionUser.getId());
userMapper.updateByPrimaryKeySelective(newUser);
}
public UserDTO getLoginUser(String userId, List<String> list) {
UserExample example = new UserExample();
example.createCriteria().andIdEqualTo(userId).andSourceIn(list);

View File

@ -34,13 +34,7 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -250,6 +250,7 @@ export default {
itemStyle: {
borderWidth: 0.1
},
inactiveBorderWidth:0.1,
textStyle: {
rich: {
name: {

View File

@ -8,7 +8,7 @@
background-color="rgba(0,0,0,0)"
@select="handleSelect"
router>
<el-menu-item index="/workstation" v-xpack v-if="check('workstation')">
<el-menu-item index="/workstation" v-if="check('workstation')">
<div>
<svg-icon iconClass="workstation" class-name="ms-menu-img"/>
<span slot="title" class="ms-menu-item-title">{{ $t('commons.my_workstation') }}</span>

View File

@ -1,35 +0,0 @@
<template>
<el-form label-position="right" label-width="115px" size="small">
<el-form-item :label="'Jira '+ $t('commons.information')">
<ms-instructions-icon size="10" :content="$t('organization.integration.jira_prompt_information')"/>
</el-form-item>
<el-form-item :label="'Jira ' + $t('organization.integration.account')" prop="account">
<el-input v-model="data.jiraAccount" :placeholder="$t('organization.integration.input_api_account')"/>
</el-form-item>
<el-form-item label="Token" prop="password">
<el-input v-model="data.jiraPassword" auto-complete="new-password"
:placeholder="$t('organization.integration.input_api_password')" show-password/>
</el-form-item>
<el-form-item>
<el-button type="primary" style="float: right" @click="$emit('auth', 'Jira')" size="mini">
{{ $t('commons.validate') }}
</el-button>
</el-form-item>
</el-form>
</template>
<script>
import MsInstructionsIcon from "../MsInstructionsIcon";
export default {
name: "JiraUserInfo",
components: {MsInstructionsIcon},
props: ['data'],
}
</script>
<style scoped>
.instructions-icon {
margin-left: -5px;
}
</style>

View File

@ -27,10 +27,6 @@ import MsDialogFooter from "../MsDialogFooter";
import {removeGoBackListener} from "../../utils";
import MsTableOperatorButton from "../MsTableOperatorButton";
import {EMAIL_REGEX, PHONE_REGEX} from "../../utils/regex";
import JiraUserInfo from "./JiraUserInfo";
import TapdUserInfo from "./TapdUserInfo";
import ZentaoUserInfo from "./ZentaoUserInfo";
import AzureDevopsUserInfo from "./AzureDevopsUserInfo";
import {useUserStore} from "@/store";
import {updateInfo} from "../../api/user";
@ -38,7 +34,7 @@ const userStore = useUserStore();
export default {
name: "MsPersonFromSetting",
components: {ZentaoUserInfo, TapdUserInfo, JiraUserInfo, AzureDevopsUserInfo, MsDialogFooter, MsTableOperatorButton},
components: {MsDialogFooter, MsTableOperatorButton},
inject: [
'reload',
'reloadTopMenus'

View File

@ -12,7 +12,7 @@
class="setting-item"></el-tab-pane>
<el-tab-pane
v-if="hasPermission('PERSONAL_INFORMATION:READ+THIRD_ACCOUNT')
&&(platformAccountConfigs.length > 0 || hasTapd || hasZentao || hasAzure) && hasPermission('WORKSPACE_SERVICE:READ')"
&&(platformAccountConfigs.length > 0 || hasTapd || hasAzure) && hasPermission('WORKSPACE_SERVICE:READ')"
name="third_account" :label="$t('commons.third_account')" class="setting-item"></el-tab-pane>
<el-tab-pane v-if="hasPermission('PERSONAL_INFORMATION:READ+UI_SETTING') && isXpack" name="commons.ui_setting"
:label="$t('commons.ui_setting')"
@ -33,7 +33,6 @@
/>
</div>
<tapd-user-info @auth="handleAuth" v-if="hasTapd" :data="currentPlatformInfo"/>
<zentao-user-info @auth="handleAuth" v-if="hasZentao" :data="currentPlatformInfo"/>
<azure-devops-user-info @auth="handleAuth" v-if="hasAzure" :data="currentPlatformInfo"/>
<el-form-item class="el-form-item-class">
<el-button size="small" @click="cancel">{{ $t('commons.cancel') }}</el-button>
@ -56,7 +55,6 @@ import PasswordInfo from "./PasswordInfo";
import UiSetting from "./UiSetting";
import {getCurrentUser, getCurrentWorkspaceId} from "../../utils/token";
import {hasLicense, hasPermission} from "../../utils/permission";
import ZentaoUserInfo from "./ZentaoUserInfo";
import TapdUserInfo from "./TapdUserInfo";
import AzureDevopsUserInfo from "./AzureDevopsUserInfo";
import {getIntegrationService} from "../../api/workspace";
@ -77,7 +75,6 @@ export default {
MsPersonFromSetting,
MsApiKeys,
PasswordInfo,
ZentaoUserInfo,
TapdUserInfo,
AzureDevopsUserInfo,
UiSetting
@ -90,7 +87,6 @@ export default {
activeIndex: '',
ruleForm: {},
hasTapd: false,
hasZentao: false,
hasAzure: false,
isXpack: false,
updatePath: '/user/update/current',
@ -131,15 +127,7 @@ export default {
},
handleAuth(type) {
let param = {...this.currentPlatformInfo};
if (type === 'Zentao') {
if (!param.zentaoUserName) {
this.$error(this.$t('organization.integration.input_api_account'));
return
} else if (!param.zentaoPassword) {
this.$error(this.$t('organization.integration.input_api_password'));
return
}
} else if (type === 'AzureDevops') {
if (type === 'AzureDevops') {
if (!param.azureDevopsPat) {
this.$error(this.$t('organization.integration.input_azure_pat'));
return
@ -171,9 +159,6 @@ export default {
if (platforms.indexOf("Tapd") !== -1) {
this.hasTapd = true;
}
if (platforms.indexOf("Zentao") !== -1) {
this.hasZentao = true;
}
if (platforms.indexOf("AzureDevops") !== -1) {
this.hasAzure = true;
}

View File

@ -52,9 +52,15 @@
<el-input v-model="form.phone" autocomplete="off"/>
</el-form-item>
</el-form>
<jira-user-info @auth="handleAuth" v-if="hasJira" :data="currentPlatformInfo"/>
<div v-for="config in platformAccountConfigs" :key="config.key">
<platform-account-config
:config="config"
:account-config="currentPlatformInfo"
v-if="showPlatformConfig(config.key)"
/>
</div>
<tapd-user-info @auth="handleAuth" v-if="hasTapd" :data="currentPlatformInfo"/>
<zentao-user-info @auth="handleAuth" v-if="hasZentao" :data="currentPlatformInfo"/>
<azure-devops-user-info @auth="handleAuth" v-if="hasAzure" :data="currentPlatformInfo"/>
<template v-slot:footer>
<ms-dialog-footer
@ -94,19 +100,20 @@ import {listenGoBack, removeGoBackListener} from "../../utils";
import {getCurrentUser, getCurrentWorkspaceId} from "../../utils/token";
import MsTableOperatorButton from "../MsTableOperatorButton";
import {EMAIL_REGEX, PHONE_REGEX} from "../../utils/regex";
import JiraUserInfo from "./JiraUserInfo";
import TapdUserInfo from "./TapdUserInfo";
import {getIntegrationService} from "../../api/workspace";
import ZentaoUserInfo from "./ZentaoUserInfo";
import AzureDevopsUserInfo from "./AzureDevopsUserInfo";
import {useUserStore} from "@/store";
import {handleAuth as _handleAuth, updateInfo, updatePassword} from "../../api/user";
import {getPlatformAccountInfo} from "../../api/platform-plugin";
import {ISSUE_PLATFORM_OPTION} from "../../utils/table-constants";
import PlatformAccountConfig from "./PlatformAccountConfig";
const userStore = useUserStore();
export default {
name: "MsPersonSetting",
components: {ZentaoUserInfo, TapdUserInfo, JiraUserInfo, AzureDevopsUserInfo, MsDialogFooter, MsTableOperatorButton},
components: {TapdUserInfo, AzureDevopsUserInfo, MsDialogFooter, MsTableOperatorButton, PlatformAccountConfig},
inject: [
'reload'
],
@ -128,10 +135,9 @@ export default {
zentaoPassword: '',
azureDevopsPat: ''
},
platformAccountConfigs: [],
ruleForm: {},
hasJira: false,
hasTapd: false,
hasZentao: false,
hasAzure: false,
rule: {
name: [
@ -188,6 +194,10 @@ export default {
},
activated() {
getPlatformAccountInfo()
.then((r) => {
this.platformAccountConfigs = r.data;
});
this.initTableData();
// remove router query _token _csrf
if (this.$route.query && Object.keys(this.$route.query).length > 0) {
@ -198,6 +208,9 @@ export default {
currentUser: () => {
return getCurrentUser();
},
showPlatformConfig(platform) {
return ISSUE_PLATFORM_OPTION.map(item => item.value).indexOf(platform) < 0;
},
edit: function (row) {
this.updateVisible = true;
this.form = Object.assign({}, row);
@ -221,12 +234,6 @@ export default {
if (platforms.indexOf("Tapd") !== -1) {
this.hasTapd = true;
}
if (platforms.indexOf("Jira") !== -1) {
this.hasJira = true;
}
if (platforms.indexOf("Zentao") !== -1) {
this.hasZentao = true;
}
if (platforms.indexOf("AzureDevops") !== -1) {
this.hasAzure = true;
}

View File

@ -1,35 +0,0 @@
<template>
<el-form label-position="right" label-width="115px" size="small">
<el-form-item :label="$t('organization.integration.zentao_info')">
<ms-instructions-icon size="10" :content="$t('organization.integration.zentao_prompt_information')"/>
</el-form-item>
<el-form-item :label="$t('organization.integration.zentao_account')" prop="account">
<el-input v-model="data.zentaoUserName" :placeholder="$t('organization.integration.input_api_account')"/>
</el-form-item>
<el-form-item :label="$t('organization.integration.zentao_password')" prop="password">
<el-input v-model="data.zentaoPassword" auto-complete="new-password"
:placeholder="$t('organization.integration.input_api_password')" show-password/>
</el-form-item>
<el-form-item>
<el-button type="primary" style="float: right" @click="$emit('auth', 'Zentao')" size="mini">
{{ $t('commons.validate') }}
</el-button>
</el-form-item>
</el-form>
</template>
<script>
import MsInstructionsIcon from "../MsInstructionsIcon";
export default {
name: "ZentaoUserInfo",
components: {MsInstructionsIcon},
props: ['data'],
}
</script>
<style scoped>
.instructions-icon {
margin-left: -5px;
}
</style>

View File

@ -1350,7 +1350,7 @@ const message = {
case: "Case",
responsible: "Executor",
title: "Create api",
path_info: "Please enter the URL of the interface, such as /api/demo/#{id}, where id is the path parameter",
path_info: "Please enter the URL of the interface, such as /api/demo/${id}, where id is the path parameter",
path_all_info: "Please enter the complete test address",
fast_debug: "Fast debug",
close_all_label: "close all label",

View File

@ -1360,7 +1360,7 @@ const message = {
case: "用例",
responsible: "责任人",
title: "创建接口",
path_info: "请输入接口的URL如/api/demo/#{id}其中id为路径参数",
path_info: "请输入接口的URL如/api/demo/${id}其中id为路径参数",
path_all_info: "请输入完整测试地址",
fast_debug: "快捷调试",
close_all_label: "关闭所有标签",

View File

@ -1357,7 +1357,7 @@ const message = {
case: "用例",
responsible: "責任人",
title: "創建接口",
path_info: "請輸入接口的URL如/api/demo/#{id}其中id為路徑參數",
path_info: "請輸入接口的URL如/api/demo/${id}其中id為路徑參數",
path_all_info: "請輸入完整測試地址",
fast_debug: "快捷調試",
close_all_label: "關閉所有標簽",

View File

@ -13,6 +13,20 @@ export function hasPermission(permission) {
});
});
let superGroupPermissions = user.userGroups.filter(ug => ug.group && ug.group.id === 'super_group')
.flatMap(ug => ug.userGroupPermissions)
.map(g => g.permissionId)
.reduce((total, current) => {
total.add(current);
return total;
}, new Set);
for (const p of superGroupPermissions) {
if (p === permission) {
return true;
}
}
// todo 权限验证
let currentProjectPermissions = user.userGroups.filter(ug => ug.group && ug.group.type === 'PROJECT')
.filter(ug => ug.sourceId === getCurrentProjectID())

View File

@ -68,7 +68,6 @@ export function CASE_TYPE_OPTION(){
export const ISSUE_PLATFORM_OPTION = [
{value: LOCAL, text: 'Local'},
{value: TAPD, text: 'Tapd'},
{value: ZEN_TAO, text: 'Zentao'},
{value: AZURE_DEVOPS, text: 'Azure Devops'},
];

View File

@ -168,6 +168,17 @@
<groupId>io.metersphere</groupId>
<artifactId>jmeter-plugins-dubbo</artifactId>
<version>${jmeter-plugins-dubbo.version}</version>
<exclusions>
<exclusion>
<artifactId>avro</artifactId>
<groupId>org.apache.avro</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>avro</artifactId>
<groupId>org.apache.avro</groupId>
<version>${avro.version}</version>
</dependency>
<dependency>
@ -180,7 +191,7 @@
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-jsr223</artifactId>
<version>${groovy.version}</version>
<version>${codehaus-groovy.version}</version>
</dependency>
<!-- 添加jmeter包支持导入的jmx能正常执行 -->
@ -255,6 +266,12 @@
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>${dingtalk-sdk.version}</version>
<exclusions>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-io</groupId>
@ -272,13 +289,7 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -319,6 +319,12 @@
<artifactId>json-lib</artifactId>
<version>${json-lib.version}</version>
<classifier>jdk15</classifier>
<exclusions>
<exclusion>
<artifactId>commons-collections</artifactId>
<groupId>commons-collections</groupId>
</exclusion>
</exclusions>
</dependency>
<!--java json schema json格式校验-->
@ -369,13 +375,7 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -20,6 +20,7 @@ public class PermissionConfig implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
LogUtil.info("load permission form {} service permission.json file", service);
try (InputStream inputStream = PermissionConfig.class.getResourceAsStream("/permission.json")){
String permission = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
stringRedisTemplate.opsForHash().put(RedisKey.MS_PERMISSION_KEY, service, permission);

View File

@ -18,7 +18,7 @@ public interface BaseProjectMapper {
int removeIssuePlatform(@Param("platform") String platform, @Param("workspaceId") String workspaceId);
List<ProjectDTO> getUserProject(@Param("proRequest") ProjectRequest request);
List<Project> getUserProject(@Param("proRequest") ProjectRequest request);
String getSystemIdByProjectId(String projectId);
@ -49,4 +49,6 @@ public interface BaseProjectMapper {
void updateUseDefaultCaseTemplateProject(@Param("originId") String originId,@Param("templateId") String templateId,@Param("projectId") String projectId);
List<String> getThirdPartProjectIds();
Project selectOne();
}

View File

@ -138,7 +138,7 @@
FROM project
WHERE workspace_id = #{workspaceId}
</select>
<select id="getUserProject" resultType="io.metersphere.dto.ProjectDTO">
<select id="getUserProject" resultType="io.metersphere.base.domain.Project">
SELECT DISTINCT p.*
FROM `group` g
JOIN user_group ug ON g.id = ug.group_id
@ -435,4 +435,7 @@
#{id}
</foreach>
</select>
<select id="selectOne" resultType="io.metersphere.base.domain.Project">
SELECT * FROM project LIMIT 1
</select>
</mapper>

View File

@ -34,4 +34,6 @@ public interface BaseUserMapper {
void updateLastProjectIdIfNull(@Param("projectId") String projectId, @Param("userId") String userId);
void updateLastWorkspaceIdIfNull(@Param("workspaceId") String workspaceId, @Param("userId") String userId);
boolean isSuperUser(String userId);
}

View File

@ -112,7 +112,9 @@
FROM user
<include refid="queryWhereCondition"/>
</select>
<select id="isSuperUser" resultType="java.lang.Boolean">
select count(*) from user_group where user_id = #{userId} and group_id = 'super_group'
</select>
<update id="updateLastProjectIdIfNull">
UPDATE user SET last_project_id = #{projectId} WHERE id = #{userId}
AND (last_project_id IS NULL OR last_project_id = '')

View File

@ -4,7 +4,7 @@ public enum FileType {
JMX(".jmx"), CSV(".csv"), JSON(".json"), PDF(".pdf"),
JPG(".jpg"), PNG(".png"), JPEG(".jpeg"), DOC(".doc"),
XLSX(".xlsx"), DOCX(".docx"), JAR(".jar"), JS(".js"), TXT(".txt"),
P12(".p12"), JKS(".jks"), PFX(".pfx"),
P12(".p12"), JKS(".jks"), PFX(".pfx"), DCM(".dcm"),
DER(".der"), CER(".cer"), PEM(".pem"), CRT(".crt"), SIDE(".side");
// 保存后缀

View File

@ -4,6 +4,7 @@ package io.metersphere.commons.constants;
* 系统内置用户组常量
*/
public class UserGroupConstants {
public static final String SUPER_GROUP = "super_group";
public static final String ADMIN = "admin";
public static final String ORG_ADMIN = "org_admin";
public static final String ORG_MEMBER = "org_member";

View File

@ -29,7 +29,7 @@ public class BaseProjectController {
* @return List<ProjectDTO>
*/
@PostMapping("/list/related")
public List<ProjectDTO> getUserProject(@RequestBody ProjectRequest request) {
public List<Project> getUserProject(@RequestBody ProjectRequest request) {
return baseProjectService.getUserProject(request);
}

View File

@ -105,4 +105,9 @@ public class BaseUserController {
public void updateCurrentUserByResourceId(@PathVariable String resourceId) {
baseUserService.updateCurrentUserByResourceId(resourceId);
}
@GetMapping("/is/super/{userid}")
public boolean isSuperUser(@PathVariable String userid) {
return baseUserService.isSuperUser(userid);
}
}

View File

@ -54,7 +54,7 @@ public class SystemParameterController {
@GetMapping("timeout")
public long getTimeout() {
return env.getProperty("session.timeout", Long.class, 43200L); // 默认43200s, 12个小时
return env.getProperty("spring.session.timeout", Long.class, 43200L); // 默认43200s, 12个小时
}
@GetMapping("/mail/info")

View File

@ -11,4 +11,10 @@ public class GroupResource implements Serializable {
private String id;
private String name;
private Boolean license = false;
/**
* 系统设置工作空间项目类型 公用的权限模块
* e.g. 个人信息
*/
private boolean global = false;
}

View File

@ -1,9 +1,9 @@
package io.metersphere.metadata.vo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import java.io.Serializable;

View File

@ -6,6 +6,7 @@ import io.metersphere.base.mapper.PluginMapper;
import io.metersphere.commons.constants.StorageConstants;
import io.metersphere.metadata.service.FileManagerService;
import io.metersphere.metadata.vo.FileRequest;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -28,6 +29,17 @@ public class BasePluginService {
return pluginMapper.selectByExampleWithBLOBs(example);
}
public PluginWithBLOBs get(String pluginId) {
return pluginMapper.selectByPrimaryKey(pluginId);
}
public PluginWithBLOBs getByScripId(String scripId) {
PluginExample example = new PluginExample();
example.createCriteria().andScriptIdEqualTo(scripId);
List<PluginWithBLOBs> plugins = pluginMapper.selectByExampleWithBLOBs(example);
return CollectionUtils.isEmpty(plugins) ? null : plugins.get(0);
}
public InputStream getPluginResource(String pluginId, String resourceName) {
FileRequest request = new FileRequest();
request.setProjectId(DIR_PATH + "/" + pluginId);
@ -38,6 +50,9 @@ public class BasePluginService {
public InputStream getPluginJar(String pluginId) {
PluginWithBLOBs plugin = pluginMapper.selectByPrimaryKey(pluginId);
if (plugin == null) {
return null;
}
return getPluginResource(pluginId, plugin.getSourceName());
}
}

View File

@ -7,6 +7,7 @@ import io.metersphere.base.mapper.UserMapper;
import io.metersphere.base.mapper.ext.BaseProjectMapper;
import io.metersphere.base.mapper.ext.BaseProjectVersionMapper;
import io.metersphere.base.mapper.ext.BaseUserGroupMapper;
import io.metersphere.base.mapper.ext.BaseUserMapper;
import io.metersphere.commons.constants.ProjectApplicationType;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.JSON;
@ -54,6 +55,8 @@ public class BaseProjectService {
private BaseProjectVersionMapper baseProjectVersionMapper;
@Resource
private BaseProjectApplicationService baseProjectApplicationService;
@Resource
private BaseUserMapper baseUserMapper;
private String genSystemId() {
@ -90,7 +93,19 @@ public class BaseProjectService {
return baseProjectMapper.getProjectWithWorkspace(request);
}
public List<ProjectDTO> getUserProject(ProjectRequest request) {
public List<Project> getUserProject(ProjectRequest request) {
boolean isSuper = baseUserMapper.isSuperUser(SessionUtils.getUserId());
if (isSuper) {
ProjectExample example = new ProjectExample();
ProjectExample.Criteria criteria = example.createCriteria();
if (StringUtils.isNotBlank(request.getName())) {
criteria.andNameLike(request.getName());
}
if (StringUtils.isNotBlank(request.getWorkspaceId())) {
criteria.andWorkspaceIdEqualTo(request.getWorkspaceId());
}
return projectMapper.selectByExample(example);
}
if (StringUtils.isNotBlank(request.getName())) {
request.setName(StringUtils.wrapIfMissing(request.getName(), "%"));
}
@ -359,4 +374,8 @@ public class BaseProjectService {
public void deleteFile(String fileId) {
fileMetadataService.deleteFile(fileId);
}
public Project selectOne() {
return baseProjectMapper.selectOne();
}
}

View File

@ -242,15 +242,24 @@ public class BaseUserService {
// 获取最新UserDTO
UserDTO user = getUserDTO(sessionUser.getId());
User newUser = new User();
boolean isSuper = baseUserMapper.isSuperUser(sessionUser.getId());
if (StringUtils.equals("workspace", sign)) {
user.setLastWorkspaceId(sourceId);
sessionUser.setLastWorkspaceId(sourceId);
List<Project> projects = getProjectListByWsAndUserId(sessionUser.getId(), sourceId);
if (projects.size() > 0) {
if (CollectionUtils.isNotEmpty(projects)) {
user.setLastProjectId(projects.get(0).getId());
} else {
user.setLastProjectId(StringUtils.EMPTY);
if (isSuper) {
ProjectExample example = new ProjectExample();
example.createCriteria().andWorkspaceIdEqualTo(sourceId);
List<Project> allWsProject = projectMapper.selectByExample(example);
if (CollectionUtils.isNotEmpty(allWsProject)) {
user.setLastProjectId(allWsProject.get(0).getId());
}
} else {
user.setLastProjectId(StringUtils.EMPTY);
}
}
}
BeanUtils.copyProperties(user, newUser);
@ -259,6 +268,19 @@ public class BaseUserService {
userMapper.updateByPrimaryKeySelective(newUser);
}
private void switchSuperUserResource(String projectId, String workspaceId, UserDTO sessionUser) {
// 获取最新UserDTO
UserDTO user = getUserDTO(sessionUser.getId());
User newUser = new User();
user.setLastWorkspaceId(workspaceId);
sessionUser.setLastWorkspaceId(workspaceId);
user.setLastProjectId(projectId);
BeanUtils.copyProperties(user, newUser);
// 切换工作空间或组织之后更新 session 里的 user
SessionUtils.putUser(SessionUser.fromUser(user, SessionUtils.getSessionId()));
userMapper.updateByPrimaryKeySelective(newUser);
}
private List<Project> getProjectListByWsAndUserId(String userId, String workspaceId) {
ProjectExample projectExample = new ProjectExample();
projectExample.createCriteria().andWorkspaceIdEqualTo(workspaceId);
@ -418,10 +440,22 @@ public class BaseUserService {
String wsId = workspaces.get(0).getSourceId();
switchUserResource("workspace", wsId, user);
} else {
// 用户登录之后没有项目和工作空间的权限就把值清空
user.setLastWorkspaceId(StringUtils.EMPTY);
user.setLastProjectId(StringUtils.EMPTY);
updateUser(user);
List<String> superGroupIds = user.getGroups()
.stream()
.map(Group::getId)
.filter(id -> StringUtils.equals(id, UserGroupConstants.SUPER_GROUP))
.collect(Collectors.toList());
if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(superGroupIds)) {
Project p = baseProjectMapper.selectOne();
if (p != null) {
switchSuperUserResource(p.getId(), p.getWorkspaceId(), user);
}
} else {
// 用户登录之后没有项目和工作空间的权限就把值清空
user.setLastWorkspaceId(StringUtils.EMPTY);
user.setLastProjectId(StringUtils.EMPTY);
updateUser(user);
}
}
} else {
UserGroup userGroup = project.stream().filter(p -> StringUtils.isNotBlank(p.getSourceId()))
@ -450,6 +484,8 @@ public class BaseUserService {
user.setLastWorkspaceId(project.getWorkspaceId());
updateUser(user);
return true;
} else {
return baseUserMapper.isSuperUser(user.getId());
}
}
return false;
@ -496,6 +532,8 @@ public class BaseUserService {
user.setLastWorkspaceId(wsId);
updateUser(user);
return true;
} else {
return baseUserMapper.isSuperUser(user.getId());
}
}
return false;
@ -739,4 +777,16 @@ public class BaseUserService {
user.setLastWorkspaceId(project.getWorkspaceId());
userMapper.updateByPrimaryKeySelective(user);
}
public boolean isSuperUser(String userid) {
if (StringUtils.isBlank(userid)) {
MSException.throwException("userid is blank.");
}
return baseUserMapper.isSuperUser(userid);
}
public List<String> getAllUserIds() {
List<User> users = userMapper.selectByExample(new UserExample());
return users.stream().map(User::getId).collect(Collectors.toList());
}
}

View File

@ -4,6 +4,7 @@ import io.metersphere.base.domain.Workspace;
import io.metersphere.base.domain.WorkspaceExample;
import io.metersphere.base.mapper.WorkspaceMapper;
import io.metersphere.base.mapper.ext.BaseUserGroupMapper;
import io.metersphere.base.mapper.ext.BaseUserMapper;
import io.metersphere.dto.RelatedSource;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@ -19,8 +20,14 @@ public class BaseWorkspaceService {
private BaseUserGroupMapper baseUserGroupMapper;
@Resource
private WorkspaceMapper workspaceMapper;
@Resource
private BaseUserMapper baseUserMapper;
public List<Workspace> getWorkspaceListByUserId(String userId) {
boolean isSuper = baseUserMapper.isSuperUser(userId);
if (isSuper) {
return workspaceMapper.selectByExample(new WorkspaceExample());
}
List<RelatedSource> relatedSource = baseUserGroupMapper.getRelatedSource(userId);
List<String> wsIds = relatedSource
.stream()

View File

@ -88,10 +88,8 @@ management.server.port=8083
management.endpoints.web.exposure.include=*
management.endpoints.enabled-by-default=false
#spring.freemarker.checkTemplateLocation=false
spring.session.timeout=${session.timeout:43200}
spring.session.timeout=43200s
spring.session.store-type=none
# cookie
server.servlet.session.cookie.max-age=${session.timeout:43200}
# eureka
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
eureka.instance.prefer-ip-address=true

View File

@ -11,7 +11,7 @@ upload_fail=文件上传失败
invalid_parameter=非法的参数
name_already_exists=该名称已经存在
resource_not_exist=资源不存在或已删除
upload_file_fail_get_file_path_fail=文件上傳失敗,獲取文件上傳路徑為
upload_file_fail_get_file_path_fail=文件上传失败,获取文件上传路径为
#user related
user_email_already_exists=用户邮箱已存在
user_id_is_null=用户ID不能为空

View File

@ -11,7 +11,7 @@ upload_fail=文件上傳失敗
invalid_parameter=非法的參數
name_already_exists=該名稱已經存在
resource_not_exist=資源不存在或已刪除
upload_file_fail_get_file_path_fail=文件上傳失敗,獲取文件路徑失敗
upload_file_fail_get_file_path_fail=文件上傳失敗,獲取文件上傳路徑為空
#user related
user_email_already_exists=用戶郵箱已存在
user_id_is_null=用戶ID不能為空

View File

@ -19,8 +19,6 @@ public class IssuesDao extends IssuesWithBLOBs {
private List<String> caseIds;
private String caseId;
private List<String> tapdUsers;
private List<String>zentaoBuilds;
private String zentaoAssigned;
private String refType;
private String refId;
private List<CustomFieldDao> fields;

View File

@ -21,14 +21,6 @@ public class IssuesRequest extends BaseQueryRequest {
* 如果是 PLAN_FUNCTIONAL 则只查询该测试计划用例所关联的缺陷
*/
private String refType;
/**
* zentao bug 处理人
*/
private String zentaoUser;
/**
* zentao bug 影响版本
*/
private List<String> zentaoBuilds;
/**
* issues id

View File

@ -22,15 +22,6 @@ public class IssuesUpdateRequest extends IssuesWithBLOBs {
private List<CustomFieldResourceDTO> addFields;
private List<CustomFieldResourceDTO> editFields;
private List<CustomFieldItemDTO> requestFields;
/**
* zentao bug 处理人
*/
private String zentaoUser;
private String zentaoAssigned;
/**
* zentao bug 影响版本
*/
private List<String> zentaoBuilds;
private boolean thirdPartPlatform;
private List<String> follows;

View File

@ -106,10 +106,10 @@ public interface IssuesPlatform {
/**
* Get请求的代理
* @param url
* @param path
* @return
*/
ResponseEntity proxyForGet(String url, Class responseEntityClazz);
ResponseEntity proxyForGet(String path, Class responseEntityClazz);
/**
* 同步MS缺陷附件到第三方平台

View File

@ -110,13 +110,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -24,7 +24,7 @@
<el-upload
v-else
style="margin-bottom: 10px"
accept=".jar,.csv,.json,.pdf,.jpg,.png,.jpeg,.doc,.docx,.xlsx,.txt,.der,.cer,.pem,.crt,.pfx,.p12,.jks"
accept=".jar,.csv,.json,.pdf,.jpg,.png,.jpeg,.doc,.docx,.xlsx,.txt,.der,.cer,.pem,.crt,.pfx,.p12,.jks,.dcm"
action=""
multiple
:show-file-list="false"
@ -65,7 +65,7 @@
<template v-slot:default="scope">
<el-upload
style="width: 38px; float: left;"
accept=".jmx,.jar,.csv,.json,.pdf,.jpg,.png,.jpeg,.doc,.docx,.xlsx,.txt,.der,.cer,.pem,.crt,.pfx,.p12,.jks"
accept=".jmx,.jar,.csv,.json,.pdf,.jpg,.png,.jpeg,.doc,.docx,.xlsx,.txt,.der,.cer,.pem,.crt,.pfx,.p12,.jks,.dcm"
action=""
:show-file-list="false"
:before-upload="beforeUpdateUploadFile"

20
pom.xml
View File

@ -22,12 +22,12 @@
<java.version>11</java.version>
<spring-cloud.version>2021.0.5</spring-cloud.version>
<spring-security.version>5.7.5</spring-security.version>
<dubbo.version>2.7.17</dubbo.version>
<platform-plugin-sdk.version>1.0.0</platform-plugin-sdk.version>
<dubbo.version>2.7.18</dubbo.version>
<platform-plugin-sdk.version>1.1.0</platform-plugin-sdk.version>
<flyway.version>7.15.0</flyway.version>
<shiro.version>1.10.0</shiro.version>
<shiro.version>1.10.1</shiro.version>
<mssql-jdbc.version>7.4.1.jre8</mssql-jdbc.version>
<postgresql.version>42.3.2</postgresql.version>
<postgresql.version>42.3.8</postgresql.version>
<java-websocket.version>1.5.3</java-websocket.version>
<easyexcel.version>3.1.1</easyexcel.version>
<dom4j.version>2.1.3</dom4j.version>
@ -37,7 +37,7 @@
<quartz-starter.version>1.0.6</quartz-starter.version>
<redisson-starter.version>3.17.7</redisson-starter.version>
<guice.version>5.1.0</guice.version>
<mybatis-starter.version>2.2.2</mybatis-starter.version>
<mybatis-starter.version>2.3.0</mybatis-starter.version>
<reflections.version>0.10.2</reflections.version>
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
<commons-io.version>2.11.0</commons-io.version>
@ -61,17 +61,20 @@
<springdoc-openapi-ui.version>1.6.11</springdoc-openapi-ui.version>
<flatten.version>1.2.7</flatten.version>
<jmeter.version>5.5</jmeter.version>
<codehaus-groovy.version>3.0.11</codehaus-groovy.version>
<jython.version>2.7.3</jython.version>
<docker-java.version>3.2.13</docker-java.version>
<jmeter-plugins-webdriver.version>3.4.4</jmeter-plugins-webdriver.version>
<oracle-database.version>19.7.0.0</oracle-database.version>
<zookeeper.version>3.4.14</zookeeper.version>
<zookeeper.version>3.8.0</zookeeper.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
<jmeter-plugins-dubbo.version>2.7.17</jmeter-plugins-dubbo.version>
<hessian-lite.version>3.2.12</hessian-lite.version>
<hessian-lite.version>3.2.13</hessian-lite.version>
<avro.version>1.11.1</avro.version>
<dec.version>0.1.2</dec.version>
<dingtalk-sdk.version>2.0.0</dingtalk-sdk.version>
<org-json.version>20171018</org-json.version>
<org-json.version>20220924</org-json.version>
<jmeter-plugins-dubbo.version>2.7.17</jmeter-plugins-dubbo.version>
<nacos.version>1.4.4</nacos.version>
<minio.version>8.4.5</minio.version>
<hikaricp.version>5.0.1</hikaricp.version>
@ -94,6 +97,7 @@
<module>report-stat</module>
<module>system-setting</module>
<module>test-track</module>
<module>workstation</module>
</modules>
<dependencies>

View File

@ -110,13 +110,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>

View File

@ -5,32 +5,9 @@
<select id="getGroupList" resultType="io.metersphere.dto.GroupDTO">
SELECT *,
<if test="request.onlyQueryCurrentProject == true">
(SELECT COUNT(DISTINCT ug.user_id) FROM user_group ug JOIN user ON ug.user_id = user.id WHERE ug.group_id =
temp.id AND ug.source_id = #{request.projectId}) AS memberSize
</if>
<if test="request.onlyQueryCurrentProject == false">
(SELECT COUNT(DISTINCT ug.user_id) FROM user_group ug JOIN user ON ug.user_id = user.id WHERE ug.group_id =
temp.id) AS memberSize
</if>
(SELECT COUNT(DISTINCT ug.user_id) FROM user_group ug JOIN user ON ug.user_id = user.id WHERE ug.group_id =
temp.id AND ug.source_id = #{request.projectId}) AS memberSize
FROM (
SELECT g.*, w.name AS scopeName FROM `group` g, workspace w
<where>
and g.scope_id = w.id
<if test="request.types != null and request.types.size() > 0">
AND g.type IN
<foreach collection="request.types" item="type" separator="," open="(" close=")">
#{type}
</foreach>
</if>
<if test="request.scopes != null and request.scopes.size() > 0">
AND g.scope_id IN
<foreach collection="request.scopes" item="scope" separator="," open="(" close=")">
#{scope}
</foreach>
</if>
</where>
UNION DISTINCT
SELECT g.*, 'global' AS scopeName FROM `group` g
<where>
g.scope_id = 'global'
@ -63,7 +40,7 @@
WHERE temp.name LIKE CONCAT('%', #{request.name},'%')
</if>
<if test="request.orders == null or request.orders.size() == 0">
ORDER BY field(temp.type, 'SYSTEM', 'ORGANIZATION', 'WORKSPACE', 'PROJECT'), temp.update_time, temp.name
ORDER BY field(temp.type, 'SYSTEM', 'WORKSPACE', 'PROJECT'), temp.update_time, temp.name
</if>
<if test="request.orders != null and request.orders.size() > 0">
ORDER BY

View File

@ -36,21 +36,12 @@ public class GroupController {
@Resource
private GroupService groupService;
@PostMapping("/get/{goPage}/{pageSize}")
@RequiresPermissions(value = {PermissionConstants.SYSTEM_GROUP_READ}, logical = Logical.OR)
public Pager<List<GroupDTO>> getGroupList(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody EditGroupRequest request) {
request.setGoPage(goPage);
request.setPageSize(pageSize);
return groupService.getGroupList(request);
}
@PostMapping("/get/current/project/{goPage}/{pageSize}")
@RequiresPermissions(value = {PermissionConstants.PROJECT_GROUP_READ}, logical = Logical.OR)
public Pager<List<GroupDTO>> getCurrentProjectGroupList(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody EditGroupRequest request) {
request.setOnlyQueryCurrentProject(true);
request.setGoPage(goPage);
request.setPageSize(pageSize);
return groupService.getGroupList(request);
return groupService.getProjectGroupList(request);
}
@GetMapping("/get/all")
@ -127,13 +118,6 @@ public class GroupController {
return groupService.getResource(type, id);
}
@PostMapping("/user/{goPage}/{pageSize}")
@RequiresPermissions(value = {PermissionConstants.SYSTEM_GROUP_READ}, logical = Logical.OR)
public Pager<List<User>> getGroupUser(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody EditGroupRequest editGroupRequest) {
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
return PageUtils.setPageInfo(page, groupService.getGroupUser(editGroupRequest));
}
@PostMapping("/current/project/user/{goPage}/{pageSize}")
@RequiresPermissions(value = {PermissionConstants.PROJECT_GROUP_READ}, logical = Logical.OR)
public Pager<List<User>> getCurrentProjectGroupUser(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody EditGroupRequest editGroupRequest) {

View File

@ -67,6 +67,8 @@ public class GroupService {
private UserMapper userMapper;
@Resource
private MicroService microService;
@Resource
private BaseUserService baseUserService;
private static final String GLOBAL = "global";
// 服务权限拼装顺序
@ -86,22 +88,27 @@ public class GroupService {
put(UserGroupType.PROJECT, "项目");
}};
public Pager<List<GroupDTO>> getGroupList(EditGroupRequest request) {
public Pager<List<GroupDTO>> getProjectGroupList(EditGroupRequest request) {
SessionUser user = SessionUtils.getUser();
List<UserGroupDTO> userGroup = baseUserGroupMapper.getUserGroup(Objects.requireNonNull(user).getId(), request.getProjectId());
List<String> groupTypeList = userGroup.stream().map(UserGroupDTO::getType).distinct().collect(Collectors.toList());
if (groupTypeList.isEmpty()) {
if (baseUserService.isSuperUser(user.getId())) {
groupTypeList.add(UserGroupType.PROJECT);
}
}
return getGroups(groupTypeList, request);
}
public void buildUserInfo(List<GroupDTO> testCases) {
if (CollectionUtils.isEmpty(testCases)) {
public void buildUserInfo(List<GroupDTO> groups) {
if (CollectionUtils.isEmpty(groups)) {
return;
}
List<String> userIds = testCases.stream().map(GroupDTO::getCreator).collect(Collectors.toList());
List<String> userIds = groups.stream().map(GroupDTO::getCreator).collect(Collectors.toList());
if (!userIds.isEmpty()) {
Map<String, String> userMap = ServiceUtils.getUserNameMap(userIds);
testCases.forEach(caseResult -> {
caseResult.setCreator(userMap.get(caseResult.getCreator()));
groups.forEach(caseResult -> {
caseResult.setCreator(userMap.getOrDefault(caseResult.getCreator(), caseResult.getCreator()));
});
}
}
@ -142,6 +149,9 @@ public class GroupService {
}
public void editGroup(EditGroupRequest request) {
if (StringUtils.equals(request.getId(), UserGroupConstants.SUPER_GROUP)) {
MSException.throwException("超级管理员无法编辑!");
}
if (StringUtils.equals(request.getId(), UserGroupConstants.ADMIN)) {
MSException.throwException("系统管理员无法编辑!");
}

View File

@ -25,11 +25,11 @@
<el-form-item :label-width="labelWidth"
:label="$t('workspace.issue_template_manage')" prop="issueTemplateId">
<template-select :platform="form.platform" :data="form" scene="ISSUE" prop="issueTemplateId"
:disabled="form.platform === 'Jira' && form.thirdPartTemplate"
:disabled="thirdPartTemplateSupport && form.thirdPartTemplate"
:platformOptions="platformOptions" :project-id="form.id"
ref="issueTemplate"/>
<el-checkbox @change="thirdPartTemplateChange" v-if="form.platform === 'Jira' && thirdPartTemplateSupport"
<el-checkbox @change="thirdPartTemplateChange" v-if="thirdPartTemplateSupport"
v-model="form.thirdPartTemplate" style="margin-left: 10px">
{{ $t('test_track.issue.use_third_party') }}
</el-checkbox>
@ -61,20 +61,6 @@
ref="platformConfig"
/>
<el-form-item :label-width="labelWidth" :label="$t('project.zentao_id')" v-if="zentao">
<el-input v-model="form.zentaoId" autocomplete="off"></el-input>
<el-button @click="check" type="primary" class="checkButton">
{{ $t('test_track.issue.check_id_exist') }}
</el-button>
<ms-instructions-icon effect="light">
<template>
禅道流程产品-项目 | 产品-迭代 | 产品-冲刺 | 项目-迭代 | 项目-冲刺 <br/><br/>
根据 "后台 -> 自定义 -> 流程" 查看对应流程根据流程填写ID <br/><br/>
产品-项目 | 产品-迭代 | 产品-冲刺 需要填写产品ID <br/><br/>
项目-迭代 | 项目-冲刺 需要填写项目ID
</template>
</ms-instructions-icon>
</el-form-item>
<el-form-item :label-width="labelWidth" :label="$t('project.azureDevops_id')" v-if="azuredevops">
<el-input v-model="form.azureDevopsId" autocomplete="off"></el-input>
</el-form-item>
@ -198,9 +184,6 @@ export default {
tapd() {
return this.showPlatform(TAPD);
},
zentao() {
return this.showPlatform(ZEN_TAO);
},
azuredevops() {
return this.showPlatform(AZURE_DEVOPS);
},

View File

@ -1,84 +0,0 @@
<template>
<div>
<el-form-item :label-width="labelWidth" :label="$t('project.jira_key')">
<el-input v-model="form.jiraKey" autocomplete="off" @blur="getIssueTypeOption"/>
<slot name="checkBtn"></slot>
<ms-instructions-icon effect="light">
<template>
<img class="jira-image" src="assets/jira-key.png"/>
</template>
</ms-instructions-icon>
</el-form-item>
<el-form-item :label-width="labelWidth" :label="$t('organization.integration.jira_issuetype')" prop="issuetype">
<el-select filterable v-model="form.issueConfigObj.jiraIssueTypeId">
<el-option v-for="item in issueTypes" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label-width="labelWidth" :label="$t('organization.integration.jira_storytype')" prop="storytype">
<el-select filterable v-model="form.issueConfigObj.jiraStoryTypeId">
<el-option v-for="item in issueTypes" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
</div>
</template>
<script>
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import {getJiraIssueType} from "../../api/project";
import {getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
export default {
name: "ProjectJiraConfig",
components: {MsInstructionsIcon},
props: ['labelWidth', 'form', 'result'],
data() {
return {
issueTypes: []
}
},
mounted() {
this.getIssueTypeOption();
},
methods: {
getIssueTypeOption() {
this.issueTypes = [];
this.result.loading = true;
let param = {
projectId: this.form.id,
workspaceId: getCurrentWorkspaceId(),
jiraKey: this.form.jiraKey
}
getJiraIssueType(param)
.then((response) => {
this.issueTypes = response.data;
let hasJiraIssueType = false;
let hasJiraStoryType = false;
if (response.data) {
response.data.forEach(item => {
if (this.form.issueConfigObj.jiraIssueTypeId === item.id) {
hasJiraIssueType = true;
} else if (this.form.issueConfigObj.jiraStoryTypeId === item.id) {
hasJiraStoryType = true;
}
});
}
if (!hasJiraIssueType) {
this.form.issueConfigObj.jiraIssueTypeId = null;
}
if (!hasJiraStoryType) {
this.form.issueConfigObj.jiraStoryTypeId = null;
}
this.result.loading = false;
});
}
}
}
</script>
<style scoped>
.el-input, .el-textarea {
width: 80%;
}
</style>

View File

@ -19,10 +19,13 @@
class="checkButton">
{{ $t('test_track.issue.check_id_exist') }}
</el-button>
<ms-instructions-icon v-if="item.instructionsIcon" effect="light">
<ms-instructions-icon v-if="item.instructionsIcon || item.instructionsTip" effect="light">
<template>
<img class="jira-image"
<img v-if="item.instructionsIcon"
:src="getPlatformImageUrl(config, item)"/>
<span v-if="item.instructionsTip">
{{ item.instructionsTip }}
</span>
</template>
</ms-instructions-icon>
</el-form-item>

View File

@ -110,13 +110,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -121,13 +121,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -5,14 +5,8 @@
<select id="getGroupList" resultType="io.metersphere.dto.GroupDTO">
SELECT *,
<if test="request.onlyQueryCurrentProject == true">
(SELECT COUNT(DISTINCT ug.user_id) FROM user_group ug JOIN user ON ug.user_id = user.id WHERE ug.group_id =
temp.id AND ug.source_id = #{request.projectId}) AS memberSize
</if>
<if test="request.onlyQueryCurrentProject == false">
(SELECT COUNT(DISTINCT ug.user_id) FROM user_group ug JOIN user ON ug.user_id = user.id WHERE ug.group_id =
temp.id) AS memberSize
</if>
(SELECT COUNT(DISTINCT ug.user_id) FROM user_group ug JOIN user ON ug.user_id = user.id WHERE ug.group_id =
temp.id) AS memberSize
FROM (
SELECT g.*, w.name AS scopeName FROM `group` g, workspace w
<where>
@ -41,6 +35,17 @@
</foreach>
</if>
</where>
UNION DISTINCT
SELECT g.*, 'system' AS scopeName FROM `group` g
<where>
g.scope_id = 'system'
<if test="request.types != null and request.types.size() > 0">
AND g.type IN
<foreach collection="request.types" item="type" separator="," open="(" close=")">
#{type}
</foreach>
</if>
</where>
union distinct
select g.*, p.name as scopeName from `group` g, project p
<where>
@ -63,7 +68,9 @@
WHERE temp.name LIKE CONCAT('%', #{request.name},'%')
</if>
<if test="request.orders == null or request.orders.size() == 0">
ORDER BY field(temp.type, 'SYSTEM', 'ORGANIZATION', 'WORKSPACE', 'PROJECT'), temp.update_time, temp.name
ORDER BY field(temp.type, 'SYSTEM', 'WORKSPACE', 'PROJECT'),
field(temp.scope_id, 'system') desc,
temp.update_time, temp.name
</if>
<if test="request.orders != null and request.orders.size() > 0">
ORDER BY

View File

@ -44,15 +44,6 @@ public class GroupController {
return groupService.getGroupList(request);
}
@PostMapping("/get/current/project/{goPage}/{pageSize}")
@RequiresPermissions(value = {PermissionConstants.PROJECT_GROUP_READ}, logical = Logical.OR)
public Pager<List<GroupDTO>> getCurrentProjectGroupList(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody EditGroupRequest request) {
request.setOnlyQueryCurrentProject(true);
request.setGoPage(goPage);
request.setPageSize(pageSize);
return groupService.getGroupList(request);
}
@GetMapping("/get/all")
public List<GroupDTO> getAllGroup() {
return groupService.getAllGroup();
@ -134,17 +125,6 @@ public class GroupController {
return PageUtils.setPageInfo(page, groupService.getGroupUser(editGroupRequest));
}
@PostMapping("/current/project/user/{goPage}/{pageSize}")
@RequiresPermissions(value = {PermissionConstants.PROJECT_GROUP_READ}, logical = Logical.OR)
public Pager<List<User>> getCurrentProjectGroupUser(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody EditGroupRequest editGroupRequest) {
editGroupRequest.setOnlyQueryCurrentProject(true);
if (StringUtils.isBlank(editGroupRequest.getProjectId())) {
editGroupRequest.setProjectId(SessionUtils.getCurrentProjectId());
}
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
return PageUtils.setPageInfo(page, groupService.getGroupUser(editGroupRequest));
}
@GetMapping("/rm/{userId}/{groupId}")
public void removeGroupMember(@PathVariable String userId, @PathVariable String groupId) {
groupService.removeGroupMember(userId, groupId);

View File

@ -0,0 +1,68 @@
package io.metersphere.job.schedule;
import com.fit2cloud.quartz.anno.QuartzScheduled;
import io.metersphere.commons.utils.JSON;
import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Component
public class CleanSessionJob {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedisIndexedSessionRepository redisIndexedSessionRepository;
@Value("${spring.session.timeout:43200s}")
private Duration timeout;
/**
* 清理没有绑定user的session
* redisson 有时会把ttl设置成-1 https://github.com/redisson/redisson/issues/4200
*/
@QuartzScheduled(cron = "0 2 0 * * ?")
public void cleanSession() {
Map<String, Long> userCount = new HashMap<>();
ScanOptions options = ScanOptions.scanOptions().match("spring:session:sessions:*").count(1000).build();
try (
Cursor<String> scan = stringRedisTemplate.scan(options)
) {
while (scan.hasNext()) {
String key = scan.next();
if (StringUtils.contains(key, "spring:session:sessions:expires:")) {
continue;
}
Boolean exists = stringRedisTemplate.opsForHash().hasKey(key, "sessionAttr:user");
if (!exists) {
stringRedisTemplate.delete(key);
} else {
Object user = redisIndexedSessionRepository.getSessionRedisOperations().opsForHash().get(key, "sessionAttr:user");
Long expire = redisIndexedSessionRepository.getSessionRedisOperations().getExpire(key);
String userId = (String) MethodUtils.invokeMethod(user, "getId");
Long count = userCount.getOrDefault(userId, 0L);
count++;
userCount.put(userId, count);
LogUtil.info(key + " : " + userId + " 过期时间: " + expire);
if (expire != null && expire.intValue() == -1) {
redisIndexedSessionRepository.getSessionRedisOperations().expire(key, timeout);
}
}
}
LogUtil.info(JSON.toJSONString(userCount));
} catch (Exception e) {
LogUtil.error(e);
}
}
}

View File

@ -1,5 +1,6 @@
package io.metersphere.listener;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.service.PlatformPluginService;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
@ -15,6 +16,7 @@ public class InitListener implements ApplicationRunner {
@Override
public void run(ApplicationArguments applicationArguments) {
LogUtil.info("================= SYSTEM-SETTING 应用启动 =================");
platformPluginService.loadPlatFormPlugins();
}
}

View File

@ -68,11 +68,20 @@ public class GroupService {
private UserMapper userMapper;
private static final String GLOBAL = "global";
private static final String SUPER_GROUP = "super_group";
private static final String PERSONAL_PREFIX = "PERSONAL";
// 服务权限拼装顺序
private static final String[] servicePermissionLoadOrder = {MicroServiceName.PROJECT_MANAGEMENT,
MicroServiceName.TEST_TRACK, MicroServiceName.API_TEST, MicroServiceName.UI_TEST,
MicroServiceName.PERFORMANCE_TEST, MicroServiceName.REPORT_STAT, MicroServiceName.SYSTEM_SETTING};
private static final String[] servicePermissionLoadOrder = {
MicroServiceName.SYSTEM_SETTING,
MicroServiceName.PROJECT_MANAGEMENT,
MicroServiceName.TEST_TRACK,
MicroServiceName.API_TEST,
MicroServiceName.UI_TEST,
MicroServiceName.PERFORMANCE_TEST,
MicroServiceName.REPORT_STAT
};
private static final Map<String, List<String>> map = new HashMap<>(4) {{
put(UserGroupType.SYSTEM, Arrays.asList(UserGroupType.SYSTEM, UserGroupType.WORKSPACE, UserGroupType.PROJECT));
@ -93,14 +102,11 @@ public class GroupService {
return getGroups(groupTypeList, request);
}
public void buildUserInfo(List<GroupDTO> testCases) {
List<String> userIds = new ArrayList();
userIds.addAll(testCases.stream().map(GroupDTO::getCreator).collect(Collectors.toList()));
public void buildUserInfo(List<GroupDTO> groups) {
List<String> userIds = groups.stream().map(GroupDTO::getCreator).collect(Collectors.toList());
if (!userIds.isEmpty()) {
Map<String, String> userMap = ServiceUtils.getUserNameMap(userIds);
testCases.forEach(caseResult -> {
caseResult.setCreator(userMap.get(caseResult.getCreator()));
});
groups.forEach(caseResult -> caseResult.setCreator(userMap.getOrDefault(caseResult.getCreator(), caseResult.getCreator())));
}
}
@ -140,6 +146,9 @@ public class GroupService {
}
public void editGroup(EditGroupRequest request) {
if (StringUtils.equals(request.getId(), UserGroupConstants.SUPER_GROUP)) {
MSException.throwException("超级管理员无法编辑!");
}
if (StringUtils.equals(request.getId(), UserGroupConstants.ADMIN)) {
MSException.throwException("系统管理员无法编辑!");
}
@ -176,19 +185,22 @@ public class GroupService {
example.createCriteria().andGroupIdEqualTo(group.getId());
List<UserGroupPermission> groupPermissions = userGroupPermissionMapper.selectByExample(example);
List<String> groupPermissionIds = groupPermissions.stream().map(UserGroupPermission::getPermissionId).collect(Collectors.toList());
GroupJson groupJson = this.loadPermissionJsonFromService();
if (groupJson == null) {
MSException.throwException(Translator.get("read_permission_file_fail"));
}
List<GroupResource> resource = groupJson.getResource();
List<GroupPermission> permissions = groupJson.getPermissions();
List<GroupResourceDTO> dtoPermissions = dto.getPermissions();
dtoPermissions.addAll(getResourcePermission(resource, permissions, group.getType(), groupPermissionIds));
dtoPermissions.addAll(getResourcePermission(resource, permissions, group, groupPermissionIds));
return dto;
}
private GroupJson loadPermissionJsonFromService() {
GroupJson groupJson = null;
List<GroupResource> globalResource = new ArrayList<>();
try {
for (String service : servicePermissionLoadOrder) {
Object obj = stringRedisTemplate.opsForHash().get(RedisKey.MS_PERMISSION_KEY, service);
@ -199,11 +211,23 @@ public class GroupService {
GroupJson temp = JSON.parseObject((String) obj, GroupJson.class);
if (groupJson == null) {
groupJson = temp;
// 全局权限放系统设置模块
if (StringUtils.equals(service, MicroServiceName.SYSTEM_SETTING)) {
globalResource = temp.getResource()
.stream()
.filter(gp -> BooleanUtils.isTrue(gp.isGlobal()))
.collect(Collectors.toList());
temp.getResource().removeIf(gp -> BooleanUtils.isTrue(gp.isGlobal()));
}
} else {
groupJson.getResource().addAll(temp.getResource());
groupJson.getPermissions().addAll(temp.getPermissions());
}
}
// 拼装权限的时候放在最后
if (groupJson != null && !globalResource.isEmpty()) {
groupJson.getResource().addAll(globalResource);
}
} catch (Exception e) {
LogUtil.error(e);
}
@ -292,8 +316,7 @@ public class GroupService {
scopeList = Arrays.asList(GLOBAL, resourceId, request.getProjectId());
}
GroupExample groupExample = new GroupExample();
groupExample.createCriteria().andScopeIdIn(scopeList)
.andTypeEqualTo(type);
groupExample.createCriteria().andScopeIdIn(scopeList).andTypeEqualTo(type);
return groupMapper.selectByExample(groupExample);
}
@ -301,15 +324,23 @@ public class GroupService {
return baseUserGroupMapper.getWorkspaceMemberGroups(workspaceId, userId);
}
private List<GroupResourceDTO> getResourcePermission(List<GroupResource> resource, List<GroupPermission> permissions, String type, List<String> permissionList) {
private List<GroupResourceDTO> getResourcePermission(List<GroupResource> resources, List<GroupPermission> permissions, Group group, List<String> permissionList) {
List<GroupResourceDTO> dto = new ArrayList<>();
List<GroupResource> resources = resource.stream().filter(g -> g.getId().startsWith(type) || g.getId().startsWith("PERSONAL")).collect(Collectors.toList());
List<GroupResource> grs;
if (StringUtils.equals(group.getId(), SUPER_GROUP)) {
grs = resources;
} else {
grs = resources
.stream()
.filter(g -> g.getId().startsWith(group.getType()) || g.getId().startsWith(PERSONAL_PREFIX))
.collect(Collectors.toList());
}
permissions.forEach(p -> {
if (permissionList.contains(p.getId())) {
p.setChecked(true);
}
});
for (GroupResource r : resources) {
for (GroupResource r : grs) {
GroupResourceDTO resourceDTO = new GroupResourceDTO();
resourceDTO.setResource(r);
List<GroupPermission> collect = permissions
@ -453,9 +484,14 @@ public class GroupService {
UserGroupExample userGroupExample = new UserGroupExample();
userGroupExample.createCriteria().andUserIdEqualTo(userId).andGroupIdEqualTo(group.getId());
List<UserGroup> userGroups = userGroupMapper.selectByExample(userGroupExample);
if (userGroups.size() <= 0) {
UserGroup userGroup = new UserGroup(UUID.randomUUID().toString(), userId, group.getId(),
"system", System.currentTimeMillis(), System.currentTimeMillis());
if (userGroups.size() == 0) {
UserGroup userGroup = new UserGroup(
UUID.randomUUID().toString(),
userId,
group.getId(),
"system",
System.currentTimeMillis(),
System.currentTimeMillis());
userGroupMapper.insertSelective(userGroup);
}
}
@ -492,7 +528,7 @@ public class GroupService {
private void checkQuota(QuotaService quotaService, String type, List<String> sourceIds, List<String> userIds) {
if (quotaService != null) {
Map<String, List<String>> addMemberMap = sourceIds.stream().collect(Collectors.toMap( id -> id, id -> userIds));
Map<String, List<String>> addMemberMap = sourceIds.stream().collect(Collectors.toMap(id -> id, id -> userIds));
quotaService.checkMemberCount(addMemberMap, type);
}
}

View File

@ -195,14 +195,6 @@ public class SystemProjectService {
return baseProjectMapper.getProjectWithWorkspace(request);
}
public List<ProjectDTO> getUserProject(ProjectRequest request) {
if (StringUtils.isNotBlank(request.getName())) {
request.setName(StringUtils.wrapIfMissing(request.getName(), "%"));
}
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
return baseProjectMapper.getUserProject(request);
}
public List<Project> getProjectByIds(List<String> ids) {
if (!CollectionUtils.isEmpty(ids)) {
ProjectExample example = new ProjectExample();

View File

@ -85,6 +85,8 @@ public class UserService {
private BaseProjectMapper baseProjectMapper;
@Resource
private BaseWorkspaceMapper baseWorkspaceMapper;
@Resource
private BaseUserService baseUserService;
public List<UserDetail> queryTypeByIds(List<String> userIds) {
return baseUserMapper.queryTypeByIds(userIds);
@ -412,30 +414,53 @@ public class UserService {
public void addMember(AddMemberRequest request) {
if (!CollectionUtils.isEmpty(request.getUserIds())) {
if (CollectionUtils.isEmpty(request.getUserIds())
|| CollectionUtils.isEmpty(request.getGroupIds())) {
LogUtil.warn("user ids or group ids is empty.");
return;
}
if (CollectionUtils.isNotEmpty(request.getUserIds())) {
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
checkQuota(quotaService, "WORKSPACE", Collections.singletonList(request.getWorkspaceId()), request.getUserIds());
QuotaService quotaService = CommonBeanFactory.getBean(QuotaService.class);
checkQuota(quotaService, "WORKSPACE", Collections.singletonList(request.getWorkspaceId()), request.getUserIds());
List<String> allUserIds = baseUserService.getAllUserIds();
GroupExample groupExample = new GroupExample();
groupExample.createCriteria().andTypeEqualTo(UserGroupType.WORKSPACE);
List<Group> wsGroups = groupMapper.selectByExample(groupExample);
List<String> wsGroupIds = wsGroups
.stream()
.map(Group::getId)
.collect(Collectors.toList());
for (String userId : request.getUserIds()) {
if (!allUserIds.contains(userId)) {
LogUtil.warn("user id {} is not exist!", userId);
continue;
}
for (String userId : request.getUserIds()) {
UserGroupExample userGroupExample = new UserGroupExample();
userGroupExample.createCriteria().andUserIdEqualTo(userId).andSourceIdEqualTo(request.getWorkspaceId());
List<UserGroup> userGroups = userGroupMapper.selectByExample(userGroupExample);
if (userGroups.size() > 0) {
MSException.throwException(Translator.get("user_already_exists"));
} else {
for (String groupId : request.getGroupIds()) {
UserGroup userGroup = new UserGroup();
userGroup.setGroupId(groupId);
userGroup.setSourceId(request.getWorkspaceId());
userGroup.setUserId(userId);
userGroup.setId(UUID.randomUUID().toString());
userGroup.setUpdateTime(System.currentTimeMillis());
userGroup.setCreateTime(System.currentTimeMillis());
userGroupMapper.insertSelective(userGroup);
}
UserGroupExample userGroupExample = new UserGroupExample();
userGroupExample.createCriteria().andUserIdEqualTo(userId).andSourceIdEqualTo(request.getWorkspaceId());
List<UserGroup> userGroups = userGroupMapper.selectByExample(userGroupExample);
if (userGroups.size() > 0) {
MSException.throwException(Translator.get("user_already_exists"));
}
for (String groupId : request.getGroupIds()) {
if (!wsGroupIds.contains(groupId)) {
LogUtil.warn("group id {} is not exist or not belong to workspace level.", groupId);
continue;
}
UserGroup userGroup = new UserGroup();
userGroup.setGroupId(groupId);
userGroup.setSourceId(request.getWorkspaceId());
userGroup.setUserId(userId);
userGroup.setId(UUID.randomUUID().toString());
userGroup.setUpdateTime(System.currentTimeMillis());
userGroup.setCreateTime(System.currentTimeMillis());
userGroupMapper.insertSelective(userGroup);
}
}
}
@ -1240,7 +1265,7 @@ public class UserService {
private void addGroupMember(String type, String sourceId, List<String> userIds, List<String> groupIds) {
if (!StringUtils.equalsAny(type, "PROJECT", "WORKSPACE") || StringUtils.isBlank(sourceId)
|| CollectionUtils.isEmpty(userIds) || CollectionUtils.isEmpty(groupIds)) {
LogUtil.info("add member warning, please check param!");
LogUtil.warn("add member warning, please check param!");
return;
}
this.checkQuotaOfMemberSize(type, sourceId, userIds);
@ -1248,7 +1273,7 @@ public class UserService {
for (String userId : userIds) {
User user = userMapper.selectByPrimaryKey(userId);
if (user == null) {
LogUtil.info("add member warning, invalid user id: " + userId);
LogUtil.warn("add member warning, invalid user id: " + userId);
continue;
}
List<String> toAddGroupIds = new ArrayList<>(groupIds);
@ -1260,8 +1285,13 @@ public class UserService {
continue;
}
for (String groupId : toAddGroupIds) {
UserGroup userGroup = new UserGroup(UUID.randomUUID().toString(), userId, groupId,
sourceId, System.currentTimeMillis(), System.currentTimeMillis());
UserGroup userGroup = new UserGroup(
UUID.randomUUID().toString(),
userId,
groupId,
sourceId,
System.currentTimeMillis(),
System.currentTimeMillis());
userGroupMapper.insertSelective(userGroup);
}
}

View File

@ -55,6 +55,9 @@ public class PluginManagerUtil {
* @param pluginManager
*/
public static void loadPlugin(String id, PluginManager pluginManager, InputStream inputStream) {
if (inputStream == null) {
return;
}
if (pluginManager == null) {
pluginManager = new PluginManager();
}

View File

@ -347,6 +347,10 @@
"id": "SYSTEM_OPERATING_LOG",
"name": "permission.system_operation_log.name"
},
{
"id": "SYSTEM_PLUGIN",
"name": "permission.system_plugin.name"
},
{
"id": "WORKSPACE_USER",
"name": "permission.workspace_user.name"
@ -372,13 +376,10 @@
"id": "WORKSPACE_OPERATING_LOG",
"name": "permission.workspace_operation_log.name"
},
{
"id": "SYSTEM_PLUGIN",
"name": "permission.system_plugin.name"
},
{
"id": "PERSONAL_INFORMATION",
"name": "permission.personal_information.name"
"name": "permission.personal_information.name",
"global": true
}
]
}

View File

@ -99,7 +99,7 @@ export default {
},
isReadOnly() {
return function (data) {
const isDefaultSystemGroup = this.group.id === 'admin' && data.resource.id === 'SYSTEM_GROUP';
const isDefaultSystemGroup = (this.group.id === 'admin' || this.group.id === 'super_group') && data.resource.id === 'SYSTEM_GROUP';
return this.readOnly || isDefaultSystemGroup;
}
}
@ -143,8 +143,13 @@ export default {
},
_getUniteMenu() {
let menu = ['TRACK', 'API', 'UI', 'PERFORMANCE', 'REPORT'];
// PROJECT_USER
let isFirstProjectType = false;
for (let i = 0; i < this.tableData.length; i++) {
if (i === 0) {
if (this.tableData[i].type === GROUP_TYPE.PROJECT) {
isFirstProjectType = true;
}
this.spanArr.push(1);
this.pos = 0
} else {
@ -153,8 +158,13 @@ export default {
if (this.tableData[i].type !== GROUP_TYPE.PROJECT) {
sign = this.tableData[i].type === this.tableData[i - 1].type;
} else {
sign = !menu.includes(this.tableData[i].resource.id.split('_')[1]) ?
true : this.tableData[i].resource.id.split('_')[1] === this.tableData[i - 1].resource.id.split('_')[1]
let hasSubModule = menu.includes(this.tableData[i].resource.id.split('_')[1]);
if (hasSubModule) {
sign = this.tableData[i].resource.id.split('_')[1] === this.tableData[i - 1].resource.id.split('_')[1];
} else {
sign = isFirstProjectType;
isFirstProjectType = true;
}
}
if (sign) {
this.spanArr[this.pos] += 1;

View File

@ -47,7 +47,7 @@ export default {
return function (permission) {
//
const isSystemGroupPermission = permission.id === 'SYSTEM_GROUP:READ' || permission.id === 'SYSTEM_GROUP:READ+SETTING_PERMISSION';
const isDefaultSystemGroup = this.group.id === 'admin' && isSystemGroupPermission;
const isDefaultSystemGroup = (this.group.id === 'admin' || this.group.id === 'super_group') && isSystemGroupPermission;
return this.readOnly || isDefaultSystemGroup;
}
}

View File

@ -28,6 +28,7 @@
<el-table-column prop="scopeName" :label="$t('group.scope')">
<template v-slot="scope">
<span v-if="scope.row.scopeId ==='global'">{{ $t('group.global') }}</span>
<span v-else-if="scope.row.scopeId ==='system'">{{ $t('group.system') }}</span>
<span v-else>{{ scope.row.scopeName }}</span>
</template>
</el-table-column>
@ -42,10 +43,22 @@
</template>
</el-table-column>
<el-table-column prop="creator" :label="$t('group.operator')"/>
<el-table-column prop="description" :label="$t('group.description')"/>
<el-table-column prop="description" :label="$t('group.description')" show-overflow-tooltip/>
<el-table-column :label="$t('commons.operating')" min-width="120">
<template v-slot:default="scope">
<div>
<template v-slot="scope">
<div v-if="scope.row.id === 'super_group'">
<ms-table-operator
:is-show="true"
@editClick="edit(scope.row)" @deleteClick="del(scope.row)">
<template v-slot:middle>
<ms-table-operator-button
v-permission="['SYSTEM_GROUP:READ+SETTING_PERMISSION']"
:tip="$t('group.set_permission')" icon="el-icon-s-tools"
@exec="setPermission(scope.row)"/>
</template>
</ms-table-operator>
</div>
<div v-else>
<ms-table-operator
:edit-permission="['SYSTEM_GROUP:READ+EDIT']"
:delete-permission="['SYSTEM_GROUP:READ+DELETE']"

View File

@ -360,6 +360,9 @@ export default {
let data = res.data;
if (data) {
this._setResource(data, index, type);
if (id === 'super_group') {
return;
}
if (isHaveWorkspace === false) {
this.addWorkspaceGroup(id, index);
}

View File

@ -11,9 +11,6 @@
<el-radio label="Tapd">
<img class="platform" src="/assets/tapd.png" alt="Tapd"/>
</el-radio>
<el-radio label="Zentao">
<img class="zentao_platform" src="/assets/zentao.jpg" alt="Zentao"/>
</el-radio>
<el-radio label="AzureDevops" v-xpack>
<img class="platform" src="/assets/AzureDevops.png" alt="AzureDevops"/>
</el-radio>
@ -21,7 +18,6 @@
</div>
<tapd-setting v-if="tapdEnable" ref="tapdSetting"/>
<zentao-setting v-if="zentaoEnable" ref="zentaoSetting"/>
<azuredevops-setting v-if="azuredevopsEnable" ref="azureDevopsSetting"/>
<div v-for="config in platformConfigs" :key="config.key">
@ -35,16 +31,14 @@
<script>
import TapdSetting from '@/business/workspace/integration/TapdSetting';
import JiraSetting from '@/business/workspace/integration/JiraSetting';
import ZentaoSetting from '@/business/workspace/integration/ZentaoSetting';
import AzuredevopsSetting from '@/business/workspace/integration/AzureDevopsSetting';
import {AZURE_DEVOPS, TAPD, ZEN_TAO} from "metersphere-frontend/src/utils/constants";
import {AZURE_DEVOPS, TAPD} from "metersphere-frontend/src/utils/constants";
import PlatformConfig from "@/business/workspace/integration/PlatformConfig";
import {generatePlatformResourceUrl, getIntegrationInfo} from "@/api/platform-plugin";
export default {
name: "BugManagement",
components: {PlatformConfig, TapdSetting, JiraSetting, ZentaoSetting, AzuredevopsSetting},
components: {PlatformConfig, TapdSetting, AzuredevopsSetting},
data() {
return {
loading: false,
@ -66,9 +60,6 @@ export default {
tapdEnable() {
return this.platform === TAPD;
},
zentaoEnable() {
return this.platform === ZEN_TAO;
},
azuredevopsEnable() {
return this.platform === AZURE_DEVOPS;
}
@ -90,9 +81,4 @@ export default {
height: 80px;
vertical-align: middle
}
.zentao_platform {
height: 100px;
vertical-align: middle
}
</style>

View File

@ -1,229 +0,0 @@
<template>
<div>
<div style="width: 500px">
<div style="margin-top: 20px;margin-bottom: 10px">{{ $t('organization.integration.basic_auth_info') }}</div>
<el-form :model="form" ref="form" label-width="100px" size="small" :disabled="show" :rules="rules">
<el-form-item :label="$t('organization.integration.account')" prop="account">
<el-input v-model="form.account" :placeholder="$t('organization.integration.input_api_account')"/>
</el-form-item>
<el-form-item label="Token" prop="password">
<el-input v-model="form.password" auto-complete="new-password" v-if="showInput"
:placeholder="$t('organization.integration.input_api_password')" show-password/>
</el-form-item>
<el-form-item :label="$t('organization.integration.jira_url')" prop="url">
<el-input v-model="form.url" :placeholder="$t('organization.integration.input_jira_url')"/>
</el-form-item>
</el-form>
</div>
<bug-manage-btn @save="save"
@init="init"
:edit-permission="['WORKSPACE_SERVICE:READ+EDIT']"
@testConnection="testConnection"
@cancelIntegration="cancelIntegration"
@reloadPassInput="reloadPassInput"
:form="form"
:show.sync="show"
ref="bugBtn"/>
<div class="defect-tip">
<div>{{ $t('organization.integration.use_tip') }}</div>
<div>
1. {{ $t('organization.integration.use_tip_jira') }}
</div>
<div>
2. {{ $t('organization.integration.use_tip_two') }}
<router-link to="/setting/project/all" style="margin-left: 5px;color: #551A8B; text-decoration: underline; cursor: pointer">
{{ $t('organization.integration.link_the_project_now') }}
</router-link>
</div>
<div>
3. {{ $t('organization.integration.use_tip_three') }}
<span style="margin-left: 5px;color: #551A8B; text-decoration: underline; cursor: pointer" @click="resVisible = true">
{{ $t('organization.integration.link_the_info_now') }}
</span>
<el-dialog :close-on-click-modal="false" width="80%"
:visible.sync="resVisible" destroy-on-close @close="closeDialog">
<ms-person-router @closeDialog = "closeDialog"/>
</el-dialog>
</div>
</div>
</div>
</template>
<script>
import BugManageBtn from "./BugManageBtn";
import {getCurrentUser, getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
import {JIRA} from "metersphere-frontend/src/utils/constants";
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import MsPersonRouter from "metersphere-frontend/src/components/personal/PersonRouter";
import {
authServiceIntegration,
delServiceIntegration,
getServiceIntegration,
saveServiceIntegration
} from "../../../api/workspace";
export default {
name: "JiraSetting",
components: {MsInstructionsIcon, BugManageBtn,MsPersonRouter},
created() {
this.init();
},
data() {
return {
show: true,
showInput: true,
resVisible:false,
form: {},
rules: {
account: {
required: true,
message: this.$t('organization.integration.input_api_account'),
trigger: ['change', 'blur']
},
password: {
required: true,
message: this.$t('organization.integration.input_api_password'),
trigger: ['change', 'blur']
},
url: {
required: true,
message: this.$t('organization.integration.input_jira_url'),
trigger: ['change', 'blur']
},
issuetype: {
required: true,
message: this.$t('organization.integration.input_jira_issuetype'),
trigger: ['change', 'blur']
},
storytype: {
required: true,
message: this.$t('organization.integration.input_jira_storytype'),
trigger: ['change', 'blur']
}
},
};
},
methods: {
init() {
const {lastWorkspaceId} = getCurrentUser();
let param = {};
param.platform = JIRA;
param.workspaceId = lastWorkspaceId;
this.$parent.loading = getServiceIntegration(param).then(res => {
let data = res.data;
if (data.configuration) {
let config = JSON.parse(data.configuration);
this.$set(this.form, 'account', config.account);
this.$set(this.form, 'password', config.password);
this.$set(this.form, 'url', config.url);
this.$set(this.form, 'issuetype', config.issuetype);
this.$set(this.form, 'storytype', config.storytype);
} else {
this.clear();
}
});
},
save() {
this.$refs['form'].validate(valid => {
if (valid) {
let formatUrl = this.form.url.trim();
if (!formatUrl.endsWith('/')) {
formatUrl = formatUrl + '/';
}
let param = {};
let auth = {
account: this.form.account,
password: this.form.password,
url: formatUrl,
issuetype: this.form.issuetype,
storytype: this.form.storytype
};
const {lastWorkspaceId} = getCurrentUser();
param.workspaceId = lastWorkspaceId;
param.platform = JIRA;
param.configuration = JSON.stringify(auth);
this.$parent.loading = saveServiceIntegration(param).then(() => {
this.show = true;
this.$refs.bugBtn.showEdit = true;
this.$refs.bugBtn.showSave = false;
this.$refs.bugBtn.showCancel = false;
this.reloadPassInput();
this.init();
this.$success(this.$t('commons.save_success'));
});
} else {
return false;
}
});
},
clear() {
this.$set(this.form, 'account', '');
this.$set(this.form, 'password', '');
this.$set(this.form, 'url', '');
this.$set(this.form, 'issuetype', '');
this.$set(this.form, 'storytype', '');
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
});
},
testConnection() {
if (this.form.account && this.form.password) {
this.$parent.loading = authServiceIntegration(getCurrentWorkspaceId(), JIRA).then(() => {
this.$success(this.$t('organization.integration.verified'));
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
return false;
}
},
cancelIntegration() {
if (this.form.account && this.form.password) {
this.$alert(this.$t('organization.integration.cancel_confirm') + JIRA + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
const {lastWorkspaceId} = getCurrentUser();
let param = {};
param.workspaceId = lastWorkspaceId;
param.platform = JIRA;
this.$parent.loading = delServiceIntegration(param).then(() => {
this.$success(this.$t('organization.integration.successful_operation'));
this.init('');
});
}
}
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
}
},
reloadPassInput() {
this.showInput = false;
this.$nextTick(function () {
this.showInput = true;
});
},
closeDialog(){
this.resVisible = false;
}
}
};
</script>
<style scoped>
.defect-tip {
background: #EDEDED;
border: solid #E1E1E1 1px;
margin: 10px 0;
padding: 10px;
border-radius: 3px;
}
.el-input {
width: 80%;
}
</style>

View File

@ -11,6 +11,15 @@
<custom-filed-component :form="form"
:data="item"
prop="defaultValue"/>
<ms-instructions-icon v-if="item.instructionsIcon || item.instructionsTip" effect="light">
<template>
<img v-if="item.instructionsIcon"
:src="getPlatformImageUrl(config, item)"/>
<span v-if="item.instructionsTip">
{{ item.instructionsTip }}
</span>
</template>
</ms-instructions-icon>
</el-form-item>
</el-form>
</div>
@ -65,7 +74,7 @@ import {
getServiceIntegration,
saveServiceIntegration
} from "../../../api/workspace";
import {validateServiceIntegration} from "@/api/platform-plugin";
import {generatePlatformResourceUrl, validateServiceIntegration} from "@/api/platform-plugin";
import {getPlatformFormRules} from "metersphere-frontend/src/utils/platform";
export default {
@ -181,6 +190,9 @@ export default {
},
closeDialog() {
this.resVisible = false;
},
getPlatformImageUrl(config, item) {
return generatePlatformResourceUrl(config.id, item.instructionsIcon);
}
}
};

View File

@ -1,235 +0,0 @@
<template>
<div>
<div style="width: 500px">
<div style="margin-top: 20px;margin-bottom: 10px">{{ $t('organization.integration.basic_auth_info') }}</div>
<el-form :model="form" ref="form" label-width="100px" size="small" :disabled="show" :rules="rules">
<el-form-item :label="$t('organization.integration.account')" prop="account">
<el-input v-model="form.account" :placeholder="$t('organization.integration.input_api_account')"/>
</el-form-item>
<el-form-item :label="$t('organization.integration.password')" prop="password">
<el-input v-model="form.password" auto-complete="new-password" v-if="showInput"
:placeholder="$t('organization.integration.input_api_password')" show-password/>
</el-form-item>
<el-form-item :label="$t('organization.integration.zentao_url')" prop="url">
<el-input v-model="form.url" :placeholder="$t('organization.integration.input_zentao_url')"/>
</el-form-item>
<el-form-item :label="$t('organization.integration.zentao_request')" prop="request">
<el-radio v-model="form.request" label="PATH_INFO" size="small" border> PATH_INFO</el-radio>
<el-radio v-model="form.request" label="GET" size="small" border>GET</el-radio>
<ms-instructions-icon effect="light" style="margin-left: -20px;">
{{ $t('organization.integration.zentao_config_tip')}} <br/><br/>
{{ $t('organization.integration.zentao_config_path')}}
</ms-instructions-icon>
</el-form-item>
</el-form>
</div>
<bug-manage-btn @save="save"
@init="init"
:edit-permission="['WORKSPACE_SERVICE:READ+EDIT']"
@testConnection="testConnection"
@cancelIntegration="cancelIntegration"
@reloadPassInput="reloadPassInput"
:form="form"
:show.sync="show"
ref="bugBtn"/>
<div class="defect-tip">
<div>{{ $t('organization.integration.use_tip') }}</div>
<div>
1. {{ $t('organization.integration.use_tip_zentao') }}
</div>
<div>
2. {{ $t('organization.integration.use_tip_two') }}
<router-link to="/setting/project/all" style="margin-left: 5px;color: #551A8B; text-decoration: underline; cursor: pointer">
{{ $t('organization.integration.link_the_project_now') }}
</router-link>
</div>
<div>
3. {{ $t('organization.integration.use_tip_three') }}
<span style="margin-left: 5px;color: #551A8B; text-decoration: underline; cursor: pointer" @click="resVisible = true">
{{ $t('organization.integration.link_the_info_now') }}
</span>
<el-dialog :close-on-click-modal="false" width="80%"
:visible.sync="resVisible" destroy-on-close @close="closeDialog">
<ms-person-router @closeDialog = "closeDialog"/>
</el-dialog>
</div>
</div>
</div>
</template>
<script>
import BugManageBtn from "./BugManageBtn";
import {getCurrentUser, getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
import {ZEN_TAO} from "metersphere-frontend/src/utils/constants";
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import MsPersonRouter from "metersphere-frontend/src/components/personal/PersonRouter";
import {
authServiceIntegration,
delServiceIntegration,
getServiceIntegration,
saveServiceIntegration
} from "../../../api/workspace";
export default {
name: "ZentaoSetting",
components: {
MsInstructionsIcon,
BugManageBtn,
MsPersonRouter
},
created() {
this.init();
},
data() {
return {
show: true,
showInput: true,
resVisible:false,
form: {},
rules: {
account: {
required: true,
message: this.$t('organization.integration.input_api_account'),
trigger: ['change', 'blur']
},
password: {
required: true,
message: this.$t('organization.integration.input_api_password'),
trigger: ['change', 'blur']
},
url: {
required: true,
message: this.$t('organization.integration.input_zentao_url'),
trigger: ['change', 'blur']
},
request: {
required: true,
message: this.$t('organization.integration.input_zentao_request'),
trigger: ['change', 'blur']
},
},
};
},
methods: {
save() {
this.$refs['form'].validate(valid => {
if (valid) {
let formatUrl = this.form.url.trim();
if (!formatUrl.endsWith('/')) {
formatUrl = formatUrl + '/';
}
const {lastWorkspaceId} = getCurrentUser();
let param = {};
let auth = {
account: this.form.account,
password: this.form.password,
url: formatUrl,
request: this.form.request
};
param.workspaceId = lastWorkspaceId;
param.platform = ZEN_TAO;
param.configuration = JSON.stringify(auth);
this.$parent.loading = saveServiceIntegration(param).then(() => {
this.show = true;
this.$refs.bugBtn.showEdit = true;
this.$refs.bugBtn.showSave = false;
this.$refs.bugBtn.showCancel = false;
this.reloadPassInput();
this.init();
this.$success(this.$t('commons.save_success'));
});
} else {
return false;
}
});
},
init() {
const {lastWorkspaceId} = getCurrentUser();
let param = {};
param.platform = ZEN_TAO;
param.workspaceId = lastWorkspaceId;
this.$parent.loading = getServiceIntegration(param).then(res => {
let data = res.data;
if (data.configuration) {
let config = JSON.parse(data.configuration);
this.$set(this.form, 'account', config.account);
this.$set(this.form, 'password', config.password);
this.$set(this.form, 'url', config.url);
this.$set(this.form, 'request', config.request ? config.request : 'PATH_INFO');
} else {
this.clear();
}
});
},
clear() {
this.$set(this.form, 'account', '');
this.$set(this.form, 'password', '');
this.$set(this.form, 'url', '');
this.$set(this.form, 'request', '');
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
});
},
testConnection() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.account && this.form.password) {
this.$parent.loading = authServiceIntegration(getCurrentWorkspaceId(), ZEN_TAO).then(() => {
this.$success(this.$t('organization.integration.verified'));
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
return false;
}
} else {
return false;
}
});
},
cancelIntegration() {
if (this.form.account && this.form.password) {
this.$alert(this.$t('organization.integration.cancel_confirm') + ZEN_TAO + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
const {lastWorkspaceId} = getCurrentUser();
let param = {};
param.workspaceId = lastWorkspaceId;
param.platform = ZEN_TAO;
this.$parent.loading = delServiceIntegration(param).then(() => {
this.$success(this.$t('organization.integration.successful_operation'));
this.init('');
});
}
}
});
} else {
this.$warning(this.$t('organization.integration.not_integrated'));
}
},
reloadPassInput() {
this.showInput = false;
this.$nextTick(function () {
this.showInput = true;
});
},
closeDialog(){
this.resVisible = false;
}
}
};
</script>
<style scoped>
.defect-tip {
background: #EDEDED;
border: solid #E1E1E1 1px;
margin: 10px 0;
padding: 10px;
border-radius: 3px;
}
</style>

View File

@ -25,11 +25,11 @@
<el-form-item :label-width="labelWidth"
:label="$t('workspace.issue_template_manage')" prop="issueTemplateId">
<template-select :platform="form.platform" :data="form" scene="ISSUE" prop="issueTemplateId"
:disabled="form.platform === 'Jira' && form.thirdPartTemplate"
:disabled="thirdPartTemplateSupport && form.thirdPartTemplate"
:platformOptions="platformOptions" :project-id="form.id"
ref="issueTemplate"/>
<el-checkbox @change="thirdPartTemplateChange" v-if="form.platform === 'Jira' && thirdPartTemplateSupport"
<el-checkbox @change="thirdPartTemplateChange" v-if="thirdPartTemplateSupport"
v-model="form.thirdPartTemplate" style="margin-left: 10px">
{{ $t('test_track.issue.use_third_party') }}
</el-checkbox>
@ -61,20 +61,6 @@
ref="platformConfig"
/>
<el-form-item :label-width="labelWidth" :label="$t('project.zentao_id')" v-if="zentao">
<el-input v-model="form.zentaoId" autocomplete="off"></el-input>
<el-button @click="check" type="primary" class="checkButton">
{{ $t('test_track.issue.check_id_exist') }}
</el-button>
<ms-instructions-icon effect="light">
<template>
禅道流程产品-项目 | 产品-迭代 | 产品-冲刺 | 项目-迭代 | 项目-冲刺 <br/><br/>
根据 "后台 -> 自定义 -> 流程" 查看对应流程根据流程填写ID <br/><br/>
产品-项目 | 产品-迭代 | 产品-冲刺 需要填写产品ID <br/><br/>
项目-迭代 | 项目-冲刺 需要填写项目ID
</template>
</ms-instructions-icon>
</el-form-item>
<el-form-item :label-width="labelWidth" :label="$t('project.azureDevops_id')" v-if="azuredevops">
<el-input v-model="form.azureDevopsId" autocomplete="off"></el-input>
</el-form-item>
@ -194,9 +180,6 @@ export default {
tapd() {
return this.showPlatform(TAPD);
},
zentao() {
return this.showPlatform(ZEN_TAO);
},
azuredevops() {
return this.showPlatform(AZURE_DEVOPS);
},

View File

@ -1,84 +0,0 @@
<template>
<div>
<el-form-item :label-width="labelWidth" :label="$t('project.jira_key')">
<el-input v-model="form.jiraKey" autocomplete="off" @blur="getIssueTypeOption"/>
<slot name="checkBtn"></slot>
<ms-instructions-icon effect="light">
<template>
<img class="jira-image" src="assets/jira-key.png"/>
</template>
</ms-instructions-icon>
</el-form-item>
<el-form-item :label-width="labelWidth" :label="$t('organization.integration.jira_issuetype')" prop="issuetype">
<el-select filterable v-model="form.issueConfigObj.jiraIssueTypeId">
<el-option v-for="item in issueTypes" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label-width="labelWidth" :label="$t('organization.integration.jira_storytype')" prop="storytype">
<el-select filterable v-model="form.issueConfigObj.jiraStoryTypeId">
<el-option v-for="item in issueTypes" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</el-form-item>
</div>
</template>
<script>
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import {getJiraIssueType} from "../../../api/project";
import {getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
export default {
name: "ProjectJiraConfig",
components: {MsInstructionsIcon},
props: ['labelWidth', 'form', 'result'],
data() {
return {
issueTypes: []
}
},
mounted() {
this.getIssueTypeOption();
},
methods: {
getIssueTypeOption() {
this.issueTypes = [];
this.result.loading = true;
let param = {
projectId: this.form.id,
workspaceId: getCurrentWorkspaceId(),
jiraKey: this.form.jiraKey
}
getJiraIssueType(param)
.then((response) => {
this.issueTypes = response.data;
let hasJiraIssueType = false;
let hasJiraStoryType = false;
if (response.data) {
response.data.forEach(item => {
if (this.form.issueConfigObj.jiraIssueTypeId === item.id) {
hasJiraIssueType = true;
} else if (this.form.issueConfigObj.jiraStoryTypeId === item.id) {
hasJiraStoryType = true;
}
});
}
if (!hasJiraIssueType) {
this.form.issueConfigObj.jiraIssueTypeId = null;
}
if (!hasJiraStoryType) {
this.form.issueConfigObj.jiraStoryTypeId = null;
}
this.result.loading = false;
});
}
}
}
</script>
<style scoped>
.el-input, .el-textarea {
width: 80%;
}
</style>

View File

@ -19,10 +19,13 @@
class="checkButton">
{{ $t('test_track.issue.check_id_exist') }}
</el-button>
<ms-instructions-icon v-if="item.instructionsIcon" effect="light">
<ms-instructions-icon v-if="item.instructionsIcon || item.instructionsTip" effect="light">
<template>
<img class="jira-image"
<img v-if="item.instructionsIcon"
:src="getPlatformImageUrl(config, item)"/>
<span v-if="item.instructionsTip">
{{ item.instructionsTip }}
</span>
</template>
</ms-instructions-icon>
</el-form-item>

Some files were not shown because too many files have changed in this diff Show More