From a5e2b87fc38264c3bdb129e35341ecde6d449ccf Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Thu, 7 Aug 2025 21:11:55 -0600 Subject: [PATCH] Added Device, Script,and Workflow Tabs in new Navigation Sidebar --- Data/Server/WebUI/src/App.jsx | 818 +++---------------- Data/Server/WebUI/src/Device_List.jsx | 165 ++++ Data/Server/WebUI/src/Navigation_Sidebar.jsx | 143 ++++ Data/Server/WebUI/src/Script_List.jsx | 127 +++ Data/Server/WebUI/src/Workflow_List.jsx | 144 ++++ 5 files changed, 714 insertions(+), 683 deletions(-) create mode 100644 Data/Server/WebUI/src/Device_List.jsx create mode 100644 Data/Server/WebUI/src/Navigation_Sidebar.jsx create mode 100644 Data/Server/WebUI/src/Script_List.jsx create mode 100644 Data/Server/WebUI/src/Workflow_List.jsx diff --git a/Data/Server/WebUI/src/App.jsx b/Data/Server/WebUI/src/App.jsx index 0333f89..a57cc9b 100644 --- a/Data/Server/WebUI/src/App.jsx +++ b/Data/Server/WebUI/src/App.jsx @@ -1,103 +1,54 @@ ////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/App.jsx -// Core React Imports -import React, { - useState, - useEffect, - useCallback, - useRef, - useMemo -} from "react"; - -// Material UI - Components +import React, { useState, useEffect, useCallback, useRef } from "react"; import { - AppBar, - Toolbar, - Typography, - Box, - Menu, - MenuItem, - Button, - CssBaseline, - ThemeProvider, - createTheme, - Accordion, - AccordionSummary, - AccordionDetails, - List, - ListItemButton, - ListItemText, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - TableSortLabel, - Paper, - Tooltip + AppBar, Toolbar, Typography, Box, Menu, MenuItem, Button, + CssBaseline, ThemeProvider, createTheme } from "@mui/material"; - -// Material UI - Icons import { KeyboardArrowDown as KeyboardArrowDownIcon, InfoOutlined as InfoOutlinedIcon, MergeType as MergeTypeIcon, - 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 + People as PeopleIcon } from "@mui/icons-material"; - -// React Flow import { ReactFlowProvider } from "reactflow"; - -// Styles import "reactflow/dist/style.css"; -// Import Borealis Modules import FlowTabs from "./Flow_Tabs"; import FlowEditor from "./Flow_Editor"; import NodeSidebar from "./Node_Sidebar"; import { - CloseAllDialog, - CreditsDialog, - RenameTabDialog, - TabContextMenu + CloseAllDialog, CreditsDialog, RenameTabDialog, TabContextMenu } from "./Dialogs"; import StatusBar from "./Status_Bar"; -// Websocket Functionality +// New imports for split pages +import NavigationSidebar from "./Navigation_Sidebar"; +import WorkflowList from "./Workflow_List"; +import DeviceList from "./Device_List"; +import ScriptList from "./Script_List"; + import { io } from "socket.io-client"; if (!window.BorealisSocket) { - window.BorealisSocket = io(window.location.origin, { - transports: ["websocket"] - }); + window.BorealisSocket = io(window.location.origin, { transports: ["websocket"] }); } - if (!window.BorealisUpdateRate) { window.BorealisUpdateRate = 200; } -const modules = import.meta.glob("./nodes/**/*.jsx", { eager: true }); +// Load node modules dynamically +const modules = import.meta.glob('./nodes/**/*.jsx', { eager: true }); const nodeTypes = {}; const categorizedNodes = {}; - Object.entries(modules).forEach(([path, mod]) => { const comp = mod.default; 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] = []; - } + if (!categorizedNodes[category]) categorizedNodes[category] = []; categorizedNodes[category].push(comp); nodeTypes[type] = component; }); @@ -105,26 +56,14 @@ Object.entries(modules).forEach(([path, mod]) => { const darkTheme = createTheme({ palette: { mode: "dark", - background: { - default: "#121212", - paper: "#1e1e1e" - }, - text: { - primary: "#ffffff" - } + background: { default: "#121212", paper: "#1e1e1e" }, + text: { primary: "#ffffff" } }, components: { MuiTooltip: { styleOverrides: { - tooltip: { - backgroundColor: "#2a2a2a", - color: "#ccc", - fontSize: "0.75rem", - border: "1px solid #444" - }, - arrow: { - color: "#2a2a2a" - } + tooltip: { backgroundColor: "#2a2a2a", color: "#ccc", fontSize: "0.75rem", border: "1px solid #444" }, + arrow: { color: "#2a2a2a" } } } } @@ -132,177 +71,11 @@ 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 ( - - - - Devices - - - Connected agents and their recent heartbeat. - - - - - - - handleSort("status")} - > - Status - - - - handleSort("hostname")} - > - Hostname - - - - handleSort("lastSeen")} - > - Last Heartbeat - - - - handleSort("os")} - > - OS - - - - - - {sorted.map((r, i) => ( - - - - {r.status} - - {r.hostname} - {timeSince(r.lastSeen)} - {r.os} - - ))} - {sorted.length === 0 && ( - - - No agents connected. - - - )} - -
-
- ); -} - -// ---------- Main App ---------- export default function App() { - const [tabs, setTabs] = useState([ - { id: "flow_1", tab_name: "Flow 1", nodes: [], edges: [] } - ]); + const [tabs, setTabs] = useState([{ id: "flow_1", tab_name: "Flow 1", nodes: [], edges: [] }]); const [activeTabId, setActiveTabId] = useState("flow_1"); - const [currentPage, setCurrentPage] = useState("jobs"); - - // 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); @@ -313,7 +86,6 @@ 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) { @@ -337,310 +109,125 @@ export default function App() { return () => clearTimeout(timeout); }, [tabs, activeTabId]); - const handleSetNodes = useCallback( - (callbackOrArray, tId) => { - const targetId = tId || activeTabId; - setTabs((old) => - old.map((tab) => { - if (tab.id !== targetId) return tab; - const newNodes = - typeof callbackOrArray === "function" - ? callbackOrArray(tab.nodes) - : callbackOrArray; - return { ...tab, nodes: newNodes }; - }) - ); - }, - [activeTabId] - ); - - const handleSetEdges = useCallback( - (callbackOrArray, tId) => { - const targetId = tId || activeTabId; - setTabs((old) => - old.map((tab) => { - if (tab.id !== targetId) return tab; - const newEdges = - typeof callbackOrArray === "function" - ? callbackOrArray(tab.edges) - : callbackOrArray; - return { ...tab, edges: newEdges }; - }) - ); - }, - [activeTabId] - ); - - // app bar menu - const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget); - const handleAboutMenuClose = () => setAboutAnchorEl(null); - const openCreditsDialog = () => { - handleAboutMenuClose(); - 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: [] }]); - setActiveTabId("flow_1"); - setConfirmCloseOpen(false); - }; - - const createNewTab = () => { - const nextIndex = tabs.length + 1; - const newId = "flow_" + nextIndex; - setTabs((old) => [ - ...old, - { id: newId, tab_name: "Flow " + nextIndex, nodes: [], edges: [] } - ]); - setActiveTabId(newId); - setCurrentPage("workflow-editor"); - }; - - const handleTabChange = (newActiveTabId) => { - setActiveTabId(newActiveTabId); - }; - - const handleTabRightClick = (evt, tabId) => { - evt.preventDefault(); - setTabMenuAnchor({ x: evt.clientX, y: evt.clientY }); - setTabMenuTabId(tabId); - }; - - const handleCloseTabMenu = () => { - setTabMenuAnchor(null); - setTabMenuTabId(null); - }; - - const handleRenameTab = () => { - setRenameDialogOpen(true); - setRenameTabId(tabMenuTabId); - const t = tabs.find((x) => x.id === tabMenuTabId); - setRenameValue(t ? t.tab_name : ""); - handleCloseTabMenu(); - }; - - const handleCloseTab = () => { - setTabs((old) => { - const idx = old.findIndex((t) => t.id === tabMenuTabId); - if (idx === -1) return old; - const newList = [...old]; - newList.splice(idx, 1); - if (tabMenuTabId === activeTabId && newList.length > 0) { - setActiveTabId(newList[0].id); - } else if (newList.length === 0) { - newList.push({ - id: "flow_1", - tab_name: "Flow 1", - nodes: [], - edges: [] - }); - setActiveTabId("flow_1"); - } - return newList; - }); - handleCloseTabMenu(); - }; - - const handleRenameDialogSave = () => { - if (!renameTabId) { - setRenameDialogOpen(false); - return; - } + const handleSetNodes = useCallback((callbackOrArray, tId) => { + const targetId = tId || activeTabId; setTabs((old) => old.map((tab) => - tab.id === renameTabId ? { ...tab, tab_name: renameValue } : tab + tab.id === targetId + ? { ...tab, nodes: typeof callbackOrArray === "function" ? callbackOrArray(tab.nodes) : callbackOrArray } + : tab ) ); - setRenameDialogOpen(false); - }; + }, [activeTabId]); - const handleExportFlow = async () => { - const activeTab = tabs.find((x) => x.id === activeTabId); - if (!activeTab) return; - const data = JSON.stringify( - { - nodes: activeTab.nodes, - edges: activeTab.edges, - tab_name: activeTab.tab_name - }, - null, - 2 + const handleSetEdges = useCallback((callbackOrArray, tId) => { + const targetId = tId || activeTabId; + setTabs((old) => + old.map((tab) => + tab.id === targetId + ? { ...tab, edges: typeof callbackOrArray === "function" ? callbackOrArray(tab.edges) : callbackOrArray } + : tab + ) ); - const blob = new Blob([data], { type: "application/json" }); - const sanitizedTabName = activeTab.tab_name.replace(/\s+/g, "_").toLowerCase(); - const suggestedFilename = sanitizedTabName + "_workflow.json"; - if (window.showSaveFilePicker) { - try { - const fileHandle = await window.showSaveFilePicker({ - suggestedName: suggestedFilename, - types: [{ description: "Workflow JSON File", accept: { "application/json": [".json"] } }] - }); - const writable = await fileHandle.createWritable(); - await writable.write(blob); - await writable.close(); - } catch (err) { - console.error("Save cancelled or failed:", err); - } - } else { - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = suggestedFilename; - a.style.display = "none"; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(a.href); - document.body.removeChild(a); - } - }; + }, [activeTabId]); - const handleImportFlow = async () => { - if (window.showOpenFilePicker) { - try { - const [fileHandle] = await window.showOpenFilePicker({ - types: [{ description: "Workflow JSON File", accept: { "application/json": [".json"] } }] - }); - const file = await fileHandle.getFile(); - const text = await file.text(); - const json = JSON.parse(text); - const newId = "flow_" + (tabs.length + 1); - setTabs((prev) => [ - ...prev, - { - id: newId, - tab_name: json.tab_name || "Imported Flow " + (tabs.length + 1), - nodes: json.nodes || [], - edges: json.edges || [] - } - ]); - setActiveTabId(newId); - setCurrentPage("workflow-editor"); - } catch (err) { - console.error("Import cancelled or failed:", err); - } - } else { - fileInputRef.current?.click(); - } - }; + const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget); + const handleAboutMenuClose = () => setAboutAnchorEl(null); + const openCreditsDialog = () => { handleAboutMenuClose(); setCreditsDialogOpen(true); }; - const handleFileInputChange = async (e) => { - const file = e.target.files[0]; - if (!file) return; - try { - const text = await file.text(); - const json = JSON.parse(text); - const newId = "flow_" + (tabs.length + 1); - setTabs((prev) => [ - ...prev, - { - id: newId, - tab_name: json.tab_name || "Imported Flow " + (tabs.length + 1), - nodes: json.nodes || [], - edges: json.edges || [] - } - ]); - setActiveTabId(newId); - setCurrentPage("workflow-editor"); - } catch (err) { - console.error("Failed to read file:", err); - } - }; - - // ---------- Main Content ---------- const renderMainContent = () => { - if (currentPage === "workflow-editor") { - return ( - - ; + + case "workflows": + return ( + { + // If workflow name exists in tabs, just switch to it + if (workflow?.name) { + const existing = tabs.find( + (t) => t.tab_name.toLowerCase() === workflow.name.toLowerCase() + ); + if (existing) { + setActiveTabId(existing.id); + setCurrentPage("workflow-editor"); + return; + } + } + // Otherwise, create a new workflow tab + const newId = "flow_" + (tabs.length + 1); + setTabs((prev) => [ + ...prev, + { + id: newId, + tab_name: workflow?.name || `Flow ${tabs.length + 1}`, + nodes: [], + edges: [] + } + ]); + setActiveTabId(newId); + setCurrentPage("workflow-editor"); + }} /> - - ; + + case "workflow-editor": + return ( + + {}} + handleImportFlow={() => {}} + handleOpenCloseAllDialog={() => {}} + fileInputRef={fileInputRef} + onFileInputChange={() => {}} /> - - {tabs.map((tab) => ( - - - handleSetNodes(val, tab.id)} - setEdges={(val) => handleSetEdges(val, tab.id)} - nodeTypes={nodeTypes} - categorizedNodes={categorizedNodes} - /> - - - ))} + + {}} + onTabRightClick={() => {}} + /> + + {tabs.map((tab) => ( + + + handleSetNodes(val, tab.id)} + setEdges={(val) => handleSetEdges(val, tab.id)} + nodeTypes={nodeTypes} + categorizedNodes={categorizedNodes} + /> + + + ))} + - - ); - } - if (currentPage === "devices") { - return ; - } - return ( - - Select a section from navigation. - - ); - }; + ); - // ---------- Nav helpers ---------- - const NavItem = ({ icon, label, pageKey, indent = 0, onClick }) => { - const active = currentPage === pageKey; - return ( - setCurrentPage(pageKey))} - sx={{ - pl: indent ? 4 : 2, - py: 1, - color: "#ccc", - position: "relative", - bgcolor: active ? "#2a2a2a" : "transparent", - "&:hover": { bgcolor: "#2c2c2c" } - }} - > - {/* left accent when active */} - - {icon && {icon}} - - - ); + default: + return ( + + Select a section from navigation. + + ); + } }; return ( @@ -670,164 +257,29 @@ export default function App() { - - {/* Main area with new wider nav styled like Node Sidebar */} - {/* Navigation Sidebar */} - - - {!navCollapsed && ( - <> - {/* Devices */} - setExpandedNav((s) => ({ ...s, devices: e }))} - square - disableGutters - sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }} - > - } - sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }} - > - - Devices - - - - } label="Devices" pageKey="devices" /> - - - - {/* Filters & Groups */} - setExpandedNav((s) => ({ ...s, filters: e }))} - square - disableGutters - sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }} - > - } - sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }} - > - - Filters & Groups - - - - } label="Filters" pageKey="filters" /> - } label="Groups" pageKey="groups" /> - - - - {/* Automation */} - setExpandedNav((s) => ({ ...s, automation: e }))} - square - disableGutters - sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }} - > - } - sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }} - > - - Automation - - - - } label="Jobs" pageKey="jobs" /> - } - label="Workflows" - pageKey="workflow-editor" - onClick={() => setCurrentPage("workflow-editor")} - /> - - - - - - } label="Community Nodes" pageKey="community" /> - - - - {/* Hidden file input for import */} - - - )} - - - {/* Collapse/Expand bar */} - 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 ? ">>" : "<<"} - - - - {/* Content */} + {renderMainContent()} - - {/* Dialogs / Menus */} - + setConfirmCloseOpen(false)} onConfirm={() => {}} /> setCreditsDialogOpen(false)} /> setRenameDialogOpen(false)} - onSave={handleRenameDialogSave} + onSave={() => {}} + /> + setTabMenuAnchor(null)} + onRename={() => {}} + onCloseTab={() => {}} /> - ); } diff --git a/Data/Server/WebUI/src/Device_List.jsx b/Data/Server/WebUI/src/Device_List.jsx new file mode 100644 index 0000000..46d283e --- /dev/null +++ b/Data/Server/WebUI/src/Device_List.jsx @@ -0,0 +1,165 @@ +////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Device_List.jsx + +import React, { useState, useEffect, useCallback, useMemo } from "react"; +import { + Paper, + Box, + Typography, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TableSortLabel +} from "@mui/material"; + +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"; +} + +export default function DeviceList() { + 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 ( + + + + Devices + + + Devices connected to Borealis via Agent and their recent heartbeats. + + + + + + + handleSort("status")} + > + Status + + + + handleSort("hostname")} + > + Device Hostname + + + + handleSort("lastSeen")} + > + Last Heartbeat + + + + handleSort("os")} + > + OS + + + + + + {sorted.map((r, i) => ( + + + + {r.status} + + {r.hostname} + {timeSince(r.lastSeen)} + {r.os} + + ))} + {sorted.length === 0 && ( + + + No agents connected. + + + )} + +
+
+ ); +} diff --git a/Data/Server/WebUI/src/Navigation_Sidebar.jsx b/Data/Server/WebUI/src/Navigation_Sidebar.jsx new file mode 100644 index 0000000..a748fd7 --- /dev/null +++ b/Data/Server/WebUI/src/Navigation_Sidebar.jsx @@ -0,0 +1,143 @@ +////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Navigation_Sidebar.jsx + +import React, { useState } from "react"; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Box, + ListItemButton, + ListItemText +} from "@mui/material"; +import { + ExpandMore as ExpandMoreIcon, + Devices as DevicesIcon, + FilterAlt as FilterIcon, + Groups as GroupsIcon, + Work as JobsIcon, + AutoAwesomeMosaic as WorkflowsIcon, + Code as ScriptIcon, + PeopleOutline as CommunityIcon +} from "@mui/icons-material"; + +export default function NavigationSidebar({ currentPage, onNavigate }) { + const [expandedNav, setExpandedNav] = useState({ + devices: true, + filters: false, + automation: true + }); + + const NavItem = ({ icon, label, pageKey, indent = 0 }) => { + const active = currentPage === pageKey; + return ( + onNavigate(pageKey)} + sx={{ + pl: indent ? 4 : 2, + py: 1, + color: "#ccc", + position: "relative", + bgcolor: active ? "#2a2a2a" : "transparent", + "&:hover": { bgcolor: "#2c2c2c" } + }} + > + + {icon && {icon}} + + + ); + }; + + return ( + + + {/* Devices */} + setExpandedNav((s) => ({ ...s, devices: e }))} + square + disableGutters + sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }} + > + } + sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }} + > + + Devices + + + + } label="Devices" pageKey="devices" /> + + + + {/* Filters & Groups */} + setExpandedNav((s) => ({ ...s, filters: e }))} + square + disableGutters + sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }} + > + } + sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }} + > + + Filters & Groups + + + + } label="Filters" pageKey="filters" /> + } label="Groups" pageKey="groups" /> + + + + {/* Automation */} + setExpandedNav((s) => ({ ...s, automation: e }))} + square + disableGutters + sx={{ "&:before": { display: "none" }, margin: 0, border: 0 }} + > + } + sx={{ backgroundColor: "#2c2c2c", minHeight: "36px", "& .MuiAccordionSummary-content": { margin: 0 } }} + > + + Automation + + + + } label="Scheduled Jobs" pageKey="jobs" /> + } label="Workflows" pageKey="workflows" /> + } label="Scripts" pageKey="scripts" /> + } label="Community Nodes" pageKey="community" /> + + + + + ); +} diff --git a/Data/Server/WebUI/src/Script_List.jsx b/Data/Server/WebUI/src/Script_List.jsx new file mode 100644 index 0000000..7998743 --- /dev/null +++ b/Data/Server/WebUI/src/Script_List.jsx @@ -0,0 +1,127 @@ +////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Script_List.jsx + +import React, { useState, useMemo } from "react"; +import { + Paper, + Box, + Typography, + Button, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TableSortLabel +} from "@mui/material"; +import { Code as ScriptIcon } from "@mui/icons-material"; + +export default function ScriptList() { + const [rows, setRows] = useState([]); + const [orderBy, setOrderBy] = useState("name"); + const [order, setOrder] = useState("asc"); + + const handleSort = (col) => { + if (orderBy === col) setOrder(order === "asc" ? "desc" : "asc"); + else { + setOrderBy(col); + setOrder("asc"); + } + }; + + const sorted = useMemo(() => { + const dir = order === "asc" ? 1 : -1; + return [...rows].sort((a, b) => { + const A = a[orderBy] || ""; + const B = b[orderBy] || ""; + return String(A).localeCompare(String(B)) * dir; + }); + }, [rows, orderBy, order]); + + return ( + + + + + Scripts + + + List of available automation scripts. + + + + + + + + + handleSort("name")} + > + Name + + + + handleSort("description")} + > + Description + + + + handleSort("category")} + > + Category + + + + handleSort("lastEdited")} + > + Last Edited + + + + + + {sorted.map((r, i) => ( + + {r.name} + {r.description} + {r.category} + {r.lastEdited} + + ))} + {sorted.length === 0 && ( + + + No scripts found. + + + )} + +
+
+ ); +} diff --git a/Data/Server/WebUI/src/Workflow_List.jsx b/Data/Server/WebUI/src/Workflow_List.jsx new file mode 100644 index 0000000..24073fd --- /dev/null +++ b/Data/Server/WebUI/src/Workflow_List.jsx @@ -0,0 +1,144 @@ +////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Workflow_List.jsx + +import React, { useState, useMemo } from "react"; +import { + Paper, + Box, + Typography, + Button, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TableSortLabel +} from "@mui/material"; +import { PlayCircle as PlayCircleIcon } from "@mui/icons-material"; + +export default function WorkflowList({ onOpenWorkflow }) { + const [rows, setRows] = useState([]); + const [orderBy, setOrderBy] = useState("name"); + const [order, setOrder] = useState("asc"); + + const handleSort = (col) => { + if (orderBy === col) setOrder(order === "asc" ? "desc" : "asc"); + else { + setOrderBy(col); + setOrder("asc"); + } + }; + + const sorted = useMemo(() => { + const dir = order === "asc" ? 1 : -1; + return [...rows].sort((a, b) => { + const A = a[orderBy] || ""; + const B = b[orderBy] || ""; + return String(A).localeCompare(String(B)) * dir; + }); + }, [rows, orderBy, order]); + + const handleNewWorkflow = () => { + if (onOpenWorkflow) { + onOpenWorkflow(); // trigger App.jsx to open editor + } + }; + + const handleRowClick = (workflow) => { + if (onOpenWorkflow) { + onOpenWorkflow(workflow); + } + }; + + return ( + + + + + Workflows + + + List of available workflows. + + + + + + + + + handleSort("name")} + > + Name + + + + handleSort("description")} + > + Description + + + + handleSort("category")} + > + Category + + + + handleSort("lastEdited")} + > + Last Edited + + + + + + {sorted.map((r, i) => ( + handleRowClick(r)} + > + {r.name} + {r.description} + {r.category} + {r.lastEdited} + + ))} + {sorted.length === 0 && ( + + + No workflows found. + + + )} + +
+
+ ); +}