added visual tweaks and document requirements REMOVED
This commit is contained in:
@@ -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 } from 'lucide-react';
|
||||
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2, ChevronDown } from 'lucide-react';
|
||||
import { useAuth } from './contexts/AuthContext';
|
||||
import LoginForm from './components/LoginForm';
|
||||
import UserMenu from './components/UserMenu';
|
||||
@@ -50,6 +50,11 @@ export default function App() {
|
||||
const [editNvdLoading, setEditNvdLoading] = useState(false);
|
||||
const [editNvdError, setEditNvdError] = useState(null);
|
||||
const [editNvdAutoFilled, setEditNvdAutoFilled] = useState(false);
|
||||
const [expandedCVEs, setExpandedCVEs] = useState({});
|
||||
|
||||
const toggleCVEExpand = (cveId) => {
|
||||
setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] }));
|
||||
};
|
||||
|
||||
const lookupNVD = async (cveId) => {
|
||||
const trimmed = cveId.trim();
|
||||
@@ -840,17 +845,6 @@ export default function App() {
|
||||
<p><strong>Status:</strong> {vendorInfo.status}</p>
|
||||
<p><strong>Documents:</strong> {vendorInfo.total_documents} attached</p>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.advisory ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{vendorInfo.compliance.advisory ? '✓' : '✗'} Advisory
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.email ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{vendorInfo.compliance.email ? '✓' : '○'} Email
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.screenshot ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{vendorInfo.compliance.screenshot ? '✓' : '○'} Screenshot
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -957,160 +951,209 @@ export default function App() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(filteredGroupedCVEs).map(([cveId, vendorEntries]) => (
|
||||
{Object.entries(filteredGroupedCVEs).map(([cveId, vendorEntries]) => {
|
||||
const isCVEExpanded = expandedCVEs[cveId];
|
||||
const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3 };
|
||||
const highestSeverity = vendorEntries.reduce((highest, entry) => {
|
||||
const currentOrder = severityOrder[entry.severity] ?? 4;
|
||||
const highestOrder = severityOrder[highest] ?? 4;
|
||||
return currentOrder < highestOrder ? entry.severity : highest;
|
||||
}, vendorEntries[0].severity);
|
||||
const totalDocCount = vendorEntries.reduce((sum, entry) => sum + (entry.document_count || 0), 0);
|
||||
const overallStatuses = [...new Set(vendorEntries.map(e => e.status))];
|
||||
|
||||
return (
|
||||
<div key={cveId} className="bg-white rounded-lg shadow-md border-2 border-gray-200">
|
||||
<div className="p-6">
|
||||
{/* CVE Header */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">{cveId}</h3>
|
||||
<p className="text-gray-600 mb-3">{vendorEntries[0].description}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span>Published: {vendorEntries[0].published_date}</span>
|
||||
<span>•</span>
|
||||
<span>{vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
||||
{canWrite() && vendorEntries.length >= 2 && (
|
||||
<button
|
||||
onClick={() => handleDeleteEntireCVE(cveId, vendorEntries.length)}
|
||||
className="ml-2 px-3 py-1 text-xs text-red-600 hover:bg-red-50 rounded transition-colors border border-red-300 flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Delete All Vendors
|
||||
</button>
|
||||
{/* Clickable CVE Header */}
|
||||
<div
|
||||
className="p-6 cursor-pointer hover:bg-gray-50 transition-colors duration-200 select-none"
|
||||
onClick={() => toggleCVEExpand(cveId)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<ChevronDown
|
||||
className={`w-5 h-5 text-gray-500 transition-transform duration-200 flex-shrink-0 ${isCVEExpanded ? 'rotate-0' : '-rotate-90'}`}
|
||||
/>
|
||||
<h3 className="text-2xl font-bold text-gray-900">{cveId}</h3>
|
||||
</div>
|
||||
|
||||
{/* Collapsed: truncated description + summary row */}
|
||||
{!isCVEExpanded && (
|
||||
<div className="ml-8">
|
||||
<p className="text-gray-600 text-sm truncate mb-2">{vendorEntries[0].description}</p>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium ${getSeverityColor(highestSeverity)}`}>
|
||||
{highestSeverity}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{vendorEntries.length} vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<FileText className="w-3 h-3" />
|
||||
{totalDocCount} doc{totalDocCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{overallStatuses.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expanded: full description + metadata */}
|
||||
{isCVEExpanded && (
|
||||
<div className="ml-8">
|
||||
<p className="text-gray-600 mb-3">{vendorEntries[0].description}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span>Published: {vendorEntries[0].published_date}</span>
|
||||
<span>•</span>
|
||||
<span>{vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
||||
{canWrite() && vendorEntries.length >= 2 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteEntireCVE(cveId, vendorEntries.length); }}
|
||||
className="ml-2 px-3 py-1 text-xs text-red-600 hover:bg-red-50 rounded transition-colors border border-red-300 flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Delete All Vendors
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vendor Entries */}
|
||||
<div className="space-y-3">
|
||||
{vendorEntries.map((cve) => {
|
||||
const key = `${cve.cve_id}-${cve.vendor}`;
|
||||
const documents = cveDocuments[key] || [];
|
||||
const isExpanded = selectedCVE === cve.cve_id && selectedVendorView === cve.vendor;
|
||||
|
||||
return (
|
||||
<div key={cve.id} className="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className="text-lg font-semibold text-gray-900">{cve.vendor}</h4>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getSeverityColor(cve.severity)}`}>
|
||||
{cve.severity}
|
||||
</span>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${cve.doc_status === 'Complete' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
|
||||
{cve.doc_status === 'Complete' ? '✓ Docs Complete' : '⚠ Incomplete'}
|
||||
</span>
|
||||
{/* Expanded: Vendor Entries */}
|
||||
{isCVEExpanded && (
|
||||
<div className="px-6 pb-6">
|
||||
<div className="space-y-3">
|
||||
{vendorEntries.map((cve) => {
|
||||
const key = `${cve.cve_id}-${cve.vendor}`;
|
||||
const documents = cveDocuments[key] || [];
|
||||
const isDocExpanded = selectedCVE === cve.cve_id && selectedVendorView === cve.vendor;
|
||||
|
||||
return (
|
||||
<div key={cve.id} className="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className="text-lg font-semibold text-gray-900">{cve.vendor}</h4>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getSeverityColor(cve.severity)}`}>
|
||||
{cve.severity}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>Status: <span className="font-medium text-gray-700">{cve.status}</span></span>
|
||||
<span className="flex items-center gap-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
{cve.document_count} document{cve.document_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>Status: <span className="font-medium text-gray-700">{cve.status}</span></span>
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleViewDocuments(cve.cve_id, cve.vendor)}
|
||||
className="px-4 py-2 text-[#0476D9] hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2 border border-[#0476D9]"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{isDocExpanded ? 'Hide' : 'View'} Documents
|
||||
</button>
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={() => handleEditCVE(cve)}
|
||||
className="px-3 py-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors flex items-center gap-1 border border-orange-300"
|
||||
title="Edit CVE entry"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={() => handleDeleteCVEEntry(cve)}
|
||||
className="px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors flex items-center gap-1 border border-red-300"
|
||||
title="Delete this vendor entry"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents Section */}
|
||||
{isDocExpanded && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-300">
|
||||
<h5 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
{cve.document_count} document{cve.document_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleViewDocuments(cve.cve_id, cve.vendor)}
|
||||
className="px-4 py-2 text-[#0476D9] hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2 border border-[#0476D9]"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{isExpanded ? 'Hide' : 'View'} Documents
|
||||
</button>
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={() => handleEditCVE(cve)}
|
||||
className="px-3 py-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors flex items-center gap-1 border border-orange-300"
|
||||
title="Edit CVE entry"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={() => handleDeleteCVEEntry(cve)}
|
||||
className="px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors flex items-center gap-1 border border-red-300"
|
||||
title="Delete this vendor entry"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents Section */}
|
||||
{isExpanded && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-300">
|
||||
<h5 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
Documents for {cve.vendor} ({documents.length})
|
||||
</h5>
|
||||
{documents.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{documents.map(doc => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center justify-between p-3 bg-white rounded-lg hover:bg-gray-50 transition-colors border border-gray-200"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDocuments.includes(doc.id)}
|
||||
onChange={() => toggleDocumentSelection(doc.id)}
|
||||
className="w-4 h-4 text-[#0476D9] rounded focus:ring-2 focus:ring-[#0476D9]"
|
||||
/>
|
||||
<FileText className="w-5 h-5 text-gray-400" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">{doc.name}</p>
|
||||
<p className="text-xs text-gray-500 capitalize">
|
||||
{doc.type} • {doc.file_size}
|
||||
{doc.notes && ` • ${doc.notes}`}
|
||||
</p>
|
||||
Documents for {cve.vendor} ({documents.length})
|
||||
</h5>
|
||||
{documents.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{documents.map(doc => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center justify-between p-3 bg-white rounded-lg hover:bg-gray-50 transition-colors border border-gray-200"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDocuments.includes(doc.id)}
|
||||
onChange={() => toggleDocumentSelection(doc.id)}
|
||||
className="w-4 h-4 text-[#0476D9] rounded focus:ring-2 focus:ring-[#0476D9]"
|
||||
/>
|
||||
<FileText className="w-5 h-5 text-gray-400" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">{doc.name}</p>
|
||||
<p className="text-xs text-gray-500 capitalize">
|
||||
{doc.type} • {doc.file_size}
|
||||
{doc.notes && ` • ${doc.notes}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href={`${API_HOST}/${doc.file_path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-3 py-1 text-sm text-[#0476D9] hover:bg-blue-50 rounded transition-colors border border-[#0476D9]"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
{isAdmin() && (
|
||||
<button
|
||||
onClick={() => handleDeleteDocument(doc.id, cve.cve_id, cve.vendor)}
|
||||
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded transition-colors border border-red-600 flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href={`${API_HOST}/${doc.file_path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-3 py-1 text-sm text-[#0476D9] hover:bg-blue-50 rounded transition-colors border border-[#0476D9]"
|
||||
>
|
||||
View
|
||||
</a>
|
||||
{isAdmin() && (
|
||||
<button
|
||||
onClick={() => handleDeleteDocument(doc.id, cve.cve_id, cve.vendor)}
|
||||
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded transition-colors border border-red-600 flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 italic">No documents attached yet</p>
|
||||
)}
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={() => handleFileUpload(cve.cve_id, cve.vendor)}
|
||||
disabled={uploadingFile}
|
||||
className="mt-3 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50 border border-gray-300"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
{uploadingFile ? 'Uploading...' : 'Upload Document'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 italic">No documents attached yet</p>
|
||||
)}
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={() => handleFileUpload(cve.cve_id, cve.vendor)}
|
||||
disabled={uploadingFile}
|
||||
className="mt-3 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50 border border-gray-300"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
{uploadingFile ? 'Uploading...' : 'Upload Document'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user