Files
cve-dashboard/frontend/src/components/modals/ArcherTicketModal.js
Jordan Ramos 4a0adfb574 Refactor home page: extract components, add toast system, debounce search
Major restructuring of the monolithic App.js (2484 lines) into focused,
testable components:

Architecture:
- App.js is now a 189-line routing shell (header, nav, page switching)
- HomePage.js orchestrates all home page state and layout
- Each visual section is its own component with clear props API

Extracted components:
- StatsBar: clickable stat cards that filter by severity
- QuickCVELookup: CVE existence check with inline results
- CVEFilters: search + vendor/severity dropdowns
- CVECard: expandable CVE with vendor entries, docs, tickets
- OpenTicketsPanel: right sidebar open JIRA tickets
- IvantiWorkflowPanel: right sidebar Ivanti workflow status + archive

Extracted modals:
- AddCVEModal: self-contained add form with NVD auto-fill
- EditCVEModal: self-contained edit form with NVD update
- JiraTicketModal: unified add/edit JIRA ticket modal
- ArcherTicketModal: unified add/edit Archer ticket modal

Performance optimizations:
- Debounced search (300ms) via useDebounce hook — eliminates
  redundant API calls on every keystroke
- Memoized groupedCVEs, openTicketCount, criticalCount via useMemo
- Proper state updates (no direct mutation of cveDocuments)
- useCallback on fetch functions to stabilize effect dependencies

UX improvements:
- Toast notification system replaces all alert() calls
- Stat cards are now clickable to filter CVE list by severity
- onKeyDown replaces deprecated onKeyPress
- aria-labels added to interactive elements

Infrastructure:
- ToastContext with auto-dismiss, typed toasts (success/error/warning/info)
- useDebounce custom hook for reuse across the app
- Toast slide-in animation in App.css
2026-06-23 11:46:39 -06:00

166 lines
6.2 KiB
JavaScript

import React, { useState } from 'react';
import { XCircle } from 'lucide-react';
import { useToast } from '../../contexts/ToastContext';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
/**
* Shared modal for adding and editing Archer risk acceptance tickets.
* Props:
* - ticket: existing ticket (edit mode) or null (add mode)
* - context: { cve_id, vendor } when adding from a CVE card
* - onClose: close handler
* - onSuccess: refresh handler
*/
export default function ArcherTicketModal({ ticket, context, onClose, onSuccess }) {
const toast = useToast();
const isEdit = !!ticket;
const [form, setForm] = useState({
exc_number: ticket?.exc_number || '',
archer_url: ticket?.archer_url || '',
status: ticket?.status || 'Draft',
cve_id: ticket?.cve_id || context?.cve_id || '',
vendor: ticket?.vendor || context?.vendor || '',
});
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (isEdit) {
const response = await fetch(`${API_BASE}/archer-tickets/${ticket.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
exc_number: form.exc_number,
archer_url: form.archer_url,
status: form.status,
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to update Archer ticket');
}
toast.success('Archer ticket updated');
} else {
const response = await fetch(`${API_BASE}/archer-tickets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(form),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to create Archer ticket');
}
toast.success('Archer ticket added');
}
onSuccess();
onClose();
} catch (err) {
toast.error(err.message);
}
};
return (
<div className="fixed inset-0 modal-overlay flex items-center justify-center z-50 p-4">
<div className="intel-card rounded-lg shadow-2xl max-w-md w-full border-purple-500">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold text-purple-400 font-mono">
{isEdit ? 'Edit Archer Risk Ticket' : 'Add Archer Risk Ticket'}
</h2>
<button onClick={onClose} className="text-gray-400 hover:text-intel-accent transition-colors" aria-label="Close">
<XCircle className="w-6 h-6" />
</button>
</div>
{isEdit && (
<div className="p-3 bg-intel-medium rounded text-sm text-white mb-4 font-mono">
{ticket.cve_id} / {ticket.vendor}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">EXC Number *</label>
<input
type="text"
required
placeholder="EXC-5754"
value={form.exc_number}
onChange={(e) => setForm({ ...form, exc_number: e.target.value.toUpperCase() })}
className="intel-input w-full"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Archer URL</label>
<input
type="url"
placeholder="https://archer.example.com/..."
value={form.archer_url}
onChange={(e) => setForm({ ...form, archer_url: e.target.value })}
className="intel-input w-full"
/>
</div>
{!isEdit && (
<>
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">CVE ID *</label>
<input
type="text"
required
placeholder="CVE-2024-1234"
value={form.cve_id}
onChange={(e) => setForm({ ...form, cve_id: e.target.value.toUpperCase() })}
className="intel-input w-full"
readOnly={!!context}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Vendor *</label>
<input
type="text"
required
placeholder="Vendor name"
value={form.vendor}
onChange={(e) => setForm({ ...form, vendor: e.target.value })}
className="intel-input w-full"
readOnly={!!context}
/>
</div>
</>
)}
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Status</label>
<select
value={form.status}
onChange={(e) => setForm({ ...form, status: e.target.value })}
className="intel-input w-full"
>
<option value="Draft">Draft</option>
<option value="Open">Open</option>
<option value="Under Review">Under Review</option>
<option value="Accepted">Accepted</option>
</select>
</div>
<div className="flex gap-3 pt-4">
<button type="submit" className="flex-1 intel-button intel-button-primary">
{isEdit ? 'Save Changes' : 'Create Ticket'}
</button>
<button type="button" onClick={onClose} className="px-4 py-2 bg-intel-dark text-gray-400 rounded border border-gray-600 hover:bg-intel-medium transition-colors font-mono text-sm uppercase tracking-wider">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
);
}