feat(reporting): add empty-cell option to column filters

Columns that contain any blank values now show a '— empty —' entry at the
top of the filter dropdown. Selecting only that entry shows findings with
nothing in that column (e.g. workflow with no FP# ticket assigned).

Uses an EMPTY_SENTINEL constant ('__EMPTY__') in the filter Set so blank
cells are handled distinctly from non-blank values. Works for both
single-value and multi-value (CVEs) columns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 13:27:16 -06:00
parent 7314dc16cb
commit ae04bc981e

View File

@@ -7,6 +7,9 @@ import { useAuth } from '../../contexts/AuthContext';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const STORAGE_KEY = 'steam_findings_columns_v2';
// Sentinel used in filter Sets to represent cells with no value (blank / —)
const EMPTY_SENTINEL = '__EMPTY__';
// ---------------------------------------------------------------------------
// Column definitions — source of truth for labels, sort behaviour, rendering
// ---------------------------------------------------------------------------
@@ -793,22 +796,28 @@ function FilterDropdown({ anchorEl, colKey, findings, activeFilter, onFilterChan
// Unique values from the full (unfiltered) findings list.
// Multi-value columns (e.g. cves) expand their array so each item is a separate option.
// EMPTY_SENTINEL is prepended when any finding has a blank/null cell.
const allValues = useMemo(() => {
const def = COLUMN_DEFS[colKey];
const vals = new Set();
let hasEmpty = false;
findings.forEach((f) => {
if (def?.multiValue) {
(f[colKey] || []).forEach((v) => { if (String(v).trim()) vals.add(String(v).trim()); });
const arr = f[colKey] || [];
if (arr.length === 0) { hasEmpty = true; return; }
arr.forEach((v) => { if (String(v).trim()) vals.add(String(v).trim()); });
} else {
const v = getFilterVal(f, colKey).trim();
if (v) vals.add(v);
if (v) vals.add(v); else hasEmpty = true;
}
});
return [...vals].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
const sorted = [...vals].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
if (hasEmpty) sorted.unshift(EMPTY_SENTINEL);
return sorted;
}, [findings, colKey]);
const displayed = search.trim()
? allValues.filter((v) => v.toLowerCase().includes(search.toLowerCase()))
? allValues.filter((v) => v === EMPTY_SENTINEL || v.toLowerCase().includes(search.toLowerCase()))
: allValues;
const isChecked = (val) => !activeFilter || activeFilter.has(val);
@@ -890,7 +899,10 @@ function FilterDropdown({ anchorEl, colKey, findings, activeFilter, onFilterChan
onChange={() => toggle(val)}
style={{ accentColor: '#0EA5E9', width: '12px', height: '12px', flexShrink: 0, cursor: 'pointer' }}
/>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{val}</span>
{val === EMPTY_SENTINEL
? <span style={{ fontStyle: 'italic', color: '#64748B', whiteSpace: 'nowrap' }}> empty </span>
: <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{val}</span>
}
</label>
))}
</div>
@@ -1188,9 +1200,12 @@ export default function ReportingPage({ filterDate, filterEXC }) {
if (!vals || vals.size === 0) return false;
const def = COLUMN_DEFS[key];
if (def?.multiValue) {
return (f[key] || []).some((v) => vals.has(String(v).trim()));
const arr = f[key] || [];
if (arr.length === 0) return vals.has(EMPTY_SENTINEL);
return arr.some((v) => vals.has(String(v).trim()));
}
return vals.has(getFilterVal(f, key).trim());
const fval = getFilterVal(f, key).trim();
return fval === '' ? vals.has(EMPTY_SENTINEL) : vals.has(fval);
})
);
}