fix: scope FP workflow counts donut by BU
- Rewrite /fp-workflow-counts endpoint to query ivanti_findings table directly with optional teams ILIKE filter (replaces pre-computed JSON blob) - Frontend passes getActiveTeamsParam() to FP counts fetch - FP counts refresh on scope toggle change alongside open/closed counts - Both FP Finding Status and FP Workflow Status donuts now respect BU scope
This commit is contained in:
@@ -1222,18 +1222,56 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
|||||||
*
|
*
|
||||||
* Return FP finding counts and unique workflow ID counts (open + closed),
|
* Return FP finding counts and unique workflow ID counts (open + closed),
|
||||||
* broken down by workflow status.
|
* broken down by workflow status.
|
||||||
|
* Accepts optional `teams` query parameter to scope to specific BUs.
|
||||||
*
|
*
|
||||||
|
* @query {string} [teams] - Comma-separated team names (e.g. 'STEAM,ACCESS-ENG')
|
||||||
* @returns {Object} 200 - { findingCounts: Object, findingTotal: number, idCounts: Object, idTotal: number }
|
* @returns {Object} 200 - { findingCounts: Object, findingTotal: number, idCounts: Object, idTotal: number }
|
||||||
* @returns {Object} 500 - { error: string } on database error
|
* @returns {Object} 500 - { error: string } on database error
|
||||||
*/
|
*/
|
||||||
router.get('/fp-workflow-counts', async (req, res) => {
|
router.get('/fp-workflow-counts', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { rows } = await pool.query('SELECT fp_workflow_counts_json, fp_id_counts_json FROM ivanti_counts_cache WHERE id=1');
|
const teamsParam = req.query.teams;
|
||||||
const row = rows[0];
|
let whereExtra = '';
|
||||||
let findingCounts = {};
|
const params = [];
|
||||||
let idCounts = {};
|
let paramIndex = 1;
|
||||||
try { findingCounts = JSON.parse(row?.fp_workflow_counts_json || '{}'); } catch (_) {}
|
|
||||||
try { idCounts = JSON.parse(row?.fp_id_counts_json || '{}'); } catch (_) {}
|
if (teamsParam) {
|
||||||
|
const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
|
if (teams.length > 0) {
|
||||||
|
const patterns = teams.map(t => `%${t}%`);
|
||||||
|
whereExtra = ` AND bu_ownership ILIKE ANY($${paramIndex++}::text[])`;
|
||||||
|
params.push(patterns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finding counts: number of findings per workflow state
|
||||||
|
const findingResult = await pool.query(
|
||||||
|
`SELECT workflow_state, COUNT(*) as count
|
||||||
|
FROM ivanti_findings
|
||||||
|
WHERE workflow_id IS NOT NULL ${whereExtra}
|
||||||
|
GROUP BY workflow_state`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
const findingCounts = {};
|
||||||
|
findingResult.rows.forEach(r => {
|
||||||
|
const state = r.workflow_state || 'Unknown';
|
||||||
|
findingCounts[state] = parseInt(r.count);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ID counts: number of unique workflow IDs per state
|
||||||
|
const idResult = await pool.query(
|
||||||
|
`SELECT workflow_state, COUNT(DISTINCT workflow_id) as count
|
||||||
|
FROM ivanti_findings
|
||||||
|
WHERE workflow_id IS NOT NULL ${whereExtra}
|
||||||
|
GROUP BY workflow_state`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
const idCounts = {};
|
||||||
|
idResult.rows.forEach(r => {
|
||||||
|
const state = r.workflow_state || 'Unknown';
|
||||||
|
idCounts[state] = parseInt(r.count);
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
findingCounts,
|
findingCounts,
|
||||||
findingTotal: Object.values(findingCounts).reduce((a, b) => a + b, 0),
|
findingTotal: Object.values(findingCounts).reduce((a, b) => a + b, 0),
|
||||||
|
|||||||
@@ -5069,7 +5069,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
|
|
||||||
const fetchFPWorkflowCounts = async () => {
|
const fetchFPWorkflowCounts = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/ivanti/findings/fp-workflow-counts`, { credentials: 'include' });
|
const teamsParam = getActiveTeamsParam();
|
||||||
|
const url = teamsParam
|
||||||
|
? `${API_BASE}/ivanti/findings/fp-workflow-counts?teams=${encodeURIComponent(teamsParam)}`
|
||||||
|
: `${API_BASE}/ivanti/findings/fp-workflow-counts`;
|
||||||
|
const res = await fetch(url, { credentials: 'include' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) setFPCounts({
|
if (res.ok) setFPCounts({
|
||||||
findingCounts: data.findingCounts || {},
|
findingCounts: data.findingCounts || {},
|
||||||
@@ -5197,6 +5201,8 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then(data => { if (data) setStatusCounts({ open: data.open ?? 0, closed: data.closed ?? 0 }); })
|
.then(data => { if (data) setStatusCounts({ open: data.open ?? 0, closed: data.closed ?? 0 }); })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
// Also refresh FP workflow counts for the new scope
|
||||||
|
fetchFPWorkflowCounts();
|
||||||
}, [adminScope]); // eslint-disable-line
|
}, [adminScope]); // eslint-disable-line
|
||||||
|
|
||||||
// Set/clear a single column filter
|
// Set/clear a single column filter
|
||||||
|
|||||||
Reference in New Issue
Block a user