Adds a template management system to the Ivanti Queue's Archer Risk Acceptance workflow. Templates store static form content (Environment Overview, Segmentation, Mitigating Controls, etc.) organized by Vendor > Platform > Model hierarchy. Features: - Full CRUD API at /api/archer-templates with search, filter, clone, and hierarchy navigation endpoints - Template Manager page (nav: Template Mgr) with grouped list view, create/edit/clone/delete modals, role-based access - TemplateSelector component integrated into Ivanti Todo Queue for Archer workflow items with per-section copy-to-clipboard buttons and Copy All functionality - Database migration with case-insensitive uniqueness enforcement - Audit logging for all template mutations New files: - backend/migrations/add_archer_templates_table.js - backend/routes/archerTemplates.js - frontend/src/components/pages/ArcherTemplatePage.js - frontend/src/components/TemplateSelector.js - frontend/src/components/TemplateFormModal.js - frontend/src/components/DeleteConfirmModal.js
272 lines
9.0 KiB
JavaScript
272 lines
9.0 KiB
JavaScript
// DeleteConfirmModal.js
|
|
// Confirmation dialog for deleting Archer templates.
|
|
// Identifies the template by vendor/platform/model before deletion.
|
|
// On confirm: calls DELETE API, invokes onConfirm callback, closes.
|
|
// On cancel: dismisses dialog, leaves template unchanged.
|
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { AlertTriangle, Trash2 } from 'lucide-react';
|
|
|
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
|
|
|
/**
|
|
* DeleteConfirmModal — confirmation dialog for deleting an Archer template.
|
|
*
|
|
* Props:
|
|
* template {object|null} The template to delete (contains id, vendor, platform, model).
|
|
* When null/undefined, modal is hidden.
|
|
* onConfirm {function} Callback after successful delete (refresh list).
|
|
* onCancel {function} Callback to close without deleting.
|
|
*/
|
|
export default function DeleteConfirmModal({ template, onConfirm, onCancel }) {
|
|
const [deleting, setDeleting] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const cancelRef = useRef(null);
|
|
|
|
// Focus cancel button on open and handle Escape key
|
|
useEffect(() => {
|
|
if (!template) return;
|
|
|
|
const timer = setTimeout(() => cancelRef.current?.focus(), 50);
|
|
|
|
const handleKey = (e) => {
|
|
if (e.key === 'Escape' && !deleting) onCancel?.();
|
|
};
|
|
document.addEventListener('keydown', handleKey);
|
|
return () => {
|
|
clearTimeout(timer);
|
|
document.removeEventListener('keydown', handleKey);
|
|
};
|
|
}, [template, deleting, onCancel]);
|
|
|
|
// Reset state when template changes (new modal open)
|
|
useEffect(() => {
|
|
if (template) {
|
|
setDeleting(false);
|
|
setError(null);
|
|
}
|
|
}, [template]);
|
|
|
|
const handleConfirm = useCallback(async () => {
|
|
if (!template) return;
|
|
setDeleting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/archer-templates/${template.id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.error || `Delete failed (${res.status})`);
|
|
}
|
|
|
|
onConfirm?.();
|
|
} catch (err) {
|
|
setError(err.message);
|
|
setDeleting(false);
|
|
}
|
|
}, [template, onConfirm]);
|
|
|
|
if (!template) return null;
|
|
|
|
return (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="delete-confirm-title"
|
|
style={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
zIndex: 70,
|
|
background: 'rgba(10, 14, 39, 0.95)',
|
|
backdropFilter: 'blur(8px)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '1rem',
|
|
}}
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget && !deleting) onCancel?.();
|
|
}}
|
|
>
|
|
<div style={{
|
|
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
|
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
borderRadius: '0.75rem',
|
|
boxShadow: '0 20px 60px rgba(0,0,0,0.7), 0 0 30px rgba(239,68,68,0.06)',
|
|
width: '100%',
|
|
maxWidth: '440px',
|
|
padding: '1.75rem 2rem',
|
|
}}>
|
|
{/* Header */}
|
|
<div style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.625rem',
|
|
marginBottom: '1rem',
|
|
}}>
|
|
<div style={{
|
|
width: '32px',
|
|
height: '32px',
|
|
borderRadius: '0.5rem',
|
|
background: 'rgba(239, 68, 68, 0.10)',
|
|
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexShrink: 0,
|
|
}}>
|
|
<AlertTriangle style={{ width: '16px', height: '16px', color: '#EF4444' }} />
|
|
</div>
|
|
<div
|
|
id="delete-confirm-title"
|
|
style={{
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
fontSize: '0.95rem',
|
|
fontWeight: '700',
|
|
color: '#EF4444',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
}}
|
|
>
|
|
Delete Template
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div style={{
|
|
fontSize: '0.82rem',
|
|
color: '#CBD5E1',
|
|
lineHeight: '1.6',
|
|
marginBottom: '1.25rem',
|
|
fontFamily: "'Outfit', system-ui, sans-serif",
|
|
}}>
|
|
<p style={{ margin: '0 0 0.75rem 0' }}>
|
|
Are you sure you want to delete this template? This action cannot be undone.
|
|
</p>
|
|
<div style={{
|
|
background: 'rgba(239, 68, 68, 0.06)',
|
|
border: '1px solid rgba(239, 68, 68, 0.15)',
|
|
borderRadius: '0.5rem',
|
|
padding: '0.75rem 1rem',
|
|
}}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.3rem' }}>
|
|
<span style={{ color: '#94A3B8', fontSize: '0.72rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
|
Vendor
|
|
</span>
|
|
<span style={{ color: '#e0e0e0', fontSize: '0.82rem', fontWeight: 600 }}>
|
|
{template.vendor}
|
|
</span>
|
|
<span style={{ color: '#94A3B8', fontSize: '0.72rem', textTransform: 'uppercase', letterSpacing: '0.05em', marginTop: '0.3rem' }}>
|
|
Platform
|
|
</span>
|
|
<span style={{ color: '#e0e0e0', fontSize: '0.82rem', fontWeight: 600 }}>
|
|
{template.platform}
|
|
</span>
|
|
<span style={{ color: '#94A3B8', fontSize: '0.72rem', textTransform: 'uppercase', letterSpacing: '0.05em', marginTop: '0.3rem' }}>
|
|
Model
|
|
</span>
|
|
<span style={{ color: '#e0e0e0', fontSize: '0.82rem', fontWeight: 600 }}>
|
|
{template.model}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error banner */}
|
|
{error && (
|
|
<div style={{
|
|
padding: '0.6rem 0.75rem',
|
|
borderRadius: '0.375rem',
|
|
background: 'rgba(239, 68, 68, 0.1)',
|
|
border: '1px solid rgba(239, 68, 68, 0.25)',
|
|
color: '#FCA5A5',
|
|
fontSize: '0.78rem',
|
|
marginBottom: '1rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.4rem',
|
|
}}>
|
|
<AlertTriangle style={{ width: '12px', height: '12px', flexShrink: 0 }} />
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
|
<button
|
|
ref={cancelRef}
|
|
onClick={onCancel}
|
|
disabled={deleting}
|
|
style={{
|
|
flex: 1,
|
|
padding: '0.625rem',
|
|
background: 'transparent',
|
|
border: '1px solid rgba(100,116,139,0.4)',
|
|
borderRadius: '0.375rem',
|
|
color: '#94A3B8',
|
|
cursor: deleting ? 'not-allowed' : 'pointer',
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
fontSize: '0.78rem',
|
|
opacity: deleting ? 0.5 : 1,
|
|
transition: 'all 0.2s ease',
|
|
}}
|
|
onMouseEnter={e => {
|
|
if (!deleting) {
|
|
e.currentTarget.style.borderColor = 'rgba(100,116,139,0.8)';
|
|
e.currentTarget.style.color = '#CBD5E1';
|
|
}
|
|
}}
|
|
onMouseLeave={e => {
|
|
e.currentTarget.style.borderColor = 'rgba(100,116,139,0.4)';
|
|
e.currentTarget.style.color = '#94A3B8';
|
|
}}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleConfirm}
|
|
disabled={deleting}
|
|
style={{
|
|
flex: 1.5,
|
|
padding: '0.625rem',
|
|
background: 'rgba(239, 68, 68, 0.10)',
|
|
border: '1px solid #EF4444',
|
|
borderRadius: '0.375rem',
|
|
color: '#EF4444',
|
|
cursor: deleting ? 'not-allowed' : 'pointer',
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
fontSize: '0.78rem',
|
|
fontWeight: '600',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.05em',
|
|
transition: 'all 0.2s ease',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '0.4rem',
|
|
opacity: deleting ? 0.7 : 1,
|
|
}}
|
|
onMouseEnter={e => {
|
|
if (!deleting) {
|
|
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.18)';
|
|
e.currentTarget.style.boxShadow = '0 0 20px rgba(239,68,68,0.15)';
|
|
}
|
|
}}
|
|
onMouseLeave={e => {
|
|
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.10)';
|
|
e.currentTarget.style.boxShadow = 'none';
|
|
}}
|
|
>
|
|
<Trash2 style={{ width: '13px', height: '13px' }} />
|
|
{deleting ? 'Deleting...' : 'Delete Template'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|