fix: revert clickable workflow badges, fix migration default, auto-sync submission lifecycle status from Ivanti findings
- Revert workflow badge to static (non-clickable) — queue panel is the entry point - Fix migration: use DEFAULT NULL for updated_at (SQLite disallows CURRENT_TIMESTAMP in ALTER TABLE) - Add useMemo enrichment to cross-reference submission lifecycle_status with actual Ivanti workflow state from findings data
This commit is contained in:
@@ -1061,42 +1061,22 @@ function TableCell({ colKey, finding, canWrite, onCveMouseEnter, onCveMouseLeave
|
|||||||
const wf = finding.workflow;
|
const wf = finding.workflow;
|
||||||
if (!wf || !wf.id) return <td style={{ padding: '0.45rem 0.75rem', color: '#334155' }}>—</td>;
|
if (!wf || !wf.id) return <td style={{ padding: '0.45rem 0.75rem', color: '#334155' }}>—</td>;
|
||||||
const ws = workflowStyle(wf.state);
|
const ws = workflowStyle(wf.state);
|
||||||
const isEditable = ['reworked', 'rejected', 'expired'].includes((wf.state || '').toLowerCase());
|
|
||||||
|
|
||||||
const handleBadgeClick = isEditable ? () => {
|
|
||||||
const numericId = parseInt(String(wf.id).replace(/\D/g, ''), 10);
|
|
||||||
const sub = fpSubmissions.find(s => s.ivanti_workflow_batch_id === numericId);
|
|
||||||
if (sub) onEditSubmission(sub);
|
|
||||||
} : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap' }}>
|
<td style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap' }}>
|
||||||
<span
|
<span
|
||||||
title={`${wf.id} · ${wf.state || 'Unknown'} · ${wf.type || ''}${isEditable ? ' · Click to edit' : ''}`}
|
title={`${wf.id} · ${wf.state || 'Unknown'} · ${wf.type || ''}`}
|
||||||
onClick={handleBadgeClick}
|
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
|
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
|
||||||
padding: '0.15rem 0.45rem', borderRadius: '0.25rem',
|
padding: '0.15rem 0.45rem', borderRadius: '0.25rem',
|
||||||
background: ws.bg, border: `1px solid ${ws.border}`,
|
background: ws.bg, border: `1px solid ${ws.border}`,
|
||||||
fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600',
|
fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600',
|
||||||
color: ws.text,
|
color: ws.text, cursor: 'default',
|
||||||
cursor: isEditable ? 'pointer' : 'default',
|
|
||||||
transition: 'all 0.15s',
|
|
||||||
}}
|
}}
|
||||||
onMouseEnter={isEditable ? (e) => {
|
|
||||||
e.currentTarget.style.borderColor = ws.text;
|
|
||||||
e.currentTarget.style.background = ws.bg.replace('0.12', '0.2');
|
|
||||||
} : undefined}
|
|
||||||
onMouseLeave={isEditable ? (e) => {
|
|
||||||
e.currentTarget.style.borderColor = ws.border;
|
|
||||||
e.currentTarget.style.background = ws.bg;
|
|
||||||
} : undefined}
|
|
||||||
>
|
>
|
||||||
{wf.id}
|
{wf.id}
|
||||||
<span style={{ fontSize: '0.58rem', opacity: 0.8, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
|
<span style={{ fontSize: '0.58rem', opacity: 0.8, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
|
||||||
{wf.state}
|
{wf.state}
|
||||||
</span>
|
</span>
|
||||||
{isEditable && <Edit3 style={{ width: '10px', height: '10px', opacity: 0.7 }} />}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
@@ -3159,9 +3139,33 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
|||||||
const [fpModalItems, setFpModalItems] = useState([]);
|
const [fpModalItems, setFpModalItems] = useState([]);
|
||||||
|
|
||||||
// FP Submission editing state
|
// FP Submission editing state
|
||||||
const [fpSubmissions, setFpSubmissions] = useState([]);
|
const [fpSubmissionsRaw, setFpSubmissions] = useState([]);
|
||||||
const [editSubmission, setEditSubmission] = useState(null);
|
const [editSubmission, setEditSubmission] = useState(null);
|
||||||
|
|
||||||
|
// Enrich submissions with actual Ivanti workflow state from findings data
|
||||||
|
const fpSubmissions = useMemo(() => {
|
||||||
|
if (fpSubmissionsRaw.length === 0 || findings.length === 0) return fpSubmissionsRaw;
|
||||||
|
const stateMap = {
|
||||||
|
'reworked': 'rework', 'rejected': 'rejected', 'expired': 'rejected',
|
||||||
|
'approved': 'approved', 'requested': 'submitted', 'actionable': 'submitted',
|
||||||
|
};
|
||||||
|
return fpSubmissionsRaw.map(sub => {
|
||||||
|
let findingIds;
|
||||||
|
try { findingIds = JSON.parse(sub.finding_ids_json || '[]'); } catch { return sub; }
|
||||||
|
if (findingIds.length === 0) return sub;
|
||||||
|
const matchedFinding = findings.find(f =>
|
||||||
|
f.workflow && findingIds.includes(String(f.id))
|
||||||
|
);
|
||||||
|
if (!matchedFinding || !matchedFinding.workflow) return sub;
|
||||||
|
const ivantiState = (matchedFinding.workflow.state || '').toLowerCase();
|
||||||
|
const mappedStatus = stateMap[ivantiState];
|
||||||
|
if (mappedStatus && mappedStatus !== sub.lifecycle_status) {
|
||||||
|
return { ...sub, lifecycle_status: mappedStatus };
|
||||||
|
}
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
}, [fpSubmissionsRaw, findings]);
|
||||||
|
|
||||||
// Queue API helpers
|
// Queue API helpers
|
||||||
const fetchQueue = useCallback(async () => {
|
const fetchQueue = useCallback(async () => {
|
||||||
setQueueLoading(true);
|
setQueueLoading(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user