added migration and feature set for archer ticekts

This commit is contained in:
2026-02-18 15:02:25 -07:00
parent 112eb8dac1
commit b0d2f915bd
4 changed files with 587 additions and 1 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2, ChevronDown } from 'lucide-react';
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2, ChevronDown, Shield } from 'lucide-react';
import { useAuth } from './contexts/AuthContext';
import LoginForm from './components/LoginForm';
import UserMenu from './components/UserMenu';
@@ -210,6 +210,16 @@ export default function App() {
// For adding ticket from within a CVE card
const [addTicketContext, setAddTicketContext] = useState(null); // { cve_id, vendor }
// Archer tickets state
const [archerTickets, setArcherTickets] = useState([]);
const [showAddArcherTicket, setShowAddArcherTicket] = useState(false);
const [showEditArcherTicket, setShowEditArcherTicket] = useState(false);
const [editingArcherTicket, setEditingArcherTicket] = useState(null);
const [archerTicketForm, setArcherTicketForm] = useState({
exc_number: '', archer_url: '', status: 'Draft', cve_id: '', vendor: ''
});
const [addArcherTicketContext, setAddArcherTicketContext] = useState(null); // { cve_id, vendor }
const toggleCVEExpand = (cveId) => {
setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] }));
};
@@ -309,6 +319,19 @@ export default function App() {
}
};
const fetchArcherTickets = async () => {
try {
const response = await fetch(`${API_BASE}/archer-tickets`, {
credentials: 'include'
});
if (!response.ok) throw new Error('Failed to fetch Archer tickets');
const data = await response.json();
setArcherTickets(data);
} catch (err) {
console.error('Error fetching Archer tickets:', err);
}
};
const fetchDocuments = async (cveId, vendor) => {
const key = `${cveId}-${vendor}`;
if (cveDocuments[key]) return;
@@ -745,12 +768,98 @@ export default function App() {
setShowAddTicket(true);
};
// ========== ARCHER TICKET HANDLERS ==========
const handleAddArcherTicket = async (e) => {
e.preventDefault();
try {
const response = await fetch(`${API_BASE}/archer-tickets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(archerTicketForm)
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to create Archer ticket');
}
alert('Archer ticket added successfully!');
setShowAddArcherTicket(false);
setAddArcherTicketContext(null);
setArcherTicketForm({ exc_number: '', archer_url: '', status: 'Draft', cve_id: '', vendor: '' });
fetchArcherTickets();
} catch (err) {
alert(`Error: ${err.message}`);
}
};
const handleEditArcherTicket = (ticket) => {
setEditingArcherTicket(ticket);
setArcherTicketForm({
exc_number: ticket.exc_number,
archer_url: ticket.archer_url || '',
status: ticket.status,
cve_id: ticket.cve_id,
vendor: ticket.vendor
});
setShowEditArcherTicket(true);
};
const handleUpdateArcherTicket = async (e) => {
e.preventDefault();
try {
const response = await fetch(`${API_BASE}/archer-tickets/${editingArcherTicket.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
exc_number: archerTicketForm.exc_number,
archer_url: archerTicketForm.archer_url,
status: archerTicketForm.status
})
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to update Archer ticket');
}
alert('Archer ticket updated!');
setShowEditArcherTicket(false);
setEditingArcherTicket(null);
setArcherTicketForm({ exc_number: '', archer_url: '', status: 'Draft', cve_id: '', vendor: '' });
fetchArcherTickets();
} catch (err) {
alert(`Error: ${err.message}`);
}
};
const handleDeleteArcherTicket = async (ticket) => {
if (!window.confirm(`Delete Archer ticket ${ticket.exc_number}?`)) return;
try {
const response = await fetch(`${API_BASE}/archer-tickets/${ticket.id}`, {
method: 'DELETE',
credentials: 'include'
});
if (!response.ok) throw new Error('Failed to delete Archer ticket');
alert('Archer ticket deleted');
fetchArcherTickets();
} catch (err) {
alert(`Error: ${err.message}`);
}
};
const openAddArcherTicketForCVE = (cve_id, vendor) => {
setAddArcherTicketContext({ cve_id, vendor });
setArcherTicketForm({ exc_number: '', archer_url: '', status: 'Draft', cve_id, vendor });
setShowAddArcherTicket(true);
};
// Fetch CVEs from API when authenticated
useEffect(() => {
if (isAuthenticated) {
fetchCVEs();
fetchVendors();
fetchJiraTickets();
fetchArcherTickets();
fetchKnowledgeBaseArticles();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1337,6 +1446,151 @@ export default function App() {
</div>
)}
{/* Add Archer Ticket Modal */}
{showAddArcherTicket && (
<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">Add Archer Risk Ticket</h2>
<button onClick={() => { setShowAddArcherTicket(false); setAddArcherTicketContext(null); }} className="text-gray-400 hover:text-intel-accent transition-colors">
<XCircle className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleAddArcherTicket} 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={archerTicketForm.exc_number}
onChange={(e) => setArcherTicketForm({...archerTicketForm, 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={archerTicketForm.archer_url}
onChange={(e) => setArcherTicketForm({...archerTicketForm, archer_url: e.target.value})}
className="intel-input w-full"
/>
</div>
<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={archerTicketForm.cve_id}
onChange={(e) => setArcherTicketForm({...archerTicketForm, cve_id: e.target.value.toUpperCase()})}
className="intel-input w-full"
readOnly={!!addArcherTicketContext}
/>
</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={archerTicketForm.vendor}
onChange={(e) => setArcherTicketForm({...archerTicketForm, vendor: e.target.value})}
className="intel-input w-full"
readOnly={!!addArcherTicketContext}
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Status</label>
<select
value={archerTicketForm.status}
onChange={(e) => setArcherTicketForm({...archerTicketForm, 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">
Create Ticket
</button>
<button type="button" onClick={() => { setShowAddArcherTicket(false); setAddArcherTicketContext(null); }} 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>
)}
{/* Edit Archer Ticket Modal */}
{showEditArcherTicket && editingArcherTicket && (
<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">Edit Archer Risk Ticket</h2>
<button onClick={() => { setShowEditArcherTicket(false); setEditingArcherTicket(null); }} className="text-gray-400 hover:text-intel-accent transition-colors">
<XCircle className="w-6 h-6" />
</button>
</div>
<div className="p-3 bg-intel-medium rounded text-sm text-white mb-4 font-mono">
{editingArcherTicket.cve_id} / {editingArcherTicket.vendor}
</div>
<form onSubmit={handleUpdateArcherTicket} 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
value={archerTicketForm.exc_number}
onChange={(e) => setArcherTicketForm({...archerTicketForm, 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"
value={archerTicketForm.archer_url}
onChange={(e) => setArcherTicketForm({...archerTicketForm, archer_url: e.target.value})}
className="intel-input w-full"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Status</label>
<select
value={archerTicketForm.status}
onChange={(e) => setArcherTicketForm({...archerTicketForm, 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">
Save Changes
</button>
<button type="button" onClick={() => { setShowEditArcherTicket(false); setEditingArcherTicket(null); }} 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>
)}
{/* Three Column Layout */}
<div className="grid grid-cols-12 gap-6">
{/* LEFT PANEL - Wiki/Knowledge Base */}
@@ -1993,6 +2247,70 @@ export default function App() {
)}
</div>
</div>
{/* Archer Risk Acceptance Tickets */}
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '3px solid #8B5CF6'}} className="rounded-lg">
<div className="flex justify-between items-center mb-4">
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#8B5CF6', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(139, 92, 246, 0.4)' }}>
<Shield className="w-5 h-5" />
Archer Risk Tickets
</h2>
{canWrite() && (
<button
onClick={() => { setAddArcherTicketContext(null); setArcherTicketForm({ exc_number: '', archer_url: '', status: 'Draft', cve_id: '', vendor: '' }); setShowAddArcherTicket(true); }}
className="intel-button intel-button-primary flex items-center gap-1 text-xs px-2 py-1"
>
<Plus className="w-3 h-3" />
</button>
)}
</div>
<div className="text-center mb-3">
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#8B5CF6', textShadow: '0 0 16px rgba(139, 92, 246, 0.4)' }}>
{archerTickets.filter(t => t.status !== 'Accepted').length}
</div>
<div className="text-xs text-gray-400 uppercase tracking-wider">Active</div>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{archerTickets.filter(t => t.status !== 'Accepted').slice(0, 10).map(ticket => (
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)', border: '1px solid rgba(139, 92, 246, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)' }}>
<div className="flex items-start justify-between gap-2 mb-1">
<a
href={ticket.archer_url || '#'}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-xs font-semibold text-intel-accent hover:text-purple-400 transition-colors"
>
{ticket.exc_number}
</a>
{canWrite() && (
<div className="flex gap-1">
<button onClick={() => handleEditArcherTicket(ticket)} className="text-gray-400 hover:text-purple-400 transition-colors">
<Edit2 className="w-3 h-3" />
</button>
<button onClick={() => handleDeleteArcherTicket(ticket)} className="text-gray-400 hover:text-intel-danger transition-colors">
<Trash2 className="w-3 h-3" />
</button>
</div>
)}
</div>
<div className="text-xs text-white font-mono mb-1">{ticket.cve_id}</div>
<div className="text-xs text-gray-400">{ticket.vendor}</div>
<div className="mt-2">
<span style={{ ...STYLES.badgeHigh, fontSize: '0.65rem', padding: '0.25rem 0.5rem', background: 'rgba(139, 92, 246, 0.2)', borderColor: '#8B5CF6' }}>
<span style={{...STYLES.glowDot('#8B5CF6'), width: '6px', height: '6px'}}></span>
{ticket.status}
</span>
</div>
</div>
))}
{archerTickets.filter(t => t.status !== 'Accepted').length === 0 && (
<div className="text-center py-8">
<CheckCircle className="w-8 h-8 text-intel-success mx-auto mb-2" />
<p className="text-sm text-gray-400 italic font-mono">No active Archer tickets</p>
</div>
)}
</div>
</div>
</div>
{/* End Right Panel */}