Add FP submissions cleanup: auto-clear approved, dismiss rejected, collapsible section
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user