Redesigned Device List

This commit is contained in:
2025-11-06 16:04:32 -07:00
parent a9b2b9d998
commit e35ddd1842
2 changed files with 492 additions and 132 deletions

View File

@@ -27,8 +27,8 @@ import AddDevice from "./Add_Device.jsx";
ModuleRegistry.registerModules([AllCommunityModule]); ModuleRegistry.registerModules([AllCommunityModule]);
const myTheme = themeQuartz.withParams({ const myTheme = themeQuartz.withParams({
accentColor: "#FFA6FF", accentColor: "#8b5cf6",
backgroundColor: "#1f2836", backgroundColor: "#070b1a",
browserColorScheme: "dark", browserColorScheme: "dark",
chromeBackgroundColor: { chromeBackgroundColor: {
ref: "foregroundColor", ref: "foregroundColor",
@@ -38,7 +38,7 @@ const myTheme = themeQuartz.withParams({
fontFamily: { fontFamily: {
googleFont: "IBM Plex Sans", googleFont: "IBM Plex Sans",
}, },
foregroundColor: "#FFF", foregroundColor: "#f4f7ff",
headerFontSize: 14, headerFontSize: 14,
}); });
@@ -46,6 +46,86 @@ const themeClassName = myTheme.themeName || "ag-theme-quartz";
const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif'; const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif';
const iconFontFamily = '"Quartz Regular"'; 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 (
<Box
sx={{
minWidth: 160,
px: 2,
py: 1.5,
borderRadius: 2,
border: `1px solid rgba(255,255,255,0.08)`,
background: gradient,
boxShadow: "0 15px 45px rgba(5, 8, 28, 0.45)",
display: "flex",
flexDirection: "column",
gap: 0.5,
}}
>
<Typography sx={{ fontSize: "0.75rem", letterSpacing: 0.6, textTransform: "uppercase", color: "rgba(255,255,255,0.68)" }}>
{label}
</Typography>
<Typography sx={{ fontSize: "1.8rem", fontWeight: 700, color: "#f8fafc", lineHeight: 1 }}>
{value}
</Typography>
{meta ? (
<Typography sx={{ fontSize: "0.85rem", color: "rgba(226,232,240,0.75)" }}>{meta}</Typography>
) : null}
</Box>
);
});
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 getOsIconClass = (osName) => {
const value = (osName || "").toString().toLowerCase(); const value = (osName || "").toString().toLowerCase();
if (!value) return ""; if (!value) return "";
@@ -513,6 +593,105 @@ export default function DeviceList({
return agentHash === repo ? "Up-to-Date" : "Needs Updated"; 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 fetchDevices = useCallback(async (options = {}) => {
const { refreshRepo = false } = options || {}; const { refreshRepo = false } = options || {};
let repoSha = repoHash; let repoSha = repoHash;
@@ -1363,119 +1542,215 @@ export default function DeviceList({
return ( return (
<Paper <Paper
sx={{ sx={{
m: 2, m: 0,
p: 0, p: 0,
bgcolor: "#1e1e1e",
fontFamily: gridFontFamily, fontFamily: gridFontFamily,
color: "#f5f7fa", color: MAGIC_UI.textBright,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
flexGrow: 1, flexGrow: 1,
minWidth: 0, minWidth: 0,
height: "100%", height: "100%",
borderRadius: 0,
border: `1px solid ${MAGIC_UI.panelBorder}`,
background: MAGIC_UI.shellBg,
boxShadow: MAGIC_UI.glow,
position: "relative",
overflow: "hidden",
}} }}
elevation={2} elevation={0}
> >
{/* Header area with title on left and controls on right */} <Box sx={{ position: "relative", zIndex: 1, p: { xs: 2, md: 3 }, pb: 2 }}>
<Box sx={{ p: 2, pb: 1, display: "flex", flexDirection: 'column', gap: 1 }}> <Box
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> sx={{
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}> borderRadius: 4,
{computedTitle} border: `1px solid ${MAGIC_UI.panelBorder}`,
</Typography> background: MAGIC_UI.panelBg,
<Box sx={{ display: 'flex', alignItems: 'center' }}> boxShadow: MAGIC_UI.glow,
{/* Views dropdown + add button */} p: { xs: 2, md: 3 },
<Box sx={{ display: 'flex', alignItems: 'center', mr: 1 }}> display: "flex",
<TextField flexWrap: "wrap",
select gap: 3,
size="small" overflow: "hidden",
value={selectedViewId} position: "relative",
onChange={(e) => { isolation: "isolate",
const val = e.target.value; }}
setSelectedViewId(val); >
if (val === "default") applyView({ id: "default" }); <Box
else { sx={{
const v = views.find((x) => String(x.id) === String(val)); flex: "1 1 320px",
if (v) applyView(v); minWidth: 0,
} display: "flex",
}} flexDirection: "column",
sx={{ gap: 1.25,
minWidth: 220, position: "relative",
mr: 0, zIndex: 1,
'& .MuiOutlinedInput-root': { }}
height: 32, >
pr: 0, <Typography
borderTopRightRadius: 0, variant="h5"
borderBottomRightRadius: 0, sx={{
'& fieldset': { borderColor: '#555', borderRight: '1px solid #555' }, fontWeight: 700,
'&:hover fieldset': { borderColor: '#888' }, letterSpacing: 0.6,
}, color: MAGIC_UI.textBright,
'& .MuiSelect-select': { textShadow: "0 10px 30px rgba(0,0,0,0.45)",
display: 'flex', }}
alignItems: 'center', >
py: 0, {computedTitle}
}, </Typography>
}} <Typography sx={{ color: MAGIC_UI.textMuted, maxWidth: 560 }}>{heroSubtitle}</Typography>
SelectProps={{ <Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
MenuProps: { {hasActiveFilters ? (
PaperProps: { sx: { bgcolor: '#1e1e1e', color: '#fff' } }, <Box sx={HERO_BADGE_SX}>
}, <span>Filters</span>
renderValue: (val) => { <Typography component="span" sx={{ fontWeight: 700, fontSize: "0.8rem", color: MAGIC_UI.accentA }}>
if (val === "default") return "Default View"; {activeFilterCount}
const v = views.find((x) => String(x.id) === String(val)); </Typography>
return v ? v.name : "Default View"; </Box>
} ) : null}
}} {selectedIds.size > 0 ? (
> <Box sx={HERO_BADGE_SX}>
<MenuItem value="default">Default View</MenuItem> <span>Selected</span>
{views.map((v) => ( <Typography component="span" sx={{ fontWeight: 700, fontSize: "0.8rem", color: MAGIC_UI.accentB }}>
<MenuItem key={v.id} value={v.id} disableRipple> {selectedIds.size}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}> </Typography>
<span>{v.name}</span> </Box>
<IconButton ) : null}
size="small"
onClick={(e) => {
e.stopPropagation();
setViewActionAnchor(e.currentTarget);
setViewActionTarget(v);
}}
sx={{ color: '#ccc' }}
>
<MoreVertIcon fontSize="small" />
</IconButton>
</Box>
</MenuItem>
))}
</TextField>
<IconButton
size="small"
onClick={() => { 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,
}}
>
<AddIcon fontSize="small" />
</IconButton>
</Box> </Box>
<Tooltip title="Refresh Devices to Detect Changes"> </Box>
<IconButton <Box sx={{ flex: "1 1 320px", minWidth: 0, position: "relative", zIndex: 1 }}>
size="small" <Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 1.2 }}>
onClick={() => fetchDevices({ refreshRepo: true })} {statTiles.map((tile) => (
sx={{ color: "#bbb", mr: 1 }} <StatTile key={tile.key} {...tile} />
> ))}
<CachedIcon fontSize="small" /> </Box>
</IconButton> </Box>
</Box>
</Box>
<Box sx={{ px: { xs: 2, md: 3 }, pb: 1.5 }}>
<Typography sx={{ fontSize: "0.72rem", color: MAGIC_UI.textMuted, textTransform: "uppercase", letterSpacing: 0.45, mb: 0.5 }}>
Custom View
</Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1, alignItems: "center" }}>
<Box sx={{ flex: "1 1 260px", minWidth: 220, display: "flex", alignItems: "center" }}>
<TextField
select
size="small"
value={selectedViewId}
onChange={(e) => {
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";
},
}}
>
<MenuItem value="default">Default View</MenuItem>
{views.map((v) => (
<MenuItem key={v.id} value={v.id} disableRipple>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
<span>{v.name}</span>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
setViewActionAnchor(e.currentTarget);
setViewActionTarget(v);
}}
sx={{ color: "#ccc" }}
>
<MoreVertIcon fontSize="small" />
</IconButton>
</Box>
</MenuItem>
))}
</TextField>
<IconButton
size="small"
onClick={() => {
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 },
}}
>
<AddIcon fontSize="small" />
</IconButton>
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 0.75, flexWrap: "wrap", ml: "auto" }}>
<Button
variant="contained"
size="small"
disabled={selectedIds.size === 0}
disableElevation
onClick={() => setQuickJobOpen(true)}
sx={{
borderRadius: 999,
px: 2.2,
textTransform: "none",
fontWeight: 600,
background: selectedIds.size === 0 ? "rgba(148,163,184,0.2)" : "linear-gradient(135deg, #34d399, #22d3ee)",
color: selectedIds.size === 0 ? MAGIC_UI.textMuted : "#041224",
border: selectedIds.size === 0 ? "1px solid rgba(148,163,184,0.35)" : "none",
boxShadow: selectedIds.size === 0 ? "none" : "0 15px 35px rgba(45, 212, 191, 0.35)",
}}
>
Quick Job
</Button>
<Tooltip title="Refresh devices to detect changes">
<span>
<IconButton
size="small"
onClick={() => fetchDevices({ refreshRepo: true })}
sx={{ color: MAGIC_UI.textBright, border: "1px solid rgba(148,163,184,0.35)", borderRadius: 2 }}
>
<CachedIcon fontSize="small" />
</IconButton>
</span>
</Tooltip> </Tooltip>
<Tooltip title="Column Chooser"> <Tooltip title="Column chooser">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => setColChooserAnchor(e.currentTarget)} onClick={(e) => setColChooserAnchor(e.currentTarget)}
sx={{ color: "#bbb", mr: 1 }} sx={{ color: MAGIC_UI.textBright, border: "1px solid rgba(148,163,184,0.35)", borderRadius: 2 }}
> >
<ViewColumnIcon fontSize="small" /> <ViewColumnIcon fontSize="small" />
</IconButton> </IconButton>
@@ -1485,7 +1760,8 @@ export default function DeviceList({
variant="contained" variant="contained"
size="small" size="small"
startIcon={<AddIcon />} startIcon={<AddIcon />}
sx={{ bgcolor: "#58a6ff", color: "#0b0f19" }} disableElevation
sx={RAINBOW_BUTTON_SX}
onClick={() => { onClick={() => {
setAddDeviceType(derivedDefaultType ?? null); setAddDeviceType(derivedDefaultType ?? null);
setAddDeviceOpen(true); setAddDeviceOpen(true);
@@ -1496,25 +1772,8 @@ export default function DeviceList({
)} )}
</Box> </Box>
</Box> </Box>
{/* Second row: Quick Job button aligned under header title */}
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="outlined"
size="small"
disabled={selectedIds.size === 0}
onClick={() => setQuickJobOpen(true)}
sx={{
color: selectedIds.size === 0 ? "#666" : "#58a6ff",
borderColor: selectedIds.size === 0 ? "#333" : "#58a6ff",
textTransform: "none"
}}
>
Quick Job
</Button>
</Box>
</Box> </Box>
{/* The Size of the Grid itself and its margins relative to the overall page */} <Box sx={{ px: { xs: 2, md: 3 }, pb: 3, flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
<Box sx={{ mt: '10px', px: 2, pb: 2, flexGrow: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
<Box <Box
className={gridWrapperClass} className={gridWrapperClass}
sx={{ sx={{
@@ -1525,13 +1784,41 @@ export default function DeviceList({
fontFamily: gridFontFamily, fontFamily: gridFontFamily,
"--ag-font-family": gridFontFamily, "--ag-font-family": gridFontFamily,
"--ag-icon-font-family": iconFontFamily, "--ag-icon-font-family": iconFontFamily,
background: "linear-gradient(165deg, rgba(2,6,23,0.9), rgba(8,12,32,0.85))",
borderRadius: 3,
border: `1px solid ${MAGIC_UI.panelBorder}`,
boxShadow: "0 20px 60px rgba(2,8,23,0.85)",
"& .ag-root-wrapper": { "& .ag-root-wrapper": {
borderRadius: 1, borderRadius: 3,
minHeight: 400, minHeight: 420,
background: "transparent",
}, },
"& .ag-root, & .ag-header, & .ag-center-cols-container, & .ag-paging-panel": { "& .ag-root, & .ag-header, & .ag-center-cols-container, & .ag-paging-panel": {
fontFamily: gridFontFamily, fontFamily: gridFontFamily,
}, },
"& .ag-header": {
backgroundColor: "rgba(15,23,42,0.9)",
borderBottom: "1px solid rgba(148,163,184,0.25)",
},
"& .ag-header-cell-label": {
color: "#e2e8f0",
fontWeight: 600,
letterSpacing: 0.3,
},
"& .ag-row": {
borderColor: "rgba(255,255,255,0.04)",
transition: "background 0.2s ease",
},
"& .ag-row:nth-of-type(even)": {
backgroundColor: "rgba(15,23,42,0.45)",
},
"& .ag-row-hover": {
backgroundColor: "rgba(124, 58, 237, 0.15) !important",
},
"& .ag-row-selected": {
backgroundColor: "rgba(56,189,248,0.18) !important",
boxShadow: "inset 0 0 0 1px rgba(56,189,248,0.35)",
},
"& .ag-icon": { "& .ag-icon": {
fontFamily: iconFontFamily, fontFamily: iconFontFamily,
}, },
@@ -1569,7 +1856,8 @@ export default function DeviceList({
rowSelection="multiple" rowSelection="multiple"
rowMultiSelectWithClick rowMultiSelectWithClick
pagination pagination
paginationPageSize={25} paginationPageSize={20}
paginationPageSizeSelector={[20, 50, 100]}
animateRows animateRows
onSelectionChanged={handleSelectionChanged} onSelectionChanged={handleSelectionChanged}
onFilterChanged={handleFilterChanged} onFilterChanged={handleFilterChanged}
@@ -1590,7 +1878,15 @@ export default function DeviceList({
anchorEl={viewActionAnchor} anchorEl={viewActionAnchor}
open={Boolean(viewActionAnchor)} open={Boolean(viewActionAnchor)}
onClose={() => { setViewActionAnchor(null); setViewActionTarget(null); }} onClose={() => { 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)",
},
}}
> >
<MenuItem onClick={() => { <MenuItem onClick={() => {
const v = viewActionTarget; const v = viewActionTarget;
@@ -1677,7 +1973,16 @@ export default function DeviceList({
anchorEl={colChooserAnchor} anchorEl={colChooserAnchor}
onClose={() => setColChooserAnchor(null)} onClose={() => setColChooserAnchor(null)}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }} 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)",
},
}}
> >
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 1 }}>
{Object.entries(COL_LABELS) {Object.entries(COL_LABELS)
@@ -1709,7 +2014,12 @@ export default function DeviceList({
size="small" size="small"
variant="outlined" variant="outlined"
onClick={() => setColumns(defaultColumns)} 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 Reset Default
</Button> </Button>
@@ -1720,7 +2030,15 @@ export default function DeviceList({
anchorEl={menuAnchor} anchorEl={menuAnchor}
open={Boolean(menuAnchor)} open={Boolean(menuAnchor)}
onClose={closeMenu} 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)",
},
}}
> >
<MenuItem onClick={async () => { <MenuItem onClick={async () => {
closeMenu(); closeMenu();
@@ -1765,7 +2083,16 @@ export default function DeviceList({
onClose={() => setAssignDialogOpen(false)} onClose={() => setAssignDialogOpen(false)}
anchorReference="anchorPosition" anchorReference="anchorPosition"
anchorPosition={{ top: Math.max(Math.floor(window.innerHeight*0.5), 200), left: Math.max(Math.floor(window.innerWidth*0.5), 300) }} 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)",
},
}}
> >
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="subtitle1">Assign {assignTargets.length} device(s) to a site</Typography> <Typography variant="subtitle1">Assign {assignTargets.length} device(s) to a site</Typography>
@@ -1775,14 +2102,32 @@ export default function DeviceList({
label="Select Site" label="Select Site"
value={assignSiteId ?? ''} value={assignSiteId ?? ''}
onChange={(e) => setAssignSiteId(Number(e.target.value))} 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) => ( {sites.map((s) => (
<MenuItem key={s.id} value={s.id}>{s.name}</MenuItem> <MenuItem key={s.id} value={s.id}>{s.name}</MenuItem>
))} ))}
</TextField> </TextField>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
<Button variant="outlined" size="small" onClick={() => setAssignDialogOpen(false)} sx={{ textTransform: 'none', borderColor: '#555', color: '#bbb' }}>Cancel</Button> <Button
variant="outlined"
size="small"
onClick={() => setAssignDialogOpen(false)}
sx={{
textTransform: 'none',
borderColor: 'rgba(148,163,184,0.4)',
color: MAGIC_UI.textBright,
}}
>
Cancel
</Button>
<Button <Button
variant="outlined" variant="outlined"
size="small" size="small"
@@ -1806,7 +2151,11 @@ export default function DeviceList({
setRows((prev) => prev.map((r) => ({ ...r, site: mapping[r.hostname]?.site_name || 'Not Configured' }))); setRows((prev) => prev.map((r) => ({ ...r, site: mapping[r.hostname]?.site_name || 'Not Configured' })));
} catch {} } catch {}
}} }}
sx={{ textTransform: 'none', borderColor: '#58a6ff', color: '#58a6ff' }} sx={{
textTransform: 'none',
borderColor: MAGIC_UI.accentA,
color: MAGIC_UI.accentA,
}}
> >
Assign Assign
</Button> </Button>

View File

@@ -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 1624px 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).