mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 09:25:48 -07:00
Redesigned Device List
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
11
Data/Engine/web-interface/src/GUI_SYLING_GUIDE.md
Normal file
11
Data/Engine/web-interface/src/GUI_SYLING_GUIDE.md
Normal 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 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).
|
||||||
Reference in New Issue
Block a user