Add FP submissions cleanup: auto-clear approved, dismiss rejected, collapsible section

This commit is contained in:
Jordan Ramos
2026-05-11 14:29:50 -06:00
parent cda1eaadc9
commit 7245352496
6 changed files with 495 additions and 11 deletions

View File

@@ -1519,7 +1519,7 @@ 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, onRedirectComplete, canWrite, fpSubmissions, onEditSubmission, cardConfigured, cardTeams, onQueueRefresh }) {
function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, onClearCompleted, onCreateFpWorkflow, onRedirectComplete, canWrite, fpSubmissions, onEditSubmission, onDismissSubmission, cardConfigured, cardTeams, onQueueRefresh }) {
const pendingCount = items.filter((i) => i.status === 'pending').length;
const completedCount = items.filter((i) => i.status === 'complete').length;
@@ -1568,6 +1568,37 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
setSelectedIds(new Set());
};
// Submissions section — collapsible state (Task 6)
const [submissionsCollapsed, setSubmissionsCollapsed] = useState(() => localStorage.getItem('steam_submissions_collapsed') === 'true');
const [dismissError, setDismissError] = useState(null);
const toggleSubmissionsCollapsed = () => {
setSubmissionsCollapsed(prev => {
const next = !prev;
try { localStorage.setItem('steam_submissions_collapsed', String(next)); } catch { /* ignore */ }
return next;
});
};
// Dismiss handler (Task 5)
const handleDismiss = async (e, submissionId) => {
e.stopPropagation();
setDismissError(null);
try {
const res = await fetch(`${API_BASE}/ivanti/fp-workflow/submissions/${submissionId}/dismiss`, {
method: 'PATCH',
credentials: 'include',
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setDismissError(data.error || 'Failed to dismiss submission');
return;
}
if (onDismissSubmission) onDismissSubmission(submissionId);
} catch (err) {
setDismissError('Network error — could not dismiss submission');
}
};
const handleRedirectSuccess = (newItem) => {
if (onRedirectComplete) onRedirectComplete(newItem);
setRedirectItem(null);
@@ -2475,24 +2506,40 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
{/* Submissions section */}
{fpSubmissions && fpSubmissions.length > 0 && (
<div style={{ padding: '0 1.25rem 0.75rem' }}>
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '0.3rem 0', marginBottom: '0.375rem',
borderBottom: '1px solid rgba(245,158,11,0.2)',
}}>
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '700', color: '#F59E0B', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
Submissions
</span>
<div
onClick={toggleSubmissionsCollapsed}
style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '0.3rem 0', marginBottom: '0.375rem',
borderBottom: '1px solid rgba(245,158,11,0.2)',
cursor: 'pointer', userSelect: 'none',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
{submissionsCollapsed
? <ChevronUp style={{ width: '12px', height: '12px', color: '#F59E0B' }} />
: <ChevronDown style={{ width: '12px', height: '12px', color: '#F59E0B' }} />
}
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '700', color: '#F59E0B', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
Submissions
</span>
</div>
<span style={{ fontFamily: 'monospace', fontSize: '0.62rem', color: '#334155' }}>
{fpSubmissions.length}
</span>
</div>
{fpSubmissions.map((sub) => {
{dismissError && (
<div style={{ fontFamily: 'monospace', fontSize: '0.62rem', color: '#EF4444', marginBottom: '0.375rem', padding: '0.25rem 0.5rem', background: 'rgba(239,68,68,0.08)', borderRadius: '0.25rem' }}>
{dismissError}
</div>
)}
{!submissionsCollapsed && fpSubmissions.map((sub) => {
const lsBadge = lifecycleStatusBadge(sub.lifecycle_status);
const findingCount = (() => {
try { return JSON.parse(sub.finding_ids_json || '[]').length; } catch { return 0; }
})();
const clickable = canWrite && onEditSubmission;
const showDismiss = sub.lifecycle_status === 'rejected' && !sub.dismissed_at;
return (
<div
key={sub.id}
@@ -2548,6 +2595,32 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
}}>
{sub.lifecycle_status || 'submitted'}
</span>
{showDismiss && (
<button
onClick={(e) => handleDismiss(e, sub.id)}
title="Dismiss rejected submission"
style={{
flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
width: '20px', height: '20px',
background: 'rgba(239,68,68,0.08)',
border: '1px solid rgba(239,68,68,0.2)',
borderRadius: '0.2rem',
cursor: 'pointer',
padding: 0,
transition: 'all 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239,68,68,0.2)';
e.currentTarget.style.borderColor = 'rgba(239,68,68,0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(239,68,68,0.08)';
e.currentTarget.style.borderColor = 'rgba(239,68,68,0.2)';
}}
>
<X style={{ width: '12px', height: '12px', color: '#EF4444' }} />
</button>
)}
</div>
);
})}
@@ -5376,6 +5449,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
});
}, [fpSubmissionsRaw, findings]);
// Filtered submissions for QueuePanel display — hide approved and dismissed
const fpSubmissionsFiltered = useMemo(() => {
return fpSubmissions.filter(s => s.lifecycle_status !== 'approved' && !s.dismissed_at);
}, [fpSubmissions]);
// Queue API helpers
const fetchQueue = useCallback(async () => {
setQueueLoading(true);
@@ -5426,6 +5504,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
fetchFindings();
}, [fetchFpSubmissions, fetchQueue]); // eslint-disable-line
const handleDismissSubmission = useCallback((submissionId) => {
// Optimistically remove the dismissed submission from local state
setFpSubmissions(prev => prev.filter(s => s.id !== submissionId));
}, []);
const addToQueue = useCallback(async () => {
if (!addPopover) return;
const { finding } = addPopover;
@@ -6413,8 +6496,9 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
));
}}
canWrite={canWrite}
fpSubmissions={fpSubmissions}
fpSubmissions={fpSubmissionsFiltered}
onEditSubmission={handleEditSubmission}
onDismissSubmission={handleDismissSubmission}
cardConfigured={cardConfigured}
cardTeams={cardTeams}
onQueueRefresh={fetchQueue}