Upgraded API Request Node
This commit is contained in:
parent
dff4938f51
commit
020eff9d5c
@ -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()
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user