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
} onClick={loadApprovals} disabled={loading}>
Refresh
{/* Feedback */}
{feedback && (
setFeedback(null)}>
{feedback.message}
)}
{error && (
{error}
)}
{/* Data Grid */}
{/* Conflict Dialog (unchanged logic) */}
);
}