Componentized the UI by isolating elements into individual JSX modules.
This commit is contained in:
parent
00a2c83186
commit
e1a359169e
File diff suppressed because it is too large
Load Diff
214
Data/WebUI/src/Flow_Editor.jsx
Normal file
214
Data/WebUI/src/Flow_Editor.jsx
Normal file
@ -0,0 +1,214 @@
|
||||
// Flow_Editor.jsx
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
addEdge,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
useReactFlow
|
||||
} from "reactflow";
|
||||
import { Menu, MenuItem } from "@mui/material";
|
||||
import {
|
||||
Polyline as PolylineIcon,
|
||||
DeleteForever as DeleteForeverIcon
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import "reactflow/dist/style.css";
|
||||
import "./Borealis.css";
|
||||
|
||||
/**
|
||||
* Single flow editor component.
|
||||
*
|
||||
* Props:
|
||||
* - nodes
|
||||
* - edges
|
||||
* - setNodes
|
||||
* - setEdges
|
||||
* - nodeTypes
|
||||
* - categorizedNodes (used to find node meta info on drop)
|
||||
*/
|
||||
export default function FlowEditor({
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
setEdges,
|
||||
nodeTypes,
|
||||
categorizedNodes
|
||||
}) {
|
||||
const wrapperRef = useRef(null);
|
||||
const { project } = useReactFlow();
|
||||
const [contextMenu, setContextMenu] = useState(null);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const type = event.dataTransfer.getData("application/reactflow");
|
||||
if (!type) return;
|
||||
|
||||
const bounds = wrapperRef.current.getBoundingClientRect();
|
||||
const position = project({
|
||||
x: event.clientX - bounds.left,
|
||||
y: event.clientY - bounds.top
|
||||
});
|
||||
|
||||
const id = "node-" + Date.now();
|
||||
|
||||
// Find node definition in the categorizedNodes
|
||||
const nodeMeta = Object.values(categorizedNodes)
|
||||
.flat()
|
||||
.find((n) => n.type === type);
|
||||
|
||||
const newNode = {
|
||||
id: id,
|
||||
type: type,
|
||||
position: position,
|
||||
data: {
|
||||
label: nodeMeta?.label || type,
|
||||
content: nodeMeta?.content
|
||||
}
|
||||
};
|
||||
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
},
|
||||
[project, setNodes, categorizedNodes]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}, []);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params) =>
|
||||
setEdges((eds) =>
|
||||
addEdge(
|
||||
{
|
||||
...params,
|
||||
type: "smoothstep",
|
||||
animated: true,
|
||||
style: {
|
||||
strokeDasharray: "6 3",
|
||||
stroke: "#58a6ff"
|
||||
}
|
||||
},
|
||||
eds
|
||||
)
|
||||
),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const handleRightClick = (e, node) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({
|
||||
mouseX: e.clientX + 2,
|
||||
mouseY: e.clientY - 6,
|
||||
nodeId: node.id
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
if (contextMenu?.nodeId) {
|
||||
setEdges((eds) =>
|
||||
eds.filter(
|
||||
(e) =>
|
||||
e.source !== contextMenu.nodeId &&
|
||||
e.target !== contextMenu.nodeId
|
||||
)
|
||||
);
|
||||
}
|
||||
setContextMenu(null);
|
||||
};
|
||||
|
||||
const handleRemoveNode = () => {
|
||||
if (contextMenu?.nodeId) {
|
||||
setNodes((nds) => nds.filter((n) => n.id !== contextMenu.nodeId));
|
||||
setEdges((eds) =>
|
||||
eds.filter(
|
||||
(e) =>
|
||||
e.source !== contextMenu.nodeId &&
|
||||
e.target !== contextMenu.nodeId
|
||||
)
|
||||
);
|
||||
}
|
||||
setContextMenu(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const nodeCountEl = document.getElementById("nodeCount");
|
||||
if (nodeCountEl) {
|
||||
nodeCountEl.innerText = nodes.length;
|
||||
}
|
||||
}, [nodes]);
|
||||
|
||||
return (
|
||||
<div className="flow-editor-container" ref={wrapperRef}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeContextMenu={handleRightClick}
|
||||
defaultViewport={{ x: 0, y: 0, zoom: 1.5 }}
|
||||
edgeOptions={{
|
||||
type: "smoothstep",
|
||||
animated: true,
|
||||
style: {
|
||||
strokeDasharray: "6 3",
|
||||
stroke: "#58a6ff"
|
||||
}
|
||||
}}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background
|
||||
variant="lines"
|
||||
gap={65}
|
||||
size={1}
|
||||
color="rgba(255, 255, 255, 0.2)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
|
||||
{/* Right-click node menu */}
|
||||
<Menu
|
||||
open={Boolean(contextMenu)}
|
||||
onClose={() => setContextMenu(null)}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
contextMenu
|
||||
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
||||
: undefined
|
||||
}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: "#1e1e1e",
|
||||
color: "#fff",
|
||||
fontSize: "13px"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={handleDisconnect}>
|
||||
<PolylineIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} />
|
||||
Disconnect All Edges
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleRemoveNode}>
|
||||
<DeleteForeverIcon sx={{ fontSize: 18, color: "#ff4f4f", mr: 1 }} />
|
||||
Remove Node
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
98
Data/WebUI/src/Flow_Tabs.jsx
Normal file
98
Data/WebUI/src/Flow_Tabs.jsx
Normal file
@ -0,0 +1,98 @@
|
||||
// Flow_Tabs.jsx
|
||||
|
||||
import React from "react";
|
||||
import { Box, Tabs, Tab } from "@mui/material";
|
||||
import { Add as AddIcon } from "@mui/icons-material";
|
||||
|
||||
/**
|
||||
* Renders the tab bar (including the "add tab" button).
|
||||
*
|
||||
* Props:
|
||||
* - tabs (array of {id, tab_name, nodes, edges})
|
||||
* - activeTabId (string)
|
||||
* - onTabChange(newActiveTabId: string)
|
||||
* - onAddTab()
|
||||
* - onTabRightClick(evt: MouseEvent, tabId: string)
|
||||
*/
|
||||
export default function FlowTabs({
|
||||
tabs,
|
||||
activeTabId,
|
||||
onTabChange,
|
||||
onAddTab,
|
||||
onTabRightClick
|
||||
}) {
|
||||
// Determine the currently active tab index
|
||||
const activeIndex = (() => {
|
||||
const idx = tabs.findIndex((t) => t.id === activeTabId);
|
||||
return idx >= 0 ? idx : 0;
|
||||
})();
|
||||
|
||||
// Handle tab clicks
|
||||
const handleChange = (event, newValue) => {
|
||||
if (newValue === "__addtab__") {
|
||||
// The "plus" tab
|
||||
onAddTab();
|
||||
} else {
|
||||
// normal tab index
|
||||
const newTab = tabs[newValue];
|
||||
if (newTab) {
|
||||
onTabChange(newTab.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#232323",
|
||||
borderBottom: "1px solid #333",
|
||||
height: "36px"
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={activeIndex}
|
||||
onChange={handleChange}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
textColor="inherit"
|
||||
TabIndicatorProps={{
|
||||
style: { backgroundColor: "#58a6ff" }
|
||||
}}
|
||||
sx={{
|
||||
minHeight: "36px",
|
||||
height: "36px",
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
label={tab.tab_name}
|
||||
value={index}
|
||||
onContextMenu={(evt) => onTabRightClick(evt, tab.id)}
|
||||
sx={{
|
||||
minHeight: "36px",
|
||||
height: "36px",
|
||||
textTransform: "none",
|
||||
backgroundColor: tab.id === activeTabId ? "#2C2C2C" : "transparent",
|
||||
color: "#58a6ff"
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{/* The "plus" tab has a special value */}
|
||||
<Tab
|
||||
icon={<AddIcon />}
|
||||
value="__addtab__"
|
||||
sx={{
|
||||
minHeight: "36px",
|
||||
height: "36px",
|
||||
color: "#58a6ff",
|
||||
textTransform: "none"
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
}
|
265
Data/WebUI/src/Node_Sidebar.jsx
Normal file
265
Data/WebUI/src/Node_Sidebar.jsx
Normal file
@ -0,0 +1,265 @@
|
||||
// Node_Sidebar.jsx
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Button,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import {
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Save as SaveIcon,
|
||||
FileOpen as FileOpenIcon,
|
||||
DeleteForever as DeleteForeverIcon,
|
||||
DragIndicator as DragIndicatorIcon,
|
||||
Polyline as PolylineIcon
|
||||
} from "@mui/icons-material";
|
||||
|
||||
/**
|
||||
* Left sidebar for managing workflows and node categories.
|
||||
*
|
||||
* Props:
|
||||
* - categorizedNodes (object of arrays, e.g. { "Category": [{...}, ...], ... })
|
||||
* - handleExportFlow() => void
|
||||
* - handleImportFlow() => void
|
||||
* - handleOpenCloseAllDialog() => void
|
||||
* - fileInputRef (ref to hidden file input)
|
||||
* - onFileInputChange(event) => void
|
||||
*/
|
||||
export default function NodeSidebar({
|
||||
categorizedNodes,
|
||||
handleExportFlow,
|
||||
handleImportFlow,
|
||||
handleOpenCloseAllDialog,
|
||||
fileInputRef,
|
||||
onFileInputChange
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 320,
|
||||
backgroundColor: "#121212",
|
||||
borderRight: "1px solid #333",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
>
|
||||
<Accordion
|
||||
defaultExpanded
|
||||
square
|
||||
disableGutters
|
||||
sx={{
|
||||
"&:before": { display: "none" },
|
||||
margin: 0,
|
||||
border: 0
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
sx={{
|
||||
backgroundColor: "#2c2c2c",
|
||||
minHeight: "36px",
|
||||
"& .MuiAccordionSummary-content": {
|
||||
margin: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
align="left"
|
||||
sx={{
|
||||
fontSize: "0.9rem",
|
||||
color: "#0475c2"
|
||||
}}
|
||||
>
|
||||
<b>Workflows</b>
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
|
||||
<Button
|
||||
fullWidth
|
||||
startIcon={<SaveIcon />}
|
||||
sx={{
|
||||
color: "#ccc",
|
||||
backgroundColor: "#232323",
|
||||
justifyContent: "flex-start",
|
||||
pl: 2,
|
||||
fontSize: "0.9rem",
|
||||
textTransform: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: "#2a2a2a"
|
||||
}
|
||||
}}
|
||||
onClick={handleExportFlow}
|
||||
>
|
||||
Export Current Flow
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
startIcon={<FileOpenIcon />}
|
||||
sx={{
|
||||
color: "#ccc",
|
||||
backgroundColor: "#232323",
|
||||
justifyContent: "flex-start",
|
||||
pl: 2,
|
||||
fontSize: "0.9rem",
|
||||
textTransform: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: "#2a2a2a"
|
||||
}
|
||||
}}
|
||||
onClick={handleImportFlow}
|
||||
>
|
||||
Import Flow
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
startIcon={<DeleteForeverIcon />}
|
||||
sx={{
|
||||
color: "#ccc",
|
||||
backgroundColor: "#232323",
|
||||
justifyContent: "flex-start",
|
||||
pl: 2,
|
||||
fontSize: "0.9rem",
|
||||
textTransform: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: "#2a2a2a"
|
||||
}
|
||||
}}
|
||||
onClick={handleOpenCloseAllDialog}
|
||||
>
|
||||
Close All Flows
|
||||
</Button>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Accordion
|
||||
defaultExpanded
|
||||
square
|
||||
disableGutters
|
||||
sx={{
|
||||
"&:before": { display: "none" },
|
||||
margin: 0,
|
||||
border: 0
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
sx={{
|
||||
backgroundColor: "#2c2c2c",
|
||||
minHeight: "36px",
|
||||
"& .MuiAccordionSummary-content": {
|
||||
margin: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
align="left"
|
||||
sx={{
|
||||
fontSize: "0.9rem",
|
||||
color: "#0475c2"
|
||||
}}
|
||||
>
|
||||
<b>Nodes</b>
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0 }}>
|
||||
{Object.entries(categorizedNodes).map(([category, items]) => (
|
||||
<div key={category} style={{ marginBottom: 0, backgroundColor: "#232323" }}>
|
||||
<Divider
|
||||
sx={{
|
||||
bgcolor: "transparent",
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
borderColor: "#333"
|
||||
}}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: "#888",
|
||||
fontSize: "0.75rem"
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</Typography>
|
||||
</Divider>
|
||||
{items.map((nodeDef) => (
|
||||
<Tooltip
|
||||
key={`${category}-${nodeDef.type}`}
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
wordWrap: "break-word",
|
||||
maxWidth: 220
|
||||
}}
|
||||
>
|
||||
{nodeDef.description || "Drag & Drop into Editor"}
|
||||
</span>
|
||||
}
|
||||
placement="right"
|
||||
arrow
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{
|
||||
color: "#ccc",
|
||||
backgroundColor: "#232323",
|
||||
justifyContent: "space-between",
|
||||
pl: 2,
|
||||
pr: 1,
|
||||
fontSize: "0.9rem",
|
||||
textTransform: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: "#2a2a2a"
|
||||
}
|
||||
}}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
event.dataTransfer.setData("application/reactflow", nodeDef.type);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
}}
|
||||
startIcon={
|
||||
<DragIndicatorIcon
|
||||
sx={{
|
||||
color: "#666",
|
||||
fontSize: 18
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span style={{ flexGrow: 1, textAlign: "left" }}>
|
||||
{nodeDef.label}
|
||||
</span>
|
||||
<PolylineIcon
|
||||
sx={{
|
||||
color: "#58a6ff",
|
||||
fontSize: 18,
|
||||
ml: 1
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Hidden file input fallback for older browsers */}
|
||||
<input
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
style={{ display: "none" }}
|
||||
ref={fileInputRef}
|
||||
onChange={onFileInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user