From bd5fcccacf7ef25971195d174c30c62d74117cfc Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Tue, 5 May 2026 12:08:01 -0600 Subject: [PATCH] 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) --- .../src/components/pages/ReportingPage.js | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js index 74bb8b6..60d0473 100644 --- a/frontend/src/components/pages/ReportingPage.js +++ b/frontend/src/components/pages/ReportingPage.js @@ -5052,11 +5052,8 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) { const fetchCounts = async () => { setCountsLoading(true); try { - const teamsParam = getActiveTeamsParam(); - const url = teamsParam - ? `${API_BASE}/ivanti/findings/counts?teams=${encodeURIComponent(teamsParam)}` - : `${API_BASE}/ivanti/findings/counts`; - const res = await fetch(url, { credentials: 'include' }); + // Fetch global counts — open count is overridden by client-side scoped findings + const res = await fetch(`${API_BASE}/ivanti/findings/counts`, { credentials: 'include' }); const data = await res.json(); if (res.ok) setStatusCounts({ open: data.open ?? 0, closed: data.closed ?? 0 }); } catch (e) { @@ -5142,11 +5139,8 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) { const fetchFindings = async () => { setLoading(true); try { - const teamsParam = getActiveTeamsParam(); - const url = teamsParam - ? `${API_BASE}/ivanti/findings?teams=${encodeURIComponent(teamsParam)}` - : `${API_BASE}/ivanti/findings`; - const res = await fetch(url, { credentials: 'include' }); + // Always fetch ALL findings — filtering happens client-side for instant scope switching + const res = await fetch(`${API_BASE}/ivanti/findings`, { credentials: 'include' }); const data = await res.json(); if (res.ok) { applyState(data); @@ -5188,12 +5182,6 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) { fetchCardStatus(); }, []); // 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 const setColFilter = useCallback((colKey, vals) => { 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 const visibleFindings = useMemo(() => { - if (hiddenRowIds.size === 0) return findings; - return findings.filter(f => !hiddenRowIds.has(String(f.id))); - }, [findings, hiddenRowIds]); + if (hiddenRowIds.size === 0) return scopedFindings; + return scopedFindings.filter(f => !hiddenRowIds.has(String(f.id))); + }, [scopedFindings, hiddenRowIds]); // Apply all active filters to produce the visible row set const filtered = useMemo(() => { @@ -5663,13 +5662,13 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
{metricsTab === 'ivanti' && (
- {/* Open vs Closed donut */} + {/* Open vs Closed donut — open count from scoped findings, closed is global */}
Open vs Closed