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:
jramos
2026-04-09 14:42:23 -06:00
parent 690c30aac0
commit 9b36a58959
7 changed files with 716 additions and 4 deletions

View File

@@ -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>
);
}