Upgraded API Request Node

This commit is contained in:
Nicole Rappe 2025-05-30 01:27:19 -06:00
parent dff4938f51
commit 020eff9d5c

View File

@ -1,67 +1,27 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/Data Collection/Node_API_Request.jsx ////////// 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 React, { useState, useEffect, useRef } from "react";
import { Handle, Position, useReactFlow, useStore } from "reactflow"; import { Handle, Position, useReactFlow, useStore } from "reactflow";
// API Request Node (Modern, Sidebar Config Enabled)
const APIRequestNode = ({ id, data }) => { const APIRequestNode = ({ id, data }) => {
const { setNodes } = useReactFlow(); const { setNodes } = useReactFlow();
const edges = useStore((state) => state.edges); const edges = useStore((state) => state.edges);
if (!window.BorealisValueBus) window.BorealisValueBus = {}; if (!window.BorealisValueBus) window.BorealisValueBus = {};
const [url, setUrl] = useState(data?.url || "http://localhost:5000/health"); // Use config values, but coerce types
const [editUrl, setEditUrl] = useState(data?.url || "http://localhost:5000/health"); const url = data?.url || "http://localhost:5000/health";
const [useProxy, setUseProxy] = useState(data?.useProxy ?? true); // Note: Store useProxy as a string ("true"/"false"), convert to boolean for logic
const [body, setBody] = useState(data?.body || ""); const useProxy = (data?.useProxy ?? "true") === "true";
const [intervalSec, setIntervalSec] = useState(data?.intervalSec ?? 10); const body = data?.body || "";
const intervalSec = parseInt(data?.intervalSec || "10", 10) || 10;
// Status State
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [statusCode, setStatusCode] = useState(null); const [statusCode, setStatusCode] = useState(null);
const [statusText, setStatusText] = useState(""); const [statusText, setStatusText] = useState("");
const resultRef = useRef(null); 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
)
);
};
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(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@ -69,12 +29,9 @@ const APIRequestNode = ({ id, data }) => {
try { try {
setError(null); setError(null);
// Allow dynamic URL override from upstream node (if present)
const inputEdge = edges.find((e) => e.target === id); const inputEdge = edges.find((e) => e.target === id);
const upstreamUrl = inputEdge ? window.BorealisValueBus[inputEdge.source] : null; const upstreamUrl = inputEdge ? window.BorealisValueBus[inputEdge.source] : null;
if (upstreamUrl && upstreamUrl !== editUrl) {
setEditUrl(upstreamUrl);
}
const resolvedUrl = upstreamUrl || url; const resolvedUrl = upstreamUrl || url;
let target = useProxy ? `/api/proxy?url=${encodeURIComponent(resolvedUrl)}` : resolvedUrl; let target = useProxy ? `/api/proxy?url=${encodeURIComponent(resolvedUrl)}` : resolvedUrl;
@ -126,119 +83,111 @@ const APIRequestNode = ({ id, data }) => {
}; };
}, [url, body, intervalSec, useProxy, id, setNodes, edges]); }, [url, body, intervalSec, useProxy, id, setNodes, edges]);
// Upstream disables direct editing of URL in the UI
const inputEdge = edges.find((e) => e.target === id); const inputEdge = edges.find((e) => e.target === id);
const hasUpstream = Boolean(inputEdge && inputEdge.source); const hasUpstream = Boolean(inputEdge && inputEdge.source);
// -- Node Card Render (minimal: sidebar handles config) --
return ( return (
<div className="borealis-node"> <div className="borealis-node">
<Handle type="target" position={Position.Left} className="borealis-handle" /> <Handle type="target" position={Position.Left} className="borealis-handle" />
<div className="borealis-node-header">API Request</div> <div className="borealis-node-header">
<div className="borealis-node-content"> {data?.label || "API Request"}
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>Request URL:</label> </div>
<input <div className="borealis-node-content" style={{ fontSize: "9px", color: "#ccc" }}>
type="text" <div>
value={editUrl} <b>Status:</b>{" "}
onChange={handleUrlInputChange} {error ? (
disabled={hasUpstream} <span style={{ color: "#f66" }}>{error}</span>
style={{ ) : statusCode !== null ? (
fontSize: "9px", <span style={{ color: "#6f6" }}>{statusCode} {statusText}</span>
padding: "4px", ) : (
width: "100%", "N/A"
background: hasUpstream ? "#2a2a2a" : "#1e1e1e", )}
color: "#ccc", </div>
border: "1px solid #444", <div style={{ marginTop: "4px" }}>
borderRadius: "2px" <b>Result:</b>
}} <pre style={{
/> background: "#181818",
<button color: "#b6ffb4",
onClick={handleUpdateUrl} fontSize: "8px",
disabled={hasUpstream} maxHeight: 62,
style={{ overflow: "auto",
fontSize: "9px", margin: 0,
marginTop: "6px", padding: "4px",
padding: "2px 6px", borderRadius: "2px"
background: "#333", }}>{data?.result ? String(data.result).slice(0, 350) : "No data"}</pre>
color: "#ccc", </div>
border: "1px solid #444",
borderRadius: "2px",
cursor: hasUpstream ? "not-allowed" : "pointer"
}}
>
Update URL
</button>
<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>
<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"
}}
/>
{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> </div>
<Handle type="source" position={Position.Right} className="borealis-handle" /> <Handle type="source" position={Position.Right} className="borealis-handle" />
</div> </div>
); );
}; };
// Node Registration Object with sidebar config + docs
export default { export default {
type: "API_Request", type: "API_Request",
label: "API Request", label: "API Request",
description: "Fetch JSON from an API endpoint with optional body, proxy toggle, and polling interval.", description: "Fetch JSON from an API endpoint with optional POST body, polling, and proxy toggle. Accepts URL from upstream.",
content: "Fetch JSON from HTTP or remote API via internal proxy to bypass CORS.", content: "Fetch JSON from HTTP or remote API endpoint, with CORS proxy option.",
component: APIRequestNode component: APIRequestNode,
config: [
{
key: "url",
label: "Request URL",
type: "text",
defaultValue: "http://localhost:5000/health"
},
{
key: "useProxy",
label: "Use Proxy (bypass CORS)",
type: "select",
options: ["true", "false"],
defaultValue: "true"
},
{
key: "body",
label: "Request Body (JSON)",
type: "textarea",
defaultValue: ""
},
{
key: "intervalSec",
label: "Polling Interval (sec)",
type: "text",
defaultValue: "10"
}
],
usage_documentation: `
### API Request Node
Fetches JSON from an HTTP or HTTPS API endpoint, with an option to POST a JSON body and control polling interval.
**Features:**
- **URL**: You can set a static URL, or connect an upstream node to dynamically control the API endpoint.
- **Use Proxy**: When enabled, requests route through the Borealis backend proxy to bypass CORS/browser restrictions.
- **Request Body**: POST JSON data (leave blank for GET).
- **Polling Interval**: Set how often (in seconds) to re-fetch the API.
**Outputs:**
- The downstream value is the parsed JSON object from the API response.
**Typical Use Cases:**
- Poll external APIs (weather, status, data, etc)
- Connect to local/internal REST endpoints
- Build data pipelines with API triggers
**Input & UI Behavior:**
- If an upstream node is connected, its output value will override the Request URL.
- All config is handled in the right sidebar (Node Properties).
**Error Handling:**
- If the fetch fails, the node displays the error in the UI.
- Only 2xx status codes are considered successful.
**Security Note:**
- Use Proxy mode for APIs requiring CORS bypass or additional privacy.
`.trim()
}; };