From ccfec9c969f6c6a0cbe90110291956c6b6c40ad0 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 28 Nov 2025 16:21:30 -0700 Subject: [PATCH] Several UI Adjustments --- .../src/Devices/Device_Approvals.jsx | 223 +++++++++--------- .../src/Devices/Filters/Filter_Editor.jsx | 11 +- .../src/Devices/Filters/Filter_List.jsx | 85 ++++--- .../web-interface/src/Sites/Site_List.jsx | 88 +++++-- Docs/Codex/USER_INTERFACE.md | 11 +- 5 files changed, 240 insertions(+), 178 deletions(-) diff --git a/Data/Engine/web-interface/src/Devices/Device_Approvals.jsx b/Data/Engine/web-interface/src/Devices/Device_Approvals.jsx index 8cacb98c..60f9aae0 100644 --- a/Data/Engine/web-interface/src/Devices/Device_Approvals.jsx +++ b/Data/Engine/web-interface/src/Devices/Device_Approvals.jsx @@ -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 No actions available; + return ( + + + No actions available + + + ); } const isBusy = actioningId === record.id; return ( - - handleGuidChange(record.id, e.target.value)} - sx={{ minWidth: 220 }} - /> - - - - - - - - - - - + + + handleGuidChange(record.id, e.target.value)} + sx={{ minWidth: 220 }} + /> + + + + + + + + + + + + - + ); }, 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} + > + + + + Status + + + + + + {!useGlobalHeader && ( - - - - - Device Approval Queue - - - - - Status - - - - + + + + Device Approval Queue + )} {/* Filters under shared header */} - {useGlobalHeader && ( - - - - Status - - - - - - )} + {useGlobalHeader && null} {/* Feedback */} {feedback && ( diff --git a/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx b/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx index 5ae92352..256f4aac 100644 --- a/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx +++ b/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx @@ -863,15 +863,16 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o - + + + + + + + - - - - - - - - - - - ); diff --git a/Data/Engine/web-interface/src/Sites/Site_List.jsx b/Data/Engine/web-interface/src/Sites/Site_List.jsx index 840173a7..681e96c2 100644 --- a/Data/Engine/web-interface/src/Sites/Site_List.jsx +++ b/Data/Engine/web-interface/src/Sites/Site_List.jsx @@ -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) => ( ), }, + { + 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} > - - {heroStats.selected > 0 ? ( - - {heroStats.selected} selected - - ) : null} - + + + {heroStats.selected > 0 ? ( + + {heroStats.selected} selected + + ) : null} @@ -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)); diff --git a/Docs/Codex/USER_INTERFACE.md b/Docs/Codex/USER_INTERFACE.md index e24ee70d..d08aa768 100644 --- a/Docs/Codex/USER_INTERFACE.md +++ b/Docs/Codex/USER_INTERFACE.md @@ -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.