From f9118e6142c61f67e58f3b9cd378b7f9925bbb96 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Thu, 20 Mar 2025 06:10:05 -0600 Subject: [PATCH] Added React Flow Editor Code and ReactFlow Library --- Data/WebUI/src/components/FlowEditor.jsx | 46 +++++++++++ Data/server.py | 101 +++++++++++++---------- Launch-Borealis.ps1 | 7 +- 3 files changed, 110 insertions(+), 44 deletions(-) create mode 100644 Data/WebUI/src/components/FlowEditor.jsx diff --git a/Data/WebUI/src/components/FlowEditor.jsx b/Data/WebUI/src/components/FlowEditor.jsx new file mode 100644 index 0000000..0de34ca --- /dev/null +++ b/Data/WebUI/src/components/FlowEditor.jsx @@ -0,0 +1,46 @@ +import React, { useState, useEffect, useCallback } from "react"; +import ReactFlow, { + addEdge, + Controls, + Background, +} from "reactflow"; +import "reactflow/dist/style.css"; + +const fetchNodes = async () => { + const response = await fetch("/api/workflow"); + return response.json(); +}; + +const saveWorkflow = async (workflow) => { + await fetch("/api/workflow", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(workflow), + }); +}; + +export default function FlowEditor() { + const [elements, setElements] = useState([]); + + useEffect(() => { + fetchNodes().then((data) => setElements([...data.nodes, ...data.edges])); + }, []); + + const onConnect = useCallback( + (params) => { + const newEdge = { id: `e${params.source}-${params.target}`, ...params }; + setElements((els) => [...els, newEdge]); + saveWorkflow({ nodes: elements.filter(e => e.type), edges: [...elements.filter(e => !e.type), newEdge] }); + }, + [elements] + ); + + return ( +
+ + + + +
+ ); +} diff --git a/Data/server.py b/Data/server.py index e484666..04db7ee 100644 --- a/Data/server.py +++ b/Data/server.py @@ -2,32 +2,34 @@ from flask import Flask, send_from_directory, jsonify, request, abort import os import importlib import inspect +import uuid from OdenGraphQt import BaseNode # Determine the absolute path for the React build folder build_folder = os.path.join(os.getcwd(), "web-interface", "build") if not os.path.exists(build_folder): - print("WARNING: web-interface build folder not found. Please run your React build process to generate 'web-interface/build'.") + print("WARNING: web-interface build folder not found. Please build your React app.") app = Flask(__name__, static_folder=build_folder, static_url_path="/") # Directory where nodes are stored NODES_PACKAGE = "Nodes" -# In-memory workflow storage (a simple scaffold) +# In-memory workflow storage workflow_data = { "nodes": [], - "connections": [] + "edges": [] # Store connections separately } def import_nodes_from_folder(package_name): + """Dynamically import node classes from the given package and list them.""" nodes_by_category = {} package = importlib.import_module(package_name) package_path = package.__path__[0] for root, _, files in os.walk(package_path): rel_path = os.path.relpath(root, package_path).replace(os.sep, ".") - module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name + module_prefix = f"{package_name}.{rel_path}" if rel_path != "." else package_name category_name = os.path.basename(root) for file in files: @@ -36,9 +38,7 @@ def import_nodes_from_folder(package_name): try: module = importlib.import_module(module_name) for name, obj in inspect.getmembers(module, inspect.isclass): - # Filter out only actual node classes you define: - if (issubclass(obj, BaseNode) - and obj.__module__ == module.__name__): + if issubclass(obj, BaseNode) and obj.__module__ == module.__name__: if category_name not in nodes_by_category: nodes_by_category[category_name] = [] nodes_by_category[category_name].append(obj.NODE_NAME) @@ -49,68 +49,83 @@ def import_nodes_from_folder(package_name): @app.route("/") def serve_frontend(): + """Serve the React app.""" index_path = os.path.join(build_folder, "index.html") if os.path.exists(index_path): return send_from_directory(app.static_folder, "index.html") - else: - return "

React App Not Found

Please build the web-interface application.

", 404 + return "

React App Not Found

Please build the web-interface application.

", 404 @app.route("/api/nodes", methods=["GET"]) def get_available_nodes(): - """Returns a list of available node categories and node types.""" + """Return available node types.""" nodes = import_nodes_from_folder(NODES_PACKAGE) return jsonify(nodes) -@app.route("/api/workflow", methods=["GET"]) -def get_workflow(): - """Returns the current workflow data.""" - return jsonify(workflow_data) - -@app.route("/api/workflow", methods=["POST"]) -def save_workflow(): - """Saves the workflow data (nodes and connections) sent from the UI.""" +@app.route("/api/workflow", methods=["GET", "POST"]) +def handle_workflow(): + """Retrieve or update the workflow.""" global workflow_data - data = request.get_json() - if not data: - abort(400, "Invalid workflow data") - workflow_data = data - return jsonify({"status": "success", "workflow": workflow_data}) + if request.method == "GET": + return jsonify(workflow_data) + elif request.method == "POST": + data = request.get_json() + if not data: + abort(400, "Invalid workflow data") + workflow_data = data + return jsonify({"status": "success", "workflow": workflow_data}) @app.route("/api/node", methods=["POST"]) def create_node(): - """Creates a new node. Expects JSON with 'nodeType' and optionally 'position' and 'properties'.""" + """Create a new node with a unique UUID.""" data = request.get_json() if not data or "nodeType" not in data: abort(400, "Invalid node data") + + node_id = str(uuid.uuid4()) # Generate a unique ID node = { - "id": len(workflow_data["nodes"]) + 1, # simple incremental ID + "id": node_id, "type": data["nodeType"], - "position": data.get("position", {"x": 0, "y": 0}), + "position": data.get("position", {"x": 100, "y": 100}), "properties": data.get("properties", {}) } workflow_data["nodes"].append(node) return jsonify({"status": "success", "node": node}) -@app.route("/api/node/", methods=["DELETE"]) -def delete_node(node_id): - """Deletes the node with the given ID.""" +@app.route("/api/node/", methods=["PUT", "DELETE"]) +def modify_node(node_id): + """Update or delete a node.""" global workflow_data - nodes = workflow_data["nodes"] - workflow_data["nodes"] = [n for n in nodes if n["id"] != node_id] - return jsonify({"status": "success", "deletedNode": node_id}) + if request.method == "PUT": + data = request.get_json() + for node in workflow_data["nodes"]: + if node["id"] == node_id: + node["position"] = data.get("position", node["position"]) + node["properties"] = data.get("properties", node["properties"]) + return jsonify({"status": "success", "node": node}) + abort(404, "Node not found") -@app.route("/api/node/", methods=["PUT"]) -def update_node(node_id): - """Updates an existing node's position or properties.""" + elif request.method == "DELETE": + workflow_data["nodes"] = [n for n in workflow_data["nodes"] if n["id"] != node_id] + return jsonify({"status": "success", "deletedNode": node_id}) + +@app.route("/api/edge", methods=["POST"]) +def create_edge(): + """Create a new connection (edge) between nodes.""" data = request.get_json() - if not data: - abort(400, "Invalid node data") - for node in workflow_data["nodes"]: - if node["id"] == node_id: - node["position"] = data.get("position", node["position"]) - node["properties"] = data.get("properties", node["properties"]) - return jsonify({"status": "success", "node": node}) - abort(404, "Node not found") + if not data or "source" not in data or "target" not in data: + abort(400, "Invalid edge data") + + edge_id = str(uuid.uuid4()) + edge = {"id": edge_id, "source": data["source"], "target": data["target"]} + workflow_data["edges"].append(edge) + return jsonify({"status": "success", "edge": edge}) + +@app.route("/api/edge/", methods=["DELETE"]) +def delete_edge(edge_id): + """Delete an edge by ID.""" + global workflow_data + workflow_data["edges"] = [e for e in workflow_data["edges"] if e["id"] != edge_id] + return jsonify({"status": "success", "deletedEdge": edge_id}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/Launch-Borealis.ps1 b/Launch-Borealis.ps1 index 4770780..22b4522 100644 --- a/Launch-Borealis.ps1 +++ b/Launch-Borealis.ps1 @@ -69,7 +69,12 @@ if (-not (Test-Path $buildFolder)) { if (Test-Path $packageJsonPath) { Write-Output "React UI build not found in '$webUIDestination'. Installing dependencies and building the React app..." Push-Location $webUIDestination - npm install + npm install --no-fund --audit=false + + # Ensure react-flow-renderer is installed + Write-Output "Installing React Flow..." + npm install reactflow --no-fund --audit=false + npm run build Pop-Location }