Skip to content

Commit

Permalink
feat(Canvas): Add containers for branches
Browse files Browse the repository at this point in the history
Currently, all children are rendered as individual nodes in the Canvas,
making complicated to understand when a step belongs to a parent EIP or
if it comes as a next step in the Camel Route.

This commit adds support for rendering EIP branches as containers.

fix: KaotoIO#368
  • Loading branch information
lordrip committed Aug 8, 2024
1 parent 6b70658 commit 482b838
Show file tree
Hide file tree
Showing 9 changed files with 4,752 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ describe('CanvasService', () => {
it('should allow consumers to create a new controller and register its factories', () => {
const layoutFactorySpy = jest.spyOn(Visualization.prototype, 'registerLayoutFactory');
const componentFactorySpy = jest.spyOn(Visualization.prototype, 'registerComponentFactory');
const baselineElementFactorySpy = jest.spyOn(Visualization.prototype, 'registerElementFactory');

const controller = CanvasService.createController();

expect(controller).toBeInstanceOf(Visualization);
expect(layoutFactorySpy).toHaveBeenCalledWith(CanvasService.baselineLayoutFactory);
expect(componentFactorySpy).toHaveBeenCalledWith(CanvasService.baselineComponentFactory);
expect(baselineElementFactorySpy).toHaveBeenCalledWith(CanvasService.baselineElementFactory);
});

describe('baselineComponentFactory', () => {
Expand Down
145 changes: 60 additions & 85 deletions packages/ui/src/components/Visualization/Canvas/canvas.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ import {
ColaLayout,
ComponentFactory,
ConcentricLayout,
DagreLayout,
DagreGroupsLayout,
DefaultEdge,
EdgeStyle,
ForceLayout,
Graph,
GraphComponent,
GraphElement,
GridLayout,
Layout,
Model,
LEFT_TO_RIGHT,
ModelKind,
TOP_TO_BOTTOM,
Visualization,
withPanZoom,
} from '@patternfly/react-topology';
import { IVisualizationNode } from '../../../models/visualization/base-visual-entity';
import { CustomGroupWithSelection, CustomNodeWithSelection } from '../Custom';
import { CustomGroupWithSelection, CustomNodeWithSelection, NoBendpointsEdge } from '../Custom';
import { CanvasDefaults } from './canvas.defaults';
import { CanvasEdge, CanvasNode, CanvasNodesAndEdges, LayoutType } from './canvas.models';

Expand All @@ -32,37 +34,11 @@ export class CanvasService {

newController.registerLayoutFactory(this.baselineLayoutFactory);
newController.registerComponentFactory(this.baselineComponentFactory);

const defaultModel: Model = {
graph: {
id: 'default',
type: 'graph',
},
};

newController.fromModel(defaultModel, false);
newController.registerElementFactory(this.baselineElementFactory);

return newController;
}

static baselineComponentFactory(kind: ModelKind, type: string): ReturnType<ComponentFactory> {
switch (type) {
case 'group':
return CustomGroupWithSelection;
default:
switch (kind) {
case ModelKind.graph:
return withPanZoom()(GraphComponent);
case ModelKind.node:
return CustomNodeWithSelection;
case ModelKind.edge:
return DefaultEdge;
default:
return undefined;
}
}
}

// ### dagre algo options, uses default value on undefined ###
// nodeSep: undefined, // the separation between adjacent nodes in the same rank
// edgeSep: undefined, // the separation between adjacent edges in the same rank
Expand Down Expand Up @@ -103,16 +79,16 @@ export class CanvasService {
case LayoutType.Concentric:
return new ConcentricLayout(graph);
case LayoutType.DagreVertical:
return new DagreLayout(graph, {
rankdir: 'TB',
return new DagreGroupsLayout(graph, {
rankdir: TOP_TO_BOTTOM,
ranker: 'network-simplex',
nodesep: 20,
edgesep: 20,
ranksep: 0,
});
case LayoutType.DagreHorizontal:
return new DagreLayout(graph, {
rankdir: 'LR',
return new DagreGroupsLayout(graph, {
rankdir: LEFT_TO_RIGHT,
ranker: 'network-simplex',
nodesep: 20,
edgesep: 20,
Expand All @@ -129,24 +105,39 @@ export class CanvasService {
}
}

static baselineComponentFactory(kind: ModelKind, type: string): ReturnType<ComponentFactory> {
switch (type) {
case 'group':
return CustomGroupWithSelection;
default:
switch (kind) {
case ModelKind.graph:
return withPanZoom()(GraphComponent);
case ModelKind.node:
return CustomNodeWithSelection;
case ModelKind.edge:
return DefaultEdge;
default:
return undefined;
}
}
}

static baselineElementFactory(kind: ModelKind): GraphElement | undefined {
switch (kind) {
case ModelKind.edge:
return new NoBendpointsEdge();
default:
return undefined;
}
}

static getFlowDiagram(vizNode: IVisualizationNode): CanvasNodesAndEdges {
this.nodes = [];
this.edges = [];
this.visitedNodes = [];

const children = vizNode.getChildren();
if (vizNode.data.isGroup && children) {
children.forEach((child) => this.appendNodesAndEdges(child));
const containerId = vizNode.getBaseEntity()?.getId() ?? 'Unknown';
const group = this.getContainer(containerId, {
label: containerId,
children: this.visitedNodes,
data: { vizNode },
});
this.nodes.push(group);
} else {
this.appendNodesAndEdges(vizNode);
}
this.appendNodesAndEdges(vizNode);

return { nodes: this.nodes, edges: this.edges };
}
Expand All @@ -157,28 +148,31 @@ export class CanvasService {
return;
}

const node = this.getCanvasNode(vizNodeParam);

/** Add node */
this.nodes.push(node);
this.visitedNodes.push(node.id);

/** Add edges */
this.edges.push(...this.getEdgesFromVizNode(vizNodeParam));
let node: CanvasNode;

/** Traverse the children nodes */
const children = vizNodeParam.getChildren();
if (children !== undefined) {
if (vizNodeParam.data.isGroup && children) {
children.forEach((child) => {
this.appendNodesAndEdges(child);
});
}

/** Traverse the next node */
const nextNode = vizNodeParam.getNextNode();
if (nextNode !== undefined) {
this.appendNodesAndEdges(nextNode);
const containerId = vizNodeParam.id;
node = this.getContainer(containerId, {
label: containerId,
children: children.map((child) => child.id),
parentNode: vizNodeParam.getParentNode()?.id,
data: { vizNode: vizNodeParam },
});
} else {
node = this.getCanvasNode(vizNodeParam);
}

/** Add node */
this.nodes.push(node);
this.visitedNodes.push(node.id);

/** Add edges */
this.edges.push(...this.getEdgesFromVizNode(vizNodeParam));
}

private static getCanvasNode(vizNodeParam: IVisualizationNode): CanvasNode {
Expand All @@ -195,43 +189,24 @@ export class CanvasService {
private static getEdgesFromVizNode(vizNodeParam: IVisualizationNode): CanvasEdge[] {
const edges: CanvasEdge[] = [];

/** Connect to previous node if it doesn't have children */
if (vizNodeParam.getPreviousNode() !== undefined && vizNodeParam.getPreviousNode()?.getChildren() === undefined) {
edges.push(this.getEdge(vizNodeParam.getPreviousNode()!.id, vizNodeParam.id));
}

/** Connect to the parent if it's not a group and there is no previous node */
if (
vizNodeParam.getParentNode() !== undefined &&
!vizNodeParam.getParentNode()?.data.isGroup &&
vizNodeParam.getPreviousNode() === undefined
) {
edges.push(this.getEdge(vizNodeParam.getParentNode()!.id, vizNodeParam.id));
}

/** Connect to each leaf of the previous node */
if (vizNodeParam.getPreviousNode() !== undefined && vizNodeParam.getPreviousNode()?.getChildren() !== undefined) {
const leafNodesIds: string[] = [];
vizNodeParam.getPreviousNode()!.populateLeafNodesIds(leafNodesIds);

leafNodesIds.forEach((leafNodeId) => {
edges.push(this.getEdge(leafNodeId, vizNodeParam.id));
});
if (vizNodeParam.getNextNode() !== undefined) {
edges.push(this.getEdge(vizNodeParam.id, vizNodeParam.getNextNode()!.id));
}

return edges;
}

private static getContainer(
id: string,
options: { label?: string; children?: string[]; data?: CanvasNode['data'] } = {},
options: { label?: string; children?: string[]; parentNode?: string; data?: CanvasNode['data'] } = {},
): CanvasNode {
return {
id,
type: 'group',
group: true,
label: options.label ?? id,
children: options.children ?? [],
parentNode: options.parentNode,
data: options.data,
style: {
padding: CanvasDefaults.DEFAULT_NODE_DIAMETER * 0.8,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BaseEdge, Point } from '@patternfly/react-topology';

export class NoBendpointsEdge extends BaseEdge {
getBendpoints(): Point[] {
return [];
}
}
1 change: 1 addition & 0 deletions packages/ui/src/components/Visualization/Custom/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Group/CustomGroup';
export * from './NoBendingEdge';
export * from './Node/CustomNode';
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,22 @@ export abstract class AbstractCamelVisualEntity<T extends object> implements Bas
}
routeGroupNode.addChild(fromNode);

fromNode.getChildren()?.forEach((child, index) => {
routeGroupNode.addChild(child);
if (index === 0) {
fromNode.setNextNode(child);
child.setPreviousNode(fromNode);
}

const previousChild = fromNode.getChildren()?.[index - 1];
if (previousChild) {
previousChild.setNextNode(child);
child.setPreviousNode(previousChild);
}
});
fromNode.getChildren()?.splice(0);
fromNode.data.isGroup = false;

return routeGroupNode;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ export class CamelStepsService {
componentLookup.processorName as keyof ProcessorDefinition,
);

if (childrenStepsProperties.length > 0) {
vizNode.data.isGroup = true;
}

childrenStepsProperties.forEach((stepsProperty) => {
const childrenVizNodes = this.getVizNodesFromChildren(path, stepsProperty, entityDefinition);
childrenVizNodes.forEach((childVizNode) => vizNode.addChild(childVizNode));
childrenVizNodes.forEach((childVizNode) => {
vizNode.addChild(childVizNode);
});
});

return vizNode;
Expand Down
34 changes: 34 additions & 0 deletions packages/ui/src/stubs/camel-route-branch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { parse } from 'yaml';

export const camelRouteBranch = parse(`
- route:
id: route-2768
from:
id: from-4014
uri: timer:template
parameters:
period: "1000"
steps:
- choice:
id: choice-3431
otherwise:
id: otherwise-3653
steps:
- log:
id: log-6808
message: \${body}
when:
- id: when-4112
steps:
- setHeader:
id: setHeader-6078
expression:
simple: {}
expression:
simple:
expression: \${header.foo} == 1
- to:
id: to-3757
uri: sql
parameters: {}
`);
Loading

0 comments on commit 482b838

Please sign in to comment.