Skip to content

Commit

Permalink
feate(Canvas): Extend DagreGroupsLayout
Browse files Browse the repository at this point in the history
Currently, `DagreLayout` is used to render the graph, and while it does
work, it doesn't support nested groups properly due to a limitation in
Dagre itself.

In the `@patternfly/react-topology` there's another layout called `DagreGroupsLayout` that performs individual layout inside each group, providing a better result when the graph contains nested groups.

This commit brings that class over and uses the initial node index for
sorting the graph node, this way the order is kept when expanding/collapsing nodes in the Canvas.

Related issue: patternfly/react-topology#230
  • Loading branch information
lordrip committed Aug 8, 2024
1 parent 482b838 commit f0a3f05
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const enum LayoutType {
export interface CanvasNode extends NodeModel {
parentNode?: string;
data?: {
index?: number;
vizNode?: IVisualizationNode;
};
}
Expand Down
15 changes: 12 additions & 3 deletions packages/ui/src/components/Visualization/Canvas/canvas.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ColaLayout,
ComponentFactory,
ConcentricLayout,
DagreGroupsLayout,
DefaultEdge,
EdgeStyle,
ForceLayout,
Expand All @@ -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';

Expand Down Expand Up @@ -79,15 +79,15 @@ 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,
edgesep: 20,
ranksep: 0,
});
case LayoutType.DagreHorizontal:
return new DagreGroupsLayout(graph, {
return new DagreGroupsExtendedLayout(graph, {
rankdir: LEFT_TO_RIGHT,
ranker: 'network-simplex',
nodesep: 20,
Expand Down Expand Up @@ -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 };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DagreNode['getUpdatableNode']> }[] = [];

// 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 });
}
}
}
Original file line number Diff line number Diff line change
@@ -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)));
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit f0a3f05

Please sign in to comment.