import React, { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { Alert, Box, Button, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, InputLabel, MenuItem, Paper, Select, Stack, TextField, Tooltip, Typography, } from "@mui/material"; import { CheckCircleOutline as ApproveIcon, HighlightOff as DenyIcon, Refresh as RefreshIcon, Security as SecurityIcon, } from "@mui/icons-material"; import { AgGridReact } from "ag-grid-react"; import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community"; // NOTE: Do NOT import global AG Grid CSS to avoid affecting other pages. // We rely on the Quartz theme class name + scoped CSS vars like the rest of MagicUI. ModuleRegistry.registerModules([AllCommunityModule]); // MagicUI palette to match Enrollment Codes / Site 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", panelBorder: "rgba(148, 163, 184, 0.35)", textBright: "#e2e8f0", accentA: "#7dd3fc", }; // Quartz theme instance (same params used across MagicUI 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 STATUS_OPTIONS = [ { value: "all", label: "All" }, { value: "pending", label: "Pending" }, { value: "approved", label: "Approved" }, { value: "completed", label: "Completed" }, { value: "denied", label: "Denied" }, { value: "expired", label: "Expired" }, ]; const statusChipColor = { pending: "warning", approved: "info", completed: "success", denied: "default", expired: "default", }; const formatDateTime = (value) => { if (!value) return "—"; const date = new Date(value); if (Number.isNaN(date.getTime())) return 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"; return status.toLowerCase(); }; export default function DeviceApprovals() { const [approvals, setApprovals] = useState([]); const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [feedback, setFeedback] = useState(null); const [guidInputs, setGuidInputs] = useState({}); const [actioningId, setActioningId] = useState(null); const [conflictPrompt, setConflictPrompt] = useState(null); const gridRef = useRef(null); const loadApprovals = useCallback(async () => { setLoading(true); setError(""); try { const query = statusFilter === "all" ? "" : `?status=${encodeURIComponent(statusFilter)}`; const resp = await fetch(`/api/admin/device-approvals${query}`, { credentials: "include" }); if (!resp.ok) { const body = await resp.json().catch(() => ({})); throw new Error(body.error || `Request failed (${resp.status})`); } const data = await resp.json(); setApprovals(Array.isArray(data.approvals) ? data.approvals : []); } catch (err) { setError(err.message || "Unable to load device approvals"); } finally { setLoading(false); } }, [statusFilter]); useEffect(() => { loadApprovals(); }, [loadApprovals]); const dedupedApprovals = useMemo(() => { const normalized = approvals .map((record) => ({ ...record, status: normalizeStatus(record.status) })) .sort((a, b) => { const left = new Date(a.created_at || 0).getTime(); const right = new Date(b.created_at || 0).getTime(); return left - right; }); if (statusFilter !== "pending") { return normalized; } const seen = new Set(); const unique = []; for (const record of normalized) { const key = record.ssl_key_fingerprint_claimed || record.hostname_claimed || record.id; if (seen.has(key)) continue; seen.add(key); unique.push(record); } return unique; }, [approvals, statusFilter]); const handleGuidChange = useCallback((id, value) => { setGuidInputs((prev) => ({ ...prev, [id]: value })); }, []); const submitApproval = useCallback( async (record, overrides = {}) => { if (!record?.id) return; setActioningId(record.id); setFeedback(null); setError(""); try { const manualGuid = (guidInputs[record.id] || "").trim(); const payload = {}; const overrideGuidRaw = overrides.guid; let overrideGuid = ""; if (typeof overrideGuidRaw === "string") { overrideGuid = overrideGuidRaw.trim(); } else if (overrideGuidRaw != null) { overrideGuid = String(overrideGuidRaw).trim(); } if (overrideGuid) { payload.guid = overrideGuid; } else if (manualGuid) { payload.guid = manualGuid; } const resolutionRaw = overrides.conflictResolution || overrides.resolution; if (typeof resolutionRaw === "string" && resolutionRaw.trim()) { payload.conflict_resolution = resolutionRaw.trim().toLowerCase(); } const resp = await fetch(`/api/admin/device-approvals/${encodeURIComponent(record.id)}/approve`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(Object.keys(payload).length ? payload : {}), }); const body = await resp.json().catch(() => ({})); if (!resp.ok) { if (resp.status === 409 && body.error === "conflict_resolution_required") { const conflict = record.hostname_conflict; const fallbackAlternate = record.alternate_hostname || (record.hostname_claimed ? `${record.hostname_claimed}-1` : ""); if (conflict) { setConflictPrompt({ record, conflict, alternate: fallbackAlternate || "", }); } return; } throw new Error(body.error || `Approval failed (${resp.status})`); } const appliedResolution = (body.conflict_resolution || payload.conflict_resolution || "").toLowerCase(); let successMessage = "Enrollment approved"; if (appliedResolution === "overwrite") successMessage = "Enrollment approved; existing device overwritten"; else if (appliedResolution === "coexist") successMessage = "Enrollment approved; devices will co-exist"; else if (appliedResolution === "auto_merge_fingerprint") successMessage = "Enrollment approved; device reconnected with its existing identity"; setFeedback({ type: "success", message: successMessage }); await loadApprovals(); } catch (err) { setFeedback({ type: "error", message: err.message || "Unable to approve request" }); } finally { setActioningId(null); } }, [guidInputs, loadApprovals] ); const startApprove = useCallback( (record) => { if (!record?.id) return; const status = normalizeStatus(record.status); if (status !== "pending") return; const manualGuid = (guidInputs[record.id] || "").trim(); const conflict = record.hostname_conflict; const requiresPrompt = Boolean(conflict?.requires_prompt ?? record.conflict_requires_prompt); if (requiresPrompt && !manualGuid) { const fallbackAlternate = record.alternate_hostname || (record.hostname_claimed ? `${record.hostname_claimed}-1` : ""); setConflictPrompt({ record, conflict, alternate: fallbackAlternate || "" }); return; } submitApproval(record); }, [guidInputs, submitApproval] ); const handleDeny = useCallback( async (record) => { if (!record?.id) return; const confirmDeny = window.confirm("Deny this enrollment request?"); if (!confirmDeny) return; setActioningId(record.id); setFeedback(null); setError(""); try { const resp = await fetch(`/api/admin/device-approvals/${encodeURIComponent(record.id)}/deny`, { method: "POST", credentials: "include", }); if (!resp.ok) { const body = await resp.json().catch(() => ({})); throw new Error(body.error || `Deny failed (${resp.status})`); } setFeedback({ type: "success", message: "Enrollment denied" }); await loadApprovals(); } catch (err) { setFeedback({ type: "error", message: err.message || "Unable to deny request" }); } finally { setActioningId(null); } }, [loadApprovals] ); const columns = useMemo(() => [ { headerName: "Status", field: "status", valueGetter: (p) => normalizeStatus(p.data?.status), cellRenderer: (params) => { const status = params.value || "pending"; // mimic MUI Chip coloring via text hues const color = status === "completed" ? "#34d399" : status === "approved" ? "#60a5fa" : status === "denied" || status === "expired" ? "#9aa0a6" : "#fbbf24"; return {status}; }, minWidth: 110, 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: "Approved By", valueGetter: (p) => p.data?.approved_by_username || p.data?.approved_by_user_id || "—", minWidth: 100, Width: 100, }, { headerName: "Actions", cellRenderer: (params) => { const record = params.data || {}; const status = normalizeStatus(record.status); const showActions = status === "pending"; const guidValue = params.context.guidInputs[record.id] || ""; const { startApprove, handleDeny, handleGuidChange, actioningId } = params.context; if (!showActions) { return No actions available; } const isBusy = actioningId === record.id; return ( handleGuidChange(record.id, e.target.value)} sx={{ minWidth: 220 }} /> ); }, minWidth: 480, flex: 1, }, ], []); const defaultColDef = useMemo(() => ({ sortable: true, filter: true, resizable: true, minWidth: 140, }), []); // Dialog helpers const conflictRecord = conflictPrompt?.record; const conflictInfo = conflictPrompt?.conflict; const conflictHostname = conflictRecord?.hostname_claimed || conflictRecord?.hostname || ""; const conflictSiteName = conflictInfo?.site_name || ""; const conflictSiteDescriptor = conflictInfo ? conflictSiteName ? `under site ${conflictSiteName}` : "under site (not assigned)" : "under site (not assigned)"; const conflictAlternate = conflictPrompt?.alternate || (conflictHostname ? `${conflictHostname}-1` : "hostname-1"); const conflictGuidDisplay = conflictInfo?.guid || ""; const handleConflictCancel = useCallback(() => setConflictPrompt(null), []); const handleConflictOverwrite = useCallback(() => { if (!conflictPrompt?.record) { setConflictPrompt(null); return; } const { record, conflict } = conflictPrompt; setConflictPrompt(null); const conflictGuid = conflict?.guid != null ? String(conflict.guid).trim() : ""; submitApproval(record, { guid: conflictGuid, conflictResolution: "overwrite" }); }, [conflictPrompt, submitApproval]); const handleConflictCoexist = useCallback(() => { if (!conflictPrompt?.record) { setConflictPrompt(null); return; } const { record } = conflictPrompt; setConflictPrompt(null); submitApproval(record, { conflictResolution: "coexist" }); }, [conflictPrompt, submitApproval]); return ( {/* Page header (no solid backdrop so gradient reaches the top) */} Device Approval Queue Status {/* Feedback */} {feedback && ( setFeedback(null)}> {feedback.message} )} {error && ( {error} )} {/* Data Grid */} {/* Conflict Dialog (unchanged logic) */} Hostname Conflict {conflictHostname ? `Device ${conflictHostname} already exists in the database ${conflictSiteDescriptor}.` : `A device with this hostname already exists in the database ${conflictSiteDescriptor}.`} Do you want this device to overwrite the existing device, or allow both to co-exist? {`Device will be renamed ${conflictAlternate} if you choose to allow both to co-exist.`} {conflictGuidDisplay ? ( Existing device GUID: {conflictGuidDisplay} ) : null} ); }