Files
cve-dashboard/frontend/src/components/RedirectModal.js

321 lines
13 KiB
JavaScript
Raw Normal View History

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 = [
{ 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' },
];
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>
);
}