From bcd9d547f56522990de535b2e2698e3224842581 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 7 Nov 2025 04:43:48 -0700 Subject: [PATCH] Redesigned Enrollment Page --- .../src/Devices/Enrollment_Codes.jsx | 540 +++++++++--------- 1 file changed, 269 insertions(+), 271 deletions(-) diff --git a/Data/Engine/web-interface/src/Devices/Enrollment_Codes.jsx b/Data/Engine/web-interface/src/Devices/Enrollment_Codes.jsx index db3e387e..5a9e4bc3 100644 --- a/Data/Engine/web-interface/src/Devices/Enrollment_Codes.jsx +++ b/Data/Engine/web-interface/src/Devices/Enrollment_Codes.jsx @@ -1,27 +1,17 @@ -////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/Server/WebUI/src/Admin/Enrollment_Codes.jsx - -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { - Alert, Box, + Paper, + Typography, Button, - Chip, - CircularProgress, + Stack, + Alert, FormControl, - IconButton, InputLabel, MenuItem, - Paper, Select, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Tooltip, - Typography, + CircularProgress, + Tooltip } from "@mui/material"; import { ContentCopy as CopyIcon, @@ -29,6 +19,36 @@ import { Refresh as RefreshIcon, Key as KeyIcon, } from "@mui/icons-material"; +import { AgGridReact } from "ag-grid-react"; +import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community"; +// IMPORTANT: Do NOT import global AG Grid CSS here to avoid overriding other pages. +// We rely on the project's existing CSS and themeQuartz class name like other MagicUI pages. +ModuleRegistry.registerModules([AllCommunityModule]); + +// Match the palette used on other pages (see Site_List / Device_List) +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)", + textBright: "#e2e8f0", + textMuted: "#94a3b8", + accentA: "#7dd3fc", + accentB: "#c084fc", +}; + +// Generate a scoped Quartz theme class (same pattern as other pages) +const gridTheme = themeQuartz.withParams({ + accentColor: "#8b5cf6", + backgroundColor: "#070b1a", + browserColorScheme: "dark", + fontFamily: { googleFont: "IBM Plex Sans" }, + foregroundColor: "#f4f7ff", + headerFontSize: 13, +}); +const themeClassName = gridTheme.themeName || "ag-theme-quartz"; const TTL_PRESETS = [ { value: 1, label: "1 hour" }, @@ -38,10 +58,22 @@ const TTL_PRESETS = [ { value: 24, label: "24 hours" }, ]; -const statusColor = { - active: "success", - used: "default", - expired: "warning", +const determineStatus = (record) => { + if (!record) return "expired"; + const maxUses = Number.isFinite(record?.max_uses) ? record.max_uses : 1; + const useCount = Number.isFinite(record?.use_count) ? record.use_count : 0; + if (useCount >= Math.max(1, maxUses || 1)) return "used"; + if (!record.expires_at) return "expired"; + const expires = new Date(record.expires_at); + if (Number.isNaN(expires.getTime())) return "expired"; + return expires.getTime() > Date.now() ? "active" : "expired"; +}; + +const formatDateTime = (value) => { + if (!value) return "—"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString(); }; const maskCode = (code) => { @@ -56,25 +88,7 @@ const maskCode = (code) => { .join("-"); }; -const formatDateTime = (value) => { - if (!value) return "—"; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; - return date.toLocaleString(); -}; - -const determineStatus = (record) => { - if (!record) return "expired"; - const maxUses = Number.isFinite(record?.max_uses) ? record.max_uses : 1; - const useCount = Number.isFinite(record?.use_count) ? record.use_count : 0; - if (useCount >= Math.max(1, maxUses || 1)) return "used"; - if (!record.expires_at) return "expired"; - const expires = new Date(record.expires_at); - if (Number.isNaN(expires.getTime())) return "expired"; - return expires.getTime() > Date.now() ? "active" : "expired"; -}; - -function EnrollmentCodes() { +export default function EnrollmentCodes() { const [codes, setCodes] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); @@ -83,20 +97,14 @@ function EnrollmentCodes() { const [ttlHours, setTtlHours] = useState(6); const [generating, setGenerating] = useState(false); const [maxUses, setMaxUses] = useState(2); - - const filteredCodes = useMemo(() => { - if (statusFilter === "all") return codes; - return codes.filter((code) => determineStatus(code) === statusFilter); - }, [codes, statusFilter]); + const gridRef = useRef(null); const fetchCodes = useCallback(async () => { setLoading(true); setError(""); try { const query = statusFilter === "all" ? "" : `?status=${encodeURIComponent(statusFilter)}`; - const resp = await fetch(`/api/admin/enrollment-codes${query}`, { - credentials: "include", - }); + const resp = await fetch(`/api/admin/enrollment-codes${query}`, { credentials: "include" }); if (!resp.ok) { const body = await resp.json().catch(() => ({})); throw new Error(body.error || `Request failed (${resp.status})`); @@ -104,19 +112,16 @@ function EnrollmentCodes() { const data = await resp.json(); setCodes(Array.isArray(data.codes) ? data.codes : []); } catch (err) { - setError(err.message || "Unable to load enrollment codes"); + setError(err.message || "Unable to load codes"); } finally { setLoading(false); } }, [statusFilter]); - useEffect(() => { - fetchCodes(); - }, [fetchCodes]); + useEffect(() => { fetchCodes(); }, [fetchCodes]); const handleGenerate = useCallback(async () => { setGenerating(true); - setError(""); try { const resp = await fetch("/api/admin/enrollment-codes", { method: "POST", @@ -128,244 +133,237 @@ function EnrollmentCodes() { const body = await resp.json().catch(() => ({})); throw new Error(body.error || `Request failed (${resp.status})`); } - const created = await resp.json(); - setFeedback({ type: "success", message: `Installer code ${created.code} created` }); await fetchCodes(); + setFeedback({ type: "success", message: "New installer code created" }); } catch (err) { - setFeedback({ type: "error", message: err.message || "Failed to create code" }); + setFeedback({ type: "error", message: err.message }); } finally { setGenerating(false); } - }, [fetchCodes, ttlHours, maxUses]); + }, [ttlHours, maxUses, fetchCodes]); - const handleDelete = useCallback( - async (id) => { - if (!id) return; - const confirmDelete = window.confirm("Delete this unused installer code?"); - if (!confirmDelete) return; - setError(""); - try { - const resp = await fetch(`/api/admin/enrollment-codes/${encodeURIComponent(id)}`, { - method: "DELETE", - credentials: "include", - }); - if (!resp.ok) { - const body = await resp.json().catch(() => ({})); - throw new Error(body.error || `Request failed (${resp.status})`); - } - setFeedback({ type: "success", message: "Installer code deleted" }); - await fetchCodes(); - } catch (err) { - setFeedback({ type: "error", message: err.message || "Failed to delete code" }); - } - }, - [fetchCodes] - ); - - const handleCopy = useCallback((code) => { + const handleCopy = (code) => { if (!code) return; try { if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(code); setFeedback({ type: "success", message: "Code copied to clipboard" }); - } else { - const textArea = document.createElement("textarea"); - textArea.value = code; - textArea.style.position = "fixed"; - textArea.style.opacity = "0"; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand("copy"); - document.body.removeChild(textArea); - setFeedback({ type: "success", message: "Code copied to clipboard" }); } - } catch (err) { - setFeedback({ type: "error", message: err.message || "Unable to copy code" }); - } - }, []); - - const renderStatusChip = (record) => { - const status = determineStatus(record); - return ; + } catch (_) {} }; + const handleDelete = async (id) => { + if (!id) return; + if (!window.confirm("Delete this installer code?")) return; + try { + const resp = await fetch(`/api/admin/enrollment-codes/${id}`, { + method: "DELETE", + credentials: "include", + }); + if (!resp.ok) { + const body = await resp.json().catch(() => ({})); + throw new Error(body.error || `Request failed (${resp.status})`); + } + await fetchCodes(); + setFeedback({ type: "success", message: "Code deleted" }); + } catch (err) { + setFeedback({ type: "error", message: err.message }); + } + }; + + const columns = useMemo(() => [ + { + headerName: "Status", + field: "status", + cellRenderer: (params) => { + const status = determineStatus(params.data); + const color = + status === "active" ? "#34d399" : + status === "used" ? "#7dd3fc" : + "#fbbf24"; + return {status}; + }, + width: 120 + }, + { + headerName: "Installer Code", + field: "code", + cellRenderer: (params) => ( + {maskCode(params.value)} + ), + minWidth: 200 + }, + { headerName: "Expires At", field: "expires_at", valueFormatter: p => formatDateTime(p.value) }, + { headerName: "Created By", field: "created_by_user_id" }, + { + headerName: "Usage", + valueGetter: (p) => `${p.data.use_count || 0} / ${p.data.max_uses || 1}`, + cellStyle: { fontFamily: "monospace" }, + width: 120 + }, + { headerName: "Last Used", field: "last_used_at", valueFormatter: p => formatDateTime(p.value) }, + { headerName: "Used By GUID", field: "used_by_guid" }, + { + headerName: "Actions", + cellRenderer: (params) => { + const record = params.data; + const disableDelete = (record.use_count || 0) !== 0; + return ( + + + + + + + + + + + + + ); + }, + width: 160 + } + ], []); + + const defaultColDef = useMemo(() => ({ + sortable: true, + filter: true, + resizable: true, + flex: 1, + minWidth: 140, + }), []); + return ( - - - - Enrollment Installer Codes - - - - - - Filter - - - - - Duration - - - - - Allowed Uses - - - - - - + {generating ? "Generating…" : "Generate Code"} + + + + - {feedback ? ( - setFeedback(null)} - variant="outlined" - > + {/* Controls */} + + + Status + + + + + Duration + + + + + Allowed Uses + + + + + {feedback && ( + + setFeedback(null)}> {feedback.message} - ) : null} + + )} + {error && ( + + {error} + + )} - {error ? ( - - {error} - - ) : null} - - - - - - Status - Installer Code - Expires At - Created By - Usage - Last Used - Consumed At - Used By GUID - Actions - - - - {loading ? ( - - - - - Loading installer codes… - - - - ) : filteredCodes.length === 0 ? ( - - - - No installer codes match this filter. - - - - ) : ( - filteredCodes.map((record) => { - const status = determineStatus(record); - const maxAllowed = Math.max(1, Number.isFinite(record?.max_uses) ? record.max_uses : 1); - const usageCount = Math.max(0, Number.isFinite(record?.use_count) ? record.use_count : 0); - const disableDelete = usageCount !== 0; - return ( - - {renderStatusChip(record)} - {maskCode(record.code)} - {formatDateTime(record.expires_at)} - {record.created_by_user_id || "—"} - {`${usageCount} / ${maxAllowed}`} - {formatDateTime(record.last_used_at)} - {formatDateTime(record.used_at)} - - {record.used_by_guid || "—"} - - - - - handleCopy(record.code)} - disabled={!record.code} - > - - - - - - - handleDelete(record.id)} - disabled={disableDelete} - > - - - - - - - ); - }) - )} - -
-
- - + {/* Grid wrapper — all overrides are SCOPED to this instance via inline CSS vars */} + + + + ); } - -export default React.memo(EnrollmentCodes);