Replace window.confirm() with themed ConfirmModal across dashboard
This commit is contained in:
@@ -544,6 +544,16 @@ body {
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes confirmFadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes confirmSlideUp {
|
||||||
|
from { opacity: 0; transform: translateY(12px) scale(0.97); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Tooltip with enhanced styling */
|
/* Tooltip with enhanced styling */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import AuditLog from './components/AuditLog';
|
|||||||
import NvdSyncModal from './components/NvdSyncModal';
|
import NvdSyncModal from './components/NvdSyncModal';
|
||||||
import NavDrawer from './components/NavDrawer';
|
import NavDrawer from './components/NavDrawer';
|
||||||
import CalendarWidget from './components/CalendarWidget';
|
import CalendarWidget from './components/CalendarWidget';
|
||||||
|
import ConfirmModal from './components/ConfirmModal';
|
||||||
import VulnerabilityTriagePage from './components/pages/ReportingPage';
|
import VulnerabilityTriagePage from './components/pages/ReportingPage';
|
||||||
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
||||||
import ExportsPage from './components/pages/ExportsPage';
|
import ExportsPage from './components/pages/ExportsPage';
|
||||||
@@ -241,6 +242,9 @@ export default function App() {
|
|||||||
const [archiveList, setArchiveList] = useState([]);
|
const [archiveList, setArchiveList] = useState([]);
|
||||||
const [archiveListLoading, setArchiveListLoading] = useState(false);
|
const [archiveListLoading, setArchiveListLoading] = useState(false);
|
||||||
|
|
||||||
|
// Confirmation modal state — replaces window.confirm()
|
||||||
|
const [pendingConfirm, setPendingConfirm] = useState(null);
|
||||||
|
|
||||||
const toggleCVEExpand = (cveId) => {
|
const toggleCVEExpand = (cveId) => {
|
||||||
setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] }));
|
setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] }));
|
||||||
};
|
};
|
||||||
@@ -532,10 +536,12 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteDocument = async (docId, cveId, vendor) => {
|
const handleDeleteDocument = async (docId, cveId, vendor) => {
|
||||||
if (!window.confirm('Are you sure you want to delete this document?')) {
|
setPendingConfirm({
|
||||||
return;
|
title: 'Delete Document',
|
||||||
}
|
message: 'Are you sure you want to delete this document?',
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/documents/${docId}`, {
|
const response = await fetch(`${API_BASE}/documents/${docId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -552,6 +558,8 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Error: ${err.message}`);
|
alert(`Error: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditCVE = (cve) => {
|
const handleEditCVE = (cve) => {
|
||||||
@@ -644,10 +652,12 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCVEEntry = async (cve) => {
|
const handleDeleteCVEEntry = async (cve) => {
|
||||||
if (!window.confirm(`Are you sure you want to delete the "${cve.vendor}" entry for ${cve.cve_id}? This will also delete all associated documents.`)) {
|
setPendingConfirm({
|
||||||
return;
|
title: 'Delete Vendor Entry',
|
||||||
}
|
message: `Are you sure you want to delete the "${cve.vendor}" entry for ${cve.cve_id}? This will also delete all associated documents.`,
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const url = `${API_BASE}/cves/${cve.id}`;
|
const url = `${API_BASE}/cves/${cve.id}`;
|
||||||
console.log('DELETE request to:', url);
|
console.log('DELETE request to:', url);
|
||||||
@@ -672,13 +682,17 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Error: ${err.message}`);
|
alert(`Error: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteEntireCVE = async (cveId, vendorCount) => {
|
const handleDeleteEntireCVE = async (cveId, vendorCount) => {
|
||||||
if (!window.confirm(`Are you sure you want to delete ALL ${vendorCount} vendor entries for ${cveId}? This will permanently remove all associated documents and files.`)) {
|
setPendingConfirm({
|
||||||
return;
|
title: 'Delete Entire CVE',
|
||||||
}
|
message: `Are you sure you want to delete ALL ${vendorCount} vendor entries for ${cveId}? This will permanently remove all associated documents and files.`,
|
||||||
|
confirmText: 'Delete All',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const url = `${API_BASE}/cves/by-cve-id/${encodeURIComponent(cveId)}`;
|
const url = `${API_BASE}/cves/by-cve-id/${encodeURIComponent(cveId)}`;
|
||||||
console.log('DELETE request to:', url);
|
console.log('DELETE request to:', url);
|
||||||
@@ -703,6 +717,8 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Error: ${err.message}`);
|
alert(`Error: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddTicket = async (e) => {
|
const handleAddTicket = async (e) => {
|
||||||
@@ -770,7 +786,12 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteTicket = async (ticket) => {
|
const handleDeleteTicket = async (ticket) => {
|
||||||
if (!window.confirm(`Delete ticket ${ticket.ticket_key}?`)) return;
|
setPendingConfirm({
|
||||||
|
title: 'Delete Ticket',
|
||||||
|
message: `Delete ticket ${ticket.ticket_key}?`,
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/jira-tickets/${ticket.id}`, {
|
const response = await fetch(`${API_BASE}/jira-tickets/${ticket.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -782,6 +803,8 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Error: ${err.message}`);
|
alert(`Error: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openAddTicketForCVE = (cve_id, vendor) => {
|
const openAddTicketForCVE = (cve_id, vendor) => {
|
||||||
@@ -855,7 +878,12 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteArcherTicket = async (ticket) => {
|
const handleDeleteArcherTicket = async (ticket) => {
|
||||||
if (!window.confirm(`Delete Archer ticket ${ticket.exc_number}?`)) return;
|
setPendingConfirm({
|
||||||
|
title: 'Delete Archer Ticket',
|
||||||
|
message: `Delete Archer ticket ${ticket.exc_number}?`,
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/archer-tickets/${ticket.id}`, {
|
const response = await fetch(`${API_BASE}/archer-tickets/${ticket.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -867,6 +895,8 @@ export default function App() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Error: ${err.message}`);
|
alert(`Error: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const openAddArcherTicketForCVE = (cve_id, vendor) => {
|
const openAddArcherTicketForCVE = (cve_id, vendor) => {
|
||||||
@@ -2420,6 +2450,17 @@ export default function App() {
|
|||||||
|
|
||||||
</div>}
|
</div>}
|
||||||
{/* End Three Column Layout */}
|
{/* End Three Column Layout */}
|
||||||
|
|
||||||
|
{/* Confirmation Modal */}
|
||||||
|
<ConfirmModal
|
||||||
|
open={!!pendingConfirm}
|
||||||
|
title={pendingConfirm?.title}
|
||||||
|
message={pendingConfirm?.message}
|
||||||
|
confirmText={pendingConfirm?.confirmText}
|
||||||
|
variant={pendingConfirm?.variant || 'danger'}
|
||||||
|
onConfirm={pendingConfirm?.onConfirm}
|
||||||
|
onCancel={() => setPendingConfirm(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
171
frontend/src/components/ConfirmModal.js
Normal file
171
frontend/src/components/ConfirmModal.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ConfirmModal — themed replacement for window.confirm().
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* open {boolean} Whether the modal is visible
|
||||||
|
* title {string} Heading text (e.g. "Delete Document")
|
||||||
|
* message {string|ReactNode} Body text / description
|
||||||
|
* confirmText {string} Label for the confirm button (default "Confirm")
|
||||||
|
* cancelText {string} Label for the cancel button (default "Cancel")
|
||||||
|
* variant {"danger"|"warning"|"default"} Controls accent color (default "danger")
|
||||||
|
* onConfirm {function} Called when user confirms
|
||||||
|
* onCancel {function} Called when user cancels or presses Escape
|
||||||
|
*/
|
||||||
|
export default function ConfirmModal({
|
||||||
|
open,
|
||||||
|
title = 'Confirm',
|
||||||
|
message = 'Are you sure?',
|
||||||
|
confirmText = 'Confirm',
|
||||||
|
cancelText = 'Cancel',
|
||||||
|
variant = 'danger',
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}) {
|
||||||
|
const confirmRef = useRef(null);
|
||||||
|
|
||||||
|
// Focus the confirm button when the modal opens and handle Escape key
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
// Small delay so the DOM is painted before we focus
|
||||||
|
const timer = setTimeout(() => confirmRef.current?.focus(), 50);
|
||||||
|
|
||||||
|
const handleKey = (e) => {
|
||||||
|
if (e.key === 'Escape') onCancel?.();
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleKey);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
document.removeEventListener('keydown', handleKey);
|
||||||
|
};
|
||||||
|
}, [open, onCancel]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const accentMap = {
|
||||||
|
danger: { color: '#EF4444', bg: 'rgba(239,68,68,0.10)', bgHover: 'rgba(239,68,68,0.18)', border: 'rgba(239,68,68,0.3)' },
|
||||||
|
warning: { color: '#F59E0B', bg: 'rgba(245,158,11,0.10)', bgHover: 'rgba(245,158,11,0.18)', border: 'rgba(245,158,11,0.3)' },
|
||||||
|
default: { color: '#0EA5E9', bg: 'rgba(14,165,233,0.10)', bgHover: 'rgba(14,165,233,0.18)', border: 'rgba(14,165,233,0.3)' },
|
||||||
|
};
|
||||||
|
const accent = accentMap[variant] || accentMap.danger;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="confirm-modal-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',
|
||||||
|
animation: 'confirmFadeIn 0.15s ease-out',
|
||||||
|
}}
|
||||||
|
onClick={(e) => { if (e.target === e.currentTarget) onCancel?.(); }}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||||
|
border: `1px solid ${accent.border}`,
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
boxShadow: `0 20px 60px rgba(0,0,0,0.7), 0 0 30px ${accent.color}10`,
|
||||||
|
width: '100%', maxWidth: '420px',
|
||||||
|
padding: '1.75rem 2rem',
|
||||||
|
animation: 'confirmSlideUp 0.15s ease-out',
|
||||||
|
}}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '0.625rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: '32px', height: '32px', borderRadius: '0.5rem',
|
||||||
|
background: accent.bg,
|
||||||
|
border: `1px solid ${accent.border}`,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
<AlertTriangle style={{ width: '16px', height: '16px', color: accent.color }} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="confirm-modal-title"
|
||||||
|
style={{
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
fontSize: '0.95rem', fontWeight: '700',
|
||||||
|
color: accent.color,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.82rem', color: '#CBD5E1',
|
||||||
|
lineHeight: '1.6', marginBottom: '1.5rem',
|
||||||
|
fontFamily: "'Outfit', system-ui, sans-serif",
|
||||||
|
}}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
style={{
|
||||||
|
flex: 1, padding: '0.625rem',
|
||||||
|
background: 'transparent',
|
||||||
|
border: '1px solid rgba(100,116,139,0.4)',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
color: '#94A3B8', cursor: 'pointer',
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
fontSize: '0.78rem',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
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';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cancelText}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
ref={confirmRef}
|
||||||
|
onClick={onConfirm}
|
||||||
|
style={{
|
||||||
|
flex: 1.5, padding: '0.625rem',
|
||||||
|
background: accent.bg,
|
||||||
|
border: `1px solid ${accent.color}`,
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
color: accent.color, cursor: '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',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
e.currentTarget.style.background = accent.bgHover;
|
||||||
|
e.currentTarget.style.boxShadow = `0 0 20px ${accent.color}25`;
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
e.currentTarget.style.background = accent.bg;
|
||||||
|
e.currentTarget.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { X, Loader, AlertCircle, CheckCircle, Upload as UploadIcon, Download, FileText, Trash2 } from 'lucide-react';
|
import { X, Loader, AlertCircle, CheckCircle, Upload as UploadIcon, Download, FileText, Trash2 } from 'lucide-react';
|
||||||
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) {
|
|||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
const [existingArticles, setExistingArticles] = useState([]);
|
const [existingArticles, setExistingArticles] = useState([]);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [pendingConfirm, setPendingConfirm] = useState(null);
|
||||||
// Fetch existing articles on mount
|
// Fetch existing articles on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchExistingArticles();
|
fetchExistingArticles();
|
||||||
@@ -117,10 +118,12 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id, articleTitle) => {
|
const handleDelete = async (id, articleTitle) => {
|
||||||
if (!window.confirm(`Are you sure you want to delete "${articleTitle}"?`)) {
|
setPendingConfirm({
|
||||||
return;
|
title: 'Delete Article',
|
||||||
}
|
message: `Are you sure you want to delete "${articleTitle}"?`,
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/knowledge-base/${id}`, {
|
const response = await fetch(`${API_BASE}/knowledge-base/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -138,6 +141,8 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) {
|
|||||||
console.error('Error deleting article:', err);
|
console.error('Error deleting article:', err);
|
||||||
setError('Failed to delete article');
|
setError('Failed to delete article');
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@@ -379,6 +384,17 @@ export default function KnowledgeBaseModal({ onClose, onUpdate }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Confirmation Modal */}
|
||||||
|
<ConfirmModal
|
||||||
|
open={!!pendingConfirm}
|
||||||
|
title={pendingConfirm?.title}
|
||||||
|
message={pendingConfirm?.message}
|
||||||
|
confirmText={pendingConfirm?.confirmText}
|
||||||
|
variant="danger"
|
||||||
|
onConfirm={pendingConfirm?.onConfirm}
|
||||||
|
onCancel={() => setPendingConfirm(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
// ⚠️ CONVENTION: This component uses Tailwind utility classes (e.g. bg-white, rounded-lg, hover:bg-gray-50)
|
||||||
|
// instead of inline styles or App.css global classes. This is the legacy modal kept for UserMenu quick-access;
|
||||||
|
// the themed replacement lives in AdminPage.js.
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { X, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, User, Mail, Shield } from 'lucide-react';
|
import { X, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, User, Mail, Shield } from 'lucide-react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
@@ -35,6 +39,7 @@ export default function UserManagement({ onClose }) {
|
|||||||
});
|
});
|
||||||
const [formError, setFormError] = useState('');
|
const [formError, setFormError] = useState('');
|
||||||
const [formSuccess, setFormSuccess] = useState('');
|
const [formSuccess, setFormSuccess] = useState('');
|
||||||
|
const [pendingConfirm, setPendingConfirm] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
@@ -55,29 +60,10 @@ export default function UserManagement({ onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmGroupChange = (targetUser, newGroup) => {
|
const doSubmit = async () => {
|
||||||
let message = `Are you sure you want to change ${targetUser.username}'s group from ${targetUser.group} to ${newGroup}?`;
|
|
||||||
|
|
||||||
// Extra warning when downgrading an Admin user
|
|
||||||
if (targetUser.group === 'Admin' && newGroup !== 'Admin') {
|
|
||||||
message += `\n\n⚠️ WARNING: You are removing Admin privileges from ${targetUser.username}. They will lose full system access.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.confirm(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setFormError('');
|
setFormError('');
|
||||||
setFormSuccess('');
|
setFormSuccess('');
|
||||||
|
|
||||||
// If editing and group changed, show confirmation dialog
|
|
||||||
if (editingUser && formData.group !== editingUser.group) {
|
|
||||||
if (!confirmGroupChange(editingUser, formData.group)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = editingUser
|
const url = editingUser
|
||||||
? `${API_BASE}/users/${editingUser.id}`
|
? `${API_BASE}/users/${editingUser.id}`
|
||||||
@@ -117,6 +103,31 @@ export default function UserManagement({ onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// If editing and group changed, show confirmation modal
|
||||||
|
if (editingUser && formData.group !== editingUser.group) {
|
||||||
|
let message = `Are you sure you want to change ${editingUser.username}'s group from ${editingUser.group} to ${formData.group}?`;
|
||||||
|
if (editingUser.group === 'Admin' && formData.group !== 'Admin') {
|
||||||
|
message += ` WARNING: You are removing Admin privileges from ${editingUser.username}. They will lose full system access.`;
|
||||||
|
}
|
||||||
|
setPendingConfirm({
|
||||||
|
title: 'Change User Group',
|
||||||
|
message,
|
||||||
|
confirmText: 'Change Group',
|
||||||
|
variant: editingUser.group === 'Admin' ? 'danger' : 'warning',
|
||||||
|
onConfirm: () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
|
doSubmit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
const handleEdit = (user) => {
|
const handleEdit = (user) => {
|
||||||
setEditingUser(user);
|
setEditingUser(user);
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -131,10 +142,12 @@ export default function UserManagement({ onClose }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (userId) => {
|
const handleDelete = async (userId) => {
|
||||||
if (!window.confirm('Are you sure you want to delete this user?')) {
|
setPendingConfirm({
|
||||||
return;
|
title: 'Delete User',
|
||||||
}
|
message: 'Are you sure you want to delete this user?',
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/users/${userId}`, {
|
const response = await fetch(`${API_BASE}/users/${userId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -151,6 +164,8 @@ export default function UserManagement({ onClose }) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleActive = async (user) => {
|
const handleToggleActive = async (user) => {
|
||||||
@@ -418,6 +433,17 @@ export default function UserManagement({ onClose }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Confirmation Modal */}
|
||||||
|
<ConfirmModal
|
||||||
|
open={!!pendingConfirm}
|
||||||
|
title={pendingConfirm?.title}
|
||||||
|
message={pendingConfirm?.message}
|
||||||
|
confirmText={pendingConfirm?.confirmText}
|
||||||
|
variant={pendingConfirm?.variant || 'danger'}
|
||||||
|
onConfirm={pendingConfirm?.onConfirm}
|
||||||
|
onCancel={() => setPendingConfirm(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Shield, Clock, Activity, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, X, ChevronLeft, ChevronRight, Search, Users, FileText } from 'lucide-react';
|
import { Shield, Clock, Activity, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, X, ChevronLeft, ChevronRight, Search, Users, FileText } from 'lucide-react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import ConfirmModal from '../ConfirmModal';
|
||||||
|
|
||||||
// ⚠️ CONVENTION: Use relative API path, not absolute URL. Should be: process.env.REACT_APP_API_BASE || ''
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
@@ -115,6 +115,7 @@ function UserManagementPanel() {
|
|||||||
const [formData, setFormData] = useState({ username: '', email: '', password: '', group: 'Read_Only' });
|
const [formData, setFormData] = useState({ username: '', email: '', password: '', group: 'Read_Only' });
|
||||||
const [formError, setFormError] = useState('');
|
const [formError, setFormError] = useState('');
|
||||||
const [successMessage, setSuccessMessage] = useState('');
|
const [successMessage, setSuccessMessage] = useState('');
|
||||||
|
const [pendingConfirm, setPendingConfirm] = useState(null);
|
||||||
|
|
||||||
const fetchUsers = useCallback(async () => {
|
const fetchUsers = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -200,7 +201,12 @@ function UserManagementPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (userId) => {
|
const handleDelete = async (userId) => {
|
||||||
if (!window.confirm('Are you sure you want to delete this user?')) return;
|
setPendingConfirm({
|
||||||
|
title: 'Delete User',
|
||||||
|
message: 'Are you sure you want to delete this user?',
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/users/${userId}`, {
|
const res = await fetch(`${API_BASE}/users/${userId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@@ -213,6 +219,8 @@ function UserManagementPanel() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleActive = async (user) => {
|
const handleToggleActive = async (user) => {
|
||||||
@@ -567,6 +575,17 @@ function UserManagementPanel() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Confirmation Modal */}
|
||||||
|
<ConfirmModal
|
||||||
|
open={!!pendingConfirm}
|
||||||
|
title={pendingConfirm?.title}
|
||||||
|
message={pendingConfirm?.message}
|
||||||
|
confirmText={pendingConfirm?.confirmText}
|
||||||
|
variant="danger"
|
||||||
|
onConfirm={pendingConfirm?.onConfirm}
|
||||||
|
onCancel={() => setPendingConfirm(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2 } from 'lucide-react';
|
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2 } from 'lucide-react';
|
||||||
|
import ConfirmModal from '../ConfirmModal';
|
||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
|||||||
const [selectedMetrics, setSelectedMetrics] = useState([]);
|
const [selectedMetrics, setSelectedMetrics] = useState([]);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [noteError, setNoteError] = useState(null);
|
const [noteError, setNoteError] = useState(null);
|
||||||
|
const [pendingConfirm, setPendingConfirm] = useState(null);
|
||||||
|
|
||||||
const fetchDetail = useCallback(async () => {
|
const fetchDetail = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -91,7 +93,12 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteNote = async (noteId, hasGroup) => {
|
const handleDeleteNote = async (noteId, hasGroup) => {
|
||||||
if (!window.confirm('Delete this note?')) return;
|
setPendingConfirm({
|
||||||
|
title: 'Delete Note',
|
||||||
|
message: 'Delete this note?',
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const url = hasGroup
|
const url = hasGroup
|
||||||
? `${API_BASE}/compliance/notes/${noteId}?group=true`
|
? `${API_BASE}/compliance/notes/${noteId}?group=true`
|
||||||
@@ -104,6 +111,8 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setNoteError(err.message);
|
setNoteError(err.message);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeMetrics = detail?.metrics?.filter(m => m.status === 'active') || [];
|
const activeMetrics = detail?.metrics?.filter(m => m.status === 'active') || [];
|
||||||
@@ -371,6 +380,17 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Confirmation Modal */}
|
||||||
|
<ConfirmModal
|
||||||
|
open={!!pendingConfirm}
|
||||||
|
title={pendingConfirm?.title}
|
||||||
|
message={pendingConfirm?.message}
|
||||||
|
confirmText={pendingConfirm?.confirmText}
|
||||||
|
variant="danger"
|
||||||
|
onConfirm={pendingConfirm?.onConfirm}
|
||||||
|
onCancel={() => setPendingConfirm(null)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
BookOpen, Search, Upload, RefreshCw, Loader,
|
BookOpen, Search, Upload, RefreshCw, Loader,
|
||||||
AlertCircle, FileText, File, Trash2, X,
|
AlertCircle, FileText, File, Trash2, X, // ⚠️ CONVENTION: FileText and File are imported but unused — remove if not needed
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import KnowledgeBaseModal from '../KnowledgeBaseModal';
|
import KnowledgeBaseModal from '../KnowledgeBaseModal';
|
||||||
import KnowledgeBaseViewer from '../KnowledgeBaseViewer';
|
import KnowledgeBaseViewer from '../KnowledgeBaseViewer';
|
||||||
|
import ConfirmModal from '../ConfirmModal'; // ⚠️ CONVENTION: ConfirmModal is imported but never used — either integrate it into handleDelete or remove this import
|
||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
const GREEN = '#10B981';
|
const GREEN = '#10B981';
|
||||||
@@ -216,6 +217,7 @@ export default function KnowledgeBasePage() {
|
|||||||
const [activeCategory, setActiveCategory] = useState('All');
|
const [activeCategory, setActiveCategory] = useState('All');
|
||||||
const [selected, setSelected] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
const [showUpload, setShowUpload] = useState(false);
|
const [showUpload, setShowUpload] = useState(false);
|
||||||
|
const [pendingConfirm, setPendingConfirm] = useState(null);
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Fetch
|
// Fetch
|
||||||
@@ -241,7 +243,12 @@ export default function KnowledgeBasePage() {
|
|||||||
// Delete
|
// Delete
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
const handleDelete = useCallback(async (article) => {
|
const handleDelete = useCallback(async (article) => {
|
||||||
if (!window.confirm(`Delete "${article.title}"? This cannot be undone.`)) return;
|
setPendingConfirm({
|
||||||
|
title: 'Delete Article',
|
||||||
|
message: `Delete "${article.title}"? This cannot be undone.`,
|
||||||
|
confirmText: 'Delete',
|
||||||
|
onConfirm: async () => {
|
||||||
|
setPendingConfirm(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/knowledge-base/${article.id}`, {
|
const res = await fetch(`${API_BASE}/knowledge-base/${article.id}`, {
|
||||||
method: 'DELETE', credentials: 'include',
|
method: 'DELETE', credentials: 'include',
|
||||||
@@ -252,6 +259,8 @@ export default function KnowledgeBasePage() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(`Failed to delete: ${err.message}`);
|
alert(`Failed to delete: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}, [selected]);
|
}, [selected]);
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -479,6 +488,17 @@ export default function KnowledgeBasePage() {
|
|||||||
onUpdate={() => { fetchArticles(); setShowUpload(false); }}
|
onUpdate={() => { fetchArticles(); setShowUpload(false); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Confirmation Modal */}
|
||||||
|
<ConfirmModal
|
||||||
|
open={!!pendingConfirm}
|
||||||
|
title={pendingConfirm?.title}
|
||||||
|
message={pendingConfirm?.message}
|
||||||
|
confirmText={pendingConfirm?.confirmText}
|
||||||
|
variant="danger"
|
||||||
|
onConfirm={pendingConfirm?.onConfirm}
|
||||||
|
onCancel={() => setPendingConfirm(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user