Added Various API Functionality
This commit is contained in:
parent
6ff4170894
commit
bce39d355f
@ -13,6 +13,7 @@
|
||||
"@mui/icons-material": "7.0.2",
|
||||
"@mui/material": "7.0.2",
|
||||
"normalize.css": "8.0.1",
|
||||
"prismjs": "1.30.0",
|
||||
"react": "19.1.0",
|
||||
"react-color": "2.19.3",
|
||||
"react-dom": "19.1.0",
|
||||
|
@ -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
|
||||
};
|
@ -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
|
||||
};
|
193
Data/Server/WebUI/src/nodes/Data Collection/Node_API_Request.jsx
Normal file
193
Data/Server/WebUI/src/nodes/Data Collection/Node_API_Request.jsx
Normal 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
|
||||
};
|
@ -7,6 +7,7 @@ torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||
Flask
|
||||
requests
|
||||
flask_socketio
|
||||
flask-cors
|
||||
eventlet
|
||||
|
||||
# GUI-related dependencies (Qt for GUI components)
|
||||
|
@ -4,8 +4,10 @@ import eventlet
|
||||
# Monkey-patch stdlib for cooperative sockets
|
||||
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_cors import CORS
|
||||
|
||||
import time
|
||||
import os # To Read Production ReactJS Server Folder
|
||||
@ -22,6 +24,9 @@ app = Flask(
|
||||
static_url_path=''
|
||||
)
|
||||
|
||||
# Enable CORS on All Routes
|
||||
CORS(app)
|
||||
|
||||
socketio = SocketIO(
|
||||
app,
|
||||
cors_allowed_origins="*",
|
||||
@ -108,6 +113,32 @@ def provision_agent():
|
||||
socketio.emit("agent_config", config)
|
||||
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
|
||||
# ---------------------------------------------
|
||||
|
Loading…
x
Reference in New Issue
Block a user