feat(项目设置): 项目设置缺陷平台插件化

This commit is contained in:
chenjianxing 2022-11-16 12:42:17 +08:00 committed by jianxing
parent acf3793c17
commit 83b2115afd
24 changed files with 840 additions and 207 deletions

View File

@ -188,6 +188,7 @@ export default {
if (this.form) {
this.$set(this.form, this.data.name, this.data[this.prop]);
}
this.$emit('change', this.data.name);
this.$forceUpdate();
},
}

View File

@ -41,7 +41,6 @@ export const ZH_TW = 'zh_TW';
export const EN_US = 'en_US';
export const TAPD = 'Tapd';
export const JIRA = 'Jira';
export const ZEN_TAO = 'Zentao';
export const LOCAL = 'Local';
export const AZURE_DEVOPS = 'AzureDevops';

View File

@ -1,6 +1,6 @@
// 模板
import i18n from "../i18n";
import {AZURE_DEVOPS, JIRA, LOCAL, TAPD, ZEN_TAO} from "./constants";
import {AZURE_DEVOPS, LOCAL, TAPD, ZEN_TAO} from "./constants";
export const CUSTOM_FIELD_TYPE_OPTION = [
{value: 'input', text: 'workspace.custom_filed.input'},
@ -68,7 +68,6 @@ export function CASE_TYPE_OPTION(){
export const ISSUE_PLATFORM_OPTION = [
{value: LOCAL, text: 'Local'},
{value: TAPD, text: 'Tapd'},
{value: JIRA, text: 'JIRA'},
{value: ZEN_TAO, text: 'Zentao'},
{value: AZURE_DEVOPS, text: 'Azure Devops'},
];

View File

@ -4,6 +4,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
@Service
@ -37,4 +38,23 @@ public class RemoteService {
public Object post(String url, Object param) {
return Optional.ofNullable(microService.postForData(serviceName, url, param)).orElse(StringUtils.EMPTY);
}
public Object get(HttpServletRequest request) {
// 返回null前端会报错
return Optional.ofNullable(microService.getForData(serviceName, wrapperQuery(request))).orElse(StringUtils.EMPTY);
}
public Object post(HttpServletRequest request, Object param) {
// 返回null前端会报错
return Optional.ofNullable(microService.postForData(serviceName, wrapperQuery(request), param)).orElse(StringUtils.EMPTY);
}
private String wrapperQuery(HttpServletRequest request) {
String url = request.getRequestURI();
if (StringUtils.isNotBlank(request.getQueryString())) {
url += "?" + request.getQueryString();
}
return url;
}
}

View File

@ -17,8 +17,6 @@ import io.metersphere.dto.WorkspaceMemberDTO;
import io.metersphere.i18n.Translator;
import io.metersphere.log.annotation.MsAuditLog;
import io.metersphere.request.AddProjectRequest;
import io.metersphere.request.JiraIssueType;
import io.metersphere.request.JiraIssueTypeRequest;
import io.metersphere.request.ProjectRequest;
import io.metersphere.request.member.AddMemberRequest;
import io.metersphere.request.member.QueryMemberRequest;
@ -139,11 +137,6 @@ public class ProjectController {
return projectService.getAllServiceIntegration();
}
@PostMapping("/issues/jira/issuetype")
public List<JiraIssueType> getJiraIssueType(@RequestBody JiraIssueTypeRequest request) {
return projectService.getJiraIssueType(request);
}
@PostMapping("/member/add")
public void addProjectMember(@RequestBody AddMemberRequest request) {
projectService.addProjectMember(request);

View File

@ -0,0 +1,35 @@
package io.metersphere.controller.remote;
import io.metersphere.remote.service.PlatformPluginService;
import io.metersphere.remote.service.SystemSettingService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping(path = {
"/platform/plugin",
})
public class SystemSettingController {
@Resource
SystemSettingService systemSettingService;
@Resource
PlatformPluginService platformPluginService;
@PostMapping("/**")
public Object list(HttpServletRequest request, @RequestBody Object param) {
return systemSettingService.post(request, param);
}
@GetMapping("/**")
public Object get(HttpServletRequest request) {
return systemSettingService.get(request);
}
@GetMapping("/resource/{pluginId}")
public void getPluginResource(@PathVariable("pluginId") String pluginId, @RequestParam("fileName") String fileName, HttpServletResponse response) {
platformPluginService.getPluginResource(pluginId, fileName, response);
}
}

View File

@ -0,0 +1,48 @@
package io.metersphere.remote.service;
import io.metersphere.commons.constants.StorageConstants;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.metadata.service.FileManagerService;
import io.metersphere.metadata.vo.FileRequest;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@Service
public class PlatformPluginService {
@Resource
FileManagerService fileManagerService;
public static final String DIR_PATH = "system/plugin";
public void getPluginResource(String pluginId, String name, HttpServletResponse response) {
FileRequest request = new FileRequest();
request.setProjectId(DIR_PATH + "/" + pluginId);
request.setFileName(name);
request.setStorage(StorageConstants.MINIO.name());
InputStream inputStream = fileManagerService.downloadFileAsStream(request);
getImage(inputStream, response);
}
public void getImage(InputStream in, HttpServletResponse response) {
response.setContentType("image/png");
try (OutputStream out = response.getOutputStream()) {
out.write(in.readAllBytes());
out.flush();
} catch (Exception e) {
LogUtil.error(e);
} finally {
try {
in.close();
} catch (IOException e) {
LogUtil.error(e);
}
}
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.remote.service;
import io.metersphere.commons.constants.MicroServiceName;
import io.metersphere.service.RemoteService;
import org.springframework.stereotype.Service;
@Service
public class SystemSettingService extends RemoteService {
public SystemSettingService() {
super(MicroServiceName.SYSTEM_SETTING);
}
}

View File

@ -428,10 +428,6 @@ public class ProjectService {
microService.postForData(MicroServiceName.TEST_TRACK, "/issues/check/third/project", project);
}
public List<JiraIssueType> getJiraIssueType(JiraIssueTypeRequest request) {
return microService.postForDataArray(MicroServiceName.TEST_TRACK, "/issues/jira/issuetype", request, JiraIssueType.class);
}
public void addOrUpdateCleanUpSchedule(AddProjectRequest project) {
Boolean cleanTrackReport = project.getCleanTrackReport();
Boolean cleanApiReport = project.getCleanApiReport();

View File

@ -0,0 +1,22 @@
import {post, get} from "metersphere-frontend/src/plugins/request";
const BASE_URL = "/platform/plugin/";
export function getPlatformProjectInfo(key) {
return key ? get(BASE_URL + `project/info/${key}`) : new Promise(r => r({}));
}
export function validateProjectConfig(pluginId, config) {
return post(BASE_URL + `project/validate/${pluginId}`, config);
}
export function getPlatformProjectOption(pluginId, request) {
return post(BASE_URL + 'project/option', request);
}
export function getPlatformOption() {
return get(BASE_URL + 'platform/option');
}
export function getThirdPartTemplateSupportPlatform() {
return get(BASE_URL + 'template/support/list');
}

View File

@ -26,10 +26,10 @@
: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"
:platformOptions="issueOptions" :project-id="form.id"
:platformOptions="platformOptions" :project-id="form.id"
ref="issueTemplate"/>
<el-checkbox @change="thirdPartTemplateChange" v-if="form.platform === 'Jira'"
<el-checkbox @change="thirdPartTemplateChange" v-if="form.platform === 'Jira' && thirdPartTemplateSupport"
v-model="form.thirdPartTemplate" style="margin-left: 10px">
{{ $t('test_track.issue.use_third_party') }}
</el-checkbox>
@ -51,13 +51,15 @@
</el-button>
</el-form-item>
<project-jira-config :result="jiraResult" v-if="jira" :label-width="labelWidth" :form="form" ref="jiraConfig">
<template #checkBtn>
<el-button @click="check" type="primary" class="checkButton">
{{ $t('test_track.issue.check_id_exist') }}
</el-button>
</template>
</project-jira-config>
<project-platform-config
v-if="form.platform === 'Jira'"
:result="jiraResult"
:platform-key="form.platform"
:label-width="labelWidth"
:project-config="platformConfig"
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">
@ -103,7 +105,7 @@ import {
getCurrentUserId,
getCurrentWorkspaceId
} from "metersphere-frontend/src/utils/token";
import {AZURE_DEVOPS, JIRA, PROJECT_ID, TAPD, ZEN_TAO} from "metersphere-frontend/src/utils/constants";
import {AZURE_DEVOPS, PROJECT_ID, TAPD, ZEN_TAO} from "metersphere-frontend/src/utils/constants";
import {PROJECT_CONFIGS} from "metersphere-frontend/src/components/search/search-components";
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import TemplateSelect from "../menu/template/TemplateSelect";
@ -117,7 +119,6 @@ import MsTablePagination from "metersphere-frontend/src/components/pagination/Ta
import MsTableHeader from "metersphere-frontend/src/components/MsTableHeader";
import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter";
import {ISSUE_PLATFORM_OPTION} from "metersphere-frontend/src/utils/table-constants";
import ProjectJiraConfig from "./ProjectJiraConfig";
import {getAllServiceIntegration} from "../../api/project";
import {
checkThirdPlatformProject,
@ -126,11 +127,13 @@ import {
saveProject
} from "../../api/project";
import {updateInfo} from "metersphere-frontend/src/api/user";
import ProjectPlatformConfig from "@/business/home/ProjectPlatformConfig";
import {getPlatformOption, getThirdPartTemplateSupportPlatform} from "@/api/platform-plugin";
export default {
name: "EditProject",
components: {
ProjectJiraConfig,
ProjectPlatformConfig,
MsInstructionsIcon,
TemplateSelect,
MsTableButton,
@ -152,6 +155,9 @@ export default {
jiraResult: {
loading: false
},
platformProjectConfigs: [],
platformConfig: {},
thirdPartTemplateSupportPlatforms: [],
btnTips: this.$t('project.create'),
title: this.$t('project.create'),
condition: {components: PROJECT_CONFIGS},
@ -171,7 +177,6 @@ export default {
],
},
platformOptions: [],
issueOptions: [],
issueTemplateId: "",
ableEdit: true,
};
@ -192,15 +197,15 @@ export default {
tapd() {
return this.showPlatform(TAPD);
},
jira() {
return this.showPlatform(JIRA);
},
zentao() {
return this.showPlatform(ZEN_TAO);
},
azuredevops() {
return this.showPlatform(AZURE_DEVOPS);
},
thirdPartTemplateSupport() {
return this.thirdPartTemplateSupportPlatforms.indexOf(this.form.platform) > -1;
}
},
inject: ['reload'],
destroyed() {
@ -230,6 +235,10 @@ export default {
if (this.$refs.apiTemplate) {
this.$refs.apiTemplate.getTemplateOptions();
}
getThirdPartTemplateSupportPlatform()
.then((r) => {
this.thirdPartTemplateSupportPlatforms = r.data;
});
},
thirdPartTemplateChange(val) {
if (val)
@ -241,38 +250,26 @@ export default {
listenGoBack(this.handleClose);
if (row) {
this.title = this.$t('project.edit');
row.issueConfigObj = row.issueConfig ? JSON.parse(row.issueConfig) : {
jiraIssueTypeId: null,
jiraStoryTypeId: null
};
//
if (!row.issueConfigObj.jiraIssueTypeId) {
row.issueConfigObj.jiraIssueTypeId = null;
}
if (!row.issueConfigObj.jiraStoryTypeId) {
row.issueConfigObj.jiraStoryTypeId = null;
}
this.platformConfig = row.issueConfig ? JSON.parse(row.issueConfig) : {};
this.form = Object.assign({}, row);
this.issueTemplateId = row.issueTemplateId;
} else {
this.form = {issueConfigObj: {jiraIssueTypeId: null, jiraStoryTypeId: null}};
}
if (this.$refs.jiraConfig) {
this.$refs.jiraConfig.getIssueTypeOption(this.form);
}
this.platformOptions = [];
this.platformOptions.push(...ISSUE_PLATFORM_OPTION);
this.loading = getAllServiceIntegration().then(res => {
let data = res.data;
let platforms = data.map(d => d.platform);
this.filterPlatformOptions(platforms, TAPD);
this.filterPlatformOptions(platforms, JIRA);
this.filterPlatformOptions(platforms, ZEN_TAO);
this.filterPlatformOptions(platforms, AZURE_DEVOPS);
this.issueOptions = this.platformOptions;
}).catch(() => {
this.ableEdit = false;
})
getPlatformOption()
.then((r) => {
this.platformOptions = [];
this.platformOptions.push(...r.data);
this.platformOptions.push(...ISSUE_PLATFORM_OPTION);
this.loading = getAllServiceIntegration().then(res => {
let data = res.data;
let platforms = data.map(d => d.platform);
this.filterPlatformOptions(platforms, TAPD);
this.filterPlatformOptions(platforms, ZEN_TAO);
this.filterPlatformOptions(platforms, AZURE_DEVOPS);
}).catch(() => {
this.ableEdit = false;
})
});
},
filterPlatformOptions(platforms, platform) {
if (platforms.indexOf(platform) === -1) {
@ -289,24 +286,34 @@ export default {
if (!valid || !this.ableEdit) {
return false;
}
let protocol = document.location.protocol;
protocol = protocol.substring(0, protocol.indexOf(":"));
this.form.protocal = protocol;
this.form.workspaceId = getCurrentWorkspaceId();
this.form.createUser = getCurrentUserId();
this.form.issueConfig = JSON.stringify(this.form.issueConfigObj);
if (this.issueTemplateId !== this.form.issueTemplateId) {
//
localStorage.removeItem("ISSUE_LIST");
let projectConfig = this.$refs.platformConfig;
if (projectConfig) {
projectConfig.validate()
.then(() => {
this.form.issueConfig = JSON.stringify(projectConfig.form);
this.handleSave()
});
} else {
this.handleSave();
}
});
},
handleSave() {
let protocol = document.location.protocol;
protocol = protocol.substring(0, protocol.indexOf(":"));
this.form.protocal = protocol;
this.form.workspaceId = getCurrentWorkspaceId();
this.form.createUser = getCurrentUserId();
if (this.issueTemplateId !== this.form.issueTemplateId) {
//
localStorage.removeItem("ISSUE_LIST");
}
let promise = this.form.id ? modifyProject(this.form) : saveProject(this.form);
this.loading = promise.then(() => {
this.createVisible = false;
this.reload();
this.$success(this.$t('commons.save_success'));
});
let promise = this.form.id ? modifyProject(this.form) : saveProject(this.form);
this.loading = promise.then(() => {
this.createVisible = false;
this.$success(this.$t('commons.save_success'));
this.reload();
});
},
handleDelete(project) {

View File

@ -0,0 +1,162 @@
<template>
<div>
<el-form :model="form" ref="form" label-width="100px" size="small" :rules="rules">
<el-form-item
:label-width="labelWidth"
v-for="item in config.formItems"
:key="item.name"
:label="item.i18n ? $t(item.label) : item.label"
:prop="item.name">
<custom-filed-component :form="form"
:data="item"
class="custom-filed"
prop="defaultValue"
@change="handleChange"/>
<el-button v-if="item.withProjectCheck"
:disabled="!form[item.name]"
@click="check"
type="primary"
class="checkButton">
{{ $t('test_track.issue.check_id_exist') }}
</el-button>
<ms-instructions-icon v-if="item.instructionsIcon" effect="light">
<template>
<img class="jira-image"
:src="'/platform/plugin/resource/' + config.id + '?fileName=' + item.instructionsIcon"/>
</template>
</ms-instructions-icon>
</el-form-item>
</el-form>
</div>
</template>
<script>
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import {getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
import {
getPlatformProjectInfo,
getPlatformProjectOption,
validateProjectConfig,
} from "@/api/platform-plugin";
import CustomFiledComponent from "metersphere-frontend/src/components/template/CustomFiledComponent";
import {getPlatformFormRules} from "@/business/home/platform";
export default {
name: "ProjectPlatformConfig",
components: {MsInstructionsIcon, CustomFiledComponent},
props: {
labelWidth: String,
result: {
type: Object,
default() {
return {}
}
},
platformKey: String,
projectConfig: {
type: Object,
default() {
return {}
},
}
},
data() {
return {
issueTypes: [],
form: {},
rules: {},
config: {}
}
},
watch: {
platformKey() {
this.getPlatformProjectInfo();
},
},
mounted() {
this.getPlatformProjectInfo();
},
methods: {
getPlatformProjectInfo() {
getPlatformProjectInfo(this.platformKey)
.then(r => {
if (r.data) {
Object.assign(this.form, this.projectConfig);
r.data.formItems.forEach(item => {
if (!item.options) {
item.options = [];
}
//
if (this.form[item.name]) {
this.$set(item, 'defaultValue', this.form[item.name]);
}
//
if (item.cascade && this.form[item.name]) {
this.getCascadeOptions(item, () => {
//
if (this.form[item.name]) {
this.$set(item, 'defaultValue', this.form[item.name]);
}
});
}
});
this.config = r.data;
this.rules = getPlatformFormRules(this.config);
}
});
},
check() {
validateProjectConfig(this.config.id, this.form)
.then(() => {
this.$success(this.$t("system.check_third_project_success"));
});
},
validate() {
return new Promise((resolve, reject) => {
this.$refs['form'].validate((valid) => {
if (!valid) {
reject();
}
resolve();
});
});
},
handleChange(name) {
this.config.formItems.forEach(item => {
if (item.cascade === name) {
this.$set(item, 'options', []);
this.getCascadeOptions(item);
}
});
},
getCascadeOptions(item, callback) {
getPlatformProjectOption(this.config.id, {
platform: this.platformKey,
optionMethod: item.optionMethod,
workspaceId: getCurrentWorkspaceId(),
projectConfig: JSON.stringify(this.form)
}).then((r) => {
this.$set(item, 'options', r.data);
if (callback) {
callback();
}
});
}
}
}
</script>
<style scoped>
.custom-filed :deep(.el-select) {
width: 260px !important;
}
.custom-filed :deep(.el-input, .el-textarea) {
width: 80% !important;
}
.checkButton {
margin-left: 5px;
}
</style>

View File

@ -0,0 +1,15 @@
import i18n from "@/i18n";
export function getPlatformFormRules(config) {
let rules = {};
if (config && config.formItems) {
config.formItems.forEach(item => {
rules[item.name] = {
required: item.required,
message: item.i18n ? i18n.t(item.message) : item.message,
trigger: ['change', 'blur']
}
});
}
return rules;
}

View File

@ -1,12 +1,14 @@
package io.metersphere.controller;
import io.metersphere.domain.SelectOption;
import io.metersphere.dto.PlatformProjectOptionRequest;
import io.metersphere.service.PlatformPluginService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
@RestController
@ -21,8 +23,37 @@ public class PlatformPluginController {
return platformPluginService.getIntegrationInfo();
}
@GetMapping("/project/info/{key}")
public Object getProjectInfo(@PathVariable("key") String key) {
return platformPluginService.getProjectInfo(key);
}
@GetMapping("/resource/{pluginId}")
public void getPluginResource(@PathVariable("pluginId") String pluginId, @RequestParam("fileName") String fileName, HttpServletResponse response) {
platformPluginService.getPluginResource(pluginId, fileName, response);
}
@PostMapping("/integration/validate/{pluginId}")
public void validateIntegration(@PathVariable("pluginId") String pluginId, @RequestBody Map config) {
platformPluginService.validateIntegration(pluginId, config);
}
@PostMapping("/project/validate/{pluginId}")
public void validateProjectConfig(@PathVariable("pluginId") String pluginId, @RequestBody Map config) {
platformPluginService.validateProjectConfig(pluginId, config);
}
@PostMapping("/project/option")
public List<SelectOption> getProjectOption(@RequestBody PlatformProjectOptionRequest request) {
return platformPluginService.getProjectOption(request);
}
@GetMapping("/platform/option")
public List<SelectOption> getPlatformOptions() {
return platformPluginService.getPlatformOptions();
}
@GetMapping("/template/support/list")
public List<String> getThirdPartTemplateSupportPlatform() {
return platformPluginService.getThirdPartTemplateSupportPlatform();
}
}

View File

@ -2,7 +2,6 @@ package io.metersphere.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.base.domain.FileMetadata;
import io.metersphere.base.domain.Project;
import io.metersphere.commons.constants.MicroServiceName;
import io.metersphere.commons.constants.OperLogConstants;
@ -17,14 +16,13 @@ import io.metersphere.dto.WorkspaceMemberDTO;
import io.metersphere.log.annotation.MsAuditLog;
import io.metersphere.request.AddProjectRequest;
import io.metersphere.request.ProjectRequest;
import io.metersphere.service.BaseProjectService;
import io.metersphere.service.BaseCheckPermissionService;
import io.metersphere.service.BaseProjectService;
import io.metersphere.service.MicroService;
import io.metersphere.service.SystemProjectService;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@ -131,9 +129,4 @@ public class SystemProjectController {
public void checkThirdProjectExist(@RequestBody Project project) {
microService.postForData(MicroServiceName.TEST_TRACK, "/issues/check/third/project", project);
}
@PostMapping("/issues/jira/issuetype")
public Object getJiraIssueType(@RequestBody Object request) {
return microService.postForData(MicroServiceName.TEST_TRACK, "/issues/jira/issuetype", request);
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.dto;
import io.metersphere.request.IntegrationRequest;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class PlatformProjectOptionRequest extends IntegrationRequest {
private String optionMethod;
private String projectConfig;
}

View File

@ -1,14 +1,23 @@
package io.metersphere.service;
import im.metersphere.loader.PluginManager;
import io.metersphere.api.Platform;
import io.metersphere.api.PluginMetaInfo;
import io.metersphere.base.domain.PluginWithBLOBs;
import io.metersphere.base.domain.ServiceIntegration;
import io.metersphere.base.mapper.PluginMapper;
import io.metersphere.commons.constants.PluginScenario;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.JSON;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.domain.GetOptionRequest;
import io.metersphere.domain.PlatformRequest;
import io.metersphere.domain.SelectOption;
import io.metersphere.dto.PlatformProjectOptionRequest;
import io.metersphere.loader.PlatformPluginManager;
import io.metersphere.request.IntegrationRequest;
import io.metersphere.utils.PluginManagerUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@ -22,6 +31,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
@ -31,12 +41,14 @@ public class PlatformPluginService {
private BasePluginService basePluginService;
@Resource
private PluginMapper pluginMapper;
@Resource
private BaseIntegrationService baseIntegrationService;
private PluginManager pluginManager;
private PlatformPluginManager pluginManager;
public PluginWithBLOBs addPlatformPlugin(MultipartFile file) {
if (pluginManager != null) {
pluginManager = new PluginManager();
pluginManager = new PlatformPluginManager();
}
String id = UUID.randomUUID().toString();
@ -51,7 +63,7 @@ public class PlatformPluginService {
plugin.setId(id);
plugin.setName(file.getOriginalFilename());
plugin.setPluginId(pluginMetaInfo.getKey() + "-" + pluginMetaInfo.getVersion());
plugin.setScriptId(plugin.getPluginId());
plugin.setScriptId(pluginMetaInfo.getKey());
plugin.setSourcePath("");
// plugin.setFormOption(item.getFormOption());
plugin.setFormScript(JSON.toJSONString(map));
@ -69,7 +81,7 @@ public class PlatformPluginService {
* 查询所有平台插件并加载
*/
public void loadPlatFormPlugins() {
pluginManager = new PluginManager();
pluginManager = new PlatformPluginManager();
List<PluginWithBLOBs> plugins = basePluginService.getPlugins(PluginScenario.platform.name());
PluginManagerUtil.loadPlugins(pluginManager, plugins);
}
@ -83,19 +95,51 @@ public class PlatformPluginService {
public Object getIntegrationInfo() {
List<PluginWithBLOBs> plugins = basePluginService.getPlugins(PluginScenario.platform.name());
List<Map> configs = new ArrayList<>();
plugins.forEach(item ->{
Map metaData = JSON.parseMap(item.getFormScript());
Map serviceIntegration = (Map) metaData.get("serviceIntegration");
serviceIntegration.put("id", metaData.get("id"));
serviceIntegration.put("key", metaData.get("key"));
configs.add(serviceIntegration);
});
plugins.forEach(item -> configs.add(getFrontendMetaDataConfig(item, "serviceIntegration")));
return configs;
}
public Map getProjectInfo(String key) {
List<PluginWithBLOBs> plugins = basePluginService.getPlugins(PluginScenario.platform.name());
for (PluginWithBLOBs plugin : plugins) {
if (StringUtils.equals(plugin.getScriptId(), key)) {
return getFrontendMetaDataConfig(plugin, "projectConfig");
}
}
return null;
}
public List<SelectOption> getProjectOption(PlatformProjectOptionRequest request) {
IntegrationRequest integrationRequest = new IntegrationRequest();
BeanUtils.copyBean(integrationRequest, request);
ServiceIntegration serviceIntegration = baseIntegrationService.get(integrationRequest);
PlatformRequest platformRequest = new PlatformRequest();
platformRequest.setIntegrationConfig(serviceIntegration.getConfiguration());
Platform platform = pluginManager.getPlatformByKey(request.getPlatform(), platformRequest);
GetOptionRequest getOptionRequest = new GetOptionRequest();
getOptionRequest.setOptionMethod(request.getOptionMethod());
getOptionRequest.setProjectConfig(request.getProjectConfig());
try {
return platform.getProjectOptions(getOptionRequest);
} catch (Exception e) {
return new ArrayList<>();
}
}
public Map getFrontendMetaDataConfig(PluginWithBLOBs plugin, String configName) {
Map metaData = JSON.parseMap(plugin.getFormScript());
Map serviceIntegration = (Map) metaData.get(configName);
serviceIntegration.put("id", metaData.get("id"));
serviceIntegration.put("key", metaData.get("key"));
return serviceIntegration;
}
public void getImage(InputStream in, HttpServletResponse response) {
response.setContentType("image/png");
try(OutputStream out = response.getOutputStream()) {
try (OutputStream out = response.getOutputStream()) {
out.write(in.readAllBytes());
out.flush();
} catch (Exception e) {
@ -118,4 +162,54 @@ public class PlatformPluginService {
LogUtil.error(e);
}
}
public Platform getPlatFormInstance(String pluginId, Map IntegrationConfig) {
PlatformRequest request = new PlatformRequest();
request.setIntegrationConfig(JSON.toJSONString(IntegrationConfig));
return pluginManager.getPlatform(pluginId, request);
}
public Platform getPlatFormInstance(String pluginId, String integrationConfig) {
PlatformRequest request = new PlatformRequest();
request.setIntegrationConfig(integrationConfig);
return pluginManager.getPlatform(pluginId, request);
}
public void validateIntegration(String pluginId, Map integrationConfig) {
Platform platform = getPlatFormInstance(pluginId, integrationConfig);
platform.validateIntegrationConfig();
}
public void validateProjectConfig(String pluginId, Map projectConfig) {
PluginMetaInfo pluginMetaInfo = pluginManager.getPluginMetaInfo(pluginId);
IntegrationRequest integrationRequest = new IntegrationRequest();
integrationRequest.setPlatform(pluginMetaInfo.getKey());
integrationRequest.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
ServiceIntegration serviceIntegration = baseIntegrationService.get(integrationRequest);
Platform platform = getPlatFormInstance(pluginId, serviceIntegration.getConfiguration());
platform.validateProjectConfig(JSON.toJSONString(projectConfig));
}
public List<SelectOption> getPlatformOptions() {
List<SelectOption> options = pluginManager.getPluginMetaInfoList()
.stream()
.map(pluginMetaInfo -> new SelectOption(pluginMetaInfo.getLabel(), pluginMetaInfo.getKey()))
.collect(Collectors.toList());
List<ServiceIntegration> integrations = baseIntegrationService.getAll(SessionUtils.getCurrentWorkspaceId());
// 过滤掉服务集成中没有的选项
return options.stream()
.filter(option ->
integrations.stream()
.filter(integration -> StringUtils.equals(integration.getPlatform(), option.getValue()))
.collect(Collectors.toList()).size() > 0
).collect(Collectors.toList());
}
public List<String> getThirdPartTemplateSupportPlatform() {
List<PluginMetaInfo> pluginMetaInfoList = pluginManager.getPluginMetaInfoList();
return pluginMetaInfoList.stream()
.filter(PluginMetaInfo::isThirdPartTemplateSupport)
.map(PluginMetaInfo::getKey)
.collect(Collectors.toList());
}
}

View File

@ -4,3 +4,27 @@ const BASE_URL = "/platform/plugin/";
export function getIntegrationInfo() {
return get(BASE_URL + 'integration/info');
}
export function getPlatformProjectInfo(key) {
return key ? get(BASE_URL + `project/info/${key}`) : new Promise(r => r({}));
}
export function validateServiceIntegration(pluginId, config) {
return post(BASE_URL + `integration/validate/${pluginId}`, config);
}
export function validateProjectConfig(pluginId, config) {
return post(BASE_URL + `project/validate/${pluginId}`, config);
}
export function getPlatformProjectOption(pluginId, request) {
return post(BASE_URL + 'project/option', request);
}
export function getPlatformOption() {
return get(BASE_URL + 'platform/option');
}
export function getThirdPartTemplateSupportPlatform() {
return get(BASE_URL + 'template/support/list');
}

View File

@ -46,11 +46,6 @@ export function getAllServiceIntegration() {
return get('/service/integration/all');
}
export function getJiraIssueType(param) {
return post('/project/issues/jira/issuetype', param);
}
export function getFieldTemplateCaseOption(projectId) {
return get(`/project/field/template/case/option/${projectId}`);
}

View File

@ -53,7 +53,7 @@ export default {
platform: TAPD,
}
},
created() {
activated() {
this.platformConfigs = [];
getIntegrationInfo()
@ -62,7 +62,6 @@ export default {
});
this.platform = TAPD;
this.platformConfigs[0].key;
},
computed: {
tapdEnable() {

View File

@ -56,17 +56,17 @@
<script>
import BugManageBtn from "./BugManageBtn";
import {getCurrentUser, getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
import {JIRA} from "metersphere-frontend/src/utils/constants";
import {getCurrentUser} from "metersphere-frontend/src/utils/token";
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import MsPersonRouter from "metersphere-frontend/src/components/personal/PersonRouter";
import CustomFiledComponent from "metersphere-frontend/src/components/template/CustomFiledComponent";
import {
authServiceIntegration,
delServiceIntegration,
getServiceIntegration,
saveServiceIntegration
} from "../../../api/workspace";
import {validateServiceIntegration} from "@/api/platform-plugin";
import {getPlatformFormRules} from "@/business/workspace/integration/platform";
export default {
name: "PlatformConfig",
@ -93,19 +93,11 @@ export default {
},
methods: {
init() {
let rules = {};
this.config.formItems.forEach(item => {
rules[item.name] = {
required: item.required,
message: item.i18n ? this.$t(item.message) : item.message,
trigger: ['change', 'blur']
}
});
this.rules = rules;
this.rules = getPlatformFormRules(this.config);
const {lastWorkspaceId} = getCurrentUser();
let param = {};
param.platform = JIRA;
param.platform = this.config.key;
param.workspaceId = lastWorkspaceId;
this.$parent.loading = getServiceIntegration(param).then(res => {
let data = res.data;
@ -116,7 +108,7 @@ export default {
this.form = form;
//
this.config.formItems.forEach(item => {
item.defaultValue = this.form[item.name];
this.$set(item, 'defaultValue', this.form[item.name]);
});
} else {
this.clear();
@ -156,36 +148,30 @@ export default {
});
},
testConnection() {
if (this.form.account && this.form.password) {
// todo
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;
}
this.$refs['form'].validate(valid => {
if (valid) {
this.$parent.loading = validateServiceIntegration(this.config.id, this.form).then(() => {
this.$success(this.$t('organization.integration.verified'));
});
}
});
},
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 = this.config.key;
this.$parent.loading = delServiceIntegration(param).then(() => {
this.$success(this.$t('organization.integration.successful_operation'));
this.init('');
});
}
this.$alert(this.$t('organization.integration.cancel_confirm') + this.config.key + "", '', {
confirmButtonText: this.$t('commons.confirm'),
callback: (action) => {
if (action === 'confirm') {
const {lastWorkspaceId} = getCurrentUser();
let param = {};
param.workspaceId = lastWorkspaceId;
param.platform = this.config.key;
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;

View File

@ -0,0 +1,15 @@
import i18n from "@/i18n";
export function getPlatformFormRules(config) {
let rules = {};
if (config && config.formItems) {
config.formItems.forEach(item => {
rules[item.name] = {
required: item.required,
message: item.i18n ? i18n.t(item.message) : item.message,
trigger: ['change', 'blur']
}
});
}
return rules;
}

View File

@ -26,10 +26,10 @@
: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"
:platformOptions="issueOptions" :project-id="form.id"
:platformOptions="platformOptions" :project-id="form.id"
ref="issueTemplate"/>
<el-checkbox @change="thirdPartTemplateChange" v-if="form.platform === 'Jira'"
<el-checkbox @change="thirdPartTemplateChange" v-if="form.platform === 'Jira' && thirdPartTemplateSupport"
v-model="form.thirdPartTemplate" style="margin-left: 10px">
{{ $t('test_track.issue.use_third_party') }}
</el-checkbox>
@ -51,13 +51,15 @@
</el-button>
</el-form-item>
<project-jira-config :result="jiraResult" v-if="jira" :label-width="labelWidth" :form="form" ref="jiraConfig">
<template #checkBtn>
<el-button @click="check" type="primary" class="checkButton">
{{ $t('test_track.issue.check_id_exist') }}
</el-button>
</template>
</project-jira-config>
<project-platform-config
v-if="form.platform === 'Jira'"
:result="jiraResult"
:platform-key="form.platform"
:label-width="labelWidth"
:project-config="platformConfig"
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">
@ -103,7 +105,7 @@ import {
getCurrentUserId,
getCurrentWorkspaceId
} from "metersphere-frontend/src/utils/token";
import {AZURE_DEVOPS, JIRA, PROJECT_ID, TAPD, ZEN_TAO} from "metersphere-frontend/src/utils/constants";
import {AZURE_DEVOPS, PROJECT_ID, TAPD, ZEN_TAO} from "metersphere-frontend/src/utils/constants";
import {PROJECT_CONFIGS} from "metersphere-frontend/src/components/search/search-components";
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import TemplateSelect from "./TemplateSelect";
@ -115,7 +117,6 @@ import MsTablePagination from "metersphere-frontend/src/components/pagination/Ta
import MsTableHeader from "metersphere-frontend/src/components/MsTableHeader";
import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter";
import {ISSUE_PLATFORM_OPTION} from "metersphere-frontend/src/utils/table-constants";
import ProjectJiraConfig from "./ProjectJiraConfig";
import {
getAllServiceIntegration,
checkThirdPlatformProject,
@ -124,11 +125,13 @@ import {
saveProject
} from "../../../api/project";
import {updateInfo} from "metersphere-frontend/src/api/user";
import {getPlatformOption, getPlatformProjectInfo, getThirdPartTemplateSupportPlatform} from "@/api/platform-plugin";
import ProjectPlatformConfig from "@/business/workspace/project/ProjectPlatformConfig";
export default {
name: "EditProject",
components: {
ProjectJiraConfig,
ProjectPlatformConfig,
MsInstructionsIcon,
TemplateSelect,
MsTableButton,
@ -148,6 +151,7 @@ export default {
jiraResult: {
loading: false
},
platformProjectConfigs: [],
btnTips: this.$t('project.create'),
title: this.$t('project.create'),
condition: {components: PROJECT_CONFIGS},
@ -167,9 +171,10 @@ export default {
],
},
platformOptions: [],
issueOptions: [],
issueTemplateId: "",
ableEdit: true,
platformConfig: {},
thirdPartTemplateSupportPlatforms: []
};
},
props: {
@ -188,15 +193,15 @@ export default {
tapd() {
return this.showPlatform(TAPD);
},
jira() {
return this.showPlatform(JIRA);
},
zentao() {
return this.showPlatform(ZEN_TAO);
},
azuredevops() {
return this.showPlatform(AZURE_DEVOPS);
},
thirdPartTemplateSupport() {
return this.thirdPartTemplateSupportPlatforms.indexOf(this.form.platform) > -1;
}
},
inject: ['reload'],
destroyed() {
@ -226,6 +231,10 @@ export default {
if (this.$refs.apiTemplate) {
this.$refs.apiTemplate.getTemplateOptions();
}
getThirdPartTemplateSupportPlatform()
.then((r) => {
this.thirdPartTemplateSupportPlatforms = r.data;
});
},
thirdPartTemplateChange(val) {
if (val)
@ -237,38 +246,32 @@ export default {
listenGoBack(this.handleClose);
if (row) {
this.title = this.$t('project.edit');
row.issueConfigObj = row.issueConfig ? JSON.parse(row.issueConfig) : {
jiraIssueTypeId: null,
jiraStoryTypeId: null
};
//
if (!row.issueConfigObj.jiraIssueTypeId) {
row.issueConfigObj.jiraIssueTypeId = null;
}
if (!row.issueConfigObj.jiraStoryTypeId) {
row.issueConfigObj.jiraStoryTypeId = null;
}
this.platformConfig = row.issueConfig ? JSON.parse(row.issueConfig) : {};
this.form = Object.assign({}, row);
this.issueTemplateId = row.issueTemplateId;
} else {
this.form = {issueConfigObj: {jiraIssueTypeId: null, jiraStoryTypeId: null}};
}
if (this.$refs.jiraConfig) {
this.$refs.jiraConfig.getIssueTypeOption(this.form);
}
this.platformOptions = [];
this.platformOptions.push(...ISSUE_PLATFORM_OPTION);
this.loading = getAllServiceIntegration().then(res => {
let data = res.data;
let platforms = data.map(d => d.platform);
this.filterPlatformOptions(platforms, TAPD);
this.filterPlatformOptions(platforms, JIRA);
this.filterPlatformOptions(platforms, ZEN_TAO);
this.filterPlatformOptions(platforms, AZURE_DEVOPS);
this.issueOptions = this.platformOptions;
}).catch(() => {
this.ableEdit = false;
})
getPlatformOption()
.then((r) => {
this.platformOptions = [];
this.platformOptions.push(...r.data);
this.platformOptions.push(...ISSUE_PLATFORM_OPTION);
this.loading = getAllServiceIntegration().then(res => {
let data = res.data;
let platforms = data.map(d => d.platform);
this.filterPlatformOptions(platforms, TAPD);
this.filterPlatformOptions(platforms, ZEN_TAO);
this.filterPlatformOptions(platforms, AZURE_DEVOPS);
}).catch(() => {
this.ableEdit = false;
})
});
},
getPlatformProjectInfo() {
getPlatformProjectInfo()
.then((r) => {
this.platformProjectConfigs = r.data;
});
},
filterPlatformOptions(platforms, platform) {
if (platforms.indexOf(platform) === -1) {
@ -285,24 +288,34 @@ export default {
if (!valid || !this.ableEdit) {
return false;
}
let protocol = document.location.protocol;
protocol = protocol.substring(0, protocol.indexOf(":"));
this.form.protocal = protocol;
this.form.workspaceId = getCurrentWorkspaceId();
this.form.createUser = getCurrentUserId();
this.form.issueConfig = JSON.stringify(this.form.issueConfigObj);
if (this.issueTemplateId !== this.form.issueTemplateId) {
//
localStorage.removeItem("ISSUE_LIST");
let projectConfig = this.$refs.platformConfig;
if (projectConfig) {
projectConfig.validate()
.then(() => {
this.form.issueConfig = JSON.stringify(projectConfig.form);
this.handleSave()
});
} else {
this.handleSave();
}
});
},
handleSave() {
let protocol = document.location.protocol;
protocol = protocol.substring(0, protocol.indexOf(":"));
this.form.protocal = protocol;
this.form.workspaceId = getCurrentWorkspaceId();
this.form.createUser = getCurrentUserId();
if (this.issueTemplateId !== this.form.issueTemplateId) {
//
localStorage.removeItem("ISSUE_LIST");
}
let promise = this.form.id ? modifyProject(this.form) : saveProject(this.form);
this.loading = promise.then(() => {
this.createVisible = false;
this.$success(this.$t('commons.save_success'));
this.reload();
});
let promise = this.form.id ? modifyProject(this.form) : saveProject(this.form);
this.loading = promise.then(() => {
this.createVisible = false;
this.$success(this.$t('commons.save_success'));
this.reload();
});
},
handleDelete(project) {

View File

@ -0,0 +1,162 @@
<template>
<div>
<el-form :model="form" ref="form" label-width="100px" size="small" :rules="rules">
<el-form-item
:label-width="labelWidth"
v-for="item in config.formItems"
:key="item.name"
:label="item.i18n ? $t(item.label) : item.label"
:prop="item.name">
<custom-filed-component :form="form"
:data="item"
class="custom-filed"
prop="defaultValue"
@change="handleChange"/>
<el-button v-if="item.withProjectCheck"
:disabled="!form[item.name]"
@click="check"
type="primary"
class="checkButton">
{{ $t('test_track.issue.check_id_exist') }}
</el-button>
<ms-instructions-icon v-if="item.instructionsIcon" effect="light">
<template>
<img class="jira-image"
:src="'/platform/plugin/resource/' + config.id + '?fileName=' + item.instructionsIcon"/>
</template>
</ms-instructions-icon>
</el-form-item>
</el-form>
</div>
</template>
<script>
import MsInstructionsIcon from "metersphere-frontend/src/components/MsInstructionsIcon";
import {getCurrentWorkspaceId} from "metersphere-frontend/src/utils/token";
import {
getPlatformProjectInfo,
getPlatformProjectOption,
validateProjectConfig,
} from "@/api/platform-plugin";
import {getPlatformFormRules} from "@/business/workspace/integration/platform";
import CustomFiledComponent from "metersphere-frontend/src/components/template/CustomFiledComponent";
export default {
name: "ProjectPlatformConfig",
components: {MsInstructionsIcon, CustomFiledComponent},
props: {
labelWidth: String,
result: {
type: Object,
default() {
return {}
}
},
platformKey: String,
projectConfig: {
type: Object,
default() {
return {}
},
}
},
data() {
return {
issueTypes: [],
form: {},
rules: {},
config: {}
}
},
watch: {
platformKey() {
this.getPlatformProjectInfo();
},
},
mounted() {
this.getPlatformProjectInfo();
},
methods: {
getPlatformProjectInfo() {
getPlatformProjectInfo(this.platformKey)
.then(r => {
if (r.data) {
Object.assign(this.form, this.projectConfig);
r.data.formItems.forEach(item => {
if (!item.options) {
item.options = [];
}
//
if (this.form[item.name]) {
this.$set(item, 'defaultValue', this.form[item.name]);
}
//
if (item.cascade && this.form[item.name]) {
this.getCascadeOptions(item, () => {
//
if (this.form[item.name]) {
this.$set(item, 'defaultValue', this.form[item.name]);
}
});
}
});
this.config = r.data;
this.rules = getPlatformFormRules(this.config);
}
});
},
check() {
validateProjectConfig(this.config.id, this.form)
.then(() => {
this.$success(this.$t("system.check_third_project_success"));
});
},
validate() {
return new Promise((resolve, reject) => {
this.$refs['form'].validate((valid) => {
if (!valid) {
reject();
}
resolve();
});
});
},
handleChange(name) {
this.config.formItems.forEach(item => {
if (item.cascade === name) {
this.$set(item, 'options', []);
this.getCascadeOptions(item);
}
});
},
getCascadeOptions(item, callback) {
getPlatformProjectOption(this.config.id, {
platform: this.platformKey,
optionMethod: item.optionMethod,
workspaceId: getCurrentWorkspaceId(),
projectConfig: JSON.stringify(this.form)
}).then((r) => {
this.$set(item, 'options', r.data);
if (callback) {
callback();
}
});
}
}
}
</script>
<style scoped>
.custom-filed :deep(.el-select) {
width: 260px !important;
}
.custom-filed :deep(.el-input, .el-textarea) {
width: 80% !important;
}
.checkButton {
margin-left: 5px;
}
</style>