From e35ddd1842206257aa2011087c03a83198db291f Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Thu, 6 Nov 2025 16:04:32 -0700 Subject: [PATCH] Redesigned Device List --- .../web-interface/src/Devices/Device_List.jsx | 613 ++++++++++++++---- .../web-interface/src/GUI_SYLING_GUIDE.md | 11 + 2 files changed, 492 insertions(+), 132 deletions(-) create mode 100644 Data/Engine/web-interface/src/GUI_SYLING_GUIDE.md diff --git a/Data/Engine/web-interface/src/Devices/Device_List.jsx b/Data/Engine/web-interface/src/Devices/Device_List.jsx index e61164cb..640e9934 100644 --- a/Data/Engine/web-interface/src/Devices/Device_List.jsx +++ b/Data/Engine/web-interface/src/Devices/Device_List.jsx @@ -27,8 +27,8 @@ import AddDevice from "./Add_Device.jsx"; ModuleRegistry.registerModules([AllCommunityModule]); const myTheme = themeQuartz.withParams({ - accentColor: "#FFA6FF", - backgroundColor: "#1f2836", + accentColor: "#8b5cf6", + backgroundColor: "#070b1a", browserColorScheme: "dark", chromeBackgroundColor: { ref: "foregroundColor", @@ -38,7 +38,7 @@ const myTheme = themeQuartz.withParams({ fontFamily: { googleFont: "IBM Plex Sans", }, - foregroundColor: "#FFF", + foregroundColor: "#f4f7ff", headerFontSize: 14, }); @@ -46,6 +46,86 @@ const themeClassName = myTheme.themeName || "ag-theme-quartz"; const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif'; const iconFontFamily = '"Quartz Regular"'; +const MAGIC_UI = { + shellBg: + "radial-gradient(120% 120% at 0% 0%, rgba(76, 186, 255, 0.16), transparent 55%), " + + "radial-gradient(120% 120% at 100% 0%, rgba(214, 130, 255, 0.18), transparent 60%), #040711", + panelBg: + "linear-gradient(135deg, rgba(10, 16, 31, 0.98) 0%, rgba(6, 10, 24, 0.94) 60%, rgba(15, 6, 26, 0.96) 100%)", + panelBorder: "rgba(148, 163, 184, 0.35)", + glassBorder: "rgba(94, 234, 212, 0.35)", + glow: "0 25px 80px rgba(6, 12, 30, 0.8)", + textMuted: "#94a3b8", + textBright: "#e2e8f0", + accentA: "#7dd3fc", + accentB: "#c084fc", + accentC: "#f472b6", + warning: "#f97316", + success: "#34d399", + surfaceOverlay: "rgba(15, 23, 42, 0.72)", +}; + +const StatTile = React.memo(function StatTile({ label, value, meta, gradient }) { + return ( + + + {label} + + + {value} + + {meta ? ( + {meta} + ) : null} + + ); +}); + +const HERO_BADGE_SX = { + px: 1.5, + py: 0.4, + borderRadius: 999, + border: "1px solid rgba(255,255,255,0.18)", + background: "rgba(12,18,35,0.85)", + fontSize: "0.72rem", + letterSpacing: 0.35, + textTransform: "uppercase", + color: MAGIC_UI.textBright, + display: "inline-flex", + alignItems: "center", + gap: 0.5, +}; + +const RAINBOW_BUTTON_SX = { + borderRadius: 999, + textTransform: "none", + fontWeight: 600, + px: 2.5, + color: "#f8fafc", + border: "1px solid transparent", + backgroundImage: + "linear-gradient(#05070f, #05070f), linear-gradient(120deg, #ff7c7c, #ffd36b, #7dffb7, #7dd3fc, #c084fc)", + backgroundOrigin: "border-box", + backgroundClip: "padding-box, border-box", + boxShadow: "0 18px 40px rgba(129, 140, 248, 0.35)", + "&:hover": { + boxShadow: "0 25px 55px rgba(99, 102, 241, 0.45)", + }, +}; + const getOsIconClass = (osName) => { const value = (osName || "").toString().toLowerCase(); if (!value) return ""; @@ -513,6 +593,105 @@ export default function DeviceList({ return agentHash === repo ? "Up-to-Date" : "Needs Updated"; }, []); + const heroStats = useMemo(() => { + const now = Date.now() / 1000; + const siteSet = new Set(); + let online = 0; + let offline = 0; + let stale = 0; + let needsUpdate = 0; + rows.forEach((row) => { + const lastSeen = + row.lastSeen ?? + row.summary?.last_seen ?? + row.summary?.lastSeen ?? + row.summary?.last_heartbeat; + if (lastSeen && now - lastSeen > 3600) { + stale += 1; + } + const siteName = (row.site || row.summary?.site_name || "").trim(); + if (siteName && siteName.toLowerCase() !== "not configured") { + siteSet.add(siteName); + } + const statusRaw = + row.status || + row.summary?.status || + statusFromHeartbeat(lastSeen); + if ((statusRaw || "").toLowerCase() === "online") online += 1; + else offline += 1; + const agentHash = + row.agentHash || + row.summary?.agent_hash || + row.summary?.agentHash || + row.summary?.agent_hash_value; + if (repoHash && computeAgentVersion(agentHash, repoHash) === "Needs Updated") { + needsUpdate += 1; + } + }); + return { + total: rows.length, + online, + offline, + sites: siteSet.size, + stale, + needsUpdate, + }; + }, [rows, repoHash, computeAgentVersion]); + + const shortRepoSha = useMemo(() => (repoHash || "").slice(0, 7), [repoHash]); + + const statTiles = useMemo(() => { + const total = heroStats.total || 1; + const onlinePct = Math.round((heroStats.online / total) * 100); + return [ + { + key: "online", + label: "Online", + value: heroStats.online, + meta: `${onlinePct}% live`, + gradient: "linear-gradient(135deg, rgba(56, 189, 248, 0.35), rgba(34, 197, 94, 0.45))", + }, + { + key: "stale", + label: "Stale (>1h)", + value: heroStats.stale, + meta: heroStats.stale ? "Needs attention" : "All synced", + gradient: "linear-gradient(135deg, rgba(249, 115, 22, 0.55), rgba(239, 68, 68, 0.55))", + }, + { + key: "updates", + label: "Needs Agent Update", + value: heroStats.needsUpdate, + meta: repoHash ? `Repo Hash: ${shortRepoSha}` : "Syncing repo…", + gradient: "linear-gradient(135deg, rgba(192, 132, 252, 0.4), rgba(14, 165, 233, 0.35))", + }, + { + key: "sites", + label: "Sites", + value: heroStats.sites, + meta: heroStats.sites === 1 ? "Single site" : "Multi-site", + gradient: "linear-gradient(135deg, rgba(125, 183, 255, 0.45), rgba(148, 163, 184, 0.35))", + }, + ]; + }, [heroStats, repoHash, shortRepoSha]); + + const activeFilterCount = useMemo( + () => Object.keys(filters || {}).length, + [filters] + ); + const hasActiveFilters = activeFilterCount > 0; + + const heroSubtitle = useMemo(() => { + if (!heroStats.total) { + return "Connect your first device to start streaming telemetry into Borealis."; + } + const sitePart = + heroStats.sites > 0 + ? `across ${heroStats.sites} ${heroStats.sites === 1 ? "managed site" : "managed sites"}` + : "across emerging sites"; + return `Monitoring ${heroStats.total} managed endpoint(s) ${sitePart}.`; + }, [heroStats]); + const fetchDevices = useCallback(async (options = {}) => { const { refreshRepo = false } = options || {}; let repoSha = repoHash; @@ -1363,119 +1542,215 @@ export default function DeviceList({ return ( - {/* Header area with title on left and controls on right */} - - - - {computedTitle} - - - {/* Views dropdown + add button */} - - { - const val = e.target.value; - setSelectedViewId(val); - if (val === "default") applyView({ id: "default" }); - else { - const v = views.find((x) => String(x.id) === String(val)); - if (v) applyView(v); - } - }} - sx={{ - minWidth: 220, - mr: 0, - '& .MuiOutlinedInput-root': { - height: 32, - pr: 0, - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - '& fieldset': { borderColor: '#555', borderRight: '1px solid #555' }, - '&:hover fieldset': { borderColor: '#888' }, - }, - '& .MuiSelect-select': { - display: 'flex', - alignItems: 'center', - py: 0, - }, - }} - SelectProps={{ - MenuProps: { - PaperProps: { sx: { bgcolor: '#1e1e1e', color: '#fff' } }, - }, - renderValue: (val) => { - if (val === "default") return "Default View"; - const v = views.find((x) => String(x.id) === String(val)); - return v ? v.name : "Default View"; - } - }} - > - Default View - {views.map((v) => ( - - - {v.name} - { - e.stopPropagation(); - setViewActionAnchor(e.currentTarget); - setViewActionTarget(v); - }} - sx={{ color: '#ccc' }} - > - - - - - ))} - - { setNewViewName(""); setCreateDialogOpen(true); }} - sx={{ - ml: '-1px', - border: '1px solid #555', - borderLeft: '1px solid #555', - borderRadius: '0 4px 4px 0', - color: '#bbb', - height: 32, - width: 32, - }} - > - - + + + + + {computedTitle} + + {heroSubtitle} + + {hasActiveFilters ? ( + + Filters + + {activeFilterCount} + + + ) : null} + {selectedIds.size > 0 ? ( + + Selected + + {selectedIds.size} + + + ) : null} - - fetchDevices({ refreshRepo: true })} - sx={{ color: "#bbb", mr: 1 }} - > - - + + + + {statTiles.map((tile) => ( + + ))} + + + + + + + Custom View + + + + { + const val = e.target.value; + setSelectedViewId(val); + if (val === "default") applyView({ id: "default" }); + else { + const v = views.find((x) => String(x.id) === String(val)); + if (v) applyView(v); + } + }} + sx={{ + minWidth: 220, + mr: 0, + "& .MuiOutlinedInput-root": { + height: 36, + pr: 0, + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + background: "rgba(4,7,17,0.6)", + "& fieldset": { borderColor: "rgba(148,163,184,0.4)", borderRight: "1px solid rgba(148,163,184,0.4)" }, + "&:hover fieldset": { borderColor: MAGIC_UI.accentA }, + }, + "& .MuiSelect-select": { + display: "flex", + alignItems: "center", + py: 0, + }, + }} + SelectProps={{ + MenuProps: { + PaperProps: { sx: { bgcolor: "rgba(8,12,24,0.98)", color: "#fff" } }, + }, + renderValue: (val) => { + if (val === "default") return "Default View"; + const v = views.find((x) => String(x.id) === String(val)); + return v ? v.name : "Default View"; + }, + }} + > + Default View + {views.map((v) => ( + + + {v.name} + { + e.stopPropagation(); + setViewActionAnchor(e.currentTarget); + setViewActionTarget(v); + }} + sx={{ color: "#ccc" }} + > + + + + + ))} + + { + setNewViewName(""); + setCreateDialogOpen(true); + }} + sx={{ + ml: "-1px", + border: "1px solid rgba(148,163,184,0.4)", + borderRadius: "0 8px 8px 0", + color: MAGIC_UI.textBright, + height: 36, + width: 36, + background: "rgba(12,18,35,0.8)", + "&:hover": { borderColor: MAGIC_UI.accentA }, + }} + > + + + + + + + + fetchDevices({ refreshRepo: true })} + sx={{ color: MAGIC_UI.textBright, border: "1px solid rgba(148,163,184,0.35)", borderRadius: 2 }} + > + + + - + setColChooserAnchor(e.currentTarget)} - sx={{ color: "#bbb", mr: 1 }} + sx={{ color: MAGIC_UI.textBright, border: "1px solid rgba(148,163,184,0.35)", borderRadius: 2 }} > @@ -1485,7 +1760,8 @@ export default function DeviceList({ variant="contained" size="small" startIcon={} - sx={{ bgcolor: "#58a6ff", color: "#0b0f19" }} + disableElevation + sx={RAINBOW_BUTTON_SX} onClick={() => { setAddDeviceType(derivedDefaultType ?? null); setAddDeviceOpen(true); @@ -1496,25 +1772,8 @@ export default function DeviceList({ )} - {/* Second row: Quick Job button aligned under header title */} - - - - {/* The Size of the Grid itself and its margins relative to the overall page */} - + { setViewActionAnchor(null); setViewActionTarget(null); }} - PaperProps={{ sx: { bgcolor: '#1e1e1e', color: '#fff', fontSize: '13px' } }} + PaperProps={{ + sx: { + bgcolor: "rgba(8,12,24,0.96)", + color: "#fff", + fontSize: "13px", + border: "1px solid rgba(148,163,184,0.3)", + backdropFilter: "blur(16px)", + }, + }} > { const v = viewActionTarget; @@ -1677,7 +1973,16 @@ export default function DeviceList({ anchorEl={colChooserAnchor} onClose={() => setColChooserAnchor(null)} anchorOrigin={{ vertical: "bottom", horizontal: "right" }} - PaperProps={{ sx: { bgcolor: "#1e1e1e", color: '#fff', p: 1 } }} + PaperProps={{ + sx: { + bgcolor: "rgba(8,12,24,0.96)", + color: "#fff", + p: 1, + border: "1px solid rgba(148,163,184,0.3)", + boxShadow: "0 12px 30px rgba(2,8,23,0.8)", + backdropFilter: "blur(14px)", + }, + }} > {Object.entries(COL_LABELS) @@ -1709,7 +2014,12 @@ export default function DeviceList({ size="small" variant="outlined" onClick={() => setColumns(defaultColumns)} - sx={{ textTransform: 'none', borderColor: '#555', color: '#bbb' }} + sx={{ + textTransform: 'none', + borderColor: 'rgba(148,163,184,0.4)', + color: MAGIC_UI.textBright, + '&:hover': { borderColor: MAGIC_UI.accentA }, + }} > Reset Default @@ -1720,7 +2030,15 @@ export default function DeviceList({ anchorEl={menuAnchor} open={Boolean(menuAnchor)} onClose={closeMenu} - PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }} + PaperProps={{ + sx: { + bgcolor: "rgba(8,12,24,0.96)", + color: "#fff", + fontSize: "13px", + border: "1px solid rgba(148,163,184,0.3)", + backdropFilter: "blur(16px)", + }, + }} > { closeMenu(); @@ -1765,7 +2083,16 @@ export default function DeviceList({ onClose={() => setAssignDialogOpen(false)} anchorReference="anchorPosition" anchorPosition={{ top: Math.max(Math.floor(window.innerHeight*0.5), 200), left: Math.max(Math.floor(window.innerWidth*0.5), 300) }} - PaperProps={{ sx: { bgcolor: '#1e1e1e', color: '#fff', p: 2, minWidth: 360 } }} + PaperProps={{ + sx: { + bgcolor: "rgba(8,12,24,0.96)", + color: "#fff", + p: 2, + minWidth: 360, + border: "1px solid rgba(148,163,184,0.35)", + boxShadow: "0 16px 40px rgba(2,8,23,0.85)", + }, + }} > Assign {assignTargets.length} device(s) to a site @@ -1775,14 +2102,32 @@ export default function DeviceList({ label="Select Site" value={assignSiteId ?? ''} onChange={(e) => setAssignSiteId(Number(e.target.value))} - sx={{ '& .MuiOutlinedInput-root': { '& fieldset': { borderColor: '#444' }, '&:hover fieldset': { borderColor: '#666' } }, label: { color: '#aaa' } }} + sx={{ + '& .MuiOutlinedInput-root': { + backgroundColor: 'rgba(4,7,17,0.65)', + '& fieldset': { borderColor: 'rgba(148,163,184,0.4)' }, + '&:hover fieldset': { borderColor: MAGIC_UI.accentA }, + }, + label: { color: MAGIC_UI.textMuted }, + }} > {sites.map((s) => ( {s.name} ))} - + diff --git a/Data/Engine/web-interface/src/GUI_SYLING_GUIDE.md b/Data/Engine/web-interface/src/GUI_SYLING_GUIDE.md new file mode 100644 index 00000000..e12d5783 --- /dev/null +++ b/Data/Engine/web-interface/src/GUI_SYLING_GUIDE.md @@ -0,0 +1,11 @@ +# Borealis MagicUI Styling Guide + +- **Aurora Shells**: Page containers should sit on aurora gradients that blend deep navy (#040711) with soft cyan/violet blooms plus a subtle border (`rgba(148,163,184,0.35)`) and low, velvety drop shadows to create depth without harsh edges. +- **Glass Panels**: Primary panels/cards use glassmorphic layers (`rgba(15,23,42,0.7)`), rounded 16–24px corners, blurred backdrops, and micro borders; add radial light flares via pseudo-elements for motion while keeping content readable. +- **Hero Storytelling**: Each view begins with a stat-forward hero—gradient StatTiles (min 160px) and uppercase pills (HERO_BADGE_SX) summarize live signals, active filters, and selections so telemetry feels alive at a glance. +- **Action Surfaces**: Control bars (view selectors, tool strips) live inside translucent glass bands with generous spacing; selectors get filled dark inputs with cyan hover borders, while primary actions are pill-shaped gradients and secondary controls use soft-outline icon buttons. +- **Anchored Controls**: Align view selectors/utility buttons directly with grid edges, keeping the controls in a single horizontal row that feels docked to the data surface; reserve glass backdrops for hero sections so the content canvas stays flush. +- **Buttons & Chips**: Reserve gradient pills (`linear-gradient(135deg,#34d399,#22d3ee)` for success, `#7dd3fc→#c084fc` for creation) for primary CTAs; neutral actions rely on rounded outlines with `rgba(148,163,184,0.4)` borders and uppercase microcopy for supporting tokens. +- **Rainbow Accents**: When highlighting creation CTAs (e.g., Add Device), use dark-fill pill buttons with rainbow border gradients (dual-layer background clip) so the surface stays matte while the perimeter shimmers through the aurora palette. +- **AG Grid Treatment**: Stick with the Quartz theme but override backgrounds so headers are matte navy, alternating rows have subtle opacity shifts, and interactions (hover/selection) glow with cyan/magenta washes; rounded wrappers, soft borders, and inset selection glows keep the grid cohesive with the rest of the MagicUI surface. +- **Overlays & Menus**: Menus, popovers, and dialogs share the same `rgba(8,12,24,0.96)` canvas, blurred backdrops, and thin steel borders; keep typography bright, inputs filled with deep blue glass, and accent colors aligned (cyan for confirm, mauve for destructive).