perf: client-side BU filtering for instant scope switching

- Fetch ALL findings once on mount (no teams param to backend)
- Filter client-side via scopedFindings useMemo keyed on adminScope
- Eliminates 5-10s round-trip on every scope change
- Open vs Closed donut now uses scopedFindings.length for open count
- Closed count remains global (no per-BU closed data available)
- Action Coverage donut automatically scoped via visibleFindings chain
- Remove server-side teams param from counts fetch (client handles it)
This commit is contained in:
Jordan Ramos
2026-05-05 12:08:01 -06:00
parent df3173a720
commit bd5fcccacf

View File

@@ -5052,11 +5052,8 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
const fetchCounts = async () => { const fetchCounts = async () => {
setCountsLoading(true); setCountsLoading(true);
try { try {
const teamsParam = getActiveTeamsParam(); // Fetch global counts — open count is overridden by client-side scoped findings
const url = teamsParam const res = await fetch(`${API_BASE}/ivanti/findings/counts`, { credentials: 'include' });
? `${API_BASE}/ivanti/findings/counts?teams=${encodeURIComponent(teamsParam)}`
: `${API_BASE}/ivanti/findings/counts`;
const res = await fetch(url, { credentials: 'include' });
const data = await res.json(); const data = await res.json();
if (res.ok) setStatusCounts({ open: data.open ?? 0, closed: data.closed ?? 0 }); if (res.ok) setStatusCounts({ open: data.open ?? 0, closed: data.closed ?? 0 });
} catch (e) { } catch (e) {
@@ -5142,11 +5139,8 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
const fetchFindings = async () => { const fetchFindings = async () => {
setLoading(true); setLoading(true);
try { try {
const teamsParam = getActiveTeamsParam(); // Always fetch ALL findings — filtering happens client-side for instant scope switching
const url = teamsParam const res = await fetch(`${API_BASE}/ivanti/findings`, { credentials: 'include' });
? `${API_BASE}/ivanti/findings?teams=${encodeURIComponent(teamsParam)}`
: `${API_BASE}/ivanti/findings`;
const res = await fetch(url, { credentials: 'include' });
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
applyState(data); applyState(data);
@@ -5188,12 +5182,6 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
fetchCardStatus(); fetchCardStatus();
}, []); // eslint-disable-line }, []); // eslint-disable-line
// Re-fetch findings and counts when admin scope toggle changes
useEffect(() => {
fetchFindings();
fetchCounts();
}, [adminScope]); // eslint-disable-line
// Set/clear a single column filter // Set/clear a single column filter
const setColFilter = useCallback((colKey, vals) => { const setColFilter = useCallback((colKey, vals) => {
setColumnFilters((prev) => { setColumnFilters((prev) => {
@@ -5206,11 +5194,22 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
}); });
}, []); }, []);
// Scope findings by selected BU teams (client-side filtering for instant switching)
const scopedFindings = useMemo(() => {
const teamsParam = getActiveTeamsParam();
if (!teamsParam) return findings; // no filter = show all
const teams = teamsParam.split(',').map(t => t.trim().toUpperCase()).filter(Boolean);
if (teams.length === 0) return findings;
return findings.filter(f =>
teams.some(t => (f.buOwnership || '').toUpperCase().includes(t))
);
}, [findings, adminScope]); // eslint-disable-line react-hooks/exhaustive-deps
// Visible findings — hidden rows removed before any other filtering // Visible findings — hidden rows removed before any other filtering
const visibleFindings = useMemo(() => { const visibleFindings = useMemo(() => {
if (hiddenRowIds.size === 0) return findings; if (hiddenRowIds.size === 0) return scopedFindings;
return findings.filter(f => !hiddenRowIds.has(String(f.id))); return scopedFindings.filter(f => !hiddenRowIds.has(String(f.id)));
}, [findings, hiddenRowIds]); }, [scopedFindings, hiddenRowIds]);
// Apply all active filters to produce the visible row set // Apply all active filters to produce the visible row set
const filtered = useMemo(() => { const filtered = useMemo(() => {
@@ -5663,13 +5662,13 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
<div role="tabpanel"> <div role="tabpanel">
{metricsTab === 'ivanti' && ( {metricsTab === 'ivanti' && (
<div style={{ display: 'flex', gap: '3rem', flexWrap: 'wrap', alignItems: 'flex-start' }}> <div style={{ display: 'flex', gap: '3rem', flexWrap: 'wrap', alignItems: 'flex-start' }}>
{/* Open vs Closed donut */} {/* Open vs Closed donut — open count from scoped findings, closed is global */}
<div style={{ flex: '0 0 auto' }}> <div style={{ flex: '0 0 auto' }}>
<div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}> <div style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.75rem' }}>
Open vs Closed Open vs Closed
</div> </div>
<StatusDonut <StatusDonut
open={statusCounts.open} open={scopedFindings.length}
closed={statusCounts.closed} closed={statusCounts.closed}
loading={countsLoading} loading={countsLoading}
/> />