Add VCL compliance reporting: exec report page, device metadata fields, bulk upload
This commit is contained in:
@@ -5,6 +5,7 @@ import ComplianceUploadModal from './ComplianceUploadModal';
|
||||
import ComplianceDetailPanel from './ComplianceDetailPanel';
|
||||
import ComplianceChartsPanel from './ComplianceChartsPanel';
|
||||
import MetricInfoPanel from './MetricInfoPanel';
|
||||
import VCLReportPage from './VCLReportPage';
|
||||
import metricDefinitionsRaw from '../../data/metricDefinitions.json';
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||
@@ -250,6 +251,7 @@ export default function CompliancePage({ onNavigate }) {
|
||||
const availableTeams = getAvailableTeams();
|
||||
const [activeTeam, setActiveTeam] = useState(() => availableTeams[0] || 'STEAM');
|
||||
const [activeTab, setActiveTab] = useState('active');
|
||||
const [vclView, setVclView] = useState(false);
|
||||
const [metricFilter, setMetricFilter] = useState(null);
|
||||
const [hostSearch, setHostSearch] = useState('');
|
||||
const [summary, setSummary] = useState({ entries: [], overall_scores: {}, upload: null });
|
||||
@@ -408,6 +410,23 @@ export default function CompliancePage({ onNavigate }) {
|
||||
onMouseLeave={e => { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.25)'; }}>
|
||||
<RefreshCw style={{ width: '16px', height: '16px' }} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setVclView(!vclView)}
|
||||
style={{
|
||||
background: vclView ? `${TEAL}18` : 'transparent',
|
||||
border: `1px solid ${vclView ? TEAL : 'rgba(20,184,166,0.25)'}`,
|
||||
color: vclView ? TEAL : '#475569',
|
||||
padding: '0.5rem 1rem',
|
||||
display: 'flex', alignItems: 'center', gap: '0.4rem',
|
||||
fontFamily: 'monospace', fontSize: '0.75rem', fontWeight: '600',
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em', cursor: 'pointer',
|
||||
borderRadius: '0.375rem', transition: 'all 0.15s',
|
||||
}}
|
||||
onMouseEnter={e => { if (!vclView) { e.currentTarget.style.color = TEAL; e.currentTarget.style.borderColor = `${TEAL}60`; }}}
|
||||
onMouseLeave={e => { if (!vclView) { e.currentTarget.style.color = '#475569'; e.currentTarget.style.borderColor = 'rgba(20,184,166,0.25)'; }}}
|
||||
>
|
||||
VCL Report
|
||||
</button>
|
||||
{canWrite() && (
|
||||
<button onClick={() => setShowUpload(true)}
|
||||
className="intel-button"
|
||||
@@ -426,8 +445,13 @@ export default function CompliancePage({ onNavigate }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── VCL Report View ─────────────────────────────────────── */}
|
||||
{vclView && (
|
||||
<VCLReportPage />
|
||||
)}
|
||||
|
||||
{/* ── Team tabs ────────────────────────────────────────────── */}
|
||||
{availableTeams.length === 0 && !isAdmin() ? (
|
||||
{!vclView && availableTeams.length === 0 && !isAdmin() ? (
|
||||
<div style={{
|
||||
padding: '1.5rem', marginBottom: '1.5rem',
|
||||
borderRadius: '0.5rem', border: '1px solid rgba(245, 158, 11, 0.3)',
|
||||
@@ -437,7 +461,7 @@ export default function CompliancePage({ onNavigate }) {
|
||||
}}>
|
||||
No BU teams assigned to your account. Contact an admin to configure your team access.
|
||||
</div>
|
||||
) : (
|
||||
) : !vclView && (
|
||||
<div style={{ display: 'flex', gap: '0.375rem', marginBottom: '1.5rem' }}>
|
||||
{availableTeams.map(team => {
|
||||
const isActive = activeTeam === team;
|
||||
@@ -463,7 +487,7 @@ export default function CompliancePage({ onNavigate }) {
|
||||
)}
|
||||
|
||||
{/* ── Metric health cards ──────────────────────────────────── */}
|
||||
{families.length > 0 ? (
|
||||
{!vclView && families.length > 0 ? (
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<div style={{ fontSize: '0.65rem', color: '#334155', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '0.625rem' }}>
|
||||
Metric Health — click to filter
|
||||
@@ -564,10 +588,10 @@ export default function CompliancePage({ onNavigate }) {
|
||||
) : null}
|
||||
|
||||
{/* ── Historical trend charts ──────────────────────────────── */}
|
||||
<ComplianceChartsPanel />
|
||||
{!vclView && <ComplianceChartsPanel />}
|
||||
|
||||
{/* ── Device table ─────────────────────────────────────────── */}
|
||||
<div style={{
|
||||
{!vclView && <div style={{
|
||||
background: 'linear-gradient(135deg,rgba(30,41,59,0.95) 0%,rgba(15,23,42,0.98) 100%)',
|
||||
border: '1px solid rgba(20,184,166,0.15)', borderRadius: '0.5rem',
|
||||
overflow: 'hidden',
|
||||
@@ -622,7 +646,7 @@ export default function CompliancePage({ onNavigate }) {
|
||||
{/* Column headers */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2.5fr 1.1fr 1fr 2fr 0.5fr 0.4fr',
|
||||
gridTemplateColumns: '2fr 1fr 0.8fr 1.8fr 1fr 1.2fr 0.5fr 0.4fr',
|
||||
padding: '0.5rem 1rem',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
fontSize: '0.62rem', color: '#334155',
|
||||
@@ -632,6 +656,8 @@ export default function CompliancePage({ onNavigate }) {
|
||||
<span>IP Address</span>
|
||||
<span>Type</span>
|
||||
<span>Failing Metrics</span>
|
||||
<span>Resolution Date</span>
|
||||
<span>Remediation Plan</span>
|
||||
<span>Seen</span>
|
||||
<span></span>
|
||||
</div>
|
||||
@@ -659,7 +685,7 @@ export default function CompliancePage({ onNavigate }) {
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{/* ── Detail panel ─────────────────────────────────────────── */}
|
||||
{selectedHost && (
|
||||
@@ -805,12 +831,17 @@ export default function CompliancePage({ onNavigate }) {
|
||||
}
|
||||
|
||||
function DeviceRow({ device, selected, onClick }) {
|
||||
const truncateText = (text, maxLen = 80) => {
|
||||
if (!text) return '—';
|
||||
return text.length > maxLen ? text.slice(0, maxLen) + '…' : text;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2.5fr 1.1fr 1fr 2fr 0.5fr 0.4fr',
|
||||
gridTemplateColumns: '2fr 1fr 0.8fr 1.8fr 1fr 1.2fr 0.5fr 0.4fr',
|
||||
padding: '0.625rem 1rem',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
cursor: 'pointer',
|
||||
@@ -844,6 +875,16 @@ function DeviceRow({ device, selected, onClick }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Resolution Date */}
|
||||
<div style={{ fontFamily: 'monospace', fontSize: '0.72rem', color: '#94A3B8' }}>
|
||||
{device.resolution_date || '—'}
|
||||
</div>
|
||||
|
||||
{/* Remediation Plan */}
|
||||
<div style={{ fontSize: '0.7rem', color: '#94A3B8', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={device.remediation_plan || ''}>
|
||||
{truncateText(device.remediation_plan)}
|
||||
</div>
|
||||
|
||||
{/* Seen count */}
|
||||
<div>
|
||||
<SeenBadge count={device.seen_count} />
|
||||
|
||||
Reference in New Issue
Block a user