Added Various API Functionality

This commit is contained in:
Nicole Rappe 2025-05-10 16:23:32 -06:00
parent 6ff4170894
commit bce39d355f
14 changed files with 538 additions and 1 deletions

View File

@ -13,6 +13,7 @@
"@mui/icons-material": "7.0.2", "@mui/icons-material": "7.0.2",
"@mui/material": "7.0.2", "@mui/material": "7.0.2",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"prismjs": "1.30.0",
"react": "19.1.0", "react": "19.1.0",
"react-color": "2.19.3", "react-color": "2.19.3",
"react-dom": "19.1.0", "react-dom": "19.1.0",

View File

@ -0,0 +1,179 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Analysis & Manipulation/Node_JSON_Display.jsx
import React, { useEffect, useState, useRef, useCallback } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow";
// For syntax highlighting, ensure prismjs is installed: npm install prismjs
import Prism from "prismjs";
import "prismjs/components/prism-json";
import "prismjs/themes/prism-okaidia.css";
const JSONPrettyDisplayNode = ({ id, data }) => {
const { setNodes } = useReactFlow();
const edges = useStore((state) => state.edges);
const containerRef = useRef(null);
const resizingRef = useRef(false);
const startPosRef = useRef({ x: 0, y: 0 });
const startDimRef = useRef({ width: 0, height: 0 });
const [jsonData, setJsonData] = useState(data?.jsonData || {});
const initW = parseInt(data?.width || "300", 10);
const initH = parseInt(data?.height || "150", 10);
const [dimensions, setDimensions] = useState({ width: initW, height: initH });
const jsonRef = useRef(jsonData);
const persistDimensions = useCallback(() => {
const w = `${Math.round(dimensions.width)}px`;
const h = `${Math.round(dimensions.height)}px`;
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, width: w, height: h } }
: n
)
);
}, [dimensions, id, setNodes]);
useEffect(() => {
const onMouseMove = (e) => {
if (!resizingRef.current) return;
const dx = e.clientX - startPosRef.current.x;
const dy = e.clientY - startPosRef.current.y;
setDimensions({
width: Math.max(100, startDimRef.current.width + dx),
height: Math.max(60, startDimRef.current.height + dy)
});
};
const onMouseUp = () => {
if (resizingRef.current) {
resizingRef.current = false;
persistDimensions();
}
};
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
return () => {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
};
}, [persistDimensions]);
const onResizeMouseDown = (e) => {
e.stopPropagation();
resizingRef.current = true;
startPosRef.current = { x: e.clientX, y: e.clientY };
startDimRef.current = { ...dimensions };
};
useEffect(() => {
let rate = window.BorealisUpdateRate;
const tick = () => {
const edge = edges.find((e) => e.target === id);
if (edge && edge.source) {
const upstream = window.BorealisValueBus[edge.source];
if (typeof upstream === "object") {
if (JSON.stringify(upstream) !== JSON.stringify(jsonRef.current)) {
jsonRef.current = upstream;
setJsonData(upstream);
window.BorealisValueBus[id] = upstream;
setNodes((nds) =>
nds.map((n) =>
n.id === id ? { ...n, data: { ...n.data, jsonData: upstream } } : n
)
);
}
}
} else {
window.BorealisValueBus[id] = jsonRef.current;
}
};
const iv = setInterval(tick, rate);
const monitor = setInterval(() => {
if (window.BorealisUpdateRate !== rate) {
clearInterval(iv);
clearInterval(monitor);
}
}, 200);
return () => { clearInterval(iv); clearInterval(monitor); };
}, [id, edges, setNodes]);
// Generate highlighted HTML
const pretty = JSON.stringify(jsonData, null, 2);
const highlighted = Prism.highlight(pretty, Prism.languages.json, "json");
return (
<div
ref={containerRef}
className="borealis-node"
style={{
display: "flex",
flexDirection: "column",
width: dimensions.width,
height: dimensions.height,
overflow: "visible",
position: "relative",
boxSizing: "border-box"
}}
>
<Handle type="target" position={Position.Left} className="borealis-handle" />
<Handle type="source" position={Position.Right} className="borealis-handle" />
<div className="borealis-node-header">Display JSON Data</div>
<div
className="borealis-node-content"
style={{
flex: 1,
padding: "4px",
fontSize: "9px",
color: "#ccc",
display: "flex",
flexDirection: "column",
overflow: "hidden"
}}
>
<div style={{ marginBottom: "4px" }}>
Display prettified JSON from upstream.
</div>
<div
style={{
flex: 1,
width: "100%",
background: "#1e1e1e",
border: "1px solid #444",
borderRadius: "2px",
padding: "4px",
overflowY: "auto",
fontFamily: "monospace",
fontSize: "9px"
}}
>
<pre
dangerouslySetInnerHTML={{ __html: highlighted }}
style={{ margin: 0 }}
/>
</div>
</div>
<div
onMouseDown={onResizeMouseDown}
style={{
position: "absolute",
width: "20px",
height: "20px",
right: "-4px",
bottom: "-4px",
cursor: "nwse-resize",
background: "transparent",
zIndex: 10
}}
/>
</div>
);
};
export default {
type: "Node_JSON_Pretty_Display",
label: "Display JSON Data",
description: "Display upstream JSON object as prettified JSON with syntax highlighting.",
content: "Display prettified multi-line JSON from upstream node.",
component: JSONPrettyDisplayNode
};

View File

@ -0,0 +1,132 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Analysis & Manipulation/Node_JSON_Value_Extractor.jsx
import React, { useState, useEffect } from "react";
import { Handle, Position, useReactFlow } from "reactflow";
const JSONValueExtractorNode = ({ id, data }) => {
const { setNodes, getEdges } = useReactFlow();
const [keyName, setKeyName] = useState(data?.keyName || "");
const [value, setValue] = useState(data?.result || "");
const handleKeyChange = (e) => {
const newKey = e.target.value;
setKeyName(newKey);
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, keyName: newKey } }
: n
)
);
};
useEffect(() => {
let currentRate = window.BorealisUpdateRate;
let intervalId;
const runNodeLogic = () => {
const edges = getEdges();
const incoming = edges.filter((e) => e.target === id);
const sourceId = incoming[0]?.source;
let newValue = "Key Not Found";
if (sourceId && window.BorealisValueBus[sourceId] !== undefined) {
let upstream = window.BorealisValueBus[sourceId];
if (upstream && typeof upstream === "object" && keyName) {
const pathSegments = keyName.split(".");
let nodeVal = upstream;
for (let segment of pathSegments) {
if (
nodeVal != null &&
(typeof nodeVal === "object" || Array.isArray(nodeVal)) &&
segment in nodeVal
) {
nodeVal = nodeVal[segment];
} else {
nodeVal = undefined;
break;
}
}
if (nodeVal !== undefined) {
newValue = String(nodeVal);
}
}
}
if (newValue !== value) {
setValue(newValue);
window.BorealisValueBus[id] = newValue;
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, result: newValue } }
: n
)
);
}
};
runNodeLogic();
intervalId = setInterval(runNodeLogic, currentRate);
const monitor = setInterval(() => {
const newRate = window.BorealisUpdateRate;
if (newRate !== currentRate) {
clearInterval(intervalId);
currentRate = newRate;
intervalId = setInterval(runNodeLogic, currentRate);
}
}, 250);
return () => {
clearInterval(intervalId);
clearInterval(monitor);
};
}, [keyName, id, setNodes, getEdges, value]);
return (
<div className="borealis-node">
<div className="borealis-node-header">JSON Value Extractor</div>
<div className="borealis-node-content">
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
Key:
</label>
<input
type="text"
value={keyName}
onChange={handleKeyChange}
placeholder="e.g. name.en"
style={{
fontSize: "9px", padding: "4px", width: "100%",
background: "#1e1e1e", color: "#ccc",
border: "1px solid #444", borderRadius: "2px"
}}
/>
<label style={{ fontSize: "9px", display: "block", margin: "8px 0 4px" }}>
Value:
</label>
<textarea
readOnly
value={value}
rows={2}
style={{
fontSize: "9px", padding: "4px", width: "100%",
background: "#1e1e1e", color: "#ccc",
border: "1px solid #444", borderRadius: "2px",
resize: "none"
}}
/>
</div>
<Handle type="target" position={Position.Left} className="borealis-handle" />
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
export default {
type: "JSON_Value_Extractor",
label: "JSON Value Extractor",
description: "Extract a nested value by dot-delimited path from upstream JSON data.",
content: "Provide a dot-separated key path (e.g. 'name.en'); outputs the extracted string or 'Key Not Found'.",
component: JSONValueExtractorNode
};

View File

@ -0,0 +1,193 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Collection/Node_API_Request.jsx
import React, { useState, useEffect, useRef } from "react";
import { Handle, Position, useReactFlow } from "reactflow";
const APIRequestNode = ({ id, data }) => {
const { setNodes } = useReactFlow();
if (!window.BorealisValueBus) window.BorealisValueBus = {};
// Stored URL (actual fetch target)
const [url, setUrl] = useState(data?.url || "http://localhost:5000/health");
// Editable URL text
const [editUrl, setEditUrl] = useState(data?.url || "http://localhost:5000/health");
const [useProxy, setUseProxy] = useState(data?.useProxy || true);
const [body, setBody] = useState(data?.body || "");
const [intervalSec, setIntervalSec] = useState(data?.intervalSec ?? 10);
const [error, setError] = useState(null);
const [statusCode, setStatusCode] = useState(null);
const [statusText, setStatusText] = useState("");
const resultRef = useRef(null);
const handleUrlInputChange = (e) => setEditUrl(e.target.value);
const handleToggleProxy = (e) => {
const flag = e.target.checked;
setUseProxy(flag);
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, useProxy: flag } }
: n
)
);
};
const handleUpdateUrl = () => {
setUrl(editUrl);
setError(null);
setStatusCode(null);
setStatusText("");
resultRef.current = null;
window.BorealisValueBus[id] = undefined;
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, url: editUrl, result: undefined } }
: n
)
);
};
// ... other handlers unchanged ...
const handleBodyChange = (e) => {
const newBody = e.target.value;
setBody(newBody);
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, body: newBody } }
: n
)
);
};
const handleIntervalChange = (e) => {
const sec = parseInt(e.target.value, 10) || 1;
setIntervalSec(sec);
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, intervalSec: sec } }
: n
)
);
};
useEffect(() => {
let cancelled = false;
const runNodeLogic = async () => {
try {
setError(null);
let target = url;
if (useProxy) {
target = `/api/proxy?url=${encodeURIComponent(url)}`;
}
const options = {};
if (body.trim()) {
options.method = "POST";
options.headers = { "Content-Type": "application/json" };
options.body = body;
}
const res = await fetch(target, options);
setStatusCode(res.status);
setStatusText(res.statusText);
if (!res.ok) {
// clear on error
resultRef.current = null;
window.BorealisValueBus[id] = undefined;
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, result: undefined } }
: n
)
);
throw new Error(`HTTP ${res.status}`);
}
const json = await res.json();
const pretty = JSON.stringify(json, null, 2);
if (!cancelled && resultRef.current !== pretty) {
resultRef.current = pretty;
window.BorealisValueBus[id] = json;
setNodes((nds) =>
nds.map((n) =>
n.id === id
? { ...n, data: { ...n.data, result: pretty } }
: n
)
);
}
} catch (err) {
console.error("API Request node fetch error:", err);
setError(err.message);
}
};
runNodeLogic();
const ms = Math.max(intervalSec, 1) * 1000;
const iv = setInterval(runNodeLogic, ms);
return () => {
cancelled = true;
clearInterval(iv);
};
}, [url, body, intervalSec, useProxy, id, setNodes]);
return (
<div className="borealis-node">
<div className="borealis-node-header">API Request</div>
<div className="borealis-node-content">
{/* URL input */}
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>Request URL:</label>
<input
type="text"
value={editUrl}
onChange={handleUrlInputChange}
style={{ fontSize: "9px", padding: "4px", width: "100%", background: "#1e1e1e", color: "#ccc", border: "1px solid #444", borderRadius: "2px" }}
/>
<button onClick={handleUpdateUrl} style={{ fontSize: "9px", marginTop: "6px", padding: "2px 6px", background: "#333", color: "#ccc", border: "1px solid #444", borderRadius: "2px", cursor: "pointer" }}>
Update URL
</button>
{/* Proxy toggle */}
<div style={{ marginTop: "6px" }}>
<input
id={`${id}-proxy-toggle`}
type="checkbox"
checked={useProxy}
onChange={handleToggleProxy}
/>
<label
htmlFor={`${id}-proxy-toggle`}
title="Query a remote API server using internal Borealis mechanisms to bypass CORS limitations."
style={{ fontSize: "8px", marginLeft: "4px", cursor: "help" }}
>
Remote API Endpoint
</label>
</div>
{/* body & interval unchanged... */}
<label style={{ fontSize: "9px", display: "block", margin: "8px 0 4px" }}>Request Body:</label>
<textarea value={body} onChange={handleBodyChange} rows={4} style={{ fontSize: "9px", padding: "4px", width: "100%", background: "#1e1e1e", color: "#ccc", border: "1px solid #444", borderRadius: "2px", resize: "vertical" }} />
<label style={{ fontSize: "9px", display: "block", margin: "8px 0 4px" }}>Polling Interval (sec):</label>
<input type="number" min="1" value={intervalSec} onChange={handleIntervalChange} style={{ fontSize: "9px", padding: "4px", width: "100%", background: "#1e1e1e", color: "#ccc", border: "1px solid #444", borderRadius: "2px" }} />
{/* indicators unchanged... */}
{statusCode !== null && statusCode >= 200 && statusCode < 300 && (
<div style={{ color: "#6f6", fontSize: "8px", marginTop: "6px" }}>Status: {statusCode} {statusText}</div>
)}
{error && (
<div style={{ color: "#f66", fontSize: "8px", marginTop: "6px" }}>Error: {error}</div>
)}
</div>
<Handle type="source" position={Position.Right} className="borealis-handle" />
</div>
);
};
export default {
type: "API_Request",
label: "API Request",
description: "Fetch JSON from an API endpoint with optional body, proxy toggle, and polling interval.",
content: "Fetch JSON from HTTP or remote API via internal proxy to bypass CORS.",
component: APIRequestNode
};

View File

@ -7,6 +7,7 @@ torchaudio --index-url https://download.pytorch.org/whl/cu121
Flask Flask
requests requests
flask_socketio flask_socketio
flask-cors
eventlet eventlet
# GUI-related dependencies (Qt for GUI components) # GUI-related dependencies (Qt for GUI components)

View File

@ -4,8 +4,10 @@ import eventlet
# Monkey-patch stdlib for cooperative sockets # Monkey-patch stdlib for cooperative sockets
eventlet.monkey_patch() eventlet.monkey_patch()
from flask import Flask, request, jsonify, Response, send_from_directory import requests
from flask import Flask, request, jsonify, Response, send_from_directory, make_response
from flask_socketio import SocketIO, emit from flask_socketio import SocketIO, emit
from flask_cors import CORS
import time import time
import os # To Read Production ReactJS Server Folder import os # To Read Production ReactJS Server Folder
@ -22,6 +24,9 @@ app = Flask(
static_url_path='' static_url_path=''
) )
# Enable CORS on All Routes
CORS(app)
socketio = SocketIO( socketio = SocketIO(
app, app,
cors_allowed_origins="*", cors_allowed_origins="*",
@ -108,6 +113,32 @@ def provision_agent():
socketio.emit("agent_config", config) socketio.emit("agent_config", config)
return jsonify({"status": "provisioned", "roles": roles}) return jsonify({"status": "provisioned", "roles": roles})
# ---------------------------------------------
# Borealis External API Proxy Endpoint
# ---------------------------------------------
@app.route("/api/proxy", methods=["GET", "POST", "OPTIONS"])
def proxy():
target = request.args.get("url")
if not target:
return {"error": "Missing ?url="}, 400
# Forward method, headers, body
upstream = requests.request(
method = request.method,
url = target,
headers = {k:v for k,v in request.headers if k.lower() != "host"},
data = request.get_data(),
timeout = 10
)
excluded = ["content-encoding","content-length","transfer-encoding","connection"]
headers = [(k,v) for k,v in upstream.raw.headers.items() if k.lower() not in excluded]
resp = make_response(upstream.content, upstream.status_code)
for k,v in headers:
resp.headers[k] = v
return resp
# --------------------------------------------- # ---------------------------------------------
# Live Screenshot Viewer for Debugging # Live Screenshot Viewer for Debugging
# --------------------------------------------- # ---------------------------------------------