Added Various API Functionality

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

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
};