Skip to content

Commit

Permalink
feat(viz): First iteration of the Visualization component
Browse files Browse the repository at this point in the history
This is the first iteration of the Visualization component,
there are still some rough edges but it will serve as an
intermediate step to move forward.

relates to: KaotoIO#34
  • Loading branch information
lordrip committed Aug 31, 2023
1 parent 29db92c commit 3b629d3
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 509 deletions.
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"@patternfly/react-core": "^5.0.0",
"@patternfly/react-icons": "^5.0.0",
"@patternfly/react-table": "^5.0.0",
"@patternfly/react-topology": "^5.0.0",
"@types/uuid": "^9.0.2",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
Expand All @@ -37,6 +36,7 @@
"uniforms": "^4.0.0-alpha.0",
"uniforms-bridge-json-schema": "^4.0.0-alpha.0",
"uuid": "^9.0.0",
"yaml": "^2.3.1",
"zustand": "^4.3.9"
},
"devDependencies": {
Expand Down
45 changes: 45 additions & 0 deletions packages/ui/src/components/Visualization/Canvas/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { FunctionComponent, PropsWithChildren, useEffect, useState } from 'react';
import ReactFlow, { Background, Controls, Edge, Node, useReactFlow } from 'reactflow';
import 'reactflow/dist/style.css';
import { CamelRoute } from '../../../models/camel-entities';
import { CanvasService } from './canvas.service';

interface CanvasProps {
contextToolbar?: React.ReactNode;
entities: CamelRoute[];
}

export const Canvas: FunctionComponent<PropsWithChildren<CanvasProps>> = (props) => {
const { fitView } = useReactFlow();
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);

/** Calculate graph */
useEffect(() => {
if (!Array.isArray(props.entities)) return;

const localNodes: Node[] = [];
const localEdges: Edge[] = [];

props.entities.forEach((entity) => {
const { nodes: childNodes, edges: childEdges } = CanvasService.getFlowChart(entity.toVizNode());
localNodes.push(...childNodes);
localEdges.push(...childEdges);
});

setNodes(localNodes);
setEdges(localEdges);

/** Find a better mechanism to update the canvas */
setTimeout(() => {
fitView();
}, 100);
}, []);

return (
<ReactFlow nodes={nodes} edges={edges}>
<Background />
<Controls />
</ReactFlow>
);
};
86 changes: 86 additions & 0 deletions packages/ui/src/components/Visualization/Canvas/canvas.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Dagre from '@dagrejs/dagre';
import { Edge, Node } from 'reactflow';
import { VisualizationNode } from '../../../models/visualization';

type NodesAndEdges = { nodes: Node[]; edges: Edge[] };

export class CanvasService {
static nodes: Node[] = [];
static edges: Edge[] = [];
static visitedNodes: string[] = [];

static getFlowChart(vizNode: VisualizationNode): NodesAndEdges {
this.nodes = [];
this.edges = [];
this.visitedNodes = [];
this.getNodesAndEdges(vizNode);

console.log('this.nodes', this.nodes, vizNode);
console.log('this.edges', this.edges);
const positionedFlowChart = this.getLayoutedElements(this.nodes, this.edges);
return { nodes: positionedFlowChart.nodes, edges: positionedFlowChart.edges };
}

/** Method for iterating over all the VisualizationNode and its children using a depth-first algorithm */
static getNodesAndEdges(vizNodeParam: VisualizationNode) {
if (this.visitedNodes.includes(vizNodeParam.id)) {
return;
}

console.log('vizNodeParam', vizNodeParam.id, vizNodeParam.label);
const node = vizNodeParam.toNode();

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

/** Add edges */
this.edges.push(...vizNodeParam.getEdges());

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

/** Traverse the next node */
const nextNode = vizNodeParam.getNextNode();
if (nextNode !== undefined) {
this.getNodesAndEdges(nextNode);
}
}

private static getLayoutedElements(nodes: Node[], edges: Edge[]): NodesAndEdges {
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
graph.setGraph({
rankdir: 'TB',
});

edges.forEach((edge) => graph.setEdge(edge.source, edge.target));
nodes.forEach((node) => {
graph.setNode(node.id, { width: node.style!.width, height: node.style!.height });
});

Dagre.layout(graph);

return {
nodes: nodes.map((node) => {
let { x, y } = graph.node(node.id);

/** Position child node relatively to its parent */
if (node.parentNode) {
const parentNode = graph.node(node.parentNode);
if (parentNode) {
x = x - parentNode.x;
y = y - parentNode.y;
}
}

return { ...node, position: { x, y } };
}),
edges,
};
}
}
1 change: 1 addition & 0 deletions packages/ui/src/components/Visualization/Canvas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Canvas';
16 changes: 14 additions & 2 deletions packages/ui/src/components/Visualization/Visualization.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { FunctionComponent, PropsWithChildren } from 'react';
import { FunctionComponent, PropsWithChildren, useState } from 'react';
import { ReactFlowProvider } from 'reactflow';
import { CamelRoute } from '../../models/camel-entities';
import { camelRoute } from '../../stubs/camel-route';
import { Canvas } from './Canvas';
import './Visualization.scss';

interface CanvasProps {
className?: string;
}

export const Visualization: FunctionComponent<PropsWithChildren<CanvasProps>> = (props) => {
return <div className={`canvasSurface ${props.className ?? ''}`}>Visualization</div>;
const [entities] = useState<CamelRoute[]>([camelRoute]);

return (
<div className={`canvasSurface ${props.className ?? ''}`}>
<ReactFlowProvider>
<Canvas entities={entities} />
</ReactFlowProvider>
</div>
);
};
Loading

0 comments on commit 3b629d3

Please sign in to comment.