mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 06:28:43 -06:00
Modifications to Navigation Sidebar
This commit is contained in:
@@ -5,7 +5,8 @@ import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef
|
||||
useRef,
|
||||
useMemo
|
||||
} from "react";
|
||||
|
||||
// Material UI - Components
|
||||
@@ -19,7 +20,21 @@ import {
|
||||
Button,
|
||||
CssBaseline,
|
||||
ThemeProvider,
|
||||
createTheme
|
||||
createTheme,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
Paper,
|
||||
Tooltip
|
||||
} from "@mui/material";
|
||||
|
||||
// Material UI - Icons
|
||||
@@ -27,7 +42,15 @@ import {
|
||||
KeyboardArrowDown as KeyboardArrowDownIcon,
|
||||
InfoOutlined as InfoOutlinedIcon,
|
||||
MergeType as MergeTypeIcon,
|
||||
People as PeopleIcon
|
||||
People as PeopleIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
PlayCircle as PlayCircleIcon,
|
||||
Devices as DevicesIcon,
|
||||
AutoAwesomeMosaic as WorkflowsIcon,
|
||||
Construction as JobsIcon,
|
||||
PeopleOutline as CommunityIcon,
|
||||
FilterAlt as FilterIcon,
|
||||
Groups as GroupsIcon
|
||||
} from "@mui/icons-material";
|
||||
|
||||
// React Flow
|
||||
@@ -61,7 +84,7 @@ if (!window.BorealisUpdateRate) {
|
||||
window.BorealisUpdateRate = 200;
|
||||
}
|
||||
|
||||
const modules = import.meta.glob('./nodes/**/*.jsx', { eager: true });
|
||||
const modules = import.meta.glob("./nodes/**/*.jsx", { eager: true });
|
||||
const nodeTypes = {};
|
||||
const categorizedNodes = {};
|
||||
|
||||
@@ -70,7 +93,7 @@ Object.entries(modules).forEach(([path, mod]) => {
|
||||
if (!comp) return;
|
||||
const { type, component } = comp;
|
||||
if (!type || !component) return;
|
||||
const parts = path.replace('./nodes/', '').split('/');
|
||||
const parts = path.replace("./nodes/", "").split("/");
|
||||
const category = parts[0];
|
||||
if (!categorizedNodes[category]) {
|
||||
categorizedNodes[category] = [];
|
||||
@@ -109,17 +132,176 @@ const darkTheme = createTheme({
|
||||
|
||||
const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|
||||
|
||||
// ---------- Utilities ----------
|
||||
function timeSince(tsSec) {
|
||||
if (!tsSec) return "unknown";
|
||||
const now = Date.now() / 1000;
|
||||
const s = Math.max(0, Math.floor(now - tsSec));
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m ${s % 60}s`;
|
||||
const h = Math.floor(m / 60);
|
||||
return `${h}h ${m % 60}m`;
|
||||
}
|
||||
|
||||
function statusFromHeartbeat(tsSec, offlineAfter = 15) {
|
||||
if (!tsSec) return "Offline";
|
||||
const now = Date.now() / 1000;
|
||||
return now - tsSec <= offlineAfter ? "Online" : "Offline";
|
||||
}
|
||||
|
||||
// ---------- Devices Table (sortable) ----------
|
||||
function DevicesTable() {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [orderBy, setOrderBy] = useState("status");
|
||||
const [order, setOrder] = useState("desc");
|
||||
|
||||
const fetchAgents = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/agents");
|
||||
const data = await res.json();
|
||||
const arr = Object.values(data || {}).map((a) => ({
|
||||
hostname: a.hostname || a.agent_id || "unknown",
|
||||
status: statusFromHeartbeat(a.last_seen),
|
||||
lastSeen: a.last_seen || 0,
|
||||
os: a.agent_operating_system || a.os || "-"
|
||||
}));
|
||||
setRows(arr);
|
||||
} catch (e) {
|
||||
console.warn("Failed to load agents:", e);
|
||||
setRows([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
const t = setInterval(fetchAgents, 5000);
|
||||
return () => clearInterval(t);
|
||||
}, [fetchAgents]);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const dir = order === "asc" ? 1 : -1;
|
||||
return [...rows].sort((a, b) => {
|
||||
const A = a[orderBy];
|
||||
const B = b[orderBy];
|
||||
if (orderBy === "lastSeen") return (A - B) * dir;
|
||||
return String(A).localeCompare(String(B)) * dir;
|
||||
});
|
||||
}, [rows, orderBy, order]);
|
||||
|
||||
const handleSort = (col) => {
|
||||
if (orderBy === col) setOrder(order === "asc" ? "desc" : "asc");
|
||||
else {
|
||||
setOrderBy(col);
|
||||
setOrder("asc");
|
||||
}
|
||||
};
|
||||
|
||||
const statusColor = (s) => (s === "Online" ? "#00d18c" : "#ff4f4f");
|
||||
|
||||
return (
|
||||
<Paper sx={{ m: 2, p: 0, bgcolor: "#1e1e1e" }} elevation={2}>
|
||||
<Box sx={{ p: 2, pb: 1 }}>
|
||||
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>
|
||||
Devices
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
||||
Connected agents and their recent heartbeat.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Table size="small" sx={{ minWidth: 680 }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sortDirection={orderBy === "status" ? order : false}>
|
||||
<TableSortLabel
|
||||
active={orderBy === "status"}
|
||||
direction={orderBy === "status" ? order : "asc"}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
Status
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell sortDirection={orderBy === "hostname" ? order : false}>
|
||||
<TableSortLabel
|
||||
active={orderBy === "hostname"}
|
||||
direction={orderBy === "hostname" ? order : "asc"}
|
||||
onClick={() => handleSort("hostname")}
|
||||
>
|
||||
Hostname
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell sortDirection={orderBy === "lastSeen" ? order : false}>
|
||||
<TableSortLabel
|
||||
active={orderBy === "lastSeen"}
|
||||
direction={orderBy === "lastSeen" ? order : "asc"}
|
||||
onClick={() => handleSort("lastSeen")}
|
||||
>
|
||||
Last Heartbeat
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
<TableCell sortDirection={orderBy === "os" ? order : false}>
|
||||
<TableSortLabel
|
||||
active={orderBy === "os"}
|
||||
direction={orderBy === "os" ? order : "asc"}
|
||||
onClick={() => handleSort("os")}
|
||||
>
|
||||
OS
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sorted.map((r, i) => (
|
||||
<TableRow key={i} hover>
|
||||
<TableCell>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 10,
|
||||
background: statusColor(r.status),
|
||||
marginRight: 8,
|
||||
verticalAlign: "middle"
|
||||
}}
|
||||
/>
|
||||
{r.status}
|
||||
</TableCell>
|
||||
<TableCell>{r.hostname}</TableCell>
|
||||
<TableCell>{timeSince(r.lastSeen)}</TableCell>
|
||||
<TableCell>{r.os}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{sorted.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} sx={{ color: "#888" }}>
|
||||
No agents connected.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Main App ----------
|
||||
export default function App() {
|
||||
const [tabs, setTabs] = useState([
|
||||
{
|
||||
id: "flow_1",
|
||||
tab_name: "Flow 1",
|
||||
nodes: [],
|
||||
edges: []
|
||||
}
|
||||
{ id: "flow_1", tab_name: "Flow 1", nodes: [], edges: [] }
|
||||
]);
|
||||
const [activeTabId, setActiveTabId] = useState("flow_1");
|
||||
|
||||
// navigation state
|
||||
const [currentPage, setCurrentPage] = useState("devices");
|
||||
const [navCollapsed, setNavCollapsed] = useState(false);
|
||||
const [expandedNav, setExpandedNav] = useState({
|
||||
devices: true,
|
||||
filters: false,
|
||||
automation: true
|
||||
});
|
||||
|
||||
// dialogs / menus
|
||||
const [aboutAnchorEl, setAboutAnchorEl] = useState(null);
|
||||
const [creditsDialogOpen, setCreditsDialogOpen] = useState(false);
|
||||
const [confirmCloseOpen, setConfirmCloseOpen] = useState(false);
|
||||
@@ -130,6 +312,7 @@ export default function App() {
|
||||
const [tabMenuTabId, setTabMenuTabId] = useState(null);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// persist tabs
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (saved) {
|
||||
@@ -187,6 +370,7 @@ export default function App() {
|
||||
[activeTabId]
|
||||
);
|
||||
|
||||
// app bar menu
|
||||
const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget);
|
||||
const handleAboutMenuClose = () => setAboutAnchorEl(null);
|
||||
const openCreditsDialog = () => {
|
||||
@@ -194,17 +378,11 @@ export default function App() {
|
||||
setCreditsDialogOpen(true);
|
||||
};
|
||||
|
||||
// flow tab helpers...
|
||||
const handleOpenCloseAllDialog = () => setConfirmCloseOpen(true);
|
||||
const handleCloseDialog = () => setConfirmCloseOpen(false);
|
||||
const handleConfirmCloseAll = () => {
|
||||
setTabs([
|
||||
{
|
||||
id: "flow_1",
|
||||
tab_name: "Flow 1",
|
||||
nodes: [],
|
||||
edges: []
|
||||
}
|
||||
]);
|
||||
setTabs([{ id: "flow_1", tab_name: "Flow 1", nodes: [], edges: [] }]);
|
||||
setActiveTabId("flow_1");
|
||||
setConfirmCloseOpen(false);
|
||||
};
|
||||
@@ -214,14 +392,10 @@ export default function App() {
|
||||
const newId = "flow_" + nextIndex;
|
||||
setTabs((old) => [
|
||||
...old,
|
||||
{
|
||||
id: newId,
|
||||
tab_name: "Flow " + nextIndex,
|
||||
nodes: [],
|
||||
edges: []
|
||||
}
|
||||
{ id: newId, tab_name: "Flow " + nextIndex, nodes: [], edges: [] }
|
||||
]);
|
||||
setActiveTabId(newId);
|
||||
setCurrentPage("workflow-editor");
|
||||
};
|
||||
|
||||
const handleTabChange = (newActiveTabId) => {
|
||||
@@ -276,9 +450,7 @@ export default function App() {
|
||||
}
|
||||
setTabs((old) =>
|
||||
old.map((tab) =>
|
||||
tab.id === renameTabId
|
||||
? { ...tab, tab_name: renameValue }
|
||||
: tab
|
||||
tab.id === renameTabId ? { ...tab, tab_name: renameValue } : tab
|
||||
)
|
||||
);
|
||||
setRenameDialogOpen(false);
|
||||
@@ -303,12 +475,7 @@ export default function App() {
|
||||
try {
|
||||
const fileHandle = await window.showSaveFilePicker({
|
||||
suggestedName: suggestedFilename,
|
||||
types: [
|
||||
{
|
||||
description: "Workflow JSON File",
|
||||
accept: { "application/json": [".json"] }
|
||||
}
|
||||
]
|
||||
types: [{ description: "Workflow JSON File", accept: { "application/json": [".json"] } }]
|
||||
});
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(blob);
|
||||
@@ -332,12 +499,7 @@ export default function App() {
|
||||
if (window.showOpenFilePicker) {
|
||||
try {
|
||||
const [fileHandle] = await window.showOpenFilePicker({
|
||||
types: [
|
||||
{
|
||||
description: "Workflow JSON File",
|
||||
accept: { "application/json": [".json"] }
|
||||
}
|
||||
]
|
||||
types: [{ description: "Workflow JSON File", accept: { "application/json": [".json"] } }]
|
||||
});
|
||||
const file = await fileHandle.getFile();
|
||||
const text = await file.text();
|
||||
@@ -353,6 +515,7 @@ export default function App() {
|
||||
}
|
||||
]);
|
||||
setActiveTabId(newId);
|
||||
setCurrentPage("workflow-editor");
|
||||
} catch (err) {
|
||||
console.error("Import cancelled or failed:", err);
|
||||
}
|
||||
@@ -378,38 +541,16 @@ export default function App() {
|
||||
}
|
||||
]);
|
||||
setActiveTabId(newId);
|
||||
setCurrentPage("workflow-editor");
|
||||
} catch (err) {
|
||||
console.error("Failed to read file:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
<Box sx={{ width: "100vw", height: "100vh", display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<AppBar position="static" sx={{ bgcolor: "#16191d" }}>
|
||||
<Toolbar sx={{ minHeight: "36px" }}>
|
||||
<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}
|
||||
endIcon={<KeyboardArrowDownIcon />}
|
||||
startIcon={<InfoOutlinedIcon />}
|
||||
sx={{ height: "36px" }}
|
||||
>
|
||||
About
|
||||
</Button>
|
||||
<Menu anchorEl={aboutAnchorEl} open={Boolean(aboutAnchorEl)} onClose={handleAboutMenuClose}>
|
||||
<MenuItem onClick={() => { handleAboutMenuClose(); window.open("https://git.bunny-lab.io/bunny-lab/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
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
// ---------- Main Content ----------
|
||||
const renderMainContent = () => {
|
||||
if (currentPage === "workflow-editor") {
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexGrow: 1, overflow: "hidden" }}>
|
||||
<NodeSidebar
|
||||
categorizedNodes={categorizedNodes}
|
||||
@@ -456,13 +597,228 @@ export default function App() {
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (currentPage === "devices") {
|
||||
return <DevicesTable />;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography>Select a section from navigation.</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------- Nav helpers ----------
|
||||
const NavItem = ({ icon, label, pageKey, indent = 0, onClick }) => {
|
||||
const active = currentPage === pageKey;
|
||||
return (
|
||||
<ListItemButton
|
||||
onClick={onClick || (() => setCurrentPage(pageKey))}
|
||||
sx={{
|
||||
pl: indent ? 4 : 2,
|
||||
py: 1,
|
||||
color: "#ccc",
|
||||
position: "relative",
|
||||
bgcolor: active ? "#2a2a2a" : "transparent",
|
||||
"&:hover": { bgcolor: "#2c2c2c" }
|
||||
}}
|
||||
>
|
||||
{/* left accent when active */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: active ? 3 : 0,
|
||||
bgcolor: "#58a6ff",
|
||||
transition: "width 0.15s ease"
|
||||
}}
|
||||
/>
|
||||
{icon && <Box sx={{ mr: 1, display: "flex", alignItems: "center" }}>{icon}</Box>}
|
||||
<ListItemText primary={label} primaryTypographyProps={{ fontSize: "0.95rem" }} />
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
<Box sx={{ width: "100vw", height: "100vh", display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<AppBar position="static" sx={{ bgcolor: "#16191d" }}>
|
||||
<Toolbar sx={{ minHeight: "36px" }}>
|
||||
<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}
|
||||
endIcon={<KeyboardArrowDownIcon />}
|
||||
startIcon={<InfoOutlinedIcon />}
|
||||
sx={{ height: "36px" }}
|
||||
>
|
||||
About
|
||||
</Button>
|
||||
<Menu anchorEl={aboutAnchorEl} open={Boolean(aboutAnchorEl)} onClose={handleAboutMenuClose}>
|
||||
<MenuItem onClick={() => { handleAboutMenuClose(); window.open("https://git.bunny-lab.io/bunny-lab/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
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* Main area with new wider nav styled like Node Sidebar */}
|
||||
<Box sx={{ display: "flex", flexGrow: 1, overflow: "hidden" }}>
|
||||
{/* Navigation Sidebar */}
|
||||
<Box
|
||||
sx={{
|
||||
width: navCollapsed ? 48 : 300,
|
||||
bgcolor: "#121212",
|
||||
borderRight: "1px solid #333",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
transition: "width 0.2s ease"
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1, overflowY: "auto" }}>
|
||||
{!navCollapsed && (
|
||||
<>
|
||||
{/* Devices */}
|
||||
<Accordion
|
||||
expanded={expandedNav.devices}
|
||||
onChange={(_, e) => setExpandedNav((s) => ({ ...s, devices: e }))}
|
||||
square
|
||||
disableGutters
|
||||
sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }}
|
||||
>
|
||||
<Typography sx={{ fontSize: "0.9rem", color: "#0475c2" }}>
|
||||
<b>Devices</b>
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
|
||||
<NavItem icon={<DevicesIcon fontSize="small" />} label="Devices" pageKey="devices" />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Filters & Groups */}
|
||||
<Accordion
|
||||
expanded={expandedNav.filters}
|
||||
onChange={(_, e) => setExpandedNav((s) => ({ ...s, filters: e }))}
|
||||
square
|
||||
disableGutters
|
||||
sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }}
|
||||
>
|
||||
<Typography sx={{ fontSize: "0.9rem", color: "#0475c2" }}>
|
||||
<b>Filters & Groups</b>
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
|
||||
<NavItem icon={<FilterIcon fontSize="small" />} label="Filters" pageKey="filters" />
|
||||
<NavItem icon={<GroupsIcon fontSize="small" />} label="Groups" pageKey="groups" />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Automation */}
|
||||
<Accordion
|
||||
expanded={expandedNav.automation}
|
||||
onChange={(_, e) => setExpandedNav((s) => ({ ...s, automation: e }))}
|
||||
square
|
||||
disableGutters
|
||||
sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }}
|
||||
>
|
||||
<Typography sx={{ fontSize: "0.9rem", color: "#0475c2" }}>
|
||||
<b>Automation</b>
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, bgcolor: "#232323" }}>
|
||||
<NavItem icon={<JobsIcon fontSize="small" />} label="Jobs" pageKey="jobs" />
|
||||
<NavItem
|
||||
icon={<WorkflowsIcon fontSize="small" />}
|
||||
label="Workflows"
|
||||
pageKey="workflow-editor"
|
||||
onClick={() => setCurrentPage("workflow-editor")}
|
||||
/>
|
||||
<Box sx={{ px: 2, pb: 1 }}>
|
||||
<Tooltip title="Create a new workflow tab and open the editor" arrow>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={createNewTab}
|
||||
startIcon={<PlayCircleIcon />}
|
||||
sx={{
|
||||
mt: 1,
|
||||
color: "#58a6ff",
|
||||
borderColor: "#58a6ff",
|
||||
textTransform: "none",
|
||||
border: "1px solid #58a6ff",
|
||||
backgroundColor: "#1e1e1e",
|
||||
"&:hover": { backgroundColor: "#1b1b1b" }
|
||||
}}
|
||||
>
|
||||
New Workflow
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<NavItem icon={<CommunityIcon fontSize="small" />} label="Community Nodes" pageKey="community" />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* Hidden file input for import */}
|
||||
<input
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
style={{ display: "none" }}
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Collapse/Expand bar */}
|
||||
<Box
|
||||
onClick={() => setNavCollapsed((c) => !c)}
|
||||
sx={{
|
||||
height: "36px",
|
||||
borderTop: "1px solid #333",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#888",
|
||||
backgroundColor: "#121212",
|
||||
"&:hover": { backgroundColor: "#1e1e1e" }
|
||||
}}
|
||||
>
|
||||
{navCollapsed ? ">>" : "<<"}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{ flexGrow: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
{renderMainContent()}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<StatusBar />
|
||||
</Box>
|
||||
<CloseAllDialog
|
||||
open={confirmCloseOpen}
|
||||
onClose={handleCloseDialog}
|
||||
onConfirm={handleConfirmCloseAll}
|
||||
/>
|
||||
|
||||
{/* Dialogs / Menus */}
|
||||
<CloseAllDialog open={confirmCloseOpen} onClose={handleCloseDialog} onConfirm={handleConfirmCloseAll} />
|
||||
<CreditsDialog open={creditsDialogOpen} onClose={() => setCreditsDialogOpen(false)} />
|
||||
<RenameTabDialog
|
||||
open={renameDialogOpen}
|
||||
@@ -471,12 +827,7 @@ export default function App() {
|
||||
onCancel={() => setRenameDialogOpen(false)}
|
||||
onSave={handleRenameDialogSave}
|
||||
/>
|
||||
<TabContextMenu
|
||||
anchor={tabMenuAnchor}
|
||||
onClose={handleCloseTabMenu}
|
||||
onRename={handleRenameTab}
|
||||
onCloseTab={handleCloseTab}
|
||||
/>
|
||||
<TabContextMenu anchor={tabMenuAnchor} onClose={handleCloseTabMenu} onRename={handleRenameTab} onCloseTab={handleCloseTab} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user