feat: add CVE tooltip on hover in Reporting Page
- Add GET /api/cves/:cveId/tooltip backend endpoint with description truncation - Create CveTooltip portal component with caching, severity badges, and viewport-aware positioning - Integrate tooltip into ReportingPage with 300ms hover delay on CVE badge spans
This commit is contained in:
@@ -4,6 +4,7 @@ import { RefreshCw, Loader, AlertCircle, PieChart, ChevronUp, ChevronDown, Chevr
|
||||
import * as XLSX from 'xlsx';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import IvantiCountsChart from './IvantiCountsChart';
|
||||
import CveTooltip from '../CveTooltip';
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||
const STORAGE_KEY = 'steam_findings_columns_v2';
|
||||
@@ -920,7 +921,7 @@ function FilterDropdown({ anchorEl, colKey, findings, activeFilter, onFilterChan
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render a single table cell by column key
|
||||
// ---------------------------------------------------------------------------
|
||||
function TableCell({ colKey, finding, canWrite }) {
|
||||
function TableCell({ colKey, finding, canWrite, onCveMouseEnter, onCveMouseLeave }) {
|
||||
switch (colKey) {
|
||||
case 'findingId':
|
||||
return (
|
||||
@@ -956,7 +957,12 @@ function TableCell({ colKey, finding, canWrite }) {
|
||||
<td style={{ padding: '0.45rem 0.75rem', minWidth: '160px', maxWidth: '240px' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.2rem' }}>
|
||||
{shown.map((cve) => (
|
||||
<span key={cve} style={{ padding: '0.1rem 0.35rem', borderRadius: '0.2rem', background: 'rgba(139,92,246,0.1)', border: '1px solid rgba(139,92,246,0.3)', color: '#A78BFA', fontFamily: 'monospace', fontSize: '0.65rem', fontWeight: '600', whiteSpace: 'nowrap' }}>
|
||||
<span
|
||||
key={cve}
|
||||
onMouseEnter={onCveMouseEnter ? (e) => onCveMouseEnter(cve, e) : undefined}
|
||||
onMouseLeave={onCveMouseLeave || undefined}
|
||||
style={{ padding: '0.1rem 0.35rem', borderRadius: '0.2rem', background: 'rgba(139,92,246,0.1)', border: '1px solid rgba(139,92,246,0.3)', color: '#A78BFA', fontFamily: 'monospace', fontSize: '0.65rem', fontWeight: '600', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{cve}
|
||||
</span>
|
||||
))}
|
||||
@@ -2312,11 +2318,32 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
const [batchWorkflowType, setBatchWorkflowType] = useState('FP');
|
||||
const [batchVendor, setBatchVendor] = useState('');
|
||||
|
||||
// CVE tooltip state & refs
|
||||
const [tooltipCveId, setTooltipCveId] = useState(null);
|
||||
const [tooltipAnchorRect, setTooltipAnchorRect] = useState(null);
|
||||
const tooltipCacheRef = useRef(new Map());
|
||||
const hoverTimerRef = useRef(null);
|
||||
|
||||
const updateColumns = useCallback((newOrder) => {
|
||||
setColumnOrder(newOrder);
|
||||
saveColumnOrder(newOrder);
|
||||
}, []);
|
||||
|
||||
// CVE tooltip hover handlers
|
||||
const handleCveMouseEnter = useCallback((cveId, e) => {
|
||||
clearTimeout(hoverTimerRef.current);
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
setTooltipCveId(cveId);
|
||||
setTooltipAnchorRect(e.target.getBoundingClientRect());
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
const handleCveMouseLeave = useCallback(() => {
|
||||
clearTimeout(hoverTimerRef.current);
|
||||
setTooltipCveId(null);
|
||||
setTooltipAnchorRect(null);
|
||||
}, []);
|
||||
|
||||
const applyState = (data) => {
|
||||
setTotal(data.total ?? 0);
|
||||
setFindings(data.findings || []);
|
||||
@@ -2358,7 +2385,10 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/ivanti/findings`, { credentials: 'include' });
|
||||
const data = await res.json();
|
||||
if (res.ok) applyState(data);
|
||||
if (res.ok) {
|
||||
applyState(data);
|
||||
tooltipCacheRef.current.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading findings:', e);
|
||||
} finally {
|
||||
@@ -2373,6 +2403,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
applyState(data);
|
||||
tooltipCacheRef.current.clear();
|
||||
fetchCounts(); // refresh counts after sync
|
||||
fetchFPWorkflowCounts(); // refresh FP workflow counts after sync
|
||||
}
|
||||
@@ -3182,7 +3213,7 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
/>
|
||||
</td>
|
||||
{visibleCols.map((col) => (
|
||||
<TableCell key={col.key} colKey={col.key} finding={finding} canWrite={canWrite()} />
|
||||
<TableCell key={col.key} colKey={col.key} finding={finding} canWrite={canWrite()} onCveMouseEnter={handleCveMouseEnter} onCveMouseLeave={handleCveMouseLeave} />
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
@@ -3245,6 +3276,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
selectedItems={fpModalItems}
|
||||
onSuccess={handleFpWorkflowSuccess}
|
||||
/>
|
||||
<CveTooltip
|
||||
cveId={tooltipCveId}
|
||||
anchorRect={tooltipAnchorRect}
|
||||
cache={tooltipCacheRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user