166 lines
6.2 KiB
JavaScript
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>
|
||
|
|
);
|
||
|
|
}
|