diff --git a/packages/ui/package.json b/packages/ui/package.json index e51a848ce..eab6d688d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -47,6 +47,7 @@ "lint:style:fix": "yarn lint:style --fix" }, "dependencies": { + "@dagrejs/dagre": "1.1.2", "@kaoto-next/uniforms-patternfly": "^0.6.15", "@kie-tools-core/editor": "0.32.0", "@kie-tools-core/notifications": "0.32.0", diff --git a/packages/ui/src/components/Visualization/Canvas/canvas.models.ts b/packages/ui/src/components/Visualization/Canvas/canvas.models.ts index d138407b9..7db8256d7 100644 --- a/packages/ui/src/components/Visualization/Canvas/canvas.models.ts +++ b/packages/ui/src/components/Visualization/Canvas/canvas.models.ts @@ -20,6 +20,7 @@ export const enum LayoutType { export interface CanvasNode extends NodeModel { parentNode?: string; data?: { + index?: number; vizNode?: IVisualizationNode; }; } diff --git a/packages/ui/src/components/Visualization/Canvas/canvas.service.ts b/packages/ui/src/components/Visualization/Canvas/canvas.service.ts index a6e172a4c..7c627415b 100644 --- a/packages/ui/src/components/Visualization/Canvas/canvas.service.ts +++ b/packages/ui/src/components/Visualization/Canvas/canvas.service.ts @@ -4,7 +4,6 @@ import { ColaLayout, ComponentFactory, ConcentricLayout, - DagreGroupsLayout, DefaultEdge, EdgeStyle, ForceLayout, @@ -21,6 +20,7 @@ import { } from '@patternfly/react-topology'; import { IVisualizationNode } from '../../../models/visualization/base-visual-entity'; import { CustomGroupWithSelection, CustomNodeWithSelection, NoBendpointsEdge } from '../Custom'; +import { DagreGroupsExtendedLayout } from '../Custom/Layout/DagreGroupsExtendedLayout'; import { CanvasDefaults } from './canvas.defaults'; import { CanvasEdge, CanvasNode, CanvasNodesAndEdges, LayoutType } from './canvas.models'; @@ -79,7 +79,7 @@ export class CanvasService { case LayoutType.Concentric: return new ConcentricLayout(graph); case LayoutType.DagreVertical: - return new DagreGroupsLayout(graph, { + return new DagreGroupsExtendedLayout(graph, { rankdir: TOP_TO_BOTTOM, ranker: 'network-simplex', nodesep: 20, @@ -87,7 +87,7 @@ export class CanvasService { ranksep: 0, }); case LayoutType.DagreHorizontal: - return new DagreGroupsLayout(graph, { + return new DagreGroupsExtendedLayout(graph, { rankdir: LEFT_TO_RIGHT, ranker: 'network-simplex', nodesep: 20, @@ -138,6 +138,15 @@ export class CanvasService { this.visitedNodes = []; this.appendNodesAndEdges(vizNode); + /** + * Add an index to each node so they can be sorted in DagreGroupsExtendedLayout + * Related issue: https://github.com/patternfly/react-topology/issues/230 + */ + this.nodes.forEach((node, index) => { + if (node.data) { + node.data.index = index; + } + }); return { nodes: this.nodes, edges: this.edges }; } diff --git a/packages/ui/src/components/Visualization/Custom/Layout/DagreGroupsExtendedLayout.ts b/packages/ui/src/components/Visualization/Custom/Layout/DagreGroupsExtendedLayout.ts new file mode 100644 index 000000000..0fed5640d --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/Layout/DagreGroupsExtendedLayout.ts @@ -0,0 +1,108 @@ +import * as dagre from '@dagrejs/dagre'; +import { + DagreGroupsLayout, + getGroupChildrenDimensions, + Graph, + GRAPH_LAYOUT_END_EVENT, + LAYOUT_DEFAULTS, + LayoutGroup, + Point, +} from '@patternfly/react-topology'; +import { DagreLink } from './DagreLink'; +import { DagreNode } from './DagreNode'; + +/** + * This class extends the DagreGroupsLayout class to provide a consistent + * layout for groups and nodes in a graph. It uses an index provided by the + * canvas.service.ts to determine the order of the nodes and groups in the + * graph. + * + * Related issue: https://github.com/patternfly/react-topology/issues/230 + */ +export class DagreGroupsExtendedLayout extends DagreGroupsLayout { + protected startLayout(graph: Graph, initialRun: boolean, addingNodes: boolean): void { + if (initialRun || addingNodes) { + const doLayout = (parentGroup?: LayoutGroup) => { + const dagreGraph = new dagre.graphlib.Graph({ compound: true }); + const options = { ...this.dagreOptions }; + + Object.keys(LAYOUT_DEFAULTS).forEach((key) => delete options[key as keyof typeof options]); + dagreGraph.setGraph(options); + + // Determine the groups, nodes, and edges that belong in this layout + const layerGroups = this.groups.filter( + (group) => group.parent?.id === parentGroup?.id || (!parentGroup && group.parent?.id === graph.getId()), + ); + const layerNodes = this.nodes.filter( + (n) => + n.element.getParent()?.getId() === parentGroup?.id || + (!parentGroup && n.element.getParent()?.getId() === graph.getId()), + ); + const layerEdges = this.edges.filter( + (edge) => + (layerGroups.find((n) => n.id === edge.sourceNode.id) || + layerNodes.find((n) => n.id === edge.sourceNode.id)) && + (layerGroups.find((n) => n.id === edge.targetNode.id) || + layerNodes.find((n) => n.id === edge.targetNode.id)), + ); + + const nodesOrder: { id: string; index: number; node: ReturnType }[] = []; + + // Layout any child groups first + layerGroups.forEach((group) => { + doLayout(group); + + // Add the child group node (now with the correct dimensions) to the graph + const dagreNode = new DagreNode(group.element, group.padding); + const updateNode = dagreNode.getUpdatableNode(); + nodesOrder.push({ id: group.id, index: group.element.getData().index, node: updateNode }); + }); + + layerNodes?.forEach((node) => { + const updateNode = (node as DagreNode).getUpdatableNode(); + nodesOrder.push({ id: node.id, index: node.element.getData().index, node: updateNode }); + }); + + // Sort the nodes by their index + nodesOrder.sort((a, b) => a.index - b.index); + + // Set the nodes in the order they were sorted + nodesOrder.forEach((node) => { + dagreGraph.setNode(node.id, node.node); + }); + + layerEdges?.forEach((dagreEdge) => { + dagreGraph.setEdge(dagreEdge.source.id, dagreEdge.target.id, dagreEdge); + }); + + dagre.layout(dagreGraph); + + // Update the node element positions + layerNodes.forEach((node) => { + (node as DagreNode).updateToNode(dagreGraph.node(node.id)); + }); + + // Update the group element positions (setting the group's positions updates its children) + layerGroups.forEach((node) => { + const dagreNode = dagreGraph.node(node.id); + node.element.setPosition(new Point(dagreNode.x, dagreNode.y)); + }); + + this.updateEdgeBendpoints(this.edges as DagreLink[]); + + // now that we've laid out the children, set the dimensions on the group (not on the graph) + if (parentGroup) { + parentGroup.element.setDimensions(getGroupChildrenDimensions(parentGroup.element)); + } + }; + + doLayout(); + } + + if (this.dagreOptions.layoutOnDrag) { + this.forceSimulation.useForceSimulation(this.nodes, this.edges, this.getFixedNodeDistance); + } else { + this.graph.getController().fireEvent(GRAPH_LAYOUT_END_EVENT, { graph: this.graph }); + } + } +} diff --git a/packages/ui/src/components/Visualization/Custom/Layout/DagreLink.ts b/packages/ui/src/components/Visualization/Custom/Layout/DagreLink.ts new file mode 100644 index 000000000..4fc43b0e2 --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/Layout/DagreLink.ts @@ -0,0 +1,17 @@ +import { LayoutLink, Point } from '@patternfly/react-topology'; + +/** + * This class extends the LayoutLink class since DagreLink is not exported from + * the react-topology library. + * + * Related issue: https://github.com/patternfly/react-topology/issues/230 + */ +export class DagreLink extends LayoutLink { + public points?: { x: number; y: number }[]; + + updateBendpoints(): void { + if (this.points && !this.isFalse && this.points.length > 2) { + this.element.setBendpoints(this.points.slice(1, -1).map((point) => new Point(point.x, point.y))); + } + } +} diff --git a/packages/ui/src/components/Visualization/Custom/Layout/DagreNode.ts b/packages/ui/src/components/Visualization/Custom/Layout/DagreNode.ts new file mode 100644 index 000000000..f5b2d65d8 --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/Layout/DagreNode.ts @@ -0,0 +1,27 @@ +import * as dagre from '@dagrejs/dagre'; +import { LayoutNode } from '@patternfly/react-topology'; + +/** + * This class extends the LayoutNode class since DagreNode is not exported from + * the react-topology library. + * + * Related issue: https://github.com/patternfly/react-topology/issues/230 + */ +export class DagreNode extends LayoutNode implements dagre.Node { + getUpdatableNode(): dagre.Node { + return { + width: this.width, + height: this.height, + x: this.x, + y: this.y, + }; + } + + updateToNode(updatedNode: dagre.Node | undefined): void { + if (updatedNode) { + this.x = updatedNode.x; + this.y = updatedNode.y; + this.update(); + } + } +} diff --git a/yarn.lock b/yarn.lock index 3c002cb71..b05729e33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2631,6 +2631,7 @@ __metadata: "@babel/preset-env": ^7.21.5 "@babel/preset-react": ^7.18.6 "@babel/preset-typescript": ^7.21.5 + "@dagrejs/dagre": 1.1.2 "@kaoto-next/uniforms-patternfly": ^0.6.15 "@kaoto/camel-catalog": "workspace:*" "@kie-tools-core/editor": 0.32.0