Add VCL compliance reporting: exec report page, device metadata fields, bulk upload
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2 } from 'lucide-react';
|
||||
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2, Calendar, FileText, Save } from 'lucide-react';
|
||||
import ConfirmModal from '../ConfirmModal';
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||
@@ -48,6 +48,31 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
||||
const [noteError, setNoteError] = useState(null);
|
||||
const [pendingConfirm, setPendingConfirm] = useState(null);
|
||||
|
||||
// Metadata fields
|
||||
const [resolutionDate, setResolutionDate] = useState('');
|
||||
const [remediationPlan, setRemediationPlan] = useState('');
|
||||
const [metaSaving, setMetaSaving] = useState(false);
|
||||
const [metaError, setMetaError] = useState(null);
|
||||
|
||||
const handleSaveMetadata = async (fields) => {
|
||||
setMetaSaving(true);
|
||||
setMetaError(null);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/compliance/items/${encodeURIComponent(hostname)}/metadata`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(fields),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to save metadata');
|
||||
} catch (err) {
|
||||
setMetaError(err.message);
|
||||
} finally {
|
||||
setMetaSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -60,6 +85,10 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
||||
// Default selected metrics to first active failing metric
|
||||
const firstActive = (data.metrics || []).find(m => m.status === 'active');
|
||||
if (firstActive) setSelectedMetrics([firstActive.metric_id]);
|
||||
|
||||
// Populate metadata fields
|
||||
setResolutionDate(data.resolution_date || '');
|
||||
setRemediationPlan(data.remediation_plan || '');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -214,6 +243,80 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Resolution Date */}
|
||||
<Section title="Resolution Date" icon={<Calendar style={{ width: '14px', height: '14px' }} />}>
|
||||
<input
|
||||
type="date"
|
||||
value={resolutionDate}
|
||||
onChange={e => setResolutionDate(e.target.value)}
|
||||
onBlur={() => handleSaveMetadata({ resolution_date: resolutionDate || null })}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'rgba(15,23,42,0.8)',
|
||||
border: '1px solid rgba(20,184,166,0.25)',
|
||||
borderRadius: '0.375rem',
|
||||
color: '#F8FAFC',
|
||||
padding: '0.5rem 0.625rem',
|
||||
fontSize: '0.8rem',
|
||||
fontFamily: 'monospace',
|
||||
outline: 'none',
|
||||
}}
|
||||
onFocus={e => e.target.style.borderColor = `${TEAL}70`}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* Remediation Plan */}
|
||||
<Section title="Remediation Plan" icon={<FileText style={{ width: '14px', height: '14px' }} />}>
|
||||
<textarea
|
||||
value={remediationPlan}
|
||||
onChange={e => {
|
||||
if (e.target.value.length <= 2000) setRemediationPlan(e.target.value);
|
||||
}}
|
||||
placeholder="Describe the remediation plan…"
|
||||
rows={4}
|
||||
style={{
|
||||
width: '100%', resize: 'vertical',
|
||||
background: 'rgba(15,23,42,0.8)',
|
||||
border: '1px solid rgba(20,184,166,0.25)',
|
||||
borderRadius: '0.375rem',
|
||||
color: '#F8FAFC',
|
||||
padding: '0.5rem 0.625rem',
|
||||
fontSize: '0.8rem',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
onFocus={e => e.target.style.borderColor = `${TEAL}70`}
|
||||
onBlur={e => e.target.style.borderColor = 'rgba(20,184,166,0.25)'}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '0.4rem' }}>
|
||||
<span style={{ fontSize: '0.68rem', fontFamily: 'monospace', color: remediationPlan.length > 1900 ? '#F59E0B' : '#475569' }}>
|
||||
{remediationPlan.length}/2000
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleSaveMetadata({ remediation_plan: remediationPlan || null })}
|
||||
disabled={metaSaving}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: '0.3rem',
|
||||
padding: '0.3rem 0.6rem',
|
||||
background: `${TEAL}15`,
|
||||
border: `1px solid ${TEAL}60`,
|
||||
borderRadius: '0.25rem',
|
||||
color: TEAL,
|
||||
fontSize: '0.68rem', fontFamily: 'monospace', fontWeight: '600',
|
||||
cursor: metaSaving ? 'wait' : 'pointer',
|
||||
opacity: metaSaving ? 0.6 : 1,
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
>
|
||||
{metaSaving
|
||||
? <Loader style={{ width: '11px', height: '11px', animation: 'spin 1s linear infinite' }} />
|
||||
: <Save style={{ width: '11px', height: '11px' }} />}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
{metaError && <div style={{ marginTop: '0.4rem', color: '#F87171', fontSize: '0.72rem', fontFamily: 'monospace' }}>{metaError}</div>}
|
||||
</Section>
|
||||
|
||||
{/* Notes */}
|
||||
<Section title="Notes" icon={<MessageSquare style={{ width: '14px', height: '14px' }} />} grow>
|
||||
{detail.notes.length === 0 && (
|
||||
|
||||
Reference in New Issue
Block a user