feat: add Ivanti Queue redirect for completed items

This commit is contained in:
jramos
2026-04-09 16:01:36 -06:00
parent 1963faf9b8
commit 0a7a7c2827
7 changed files with 881 additions and 3 deletions

View File

@@ -1,10 +1,11 @@
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import ReactDOM from 'react-dom';
import { RefreshCw, Loader, AlertCircle, PieChart, ChevronUp, ChevronDown, ChevronsUpDown, Settings2, GripVertical, Eye, EyeOff, Filter, Download, RotateCcw, Trash2, X, ListTodo, Upload, FileText, Check, AlertTriangle } from 'lucide-react';
import { RefreshCw, Loader, AlertCircle, PieChart, ChevronUp, ChevronDown, ChevronsUpDown, Settings2, GripVertical, Eye, EyeOff, Filter, Download, RotateCcw, Trash2, X, ListTodo, Upload, FileText, Check, AlertTriangle, CornerUpRight } from 'lucide-react';
import * as XLSX from 'xlsx';
import { useAuth } from '../../contexts/AuthContext';
import IvantiCountsChart from './IvantiCountsChart';
import CveTooltip from '../CveTooltip';
import RedirectModal from '../RedirectModal';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const STORAGE_KEY = 'steam_findings_columns_v2';
@@ -1248,11 +1249,13 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd
// ---------------------------------------------------------------------------
// QueuePanel — fixed slide-out panel showing the user's Ivanti queue
// ---------------------------------------------------------------------------
function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, onClearCompleted, onCreateFpWorkflow, canWrite }) {
function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, onClearCompleted, onCreateFpWorkflow, onRedirectComplete, canWrite }) {
const pendingCount = items.filter((i) => i.status === 'pending').length;
const completedCount = items.filter((i) => i.status === 'complete').length;
const [selectedIds, setSelectedIds] = useState(new Set());
const [redirectItem, setRedirectItem] = useState(null);
const [redirectSuccess, setRedirectSuccess] = useState(null);
// Drop any selected IDs that no longer exist in items
useEffect(() => {
@@ -1277,6 +1280,13 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
setSelectedIds(new Set());
};
const handleRedirectSuccess = (newItem) => {
if (onRedirectComplete) onRedirectComplete(newItem);
setRedirectItem(null);
setRedirectSuccess(`Redirected to ${newItem.workflow_type}`);
setTimeout(() => setRedirectSuccess(null), 3000);
};
// CARD items are their own top section; everything else groups by vendor
const grouped = useMemo(() => {
const cardItems = items.filter((i) => i.workflow_type === 'CARD');
@@ -1508,6 +1518,19 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
{item.workflow_type}
</span>
{/* Redirect button — completed items only */}
{canWrite && done && (
<button
onClick={() => setRedirectItem(item)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#334155', padding: '1px', lineHeight: 1, flexShrink: 0 }}
onMouseEnter={(e) => e.currentTarget.style.color = '#0EA5E9'}
onMouseLeave={(e) => e.currentTarget.style.color = '#334155'}
title="Redirect to another workflow"
>
<CornerUpRight style={{ width: '13px', height: '13px' }} />
</button>
)}
{/* Delete button */}
<button
onClick={() => onDelete(item.id)}
@@ -1594,6 +1617,35 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
</button>
</div>
</div>
{/* Redirect success notification */}
{redirectSuccess && (
<div style={{
position: 'fixed', top: '1rem', right: '440px',
zIndex: 10001,
display: 'flex', alignItems: 'center', gap: '0.5rem',
padding: '0.5rem 1rem',
background: 'rgba(16, 185, 129, 0.15)',
border: '1px solid rgba(16, 185, 129, 0.4)',
borderRadius: '0.375rem',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)',
fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.75rem', fontWeight: '600',
color: '#10B981',
}}>
<Check style={{ width: '14px', height: '14px' }} />
{redirectSuccess}
</div>
)}
{/* Redirect modal */}
{redirectItem && (
<RedirectModal
item={redirectItem}
onClose={() => setRedirectItem(null)}
onRedirect={handleRedirectSuccess}
/>
)}
</>
);
}
@@ -3268,6 +3320,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
onDeleteMany={deleteQueueItems}
onClearCompleted={clearCompleted}
onCreateFpWorkflow={handleCreateFpWorkflow}
onRedirectComplete={(newItem) => {
setQueueItems((prev) => [...prev, newItem].sort((a, b) =>
(a.vendor || '').localeCompare(b.vendor || '') || a.id - b.id
));
}}
canWrite={canWrite}
/>
<FpWorkflowModal