Milestone 2
This commit is contained in:
parent
8e1d7d0de7
commit
6f6fc96d17
@ -53,8 +53,7 @@ export default function FlowEditor({
|
||||
return () => {
|
||||
delete window.BorealisOpenDrawer;
|
||||
};
|
||||
}, []);
|
||||
|
||||
}, [nodes]);
|
||||
|
||||
const wrapperRef = useRef(null);
|
||||
const { project } = useReactFlow();
|
||||
@ -353,18 +352,26 @@ export default function FlowEditor({
|
||||
if (nodeCountEl) nodeCountEl.innerText = nodes.length;
|
||||
}, [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 (
|
||||
<div
|
||||
className="flow-editor-container"
|
||||
ref={wrapperRef}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
|
||||
<NodeConfigurationSidebar
|
||||
drawerOpen={drawerOpen}
|
||||
setDrawerOpen={setDrawerOpen}
|
||||
title={selectedNodeLabel}
|
||||
nodeData={nodeTypeMeta}
|
||||
/>
|
||||
|
||||
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
|
@ -1,26 +1,38 @@
|
||||
////////// 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 }) {
|
||||
return (
|
||||
<>
|
||||
{/* Dim overlay when drawer is open */}
|
||||
{drawerOpen && (
|
||||
<Box
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
zIndex: 10
|
||||
export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, title, nodeData }) {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const handleTabChange = (_, newValue) => setActiveTab(newValue);
|
||||
|
||||
const renderConfigFields = () => {
|
||||
const config = nodeData?.config || [];
|
||||
return config.map((field, index) => (
|
||||
<Box key={index} sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: "#ccc", mb: 0.5 }}>
|
||||
{field.label || field.key}
|
||||
</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
size="small"
|
||||
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
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
sx={{
|
||||
@ -37,7 +49,6 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Animated right drawer panel */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
@ -56,27 +67,32 @@ export default function NodeConfigurationSidebar({ drawerOpen, setDrawerOpen, ti
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Title bar section */}
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#232323",
|
||||
padding: "16px",
|
||||
borderBottom: "1px solid #333"
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ color: "#0475c2" }}>
|
||||
{title || "Node Configuration Panel"}
|
||||
<Box sx={{ backgroundColor: "#232323", borderBottom: "1px solid #333" }}>
|
||||
<Box sx={{ padding: "12px 16px" }}>
|
||||
<Typography variant="h7" sx={{ color: "#0475c2", fontWeight: "bold" }}>
|
||||
{"Edit " + (title || "Node")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
variant="fullWidth"
|
||||
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>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{ padding: 2 }}>
|
||||
<p style={{ fontSize: "0.85rem", color: "#aaa" }}>
|
||||
This sidebar will be used to configure nodes in the future.
|
||||
</p>
|
||||
<p style={{ fontSize: "0.85rem", color: "#aaa" }}>
|
||||
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.
|
||||
</p>
|
||||
{activeTab === 0 && renderConfigFields()}
|
||||
{activeTab === 1 && (
|
||||
<Typography sx={{ whiteSpace: "pre-wrap", fontSize: "0.85rem", color: "#aaa" }}>
|
||||
{nodeData?.usage_documentation || "No documentation provided for this node."}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
|
@ -1,81 +1,27 @@
|
||||
////////// 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 { Handle, Position, useReactFlow, useStore } from "reactflow";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { Settings as SettingsIcon } from "@mui/icons-material";
|
||||
|
||||
// Global Shared Bus for Node Data Propagation
|
||||
if (!window.BorealisValueBus) {
|
||||
window.BorealisValueBus = {};
|
||||
}
|
||||
|
||||
// Global Update Rate (ms) for All Data Nodes
|
||||
if (!window.BorealisUpdateRate) {
|
||||
window.BorealisUpdateRate = 100;
|
||||
}
|
||||
if (!window.BorealisValueBus) window.BorealisValueBus = {};
|
||||
if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100;
|
||||
|
||||
const DataNode = ({ id, data }) => {
|
||||
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);
|
||||
|
||||
// Manual input handler (disabled if connected to input)
|
||||
const handleManualInput = (e) => {
|
||||
const newValue = e.target.value;
|
||||
|
||||
// TODO: Add input validation/sanitization here if needed
|
||||
valueRef.current = newValue;
|
||||
setRenderValue(newValue);
|
||||
|
||||
window.BorealisValueBus[id] = newValue;
|
||||
|
||||
setNodes(nds =>
|
||||
nds.map(n =>
|
||||
n.id === id
|
||||
? { ...n, data: { ...n.data, value: newValue } }
|
||||
: n
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, value: newValue } } : n
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -85,46 +31,33 @@ const DataNode = ({ id, data }) => {
|
||||
let intervalId = null;
|
||||
|
||||
const runNodeLogic = () => {
|
||||
const inputEdge = edges.find(e => e?.target === id);
|
||||
const hasInput = Boolean(inputEdge);
|
||||
const inputEdge = edges.find((e) => e?.target === id);
|
||||
const hasInput = Boolean(inputEdge?.source);
|
||||
|
||||
if (hasInput && inputEdge.source) {
|
||||
if (hasInput) {
|
||||
const upstreamValue = window.BorealisValueBus[inputEdge.source] ?? "";
|
||||
|
||||
// TODO: Insert custom transform logic here (e.g., parseInt, apply formula)
|
||||
|
||||
if (upstreamValue !== valueRef.current) {
|
||||
valueRef.current = upstreamValue;
|
||||
setRenderValue(upstreamValue);
|
||||
window.BorealisValueBus[id] = upstreamValue;
|
||||
|
||||
setNodes(nds =>
|
||||
nds.map(n =>
|
||||
n.id === id
|
||||
? { ...n, data: { ...n.data, value: upstreamValue } }
|
||||
: n
|
||||
setNodes((nds) =>
|
||||
nds.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, value: upstreamValue } } : n
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// OUTPUT BROADCAST: emits to downstream via shared memory
|
||||
window.BorealisValueBus[id] = valueRef.current;
|
||||
}
|
||||
};
|
||||
|
||||
const startInterval = () => {
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
};
|
||||
|
||||
startInterval();
|
||||
|
||||
// Monitor for global update rate changes
|
||||
const monitor = setInterval(() => {
|
||||
const newRate = window.BorealisUpdateRate || 100;
|
||||
if (newRate !== currentRate) {
|
||||
currentRate = newRate;
|
||||
clearInterval(intervalId);
|
||||
startInterval();
|
||||
currentRate = newRate;
|
||||
intervalId = setInterval(runNodeLogic, currentRate);
|
||||
}
|
||||
}, 250);
|
||||
|
||||
@ -134,47 +67,28 @@ const DataNode = ({ id, data }) => {
|
||||
};
|
||||
}, [id, setNodes, edges]);
|
||||
|
||||
const inputEdge = edges.find(e => e?.target === id);
|
||||
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")
|
||||
window.BorealisOpenDrawer(data?.label || "Unknown Node", data)
|
||||
}
|
||||
sx={{
|
||||
padding: "0px",
|
||||
marginRight: "-3px",
|
||||
color: "#58a6ff",
|
||||
fontSize: "14px", // affects inner icon when no size prop
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
minWidth: "20px"
|
||||
}}
|
||||
sx={{ padding: 0, marginRight: "-3px", color: "#58a6ff", width: "20px", height: "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>
|
||||
<div style={{ marginBottom: "8px", fontSize: "9px", color: "#ccc" }}>{data?.content}</div>
|
||||
<label style={{ fontSize: "9px", display: "block", marginBottom: "4px" }}>Value:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={renderValue}
|
||||
@ -191,23 +105,34 @@ const DataNode = ({ id, data }) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Handle type="source" position={Position.Right} className="borealis-handle" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
type: "DataNode", // REQUIRED: unique identifier for the node type
|
||||
type: "DataNode",
|
||||
label: "String / Number",
|
||||
description: `
|
||||
Foundational Data Node
|
||||
|
||||
- Accepts input from another node
|
||||
- If no input is connected, allows user-defined value
|
||||
- Pushes value to downstream nodes every X ms
|
||||
- Uses BorealisValueBus to communicate with other nodes
|
||||
`.trim(),
|
||||
description: "Foundational node for live value propagation.\n\n- Accepts input or manual value\n- Pushes downstream\n- Uses shared memory",
|
||||
content: "Store a String or Number",
|
||||
component: DataNode
|
||||
component: DataNode,
|
||||
config: [
|
||||
{ key: "value", label: "Initial Value", type: "text" }
|
||||
],
|
||||
usage_documentation: `
|
||||
### DataNode Usage
|
||||
|
||||
This node acts as a live data emitter. When connected to an upstream source, it inherits its value. Otherwise, it accepts user-defined input.
|
||||
|
||||
- **Use Cases**:
|
||||
- Static constants
|
||||
- Pass-through conduit
|
||||
- Manual value input
|
||||
|
||||
- **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