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