feat(通用功能): 开源生成关系依赖图功能

--task=1010906 --user=陈建星 【开源计划】依赖关系拓扑图 https://www.tapd.cn/55049933/s/1331769
This commit is contained in:
chenjianxing 2023-02-06 14:07:56 +08:00 committed by jianxing
parent a8725ca9e5
commit e5733db8c1
18 changed files with 456 additions and 101 deletions

View File

@ -1,7 +1,7 @@
package io.metersphere.controller; package io.metersphere.controller;
import io.metersphere.request.GraphBatchRequest;
import io.metersphere.service.ApiGraphService; import io.metersphere.service.ApiGraphService;
import io.metersphere.xpack.graph.request.GraphBatchRequest;
import io.metersphere.dto.RelationshipGraphData; import io.metersphere.dto.RelationshipGraphData;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;

View File

@ -1,11 +1,9 @@
package io.metersphere.service; package io.metersphere.service;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.xpack.graph.request.GraphBatchRequest;
import io.metersphere.base.mapper.ext.ExtApiDefinitionMapper; import io.metersphere.base.mapper.ext.ExtApiDefinitionMapper;
import io.metersphere.base.mapper.ext.ExtApiScenarioMapper; import io.metersphere.base.mapper.ext.ExtApiScenarioMapper;
import io.metersphere.dto.RelationshipGraphData; import io.metersphere.dto.RelationshipGraphData;
import io.metersphere.xpack.graph.GraphService; import io.metersphere.request.GraphBatchRequest;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -19,9 +17,10 @@ public class ApiGraphService {
ExtApiDefinitionMapper extApiDefinitionMapper; ExtApiDefinitionMapper extApiDefinitionMapper;
@Resource @Resource
private ExtApiScenarioMapper extApiScenarioMapper; private ExtApiScenarioMapper extApiScenarioMapper;
@Resource
private GraphService graphService;
public RelationshipGraphData getGraphData(String id, String type) { public RelationshipGraphData getGraphData(String id, String type) {
GraphService graphService = CommonBeanFactory.getBean(GraphService.class);
if (StringUtils.equals(type, "API")) { if (StringUtils.equals(type, "API")) {
return graphService.getGraphData(id, extApiDefinitionMapper::getForGraph); return graphService.getGraphData(id, extApiDefinitionMapper::getForGraph);
} }
@ -29,7 +28,6 @@ public class ApiGraphService {
} }
public RelationshipGraphData getGraphDataByCondition(GraphBatchRequest request, String type) { public RelationshipGraphData getGraphDataByCondition(GraphBatchRequest request, String type) {
GraphService graphService = CommonBeanFactory.getBean(GraphService.class);
request.getCondition().setNotEqStatus("Trash"); request.getCondition().setNotEqStatus("Trash");
if (StringUtils.equals(type, "API_SCENARIO")) { if (StringUtils.equals(type, "API_SCENARIO")) {
return graphService.getGraphDataByCondition(request, extApiScenarioMapper::selectIdsByQuery, extApiScenarioMapper::getTestCaseForGraph); return graphService.getGraphDataByCondition(request, extApiScenarioMapper::selectIdsByQuery, extApiScenarioMapper::getTestCaseForGraph);

View File

@ -0,0 +1,88 @@
package io.metersphere.service.scenario;
import io.metersphere.base.domain.ApiScenarioWithBLOBs;
import io.metersphere.request.RelationshipEdgeRequest;
import io.metersphere.service.RelationshipEdgeService;
import io.metersphere.util.ObjectUtil;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional(rollbackFor = Exception.class)
public class ApiAutomationRelationshipEdgeService {
@Resource
private RelationshipEdgeService relationshipEdgeService;
// 初始化场景关系
public void initRelationshipEdge(ApiScenarioWithBLOBs preBlobs, ApiScenarioWithBLOBs scenarioWithBLOBs) {
if (scenarioWithBLOBs == null || StringUtils.isEmpty(scenarioWithBLOBs.getScenarioDefinition())) {
return;
}
// 更新操作检查更新前后是否有变更
List<String> beforeReferenceRelationships = new ArrayList<>();
if (preBlobs != null && StringUtils.isNotEmpty(preBlobs.getScenarioDefinition())) {
beforeReferenceRelationships = this.contentAnalysis(preBlobs);
}
// 当前场景
List<String> referenceRelationships = this.contentAnalysis(scenarioWithBLOBs);
if (CollectionUtils.isNotEmpty(beforeReferenceRelationships)) {
beforeReferenceRelationships.removeAll(referenceRelationships);
// 删除多余的关系
if (CollectionUtils.isNotEmpty(beforeReferenceRelationships)) {
relationshipEdgeService.delete(scenarioWithBLOBs.getId(),beforeReferenceRelationships);
}
}
if (CollectionUtils.isNotEmpty(referenceRelationships)) {
RelationshipEdgeRequest request = new RelationshipEdgeRequest();
request.setId(scenarioWithBLOBs.getId());
request.setTargetIds(referenceRelationships);
request.setType("API_SCENARIO");
relationshipEdgeService.saveBatch(request);
}
}
private List<String> contentAnalysis(ApiScenarioWithBLOBs scenarioWithBLOBs) {
List<String> referenceRelationships = new ArrayList<>();
if (scenarioWithBLOBs.getScenarioDefinition().contains("\"referenced\":\"REF\"")) {
// 深度解析对比防止是复制的关系
JSONObject element = ObjectUtil.parseObject(scenarioWithBLOBs.getScenarioDefinition());
// 历史数据处理
this.relationships(element.getJSONArray("hashTree"), referenceRelationships);
}
return referenceRelationships;
}
/**
* 只找出场景直接依赖
*
* @param hashTree
* @param referenceRelationships
*/
public static void relationships(JSONArray hashTree, List<String> referenceRelationships) {
for (int i = 0; i < hashTree.length(); i++) {
JSONObject element = hashTree.getJSONObject(i);
if (element != null && StringUtils.equals(element.get("type").toString(), "scenario") && StringUtils.equals(element.get("referenced").toString(), "REF")) {
if (!referenceRelationships.contains(element.get("id").toString()) && element.get("id").toString().length() < 50) {
referenceRelationships.add(element.get("id").toString());
}
} else {
if (element.has("hashTree")) {
JSONArray elementJSONArray = element.getJSONArray("hashTree");
relationships(elementJSONArray, referenceRelationships);
}
}
}
}
}

View File

@ -57,7 +57,6 @@ import io.metersphere.service.definition.TcpApiParamService;
import io.metersphere.service.ext.ExtApiScheduleService; import io.metersphere.service.ext.ExtApiScheduleService;
import io.metersphere.service.ext.ExtFileAssociationService; import io.metersphere.service.ext.ExtFileAssociationService;
import io.metersphere.service.plan.TestPlanScenarioCaseService; import io.metersphere.service.plan.TestPlanScenarioCaseService;
import io.metersphere.xpack.api.service.ApiAutomationRelationshipEdgeService;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections4.MapUtils; import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
@ -159,6 +158,8 @@ public class ApiScenarioService {
private ExtTestPlanApiScenarioMapper extTestPlanApiScenarioMapper; private ExtTestPlanApiScenarioMapper extTestPlanApiScenarioMapper;
@Resource @Resource
private BaseQuotaService baseQuotaService; private BaseQuotaService baseQuotaService;
@Resource
private ApiAutomationRelationshipEdgeService apiAutomationRelationshipEdgeService;
private ThreadLocal<Long> currentScenarioOrder = new ThreadLocal<>(); private ThreadLocal<Long> currentScenarioOrder = new ThreadLocal<>();
@ -290,10 +291,8 @@ public class ApiScenarioService {
apiScenarioMapper.insert(scenario); apiScenarioMapper.insert(scenario);
apiScenarioReferenceIdService.saveApiAndScenarioRelation(scenario); apiScenarioReferenceIdService.saveApiAndScenarioRelation(scenario);
// 存储依赖关系 // 存储依赖关系
ApiAutomationRelationshipEdgeService relationshipEdgeService = CommonBeanFactory.getBean(ApiAutomationRelationshipEdgeService.class); apiAutomationRelationshipEdgeService.initRelationshipEdge(null, scenario);
if (relationshipEdgeService != null) {
relationshipEdgeService.initRelationshipEdge(null, scenario);
}
uploadFiles(request, bodyFiles, scenarioFiles); uploadFiles(request, bodyFiles, scenarioFiles);
return scenario; return scenario;
} }
@ -396,10 +395,8 @@ public class ApiScenarioService {
uploadFiles(request, bodyFiles, scenarioFiles); uploadFiles(request, bodyFiles, scenarioFiles);
// 存储依赖关系 // 存储依赖关系
ApiAutomationRelationshipEdgeService relationshipEdgeService = CommonBeanFactory.getBean(ApiAutomationRelationshipEdgeService.class); apiAutomationRelationshipEdgeService.initRelationshipEdge(beforeScenario, scenario);
if (relationshipEdgeService != null) {
relationshipEdgeService.initRelationshipEdge(beforeScenario, scenario);
}
String defaultVersion = baseProjectVersionMapper.getDefaultVersion(request.getProjectId()); String defaultVersion = baseProjectVersionMapper.getDefaultVersion(request.getProjectId());
if (StringUtils.equalsIgnoreCase(request.getVersionId(), defaultVersion)) { if (StringUtils.equalsIgnoreCase(request.getVersionId(), defaultVersion)) {
checkAndSetLatestVersion(beforeScenario.getRefId()); checkAndSetLatestVersion(beforeScenario.getRefId());
@ -1332,10 +1329,8 @@ public class ApiScenarioService {
sendImportScenarioCreateNotice(scenarioWithBLOBs); sendImportScenarioCreateNotice(scenarioWithBLOBs);
batchMapper.insert(scenarioWithBLOBs); batchMapper.insert(scenarioWithBLOBs);
// 存储依赖关系 // 存储依赖关系
ApiAutomationRelationshipEdgeService relationshipEdgeService = CommonBeanFactory.getBean(ApiAutomationRelationshipEdgeService.class); apiAutomationRelationshipEdgeService.initRelationshipEdge(null, scenarioWithBLOBs);
if (relationshipEdgeService != null) {
relationshipEdgeService.initRelationshipEdge(null, scenarioWithBLOBs);
}
apiScenarioReferenceIdService.saveApiAndScenarioRelation(scenarioWithBLOBs); apiScenarioReferenceIdService.saveApiAndScenarioRelation(scenarioWithBLOBs);
extApiScenarioMapper.clearLatestVersion(scenarioWithBLOBs.getRefId()); extApiScenarioMapper.clearLatestVersion(scenarioWithBLOBs.getRefId());
extApiScenarioMapper.addLatestVersion(scenarioWithBLOBs.getRefId()); extApiScenarioMapper.addLatestVersion(scenarioWithBLOBs.getRefId());

View File

@ -0,0 +1,22 @@
package io.metersphere.util;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.JSON;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import java.util.Map;
public class ObjectUtil {
public static JSONObject parseObject(String value) {
try {
if (StringUtils.isEmpty(value)) {
MSException.throwException("value is null");
}
Map<String, Object> map = JSON.parseObject(value, Map.class);
return new JSONObject(map);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -360,7 +360,7 @@
@errorRefresh="errorRefresh" @errorRefresh="errorRefresh"
ref="runTest"/> ref="runTest"/>
<ms-task-center ref="taskCenter" :show-menu="false"/> <ms-task-center ref="taskCenter" :show-menu="false"/>
<mx-relationship-graph-drawer v-xpack :graph-data="graphData" ref="relationshipGraph"/> <relationship-graph-drawer :graph-data="graphData" ref="relationshipGraph"/>
<!-- 删除接口提示 --> <!-- 删除接口提示 -->
<scenario-delete-confirm ref="apiDeleteConfirmVersion" @handleDelete="_handleDelete"/> <scenario-delete-confirm ref="apiDeleteConfirmVersion" @handleDelete="_handleDelete"/>
<!-- 删除场景弹窗 --> <!-- 删除场景弹窗 -->
@ -472,7 +472,7 @@ export default {
MsTableOperatorButton: () => import('metersphere-frontend/src/components/MsTableOperatorButton'), MsTableOperatorButton: () => import('metersphere-frontend/src/components/MsTableOperatorButton'),
MsTaskCenter: () => import('metersphere-frontend/src/components/task/TaskCenter'), MsTaskCenter: () => import('metersphere-frontend/src/components/task/TaskCenter'),
MsRun: () => import('./DebugRun'), MsRun: () => import('./DebugRun'),
MxRelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer'), RelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/RelationshipGraphDrawer'),
}, },
props: { props: {
referenced: { referenced: {
@ -637,7 +637,6 @@ export default {
{ {
name: this.$t('test_track.case.generate_dependencies'), name: this.$t('test_track.case.generate_dependencies'),
handleClick: this.generateGraph, handleClick: this.generateGraph,
isXPack: true,
permissions: ['PROJECT_API_SCENARIO:READ+EDIT'], permissions: ['PROJECT_API_SCENARIO:READ+EDIT'],
}, },
{ {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="dependencies-container"> <div class="dependencies-container">
<el-tooltip v-xpack class="item" effect="dark" :content="$t('commons.relationship.graph')" placement="left"> <el-tooltip class="item" effect="dark" :content="$t('commons.relationship.graph')" placement="left">
<font-awesome-icon class="graph-icon" :icon="['fas', 'sitemap']" size="lg" @click="openGraph" /> <font-awesome-icon class="graph-icon" :icon="['fas', 'sitemap']" size="lg" @click="openGraph" />
</el-tooltip> </el-tooltip>
@ -25,7 +25,7 @@
:resource-type="resourceType" :resource-type="resourceType"
ref="postRelationshipList" /> ref="postRelationshipList" />
<mx-relationship-graph-drawer v-xpack v-permission :graph-data="graphData" ref="relationshipGraph" /> <relationship-graph-drawer v-permission :graph-data="graphData" ref="relationshipGraph" />
</div> </div>
</template> </template>
@ -37,7 +37,7 @@ export default {
name: 'DependenciesList', name: 'DependenciesList',
components: { components: {
RelationshipList, RelationshipList,
MxRelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer'), RelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/RelationshipGraphDrawer'),
}, },
props: ['resourceId', 'resourceType', 'readOnly', 'versionEnable'], props: ['resourceId', 'resourceType', 'readOnly', 'versionEnable'],
data() { data() {

View File

@ -238,7 +238,7 @@
<ms-show-reference ref="viewRef" :show-plan="false" :is-has-ref="false" api-type="API"/> <ms-show-reference ref="viewRef" :show-plan="false" :is-has-ref="false" api-type="API"/>
<case-batch-move @refresh="initTable" @moveSave="moveSave" ref="testCaseBatchMove"/> <case-batch-move @refresh="initTable" @moveSave="moveSave" ref="testCaseBatchMove"/>
<mx-relationship-graph-drawer v-xpack :graph-data="graphData" ref="relationshipGraph"/> <relationship-graph-drawer :graph-data="graphData" ref="relationshipGraph"/>
<!-- 删除接口提示 --> <!-- 删除接口提示 -->
<list-item-delete-confirm ref="apiDeleteConfirm" @handleDelete="_handleDelete"/> <list-item-delete-confirm ref="apiDeleteConfirm" @handleDelete="_handleDelete"/>
</span> </span>
@ -333,7 +333,7 @@ export default {
TableExtendBtns, TableExtendBtns,
MsShowReference, MsShowReference,
MsApiReportStatus: () => import('../../../automation/report/ApiReportStatus'), MsApiReportStatus: () => import('../../../automation/report/ApiReportStatus'),
MxRelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer'), RelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/RelationshipGraphDrawer'),
}, },
data() { data() {
return { return {
@ -381,7 +381,6 @@ export default {
}, },
{ {
name: this.$t('test_track.case.generate_dependencies'), name: this.$t('test_track.case.generate_dependencies'),
isXPack: true,
handleClick: this.generateGraph, handleClick: this.generateGraph,
permissions: ['PROJECT_API_DEFINITION:READ+EDIT_API'], permissions: ['PROJECT_API_DEFINITION:READ+EDIT_API'],
}, },

View File

@ -8,7 +8,7 @@ import DrawerHeader from "../head/DrawerHeader";
import MsChart from "../chart/MsChart"; import MsChart from "../chart/MsChart";
export default { export default {
name: "MxRelationshipGraph", name: "RelationshipGraph",
components: {MsChart, DrawerHeader, MsDrawer}, components: {MsChart, DrawerHeader, MsDrawer},
props: { props: {
data: Array, links: Array, data: Array, links: Array,

View File

@ -16,10 +16,10 @@
<script> <script>
import MsDrawer from "../MsDrawer"; import MsDrawer from "../MsDrawer";
import DrawerHeader from "../head/DrawerHeader"; import DrawerHeader from "../head/DrawerHeader";
import RelationshipGraph from "./MxRelationshipGraph"; import RelationshipGraph from "./RelationshipGraph";
export default { export default {
name: "MxRelationshipGraphDrawer", name: "RelationshipGraphDrawer",
components: {RelationshipGraph, DrawerHeader, MsDrawer}, components: {RelationshipGraph, DrawerHeader, MsDrawer},
props: ['graphData'], props: ['graphData'],
data() { data() {

View File

@ -1,7 +1,5 @@
package io.metersphere.xpack.graph.request; package io.metersphere.request;
import io.metersphere.request.BaseQueryRequest;
import io.metersphere.request.OrderRequest;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@ -0,0 +1,311 @@
package io.metersphere.service;
import io.metersphere.base.domain.RelationshipEdge;
import io.metersphere.base.domain.RelationshipEdgeExample;
import io.metersphere.base.mapper.RelationshipEdgeMapper;
import io.metersphere.dto.RelationshipGraphData;
import io.metersphere.request.BaseQueryRequest;
import io.metersphere.request.GraphBatchRequest;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
public class GraphService {
@Resource
RelationshipEdgeMapper relationshipEdgeMapper;
private static Integer GRAPH_EDGES_WIDTH = 5;
public RelationshipGraphData getGraphDataByCondition(GraphBatchRequest request,
Function<BaseQueryRequest, List<String>> getIdsByBaseQueryFunc,
Function<Set<String>, List<RelationshipGraphData.Node>> findNodesFunc) {
ServiceUtils.getSelectAllIds(request, request.getCondition(),
getIdsByBaseQueryFunc::apply);
List<String> ids = request.getIds();
if (CollectionUtils.isEmpty(ids)) {
return new RelationshipGraphData();
}
return getGraphData(ids, findNodesFunc::apply);
}
/**
* 获取图的数据
*
* @param centerNodeIds 给定中心节点的 id
* @param findNodesFunc 根据id列表查询节点
* @return
*/
public RelationshipGraphData getGraphData(List<String> centerNodeIds, Function<Set<String>, List<RelationshipGraphData.Node>> findNodesFunc) {
List<RelationshipGraphData.Node> nodes = new ArrayList<>();
List<RelationshipGraphData.Edge> edges = new ArrayList<>();
// 标记搜索过的节点避免出现死循环
Set<String> markSet = new HashSet<>();
Integer xAxis = null;
Comparator<RelationshipGraphData.Node> comparatorX = Comparator.comparing(RelationshipGraphData.Node::getX);
Comparator<RelationshipGraphData.Node> comparatorY = Comparator.comparing(RelationshipGraphData.Node::getY);
Set<String> centerNodeIdSet = centerNodeIds.stream().collect(Collectors.toSet());
for (int i = 0; i < centerNodeIds.size(); i++) {
String centerNodeId = centerNodeIds.get(i);
if (markSet.contains(centerNodeId)) {
continue;
}
// 记录当前 Y 坐标的层级中X 坐标的最小值和最大值
HashMap<Integer, RelationshipGraphData.XAxisMark> axisMarkMap = new HashMap<>();
// 查询该节点所在图的所有边
List<RelationshipEdge> relationshipEdges = getByNodeId(centerNodeId);
Set<String> nodeIds = new HashSet<>(); // 所有顶点的id
Set<String> sourceIds = relationshipEdges.stream().map(RelationshipEdge::getSourceId).collect(Collectors.toSet());
Set<String> targetIds = relationshipEdges.stream().map(RelationshipEdge::getTargetId).collect(Collectors.toSet());
nodeIds.addAll(sourceIds);
nodeIds.addAll(targetIds);
// 获取所有顶点
List<RelationshipGraphData.Node> currentNodes;
if (CollectionUtils.isEmpty(nodeIds)) {
// 如果是孤岛的话根据 id 查询出该节点
currentNodes = findNodesFunc.apply(new HashSet<>(Arrays.asList(centerNodeId)));
} else {
// 查询当前点能遍历的所有顶点排除了回收站的节点
currentNodes = findNodesFunc.apply(nodeIds);
}
Map<String, RelationshipGraphData.Node> nodeMap = currentNodes.stream().collect(Collectors.toMap(RelationshipGraphData.Node::getId, item -> item));
Iterator<RelationshipEdge> iterator = relationshipEdges.iterator();
while (iterator.hasNext()) {
// 放进回收站的边不遍历
RelationshipEdge next = iterator.next();
if (nodeMap.get(next.getTargetId()) == null || nodeMap.get(next.getSourceId()) == null) {
iterator.remove();
}
}
RelationshipGraphData.Node node = nodeMap.get(centerNodeId);
if (node == null) {
continue;
}
node.setY(0);
node.setX(0);
axisMarkMap.put(0, new RelationshipGraphData.XAxisMark(0, 0));
bfsForXY(node, relationshipEdges, nodeMap, markSet, axisMarkMap, true);
bfsForXY(node, relationshipEdges, nodeMap, markSet, axisMarkMap, false);
// X 为空说明没有遍历到可能是因为图中某个节点放入回收站导致中心节点访问不到其他节点
currentNodes = currentNodes.stream().filter(item -> item.getX() != null).collect(Collectors.toList());
if (xAxis != null) {
Integer min = currentNodes.stream().min(comparatorX).get().getX();
// 将这张图平移到上一张图的右侧
for (RelationshipGraphData.Node currentNode : currentNodes) {
currentNode.setX(currentNode.getX() + xAxis - min + GRAPH_EDGES_WIDTH);
}
}
if (CollectionUtils.isEmpty(currentNodes)) {
continue;
}
// 记录当前 X 的最大值
xAxis = currentNodes.stream().max(comparatorX).get().getX();
int nodesSize = nodes.size();
nodes.addAll(currentNodes);
currentNodes.forEach(item -> {
item.setCategory(2); // 默认其他节点为分组2
});
for (int j = nodesSize; j < nodes.size(); j++) {
nodes.get(j).setIndex(j); // 设置下标
if (centerNodeIds.contains(nodes.get(j).getId())) {
nodes.get(j).setCategory(0); // 中心节点为分组0
}
}
// 记录与中心节点直接关联的节点id
Set<String> directRelationshipNode = new HashSet<>();
// 得到所有的边
for (RelationshipEdge relationshipEdge : relationshipEdges) {
RelationshipGraphData.Edge edge = new RelationshipGraphData.Edge();
RelationshipGraphData.Node sourceNode = nodeMap.get(relationshipEdge.getSourceId());
RelationshipGraphData.Node targetNode = nodeMap.get(relationshipEdge.getTargetId());
if (sourceNode == null || targetNode == null) {
continue;
}
edge.setSource(sourceNode.getIndex());
edge.setTarget(targetNode.getIndex());
if (sourceNode.getX() != null && targetNode.getX() != null && Math.abs(sourceNode.getX() - targetNode.getX()) > 5 && sourceNode.getY() == targetNode.getY()) {
edge.setCurveness(0.2F);
}
edges.add(edge);
if (centerNodeIdSet.contains(relationshipEdge.getSourceId())) {
directRelationshipNode.add(relationshipEdge.getTargetId());
}
if (centerNodeIdSet.contains(relationshipEdge.getTargetId())) {
directRelationshipNode.add(relationshipEdge.getSourceId());
}
}
currentNodes.forEach(item -> {
if (directRelationshipNode.contains(item.getId()) && item.getCategory() != 0) {
item.setCategory(1); // 直接关联的节点为分组1
}
});
}
RelationshipGraphData relationshipGraphData = new RelationshipGraphData();
relationshipGraphData.setLinks(edges);
relationshipGraphData.setData(nodes);
if (CollectionUtils.isNotEmpty(nodes)) {
int xWith = nodes.stream().max(comparatorX).get().getX() - nodes.stream().min(comparatorX).get().getX();
int yWith = nodes.stream().max(comparatorY).get().getY() - nodes.stream().min(comparatorY).get().getY();
xWith = xWith == 0 ? 1 : xWith;
yWith = yWith == 0 ? 1 : yWith;
if (xWith / yWith > 2) {
// 如果太扁平沿Y轴拉伸一下
nodes.forEach(node -> node.setY(node.getY() * 2));
} else if (yWith / xWith > 2) {
// 如果太窄沿X轴拉伸一下
nodes.forEach(node -> node.setX(node.getX() * 2));
}
int xUnitCount = (xWith) / GRAPH_EDGES_WIDTH + 1;
int yUnitCount = (yWith) / GRAPH_EDGES_WIDTH + 1;
relationshipGraphData.setXUnitCount(xUnitCount);
relationshipGraphData.setYUnitCount(yUnitCount);
}
return relationshipGraphData;
}
public RelationshipGraphData getGraphData(String centerNode, Function<Set<String>, List<RelationshipGraphData.Node>> findNodesFunc) {
return this.getGraphData(Arrays.asList(centerNode), findNodesFunc);
}
/**
* 广度优先搜索设置 x,y 的值
*
* @param node
* @param edges
* @param nodeMap
* @param markSet 标记当前节点已搜索过避免反向搜索时出现死循环
* @param isForwardDirection 箭头方向按不同方向遍历
*/
private void bfsForXY(RelationshipGraphData.Node node, List<RelationshipEdge> edges, Map<String, RelationshipGraphData.Node> nodeMap, Set<String> markSet, HashMap<Integer, RelationshipGraphData.XAxisMark> axisMarkMap, Boolean isForwardDirection) {
markSet.add(node.getId());
List<RelationshipGraphData.Node> nextLevelNodes = new ArrayList<>();
for (RelationshipEdge relationshipEdge : edges) {
RelationshipGraphData.Node nextNode = null;
if (isForwardDirection) {// 正向则搜索 sourceId 是当前节点的边
if (node.getId().equals(relationshipEdge.getSourceId())) {
nextNode = nodeMap.get(relationshipEdge.getTargetId());
}
} else {
if (node.getId().equals(relationshipEdge.getTargetId())) {
nextNode = nodeMap.get(relationshipEdge.getSourceId());
}
}
if (nextNode != null) { // 节点可能在回收站
nextLevelNodes.add(nextNode);
}
}
int nextLevelNodesCount = nextLevelNodes.size();
Integer nextY;
if (isForwardDirection) {
// 正向搜索Y -1
nextY = node.getY() - GRAPH_EDGES_WIDTH;
} else {
nextY = node.getY() + GRAPH_EDGES_WIDTH;
}
// 获取下一层级的 x 坐标轴已分配的最大最小值
RelationshipGraphData.XAxisMark nextLevelXAxisMark;
nextLevelXAxisMark = axisMarkMap.get(nextY);
for (int i = 0; i < nextLevelNodesCount; i++) {
RelationshipGraphData.Node nextNode = nextLevelNodes.get(i);
if (nextNode.getY() == null) {
nextNode.setY(nextY);
}
if (nextLevelXAxisMark == null && nextNode.getX() == null) {
nextLevelXAxisMark = new RelationshipGraphData.XAxisMark(node.getX(), node.getX());
axisMarkMap.put(nextY, nextLevelXAxisMark);
if (i == 0 && nextLevelNodesCount % 2 != 0) {
// 如果是奇数个把第一个放在与当前节点 x 轴相同的位置
nextNode.setX(node.getX());
}
}
// 没设置过才设置
if (nextNode.getX() == null) {
if (i % 2 != 0) { // 左边放一个右边放一个
nextNode.setX(nextLevelXAxisMark.getMin() - GRAPH_EDGES_WIDTH);
nextLevelXAxisMark.setMin(nextLevelXAxisMark.getMin() - GRAPH_EDGES_WIDTH);
} else {
nextNode.setX(nextLevelXAxisMark.getMax() + GRAPH_EDGES_WIDTH);
nextLevelXAxisMark.setMax(nextLevelXAxisMark.getMax() + GRAPH_EDGES_WIDTH);
}
}
}
nextLevelNodes.forEach(nextNode -> {
if (!markSet.contains(nextNode.getId())) {
bfsForXY(nextNode, edges, nodeMap, markSet, axisMarkMap, true);
bfsForXY(nextNode, edges, nodeMap, markSet, axisMarkMap, false);
}
});
}
/**
* 给定一个节点搜索出该节点所在的图的所有边
*
* @param nodeId
*/
public List<RelationshipEdge> getByNodeId(String nodeId) {
String graphId = getGraphIdByNodeId(nodeId);
if (StringUtils.isNotBlank(graphId)) {
RelationshipEdgeExample example = new RelationshipEdgeExample();
example.createCriteria().andGraphIdEqualTo(graphId);
return relationshipEdgeMapper.selectByExample(example);
}
return new ArrayList<>();
}
public String getGraphIdByNodeId(String nodeId) {
RelationshipEdgeExample example = new RelationshipEdgeExample();
example.createCriteria().andSourceIdEqualTo(nodeId);
List<RelationshipEdge> relationshipEdges = relationshipEdgeMapper.selectByExample(example);
if (CollectionUtils.isEmpty(relationshipEdges)) {
example.clear();
example.createCriteria().andTargetIdEqualTo(nodeId);
relationshipEdges = relationshipEdgeMapper.selectByExample(example);
}
if (CollectionUtils.isNotEmpty(relationshipEdges)) {
return relationshipEdges.get(0).getGraphId();
}
return null;
}
}

View File

@ -1,8 +0,0 @@
package io.metersphere.xpack.api.service;
import io.metersphere.base.domain.ApiScenarioWithBLOBs;
public interface ApiAutomationRelationshipEdgeService {
// 初始化引用关系
void initRelationshipEdge(ApiScenarioWithBLOBs before, ApiScenarioWithBLOBs now);
}

View File

@ -1,43 +0,0 @@
package io.metersphere.xpack.graph;
import io.metersphere.dto.RelationshipGraphData;
import io.metersphere.request.BaseQueryRequest;
import io.metersphere.xpack.graph.request.GraphBatchRequest;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
public interface GraphService {
/**
* 生成关系图
* @param centerNodeIds 选中的节点ID数组
* @param findNodesFunc 根据节点ID数组查找对应节点信息的方法
* @return
*/
RelationshipGraphData getGraphData(List<String> centerNodeIds,
Function<Set<String>,
List<RelationshipGraphData.Node>> findNodesFunc);
/**
* 生成关系图
* @param centerNodeId 选中的节点ID
* @param findNodesFunc 根据节点ID数组查找对应节点信息的方法
* @return
*/
RelationshipGraphData getGraphData(String centerNodeId,
Function<Set<String>,
List<RelationshipGraphData.Node>> findNodesFunc);
/**
* 列表勾选-生成关系图
* @param request 列表的查询条件
* @param getIdsByBaseQueryFunc 根据查询条件查询ID的方法
* @param findNodesFunc 根据节点ID数组查找对应节点信息的方法
* @return
*/
RelationshipGraphData getGraphDataByCondition(GraphBatchRequest request,
Function<BaseQueryRequest, List<String>> getIdsByBaseQueryFunc,
Function<Set<String>, List<RelationshipGraphData.Node>> findNodesFunc);
}

View File

@ -1,8 +1,8 @@
package io.metersphere.controller.wapper; package io.metersphere.controller.wapper;
import io.metersphere.dto.RelationshipGraphData; import io.metersphere.dto.RelationshipGraphData;
import io.metersphere.request.GraphBatchRequest;
import io.metersphere.service.wapper.TrackGraphService; import io.metersphere.service.wapper.TrackGraphService;
import io.metersphere.xpack.graph.request.GraphBatchRequest;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;

View File

@ -1,30 +1,27 @@
package io.metersphere.service.wapper; package io.metersphere.service.wapper;
import io.metersphere.base.mapper.ext.ExtTestCaseMapper; import io.metersphere.base.mapper.ext.ExtTestCaseMapper;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.dto.RelationshipGraphData; import io.metersphere.dto.RelationshipGraphData;
import io.metersphere.xpack.graph.GraphService; import io.metersphere.request.GraphBatchRequest;
import io.metersphere.xpack.graph.request.GraphBatchRequest; import io.metersphere.service.GraphService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
@Service @Service
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public class TrackGraphService { public class TrackGraphService {
@Resource @Resource
ExtTestCaseMapper extTestCaseMapper; ExtTestCaseMapper extTestCaseMapper;
@Resource
GraphService graphService;
public RelationshipGraphData getGraphData(String id, String type) { public RelationshipGraphData getGraphData(String id, String type) {
GraphService graphService = CommonBeanFactory.getBean(GraphService.class);
return graphService.getGraphData(id, extTestCaseMapper::getTestCaseForGraph); return graphService.getGraphData(id, extTestCaseMapper::getTestCaseForGraph);
} }
public RelationshipGraphData getGraphDataByCondition(GraphBatchRequest request, String type) { public RelationshipGraphData getGraphDataByCondition(GraphBatchRequest request, String type) {
request.getCondition().setNotEqStatus("Trash"); request.getCondition().setNotEqStatus("Trash");
GraphService graphService = CommonBeanFactory.getBean(GraphService.class);
return graphService.getGraphDataByCondition(request, extTestCaseMapper::selectIds, extTestCaseMapper::getTestCaseForGraph); return graphService.getGraphDataByCondition(request, extTestCaseMapper::selectIds, extTestCaseMapper::getTestCaseForGraph);
} }
} }

View File

@ -208,7 +208,7 @@
<test-case-preview ref="testCasePreview" :loading="rowCaseResult.loading"/> <test-case-preview ref="testCasePreview" :loading="rowCaseResult.loading"/>
<relationship-graph-drawer v-xpack :graph-data="graphData" ref="relationshipGraph"/> <relationship-graph-drawer :graph-data="graphData" ref="relationshipGraph"/>
<!-- 删除接口提示 --> <!-- 删除接口提示 -->
<list-item-delete-confirm ref="apiDeleteConfirm" @handleDelete="_handleDeleteVersion"/> <list-item-delete-confirm ref="apiDeleteConfirm" @handleDelete="_handleDeleteVersion"/>
@ -264,7 +264,7 @@ import {
} from "@/api/testCase"; } from "@/api/testCase";
import {getGraphByCondition} from "@/api/graph"; import {getGraphByCondition} from "@/api/graph";
import ListItemDeleteConfirm from "metersphere-frontend/src/components/ListItemDeleteConfirm"; import ListItemDeleteConfirm from "metersphere-frontend/src/components/ListItemDeleteConfirm";
import RelationshipGraphDrawer from "metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer"; import RelationshipGraphDrawer from "metersphere-frontend/src/components/graph/RelationshipGraphDrawer";
import MsSearch from "metersphere-frontend/src/components/search/MsSearch"; import MsSearch from "metersphere-frontend/src/components/search/MsSearch";
import {mapState} from "pinia"; import {mapState} from "pinia";
import {useStore} from "@/store" import {useStore} from "@/store"
@ -355,7 +355,6 @@ export default {
}, },
{ {
name: this.$t('test_track.case.generate_dependencies'), name: this.$t('test_track.case.generate_dependencies'),
isXPack: true,
handleClick: this.generateGraph, handleClick: this.generateGraph,
permissions: ['PROJECT_TRACK_CASE:READ+GENERATE_DEPENDENCIES'] permissions: ['PROJECT_TRACK_CASE:READ+GENERATE_DEPENDENCIES']
}, },

View File

@ -20,25 +20,25 @@
</div> </div>
<div class="left-icon"> <div class="left-icon">
<el-tooltip v-xpack class="item" effect="dark" :content="$t('commons.relationship.graph')" placement="left"> <el-tooltip class="item" effect="dark" :content="$t('commons.relationship.graph')" placement="left">
<font-awesome-icon class="graph-icon" :icon="['fas', 'sitemap']" size="lg" @click="openGraph"/> <font-awesome-icon class="graph-icon" :icon="['fas', 'sitemap']" size="lg" @click="openGraph"/>
</el-tooltip> </el-tooltip>
</div> </div>
<mx-relationship-graph-drawer v-xpack v-permission :graph-data="graphData" @closeRelationGraph="closeRelationGraph" ref="relationshipGraph"/> <relationship-graph-drawer v-permission :graph-data="graphData" @closeRelationGraph="closeRelationGraph" ref="relationshipGraph"/>
</div> </div>
</template> </template>
<script> <script>
import MxRelationshipGraphDrawer from "metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer"; import RelationshipGraphDrawer from "metersphere-frontend/src/components/graph/RelationshipGraphDrawer";
import RelationshipList from "./RelationshipList"; import RelationshipList from "./RelationshipList";
import {getRelationshipGraph} from "@/api/graph"; import {getRelationshipGraph} from "@/api/graph";
export default { export default {
name: "DependenciesList", name: "DependenciesList",
components: { components: {
MxRelationshipGraphDrawer, RelationshipGraphDrawer,
RelationshipList, RelationshipList,
}, },
props: [ props: [