Milestone 2
This commit is contained in:
parent
8e1d7d0de7
commit
6f6fc96d17
@ -53,8 +53,7 @@ export default function FlowEditor({
|
|||||||
return () => {
|
return () => {
|
||||||
delete window.BorealisOpenDrawer;
|
delete window.BorealisOpenDrawer;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [nodes]);
|
||||||
|
|
||||||
|
|
||||||
const wrapperRef = useRef(null);
|
const wrapperRef = useRef(null);
|
||||||
const { project } = useReactFlow();
|
const { project } = useReactFlow();
|
||||||
@ -353,17 +352,25 @@ export default function FlowEditor({
|
|||||||
if (nodeCountEl) nodeCountEl.innerText = nodes.length;
|
if (nodeCountEl) nodeCountEl.innerText = nodes.length;
|
||||||
}, [nodes]);
|
}, [nodes]);
|
||||||
|
|
||||||
|
const selectedNode = nodes.find((n) => n.data?.label === selectedNodeLabel);
|
||||||
|
const nodeTypeMeta = selectedNode
|
||||||
|
? Object.values(categorizedNodes).flat().find((def) => def.type === selectedNode.type)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flow-editor-container"
|
className="flow-editor-container"
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
style={{ position: "relative" }}
|
style={{ position: "relative" }}
|
||||||
>
|
>
|
||||||
<NodeConfigurationSidebar
|
|
||||||
drawerOpen={drawerOpen}
|
<NodeConfigurationSidebar
|
||||||
setDrawerOpen={setDrawerOpen}
|
drawerOpen={drawerOpen}
|
||||||
title={selectedNodeLabel}
|
setDrawerOpen={setDrawerOpen}
|
||||||
/>
|
title={selectedNodeLabel}
|
||||||
|
nodeData={nodeTypeMeta}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
|
@ -1,26 +1,38 @@
|
|||||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Node_Configuration_Sidebar.jsx
|
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Node_Configuration_Sidebar.jsx
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box, Typography, Tabs, Tab, TextField } from "@mui/material";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, title }) {
|
export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, title, nodeData }) {
|
||||||
return (
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
<>
|
const handleTabChange = (_, newValue) => setActiveTab(newValue);
|
||||||
{/* Dim overlay when drawer is open */}
|
|
||||||
{drawerOpen && (
|
const renderConfigFields = () => {
|
||||||
<Box
|
const config = nodeData?.config || [];
|
||||||
onClick={() => setDrawerOpen(false)}
|
return config.map((field, index) => (
|
||||||
sx={{
|
<Box key={index} sx={{ mb: 2 }}>
|
||||||
position: "absolute",
|
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
|
||||||
top: 0,
|
{field.label || field.key}
|
||||||
left: 0,
|
</Typography>
|
||||||
right: 0,
|
<TextField
|
||||||
bottom: 0,
|
variant="outlined"
|
||||||
backgroundColor: "rgba(0,0,0,0.4)",
|
size="small"
|
||||||
zIndex: 10
|
fullWidth
|
||||||
|
value={nodeData?.[field.key] || ""}
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
backgroundColor: "#1e1e1e",
|
||||||
|
color: "#ccc",
|
||||||
|
"& fieldset": { borderColor: "#444" },
|
||||||
|
"&:hover fieldset": { borderColor: "#666" }
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Box>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
{/* Right-Side Node Configuration Panel */}
|
return (
|
||||||
|
<>
|
||||||
<Box
|
<Box
|
||||||
onClick={() => setDrawerOpen(false)}
|
onClick={() => setDrawerOpen(false)}
|
||||||
sx={{
|
sx={{
|
||||||
@ -37,7 +49,6 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Animated right drawer panel */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@ -56,27 +67,32 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Title bar section */}
|
<Box sx={{ backgroundColor: "#232323", borderBottom: "1px solid #333" }}>
|
||||||
<Box
|
<Box sx={{ padding: "12px 16px" }}>
|
||||||
sx={{
|
<Typography variant="h7" sx={{ color: "#0475c2", fontWeight: "bold" }}>
|
||||||
backgroundColor: "#232323",
|
{"Edit " + (title || "Node")}
|
||||||
padding: "16px",
|
</Typography>
|
||||||
borderBottom: "1px solid #333"
|
</Box>
|
||||||
}}
|
<Tabs
|
||||||
>
|
value={activeTab}
|
||||||
<Typography variant="h6" sx={{ color: "#0475c2" }}>
|
onChange={handleTabChange}
|
||||||
{title || "Node Configuration Panel"}
|
variant="fullWidth"
|
||||||
</Typography>
|
textColor="inherit"
|
||||||
|
TabIndicatorProps={{ style: { backgroundColor: "#58a6ff" } }}
|
||||||
|
sx={{ borderTop: "1px solid #333", borderBottom: "1px solid #333", minHeight: "36px", height: "36px" }}
|
||||||
|
>
|
||||||
|
<Tab label="Config" sx={{ color: "#ccc", minHeight: "36px", height: "36px", textTransform: "none" }} />
|
||||||
|
<Tab label="Usage Docs" sx={{ color: "#ccc", minHeight: "36px", height: "36px", textTransform: "none" }} />
|
||||||
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<Box sx={{ padding: 2 }}>
|
<Box sx={{ padding: 2 }}>
|
||||||
<p style={{ fontSize: "0.85rem", color: "#aaa" }}>
|
{activeTab === 0 && renderConfigFields()}
|
||||||
This sidebar will be used to configure nodes in the future.
|
{activeTab === 1 && (
|
||||||
</p>
|
<Typography sx={{ whiteSpace: "pre-wrap", fontSize: "0.85rem", color: "#aaa" }}>
|
||||||
<p style={{ fontSize: "0.85rem", color: "#aaa" }}>
|
{nodeData?.usage_documentation || "No documentation provided for this node."}
|
||||||
The idea is that this area will allow for more node configuration controls to be dynamically-populated by the nodes to allow more complex node documentation and configuration.
|
</Typography>
|
||||||
</p>
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
@ -1,213 +1,138 @@
|
|||||||
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx
|
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/nodes/General Purpose/Node_Data.jsx
|
||||||
|
|
||||||
/**
|
|
||||||
* ============================================
|
|
||||||
* Borealis - Standard Live Data Node Template
|
|
||||||
* ============================================
|
|
||||||
*
|
|
||||||
* COMPONENT ROLE:
|
|
||||||
* This component defines a "data conduit" node that can accept input,
|
|
||||||
* process/override it with local logic, and forward the output on a timed basis.
|
|
||||||
*
|
|
||||||
* It serves as the core behavior model for other nodes that rely on live propagation.
|
|
||||||
* Clone and extend this file to create nodes with specialized logic.
|
|
||||||
*
|
|
||||||
* CORE CONCEPTS:
|
|
||||||
* - Uses a centralized shared memory (window.BorealisValueBus) for value sharing
|
|
||||||
* - Synchronizes with upstream nodes based on ReactFlow edges
|
|
||||||
* - Emits to downstream nodes by updating its own BorealisValueBus[id] value
|
|
||||||
* - Controlled by a global polling timer (window.BorealisUpdateRate)
|
|
||||||
*
|
|
||||||
* LIFECYCLE SUMMARY:
|
|
||||||
* - onMount: initializes logic loop and sync monitor
|
|
||||||
* - onUpdate: watches edges and global rate, reconfigures as needed
|
|
||||||
* - onUnmount: cleans up all timers
|
|
||||||
*
|
|
||||||
* DATA FLOW OVERVIEW:
|
|
||||||
* - INPUT: if a left-edge (target) is connected, disables manual editing
|
|
||||||
* - OUTPUT: propagates renderValue to downstream nodes via right-edge (source)
|
|
||||||
*
|
|
||||||
* STRUCTURE:
|
|
||||||
* - Node UI includes:
|
|
||||||
* * Label (from data.label)
|
|
||||||
* * Body description (from data.content)
|
|
||||||
* * Input textbox (disabled if input is connected)
|
|
||||||
*
|
|
||||||
* HOW TO EXTEND:
|
|
||||||
* - For transformations, insert logic into runNodeLogic()
|
|
||||||
* - To validate or restrict input types, modify handleManualInput()
|
|
||||||
* - For side-effects or external API calls, add hooks inside runNodeLogic()
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Handle, Position, useReactFlow, useStore } from "reactflow";
|
import { Handle, Position, useReactFlow, useStore } from "reactflow";
|
||||||
import { IconButton } from "@mui/material";
|
import { IconButton } from "@mui/material";
|
||||||
import { Settings as SettingsIcon } from "@mui/icons-material";
|
import { Settings as SettingsIcon } from "@mui/icons-material";
|
||||||
|
|
||||||
// Global Shared Bus for Node Data Propagation
|
if (!window.BorealisValueBus) window.BorealisValueBus = {};
|
||||||
if (!window.BorealisValueBus) {
|
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
|
||||||
window.BorealisValueBus = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global Update Rate (ms) for All Data Nodes
|
|
||||||
if (!window.BorealisUpdateRate) {
|
|
||||||
window.BorealisUpdateRate = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DataNode = ({ id, data }) => {
|
const DataNode = ({ id, data }) => {
|
||||||
const { setNodes } = useReactFlow();
|
const { setNodes } = useReactFlow();
|
||||||
const edges = useStore(state => state.edges);
|
const edges = useStore((state) => state.edges);
|
||||||
|
const [renderValue, setRenderValue] = useState(data?.value || "");
|
||||||
|
const valueRef = useRef(renderValue);
|
||||||
|
|
||||||
const [renderValue, setRenderValue] = useState(data?.value || "");
|
const handleManualInput = (e) => {
|
||||||
const valueRef = useRef(renderValue);
|
const newValue = e.target.value;
|
||||||
|
valueRef.current = newValue;
|
||||||
|
setRenderValue(newValue);
|
||||||
|
window.BorealisValueBus[id] = newValue;
|
||||||
|
setNodes((nds) =>
|
||||||
|
nds.map((n) =>
|
||||||
|
n.id === id ? { ...n, data: { ...n.data, value: newValue } } : n
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Manual input handler (disabled if connected to input)
|
useEffect(() => {
|
||||||
const handleManualInput = (e) => {
|
let currentRate = window.BorealisUpdateRate || 100;
|
||||||
const newValue = e.target.value;
|
let intervalId = null;
|
||||||
|
|
||||||
// TODO: Add input validation/sanitization here if needed
|
const runNodeLogic = () => {
|
||||||
valueRef.current = newValue;
|
const inputEdge = edges.find((e) => e?.target === id);
|
||||||
setRenderValue(newValue);
|
const hasInput = Boolean(inputEdge?.source);
|
||||||
|
|
||||||
window.BorealisValueBus[id] = newValue;
|
if (hasInput) {
|
||||||
|
const upstreamValue = window.BorealisValueBus[inputEdge.source] ?? "";
|
||||||
setNodes(nds =>
|
if (upstreamValue !== valueRef.current) {
|
||||||
nds.map(n =>
|
valueRef.current = upstreamValue;
|
||||||
n.id === id
|
setRenderValue(upstreamValue);
|
||||||
? { ...n, data: { ...n.data, value: newValue } }
|
window.BorealisValueBus[id] = upstreamValue;
|
||||||
: n
|
setNodes((nds) =>
|
||||||
|
nds.map((n) =>
|
||||||
|
n.id === id ? { ...n, data: { ...n.data, value: upstreamValue } } : n
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.BorealisValueBus[id] = valueRef.current;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
intervalId = setInterval(runNodeLogic, currentRate);
|
||||||
let currentRate = window.BorealisUpdateRate || 100;
|
const monitor = setInterval(() => {
|
||||||
let intervalId = null;
|
const newRate = window.BorealisUpdateRate || 100;
|
||||||
|
if (newRate !== currentRate) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
currentRate = newRate;
|
||||||
|
intervalId = setInterval(runNodeLogic, currentRate);
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
|
||||||
const runNodeLogic = () => {
|
return () => {
|
||||||
const inputEdge = edges.find(e => e?.target === id);
|
clearInterval(intervalId);
|
||||||
const hasInput = Boolean(inputEdge);
|
clearInterval(monitor);
|
||||||
|
};
|
||||||
|
}, [id, setNodes, edges]);
|
||||||
|
|
||||||
if (hasInput && inputEdge.source) {
|
const inputEdge = edges.find((e) => e?.target === id);
|
||||||
const upstreamValue = window.BorealisValueBus[inputEdge.source] ?? "";
|
const hasInput = Boolean(inputEdge);
|
||||||
|
|
||||||
// TODO: Insert custom transform logic here (e.g., parseInt, apply formula)
|
return (
|
||||||
|
<div className="borealis-node">
|
||||||
if (upstreamValue !== valueRef.current) {
|
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
||||||
valueRef.current = upstreamValue;
|
<div className="borealis-node-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
setRenderValue(upstreamValue);
|
<span>{data?.label || "Data Node"}</span>
|
||||||
window.BorealisValueBus[id] = upstreamValue;
|
<IconButton
|
||||||
|
size="small"
|
||||||
setNodes(nds =>
|
onClick={() =>
|
||||||
nds.map(n =>
|
window.BorealisOpenDrawer &&
|
||||||
n.id === id
|
window.BorealisOpenDrawer(data?.label || "Unknown Node", data)
|
||||||
? { ...n, data: { ...n.data, value: upstreamValue } }
|
}
|
||||||
: n
|
sx={{ padding: 0, marginRight: "-3px", color: "#58a6ff", width: "20px", height: "20px" }}
|
||||||
)
|
>
|
||||||
);
|
<SettingsIcon sx={{ fontSize: "16px" }} />
|
||||||
}
|
</IconButton>
|
||||||
} else {
|
</div>
|
||||||
// OUTPUT BROADCAST: emits to downstream via shared memory
|
<div className="borealis-node-content">
|
||||||
window.BorealisValueBus[id] = valueRef.current;
|
<div style={{ marginBottom: "8px", fontSize: "9px", color: "#ccc" }}>{data?.content}</div>
|
||||||
}
|
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>Value:</label>
|
||||||
};
|
<input
|
||||||
|
type="text"
|
||||||
const startInterval = () => {
|
value={renderValue}
|
||||||
intervalId = setInterval(runNodeLogic, currentRate);
|
onChange={handleManualInput}
|
||||||
};
|
disabled={hasInput}
|
||||||
|
style={{
|
||||||
startInterval();
|
fontSize: "9px",
|
||||||
|
padding: "4px",
|
||||||
// Monitor for global update rate changes
|
background: hasInput ? "#2a2a2a" : "#1e1e1e",
|
||||||
const monitor = setInterval(() => {
|
color: "#ccc",
|
||||||
const newRate = window.BorealisUpdateRate || 100;
|
border: "1px solid #444",
|
||||||
if (newRate !== currentRate) {
|
borderRadius: "2px",
|
||||||
currentRate = newRate;
|
width: "100%"
|
||||||
clearInterval(intervalId);
|
}}
|
||||||
startInterval();
|
/>
|
||||||
}
|
</div>
|
||||||
}, 250);
|
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||||
|
</div>
|
||||||
return () => {
|
);
|
||||||
clearInterval(intervalId);
|
|
||||||
clearInterval(monitor);
|
|
||||||
};
|
|
||||||
}, [id, setNodes, edges]);
|
|
||||||
|
|
||||||
const inputEdge = edges.find(e => e?.target === id);
|
|
||||||
const hasInput = Boolean(inputEdge);
|
|
||||||
const upstreamId = inputEdge?.source || "";
|
|
||||||
const upstreamValue = window.BorealisValueBus[upstreamId] || "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="borealis-node">
|
|
||||||
<Handle type="target" position={Position.Left} className="borealis-handle" />
|
|
||||||
|
|
||||||
<div className="borealis-node-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
||||||
<span>{data?.label || "Data Node"}</span>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() =>
|
|
||||||
window.BorealisOpenDrawer &&
|
|
||||||
window.BorealisOpenDrawer(data?.label || "Unknown Node")
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
padding: "0px",
|
|
||||||
marginRight: "-3px",
|
|
||||||
color: "#58a6ff",
|
|
||||||
fontSize: "14px", // affects inner icon when no size prop
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
minWidth: "20px"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SettingsIcon sx={{ fontSize: "16px" }} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="borealis-node-content">
|
|
||||||
{/* Description visible in node body */}
|
|
||||||
<div style={{ marginBottom: "8px", fontSize: "9px", color: "#ccc" }}>
|
|
||||||
{data?.content || "Foundational node for live value propagation."}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>
|
|
||||||
Value:
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={renderValue}
|
|
||||||
onChange={handleManualInput}
|
|
||||||
disabled={hasInput}
|
|
||||||
style={{
|
|
||||||
fontSize: "9px",
|
|
||||||
padding: "4px",
|
|
||||||
background: hasInput ? "#2a2a2a" : "#1e1e1e",
|
|
||||||
color: "#ccc",
|
|
||||||
border: "1px solid #444",
|
|
||||||
borderRadius: "2px",
|
|
||||||
width: "100%"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
type: "DataNode", // REQUIRED: unique identifier for the node type
|
type: "DataNode",
|
||||||
label: "String / Number",
|
label: "String / Number",
|
||||||
description: `
|
description: "Foundational node for live value propagation.\n\n- Accepts input or manual value\n- Pushes downstream\n- Uses shared memory",
|
||||||
Foundational Data Node
|
content: "Store a String or Number",
|
||||||
|
component: DataNode,
|
||||||
|
config: [
|
||||||
|
{ key: "value", label: "Initial Value", type: "text" }
|
||||||
|
],
|
||||||
|
usage_documentation: `
|
||||||
|
### DataNode Usage
|
||||||
|
|
||||||
- Accepts input from another node
|
This node acts as a live data emitter. When connected to an upstream source, it inherits its value. Otherwise, it accepts user-defined input.
|
||||||
- If no input is connected, allows user-defined value
|
|
||||||
- Pushes value to downstream nodes every X ms
|
- **Use Cases**:
|
||||||
- Uses BorealisValueBus to communicate with other nodes
|
- Static constants
|
||||||
`.trim(),
|
- Pass-through conduit
|
||||||
content: "Store a String or Number",
|
- Manual value input
|
||||||
component: DataNode
|
|
||||||
|
- **Behavior**:
|
||||||
|
- Automatically updates on interval
|
||||||
|
- Emits data through BorealisValueBus
|
||||||
|
|
||||||
|
Ensure no input edge if manual input is required.
|
||||||
|
`.trim()
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user