Files
cve-dashboard/frontend/src/components/modals/ArcherTicketModal.js

166 lines
6.2 KiB
JavaScript
Raw Normal View History

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
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>
);
}