Merge pull request #103 from bunny-lab-io:codex/implement-url-based-breadcrumbing-system

Add client-side routing for Borealis WebUI
This commit is contained in:
2025-10-16 05:44:05 -06:00
committed by GitHub

View File

@@ -94,7 +94,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
export default function 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 [activeTabId, setActiveTabId] = useState("flow_1");
const [currentPage, setCurrentPage] = useState("devices"); const [currentPage, setCurrentPageState] = useState("devices");
const [selectedDevice, setSelectedDevice] = useState(null); const [selectedDevice, setSelectedDevice] = useState(null);
const [userMenuAnchorEl, setUserMenuAnchorEl] = useState(null); const [userMenuAnchorEl, setUserMenuAnchorEl] = useState(null);
@@ -111,6 +111,9 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
const [editingJob, setEditingJob] = useState(null); const [editingJob, setEditingJob] = useState(null);
const [jobsRefreshToken, setJobsRefreshToken] = useState(0); const [jobsRefreshToken, setJobsRefreshToken] = useState(0);
const [assemblyEditorState, setAssemblyEditorState] = useState(null); // { path, mode, context, nonce } 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); const [notAuthorizedOpen, setNotAuthorizedOpen] = useState(false);
// Top-bar search state // Top-bar search state
@@ -153,6 +156,213 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
} }
}, []); }, []);
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_users":
return "/access_management/users";
case "server_info":
return "/admin/server_info";
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/credentials") return { page: "access_credentials", options: {} };
if (path === "/admin/server_info") return { page: "server_info", 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 // Build breadcrumb items for current view
const breadcrumbs = React.useMemo(() => { const breadcrumbs = React.useMemo(() => {
const items = []; const items = [];
@@ -247,38 +457,106 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
return items; return items;
}, [currentPage, selectedDevice, editingJob]); }, [currentPage, selectedDevice, editingJob]);
useEffect(() => { useEffect(() => {
const session = localStorage.getItem("borealis_session"); let canceled = false;
if (session) { const hydrateSession = async () => {
try { const session = localStorage.getItem("borealis_session");
const data = JSON.parse(session); if (session) {
if (Date.now() - data.timestamp < 3600 * 1000) { try {
setUser(data.username); const data = JSON.parse(session);
setUserRole(data.role || null); if (Date.now() - data.timestamp < 3600 * 1000) {
setUserDisplayName(data.display_name || data.username); if (!canceled) {
} else { setUser(data.username);
setUserRole(data.role || null);
setUserDisplayName(data.display_name || data.username);
}
} else {
localStorage.removeItem("borealis_session");
}
} catch {
localStorage.removeItem("borealis_session"); localStorage.removeItem("borealis_session");
} }
} catch {
localStorage.removeItem("borealis_session");
} }
}
(async () => {
try { try {
const resp = await fetch('/api/auth/me', { credentials: 'include' }); const resp = await fetch('/api/auth/me', { credentials: 'include' });
if (resp.ok) { if (resp.ok) {
const me = await resp.json(); const me = await resp.json();
setUser(me.username); if (!canceled) {
setUserRole(me.role || null); setUser(me.username);
setUserDisplayName(me.display_name || me.username); setUserRole(me.role || null);
setUserDisplayName(me.display_name || me.username);
}
localStorage.setItem( localStorage.setItem(
"borealis_session", "borealis_session",
JSON.stringify({ username: me.username, display_name: me.display_name || me.username, role: me.role, timestamp: Date.now() }) JSON.stringify({ username: me.username, display_name: me.display_name || me.username, role: me.role, timestamp: Date.now() })
); );
} }
} catch {} } 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 // Suggest fetcher with debounce
const fetchSuggestions = useCallback((field, q) => { const fetchSuggestions = useCallback((field, q) => {
@@ -311,7 +589,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
field === 'site_name' ? { name: q } : { description: q } field === 'site_name' ? { name: q } : { description: q }
)); ));
} catch {} } catch {}
if (navigateImmediate) setCurrentPage("sites"); if (navigateImmediate) navigateTo("sites");
} else { } else {
// device field // device field
// Map API field -> Device_List filter key // Map API field -> Device_List filter key
@@ -327,8 +605,12 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
const qLc = String(q || '').toLowerCase(); const qLc = String(q || '').toLowerCase();
const exact = (suggestions.devices || []).find((d) => String(d.hostname || d.value || '').toLowerCase() === qLc); const exact = (suggestions.devices || []).find((d) => String(d.hostname || d.value || '').toLowerCase() === qLc);
if (exact && (exact.hostname || '').trim()) { if (exact && (exact.hostname || '').trim()) {
setSelectedDevice({ hostname: exact.hostname.trim() }); const device = { hostname: exact.hostname.trim() };
if (navigateImmediate) setCurrentPage('device_details'); if (navigateImmediate) {
navigateTo('device_details', { device });
} else {
setSelectedDevice(device);
}
} else if (field === 'hostname') { } else if (field === 'hostname') {
// Probe device existence and open directly if found // Probe device existence and open directly if found
try { try {
@@ -336,30 +618,34 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
if (resp.ok) { if (resp.ok) {
const data = await resp.json(); const data = await resp.json();
if (data && (data.summary?.hostname || Object.keys(data).length > 0)) { if (data && (data.summary?.hostname || Object.keys(data).length > 0)) {
setSelectedDevice({ hostname: q }); const device = { hostname: q };
if (navigateImmediate) setCurrentPage('device_details'); if (navigateImmediate) {
navigateTo('device_details', { device });
} else {
setSelectedDevice(device);
}
} else { } else {
try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {} try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {}
if (navigateImmediate) setCurrentPage('devices'); if (navigateImmediate) navigateTo('devices');
} }
} else { } else {
try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {} try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {}
if (navigateImmediate) setCurrentPage('devices'); if (navigateImmediate) navigateTo('devices');
} }
} catch { } catch {
try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {} try { localStorage.setItem('device_list_initial_filters', JSON.stringify({ [k]: q })); } catch {}
if (navigateImmediate) setCurrentPage('devices'); if (navigateImmediate) navigateTo('devices');
} }
} else { } else {
try { try {
const payload = (k === 'serialNumber') ? {} : { [k]: q }; const payload = (k === 'serialNumber') ? {} : { [k]: q };
localStorage.setItem('device_list_initial_filters', JSON.stringify(payload)); localStorage.setItem('device_list_initial_filters', JSON.stringify(payload));
} catch {} } catch {}
if (navigateImmediate) setCurrentPage("devices"); if (navigateImmediate) navigateTo("devices");
} }
} }
setSearchOpen(false); setSearchOpen(false);
}, [SEARCH_CATEGORIES, setCurrentPage, suggestions.devices]); }, [SEARCH_CATEGORIES, navigateTo, suggestions.devices]);
const handleLoginSuccess = ({ username, role }) => { const handleLoginSuccess = ({ username, role }) => {
setUser(username); setUser(username);
@@ -383,6 +669,12 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
} }
} catch {} } catch {}
})(); })();
if (pendingPathRef.current) {
navigateByPath(pendingPathRef.current, { replace: true, allowUnauthenticated: true });
pendingPathRef.current = null;
} else {
navigateTo('devices', { replace: true, allowUnauthenticated: true });
}
}; };
useEffect(() => { useEffect(() => {
@@ -440,6 +732,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
setUser(null); setUser(null);
setUserRole(null); setUserRole(null);
setUserDisplayName(null); setUserDisplayName(null);
navigateTo('login', { replace: true, allowUnauthenticated: true, suppressPending: true });
}; };
const handleTabRightClick = (evt, tabId) => { const handleTabRightClick = (evt, tabId) => {
@@ -526,7 +819,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
} }
]); ]);
setActiveTabId(newId); setActiveTabId(newId);
setCurrentPage("workflow-editor"); navigateTo("workflow-editor");
} catch (err) { } catch (err) {
console.error("Failed to import workflow:", err); console.error("Failed to import workflow:", err);
} }
@@ -534,7 +827,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
reader.readAsText(file); reader.readAsText(file);
e.target.value = ""; e.target.value = "";
}, },
[setTabs] [navigateTo, setTabs]
); );
const handleSaveFlow = useCallback( const handleSaveFlow = useCallback(
@@ -582,9 +875,9 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
|| currentPage === 'agent_devices'; || currentPage === 'agent_devices';
if (!isAdmin && requiresAdmin) { if (!isAdmin && requiresAdmin) {
setNotAuthorizedOpen(true); setNotAuthorizedOpen(true);
setCurrentPage('devices'); navigateTo('devices', { replace: true, suppressPending: true });
} }
}, [currentPage, isAdmin]); }, [currentPage, isAdmin, navigateTo]);
const renderMainContent = () => { const renderMainContent = () => {
switch (currentPage) { switch (currentPage) {
@@ -595,7 +888,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
try { try {
localStorage.setItem('device_list_initial_site_filter', String(siteName || '')); localStorage.setItem('device_list_initial_site_filter', String(siteName || ''));
} catch {} } catch {}
setCurrentPage("devices"); navigateTo("devices");
}} }}
/> />
); );
@@ -603,8 +896,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
return ( return (
<DeviceList <DeviceList
onSelectDevice={(d) => { onSelectDevice={(d) => {
setSelectedDevice(d); navigateTo("device_details", { device: d });
setCurrentPage("device_details");
}} }}
/> />
); );
@@ -612,8 +904,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
return ( return (
<AgentDevices <AgentDevices
onSelectDevice={(d) => { onSelectDevice={(d) => {
setSelectedDevice(d); navigateTo("device_details", { device: d });
setCurrentPage("device_details");
}} }}
/> />
); );
@@ -627,7 +918,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
<DeviceDetails <DeviceDetails
device={selectedDevice} device={selectedDevice}
onBack={() => { onBack={() => {
setCurrentPage("devices"); navigateTo("devices");
setSelectedDevice(null); setSelectedDevice(null);
}} }}
/> />
@@ -636,8 +927,8 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
case "jobs": case "jobs":
return ( return (
<ScheduledJobsList <ScheduledJobsList
onCreateJob={() => { setEditingJob(null); setCurrentPage("create_job"); }} onCreateJob={() => { setEditingJob(null); navigateTo("create_job"); }}
onEditJob={(job) => { setEditingJob(job); setCurrentPage("create_job"); }} onEditJob={(job) => { setEditingJob(job); navigateTo("create_job"); }}
refreshToken={jobsRefreshToken} refreshToken={jobsRefreshToken}
/> />
); );
@@ -646,8 +937,8 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
return ( return (
<CreateJob <CreateJob
initialJob={editingJob} initialJob={editingJob}
onCancel={() => { setCurrentPage("jobs"); setEditingJob(null); }} onCancel={() => { navigateTo("jobs"); setEditingJob(null); }}
onCreated={() => { setCurrentPage("jobs"); setEditingJob(null); setJobsRefreshToken(Date.now()); }} onCreated={() => { navigateTo("jobs"); setEditingJob(null); setJobsRefreshToken(Date.now()); }}
/> />
); );
@@ -671,7 +962,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
setTabs([{ id: newId, tab_name: name || "Flow", nodes: [], edges: [], folderPath: folderPath || "" }]); setTabs([{ id: newId, tab_name: name || "Flow", nodes: [], edges: [], folderPath: folderPath || "" }]);
} }
setActiveTabId(newId); setActiveTabId(newId);
setCurrentPage("workflow-editor"); navigateTo("workflow-editor");
}} }}
onOpenScript={(rel, mode, context) => { onOpenScript={(rel, mode, context) => {
const nonce = Date.now(); const nonce = Date.now();
@@ -681,7 +972,14 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
context: context ? { ...context, nonce } : null, context: context ? { ...context, nonce } : null,
nonce nonce
}); });
setCurrentPage(mode === 'ansible' ? 'ansible_editor' : 'scripts'); navigateTo(mode === 'ansible' ? 'ansible_editor' : 'scripts', {
assemblyState: {
path: rel || '',
mode,
context: context ? { ...context, nonce } : null,
nonce
}
});
}} }}
/> />
); );
@@ -706,7 +1004,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
setTabs([{ id: newId, tab_name: name || "Flow", nodes: [], edges: [], folderPath: folderPath || "" }]); setTabs([{ id: newId, tab_name: name || "Flow", nodes: [], edges: [], folderPath: folderPath || "" }]);
} }
setActiveTabId(newId); setActiveTabId(newId);
setCurrentPage("workflow-editor"); navigateTo("workflow-editor");
}} }}
onOpenScript={(rel, mode, context) => { onOpenScript={(rel, mode, context) => {
const nonce = Date.now(); const nonce = Date.now();
@@ -716,7 +1014,14 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
context: context ? { ...context, nonce } : null, context: context ? { ...context, nonce } : null,
nonce nonce
}); });
setCurrentPage(mode === 'ansible' ? 'ansible_editor' : 'scripts'); navigateTo(mode === 'ansible' ? 'ansible_editor' : 'scripts', {
assemblyState: {
path: rel || '',
mode,
context: context ? { ...context, nonce } : null,
nonce
}
});
}} }}
/> />
); );
@@ -730,7 +1035,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
onConsumeInitialData={() => onConsumeInitialData={() =>
setAssemblyEditorState((prev) => (prev && prev.mode === 'scripts' ? null : prev)) setAssemblyEditorState((prev) => (prev && prev.mode === 'scripts' ? null : prev))
} }
onSaved={() => setCurrentPage('assemblies')} onSaved={() => navigateTo('assemblies')}
/> />
); );
@@ -743,7 +1048,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
onConsumeInitialData={() => onConsumeInitialData={() =>
setAssemblyEditorState((prev) => (prev && prev.mode === 'ansible' ? null : prev)) setAssemblyEditorState((prev) => (prev && prev.mode === 'ansible' ? null : prev))
} }
onSaved={() => setCurrentPage('assemblies')} onSaved={() => navigateTo('assemblies')}
/> />
); );
@@ -857,7 +1162,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
return ( return (
<Button <Button
key={idx} key={idx}
onClick={() => setCurrentPage(c.page)} onClick={() => navigateTo(c.page)}
size="small" size="small"
sx={{ sx={{
color: "#7db7ff", color: "#7db7ff",
@@ -958,7 +1263,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
? highlightText(secVal, searchQuery) ? highlightText(secVal, searchQuery)
: (d.internal_ip || d.external_ip || d.description || d.last_user || ''); : (d.internal_ip || d.external_ip || d.description || d.last_user || '');
return ( return (
<Box key={idx} onClick={() => { setSelectedDevice({ hostname: d.hostname || d.value }); setCurrentPage('device_details'); setSearchOpen(false); }} sx={{ px: 1.2, py: 0.6, '&:hover': { bgcolor: '#22272e' }, cursor: 'pointer' }}> <Box key={idx} onClick={() => { navigateTo('device_details', { device: { hostname: d.hostname || d.value } }); setSearchOpen(false); }} sx={{ px: 1.2, py: 0.6, '&:hover': { bgcolor: '#22272e' }, cursor: 'pointer' }}>
<Typography variant="body2" sx={{ color: '#e8eaed' }}>{primary}</Typography> <Typography variant="body2" sx={{ color: '#e8eaed' }}>{primary}</Typography>
<Typography variant="caption" sx={{ color: '#9aa0a6' }}> <Typography variant="caption" sx={{ color: '#9aa0a6' }}>
{d.site_name || ''}{(d.site_name && (secVal || (d.internal_ip || d.external_ip || d.description || d.last_user))) ? ' • ' : ''}{secHighlighted} {d.site_name || ''}{(d.site_name && (secVal || (d.internal_ip || d.external_ip || d.description || d.last_user))) ? ' • ' : ''}{secHighlighted}
@@ -1012,7 +1317,7 @@ const LOCAL_STORAGE_KEY = "borealis_persistent_state";
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Box sx={{ display: "flex", flexGrow: 1, overflow: "auto", minHeight: 0 }}> <Box sx={{ display: "flex", flexGrow: 1, overflow: "auto", minHeight: 0 }}>
<NavigationSidebar currentPage={currentPage} onNavigate={setCurrentPage} isAdmin={isAdmin} /> <NavigationSidebar currentPage={currentPage} onNavigate={navigateTo} isAdmin={isAdmin} />
<Box <Box
sx={{ sx={{
flexGrow: 1, flexGrow: 1,