mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 19:21:58 -06:00
Added Device Conflict Resolution
This commit is contained in:
@@ -7,6 +7,11 @@ import {
|
||||
Button,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
@@ -76,6 +81,7 @@ function DeviceApprovals() {
|
||||
const [feedback, setFeedback] = useState(null);
|
||||
const [guidInputs, setGuidInputs] = useState({});
|
||||
const [actioningId, setActioningId] = useState(null);
|
||||
const [conflictPrompt, setConflictPrompt] = useState(null);
|
||||
|
||||
const loadApprovals = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -126,25 +132,49 @@ function DeviceApprovals() {
|
||||
setGuidInputs((prev) => ({ ...prev, [id]: value }));
|
||||
}, []);
|
||||
|
||||
const handleApprove = useCallback(
|
||||
async (record) => {
|
||||
const submitApproval = useCallback(
|
||||
async (record, overrides = {}) => {
|
||||
if (!record?.id) return;
|
||||
setActioningId(record.id);
|
||||
setFeedback(null);
|
||||
setError("");
|
||||
try {
|
||||
const guid = (guidInputs[record.id] || "").trim();
|
||||
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(guid ? { guid } : {}),
|
||||
body: JSON.stringify(Object.keys(payload).length ? payload : {}),
|
||||
});
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) {
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Approval failed (${resp.status})`);
|
||||
}
|
||||
setFeedback({ type: "success", message: "Enrollment approved" });
|
||||
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";
|
||||
}
|
||||
setFeedback({ type: "success", message: successMessage });
|
||||
await loadApprovals();
|
||||
} catch (err) {
|
||||
setFeedback({ type: "error", message: err.message || "Unable to approve request" });
|
||||
@@ -155,6 +185,73 @@ function DeviceApprovals() {
|
||||
[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;
|
||||
if (conflict && !manualGuid) {
|
||||
const fallbackAlternate =
|
||||
record.alternate_hostname ||
|
||||
(record.hostname_claimed ? `${record.hostname_claimed}-1` : "");
|
||||
setConflictPrompt({
|
||||
record,
|
||||
conflict,
|
||||
alternate: fallbackAlternate || "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
submitApproval(record);
|
||||
},
|
||||
[guidInputs, submitApproval]
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
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 handleDeny = useCallback(
|
||||
async (record) => {
|
||||
if (!record?.id) return;
|
||||
@@ -303,7 +400,7 @@ function DeviceApprovals() {
|
||||
<span>
|
||||
<IconButton
|
||||
color="success"
|
||||
onClick={() => handleApprove(record)}
|
||||
onClick={() => startApprove(record)}
|
||||
disabled={actioningId === record.id}
|
||||
>
|
||||
{actioningId === record.id ? (
|
||||
@@ -341,6 +438,48 @@ function DeviceApprovals() {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
<Dialog
|
||||
open={Boolean(conflictPrompt)}
|
||||
onClose={handleConflictCancel}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>Hostname Conflict</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Stack spacing={2}>
|
||||
<DialogContentText>
|
||||
{conflictHostname
|
||||
? `Device ${conflictHostname} already exists in the database ${conflictSiteDescriptor}.`
|
||||
: `A device with this hostname already exists in the database ${conflictSiteDescriptor}.`}
|
||||
</DialogContentText>
|
||||
<DialogContentText>
|
||||
Do you want this device to overwrite the existing device, or allow both to co-exist?
|
||||
</DialogContentText>
|
||||
<DialogContentText>
|
||||
{`Device will be renamed ${conflictAlternate} if you choose to allow both to co-exist.`}
|
||||
</DialogContentText>
|
||||
{conflictGuidDisplay ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Existing device GUID: {conflictGuidDisplay}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleConflictCancel}>Cancel</Button>
|
||||
<Button onClick={handleConflictCoexist} color="info" variant="outlined">
|
||||
Allow Both
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConflictOverwrite}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={!conflictGuidDisplay}
|
||||
>
|
||||
Overwrite Existing
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user