engine-bay/admin-portal

View on GitHub
EngineBay.AdminPortal/AdminPortal/src/pages/studio/BlueprintVisualizer.tsx

Summary

Maintainability
F
3 days
Test Coverage
import { useEffect, useState } from "react";
import ReactFlow, {
  Background,
  Controls,
  Node,
  Edge,
  MiniMap,
  ReactFlowProvider,
  ConnectionLineType,
  useReactFlow,
  useNodesState,
  useEdgesState,
  Position,
  getOutgoers,
  getIncomers,
} from "reactflow";
import dagre from "dagre";
import "reactflow/dist/style.css";
import { Blueprint, ElementType } from "../../lib";
import { SelectedElement } from "./SelectedElement";
import { v4 as uuidv4 } from "uuid";

const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));

const nodeWidth = 120;
const nodeHeight = 30;

const getLayoutedElements = (
  nodes: Node[],
  edges: Edge[],
  direction = "TB"
) => {
  const isHorizontal = direction === "LR";
  dagreGraph.setGraph({ rankdir: direction });

  nodes.forEach((node) => {
    dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
  });

  edges.forEach((edge) => {
    dagreGraph.setEdge(edge.source, edge.target);
  });

  dagre.layout(dagreGraph);

  nodes.forEach((node) => {
    const nodeWithPosition = dagreGraph.node(node.id);
    node.targetPosition = isHorizontal ? Position.Left : Position.Top;
    node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;

    // We are shifting the dagre node position (anchor=center center) to the top left
    // so it matches the React Flow node anchor point (top left).
    node.position = {
      x: nodeWithPosition.x - nodeWidth / 2,
      y: nodeWithPosition.y - nodeHeight / 2,
    };

    return node;
  });

  return { nodes, edges };
};

export type BlueprintVisualizerProps = {
  blueprint?: Blueprint;
  selectedElement?: SelectedElement;
  onElementSelected: (selectedElement: SelectedElement) => void;
};

const BlueprintVisualizerCanvas = ({
  blueprint,
  selectedElement,
}: BlueprintVisualizerProps): JSX.Element => {
  const { fitView } = useReactFlow();

  let initialNodes: Node[] = [];
  let initialEdges: Edge[] = [];

  const [nodes, setNodes] = useNodesState(initialNodes);
  const [edges, setEdges] = useEdgesState(initialEdges);
  const [selectedNode, setSelectedNode] = useState<Node>();

  useEffect(() => {
    initialNodes = [];
    initialEdges = [];

    if (blueprint) {
      for (const expressionBlueprint of blueprint.expressionBlueprints) {
        initialNodes.push({
          id: expressionBlueprint.id || uuidv4(),
          data: {
            label:
              expressionBlueprint.objective || expressionBlueprint.expression,
            type: ElementType.Blueprint,
          },
          position: { x: 0, y: 0 },
        });

        // map the inputs
        for (const inputDataVariableBlueprint of expressionBlueprint.inputDataVariableBlueprints) {
          const sourceDataVariableBlueprint =
            blueprint.dataVariableBlueprints.find(
              (x) =>
                x.name == inputDataVariableBlueprint.name &&
                x.namespace == inputDataVariableBlueprint.namespace
            );
          if (sourceDataVariableBlueprint) {
            initialEdges.push({
              id: `${sourceDataVariableBlueprint.id}-${expressionBlueprint.id}`,
              source: sourceDataVariableBlueprint.id || uuidv4(),
              target: expressionBlueprint.id || uuidv4(),
              animated: true,
            });
          }
        }

        // map the inputs
        for (const inputDataTableBlueprint of expressionBlueprint.inputDataTableBlueprints) {
          const sourceDataVariableBlueprint =
            blueprint.dataVariableBlueprints.find(
              (x) =>
                x.name == inputDataTableBlueprint.name &&
                x.namespace == inputDataTableBlueprint.namespace
            );
          if (sourceDataVariableBlueprint) {
            initialEdges.push({
              id: `${sourceDataVariableBlueprint.id}-${expressionBlueprint.id}`,
              source: sourceDataVariableBlueprint.id || uuidv4(),
              target: expressionBlueprint.id || uuidv4(),
              animated: true,
            });
          }
        }

        // map the output
        const targetDataVariableBlueprint =
          blueprint.dataVariableBlueprints.find(
            (x) =>
              x.name == expressionBlueprint.outputDataVariableBlueprint.name &&
              x.namespace ==
                expressionBlueprint.outputDataVariableBlueprint.namespace
          );
        if (targetDataVariableBlueprint) {
          initialEdges.push({
            id: `${expressionBlueprint.id}-${targetDataVariableBlueprint.id}`,
            source: expressionBlueprint.id || uuidv4(),
            target: targetDataVariableBlueprint.id || uuidv4(),
            animated: true,
          });
        }
      }

      for (const triggerBlueprint of blueprint.triggerBlueprints) {
        initialNodes.push({
          id: triggerBlueprint.id || uuidv4(),
          data: {
            label: triggerBlueprint.name,
            type: ElementType.Trigger,
          },
          position: { x: 0, y: 0 },
        });

        // map the inputs
        for (const triggerExpressionBlueprints of triggerBlueprint.triggerExpressionBlueprints) {
          const sourceDataVariableBlueprint =
            blueprint.dataVariableBlueprints.find(
              (x) =>
                x.name ==
                  triggerExpressionBlueprints.inputDataVariableBlueprint.name &&
                x.namespace ==
                  triggerExpressionBlueprints.inputDataVariableBlueprint
                    .namespace
            );
          if (sourceDataVariableBlueprint) {
            initialEdges.push({
              id: `${sourceDataVariableBlueprint.id}-${triggerBlueprint.id}`,
              source: sourceDataVariableBlueprint.id || uuidv4(),
              target: triggerBlueprint.id || uuidv4(),
              animated: true,
            });
          }
        }

        // map the output
        const targetDataVariableBlueprint =
          blueprint.dataVariableBlueprints.find(
            (x) =>
              x.name == triggerBlueprint.outputDataVariableBlueprint.name &&
              x.namespace ==
                triggerBlueprint.outputDataVariableBlueprint.namespace
          );
        if (targetDataVariableBlueprint) {
          initialEdges.push({
            id: `${triggerBlueprint.id}-${targetDataVariableBlueprint.id}`,
            source: triggerBlueprint.id || uuidv4(),
            target: targetDataVariableBlueprint.id || uuidv4(),
            animated: true,
          });
        }
      }

      for (const dataTableBlueprint of blueprint.dataTableBlueprints) {
        initialNodes.push({
          id: dataTableBlueprint.id || uuidv4(),
          data: {
            label: dataTableBlueprint.name,
            type: ElementType.DataTable,
          },
          position: { x: 0, y: 0 },
        });

        // map the inputs
        for (const inputDataVariableBlueprint of dataTableBlueprint.inputDataVariableBlueprints) {
          const sourceDataVariableBlueprint =
            blueprint.dataVariableBlueprints.find(
              (x) =>
                x.name == inputDataVariableBlueprint.name &&
                x.namespace == inputDataVariableBlueprint.namespace
            );
          if (sourceDataVariableBlueprint) {
            initialEdges.push({
              id: `${sourceDataVariableBlueprint.id}-${dataTableBlueprint.id}`,
              source: sourceDataVariableBlueprint.id || uuidv4(),
              target: dataTableBlueprint.id || uuidv4(),
              animated: true,
            });
          }
        }

        // map the output
        const targetDataVariableBlueprint =
          blueprint.dataVariableBlueprints.find(
            (x) =>
              x.name == dataTableBlueprint.name &&
              x.namespace == dataTableBlueprint.namespace
          );
        if (targetDataVariableBlueprint) {
          initialEdges.push({
            id: `${dataTableBlueprint.id}-${targetDataVariableBlueprint.id}`,
            source: dataTableBlueprint.id || uuidv4(),
            target: targetDataVariableBlueprint.id || uuidv4(),
            animated: true,
          });
        }
      }

      for (const dataVariableBlueprint of blueprint.dataVariableBlueprints) {
        initialNodes.push({
          id: dataVariableBlueprint.id || uuidv4(),
          data: {
            label: dataVariableBlueprint.name,
            type: ElementType.DataVariable,
          },
          position: { x: 0, y: 0 },
        });
      }
    }

    const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
      initialNodes,
      initialEdges
    );

    if (selectedElement) {
      const selectedNodeIndex = layoutedNodes.findIndex(
        (node) => node.id === selectedElement.elementId
      );
      const selectedNode = layoutedNodes[selectedNodeIndex];

      if (selectedNode) {
        selectedNode.style = { backgroundColor: "#e6f4ff" };
      }
      setSelectedNode(selectedNode);
    } else {
      setSelectedNode(undefined);
    }

    setNodes(layoutedNodes);
    setEdges(layoutedEdges);
  }, [blueprint, selectedElement]);

  useEffect(() => {
    if (selectedNode) {
      const outgoers = getOutgoers(selectedNode, nodes, edges);
      const incomers = getIncomers(selectedNode, nodes, edges);
      fitView({
        padding: 2,
        nodes: [selectedNode, ...outgoers, ...incomers],
      });
    } else {
      fitView({
        padding: 2,
        nodes,
      });
    }
  }, [selectedNode, nodes, edges, fitView]);

  const onNodeClick = (_: React.SyntheticEvent, node: Node) => {
    console.log("node: ", node);
    // const selectedNodeId: string = nodeIds;
    // const selectedBlueprintId = blueprintIdDictionary[selectedNodeId];
    // const selectedElementId = elementIdDictionary[selectedNodeId];
    // const selectedElementType = elementTypeDictionary[selectedNodeId];
    // const selectedBlueprint = workbook?.blueprints.find(
    //   (x: Blueprint) => x.id === selectedBlueprintId
    // );

    // if (selectedBlueprint) {
    //   onBlueprintSelected(selectedBlueprint);
    // }

    // if (selectedElementId && selectedElementType) {
    //   onElementSelected({
    //     elementId: selectedElementId,
    //     type: selectedElementType,
    //   });
    // }
  };

  if (!blueprint) return <></>;

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      snapToGrid={true}
      onlyRenderVisibleElements
      onNodeClick={onNodeClick}
      connectionLineType={ConnectionLineType.SmoothStep}
    >
      <MiniMap />
      <Background />
      <Controls />
    </ReactFlow>
  );
};

// wrapping with ReactFlowProvider is done outside of the component
export const BlueprintVisualizer = (props: BlueprintVisualizerProps) => {
  return (
    <ReactFlowProvider>
      <BlueprintVisualizerCanvas {...props} />
    </ReactFlowProvider>
  );
};