////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/App.jsx //Shared Imports import React, { useState, useEffect, useCallback, useRef } from "react"; import { ReactFlowProvider } from "reactflow"; import "reactflow/dist/style.css"; import { CloseAllDialog, RenameTabDialog, TabContextMenu, NotAuthorizedDialog } from "./Dialogs"; import NavigationSidebar from "./Navigation_Sidebar"; // Styling Imports import { AppBar, Toolbar, Typography, Box, Menu, MenuItem, Button, CssBaseline, ThemeProvider, createTheme, Breadcrumbs } from "@mui/material"; import { KeyboardArrowDown as KeyboardArrowDownIcon, Logout as LogoutIcon, NavigateNext as NavigateNextIcon } from "@mui/icons-material"; import ClickAwayListener from "@mui/material/ClickAwayListener"; import SearchIcon from "@mui/icons-material/Search"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp"; // Workflow Editor Imports import FlowTabs from "./Flow_Editor/Flow_Tabs"; import FlowEditor from "./Flow_Editor/Flow_Editor"; import NodeSidebar from "./Flow_Editor/Node_Sidebar"; import StatusBar from "./Status_Bar"; // Borealis Page Imports import Login from "./Login.jsx"; import SiteList from "./Sites/Site_List"; import DeviceList from "./Devices/Device_List"; import DeviceDetails from "./Devices/Device_Details"; import AgentDevices from "./Devices/Agent_Devices.jsx"; import SSHDevices from "./Devices/SSH_Devices.jsx"; import WinRMDevices from "./Devices/WinRM_Devices.jsx"; import AssemblyList from "./Assemblies/Assembly_List"; import AssemblyEditor from "./Assemblies/Assembly_Editor"; import ScheduledJobsList from "./Scheduling/Scheduled_Jobs_List"; import CreateJob from "./Scheduling/Create_Job.jsx"; import CredentialList from "./Access_Management/Credential_List.jsx"; import UserManagement from "./Access_Management/Users.jsx"; import GithubAPIToken from "./Access_Management/Github_API_Token.jsx"; import ServerInfo from "./Admin/Server_Info.jsx"; import EnrollmentCodes from "./Admin/Enrollment_Codes.jsx"; import DeviceApprovals from "./Admin/Device_Approvals.jsx"; // Networking Imports import { io } from "socket.io-client"; if (!window.BorealisSocket) { window.BorealisSocket = io(window.location.origin, { transports: ["websocket"] }); } if (!window.BorealisUpdateRate) { window.BorealisUpdateRate = 200; } /////////////////////////////////////////////////////////////////////////////////////////////////// // 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 category = parts[0]; if (!categorizedNodes[category]) categorizedNodes[category] = []; categorizedNodes[category].push(comp); nodeTypes[type] = component; }); const darkTheme = createTheme({ palette: { mode: "dark", 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" } } } } }); const LOCAL_STORAGE_KEY = "borealis_persistent_state"; export default function App() { const [tabs, setTabs] = useState([{ id: "flow_1", tab_name: "Flow 1", nodes: [], edges: [] }]); const [activeTabId, setActiveTabId] = useState("flow_1"); const [currentPage, setCurrentPageState] = useState("devices"); const [selectedDevice, setSelectedDevice] = useState(null); const [userMenuAnchorEl, setUserMenuAnchorEl] = useState(null); const [confirmCloseOpen, setConfirmCloseOpen] = useState(false); const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [renameTabId, setRenameTabId] = useState(null); const [renameValue, setRenameValue] = useState(""); const [tabMenuAnchor, setTabMenuAnchor] = useState(null); const [tabMenuTabId, setTabMenuTabId] = useState(null); const fileInputRef = useRef(null); const [user, setUser] = useState(null); const [userRole, setUserRole] = useState(null); const [userDisplayName, setUserDisplayName] = useState(null); const [editingJob, setEditingJob] = useState(null); const [jobsRefreshToken, setJobsRefreshToken] = useState(0); const [assemblyEditorState, setAssemblyEditorState] = useState(null); // { path, mode, context, nonce } const [sessionResolved, setSessionResolved] = useState(false); const initialPathRef = useRef(window.location.pathname + window.location.search); const pendingPathRef = useRef(null); const [notAuthorizedOpen, setNotAuthorizedOpen] = useState(false); // Top-bar search state const SEARCH_CATEGORIES = [ { key: "hostname", label: "Hostname", scope: "device", placeholder: "Search Hostname" }, { key: "internal_ip", label: "Internal IP", scope: "device", placeholder: "Search Internal IP" }, { key: "external_ip", label: "External IP", scope: "device", placeholder: "Search External IP" }, { key: "description", label: "Description", scope: "device", placeholder: "Search Description" }, { key: "last_user", label: "Last User", scope: "device", placeholder: "Search Last User" }, { key: "serial_number", label: "Serial Number (Soon)", scope: "device", placeholder: "Search Serial Number" }, { key: "site_name", label: "Site Name", scope: "site", placeholder: "Search Site Name" }, { key: "site_description", label: "Site Description", scope: "site", placeholder: "Search Site Description" }, ]; const [searchCategory, setSearchCategory] = useState("hostname"); const [searchOpen, setSearchOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [searchMenuEl, setSearchMenuEl] = useState(null); const [suggestions, setSuggestions] = useState({ devices: [], sites: [], q: "", field: "" }); const searchAnchorRef = useRef(null); const searchDebounceRef = useRef(null); // Gentle highlight helper for matched substrings const highlightText = useCallback((text, query) => { const t = String(text ?? ""); const q = String(query ?? "").trim(); if (!q) return t; try { const esc = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(`(${esc})`, "ig"); const parts = t.split(re); return parts.map((part, i) => part.toLowerCase() === q.toLowerCase() ? ( {part} ) : {part} ); } catch { return t; } }, []); const pageToPath = useCallback( (page, options = {}) => { switch (page) { case "login": return "/login"; case "sites": return "/sites"; case "devices": return "/devices"; case "agent_devices": return "/devices/agent"; case "ssh_devices": return "/devices/ssh"; case "winrm_devices": return "/devices/winrm"; case "device_details": { const device = options.device || selectedDevice || (options.deviceId ? { agent_guid: options.deviceId, hostname: options.deviceName || options.deviceId } : null); const deviceId = device?.agent_guid || device?.guid || device?.summary?.agent_guid || device?.hostname || device?.id; if (deviceId) { return `/device/${encodeURIComponent(deviceId)}`; } return "/devices"; } case "jobs": return "/scheduling"; case "create_job": return "/scheduling/create_job"; case "workflows": return "/workflows"; case "workflow-editor": return "/workflows/editor"; case "assemblies": return "/assemblies"; case "scripts": case "ansible_editor": { const mode = page === "ansible_editor" ? "ansible" : "scripts"; const params = new URLSearchParams(); if (mode === "ansible") { params.set("mode", "ansible"); } const state = options.assemblyState || assemblyEditorState; if (state?.path) { params.set("path", state.path); } const query = params.toString(); return query ? `/assemblies/editor?${query}` : "/assemblies/editor"; } case "access_credentials": return "/access_management/credentials"; case "access_github_token": return "/access_management/github_token"; case "access_users": return "/access_management/users"; case "server_info": return "/admin/server_info"; case "admin_enrollment_codes": return "/admin/enrollment-codes"; case "admin_device_approvals": return "/admin/device-approvals"; default: return "/devices"; } }, [assemblyEditorState, selectedDevice] ); const interpretPath = useCallback((rawPath) => { try { const url = new URL(rawPath || "/", window.location.origin); let path = url.pathname || "/"; if (path.length > 1 && path.endsWith("/")) { path = path.slice(0, -1); } const segments = path.split("/").filter(Boolean); const params = url.searchParams; if (path === "/login") return { page: "login", options: {} }; if (path === "/" || path === "") return { page: "devices", options: {} }; if (path === "/devices") return { page: "devices", options: {} }; if (path === "/devices/agent") return { page: "agent_devices", options: {} }; if (path === "/devices/ssh") return { page: "ssh_devices", options: {} }; if (path === "/devices/winrm") return { page: "winrm_devices", options: {} }; if (segments[0] === "device" && segments[1]) { const id = decodeURIComponent(segments[1]); return { page: "device_details", options: { device: { agent_guid: id, hostname: id } } }; } if (path === "/sites") return { page: "sites", options: {} }; if (path === "/scheduling") return { page: "jobs", options: {} }; if (path === "/scheduling/create_job") return { page: "create_job", options: {} }; if (path === "/workflows") return { page: "workflows", options: {} }; if (path === "/workflows/editor") return { page: "workflow-editor", options: {} }; if (path === "/assemblies") return { page: "assemblies", options: {} }; if (path === "/assemblies/editor") { const mode = params.get("mode"); const relPath = params.get("path") || ""; const state = relPath ? { path: relPath, mode: mode === "ansible" ? "ansible" : "scripts", nonce: Date.now() } : null; return { page: mode === "ansible" ? "ansible_editor" : "scripts", options: state ? { assemblyState: state } : {} }; } if (path === "/access_management/users") return { page: "access_users", options: {} }; if (path === "/access_management/github_token") return { page: "access_github_token", options: {} }; if (path === "/access_management/credentials") return { page: "access_credentials", options: {} }; if (path === "/admin/server_info") return { page: "server_info", options: {} }; if (path === "/admin/enrollment-codes") return { page: "admin_enrollment_codes", options: {} }; if (path === "/admin/device-approvals") return { page: "admin_device_approvals", options: {} }; return { page: "devices", options: {} }; } catch { return { page: "devices", options: {} }; } }, []); const updateStateForPage = useCallback( (page, options = {}) => { setCurrentPageState(page); if (page === "device_details") { if (options.device) { setSelectedDevice(options.device); } else if (options.deviceId) { const fallbackId = options.deviceId; const fallbackName = options.deviceName || options.deviceId; setSelectedDevice((prev) => { const prevId = prev?.agent_guid || prev?.guid || prev?.hostname || ""; if (prevId === fallbackId || prevId === fallbackName) { return prev; } return { agent_guid: fallbackId, hostname: fallbackName }; }); } } else if (!options.preserveDevice) { setSelectedDevice(null); } if ((page === "scripts" || page === "ansible_editor") && options.assemblyState) { setAssemblyEditorState(options.assemblyState); } }, [setAssemblyEditorState, setCurrentPageState, setSelectedDevice] ); const navigateTo = useCallback( (page, options = {}) => { const { replace = false, allowUnauthenticated = false, suppressPending = false } = options; const targetPath = pageToPath(page, options); if (!allowUnauthenticated && !user && page !== "login") { if (!suppressPending && targetPath) { pendingPathRef.current = targetPath; } updateStateForPage("login", {}); const loginPath = "/login"; const method = replace ? "replaceState" : "pushState"; const current = window.location.pathname + window.location.search; if (replace || current !== loginPath) { window.history[method]({}, "", loginPath); } return; } if (page === "login") { updateStateForPage("login", {}); const loginPath = "/login"; const method = replace ? "replaceState" : "pushState"; const current = window.location.pathname + window.location.search; if (replace || current !== loginPath) { window.history[method]({}, "", loginPath); } return; } pendingPathRef.current = null; updateStateForPage(page, options); if (targetPath) { const method = replace ? "replaceState" : "pushState"; const current = window.location.pathname + window.location.search; if (replace || current !== targetPath) { window.history[method]({}, "", targetPath); } } }, [pageToPath, updateStateForPage, user] ); const navigateByPath = useCallback( (path, { replace = false, allowUnauthenticated = false } = {}) => { const { page, options } = interpretPath(path); navigateTo(page, { ...(options || {}), replace, allowUnauthenticated }); }, [interpretPath, navigateTo] ); const navigateToRef = useRef(navigateTo); const navigateByPathRef = useRef(navigateByPath); useEffect(() => { navigateToRef.current = navigateTo; navigateByPathRef.current = navigateByPath; }, [navigateTo, navigateByPath]); // Build breadcrumb items for current view const breadcrumbs = React.useMemo(() => { const items = []; switch (currentPage) { case "sites": items.push({ label: "Sites", page: "sites" }); items.push({ label: "Site List", page: "sites" }); break; case "devices": items.push({ label: "Inventory", page: "devices" }); items.push({ label: "Devices", page: "devices" }); break; case "device_details": items.push({ label: "Devices", page: "devices" }); items.push({ label: "Device List", page: "devices" }); items.push({ label: "Device Details" }); break; case "jobs": items.push({ label: "Automation", page: "jobs" }); items.push({ label: "Scheduled Jobs", page: "jobs" }); break; case "create_job": items.push({ label: "Automation", page: "jobs" }); items.push({ label: "Scheduled Jobs", page: "jobs" }); items.push({ label: editingJob ? "Edit Job" : "Create Job", page: "create_job" }); break; case "workflows": items.push({ label: "Automation", page: "jobs" }); items.push({ label: "Workflows", page: "workflows" }); break; case "workflow-editor": items.push({ label: "Automation", page: "jobs" }); items.push({ label: "Workflows", page: "workflows" }); items.push({ label: "Flow Editor" }); break; case "scripts": items.push({ label: "Automation", page: "jobs" }); items.push({ label: "Scripts", page: "scripts" }); break; case "ansible_editor": items.push({ label: "Automation", page: "jobs" }); items.push({ label: "Ansible Playbooks", page: "assemblies" }); items.push({ label: "Playbook Editor" }); break; case "assemblies": items.push({ label: "Automation", page: "jobs" }); items.push({ label: "Assemblies", page: "assemblies" }); break; case "community": items.push({ label: "Automation", page: "jobs" }); items.push({ label: "Community Content", page: "community" }); break; case "agent_devices": items.push({ label: "Inventory", page: "devices" }); items.push({ label: "Devices", page: "devices" }); items.push({ label: "Agent Devices", page: "agent_devices" }); break; case "ssh_devices": items.push({ label: "Inventory", page: "devices" }); items.push({ label: "Devices", page: "devices" }); items.push({ label: "SSH Devices", page: "ssh_devices" }); break; case "winrm_devices": items.push({ label: "Inventory", page: "devices" }); items.push({ label: "Devices", page: "devices" }); items.push({ label: "WinRM Devices", page: "winrm_devices" }); break; case "access_credentials": items.push({ label: "Access Management", page: "access_credentials" }); items.push({ label: "Credentials", page: "access_credentials" }); break; case "access_github_token": items.push({ label: "Access Management", page: "access_credentials" }); items.push({ label: "GitHub API Token", page: "access_github_token" }); break; case "access_users": items.push({ label: "Access Management", page: "access_credentials" }); items.push({ label: "Users", page: "access_users" }); break; case "server_info": items.push({ label: "Admin Settings" }); items.push({ label: "Server Info", page: "server_info" }); break; case "admin_enrollment_codes": items.push({ label: "Admin Settings", page: "server_info" }); items.push({ label: "Installer Codes", page: "admin_enrollment_codes" }); break; case "admin_device_approvals": items.push({ label: "Admin Settings", page: "server_info" }); items.push({ label: "Device Approvals", page: "admin_device_approvals" }); break; case "filters": items.push({ label: "Filters & Groups", page: "filters" }); items.push({ label: "Filters", page: "filters" }); break; case "groups": items.push({ label: "Filters & Groups", page: "filters" }); items.push({ label: "Groups", page: "groups" }); break; default: // Fallback to a neutral crumb if unknown if (currentPage) items.push({ label: String(currentPage) }); } return items; }, [currentPage, selectedDevice, editingJob]); useEffect(() => { let canceled = false; const hydrateSession = async () => { const session = localStorage.getItem("borealis_session"); if (session) { try { const data = JSON.parse(session); if (Date.now() - data.timestamp < 3600 * 1000) { if (!canceled) { setUser(data.username); setUserRole(data.role || null); setUserDisplayName(data.display_name || data.username); } } else { localStorage.removeItem("borealis_session"); } } catch { localStorage.removeItem("borealis_session"); } } try { const resp = await fetch('/api/auth/me', { credentials: 'include' }); if (resp.ok) { const me = await resp.json(); if (!canceled) { setUser(me.username); setUserRole(me.role || null); setUserDisplayName(me.display_name || me.username); } localStorage.setItem( "borealis_session", JSON.stringify({ username: me.username, display_name: me.display_name || me.username, role: me.role, timestamp: Date.now() }) ); } } catch {} if (!canceled) { setSessionResolved(true); } }; hydrateSession(); return () => { canceled = true; }; }, []); useEffect(() => { if (!sessionResolved) return; const navTo = navigateToRef.current; const navByPath = navigateByPathRef.current; if (user) { const stored = initialPathRef.current; const currentLocation = window.location.pathname + window.location.search; const targetPath = stored && stored !== "/login" ? stored : currentLocation === "/login" || currentLocation === "" ? "/devices" : currentLocation; navByPath(targetPath, { replace: true, allowUnauthenticated: true }); initialPathRef.current = null; pendingPathRef.current = null; } else { const stored = initialPathRef.current; const currentLocation = window.location.pathname + window.location.search; const rememberPath = stored && !stored.startsWith("/login") ? stored : !currentLocation.startsWith("/login") ? currentLocation : null; if (rememberPath) { pendingPathRef.current = rememberPath; } navTo("login", { replace: true, allowUnauthenticated: true, suppressPending: true }); } }, [sessionResolved, user]); useEffect(() => { if (!sessionResolved) return; const handlePopState = () => { const path = window.location.pathname + window.location.search; if (!user) { if (!path.startsWith("/login")) { pendingPathRef.current = path; } navigateToRef.current("login", { replace: true, allowUnauthenticated: true, suppressPending: true }); return; } navigateByPathRef.current(path, { replace: true, allowUnauthenticated: true }); }; window.addEventListener("popstate", handlePopState); return () => window.removeEventListener("popstate", handlePopState); }, [sessionResolved, user]); // Suggest fetcher with debounce const fetchSuggestions = useCallback((field, q) => { const query = String(q || "").trim(); if (query.length < 3) { setSuggestions({ devices: [], sites: [], q: query, field }); return; } const params = new URLSearchParams({ field, q: query, limit: "5" }); fetch(`/api/search/suggest?${params.toString()}`) .then((r) => (r.ok ? r.json() : { devices: [], sites: [], q: query, field })) .then((data) => setSuggestions(data)) .catch(() => setSuggestions({ devices: [], sites: [], q: query, field })); }, []); useEffect(() => { if (!searchOpen) return; if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); searchDebounceRef.current = setTimeout(() => { fetchSuggestions(searchCategory, searchQuery); }, 220); return () => { if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); }; }, [searchOpen, searchCategory, searchQuery, fetchSuggestions]); const execSearch = useCallback(async (field, q, navigateImmediate = true) => { const cat = SEARCH_CATEGORIES.find((c) => c.key === field) || SEARCH_CATEGORIES[0]; if (cat.scope === "site") { try { localStorage.setItem('site_list_initial_filters', JSON.stringify( field === 'site_name' ? { name: q } : { description: q } )); } catch {} if (navigateImmediate) navigateTo("sites"); } else { // device field // Map API field -> Device_List filter key const fieldMap = { hostname: 'hostname', description: 'description', last_user: 'lastUser', internal_ip: 'internalIp', external_ip: 'externalIp', serial_number: 'serialNumber', // placeholder (ignored by Device_List for now) }; const k = fieldMap[field] || 'hostname'; const qLc = String(q || '').toLowerCase(); const exact = (suggestions.devices || []).find((d) => String(d.hostname || d.value || '').toLowerCase() === qLc); if (exact && (exact.hostname || '').trim()) { const device = { hostname: exact.hostname.trim() }; if (navigateImmediate) { navigateTo('device_details', { device }); } else { setSelectedDevice(device); } } else if (field === 'hostname') { // Probe device existence and open directly if found try { const resp = await fetch(`/api/device/details/${encodeURIComponent(q)}`); if (resp.ok) { const data = await resp.json(); if (data && (data.summary?.hostname || Object.keys(data).length > 0)) { const device = { hostname: q }; if (navigateImmediate) { navigateTo('device_details', { device }); } else { setSelectedDevice(device); } } else { try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {} if (navigateImmediate) navigateTo('devices'); } } else { try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {} if (navigateImmediate) navigateTo('devices'); } } catch { try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {} if (navigateImmediate) navigateTo('devices'); } } else { try { const payload = (k === 'serialNumber') ? {} : { [k]: q }; localStorage.setItem('device_list_initial_filters', JSON.stringify(payload)); } catch {} if (navigateImmediate) navigateTo("devices"); } } setSearchOpen(false); }, [SEARCH_CATEGORIES, navigateTo, suggestions.devices]); const handleLoginSuccess = ({ username, role }) => { setUser(username); setUserRole(role || null); setUserDisplayName(username); localStorage.setItem( "borealis_session", JSON.stringify({ username, display_name: username, role: role || null, timestamp: Date.now() }) ); // Refresh full profile (to get display_name) in background (async () => { try { const resp = await fetch('/api/auth/me', { credentials: 'include' }); if (resp.ok) { const me = await resp.json(); setUserDisplayName(me.display_name || me.username); localStorage.setItem( "borealis_session", JSON.stringify({ username: me.username, display_name: me.display_name || me.username, role: me.role, timestamp: Date.now() }) ); } } catch {} })(); if (pendingPathRef.current) { navigateByPath(pendingPathRef.current, { replace: true, allowUnauthenticated: true }); pendingPathRef.current = null; } else { navigateTo('devices', { replace: true, allowUnauthenticated: true }); } }; useEffect(() => { const saved = localStorage.getItem(LOCAL_STORAGE_KEY); if (saved) { try { const parsed = JSON.parse(saved); if (Array.isArray(parsed.tabs) && parsed.activeTabId) { setTabs(parsed.tabs); setActiveTabId(parsed.activeTabId); } } catch (err) { console.warn("Failed to parse saved state:", err); } } }, []); useEffect(() => { const timeout = setTimeout(() => { const data = JSON.stringify({ tabs, activeTabId }); localStorage.setItem(LOCAL_STORAGE_KEY, data); }, 1000); return () => clearTimeout(timeout); }, [tabs, activeTabId]); const handleSetNodes = useCallback((callbackOrArray, tId) => { const targetId = tId || activeTabId; setTabs((old) => old.map((tab) => tab.id === targetId ? { ...tab, nodes: typeof callbackOrArray === "function" ? callbackOrArray(tab.nodes) : callbackOrArray } : tab ) ); }, [activeTabId]); 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 ) ); }, [activeTabId]); const handleUserMenuOpen = (event) => setUserMenuAnchorEl(event.currentTarget); const handleUserMenuClose = () => setUserMenuAnchorEl(null); const handleLogout = async () => { try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); } catch {} try { localStorage.removeItem('borealis_session'); } catch {} setUser(null); setUserRole(null); setUserDisplayName(null); navigateTo('login', { replace: true, allowUnauthenticated: true, suppressPending: true }); }; const handleTabRightClick = (evt, tabId) => { evt.preventDefault(); setTabMenuAnchor({ x: evt.clientX, y: evt.clientY }); setTabMenuTabId(tabId); }; const handleCloseTab = () => { setTabs((prev) => { const filtered = prev.filter((t) => t.id !== tabMenuTabId); if (filtered.length === 0) { const newTab = { id: "flow_1", tab_name: "Flow 1", nodes: [], edges: [] }; setActiveTabId(newTab.id); return [newTab]; } if (activeTabId === tabMenuTabId) { setActiveTabId(filtered[0].id); } return filtered; }); setTabMenuAnchor(null); }; const handleRenameTab = () => { const tab = tabs.find((t) => t.id === tabMenuTabId); if (tab) { setRenameTabId(tabMenuTabId); setRenameValue(tab.tab_name); setRenameDialogOpen(true); } setTabMenuAnchor(null); }; const handleSaveRename = () => { setTabs((prev) => prev.map((t) => (t.id === renameTabId ? { ...t, tab_name: renameValue } : t)) ); setRenameDialogOpen(false); }; const handleExportFlow = useCallback(() => { const tab = tabs.find((t) => t.id === activeTabId); if (!tab) return; const payload = { tab_name: tab.tab_name, nodes: tab.nodes, edges: tab.edges }; const fileName = `${tab.tab_name || "workflow"}.json`; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); }, [tabs, activeTabId]); const handleImportFlow = useCallback(() => { if (fileInputRef.current) { fileInputRef.current.value = null; fileInputRef.current.click(); } }, []); const onFileInputChange = useCallback( (e) => { const file = e.target.files && e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const data = JSON.parse(reader.result); const newId = "flow_" + Date.now(); setTabs((prev) => [ ...prev, { id: newId, tab_name: data.tab_name || data.name || file.name.replace(/\.json$/i, ""), nodes: data.nodes || [], edges: data.edges || [] } ]); setActiveTabId(newId); navigateTo("workflow-editor"); } catch (err) { console.error("Failed to import workflow:", err); } }; reader.readAsText(file); e.target.value = ""; }, [navigateTo, setTabs] ); const handleSaveFlow = useCallback( async (name) => { const tab = tabs.find((t) => t.id === activeTabId); if (!tab || !name) return; const payload = { path: tab.folderPath ? `${tab.folderPath}/${name}` : name, workflow: { tab_name: tab.tab_name, nodes: tab.nodes, edges: tab.edges } }; try { const body = { island: 'workflows', kind: 'file', path: payload.path, content: payload.workflow }; await fetch("/api/assembly/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); setTabs((prev) => prev.map((t) => (t.id === activeTabId ? { ...t, tab_name: name } : t)) ); } catch (err) { console.error("Failed to save workflow:", err); } }, [tabs, activeTabId] ); const isAdmin = (String(userRole || '').toLowerCase() === 'admin'); useEffect(() => { const requiresAdmin = currentPage === 'server_info' || currentPage === 'admin_enrollment_codes' || currentPage === 'admin_device_approvals' || currentPage === 'access_credentials' || currentPage === 'access_github_token' || currentPage === 'access_users' || currentPage === 'ssh_devices' || currentPage === 'winrm_devices' || currentPage === 'agent_devices'; if (!isAdmin && requiresAdmin) { setNotAuthorizedOpen(true); navigateTo('devices', { replace: true, suppressPending: true }); } }, [currentPage, isAdmin, navigateTo]); const renderMainContent = () => { switch (currentPage) { case "sites": return ( { try { localStorage.setItem('device_list_initial_site_filter', String(siteName || '')); } catch {} navigateTo("devices"); }} /> ); case "devices": return ( { navigateTo("device_details", { device: d }); }} /> ); case "agent_devices": return ( { navigateTo("device_details", { device: d }); }} /> ); case "ssh_devices": return ; case "winrm_devices": return ; case "device_details": return ( { navigateTo("devices"); setSelectedDevice(null); }} /> ); case "jobs": return ( { setEditingJob(null); navigateTo("create_job"); }} onEditJob={(job) => { setEditingJob(job); navigateTo("create_job"); }} refreshToken={jobsRefreshToken} /> ); case "create_job": return ( { navigateTo("jobs"); setEditingJob(null); }} onCreated={() => { navigateTo("jobs"); setEditingJob(null); setJobsRefreshToken(Date.now()); }} /> ); case "workflows": return ( { const newId = "flow_" + Date.now(); if (workflow && workflow.rel_path) { const folder = workflow.rel_path.split("/").slice(0, -1).join("/"); try { const resp = await fetch(`/api/assembly/load?island=workflows&path=${encodeURIComponent(workflow.rel_path)}`); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); setTabs([{ id: newId, tab_name: data.tab_name || workflow.name || workflow.file_name || "Workflow", nodes: data.nodes || [], edges: data.edges || [], folderPath: folder }]); } catch (err) { console.error("Failed to load workflow:", err); setTabs([{ id: newId, tab_name: workflow?.name || "Workflow", nodes: [], edges: [], folderPath: folder }]); } } else { setTabs([{ id: newId, tab_name: name || "Flow", nodes: [], edges: [], folderPath: folderPath || "" }]); } setActiveTabId(newId); navigateTo("workflow-editor"); }} onOpenScript={(rel, mode, context) => { const nonce = Date.now(); setAssemblyEditorState({ path: rel || '', mode, context: context ? { ...context, nonce } : null, nonce }); navigateTo(mode === 'ansible' ? 'ansible_editor' : 'scripts', { assemblyState: { path: rel || '', mode, context: context ? { ...context, nonce } : null, nonce } }); }} /> ); case "assemblies": return ( { const newId = "flow_" + Date.now(); if (workflow && workflow.rel_path) { const folder = workflow.rel_path.split("/").slice(0, -1).join("/"); try { const resp = await fetch(`/api/assembly/load?island=workflows&path=${encodeURIComponent(workflow.rel_path)}`); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); setTabs([{ id: newId, tab_name: data.tab_name || workflow.name || workflow.file_name || "Workflow", nodes: data.nodes || [], edges: data.edges || [], folderPath: folder }]); } catch (err) { console.error("Failed to load workflow:", err); setTabs([{ id: newId, tab_name: workflow?.name || "Workflow", nodes: [], edges: [], folderPath: folder }]); } } else { setTabs([{ id: newId, tab_name: name || "Flow", nodes: [], edges: [], folderPath: folderPath || "" }]); } setActiveTabId(newId); navigateTo("workflow-editor"); }} onOpenScript={(rel, mode, context) => { const nonce = Date.now(); setAssemblyEditorState({ path: rel || '', mode, context: context ? { ...context, nonce } : null, nonce }); navigateTo(mode === 'ansible' ? 'ansible_editor' : 'scripts', { assemblyState: { path: rel || '', mode, context: context ? { ...context, nonce } : null, nonce } }); }} /> ); case "scripts": return ( setAssemblyEditorState((prev) => (prev && prev.mode === 'scripts' ? null : prev)) } onSaved={() => navigateTo('assemblies')} /> ); case "ansible_editor": return ( setAssemblyEditorState((prev) => (prev && prev.mode === 'ansible' ? null : prev)) } onSaved={() => navigateTo('assemblies')} /> ); case "access_credentials": return ; case "access_github_token": return ; case "access_users": return ; case "server_info": return ; case "admin_enrollment_codes": return ; case "admin_device_approvals": return ; case "workflow-editor": return ( setConfirmCloseOpen(true)} fileInputRef={fileInputRef} onFileInputChange={onFileInputChange} currentTabName={tabs.find((t) => t.id === activeTabId)?.tab_name} /> {}} onTabRightClick={handleTabRightClick} /> {tabs.map((tab) => ( handleSetNodes(val, tab.id)} setEdges={(val) => handleSetEdges(val, tab.id)} nodeTypes={nodeTypes} categorizedNodes={categorizedNodes} /> ))} ); default: return ( Select a section from navigation. ); } }; if (!user) { return ( ); } return ( {/* Breadcrumbs inline in top bar (transparent), aligned to content area */} } aria-label="breadcrumb" sx={{ color: "#9aa0a6", fontSize: "0.825rem", // 50% larger than previous '& .MuiBreadcrumbs-separator': { mx: 0.6 }, pointerEvents: 'auto' }} > {breadcrumbs.map((c, idx) => { if (c.page) { return ( ); } return ( {c.label} ); })} {/* Top search: category + input */} setSearchOpen(false)}> setSearchMenuEl(null)} PaperProps={{ sx: { bgcolor: '#1e1e1e', color: '#fff', minWidth: 240 } }} > {SEARCH_CATEGORIES.map((c) => ( { setSearchCategory(c.key); setSearchMenuEl(null); setSearchQuery(''); setSuggestions({ devices: [], sites: [], q: '', field: '' }); }}> {c.label} ))} { setSearchQuery(e.target.value); setSearchOpen(true); }} onFocus={() => setSearchOpen(true)} onKeyDown={(e) => { if (e.key === 'Enter') { execSearch(searchCategory, searchQuery); } else if (e.key === 'Escape') { setSearchOpen(false); } }} placeholder={(SEARCH_CATEGORIES.find(c => c.key === searchCategory) || {}).placeholder || 'Search'} style={{ outline: 'none', border: 'none', background: 'transparent', color: '#e8eaed', paddingLeft: 10, paddingRight: 28, width: 360, height: '100%' }} /> {searchOpen && (((SEARCH_CATEGORIES.find(c=>c.key===searchCategory)?.scope==='device') && (suggestions.devices||[]).length>0) || ((SEARCH_CATEGORIES.find(c=>c.key===searchCategory)?.scope==='site') && (suggestions.sites||[]).length>0)) && ( {/* Devices group */} {((suggestions.devices || []).length > 0 && (SEARCH_CATEGORIES.find(c=>c.key===searchCategory)?.scope==='device')) && ( Devices {suggestions.devices && suggestions.devices.length > 0 ? ( suggestions.devices.map((d, idx) => { const primary = (searchCategory === 'hostname') ? highlightText(d.hostname || d.value, searchQuery) : (d.hostname || d.value); // Choose a secondary value based on category; fallback to best-available info let secVal = ''; if (searchCategory === 'internal_ip') secVal = d.internal_ip || ''; else if (searchCategory === 'external_ip') secVal = d.external_ip || ''; else if (searchCategory === 'description') secVal = d.description || ''; else if (searchCategory === 'last_user') secVal = d.last_user || ''; const secHighlighted = (searchCategory !== 'hostname' && secVal) ? highlightText(secVal, searchQuery) : (d.internal_ip || d.external_ip || d.description || d.last_user || ''); return ( { navigateTo('device_details', { device: { hostname: d.hostname || d.value } }); setSearchOpen(false); }} sx={{ px: 1.2, py: 0.6, '&:hover': { bgcolor: '#22272e' }, cursor: 'pointer' }}> {primary} {d.site_name || ''}{(d.site_name && (secVal || (d.internal_ip || d.external_ip || d.description || d.last_user))) ? ' • ' : ''}{secHighlighted} ); }) ) : ( {searchCategory === 'serial_number' ? 'Serial numbers are not tracked yet.' : 'No matches'} )} )} {/* Sites group */} {((suggestions.sites || []).length > 0 && (SEARCH_CATEGORIES.find(c=>c.key===searchCategory)?.scope==='site')) && ( Sites {suggestions.sites && suggestions.sites.length > 0 ? ( suggestions.sites.map((s, idx) => ( execSearch(searchCategory, s.value)} sx={{ px: 1.2, py: 0.6, '&:hover': { bgcolor: '#22272e' }, cursor: 'pointer' }}> {searchCategory === 'site_name' ? highlightText(s.site_name, searchQuery) : s.site_name} {searchCategory === 'site_description' ? highlightText(s.site_description || '', searchQuery) : (s.site_description || '')} )) ) : ( No matches )} )} )} {/* Spacer to keep user menu aligned right */} { handleUserMenuClose(); handleLogout(); }}> Logout *': { alignSelf: 'stretch', minHeight: 'calc(100% - 32px)' // account for typical m:2 top+bottom margins } }} > {renderMainContent()} setConfirmCloseOpen(false)} onConfirm={() => {}} /> setRenameDialogOpen(false)} onSave={handleSaveRename} /> setTabMenuAnchor(null)} onRename={handleRenameTab} onCloseTab={handleCloseTab} /> setNotAuthorizedOpen(false)} /> ); }