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:
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user