2026-04-09 16:01:36 -06:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { CornerUpRight, X, Loader, AlertCircle } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
|
|
|
|
|
|
|
|
|
const WORKFLOW_OPTIONS = [
|
2026-04-14 15:33:19 -06:00
|
|
|
{ key: 'FP', label: 'FP', col: '#F59E0B', rgb: '245,158,11' },
|
|
|
|
|
{ key: 'Archer', label: 'Archer', col: '#0EA5E9', rgb: '14,165,233' },
|
|
|
|
|
{ key: 'CARD', label: 'CARD', col: '#10B981', rgb: '16,185,129' },
|
|
|
|
|
{ key: 'GRANITE', label: 'GRANITE', col: '#A1887F', rgb: '161,136,127' },
|
2026-04-09 16:01:36 -06:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export default function RedirectModal({ item, onClose, onRedirect }) {
|
|
|
|
|
const [workflowType, setWorkflowType] = useState('FP');
|
|
|
|
|
const [vendor, setVendor] = useState('');
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
|
|
|
|
|
const needsVendor = workflowType === 'FP' || workflowType === 'Archer';
|
|
|
|
|
const canSubmit = !loading && (!needsVendor || vendor.trim().length > 0);
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
if (!canSubmit) return;
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const body = { workflow_type: workflowType };
|
|
|
|
|
if (needsVendor) body.vendor = vendor.trim();
|
|
|
|
|
|
|
|
|
|
const res = await fetch(`${API_BASE}/ivanti/todo-queue/${item.id}/redirect`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
setError(data.error || 'Redirect failed.');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onRedirect(data);
|
|
|
|
|
onClose();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err.message || 'Network error.');
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
style={{
|
|
|
|
|
position: 'fixed', inset: 0,
|
|
|
|
|
background: 'rgba(10, 14, 39, 0.92)',
|
|
|
|
|
backdropFilter: 'blur(8px)',
|
|
|
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
|
|
|
zIndex: 10000,
|
|
|
|
|
padding: '1rem',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
style={{
|
|
|
|
|
width: '100%', maxWidth: '460px',
|
|
|
|
|
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.98), rgba(51, 65, 85, 0.95))',
|
|
|
|
|
border: '2px solid rgba(14, 165, 233, 0.4)',
|
|
|
|
|
borderRadius: '0.75rem',
|
|
|
|
|
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.7), 0 0 28px rgba(14, 165, 233, 0.12)',
|
|
|
|
|
display: 'flex', flexDirection: 'column',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{/* Top accent line */}
|
|
|
|
|
<div style={{
|
|
|
|
|
height: '2px',
|
|
|
|
|
background: 'linear-gradient(90deg, transparent, #0EA5E9, transparent)',
|
|
|
|
|
boxShadow: '0 0 8px rgba(14, 165, 233, 0.4)',
|
|
|
|
|
}} />
|
|
|
|
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
|
|
|
padding: '1rem 1.25rem',
|
|
|
|
|
borderBottom: '1px solid rgba(14, 165, 233, 0.15)',
|
|
|
|
|
}}>
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
|
|
|
|
|
<CornerUpRight style={{ width: '18px', height: '18px', color: '#0EA5E9' }} />
|
|
|
|
|
<span style={{
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
|
|
|
fontSize: '0.95rem', fontWeight: '700',
|
|
|
|
|
color: '#E2E8F0',
|
|
|
|
|
textTransform: 'uppercase', letterSpacing: '0.08em',
|
|
|
|
|
}}>
|
|
|
|
|
Redirect Queue Item
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#475569', padding: '4px', lineHeight: 1 }}
|
|
|
|
|
>
|
|
|
|
|
<X style={{ width: '18px', height: '18px' }} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Body */}
|
|
|
|
|
<div style={{ padding: '1.25rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
|
|
|
{/* Read-only context */}
|
|
|
|
|
<div style={{
|
|
|
|
|
background: 'rgba(15, 23, 42, 0.6)',
|
|
|
|
|
border: '1px solid rgba(14, 165, 233, 0.15)',
|
|
|
|
|
borderRadius: '0.5rem',
|
|
|
|
|
padding: '0.75rem 1rem',
|
|
|
|
|
display: 'flex', flexDirection: 'column', gap: '0.375rem',
|
|
|
|
|
}}>
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
|
|
|
|
Finding Title
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.78rem', fontWeight: '600',
|
|
|
|
|
color: '#CBD5E1',
|
|
|
|
|
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
|
|
|
}} title={item.finding_title}>
|
|
|
|
|
{item.finding_title || '—'}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '1.5rem', marginTop: '0.25rem' }}>
|
|
|
|
|
<div>
|
|
|
|
|
<span style={{ fontFamily: 'monospace', fontSize: '0.62rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
|
|
|
|
Finding ID
|
|
|
|
|
</span>
|
|
|
|
|
<div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.72rem', color: '#94A3B8', marginTop: '2px' }}>
|
|
|
|
|
{item.finding_id || '—'}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<span style={{ fontFamily: 'monospace', fontSize: '0.62rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
|
|
|
|
Current Type
|
|
|
|
|
</span>
|
|
|
|
|
<div style={{ marginTop: '2px' }}>
|
|
|
|
|
{(() => {
|
|
|
|
|
const opt = WORKFLOW_OPTIONS.find((o) => o.key === item.workflow_type) || WORKFLOW_OPTIONS[0];
|
|
|
|
|
return (
|
|
|
|
|
<span style={{
|
|
|
|
|
display: 'inline-block',
|
|
|
|
|
padding: '0.1rem 0.4rem',
|
|
|
|
|
borderRadius: '0.2rem',
|
|
|
|
|
background: `rgba(${opt.rgb}, 0.12)`,
|
|
|
|
|
border: `1px solid rgba(${opt.rgb}, 0.3)`,
|
|
|
|
|
color: opt.col,
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.65rem', fontWeight: '700',
|
|
|
|
|
}}>
|
|
|
|
|
{item.workflow_type}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})()}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Workflow type selector */}
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{
|
|
|
|
|
display: 'block',
|
|
|
|
|
fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600',
|
|
|
|
|
color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.1em',
|
|
|
|
|
marginBottom: '0.5rem',
|
|
|
|
|
}}>
|
|
|
|
|
Target Workflow Type
|
|
|
|
|
</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
|
|
|
{WORKFLOW_OPTIONS.map(({ key, label, col, rgb }) => {
|
|
|
|
|
const active = workflowType === key;
|
|
|
|
|
return (
|
|
|
|
|
<label
|
|
|
|
|
key={key}
|
|
|
|
|
style={{
|
|
|
|
|
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
|
|
|
gap: '0.375rem',
|
|
|
|
|
padding: '0.45rem 0.5rem',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
background: active ? `rgba(${rgb}, 0.15)` : 'transparent',
|
|
|
|
|
border: `1.5px solid ${active ? `rgba(${rgb}, 0.5)` : 'rgba(255,255,255,0.08)'}`,
|
|
|
|
|
color: active ? col : '#475569',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.72rem', fontWeight: '700',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
name="redirect-workflow-type"
|
|
|
|
|
value={key}
|
|
|
|
|
checked={active}
|
|
|
|
|
onChange={() => setWorkflowType(key)}
|
|
|
|
|
style={{ display: 'none' }}
|
|
|
|
|
/>
|
|
|
|
|
<span style={{
|
|
|
|
|
width: '8px', height: '8px', borderRadius: '50%',
|
|
|
|
|
background: active ? col : 'rgba(255,255,255,0.1)',
|
|
|
|
|
boxShadow: active ? `0 0 6px ${col}` : 'none',
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}} />
|
|
|
|
|
{label}
|
|
|
|
|
</label>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Vendor input — conditional */}
|
|
|
|
|
{needsVendor && (
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{
|
|
|
|
|
display: 'block',
|
|
|
|
|
fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '600',
|
|
|
|
|
color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.1em',
|
|
|
|
|
marginBottom: '0.5rem',
|
|
|
|
|
}}>
|
|
|
|
|
Vendor <span style={{ color: '#EF4444' }}>*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={vendor}
|
|
|
|
|
onChange={(e) => setVendor(e.target.value)}
|
|
|
|
|
placeholder="e.g. Cisco, Juniper, ADTRAN…"
|
|
|
|
|
maxLength={200}
|
|
|
|
|
style={{
|
|
|
|
|
width: '100%', boxSizing: 'border-box',
|
|
|
|
|
background: 'rgba(30, 41, 59, 0.6)',
|
|
|
|
|
border: '1px solid rgba(14, 165, 233, 0.25)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
padding: '0.5rem 0.75rem',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.8rem',
|
|
|
|
|
color: '#E2E8F0',
|
|
|
|
|
outline: 'none',
|
|
|
|
|
transition: 'border-color 0.2s',
|
|
|
|
|
}}
|
|
|
|
|
onFocus={(e) => { e.target.style.borderColor = '#0EA5E9'; }}
|
|
|
|
|
onBlur={(e) => { e.target.style.borderColor = 'rgba(14, 165, 233, 0.25)'; }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Error message */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex', alignItems: 'flex-start', gap: '0.5rem',
|
|
|
|
|
padding: '0.625rem 0.75rem',
|
|
|
|
|
background: 'rgba(239, 68, 68, 0.1)',
|
|
|
|
|
border: '1px solid rgba(239, 68, 68, 0.35)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
}}>
|
|
|
|
|
<AlertCircle style={{ width: '16px', height: '16px', color: '#EF4444', flexShrink: 0, marginTop: '1px' }} />
|
|
|
|
|
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.75rem', color: '#FCA5A5' }}>
|
|
|
|
|
{error}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex', justifyContent: 'flex-end', gap: '0.625rem',
|
|
|
|
|
padding: '0.875rem 1.25rem',
|
|
|
|
|
borderTop: '1px solid rgba(14, 165, 233, 0.1)',
|
|
|
|
|
}}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '0.45rem 1rem',
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
color: '#94A3B8',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.75rem', fontWeight: '600',
|
|
|
|
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
disabled={!canSubmit}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex', alignItems: 'center', gap: '0.375rem',
|
|
|
|
|
padding: '0.45rem 1.1rem',
|
|
|
|
|
background: canSubmit
|
|
|
|
|
? 'linear-gradient(135deg, rgba(14, 165, 233, 0.15), rgba(14, 165, 233, 0.1))'
|
|
|
|
|
: 'transparent',
|
|
|
|
|
border: `1.5px solid ${canSubmit ? '#0EA5E9' : 'rgba(255,255,255,0.06)'}`,
|
|
|
|
|
borderRadius: '0.375rem',
|
|
|
|
|
color: canSubmit ? '#38BDF8' : '#334155',
|
|
|
|
|
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.75rem', fontWeight: '600',
|
|
|
|
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
|
|
|
|
cursor: canSubmit ? 'pointer' : 'not-allowed',
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{loading ? (
|
|
|
|
|
<Loader style={{ width: '14px', height: '14px', animation: 'spin 1s linear infinite' }} />
|
|
|
|
|
) : (
|
|
|
|
|
<CornerUpRight style={{ width: '14px', height: '14px' }} />
|
|
|
|
|
)}
|
|
|
|
|
{loading ? 'Redirecting…' : 'Redirect'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|