mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-16 03:25:48 -07:00
Several UI Adjustments
This commit is contained in:
@@ -77,13 +77,6 @@ const formatDateTime = (value) => {
|
||||
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) => {
|
||||
if (!status) return "pending";
|
||||
if (status === "completed") return "completed";
|
||||
@@ -289,29 +282,24 @@ export default function DeviceApprovals({ onPageMetaChange }) {
|
||||
width: 110,
|
||||
},
|
||||
{ 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",
|
||||
field: "site_name",
|
||||
valueGetter: (p) => p.data?.site_name || (p.data?.site_id ? `Site ${p.data.site_id}` : "—"),
|
||||
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",
|
||||
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 { startApprove, handleDeny, handleGuidChange, actioningId } = params.context;
|
||||
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;
|
||||
return (
|
||||
<Stack direction="row" spacing={8} alignItems="center">
|
||||
<TextField
|
||||
size="small"
|
||||
label="Optional GUID"
|
||||
placeholder="Leave empty to auto-generate"
|
||||
value={guidValue}
|
||||
onChange={(e) => handleGuidChange(record.id, e.target.value)}
|
||||
sx={{ minWidth: 220 }}
|
||||
/>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Tooltip title="Approve enrollment">
|
||||
<span>
|
||||
<Button
|
||||
color="success"
|
||||
variant="text"
|
||||
onClick={() => startApprove(record)}
|
||||
disabled={isBusy}
|
||||
startIcon={isBusy ? <CircularProgress size={16} color="success" /> : <ApproveIcon fontSize="small" />}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Deny enrollment">
|
||||
<span>
|
||||
<Button
|
||||
color="error"
|
||||
variant="text"
|
||||
onClick={() => handleDeny(record)}
|
||||
disabled={isBusy}
|
||||
startIcon={<DenyIcon fontSize="small" />}
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Box sx={{ display: "flex", alignItems: "center", height: "100%" }}>
|
||||
<Stack direction="row" spacing={8} alignItems="center">
|
||||
<TextField
|
||||
size="small"
|
||||
label="Optional GUID"
|
||||
placeholder="Leave empty to auto-generate"
|
||||
value={guidValue}
|
||||
onChange={(e) => handleGuidChange(record.id, e.target.value)}
|
||||
sx={{ minWidth: 220 }}
|
||||
/>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Tooltip title="Approve enrollment">
|
||||
<span>
|
||||
<Button
|
||||
color="success"
|
||||
variant="text"
|
||||
onClick={() => startApprove(record)}
|
||||
disabled={isBusy}
|
||||
startIcon={isBusy ? <CircularProgress size={16} color="success" /> : <ApproveIcon fontSize="small" />}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Deny enrollment">
|
||||
<span>
|
||||
<Button
|
||||
color="error"
|
||||
variant="text"
|
||||
onClick={() => handleDeny(record)}
|
||||
disabled={isBusy}
|
||||
startIcon={<DenyIcon fontSize="small" />}
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
minWidth: 480,
|
||||
@@ -427,68 +423,61 @@ export default function DeviceApprovals({ onPageMetaChange }) {
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
boxShadow: "none",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
elevation={0}
|
||||
>
|
||||
overflow: "hidden",
|
||||
}}
|
||||
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 && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<SecurityIcon sx={{ color: MAGIC_UI.accentA }} />
|
||||
<Typography variant="h6" sx={{ color: MAGIC_UI.textBright, fontWeight: 700 }}>
|
||||
Device Approval Queue
|
||||
</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 direction="row" spacing={1} alignItems="center">
|
||||
<SecurityIcon sx={{ color: MAGIC_UI.accentA }} />
|
||||
<Typography variant="h6" sx={{ color: MAGIC_UI.textBright, fontWeight: 700 }}>
|
||||
Device Approval Queue
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Filters under shared header */}
|
||||
{useGlobalHeader && (
|
||||
<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>
|
||||
)}
|
||||
{useGlobalHeader && null}
|
||||
|
||||
{/* Feedback */}
|
||||
{feedback && (
|
||||
|
||||
@@ -863,15 +863,16 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 12,
|
||||
right: 24,
|
||||
position: "fixed",
|
||||
top: { xs: 72, md: 88 }, // align with page title padding beneath the menu bar
|
||||
right: { xs: 12, md: 24 },
|
||||
display: "flex",
|
||||
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">
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
||||
@@ -348,6 +348,55 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh
|
||||
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
|
||||
className={gridTheme.themeName}
|
||||
@@ -428,42 +477,6 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh
|
||||
style={{ width: "100%", height: "100%", flex: 1, fontFamily: gridFontFamily }}
|
||||
/>
|
||||
</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>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,8 @@ const themeClassName = myTheme.themeName || "ag-theme-quartz";
|
||||
const gridFontFamily = '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif';
|
||||
const iconFontFamily = '"Quartz Regular"';
|
||||
|
||||
const AUTO_SIZE_COLUMNS = ["__select__", "device_count", "enrollment_code"];
|
||||
|
||||
const MAGIC_UI = {
|
||||
shellBg:
|
||||
"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 [rotatingId, setRotatingId] = useState(null);
|
||||
const gridRef = useRef(null);
|
||||
const gridApiRef = useRef(null);
|
||||
const sendNotification = useCallback(async (message) => {
|
||||
if (!message) return;
|
||||
try {
|
||||
@@ -118,6 +121,25 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
|
||||
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 value = (code || "").trim();
|
||||
if (!value) return;
|
||||
@@ -160,13 +182,19 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
field: "__select__",
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true,
|
||||
minWidth: 52,
|
||||
width: 52,
|
||||
maxWidth: 52,
|
||||
pinned: "left",
|
||||
filter: false,
|
||||
sortable: false,
|
||||
suppressMenu: true,
|
||||
},
|
||||
{
|
||||
headerName: "Name",
|
||||
field: "name",
|
||||
minWidth: 180,
|
||||
minWidth: 220,
|
||||
flex: 1,
|
||||
cellRenderer: (params) => (
|
||||
<span
|
||||
style={{ color: "#7dd3fc", cursor: "pointer", fontWeight: 500 }}
|
||||
@@ -176,11 +204,23 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
headerName: "Description",
|
||||
field: "description",
|
||||
minWidth: 220,
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
headerName: "Devices",
|
||||
field: "device_count",
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
headerName: "Agent Enrollment Code",
|
||||
field: "enrollment_code",
|
||||
minWidth: 320,
|
||||
flex: 1.2,
|
||||
minWidth: 260,
|
||||
filter: false,
|
||||
suppressMenu: true,
|
||||
cellRenderer: (params) => {
|
||||
const code = params.value || "—";
|
||||
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]);
|
||||
|
||||
const defaultColDef = useMemo(() => ({
|
||||
sortable: true,
|
||||
filter: "agTextColumnFilter",
|
||||
resizable: true,
|
||||
flex: 1,
|
||||
minWidth: 160,
|
||||
}), []);
|
||||
|
||||
@@ -254,13 +291,32 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
}}
|
||||
elevation={0}
|
||||
>
|
||||
<Box sx={{ p: { xs: 2, md: 3 }, pb: 1, display: "flex", alignItems: "center", justifyContent: "flex-end", flexWrap: "wrap", gap: 1 }}>
|
||||
{heroStats.selected > 0 ? (
|
||||
<Typography sx={{ color: MAGIC_UI.accentA, fontSize: "0.85rem", fontWeight: 600, mr: 1 }}>
|
||||
{heroStats.selected} selected
|
||||
</Typography>
|
||||
) : null}
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap", justifyContent: "flex-end" }}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: { xs: 72, md: 88 },
|
||||
right: { xs: 12, md: 24 },
|
||||
display: "flex",
|
||||
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)}>
|
||||
Create Site
|
||||
</Button>
|
||||
@@ -350,8 +406,12 @@ export default function SiteList({ onOpenDevicesForSite, onPageMetaChange }) {
|
||||
paginationPageSize={20}
|
||||
paginationPageSizeSelector={[20, 50, 100]}
|
||||
animateRows
|
||||
onGridReady={(params) => {
|
||||
gridApiRef.current = params.api;
|
||||
autoSizeColumns();
|
||||
}}
|
||||
onSelectionChanged={() => {
|
||||
const api = gridRef.current?.api;
|
||||
const api = gridApiRef.current || gridRef.current?.api;
|
||||
if (!api) return;
|
||||
const selected = api.getSelectedNodes().map((n) => n.data?.id).filter(Boolean);
|
||||
setSelectedIds(new Set(selected));
|
||||
|
||||
Reference in New Issue
Block a user