Several UI Adjustments

This commit is contained in:
2025-11-28 16:21:30 -07:00
parent 2bfc75a53c
commit ccfec9c969
5 changed files with 240 additions and 178 deletions

View File

@@ -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 && (

View File

@@ -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"

View File

@@ -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>
);

View File

@@ -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));

View File

@@ -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.
- 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
- 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).
- Wrap the tab stack and actions in a `position: relative` container so actions can be absolutely positioned without leaving the shell flow.
- 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.
- Use the same gradient primary pill and outlined secondary styles from the template; preserve the rounded 999 radius and MagicUI colors.
- 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.
## Page-Level Action Buttons
- 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.
- 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.
- 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.
- 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.
## AG Grid Column Behavior (All Tables)
- Auto-size value columns and let the last column absorb remaining width so views span available space.