Initial Commit

This commit is contained in:
2025-03-28 21:20:36 -06:00
parent 7ef3ce99bb
commit 4b0cc05512
9 changed files with 761 additions and 0 deletions

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="Borealis"
content="Workflow Automation Tool"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Borealis</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

139
Data/WebUI/src/App.js Normal file
View File

@ -0,0 +1,139 @@
import React from "react";
import FlowEditor from "./components/FlowEditor";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import {
AppBar,
Toolbar,
Typography,
Box,
Menu,
MenuItem,
Button,
CssBaseline,
ThemeProvider,
createTheme
} from "@mui/material";
const darkTheme = createTheme({
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 handleWorkflowsMenuOpen = (event) => {
setWorkflowsAnchorEl(event.currentTarget);
};
const handleAboutMenuOpen = (event) => {
setAboutAnchorEl(event.currentTarget);
};
const handleWorkflowsMenuClose = () => {
setWorkflowsAnchorEl(null);
};
const handleAboutMenuClose = () => {
setAboutAnchorEl(null);
};
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
{/*
Main container that:
- fills 100% viewport height
- organizes content with flexbox (vertical)
*/}
<Box display="flex" flexDirection="column" height="100vh">
{/* --- TOP BAR --- */}
<AppBar position="static" sx={{ bgcolor: "#092c44" }}>
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Borealis - Workflow Automation Tool
</Typography>
{/* Workflows Menu */}
<Button
color="inherit"
onClick={handleWorkflowsMenuOpen}
endIcon={<KeyboardArrowDownIcon />}
>
Workflows
</Button>
<Menu
anchorEl={workflowsAnchorEl}
open={Boolean(workflowsAnchorEl)}
onClose={handleWorkflowsMenuClose}
>
<MenuItem onClick={handleWorkflowsMenuClose}>Save Workflow</MenuItem>
<MenuItem onClick={handleWorkflowsMenuClose}>Load Workflow</MenuItem>
<MenuItem onClick={handleWorkflowsMenuClose}>Close Workflow</MenuItem>
</Menu>
{/* About Menu */}
<Button
color="inherit"
onClick={handleAboutMenuOpen}
endIcon={<KeyboardArrowDownIcon />}
>
About
</Button>
<Menu
anchorEl={aboutAnchorEl}
open={Boolean(aboutAnchorEl)}
onClose={handleAboutMenuClose}
>
<MenuItem onClick={handleAboutMenuClose}>Gitea Project</MenuItem>
<MenuItem onClick={handleAboutMenuClose}>Credits</MenuItem>
</Menu>
</Toolbar>
</AppBar>
{/* --- 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.
*/}
<Box flexGrow={1} overflow="hidden" sx={{ mt: 0 }}>
<FlowEditor
updateNodeCount={(count) => {
document.getElementById("nodeCount").innerText = count;
}}
/>
</Box>
{/* --- STATUS BAR at BOTTOM --- */}
<Box
component="footer"
sx={{
bgcolor: "#1e1e1e",
color: "white",
px: 2,
py: 1,
textAlign: "left"
}}
>
<b>Nodes</b>: <span id="nodeCount">0</span> | <b>Update Rate</b>: 500ms | <b>Flask API Server:</b>{" "}
<a
href="http://127.0.0.1:5000/api/nodes"
style={{ color: "#3c78b4" }}
>
http://127.0.0.1:5000/data/api/nodes
</a>
</Box>
</Box>
</ThemeProvider>
);
}

View File

@ -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 */
}

View File

@ -0,0 +1,68 @@
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 (
<div className="flow-editor-container">
<ReactFlow
proOptions={{ hideAttribution: true }} // Remove the React Flow watermark
elements={elements}
onConnect={onConnect}
>
<Controls />
<Background
variant="lines"
gap={100}
size={1}
color="rgba(255, 255, 255, 0.2)" // White grid lines at 20% opacity
/>
</ReactFlow>
</div>
);
}

131
Data/server.py Normal file
View File

@ -0,0 +1,131 @@
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 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
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."""
index_path = os.path.join(build_folder, "index.html")
if os.path.exists(index_path):
return send_from_directory(app.static_folder, "index.html")
return "<h1>Borealis React App Code Not Found</h1><p>Please re-deploy Borealis Workflow Automation Tool</p>", 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/<string:node_id>", 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/<string:edge_id>", 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)