Files
cve-dashboard/frontend/src/components/AtlasSlideOutPanel.js
root 4c04c9870a Add Atlas InfoSec action plans integration
Integrate Atlas InfoSec API to manage compliance action plans directly from
the ReportingPage. Users can view, create, and update action plans for host
findings without switching to the Atlas web tool.

Backend:
- Add atlasApi.js helper with Basic Auth, TLS skip, GET/PUT/PATCH/POST
- Add atlas_action_plans_cache migration for SQLite cache table
- Add atlas.js router with sync, status, and proxy CRUD endpoints
- Mount Atlas router at /api/atlas in server.js
- Extract hostId from Ivanti host findings during sync

Frontend:
- Add AtlasBadge component (amber=needs plan, green=has plan)
- Add AtlasSlideOutPanel with plan list, create form, edit capability
- Separate active plans from inactive history in collapsible section
- Custom dark-themed plan type dropdown
- Optimistic local state shows pending plans immediately after creation
- Atlas sync button on ReportingPage toolbar
- Prepopulate finding ID in create form from clicked row

Environment:
- Add ATLAS_API_URL, ATLAS_API_USER, ATLAS_API_PASS, ATLAS_SKIP_TLS to .env.example
2026-04-23 21:52:53 +00:00

871 lines
36 KiB
JavaScript

import React, { useState, useEffect, useCallback, useRef } from 'react';
import { X, Loader, Shield, Plus, Edit3, Check, AlertCircle, ChevronDown } from 'lucide-react';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
// ---------------------------------------------------------------------------
// Plan type badge colors
// ---------------------------------------------------------------------------
const PLAN_TYPE_COLORS = {
remediation: '#0EA5E9',
decommission: '#EF4444',
false_positive: '#F59E0B',
risk_acceptance: '#A855F7',
scan_exclusion: '#64748B',
};
const VALID_PLAN_TYPES = Object.keys(PLAN_TYPE_COLORS);
// ---------------------------------------------------------------------------
// Shared inline style constants
// ---------------------------------------------------------------------------
const ACCENT = '#0EA5E9';
const panelStyle = {
position: 'fixed', top: 0, right: 0, bottom: 0, width: '420px',
background: '#0A1220',
borderLeft: '1px solid rgba(14,165,233,0.15)',
boxShadow: '-8px 0 32px rgba(0,0,0,0.6)',
zIndex: 41,
display: 'flex', flexDirection: 'column',
overflowY: 'auto',
fontFamily: "'JetBrains Mono', monospace",
};
const backdropStyle = {
position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.4)',
zIndex: 40,
};
const headerStyle = {
padding: '1.25rem 1.25rem 1rem',
borderBottom: '1px solid rgba(255,255,255,0.06)',
flexShrink: 0,
};
const sectionTitleStyle = {
display: 'flex', alignItems: 'center', gap: '0.4rem',
fontSize: '0.68rem', fontFamily: "'JetBrains Mono', monospace",
textTransform: 'uppercase', letterSpacing: '0.1em',
color: '#475569', marginBottom: '0.75rem',
};
const inputStyle = {
width: '100%', boxSizing: 'border-box',
background: 'rgba(14,165,233,0.06)',
border: '1px solid rgba(14,165,233,0.2)',
borderRadius: '0.375rem',
color: '#E2E8F0',
padding: '0.5rem 0.625rem',
fontSize: '0.78rem',
fontFamily: "'JetBrains Mono', monospace",
outline: 'none',
transition: 'border-color 0.15s',
};
const labelStyle = {
display: 'block',
fontSize: '0.68rem',
fontFamily: "'JetBrains Mono', monospace",
color: '#94A3B8',
marginBottom: '0.3rem',
textTransform: 'uppercase',
letterSpacing: '0.05em',
};
const primaryBtnStyle = {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '0.4rem',
padding: '0.5rem 1rem',
background: 'rgba(14,165,233,0.15)',
border: '1px solid #0EA5E9',
borderRadius: '0.375rem',
color: '#38BDF8',
fontSize: '0.75rem',
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.15s',
textTransform: 'uppercase',
letterSpacing: '0.05em',
};
// ---------------------------------------------------------------------------
// Custom dropdown — dark-themed replacement for native <select>
// ---------------------------------------------------------------------------
function PlanTypeDropdown({ value, onChange }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const handleClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [open]);
const color = PLAN_TYPE_COLORS[value] || '#94A3B8';
return (
<div ref={ref} style={{ position: 'relative' }}>
<button
type="button"
onClick={() => setOpen(!open)}
style={{
...inputStyle,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
cursor: 'pointer', textAlign: 'left',
borderColor: open ? 'rgba(14,165,233,0.5)' : 'rgba(14,165,233,0.2)',
}}
>
<span style={{ color, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.03em' }}>
{value.replace(/_/g, ' ')}
</span>
<ChevronDown style={{ width: 14, height: 14, color: '#475569', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} />
</button>
{open && (
<div style={{
position: 'absolute', top: '100%', left: 0, right: 0, marginTop: '4px',
background: '#0F1A2E',
border: '1px solid rgba(14,165,233,0.25)',
borderRadius: '0.375rem',
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
zIndex: 50, overflow: 'hidden',
}}>
{VALID_PLAN_TYPES.map(t => {
const c = PLAN_TYPE_COLORS[t] || '#94A3B8';
const isSelected = t === value;
return (
<div
key={t}
onClick={() => { onChange(t); setOpen(false); }}
style={{
padding: '0.5rem 0.625rem',
cursor: 'pointer',
background: isSelected ? 'rgba(14,165,233,0.12)' : 'transparent',
color: c,
fontSize: '0.78rem',
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.03em',
transition: 'background 0.1s',
}}
onMouseEnter={e => { if (!isSelected) e.currentTarget.style.background = 'rgba(14,165,233,0.06)'; }}
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = 'transparent'; }}
>
{t.replace(/_/g, ' ')}
</div>
);
})}
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// PlanTypeBadge — colored pill for plan type
// ---------------------------------------------------------------------------
function PlanTypeBadge({ type }) {
const color = PLAN_TYPE_COLORS[type] || '#94A3B8';
return (
<span style={{
display: 'inline-flex', alignItems: 'center',
padding: '0.2rem 0.5rem',
background: `${color}18`,
border: `1px solid ${color}50`,
borderRadius: '0.25rem',
color,
fontSize: '0.7rem',
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.03em',
}}>
{type.replace(/_/g, ' ')}
</span>
);
}
// ---------------------------------------------------------------------------
// PlanCard — displays a single action plan
// ---------------------------------------------------------------------------
function PlanCard({ plan, canWrite, onSaveEdit, editingId, onStartEdit, onCancelEdit }) {
const isEditing = editingId === (plan.action_plan_id || plan.id);
const [editForm, setEditForm] = useState({
commit_date: plan.commit_date || '',
qualys_id: plan.qualys_id || '',
active_host_findings_id: plan.active_host_findings_id || '',
jira_vnr: plan.jira_vnr || '',
archer_exc: plan.archer_exc || '',
});
const [saving, setSaving] = useState(false);
const [editError, setEditError] = useState(null);
const handleSave = async () => {
setSaving(true);
setEditError(null);
try {
await onSaveEdit(plan.action_plan_id || plan.id, editForm);
} catch (err) {
setEditError(err.message);
} finally {
setSaving(false);
}
};
const isPending = !!plan._localPending;
return (
<div style={{
marginBottom: '0.625rem', padding: '0.625rem 0.75rem',
background: isPending ? 'rgba(245,158,11,0.06)' : 'rgba(14,165,233,0.04)',
border: isPending ? '1px solid rgba(245,158,11,0.25)' : '1px solid rgba(14,165,233,0.12)',
borderRadius: '0.375rem',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.4rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
<PlanTypeBadge type={plan.plan_type || 'unknown'} />
{isPending && (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: '3px',
padding: '0.15rem 0.4rem',
background: 'rgba(245,158,11,0.12)',
border: '1px solid rgba(245,158,11,0.35)',
borderRadius: '0.25rem',
color: '#F59E0B',
fontSize: '0.6rem',
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}>
pending
</span>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
{plan.status && !isPending && (
<span style={{
display: 'inline-flex', alignItems: 'center', gap: '3px',
padding: '0.15rem 0.4rem',
background: 'rgba(16,185,129,0.12)',
border: '1px solid rgba(16,185,129,0.35)',
borderRadius: '0.25rem',
color: '#10B981',
fontSize: '0.6rem',
fontFamily: "'JetBrains Mono', monospace",
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}>
{plan.status}
</span>
)}
{canWrite && !isEditing && !isPending && (
<button
onClick={() => onStartEdit(plan.action_plan_id || plan.id)}
title="Edit plan"
style={{
background: 'none', border: '1px solid rgba(14,165,233,0.15)',
borderRadius: '0.25rem', padding: '0.2rem',
cursor: 'pointer', color: '#475569',
transition: 'all 0.15s', lineHeight: 1,
}}
onMouseEnter={e => { e.currentTarget.style.color = ACCENT; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.5)'; }}
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(14,165,233,0.15)'; }}
>
<Edit3 style={{ width: 11, height: 11 }} />
</button>
)}
</div>
</div>
{!isEditing ? (
<>
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.25rem' }}>
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '72px' }}>Commit</span>
<span style={{ fontSize: '0.68rem', color: '#E2E8F0', fontFamily: "'JetBrains Mono', monospace" }}>{plan.commit_date || '—'}</span>
</div>
{plan.jira_vnr && (
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.25rem' }}>
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '72px' }}>VNR</span>
<span style={{ fontSize: '0.68rem', color: '#E2E8F0', fontFamily: "'JetBrains Mono', monospace" }}>{plan.jira_vnr}</span>
</div>
)}
{plan.archer_exc && (
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '0.25rem' }}>
<span style={{ fontSize: '0.68rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '72px' }}>EXC</span>
<span style={{ fontSize: '0.68rem', color: '#E2E8F0', fontFamily: "'JetBrains Mono', monospace" }}>{plan.archer_exc}</span>
</div>
)}
</>
) : (
<div style={{ marginTop: '0.5rem' }}>
<div style={{ marginBottom: '0.5rem' }}>
<label style={labelStyle}>Commit Date</label>
<input
type="date"
value={editForm.commit_date}
onChange={e => setEditForm({ ...editForm, commit_date: e.target.value })}
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
<div style={{ marginBottom: '0.5rem' }}>
<label style={labelStyle}>Qualys ID</label>
<input
type="text"
value={editForm.qualys_id}
onChange={e => setEditForm({ ...editForm, qualys_id: e.target.value })}
placeholder="Optional"
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
<div style={{ marginBottom: '0.5rem' }}>
<label style={labelStyle}>Findings ID</label>
<input
type="number"
value={editForm.active_host_findings_id}
onChange={e => setEditForm({ ...editForm, active_host_findings_id: e.target.value })}
placeholder="Optional"
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
<div style={{ marginBottom: '0.5rem' }}>
<label style={labelStyle}>Jira VNR</label>
<input
type="text"
value={editForm.jira_vnr}
onChange={e => setEditForm({ ...editForm, jira_vnr: e.target.value })}
placeholder="Optional"
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
<div style={{ marginBottom: '0.5rem' }}>
<label style={labelStyle}>Archer EXC</label>
<input
type="text"
value={editForm.archer_exc}
onChange={e => setEditForm({ ...editForm, archer_exc: e.target.value })}
placeholder="Optional"
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
{editError && (
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', color: '#F87171', fontSize: '0.72rem', marginBottom: '0.5rem' }}>
<AlertCircle style={{ width: 12, height: 12, flexShrink: 0 }} />{editError}
</div>
)}
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button
onClick={handleSave}
disabled={saving}
style={{ ...primaryBtnStyle, fontSize: '0.68rem', padding: '0.35rem 0.75rem', opacity: saving ? 0.6 : 1 }}
>
{saving ? <Loader style={{ width: 12, height: 12, animation: 'spin 1s linear infinite' }} /> : <Check style={{ width: 12, height: 12 }} />}
Save
</button>
<button
onClick={onCancelEdit}
style={{
padding: '0.35rem 0.75rem',
background: 'transparent',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '0.375rem',
color: '#94A3B8',
fontSize: '0.68rem',
fontFamily: "'JetBrains Mono', monospace",
cursor: 'pointer',
transition: 'all 0.15s',
}}
>
Cancel
</button>
</div>
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// InactiveSection — collapsible history of overridden/inactive plans
// ---------------------------------------------------------------------------
function InactiveSection({ plans }) {
const [expanded, setExpanded] = useState(false);
return (
<div style={{ padding: '0.75rem 1.25rem', borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
<button
onClick={() => setExpanded(!expanded)}
style={{
display: 'flex', alignItems: 'center', gap: '0.4rem',
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
fontSize: '0.68rem', fontFamily: "'JetBrains Mono', monospace",
textTransform: 'uppercase', letterSpacing: '0.1em',
color: '#475569', width: '100%',
}}
>
<ChevronDown style={{ width: 12, height: 12, transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform 0.15s' }} />
History ({plans.length})
</button>
{expanded && (
<div style={{ marginTop: '0.625rem' }}>
{plans.map((plan, idx) => (
<div key={plan.action_plan_id || idx} style={{
marginBottom: '0.5rem', padding: '0.5rem 0.625rem',
background: 'rgba(100,116,139,0.04)',
border: '1px solid rgba(100,116,139,0.1)',
borderRadius: '0.375rem',
opacity: 0.7,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.25rem' }}>
<PlanTypeBadge type={plan.plan_type || 'unknown'} />
<span style={{
fontSize: '0.6rem', color: '#64748B',
fontFamily: "'JetBrains Mono', monospace",
textTransform: 'uppercase',
}}>
{plan.status || 'inactive'}
</span>
</div>
<div style={{ display: 'flex', gap: '0.4rem' }}>
<span style={{ fontSize: '0.65rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '60px' }}>Commit</span>
<span style={{ fontSize: '0.65rem', color: '#94A3B8', fontFamily: "'JetBrains Mono', monospace" }}>{plan.commit_date || '—'}</span>
</div>
{plan.created_at && (
<div style={{ display: 'flex', gap: '0.4rem' }}>
<span style={{ fontSize: '0.65rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace", minWidth: '60px' }}>Created</span>
<span style={{ fontSize: '0.65rem', color: '#94A3B8', fontFamily: "'JetBrains Mono', monospace" }}>{plan.created_at.split('T')[0]}</span>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// AtlasSlideOutPanel — main exported component
// ---------------------------------------------------------------------------
export default function AtlasSlideOutPanel({ hostId, hostName, findingId, qualysId, onClose, canWrite, onPlanChange }) {
const [plans, setPlans] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [editingId, setEditingId] = useState(null);
// Create form state — prepopulate qualys_id and findings ID from the clicked finding
const [showCreate, setShowCreate] = useState(false);
const [createForm, setCreateForm] = useState({
plan_type: 'remediation',
commit_date: '',
qualys_id: qualysId || '',
active_host_findings_id: findingId || '',
jira_vnr: '',
archer_exc: '',
});
const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState(null);
const [successMsg, setSuccessMsg] = useState(null);
// -----------------------------------------------------------------------
// Parse Atlas response — handles { active: [...], inactive: [...] } format
// -----------------------------------------------------------------------
function parseAtlasPlans(data) {
if (Array.isArray(data)) return data;
if (data && typeof data === 'object') {
const active = Array.isArray(data.active) ? data.active : [];
const inactive = Array.isArray(data.inactive) ? data.inactive : [];
return [...active, ...inactive];
}
return [];
}
// -----------------------------------------------------------------------
// Fetch plans
// -----------------------------------------------------------------------
const fetchPlans = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(`${API_BASE}/atlas/hosts/${hostId}/action-plans`, { credentials: 'include' });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Failed to load plans (${res.status})`);
}
const data = await res.json();
const remotePlans = parseAtlasPlans(data);
// Merge: keep local pending plans that aren't yet confirmed by Atlas
setPlans(prev => {
const localPending = prev.filter(p => p._localPending);
const remoteIds = new Set(remotePlans.map(p => p.action_plan_id));
// Remove local pending plans that now appear in remote (confirmed)
const stillPending = localPending.filter(p => !remoteIds.has(p.action_plan_id));
return [...remotePlans, ...stillPending];
});
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [hostId]);
useEffect(() => { fetchPlans(); }, [fetchPlans]);
// Clear success message after 3s
useEffect(() => {
if (!successMsg) return;
const t = setTimeout(() => setSuccessMsg(null), 3000);
return () => clearTimeout(t);
}, [successMsg]);
// -----------------------------------------------------------------------
// Create plan
// -----------------------------------------------------------------------
const handleCreate = async () => {
if (!createForm.commit_date) {
setCreateError('Commit date is required');
return;
}
setCreating(true);
setCreateError(null);
try {
const body = {
plan_type: createForm.plan_type,
commit_date: createForm.commit_date,
};
if (createForm.qualys_id.trim()) body.qualys_id = createForm.qualys_id.trim();
if (createForm.active_host_findings_id) body.active_host_findings_id = Number(createForm.active_host_findings_id);
if (createForm.jira_vnr.trim()) body.jira_vnr = createForm.jira_vnr.trim();
if (createForm.archer_exc.trim()) body.archer_exc = createForm.archer_exc.trim();
const res = await fetch(`${API_BASE}/atlas/hosts/${hostId}/action-plans`, {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `Create failed (${res.status})`);
// Add optimistic local plan immediately — shown as "pending" until sync confirms
const localPlan = {
action_plan_id: data.action_plan_id || ('local-' + Date.now()),
plan_type: body.plan_type,
commit_date: body.commit_date,
qualys_id: body.qualys_id || null,
active_host_findings_id: body.active_host_findings_id || null,
jira_vnr: body.jira_vnr || null,
archer_exc: body.archer_exc || null,
status: 'pending',
_localPending: true,
created_at: new Date().toISOString(),
};
setPlans(prev => [localPlan, ...prev]);
// Reset form
setCreateForm({ plan_type: 'remediation', commit_date: '', qualys_id: qualysId || '', active_host_findings_id: findingId || '', jira_vnr: '', archer_exc: '' });
setShowCreate(false);
setSuccessMsg('Action plan created');
if (onPlanChange) onPlanChange();
} catch (err) {
setCreateError(err.message);
} finally {
setCreating(false);
}
};
// -----------------------------------------------------------------------
// Edit plan
// -----------------------------------------------------------------------
const handleSaveEdit = async (actionPlanId, updates) => {
const res = await fetch(`${API_BASE}/atlas/hosts/${hostId}/action-plans`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action_plan_id: actionPlanId, updates }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `Update failed (${res.status})`);
setEditingId(null);
setSuccessMsg('Action plan updated');
await fetchPlans();
if (onPlanChange) onPlanChange();
};
// -----------------------------------------------------------------------
// Render
// -----------------------------------------------------------------------
return (
<>
{/* Backdrop */}
<div onClick={onClose} style={backdropStyle} data-testid="atlas-panel-backdrop" />
{/* Panel */}
<div style={panelStyle} data-testid="atlas-slide-out-panel">
{/* Header */}
<div style={headerStyle}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ minWidth: 0, flex: 1, marginRight: '0.75rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.3rem' }}>
<Shield style={{ width: 16, height: 16, color: ACCENT, flexShrink: 0 }} />
<span style={{ fontSize: '0.9rem', fontWeight: 700, color: '#E2E8F0', wordBreak: 'break-all', lineHeight: 1.3 }}>
{hostName || 'Unknown Host'}
</span>
</div>
<span style={{ fontSize: '0.72rem', color: '#475569', fontFamily: "'JetBrains Mono', monospace" }}>
Host ID: {hostId}
</span>
</div>
<button
onClick={onClose}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#475569', flexShrink: 0, padding: '0.25rem' }}
onMouseEnter={e => e.currentTarget.style.color = '#E2E8F0'}
onMouseLeave={e => e.currentTarget.style.color = '#475569'}
data-testid="atlas-panel-close"
>
<X style={{ width: 18, height: 18 }} />
</button>
</div>
</div>
{/* Success message */}
{successMsg && (
<div style={{
margin: '0.75rem 1.25rem 0', padding: '0.5rem 0.75rem',
background: 'rgba(16,185,129,0.1)',
border: '1px solid rgba(16,185,129,0.3)',
borderRadius: '0.375rem',
color: '#10B981', fontSize: '0.75rem',
display: 'flex', alignItems: 'center', gap: '0.4rem',
}}>
<Check style={{ width: 14, height: 14 }} />{successMsg}
</div>
)}
{/* Loading */}
{loading && (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '3rem 0' }}>
<Loader style={{ width: 28, height: 28, color: ACCENT, animation: 'spin 1s linear infinite' }} />
</div>
)}
{/* Error */}
{error && !loading && (
<div style={{ padding: '1.25rem', display: 'flex', flexDirection: 'column', gap: '0.75rem', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '0.5rem', color: '#F87171', fontSize: '0.8rem', alignItems: 'center' }}>
<AlertCircle style={{ width: 16, height: 16, flexShrink: 0 }} />{error}
</div>
<button
onClick={fetchPlans}
style={{
...primaryBtnStyle,
fontSize: '0.68rem',
padding: '0.35rem 0.75rem',
}}
>
Retry
</button>
</div>
)}
{/* Plan list */}
{!loading && !error && (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
{/* Section: Active plans */}
<div style={{ padding: '1rem 1.25rem', borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
<div style={sectionTitleStyle}>
<Shield style={{ width: 14, height: 14, color: ACCENT }} />
Active Plans ({plans.filter(p => p.status === 'active' || p._localPending).length})
</div>
{plans.filter(p => p.status === 'active' || p._localPending).length === 0 && (
<div style={{ color: '#475569', fontSize: '0.75rem', fontStyle: 'italic' }}>
No active action plans for this host.
</div>
)}
{plans.filter(p => p.status === 'active' || p._localPending).map((plan, idx) => (
<PlanCard
key={plan.action_plan_id || plan.id || idx}
plan={plan}
canWrite={canWrite}
editingId={editingId}
onStartEdit={setEditingId}
onCancelEdit={() => setEditingId(null)}
onSaveEdit={handleSaveEdit}
/>
))}
</div>
{/* Section: Inactive plans (history) — collapsible */}
{plans.filter(p => p.status !== 'active' && !p._localPending).length > 0 && (
<InactiveSection plans={plans.filter(p => p.status !== 'active' && !p._localPending)} />
)}
{/* Section: Create form */}
{canWrite && (
<div style={{ padding: '1rem 1.25rem' }}>
{!showCreate ? (
<button
onClick={() => setShowCreate(true)}
style={{
...primaryBtnStyle,
width: '100%',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.25)'; e.currentTarget.style.boxShadow = '0 0 20px rgba(14,165,233,0.25)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.15)'; e.currentTarget.style.boxShadow = 'none'; }}
data-testid="atlas-create-plan-btn"
>
<Plus style={{ width: 14, height: 14 }} />
New Action Plan
</button>
) : (
<div data-testid="atlas-create-form">
<div style={sectionTitleStyle}>
<Plus style={{ width: 14, height: 14, color: ACCENT }} />
Create Action Plan
</div>
{/* Plan type */}
<div style={{ marginBottom: '0.625rem' }}>
<label style={labelStyle}>Plan Type</label>
<PlanTypeDropdown
value={createForm.plan_type}
onChange={val => setCreateForm({ ...createForm, plan_type: val })}
/>
</div>
{/* Commit date */}
<div style={{ marginBottom: '0.625rem' }}>
<label style={labelStyle}>Commit Date *</label>
<input
type="date"
value={createForm.commit_date}
onChange={e => setCreateForm({ ...createForm, commit_date: e.target.value })}
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
{/* Qualys ID */}
<div style={{ marginBottom: '0.625rem' }}>
<label style={labelStyle}>Qualys ID</label>
<input
type="text"
value={createForm.qualys_id}
onChange={e => setCreateForm({ ...createForm, qualys_id: e.target.value })}
placeholder="Optional"
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
{/* Active Host Findings ID */}
<div style={{ marginBottom: '0.625rem' }}>
<label style={labelStyle}>Findings ID</label>
<input
type="number"
value={createForm.active_host_findings_id}
onChange={e => setCreateForm({ ...createForm, active_host_findings_id: e.target.value })}
placeholder="Optional"
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
{/* Jira VNR */}
<div style={{ marginBottom: '0.625rem' }}>
<label style={labelStyle}>Jira VNR</label>
<input
type="text"
value={createForm.jira_vnr}
onChange={e => setCreateForm({ ...createForm, jira_vnr: e.target.value })}
placeholder="Optional"
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
{/* Archer EXC */}
<div style={{ marginBottom: '0.625rem' }}>
<label style={labelStyle}>Archer EXC</label>
<input
type="text"
value={createForm.archer_exc}
onChange={e => setCreateForm({ ...createForm, archer_exc: e.target.value })}
placeholder="Optional"
style={inputStyle}
onFocus={e => e.target.style.borderColor = 'rgba(14,165,233,0.5)'}
onBlur={e => e.target.style.borderColor = 'rgba(14,165,233,0.2)'}
/>
</div>
{/* Create error */}
{createError && (
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', color: '#F87171', fontSize: '0.72rem', marginBottom: '0.625rem' }}>
<AlertCircle style={{ width: 12, height: 12, flexShrink: 0 }} />{createError}
</div>
)}
{/* Buttons */}
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button
onClick={handleCreate}
disabled={creating}
style={{ ...primaryBtnStyle, opacity: creating ? 0.6 : 1 }}
onMouseEnter={e => { if (!creating) { e.currentTarget.style.background = 'rgba(14,165,233,0.25)'; } }}
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(14,165,233,0.15)'; }}
>
{creating
? <Loader style={{ width: 14, height: 14, animation: 'spin 1s linear infinite' }} />
: <Check style={{ width: 14, height: 14 }} />}
Create
</button>
<button
onClick={() => { setShowCreate(false); setCreateError(null); }}
style={{
padding: '0.5rem 1rem',
background: 'transparent',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '0.375rem',
color: '#94A3B8',
fontSize: '0.75rem',
fontFamily: "'JetBrains Mono', monospace",
cursor: 'pointer',
transition: 'all 0.15s',
}}
>
Cancel
</button>
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
</>
);
}