feat(通用功能): 开源生成关系依赖图功能
--task=1010906 --user=陈建星 【开源计划】依赖关系拓扑图 https://www.tapd.cn/55049933/s/1331769
This commit is contained in:
parent
a8725ca9e5
commit
e5733db8c1
|
@ -1,7 +1,7 @@
|
|||
package io.metersphere.controller;
|
||||
|
||||
import io.metersphere.request.GraphBatchRequest;
|
||||
import io.metersphere.service.ApiGraphService;
|
||||
import io.metersphere.xpack.graph.request.GraphBatchRequest;
|
||||
import io.metersphere.dto.RelationshipGraphData;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
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.ExtApiScenarioMapper;
|
||||
import io.metersphere.dto.RelationshipGraphData;
|
||||
import io.metersphere.xpack.graph.GraphService;
|
||||
import io.metersphere.request.GraphBatchRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
@ -19,9 +17,10 @@ public class ApiGraphService {
|
|||
ExtApiDefinitionMapper extApiDefinitionMapper;
|
||||
@Resource
|
||||
private ExtApiScenarioMapper extApiScenarioMapper;
|
||||
@Resource
|
||||
private GraphService graphService;
|
||||
|
||||
public RelationshipGraphData getGraphData(String id, String type) {
|
||||
GraphService graphService = CommonBeanFactory.getBean(GraphService.class);
|
||||
if (StringUtils.equals(type, "API")) {
|
||||
return graphService.getGraphData(id, extApiDefinitionMapper::getForGraph);
|
||||
}
|
||||
|
@ -29,7 +28,6 @@ public class ApiGraphService {
|
|||
}
|
||||
|
||||
public RelationshipGraphData getGraphDataByCondition(GraphBatchRequest request, String type) {
|
||||
GraphService graphService = CommonBeanFactory.getBean(GraphService.class);
|
||||
request.getCondition().setNotEqStatus("Trash");
|
||||
if (StringUtils.equals(type, "API_SCENARIO")) {
|
||||
return graphService.getGraphDataByCondition(request, extApiScenarioMapper::selectIdsByQuery, extApiScenarioMapper::getTestCaseForGraph);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,7 +57,6 @@ import io.metersphere.service.definition.TcpApiParamService;
|
|||
import io.metersphere.service.ext.ExtApiScheduleService;
|
||||
import io.metersphere.service.ext.ExtFileAssociationService;
|
||||
import io.metersphere.service.plan.TestPlanScenarioCaseService;
|
||||
import io.metersphere.xpack.api.service.ApiAutomationRelationshipEdgeService;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.collections4.MapUtils;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
|
@ -159,6 +158,8 @@ public class ApiScenarioService {
|
|||
private ExtTestPlanApiScenarioMapper extTestPlanApiScenarioMapper;
|
||||
@Resource
|
||||
private BaseQuotaService baseQuotaService;
|
||||
@Resource
|
||||
private ApiAutomationRelationshipEdgeService apiAutomationRelationshipEdgeService;
|
||||
|
||||
private ThreadLocal<Long> currentScenarioOrder = new ThreadLocal<>();
|
||||
|
||||
|
@ -290,10 +291,8 @@ public class ApiScenarioService {
|
|||
apiScenarioMapper.insert(scenario);
|
||||
apiScenarioReferenceIdService.saveApiAndScenarioRelation(scenario);
|
||||
// 存储依赖关系
|
||||
ApiAutomationRelationshipEdgeService relationshipEdgeService = CommonBeanFactory.getBean(ApiAutomationRelationshipEdgeService.class);
|
||||
if (relationshipEdgeService != null) {
|
||||
relationshipEdgeService.initRelationshipEdge(null, scenario);
|
||||
}
|
||||
apiAutomationRelationshipEdgeService.initRelationshipEdge(null, scenario);
|
||||
|
||||
uploadFiles(request, bodyFiles, scenarioFiles);
|
||||
return scenario;
|
||||
}
|
||||
|
@ -396,10 +395,8 @@ public class ApiScenarioService {
|
|||
uploadFiles(request, bodyFiles, scenarioFiles);
|
||||
|
||||
// 存储依赖关系
|
||||
ApiAutomationRelationshipEdgeService relationshipEdgeService = CommonBeanFactory.getBean(ApiAutomationRelationshipEdgeService.class);
|
||||
if (relationshipEdgeService != null) {
|
||||
relationshipEdgeService.initRelationshipEdge(beforeScenario, scenario);
|
||||
}
|
||||
apiAutomationRelationshipEdgeService.initRelationshipEdge(beforeScenario, scenario);
|
||||
|
||||
String defaultVersion = baseProjectVersionMapper.getDefaultVersion(request.getProjectId());
|
||||
if (StringUtils.equalsIgnoreCase(request.getVersionId(), defaultVersion)) {
|
||||
checkAndSetLatestVersion(beforeScenario.getRefId());
|
||||
|
@ -1332,10 +1329,8 @@ public class ApiScenarioService {
|
|||
sendImportScenarioCreateNotice(scenarioWithBLOBs);
|
||||
batchMapper.insert(scenarioWithBLOBs);
|
||||
// 存储依赖关系
|
||||
ApiAutomationRelationshipEdgeService relationshipEdgeService = CommonBeanFactory.getBean(ApiAutomationRelationshipEdgeService.class);
|
||||
if (relationshipEdgeService != null) {
|
||||
relationshipEdgeService.initRelationshipEdge(null, scenarioWithBLOBs);
|
||||
}
|
||||
apiAutomationRelationshipEdgeService.initRelationshipEdge(null, scenarioWithBLOBs);
|
||||
|
||||
apiScenarioReferenceIdService.saveApiAndScenarioRelation(scenarioWithBLOBs);
|
||||
extApiScenarioMapper.clearLatestVersion(scenarioWithBLOBs.getRefId());
|
||||
extApiScenarioMapper.addLatestVersion(scenarioWithBLOBs.getRefId());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -360,7 +360,7 @@
|
|||
@errorRefresh="errorRefresh"
|
||||
ref="runTest"/>
|
||||
<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"/>
|
||||
<!-- 删除场景弹窗 -->
|
||||
|
@ -472,7 +472,7 @@ export default {
|
|||
MsTableOperatorButton: () => import('metersphere-frontend/src/components/MsTableOperatorButton'),
|
||||
MsTaskCenter: () => import('metersphere-frontend/src/components/task/TaskCenter'),
|
||||
MsRun: () => import('./DebugRun'),
|
||||
MxRelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer'),
|
||||
RelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/RelationshipGraphDrawer'),
|
||||
},
|
||||
props: {
|
||||
referenced: {
|
||||
|
@ -637,7 +637,6 @@ export default {
|
|||
{
|
||||
name: this.$t('test_track.case.generate_dependencies'),
|
||||
handleClick: this.generateGraph,
|
||||
isXPack: true,
|
||||
permissions: ['PROJECT_API_SCENARIO:READ+EDIT'],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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" />
|
||||
</el-tooltip>
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
|||
:resource-type="resourceType"
|
||||
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>
|
||||
</template>
|
||||
|
||||
|
@ -37,7 +37,7 @@ export default {
|
|||
name: 'DependenciesList',
|
||||
components: {
|
||||
RelationshipList,
|
||||
MxRelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer'),
|
||||
RelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/RelationshipGraphDrawer'),
|
||||
},
|
||||
props: ['resourceId', 'resourceType', 'readOnly', 'versionEnable'],
|
||||
data() {
|
||||
|
|
|
@ -238,7 +238,7 @@
|
|||
<ms-show-reference ref="viewRef" :show-plan="false" :is-has-ref="false" api-type="API"/>
|
||||
<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"/>
|
||||
</span>
|
||||
|
@ -333,7 +333,7 @@ export default {
|
|||
TableExtendBtns,
|
||||
MsShowReference,
|
||||
MsApiReportStatus: () => import('../../../automation/report/ApiReportStatus'),
|
||||
MxRelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer'),
|
||||
RelationshipGraphDrawer: () => import('metersphere-frontend/src/components/graph/RelationshipGraphDrawer'),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -381,7 +381,6 @@ export default {
|
|||
},
|
||||
{
|
||||
name: this.$t('test_track.case.generate_dependencies'),
|
||||
isXPack: true,
|
||||
handleClick: this.generateGraph,
|
||||
permissions: ['PROJECT_API_DEFINITION:READ+EDIT_API'],
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@ import DrawerHeader from "../head/DrawerHeader";
|
|||
import MsChart from "../chart/MsChart";
|
||||
|
||||
export default {
|
||||
name: "MxRelationshipGraph",
|
||||
name: "RelationshipGraph",
|
||||
components: {MsChart, DrawerHeader, MsDrawer},
|
||||
props: {
|
||||
data: Array, links: Array,
|
|
@ -16,10 +16,10 @@
|
|||
<script>
|
||||
import MsDrawer from "../MsDrawer";
|
||||
import DrawerHeader from "../head/DrawerHeader";
|
||||
import RelationshipGraph from "./MxRelationshipGraph";
|
||||
import RelationshipGraph from "./RelationshipGraph";
|
||||
|
||||
export default {
|
||||
name: "MxRelationshipGraphDrawer",
|
||||
name: "RelationshipGraphDrawer",
|
||||
components: {RelationshipGraph, DrawerHeader, MsDrawer},
|
||||
props: ['graphData'],
|
||||
data() {
|
|
@ -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.Setter;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package io.metersphere.controller.wapper;
|
||||
|
||||
import io.metersphere.dto.RelationshipGraphData;
|
||||
import io.metersphere.request.GraphBatchRequest;
|
||||
import io.metersphere.service.wapper.TrackGraphService;
|
||||
import io.metersphere.xpack.graph.request.GraphBatchRequest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
package io.metersphere.service.wapper;
|
||||
|
||||
|
||||
import io.metersphere.base.mapper.ext.ExtTestCaseMapper;
|
||||
import io.metersphere.commons.utils.CommonBeanFactory;
|
||||
import io.metersphere.dto.RelationshipGraphData;
|
||||
import io.metersphere.xpack.graph.GraphService;
|
||||
import io.metersphere.xpack.graph.request.GraphBatchRequest;
|
||||
import io.metersphere.request.GraphBatchRequest;
|
||||
import io.metersphere.service.GraphService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
@Service
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public class TrackGraphService {
|
||||
@Resource
|
||||
ExtTestCaseMapper extTestCaseMapper;
|
||||
@Resource
|
||||
GraphService graphService;
|
||||
|
||||
public RelationshipGraphData getGraphData(String id, String type) {
|
||||
GraphService graphService = CommonBeanFactory.getBean(GraphService.class);
|
||||
return graphService.getGraphData(id, extTestCaseMapper::getTestCaseForGraph);
|
||||
}
|
||||
|
||||
public RelationshipGraphData getGraphDataByCondition(GraphBatchRequest request, String type) {
|
||||
request.getCondition().setNotEqStatus("Trash");
|
||||
GraphService graphService = CommonBeanFactory.getBean(GraphService.class);
|
||||
return graphService.getGraphDataByCondition(request, extTestCaseMapper::selectIds, extTestCaseMapper::getTestCaseForGraph);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,7 +208,7 @@
|
|||
|
||||
<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"/>
|
||||
|
@ -264,7 +264,7 @@ import {
|
|||
} from "@/api/testCase";
|
||||
import {getGraphByCondition} from "@/api/graph";
|
||||
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 {mapState} from "pinia";
|
||||
import {useStore} from "@/store"
|
||||
|
@ -355,7 +355,6 @@ export default {
|
|||
},
|
||||
{
|
||||
name: this.$t('test_track.case.generate_dependencies'),
|
||||
isXPack: true,
|
||||
handleClick: this.generateGraph,
|
||||
permissions: ['PROJECT_TRACK_CASE:READ+GENERATE_DEPENDENCIES']
|
||||
},
|
||||
|
|
|
@ -20,25 +20,25 @@
|
|||
</div>
|
||||
|
||||
<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"/>
|
||||
</el-tooltip>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MxRelationshipGraphDrawer from "metersphere-frontend/src/components/graph/MxRelationshipGraphDrawer";
|
||||
import RelationshipGraphDrawer from "metersphere-frontend/src/components/graph/RelationshipGraphDrawer";
|
||||
import RelationshipList from "./RelationshipList";
|
||||
import {getRelationshipGraph} from "@/api/graph";
|
||||
|
||||
export default {
|
||||
name: "DependenciesList",
|
||||
components: {
|
||||
MxRelationshipGraphDrawer,
|
||||
RelationshipGraphDrawer,
|
||||
RelationshipList,
|
||||
},
|
||||
props: [
|
||||
|
|
Loading…
Reference in New Issue