diff --git a/Data/WebUI/src/App.js b/Data/WebUI/src/App.js
index a26f26b..a6adc33 100644
--- a/Data/WebUI/src/App.js
+++ b/Data/WebUI/src/App.js
@@ -1,139 +1,329 @@
-import React from "react";
-import FlowEditor from "./components/FlowEditor";
-import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
+import React, { useState, useEffect, useCallback, useRef } from "react";
import {
- AppBar,
- Toolbar,
- Typography,
- Box,
- Menu,
- MenuItem,
- Button,
- CssBaseline,
- ThemeProvider,
- createTheme
+ AppBar,
+ Toolbar,
+ Typography,
+ Box,
+ Menu,
+ MenuItem,
+ Button,
+ CssBaseline,
+ ThemeProvider,
+ createTheme,
+ Accordion,
+ AccordionSummary,
+ AccordionDetails
} from "@mui/material";
+import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+
+import ReactFlow, {
+ Background,
+ addEdge,
+ applyNodeChanges,
+ applyEdgeChanges,
+ ReactFlowProvider,
+ useReactFlow,
+ Handle,
+ Position
+} from "reactflow";
+import "reactflow/dist/style.css";
+import "./Borealis.css";
+
+// ✅ Custom styled node with handles
+const CustomNode = ({ data }) => {
+ return (
+
+ {/* Input handle on the left */}
+
+
+ {/* Output handle on the right */}
+
+
+
+ {data.label || "Custom Node"}
+
+
+ {data.content || "Placeholder"}
+
+
+ );
+};
+
+// FlowEditor with drag-and-drop and updated behavior
+function FlowEditor({ nodes, edges, setNodes, setEdges }) {
+ const reactFlowWrapper = useRef(null);
+ const { project } = useReactFlow();
+
+ const onDrop = useCallback(
+ (event) => {
+ event.preventDefault();
+ const type = event.dataTransfer.getData("application/reactflow");
+ if (!type) return;
+
+ const bounds = reactFlowWrapper.current.getBoundingClientRect();
+ const position = project({
+ x: event.clientX - bounds.left,
+ y: event.clientY - bounds.top
+ });
+
+ const id = `node-${Date.now()}`;
+ const newNode = {
+ id,
+ type: "custom",
+ position,
+ data: {
+ label: "Custom Node",
+ content: "Placeholder"
+ }
+ };
+
+ setNodes((nds) => [...nds, newNode]);
+ },
+ [project, setNodes]
+ );
+
+ const onDragOver = useCallback((event) => {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = "move";
+ }, []);
+
+ const onConnect = useCallback(
+ (params) =>
+ setEdges((eds) =>
+ addEdge(
+ {
+ ...params,
+ type: "smoothstep",
+ animated: true,
+ style: {
+ strokeDasharray: "6 3",
+ stroke: "#58a6ff"
+ }
+ },
+ eds
+ )
+ ),
+ [setEdges]
+ );
+
+ const onNodesChange = useCallback(
+ (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
+ [setNodes]
+ );
+
+ const onEdgesChange = useCallback(
+ (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
+ [setEdges]
+ );
+
+ useEffect(() => {
+ const nodeCountEl = document.getElementById("nodeCount");
+ if (nodeCountEl) {
+ nodeCountEl.innerText = nodes.length;
+ }
+ }, [nodes]);
+
+ return (
+
+
+
+
+
+ );
+}
+
const darkTheme = createTheme({
- palette: {
- mode: "dark",
- background: {
- default: "#121212",
- paper: "#1e1e1e"
- },
- text: {
- primary: "#ffffff"
+ palette: {
+ mode: "dark",
+ background: {
+ default: "#121212",
+ paper: "#1e1e1e"
+ },
+ text: {
+ primary: "#ffffff"
+ }
}
- }
});
export default function App() {
- const [workflowsAnchorEl, setWorkflowsAnchorEl] = React.useState(null);
- const [aboutAnchorEl, setAboutAnchorEl] = React.useState(null);
+ const [aboutAnchorEl, setAboutAnchorEl] = useState(null);
+ const [nodes, setNodes] = useState([]);
+ const [edges, setEdges] = useState([]);
- const handleWorkflowsMenuOpen = (event) => {
- setWorkflowsAnchorEl(event.currentTarget);
- };
+ const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget);
+ const handleAboutMenuClose = () => setAboutAnchorEl(null);
- const handleAboutMenuOpen = (event) => {
- setAboutAnchorEl(event.currentTarget);
- };
+ const handleAddTestNode = () => {
+ const id = `test-node-${Date.now()}`;
+ const newNode = {
+ id,
+ type: "custom",
+ data: {
+ label: "Custom Node",
+ content: "Placeholder"
+ },
+ position: {
+ x: 250 + Math.random() * 300,
+ y: 150 + Math.random() * 200
+ }
+ };
+ setNodes((nds) => [...nds, newNode]);
+ };
- const handleWorkflowsMenuClose = () => {
- setWorkflowsAnchorEl(null);
- };
+ return (
+
+
+
+
+
+ Borealis - Workflow Automation Tool
+
+ }
+ >
+ About
+
+
+
+
- const handleAboutMenuClose = () => {
- setAboutAnchorEl(null);
- };
+
+
+
+
+ } sx={accordionHeaderStyle}>
+ Workflows
+
+
+
+
+
+
+
- return (
-
-
- {/*
- Main container that:
- - fills 100% viewport height
- - organizes content with flexbox (vertical)
- */}
-
- {/* --- TOP BAR --- */}
-
-
-
- Borealis - Workflow Automation Tool
-
+
+ } sx={accordionHeaderStyle}>
+ Nodes
+
+
+
+
+
+
- {/* Workflows Menu */}
- }
- >
- Workflows
-
-
+
+
+
+
+
+
- {/* About Menu */}
- }
- >
- About
-
-
-
-
-
- {/* --- REACT FLOW EDITOR --- */}
- {/*
- flexGrow={1} ⇒ This box expands to fill remaining vertical space
- overflow="hidden" ⇒ No scroll bars, so React Flow does internal panning
- mt: 1 ⇒ Add top margin so the gradient starts closer to the AppBar.
- */}
-
- {
- document.getElementById("nodeCount").innerText = count;
- }}
- />
-
-
- {/* --- STATUS BAR at BOTTOM --- */}
-
- Nodes: 0 | Update Rate: 500ms | Flask API Server:{" "}
-
- http://127.0.0.1:5000/data/api/nodes
-
-
-
-
- );
+
+ Nodes: 0 | Update Rate: 500ms
+
+
+
+ );
}
+
+const sidebarBtnStyle = {
+ color: "#ccc",
+ backgroundColor: "#232323",
+ justifyContent: "flex-start",
+ pl: 2,
+ fontSize: "0.9rem"
+};
+
+const accordionHeaderStyle = {
+ backgroundColor: "#2c2c2c",
+ minHeight: "36px",
+ "& .MuiAccordionSummary-content": { margin: 0 }
+};
diff --git a/Data/WebUI/src/Borealis.css b/Data/WebUI/src/Borealis.css
new file mode 100644
index 0000000..9341699
--- /dev/null
+++ b/Data/WebUI/src/Borealis.css
@@ -0,0 +1,23 @@
+/* FlowEditor background container */
+.flow-editor-container {
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+}
+
+/* Blue Gradient Overlay */
+.flow-editor-container::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none; /* Ensures grid and nodes remain fully interactive */
+ background: linear-gradient( to bottom, rgba(9, 44, 68, 0.9) 0%, /* Deep blue at the top */
+ rgba(30, 30, 30, 0) 45%, /* Fade out towards center */
+ rgba(30, 30, 30, 0) 75%, /* No gradient in the middle */
+ rgba(9, 44, 68, 0.7) 100% /* Deep blue at the bottom */
+ );
+ z-index: -1; /* Ensures it stays behind the React Flow elements */
+}
\ No newline at end of file
diff --git a/Data/WebUI/src/components/FlowEditor.css b/Data/WebUI/src/components/FlowEditor.css
deleted file mode 100644
index 4990c4f..0000000
--- a/Data/WebUI/src/components/FlowEditor.css
+++ /dev/null
@@ -1,23 +0,0 @@
-/* FlowEditor background container */
-.flow-editor-container {
- position: relative;
- width: 100vw;
- height: 100vh;
-}
-
- /* Blue Gradient Overlay */
- .flow-editor-container::before {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- pointer-events: none; /* Ensures grid and nodes remain fully interactive */
- background: linear-gradient( to bottom, rgba(9, 44, 68, 0.9) 0%, /* Deep blue at the top */
- rgba(30, 30, 30, 0) 45%, /* Fade out towards center */
- rgba(30, 30, 30, 0) 75%, /* No gradient in the middle */
- rgba(9, 44, 68, 0.7) 100% /* Deep blue at the bottom */
- );
- z-index: -1; /* Ensures it stays behind the React Flow elements */
- }
diff --git a/Data/WebUI/src/components/FlowEditor.jsx b/Data/WebUI/src/components/FlowEditor.jsx
deleted file mode 100644
index 4c72a61..0000000
--- a/Data/WebUI/src/components/FlowEditor.jsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React, { useState, useEffect, useCallback } from "react";
-import ReactFlow, {
- addEdge,
- Controls,
- Background,
-} from "reactflow";
-import "reactflow/dist/style.css";
-import "./FlowEditor.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) => {
- // Data should contain nodes and edges arrays
- const newElements = [...data.nodes, ...data.edges];
- setElements(newElements);
- });
- }, []);
-
- const onConnect = useCallback(
- (params) => {
- const newEdge = { id: `e${params.source}-${params.target}`, ...params };
- setElements((els) => [...els, newEdge]);
-
- // Separate nodes/edges for saving:
- const nodes = elements.filter((el) => el.type);
- const edges = elements.filter((el) => !el.type);
-
- saveWorkflow({
- nodes,
- edges: [...edges, newEdge],
- });
- },
- [elements]
- );
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/Data/WebUI/src/nodes/TestNode.jsx b/Data/WebUI/src/nodes/TestNode.jsx
new file mode 100644
index 0000000..cb69241
--- /dev/null
+++ b/Data/WebUI/src/nodes/TestNode.jsx
@@ -0,0 +1,14 @@
+// Data/WebUI/src/nodes/TestNode.jsx
+import React from 'react';
+import { Handle, Position } from 'reactflow';
+
+export default function TestNode({ data }) {
+ return (
+
+
Test Node
+
{data.label}
+
+
+
+ );
+}
diff --git a/Data/server.py b/Data/server.py
index 377227e..424cff4 100644
--- a/Data/server.py
+++ b/Data/server.py
@@ -1,9 +1,5 @@
-from flask import Flask, send_from_directory, jsonify, request, abort
+from flask import Flask, send_from_directory
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")
@@ -12,41 +8,6 @@ if not os.path.exists(build_folder):
app = Flask(__name__, static_folder=build_folder, static_url_path="/")
-# Directory where nodes are stored
-NODES_PACKAGE = "Nodes"
-
-# In-memory workflow storage
-workflow_data = {
- "nodes": [],
- "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
- category_name = os.path.basename(root)
-
- for file in files:
- if file.endswith(".py") and file != "__init__.py":
- module_name = f"{module_prefix}.{file[:-3]}"
- try:
- module = importlib.import_module(module_name)
- for name, obj in inspect.getmembers(module, inspect.isclass):
- 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)
- except Exception as e:
- print(f"Failed to import {module_name}: {e}")
-
- return nodes_by_category
-
@app.route("/")
def serve_frontend():
"""Serve the React app."""
@@ -55,77 +16,5 @@ def serve_frontend():
return send_from_directory(app.static_folder, "index.html")
return "Borealis React App Code Not Found
Please re-deploy Borealis Workflow Automation Tool
", 404
-@app.route("/api/nodes", methods=["GET"])
-def get_available_nodes():
- """Return available node types."""
- nodes = import_nodes_from_folder(NODES_PACKAGE)
- return jsonify(nodes)
-
-@app.route("/api/workflow", methods=["GET", "POST"])
-def handle_workflow():
- """Retrieve or update the workflow."""
- global 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():
- """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": node_id,
- "type": data["nodeType"],
- "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=["PUT", "DELETE"])
-def modify_node(node_id):
- """Update or delete a node."""
- global workflow_data
- 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")
-
- 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 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=False)