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 + + + + Gitea Project + Credits + + + - 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 */} - - - Save Workflow - Load Workflow - Close Workflow - + + + + + + - {/* About Menu */} - - - Gitea Project - Credits - - - - - {/* --- 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)