added stop start files and testing multivendor support
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2 } from 'lucide-react';
|
||||
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus } from 'lucide-react';
|
||||
|
||||
const API_BASE = 'http://192.168.2.117:3001/api';
|
||||
const API_BASE = 'http://71.85.90.6:3001/api';
|
||||
|
||||
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function App() {
|
||||
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
|
||||
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
|
||||
const [selectedCVE, setSelectedCVE] = useState(null);
|
||||
const [selectedVendorView, setSelectedVendorView] = useState(null);
|
||||
const [selectedDocuments, setSelectedDocuments] = useState([]);
|
||||
const [cves, setCves] = useState([]);
|
||||
const [vendors, setVendors] = useState(['All Vendors']);
|
||||
@@ -73,14 +74,15 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDocuments = async (cveId) => {
|
||||
if (cveDocuments[cveId]) return;
|
||||
const fetchDocuments = async (cveId, vendor) => {
|
||||
const key = `${cveId}-${vendor}`;
|
||||
if (cveDocuments[key]) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/cves/${cveId}/documents`);
|
||||
const response = await fetch(`${API_BASE}/cves/${cveId}/documents?vendor=${vendor}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch documents');
|
||||
const data = await response.json();
|
||||
setCveDocuments(prev => ({ ...prev, [cveId]: data }));
|
||||
setCveDocuments(prev => ({ ...prev, [key]: data }));
|
||||
} catch (err) {
|
||||
console.error('Error fetching documents:', err);
|
||||
}
|
||||
@@ -100,12 +102,15 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDocuments = async (cveId) => {
|
||||
if (selectedCVE === cveId) {
|
||||
const handleViewDocuments = async (cveId, vendor) => {
|
||||
const key = `${cveId}-${vendor}`;
|
||||
if (selectedCVE === cveId && selectedVendorView === vendor) {
|
||||
setSelectedCVE(null);
|
||||
setSelectedVendorView(null);
|
||||
} else {
|
||||
setSelectedCVE(cveId);
|
||||
await fetchDocuments(cveId);
|
||||
setSelectedVendorView(vendor);
|
||||
await fetchDocuments(cveId, vendor);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,9 +145,12 @@ export default function App() {
|
||||
body: JSON.stringify(newCVE)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to add CVE');
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Failed to add CVE');
|
||||
}
|
||||
|
||||
alert(`CVE ${newCVE.cve_id} added successfully!`);
|
||||
alert(`CVE ${newCVE.cve_id} added successfully for vendor: ${newCVE.vendor}!`);
|
||||
setShowAddCVE(false);
|
||||
setNewCVE({
|
||||
cve_id: '',
|
||||
@@ -192,8 +200,9 @@ export default function App() {
|
||||
if (!response.ok) throw new Error('Failed to upload document');
|
||||
|
||||
alert(`Document uploaded successfully!`);
|
||||
delete cveDocuments[cveId];
|
||||
await fetchDocuments(cveId);
|
||||
const key = `${cveId}-${vendor}`;
|
||||
delete cveDocuments[key];
|
||||
await fetchDocuments(cveId, vendor);
|
||||
fetchCVEs();
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
@@ -205,7 +214,7 @@ export default function App() {
|
||||
fileInput.click();
|
||||
};
|
||||
|
||||
const handleDeleteDocument = async (docId, cveId) => {
|
||||
const handleDeleteDocument = async (docId, cveId, vendor) => {
|
||||
if (!window.confirm('Are you sure you want to delete this document?')) {
|
||||
return;
|
||||
}
|
||||
@@ -218,20 +227,30 @@ export default function App() {
|
||||
if (!response.ok) throw new Error('Failed to delete document');
|
||||
|
||||
alert('Document deleted successfully!');
|
||||
delete cveDocuments[cveId];
|
||||
await fetchDocuments(cveId);
|
||||
const key = `${cveId}-${vendor}`;
|
||||
delete cveDocuments[key];
|
||||
await fetchDocuments(cveId, vendor);
|
||||
fetchCVEs();
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCVEs = cves;
|
||||
// Group CVEs by CVE ID
|
||||
const groupedCVEs = cves.reduce((acc, cve) => {
|
||||
if (!acc[cve.cve_id]) {
|
||||
acc[cve.cve_id] = [];
|
||||
}
|
||||
acc[cve.cve_id].push(cve);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const filteredGroupedCVEs = groupedCVEs;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header with Charter Branding */}
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">CVE Dashboard</h1>
|
||||
@@ -241,8 +260,8 @@ export default function App() {
|
||||
onClick={() => setShowAddCVE(true)}
|
||||
className="px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors flex items-center gap-2 shadow-md"
|
||||
>
|
||||
<span className="text-xl">+</span>
|
||||
Add New CVE
|
||||
<Plus className="w-5 h-5" />
|
||||
Add CVE/Vendor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -252,7 +271,7 @@ export default function App() {
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Add New CVE</h2>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Add CVE Entry</h2>
|
||||
<button
|
||||
onClick={() => setShowAddCVE(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
@@ -261,6 +280,13 @@ export default function App() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Tip:</strong> You can add the same CVE-ID multiple times with different vendors.
|
||||
Each vendor will have its own documents folder.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAddCVE} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@@ -274,6 +300,7 @@ export default function App() {
|
||||
onChange={(e) => setNewCVE({...newCVE, cve_id: e.target.value.toUpperCase()})}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Can be the same as existing CVE if adding another vendor</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -288,6 +315,7 @@ export default function App() {
|
||||
onChange={(e) => setNewCVE({...newCVE, vendor: e.target.value})}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Must be unique for this CVE-ID</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -338,7 +366,7 @@ export default function App() {
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors font-medium shadow-md"
|
||||
>
|
||||
Add CVE
|
||||
Add CVE Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -354,7 +382,7 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Check with Charter Blue */}
|
||||
{/* Quick Check */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-blue-100 rounded-lg shadow-md p-6 mb-6 border-2 border-[#0476D9]">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Quick CVE Status Check</h2>
|
||||
<div className="flex gap-3">
|
||||
@@ -388,23 +416,29 @@ export default function App() {
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-green-900">✓ CVE Addressed</p>
|
||||
<div className="mt-2 space-y-1 text-sm text-gray-700">
|
||||
<p><strong>Vendor:</strong> {quickCheckResult.cve.vendor}</p>
|
||||
<p><strong>Severity:</strong> {quickCheckResult.cve.severity}</p>
|
||||
<p><strong>Status:</strong> {quickCheckResult.cve.status}</p>
|
||||
<p><strong>Documents:</strong> {quickCheckResult.cve.total_documents} attached</p>
|
||||
<div className="mt-2 flex gap-3">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.advisory ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{quickCheckResult.compliance.advisory ? '✓' : '✗'} Advisory
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.email ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{quickCheckResult.compliance.email ? '✓' : '○'} Email
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.screenshot ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{quickCheckResult.compliance.screenshot ? '✓' : '○'} Screenshot
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-medium text-green-900">✓ CVE Addressed ({quickCheckResult.vendors.length} vendor{quickCheckResult.vendors.length > 1 ? 's' : ''})</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
{quickCheckResult.vendors.map((vendorInfo, idx) => (
|
||||
<div key={idx} className="p-3 bg-white rounded border border-green-200">
|
||||
<p className="font-semibold text-gray-900 mb-2">{vendorInfo.vendor}</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-gray-700 mb-2">
|
||||
<p><strong>Severity:</strong> {vendorInfo.severity}</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -475,7 +509,8 @@ export default function App() {
|
||||
{/* Results Summary */}
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<p className="text-gray-600">
|
||||
Found {filteredCVEs.length} CVE{filteredCVEs.length !== 1 ? 's' : ''}
|
||||
Found {Object.keys(filteredGroupedCVEs).length} CVE{Object.keys(filteredGroupedCVEs).length !== 1 ? 's' : ''}
|
||||
({cves.length} vendor entr{cves.length !== 1 ? 'ies' : 'y'})
|
||||
</p>
|
||||
{selectedDocuments.length > 0 && (
|
||||
<button
|
||||
@@ -488,7 +523,7 @@ export default function App() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* CVE List */}
|
||||
{/* CVE List - Grouped by CVE ID */}
|
||||
{loading ? (
|
||||
<div className="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<Loader className="w-12 h-12 text-[#0476D9] mx-auto mb-4 animate-spin" />
|
||||
@@ -508,114 +543,131 @@ export default function App() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredCVEs.map(cve => {
|
||||
const documents = cveDocuments[cve.cve_id] || [];
|
||||
|
||||
return (
|
||||
<div key={cve.cve_id} className="bg-white rounded-lg shadow-md border border-gray-200">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-xl font-semibold text-gray-900">{cve.cve_id}</h3>
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-gray-700 mb-2">{cve.description}</p>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>Vendor: <span className="font-medium text-gray-700">{cve.vendor}</span></span>
|
||||
<span>Published: {cve.published_date}</span>
|
||||
<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>
|
||||
<button
|
||||
onClick={() => handleViewDocuments(cve.cve_id)}
|
||||
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" />
|
||||
{selectedCVE === cve.cve_id ? 'Hide' : 'View'} Documents
|
||||
</button>
|
||||
{Object.entries(filteredGroupedCVEs).map(([cveId, vendorEntries]) => (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents Section */}
|
||||
{selectedCVE === cve.cve_id && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
Attached Documents ({documents.length})
|
||||
</h4>
|
||||
{documents.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{documents.map(doc => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<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={`http://192.168.2.117:3001/${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>
|
||||
<button
|
||||
onClick={() => handleDeleteDocument(doc.id, cve.cve_id)}
|
||||
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>
|
||||
{/* 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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 italic">No documents attached yet</p>
|
||||
)}
|
||||
<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 New Document'}
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href={`http://71.85.90.6:3001/${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>
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
)}
|
||||
|
||||
{filteredCVEs.length === 0 && !loading && (
|
||||
{Object.keys(filteredGroupedCVEs).length === 0 && !loading && (
|
||||
<div className="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No CVEs Found</h3>
|
||||
|
||||
Reference in New Issue
Block a user