Modularized Dialogs and Status Bar

This commit is contained in:
Nicole Rappe 2025-04-15 03:16:17 -06:00
parent e8f9894e05
commit ed7f7d1021
6 changed files with 214 additions and 291 deletions

View File

@ -9,7 +9,6 @@
"dependencies": {
"@mui/material": "7.0.2",
"@mui/icons-material": "7.0.2",
"@mui/x-tree-view": "7.28.1",
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"react-resizable": "3.0.5",

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -19,14 +19,7 @@ import React, {
Button,
CssBaseline,
ThemeProvider,
createTheme,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Divider,
TextField
createTheme
} from "@mui/material";
// Material UI - Icons
@ -43,10 +36,17 @@ import React, {
// Styles
import "reactflow/dist/style.css";
// Import our new components
// Import Borealis Modules
import FlowTabs from "./Flow_Tabs";
import FlowEditor from "./Flow_Editor";
import NodeSidebar from "./Node_Sidebar";
import {
CloseAllDialog,
CreditsDialog,
RenameTabDialog,
TabContextMenu
} from "./Dialogs";
import StatusBar from "./Status_Bar";
// Global Node Update Timer Variable
if (!window.BorealisUpdateRate) {
@ -99,26 +99,16 @@ import React, {
]);
const [activeTabId, setActiveTabId] = useState("flow_1");
// About menu
const [aboutAnchorEl, setAboutAnchorEl] = useState(null);
const [creditsDialogOpen, setCreditsDialogOpen] = useState(false);
// Close all flows
const [confirmCloseOpen, setConfirmCloseOpen] = useState(false);
// Rename tab
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [renameTabId, setRenameTabId] = useState(null);
const [renameValue, setRenameValue] = useState("");
// Right-click tab menu
const [tabMenuAnchor, setTabMenuAnchor] = useState(null);
const [tabMenuTabId, setTabMenuTabId] = useState(null);
// File input ref (for imports on older browsers)
const fileInputRef = useRef(null);
// Setup callbacks to update nodes/edges in the currently active tab
const handleSetNodes = useCallback(
(callbackOrArray, tId) => {
const targetId = tId || activeTabId;
@ -153,17 +143,13 @@ import React, {
[activeTabId]
);
// About menu
const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget);
const handleAboutMenuClose = () => setAboutAnchorEl(null);
// Credits
const openCreditsDialog = () => {
handleAboutMenuClose();
setCreditsDialogOpen(true);
};
// Close all dialog
const handleOpenCloseAllDialog = () => setConfirmCloseOpen(true);
const handleCloseDialog = () => setConfirmCloseOpen(false);
const handleConfirmCloseAll = () => {
@ -179,7 +165,6 @@ import React, {
setConfirmCloseOpen(false);
};
// Create new tab
const createNewTab = () => {
const nextIndex = tabs.length + 1;
const newId = "flow_" + nextIndex;
@ -195,23 +180,21 @@ import React, {
setActiveTabId(newId);
};
// Handle user clicking on a tab
const handleTabChange = (newActiveTabId) => {
setActiveTabId(newActiveTabId);
};
// Right-click tab menu
const handleTabRightClick = (evt, tabId) => {
evt.preventDefault();
setTabMenuAnchor({ x: evt.clientX, y: evt.clientY });
setTabMenuTabId(tabId);
};
const handleCloseTabMenu = () => {
setTabMenuAnchor(null);
setTabMenuTabId(null);
};
// Rename / close tab
const handleRenameTab = () => {
setRenameDialogOpen(true);
setRenameTabId(tabMenuTabId);
@ -219,6 +202,7 @@ import React, {
setRenameValue(t ? t.tab_name : "");
handleCloseTabMenu();
};
const handleCloseTab = () => {
setTabs((old) => {
const idx = old.findIndex((t) => t.id === tabMenuTabId);
@ -227,11 +211,9 @@ import React, {
const newList = [...old];
newList.splice(idx, 1);
// If we closed the current tab, pick a new active tab
if (tabMenuTabId === activeTabId && newList.length > 0) {
setActiveTabId(newList[0].id);
} else if (newList.length === 0) {
// If we closed the only tab, create a fresh one
newList.push({
id: "flow_1",
tab_name: "Flow 1",
@ -260,12 +242,10 @@ import React, {
setRenameDialogOpen(false);
};
// Export current tab
const handleExportFlow = async () => {
const activeTab = tabs.find((x) => x.id === activeTabId);
if (!activeTab) return;
// Build JSON data from the active tab
const data = JSON.stringify(
{
nodes: activeTab.nodes,
@ -276,15 +256,9 @@ import React, {
2
);
const blob = new Blob([data], { type: "application/json" });
// Suggested filename based on the tab name
// e.g. "Nicole Work Flow" => "nicole_work_flow_workflow.json"
const sanitizedTabName = activeTab.tab_name
.replace(/\s+/g, "_")
.toLowerCase();
const sanitizedTabName = activeTab.tab_name.replace(/\s+/g, "_").toLowerCase();
const suggestedFilename = sanitizedTabName + "_workflow.json";
// Check if showSaveFilePicker is available (Chrome/Edge)
if (window.showSaveFilePicker) {
try {
const fileHandle = await window.showSaveFilePicker({
@ -304,21 +278,17 @@ import React, {
console.error("Save cancelled or failed:", err);
}
} else {
// Fallback for browsers like Firefox
// (Relies on browser settings to ask user where to save)
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = suggestedFilename; // e.g. nicole_work_flow_workflow.json
a.download = suggestedFilename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
// Cleanup
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
}
};
// Import flow -> new tab
const handleImportFlow = async () => {
if (window.showOpenFilePicker) {
try {
@ -349,12 +319,10 @@ import React, {
console.error("Import cancelled or failed:", err);
}
} else {
// Fallback for older browsers
fileInputRef.current?.click();
}
};
// Fallback <input> import
const handleFileInputChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
@ -382,35 +350,11 @@ import React, {
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Box
sx={{
width: "100vw",
height: "100vh",
display: "flex",
flexDirection: "column",
overflow: "hidden"
}}
>
<Box sx={{ width: "100vw", height: "100vh", display: "flex", flexDirection: "column", overflow: "hidden" }}>
<AppBar position="static" sx={{ bgcolor: "#16191d" }}>
<Toolbar sx={{ minHeight: "36px" }}>
{/* Logo */}
<Box
component="img"
src="/Borealis_Logo_Full.png"
alt="Borealis Logo"
sx={{
height: "52px",
marginRight: "8px"
}}
/>
<Typography
variant="h6"
sx={{ flexGrow: 1, fontSize: "1rem" }}
>
{/* Additional Title/Info if desired */}
</Typography>
<Box component="img" src="/Borealis_Logo_Full.png" alt="Borealis Logo" sx={{ height: "52px", marginRight: "8px" }} />
<Typography variant="h6" sx={{ flexGrow: 1, fontSize: "1rem" }}></Typography>
<Button
color="inherit"
onClick={handleAboutMenuOpen}
@ -420,43 +364,18 @@ import React, {
>
About
</Button>
<Menu
anchorEl={aboutAnchorEl}
open={Boolean(aboutAnchorEl)}
onClose={handleAboutMenuClose}
>
<MenuItem
onClick={() => {
handleAboutMenuClose();
window.open("https://git.bunny-lab.io/Borealis", "_blank");
}}
>
<MergeTypeIcon
sx={{
fontSize: 18,
color: "#58a6ff",
mr: 1
}}
/>
Gitea Project
<Menu anchorEl={aboutAnchorEl} open={Boolean(aboutAnchorEl)} onClose={handleAboutMenuClose}>
<MenuItem onClick={() => { handleAboutMenuClose(); window.open("https://git.bunny-lab.io/Borealis", "_blank"); }}>
<MergeTypeIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} /> Gitea Project
</MenuItem>
<MenuItem onClick={openCreditsDialog}>
<PeopleIcon
sx={{
fontSize: 18,
color: "#58a6ff",
mr: 1
}}
/>
Credits
<PeopleIcon sx={{ fontSize: 18, color: "#58a6ff", mr: 1 }} /> Credits
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
<Box sx={{ display: "flex", flexGrow: 1, overflow: "hidden" }}>
{/* Sidebar */}
<NodeSidebar
categorizedNodes={categorizedNodes}
handleExportFlow={handleExportFlow}
@ -466,16 +385,7 @@ import React, {
onFileInputChange={handleFileInputChange}
/>
{/* Right content: tab bar + flow editors */}
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "hidden"
}}
>
{/* Tab bar */}
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1, overflow: "hidden" }}>
<FlowTabs
tabs={tabs}
activeTabId={activeTabId}
@ -484,7 +394,6 @@ import React, {
onTabRightClick={handleTabRightClick}
/>
{/* The flow editors themselves */}
<Box sx={{ flexGrow: 1, position: "relative" }}>
{tabs.map((tab) => (
<Box
@ -514,188 +423,28 @@ import React, {
</Box>
</Box>
{/* Bottom status bar */}
<Box
component="footer"
sx={{
bgcolor: "#1e1e1e",
color: "white",
px: 2,
py: 1,
display: "flex",
alignItems: "center",
gap: 2
}}
>
<b>Nodes</b>: <span id="nodeCount">0</span>
<Divider
orientation="vertical"
flexItem
sx={{ borderColor: "#444" }}
/>
<b>Update Rate (ms):</b>
<input
id="updateRateInput"
type="number"
min="50"
step="50"
defaultValue={window.BorealisUpdateRate}
style={{
width: "80px",
background: "#121212",
color: "#fff",
border: "1px solid #444",
borderRadius: "3px",
padding: "3px",
fontSize: "0.8rem"
}}
/>
<Button
variant="outlined"
size="small"
onClick={() => {
const val = parseInt(
document.getElementById("updateRateInput")?.value
);
if (!isNaN(val) && val >= 50) {
window.BorealisUpdateRate = val;
console.log("Global update rate set to", val + "ms");
} else {
alert("Please enter a valid number (min 50).");
}
}}
sx={{
color: "#58a6ff",
borderColor: "#58a6ff",
fontSize: "0.75rem",
textTransform: "none",
px: 1.5
}}
>
Apply Rate
</Button>
</Box>
<StatusBar />
</Box>
{/* Close All Dialog */}
<Dialog
<CloseAllDialog
open={confirmCloseOpen}
onClose={handleCloseDialog}
PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}
>
<DialogTitle>Close All Flow Tabs?</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: "#ccc" }}>
This will remove all existing flow tabs and create a fresh tab named Flow 1.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={handleCloseDialog}
sx={{ color: "#58a6ff" }}
>
Cancel
</Button>
<Button
onClick={handleConfirmCloseAll}
sx={{ color: "#ff4f4f" }}
>
Close All
</Button>
</DialogActions>
</Dialog>
{/* Credits */}
<Dialog
open={creditsDialogOpen}
onClose={() => setCreditsDialogOpen(false)}
PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}
>
<DialogTitle>Borealis Workflow Automation Tool</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: "#ccc" }}>
Designed by Nicole Rappe @ Bunny Lab
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => setCreditsDialogOpen(false)}
sx={{ color: "#58a6ff" }}
>
Close
</Button>
</DialogActions>
</Dialog>
{/* Tab Context Menu */}
<Menu
open={Boolean(tabMenuAnchor)}
onClose={handleCloseTabMenu}
anchorReference="anchorPosition"
anchorPosition={
tabMenuAnchor
? { top: tabMenuAnchor.y, left: tabMenuAnchor.x }
: undefined
}
PaperProps={{
sx: {
bgcolor: "#1e1e1e",
color: "#fff",
fontSize: "13px"
}
}}
>
<MenuItem onClick={handleRenameTab}>Rename</MenuItem>
<MenuItem onClick={handleCloseTab}>Close</MenuItem>
</Menu>
{/* Rename Tab Dialog */}
<Dialog
onConfirm={handleConfirmCloseAll}
/>
<CreditsDialog open={creditsDialogOpen} onClose={() => setCreditsDialogOpen(false)} />
<RenameTabDialog
open={renameDialogOpen}
onClose={() => setRenameDialogOpen(false)}
PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}
>
<DialogTitle>Rename Tab</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Tab Name"
fullWidth
variant="outlined"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#2a2a2a",
color: "#ccc",
"& fieldset": {
borderColor: "#444"
},
"&:hover fieldset": {
borderColor: "#666"
}
},
label: { color: "#aaa" },
mt: 1
}}
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => setRenameDialogOpen(false)}
sx={{ color: "#58a6ff" }}
>
Cancel
</Button>
<Button
onClick={handleRenameDialogSave}
sx={{ color: "#58a6ff" }}
>
Save
</Button>
</DialogActions>
</Dialog>
value={renameValue}
onChange={setRenameValue}
onCancel={() => setRenameDialogOpen(false)}
onSave={handleRenameDialogSave}
/>
<TabContextMenu
anchor={tabMenuAnchor}
onClose={handleCloseTabMenu}
onRename={handleRenameTab}
onCloseTab={handleCloseTab}
/>
</ThemeProvider>
);
}

105
Data/WebUI/src/Dialogs.jsx Normal file
View File

@ -0,0 +1,105 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Dialogs.jsx
import React from "react";
import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Button,
Menu,
MenuItem,
TextField
} from "@mui/material";
export function CloseAllDialog({ open, onClose, onConfirm }) {
return (
<Dialog open={open} onClose={onClose} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
<DialogTitle>Close All Flow Tabs?</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: "#ccc" }}>
This will remove all existing flow tabs and create a fresh tab named Flow 1.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} sx={{ color: "#58a6ff" }}>Cancel</Button>
<Button onClick={onConfirm} sx={{ color: "#ff4f4f" }}>Close All</Button>
</DialogActions>
</Dialog>
);
}
export function CreditsDialog({ open, onClose }) {
return (
<Dialog open={open} onClose={onClose} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
<DialogTitle>Borealis Workflow Automation Tool</DialogTitle>
<DialogContent>
<DialogContentText sx={{ color: "#ccc" }}>
Designed by Nicole Rappe @ Bunny Lab
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} sx={{ color: "#58a6ff" }}>Close</Button>
</DialogActions>
</Dialog>
);
}
export function RenameTabDialog({ open, value, onChange, onCancel, onSave }) {
return (
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
<DialogTitle>Rename Tab</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label="Tab Name"
fullWidth
variant="outlined"
value={value}
onChange={(e) => onChange(e.target.value)}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#2a2a2a",
color: "#ccc",
"& fieldset": {
borderColor: "#444"
},
"&:hover fieldset": {
borderColor: "#666"
}
},
label: { color: "#aaa" },
mt: 1
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={onCancel} sx={{ color: "#58a6ff" }}>Cancel</Button>
<Button onClick={onSave} sx={{ color: "#58a6ff" }}>Save</Button>
</DialogActions>
</Dialog>
);
}
export function TabContextMenu({ anchor, onClose, onRename, onCloseTab }) {
return (
<Menu
open={Boolean(anchor)}
onClose={onClose}
anchorReference="anchorPosition"
anchorPosition={anchor ? { top: anchor.y, left: anchor.x } : undefined}
PaperProps={{
sx: {
bgcolor: "#1e1e1e",
color: "#fff",
fontSize: "13px"
}
}}
>
<MenuItem onClick={onRename}>Rename</MenuItem>
<MenuItem onClick={onCloseTab}>Close</MenuItem>
</Menu>
);
}

View File

@ -6,7 +6,6 @@ import {
AccordionSummary,
AccordionDetails,
Button,
Divider,
Tooltip,
Typography
} from "@mui/material";

View File

@ -0,0 +1,71 @@
////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/WebUI/src/Status_Bar.jsx
import React from "react";
import { Box, Button, Divider } from "@mui/material";
export default function StatusBar() {
const applyRate = () => {
const val = parseInt(
document.getElementById("updateRateInput")?.value
);
if (!isNaN(val) && val >= 50) {
window.BorealisUpdateRate = val;
console.log("Global update rate set to", val + "ms");
} else {
alert("Please enter a valid number (min 50).");
}
};
return (
<Box
component="footer"
sx={{
bgcolor: "#1e1e1e",
color: "white",
px: 2,
py: 1,
display: "flex",
alignItems: "center",
gap: 2
}}
>
<b>Nodes</b>: <span id="nodeCount">0</span>
<Divider
orientation="vertical"
flexItem
sx={{ borderColor: "#444" }}
/>
<b>Update Rate (ms):</b>
<input
id="updateRateInput"
type="number"
min="50"
step="50"
defaultValue={window.BorealisUpdateRate}
style={{
width: "80px",
background: "#121212",
color: "#fff",
border: "1px solid #444",
borderRadius: "3px",
padding: "3px",
fontSize: "0.8rem"
}}
/>
<Button
variant="outlined"
size="small"
onClick={applyRate}
sx={{
color: "#58a6ff",
borderColor: "#58a6ff",
fontSize: "0.75rem",
textTransform: "none",
px: 1.5
}}
>
Apply Rate
</Button>
</Box>
);
}