mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-15 23:25:48 -07:00
Several UI Adjustments
This commit is contained in:
@@ -77,13 +77,6 @@ const formatDateTime = (value) => {
|
|||||||
return date.toLocaleString();
|
return date.toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatFingerprint = (fp) => {
|
|
||||||
if (!fp) return "—";
|
|
||||||
const normalized = fp.replace(/[^a-f0-9]/gi, "").toLowerCase();
|
|
||||||
if (!normalized) return fp;
|
|
||||||
return normalized.match(/.{1,4}/g)?.join(" ") ?? normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeStatus = (status) => {
|
const normalizeStatus = (status) => {
|
||||||
if (!status) return "pending";
|
if (!status) return "pending";
|
||||||
if (status === "completed") return "completed";
|
if (status === "completed") return "completed";
|
||||||
@@ -289,29 +282,24 @@ export default function DeviceApprovals({ onPageMetaChange }) {
|
|||||||
width: 110,
|
width: 110,
|
||||||
},
|
},
|
||||||
{ headerName: "Hostname", field: "hostname_claimed", minWidth: 180 },
|
{ headerName: "Hostname", field: "hostname_claimed", minWidth: 180 },
|
||||||
{
|
|
||||||
headerName: "Fingerprint",
|
|
||||||
field: "ssl_key_fingerprint_claimed",
|
|
||||||
valueFormatter: (p) => formatFingerprint(p.value),
|
|
||||||
cellStyle: { fontFamily: "monospace", whiteSpace: "nowrap" },
|
|
||||||
minWidth: 150,
|
|
||||||
Width: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: "Enrollment Code",
|
|
||||||
field: "enrollment_code_id",
|
|
||||||
cellStyle: { fontFamily: "monospace" },
|
|
||||||
minWidth: 100,
|
|
||||||
Width: 100,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
headerName: "Site",
|
headerName: "Site",
|
||||||
field: "site_name",
|
field: "site_name",
|
||||||
valueGetter: (p) => p.data?.site_name || (p.data?.site_id ? `Site ${p.data.site_id}` : "—"),
|
valueGetter: (p) => p.data?.site_name || (p.data?.site_id ? `Site ${p.data.site_id}` : "—"),
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
},
|
},
|
||||||
{ headerName: "Created", field: "created_at", valueFormatter: (p) => formatDateTime(p.value), minWidth: 160 },
|
{
|
||||||
{ headerName: "Updated", field: "updated_at", valueFormatter: (p) => formatDateTime(p.value), minWidth: 160 },
|
headerName: "Date of Enrollment Request",
|
||||||
|
field: "created_at",
|
||||||
|
valueFormatter: (p) => formatDateTime(p.value),
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: "Date of Approval",
|
||||||
|
field: "updated_at",
|
||||||
|
valueFormatter: (p) => formatDateTime(p.value),
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
headerName: "Approved By",
|
headerName: "Approved By",
|
||||||
valueGetter: (p) => p.data?.approved_by_username || p.data?.approved_by_user_id || "—",
|
valueGetter: (p) => p.data?.approved_by_username || p.data?.approved_by_user_id || "—",
|
||||||
@@ -327,48 +315,56 @@ export default function DeviceApprovals({ onPageMetaChange }) {
|
|||||||
const guidValue = params.context.guidInputs[record.id] || "";
|
const guidValue = params.context.guidInputs[record.id] || "";
|
||||||
const { startApprove, handleDeny, handleGuidChange, actioningId } = params.context;
|
const { startApprove, handleDeny, handleGuidChange, actioningId } = params.context;
|
||||||
if (!showActions) {
|
if (!showActions) {
|
||||||
return <Typography variant="body2" style={{ color: "#9aa0a6" }}>No actions available</Typography>;
|
return (
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", height: "100%" }}>
|
||||||
|
<Typography variant="body2" sx={{ color: "#9aa0a6" }}>
|
||||||
|
No actions available
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const isBusy = actioningId === record.id;
|
const isBusy = actioningId === record.id;
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" spacing={8} alignItems="center">
|
<Box sx={{ display: "flex", alignItems: "center", height: "100%" }}>
|
||||||
<TextField
|
<Stack direction="row" spacing={8} alignItems="center">
|
||||||
size="small"
|
<TextField
|
||||||
label="Optional GUID"
|
size="small"
|
||||||
placeholder="Leave empty to auto-generate"
|
label="Optional GUID"
|
||||||
value={guidValue}
|
placeholder="Leave empty to auto-generate"
|
||||||
onChange={(e) => handleGuidChange(record.id, e.target.value)}
|
value={guidValue}
|
||||||
sx={{ minWidth: 220 }}
|
onChange={(e) => handleGuidChange(record.id, e.target.value)}
|
||||||
/>
|
sx={{ minWidth: 220 }}
|
||||||
<Stack direction="row" spacing={1}>
|
/>
|
||||||
<Tooltip title="Approve enrollment">
|
<Stack direction="row" spacing={1}>
|
||||||
<span>
|
<Tooltip title="Approve enrollment">
|
||||||
<Button
|
<span>
|
||||||
color="success"
|
<Button
|
||||||
variant="text"
|
color="success"
|
||||||
onClick={() => startApprove(record)}
|
variant="text"
|
||||||
disabled={isBusy}
|
onClick={() => startApprove(record)}
|
||||||
startIcon={isBusy ? <CircularProgress size={16} color="success" /> : <ApproveIcon fontSize="small" />}
|
disabled={isBusy}
|
||||||
>
|
startIcon={isBusy ? <CircularProgress size={16} color="success" /> : <ApproveIcon fontSize="small" />}
|
||||||
Approve
|
>
|
||||||
</Button>
|
Approve
|
||||||
</span>
|
</Button>
|
||||||
</Tooltip>
|
</span>
|
||||||
<Tooltip title="Deny enrollment">
|
</Tooltip>
|
||||||
<span>
|
<Tooltip title="Deny enrollment">
|
||||||
<Button
|
<span>
|
||||||
color="error"
|
<Button
|
||||||
variant="text"
|
color="error"
|
||||||
onClick={() => handleDeny(record)}
|
variant="text"
|
||||||
disabled={isBusy}
|
onClick={() => handleDeny(record)}
|
||||||
startIcon={<DenyIcon fontSize="small" />}
|
disabled={isBusy}
|
||||||
>
|
startIcon={<DenyIcon fontSize="small" />}
|
||||||
Deny
|
>
|
||||||
</Button>
|
Deny
|
||||||
</span>
|
</Button>
|
||||||
</Tooltip>
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
minWidth: 480,
|
minWidth: 480,
|
||||||
@@ -427,68 +423,61 @@ export default function DeviceApprovals({ onPageMetaChange }) {
|
|||||||
border: "none",
|
border: "none",
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
boxShadow: "none",
|
boxShadow: "none",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
elevation={0}
|
elevation={0}
|
||||||
>
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "fixed",
|
||||||
|
top: { xs: 72, md: 88 },
|
||||||
|
right: { xs: 12, md: 24 },
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
zIndex: 1400,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", sm: "row" }}
|
||||||
|
spacing={1.25}
|
||||||
|
alignItems="center"
|
||||||
|
sx={{ pointerEvents: "auto" }}
|
||||||
|
>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||||||
|
<InputLabel id="approval-status-filter-label">Status</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="approval-status-filter-label"
|
||||||
|
label="Status"
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
{STATUS_OPTIONS.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={loadApprovals} disabled={loading}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{!useGlobalHeader && (
|
{!useGlobalHeader && (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<SecurityIcon sx={{ color: MAGIC_UI.accentA }} />
|
||||||
<SecurityIcon sx={{ color: MAGIC_UI.accentA }} />
|
<Typography variant="h6" sx={{ color: MAGIC_UI.textBright, fontWeight: 700 }}>
|
||||||
<Typography variant="h6" sx={{ color: MAGIC_UI.textBright, fontWeight: 700 }}>
|
Device Approval Queue
|
||||||
Device Approval Queue
|
</Typography>
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems={{ xs: "stretch", sm: "center" }}>
|
|
||||||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
|
||||||
<InputLabel id="approval-status-filter-label">Status</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="approval-status-filter-label"
|
|
||||||
label="Status"
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
|
||||||
>
|
|
||||||
{STATUS_OPTIONS.map((option) => (
|
|
||||||
<MenuItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={loadApprovals} disabled={loading}>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filters under shared header */}
|
{/* Filters under shared header */}
|
||||||
{useGlobalHeader && (
|
{useGlobalHeader && null}
|
||||||
<Box sx={{ px: 3, pt: 2, pb: 1 }}>
|
|
||||||
<Stack direction={{ xs: "column", sm: "row" }} spacing={2} alignItems={{ xs: "stretch", sm: "center" }}>
|
|
||||||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
|
||||||
<InputLabel id="approval-status-filter-label">Status</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="approval-status-filter-label"
|
|
||||||
label="Status"
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
|
||||||
>
|
|
||||||
{STATUS_OPTIONS.map((option) => (
|
|
||||||
<MenuItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={loadApprovals} disabled={loading}>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Feedback */}
|
{/* Feedback */}
|
||||||
{feedback && (
|
{feedback && (
|
||||||
|
|||||||
@@ -863,15 +863,16 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
|
|||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "fixed",
|
||||||
top: 12,
|
top: { xs: 72, md: 88 }, // align with page title padding beneath the menu bar
|
||||||
right: 24,
|
right: { xs: 12, md: 24 },
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
zIndex: 3,
|
zIndex: 1400,
|
||||||
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack direction="row" spacing={1.25}>
|
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
|
||||||
<Tooltip title="Cancel and return">
|
<Tooltip title="Cancel and return">
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -348,6 +348,55 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "fixed",
|
||||||
|
top: { xs: 72, md: 88 },
|
||||||
|
right: { xs: 12, md: 24 },
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
zIndex: 1400,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={1.25} sx={{ pointerEvents: "auto" }}>
|
||||||
|
<Tooltip title="Refresh">
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
startIcon={<CachedIcon fontSize="small" />}
|
||||||
|
variant="outlined"
|
||||||
|
aria-label="Refresh filters"
|
||||||
|
onClick={loadFilters}
|
||||||
|
sx={{
|
||||||
|
textTransform: "none",
|
||||||
|
color: "#a5e0ff",
|
||||||
|
borderColor: "rgba(148,163,184,0.4)",
|
||||||
|
backgroundColor: "rgba(5,7,15,0.6)",
|
||||||
|
borderRadius: 999,
|
||||||
|
px: 2.4,
|
||||||
|
minWidth: 126,
|
||||||
|
height: 38,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "rgba(125,183,255,0.16)",
|
||||||
|
borderColor: "rgba(148,163,184,0.6)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => onCreateFilter?.()}
|
||||||
|
sx={gradientButtonSx}
|
||||||
|
>
|
||||||
|
New Filter
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", gap: 1.5 }}>
|
<Box sx={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||||
<Box
|
<Box
|
||||||
className={gridTheme.themeName}
|
className={gridTheme.themeName}
|
||||||
@@ -428,42 +477,6 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh
|
|||||||
style={{ width: "100%", height: "100%", flex: 1, fontFamily: gridFontFamily }}
|
style={{ width: "100%", height: "100%", flex: 1, fontFamily: gridFontFamily }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 1.5 }}>
|
|
||||||
<Stack direction="row" gap={1.75}>
|
|
||||||
<Tooltip title="Refresh">
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
startIcon={<CachedIcon fontSize="small" />}
|
|
||||||
variant="outlined"
|
|
||||||
aria-label="Refresh filters"
|
|
||||||
onClick={loadFilters}
|
|
||||||
sx={{
|
|
||||||
textTransform: "none",
|
|
||||||
color: "#a5e0ff",
|
|
||||||
borderColor: "rgba(148,163,184,0.4)",
|
|
||||||
backgroundColor: "rgba(5,7,15,0.6)",
|
|
||||||
borderRadius: 999,
|
|
||||||
px: 2.4,
|
|
||||||
minWidth: 126,
|
|
||||||
height: 38,
|
|
||||||
"&:hover": { backgroundColor: "rgba(125,183,255,0.16)", borderColor: "rgba(148,163,184,0.6)" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Button
|
|
||||||
startIcon={<AddIcon />}
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => onCreateFilter?.()}
|
|
||||||
sx={gradientButtonSx}
|
|
||||||
>
|
|
||||||
New Filter
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ 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 AUTO_SIZE_COLUMNS = ["__select__", "device_count", "enrollment_code"];
|
||||||
|
|
||||||
const MAGIC_UI = {
|
const MAGIC_UI = {
|
||||||
shellBg:
|
shellBg:
|
||||||
"radial-gradient(120% 120% at 0% 0%, rgba(76, 186, 255, 0.16), transparent 55%), " +
|
"radial-gradient(120% 120% at 0% 0%, rgba(76, 186, 255, 0.16), transparent 55%), " +
|
||||||
@@ -78,6 +80,7 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
|||||||
const [renameValue, setRenameValue] = useState("");
|
const [renameValue, setRenameValue] = useState("");
|
||||||
const [rotatingId, setRotatingId] = useState(null);
|
const [rotatingId, setRotatingId] = useState(null);
|
||||||
const gridRef = useRef(null);
|
const gridRef = useRef(null);
|
||||||
|
const gridApiRef = useRef(null);
|
||||||
const sendNotification = useCallback(async (message) => {
|
const sendNotification = useCallback(async (message) => {
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
try {
|
try {
|
||||||
@@ -118,6 +121,25 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
|||||||
|
|
||||||
useEffect(() => { fetchSites(); }, [fetchSites]);
|
useEffect(() => { fetchSites(); }, [fetchSites]);
|
||||||
|
|
||||||
|
const autoSizeColumns = useCallback(() => {
|
||||||
|
const api = gridApiRef.current || gridRef.current?.api;
|
||||||
|
if (!api || !rows.length) return;
|
||||||
|
const doSize = () => {
|
||||||
|
try {
|
||||||
|
api.autoSizeColumns(AUTO_SIZE_COLUMNS, true);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
if (typeof requestAnimationFrame === "function") {
|
||||||
|
requestAnimationFrame(doSize);
|
||||||
|
} else {
|
||||||
|
setTimeout(doSize, 0);
|
||||||
|
}
|
||||||
|
}, [rows.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
autoSizeColumns();
|
||||||
|
}, [rows, autoSizeColumns]);
|
||||||
|
|
||||||
const handleCopy = useCallback(async (code) => {
|
const handleCopy = useCallback(async (code) => {
|
||||||
const value = (code || "").trim();
|
const value = (code || "").trim();
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
@@ -160,13 +182,19 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
|||||||
field: "__select__",
|
field: "__select__",
|
||||||
checkboxSelection: true,
|
checkboxSelection: true,
|
||||||
headerCheckboxSelection: true,
|
headerCheckboxSelection: true,
|
||||||
|
minWidth: 52,
|
||||||
width: 52,
|
width: 52,
|
||||||
|
maxWidth: 52,
|
||||||
pinned: "left",
|
pinned: "left",
|
||||||
|
filter: false,
|
||||||
|
sortable: false,
|
||||||
|
suppressMenu: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headerName: "Name",
|
headerName: "Name",
|
||||||
field: "name",
|
field: "name",
|
||||||
minWidth: 180,
|
minWidth: 220,
|
||||||
|
flex: 1,
|
||||||
cellRenderer: (params) => (
|
cellRenderer: (params) => (
|
||||||
<span
|
<span
|
||||||
style={{ color: "#7dd3fc", cursor: "pointer", fontWeight: 500 }}
|
style={{ color: "#7dd3fc", cursor: "pointer", fontWeight: 500 }}
|
||||||
@@ -176,11 +204,23 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
headerName: "Description",
|
||||||
|
field: "description",
|
||||||
|
minWidth: 220,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: "Devices",
|
||||||
|
field: "device_count",
|
||||||
|
minWidth: 140,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
headerName: "Agent Enrollment Code",
|
headerName: "Agent Enrollment Code",
|
||||||
field: "enrollment_code",
|
field: "enrollment_code",
|
||||||
minWidth: 320,
|
minWidth: 260,
|
||||||
flex: 1.2,
|
filter: false,
|
||||||
|
suppressMenu: true,
|
||||||
cellRenderer: (params) => {
|
cellRenderer: (params) => {
|
||||||
const code = params.value || "—";
|
const code = params.value || "—";
|
||||||
const site = params.data || {};
|
const site = params.data || {};
|
||||||
@@ -218,15 +258,12 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headerName: "Description", field: "description", minWidth: 220 },
|
|
||||||
{ headerName: "Devices", field: "device_count", minWidth: 120 },
|
|
||||||
], [onOpenDevicesForSite, handleRotate, handleCopy, rotatingId]);
|
], [onOpenDevicesForSite, handleRotate, handleCopy, rotatingId]);
|
||||||
|
|
||||||
const defaultColDef = useMemo(() => ({
|
const defaultColDef = useMemo(() => ({
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filter: "agTextColumnFilter",
|
filter: "agTextColumnFilter",
|
||||||
resizable: true,
|
resizable: true,
|
||||||
flex: 1,
|
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
@@ -254,13 +291,32 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
|||||||
}}
|
}}
|
||||||
elevation={0}
|
elevation={0}
|
||||||
>
|
>
|
||||||
<Box sx={{ p: { xs: 2, md: 3 }, pb: 1, display: "flex", alignItems: "center", justifyContent: "flex-end", flexWrap: "wrap", gap: 1 }}>
|
<Box
|
||||||
{heroStats.selected > 0 ? (
|
sx={{
|
||||||
<Typography sx={{ color: MAGIC_UI.accentA, fontSize: "0.85rem", fontWeight: 600, mr: 1 }}>
|
position: "fixed",
|
||||||
{heroStats.selected} selected
|
top: { xs: 72, md: 88 },
|
||||||
</Typography>
|
right: { xs: 12, md: 24 },
|
||||||
) : null}
|
display: "flex",
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap", justifyContent: "flex-end" }}>
|
justifyContent: "flex-end",
|
||||||
|
zIndex: 1400,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{heroStats.selected > 0 ? (
|
||||||
|
<Typography sx={{ color: MAGIC_UI.accentA, fontSize: "0.85rem", fontWeight: 600, mr: 1 }}>
|
||||||
|
{heroStats.selected} selected
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
<Button variant="contained" size="small" startIcon={<AddIcon />} sx={RAINBOW_BUTTON_SX} onClick={() => setCreateOpen(true)}>
|
<Button variant="contained" size="small" startIcon={<AddIcon />} sx={RAINBOW_BUTTON_SX} onClick={() => setCreateOpen(true)}>
|
||||||
Create Site
|
Create Site
|
||||||
</Button>
|
</Button>
|
||||||
@@ -350,8 +406,12 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
|||||||
paginationPageSize={20}
|
paginationPageSize={20}
|
||||||
paginationPageSizeSelector={[20, 50, 100]}
|
paginationPageSizeSelector={[20, 50, 100]}
|
||||||
animateRows
|
animateRows
|
||||||
|
onGridReady={(params) => {
|
||||||
|
gridApiRef.current = params.api;
|
||||||
|
autoSizeColumns();
|
||||||
|
}}
|
||||||
onSelectionChanged={() => {
|
onSelectionChanged={() => {
|
||||||
const api = gridRef.current?.api;
|
const api = gridApiRef.current || gridRef.current?.api;
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
const selected = api.getSelectedNodes().map((n) => n.data?.id).filter(Boolean);
|
const selected = api.getSelectedNodes().map((n) => n.data?.id).filter(Boolean);
|
||||||
setSelectedIds(new Set(selected));
|
setSelectedIds(new Set(selected));
|
||||||
|
|||||||
@@ -83,12 +83,11 @@ Applies to all Borealis frontends. Use `Data/Engine/web-interface/src/Admin/Page
|
|||||||
- Interaction rules: tabs should never scroll vertically; rely on horizontal scroll for overflow. Always align the tab rail with the first section header on the page so the aurora indicator lines up with hero metrics.
|
- Interaction rules: tabs should never scroll vertically; rely on horizontal scroll for overflow. Always align the tab rail with the first section header on the page so the aurora indicator lines up with hero metrics.
|
||||||
- Accessibility: keep `aria-label`/`aria-controls` pairs when the panes hold complex content, and ensure the gradient backgrounds preserve 4.5:1 contrast for the text (the current cyan on dark meets this).
|
- Accessibility: keep `aria-label`/`aria-controls` pairs when the panes hold complex content, and ensure the gradient backgrounds preserve 4.5:1 contrast for the text (the current cyan on dark meets this).
|
||||||
|
|
||||||
## Page-Level Actions with Tab Rails
|
## Page-Level Action Buttons
|
||||||
- When a page uses Aurora Tabs, place primary/secondary page actions inline with the tab rail, floating on the top-right of the content layer (under the global nav).
|
- Place page-level actions/buttons/hero-badges in a fixed overlay at the top-right, just below the global menu bar. Match the Filter Editor's placement if an example is needed `Data\Engine\web-interface\src\Devices\Filters\Filter_Editor.jsx`: wrapper `position: "fixed"`, `top: { xs: 72, md: 88 }`, `right: { xs: 12, md: 20 }`, `zIndex: 1400`, with `pointerEvents: "none"` on the wrapper and `pointerEvents: "auto"` on the inner `Stack` so underlying content remains clickable.
|
||||||
- Wrap the tab stack and actions in a `position: relative` container so actions can be absolutely positioned without leaving the shell flow.
|
- Use gradient primary pills and outlined secondary pills (rounded 999 radius, MagicUI colors). Keep horizontal spacing via a `Stack` (e.g., `spacing={1.25}`); do not nest these buttons inside the title grid or tab rail.
|
||||||
- Position the action bar as `position: "absolute", top: 12, right: 24, zIndex: 3` (adjust top/right to match your page padding) and keep it `display: "flex"` with a `Stack` for spacing.
|
- Tabs stay in normal document flow beneath the title/subtitle; the floating action bar should not shift layout. When operators request moving page actions (or when building new pages), apply this fixed overlay pattern instead of absolute positioning tied to tab rails.
|
||||||
- Use the same gradient primary pill and outlined secondary styles from the template; preserve the rounded 999 radius and MagicUI colors.
|
- Keep the responsive offsets (xs/md) unless a specific page has a different header height/padding; only adjust the numeric values when explicitly needed to align with a nonstandard shell.
|
||||||
- Keep the bar above content but separate from the title/subtitle block—do not nest buttons inside the title grid. This matches the Filter Editor pattern and keeps tabs and actions visually aligned.
|
|
||||||
|
|
||||||
## AG Grid Column Behavior (All Tables)
|
## AG Grid Column Behavior (All Tables)
|
||||||
- Auto-size value columns and let the last column absorb remaining width so views span available space.
|
- Auto-size value columns and let the last column absorb remaining width so views span available space.
|
||||||
|
|||||||
Reference in New Issue
Block a user