New feature: multi-file per-vertical compliance xlsx upload with scoped resolution logic, executive-level aggregated reporting, and drill-down by vertical and metric. Supports daily upload cadence and batch commit. Backend: - Migration: add vertical column to compliance_items/uploads, create vcl_multi_vertical_summary table - New route module: routes/vclMultiVertical.js with preview, commit, stats, trend, metric drill-down, device list, and burndown endpoints - New helpers: parseVerticalFilename(), computeVerticalBurndown() - Vertical-scoped resolution: uploading one vertical never resolves items from other verticals Frontend: - CCPMetricsPage with stats bar, trend chart, donut, vertical table - Drill-down: vertical -> metrics by category -> device list - Per-vertical burndown forecast chart - MultiVerticalUploadModal: multi-file drag-drop, batch preview, commit - Nav entry: CCP Metrics (Building2 icon) Docs: - Design brief for stakeholder meeting (docs/vcl-multi-vertical-design-brief.md)
190 lines
8.5 KiB
JavaScript
190 lines
8.5 KiB
JavaScript
import React from 'react';
|
|
import { X, Home, BarChart2, BookOpen, Download, ShieldCheck, Settings, Ticket, Building2 } from 'lucide-react';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
const NAV_ITEMS = [
|
|
{ id: 'home', label: 'Home', icon: Home, color: '#0EA5E9', description: 'Main dashboard' },
|
|
{ id: 'triage', label: 'Vuln Triage', icon: BarChart2, color: '#F59E0B', description: 'Active findings & CVE triage' },
|
|
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck, color: '#14B8A6', description: 'AEO posture & metrics' },
|
|
{ id: 'ccp-metrics', label: 'CCP Metrics', icon: Building2, color: '#A78BFA', description: 'Cross-vertical VCL reporting' },
|
|
{ id: 'knowledge-base', label: 'Knowledge Base', icon: BookOpen, color: '#10B981', description: 'Articles & documentation' },
|
|
{ id: 'exports', label: 'Exports', icon: Download, color: '#8B5CF6', description: 'Export data & reports' },
|
|
{ id: 'jira', label: 'Jira Tickets', icon: Ticket, color: '#6366F1', description: 'Jira issue tracking & sync' },
|
|
];
|
|
|
|
const ADMIN_ITEM = { id: 'admin', label: 'Admin Panel', icon: Settings, color: '#EF4444', description: 'User management & audit' };
|
|
|
|
export default function NavDrawer({ isOpen, onClose, currentPage, onNavigate }) {
|
|
const { isAdmin } = useAuth();
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
<div
|
|
onClick={onClose}
|
|
style={{
|
|
position: 'fixed', inset: 0,
|
|
background: 'rgba(0, 0, 0, 0.65)',
|
|
backdropFilter: 'blur(3px)',
|
|
zIndex: 50
|
|
}}
|
|
/>
|
|
|
|
{/* Drawer */}
|
|
<div style={{
|
|
position: 'fixed', top: 0, left: 0, bottom: 0, width: '280px',
|
|
background: 'linear-gradient(180deg, #0F1A2E 0%, #0A1628 100%)',
|
|
borderRight: '1px solid rgba(14, 165, 233, 0.2)',
|
|
boxShadow: '6px 0 32px rgba(0, 0, 0, 0.7)',
|
|
zIndex: 51,
|
|
display: 'flex', flexDirection: 'column',
|
|
padding: '1.5rem'
|
|
}}>
|
|
{/* Drawer header */}
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '2rem' }}>
|
|
<div>
|
|
<div style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: '700', color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.15em', textShadow: '0 0 12px rgba(14, 165, 233, 0.5)' }}>
|
|
STEAM
|
|
</div>
|
|
<div style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569', textTransform: 'uppercase', letterSpacing: '0.1em', marginTop: '2px' }}>
|
|
Security Dashboard
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
style={{ color: '#475569', padding: '0.25rem', background: 'none', border: 'none', cursor: 'pointer', lineHeight: 1 }}
|
|
onMouseEnter={e => e.currentTarget.style.color = '#E2E8F0'}
|
|
onMouseLeave={e => e.currentTarget.style.color = '#475569'}
|
|
>
|
|
<X style={{ width: '20px', height: '20px' }} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Nav items */}
|
|
<nav style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
|
|
{NAV_ITEMS.map(({ id, label, icon: Icon, color, description }) => {
|
|
const active = currentPage === id;
|
|
return (
|
|
<button
|
|
key={id}
|
|
onClick={() => { onNavigate(id); onClose(); }}
|
|
style={{
|
|
display: 'flex', alignItems: 'center', gap: '0.875rem',
|
|
padding: '0.75rem 0.875rem',
|
|
borderRadius: '0.5rem',
|
|
border: active ? `1px solid ${color}50` : '1px solid transparent',
|
|
background: active ? `${color}18` : 'transparent',
|
|
cursor: 'pointer', textAlign: 'left', width: '100%',
|
|
transition: 'background 0.15s, border-color 0.15s'
|
|
}}
|
|
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; }}
|
|
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
|
|
>
|
|
{/* Icon box */}
|
|
<div style={{
|
|
width: '36px', height: '36px', flexShrink: 0,
|
|
borderRadius: '0.375rem',
|
|
background: `${color}18`,
|
|
border: `1px solid ${color}40`,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|
}}>
|
|
<Icon style={{ width: '17px', height: '17px', color }} />
|
|
</div>
|
|
|
|
{/* Label + description */}
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{
|
|
fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '600',
|
|
color: active ? color : '#CBD5E1',
|
|
textTransform: 'uppercase', letterSpacing: '0.06em'
|
|
}}>
|
|
{label}
|
|
</div>
|
|
<div style={{ fontSize: '0.68rem', color: '#475569', marginTop: '1px' }}>
|
|
{description}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active indicator dot */}
|
|
{active && (
|
|
<div style={{
|
|
width: '6px', height: '6px', borderRadius: '50%',
|
|
background: color, boxShadow: `0 0 8px ${color}`, flexShrink: 0
|
|
}} />
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
|
|
{/* Admin panel link — visible only to Admin group */}
|
|
{isAdmin() && (() => {
|
|
const { id, label, icon: Icon, color, description } = ADMIN_ITEM;
|
|
const active = currentPage === id;
|
|
return (
|
|
<button
|
|
key={id}
|
|
onClick={() => { onNavigate(id); onClose(); }}
|
|
style={{
|
|
display: 'flex', alignItems: 'center', gap: '0.875rem',
|
|
padding: '0.75rem 0.875rem',
|
|
borderRadius: '0.5rem',
|
|
border: active ? `1px solid ${color}50` : '1px solid transparent',
|
|
background: active ? `${color}18` : 'transparent',
|
|
cursor: 'pointer', textAlign: 'left', width: '100%',
|
|
marginTop: '0.5rem',
|
|
borderTop: '1px solid rgba(255, 255, 255, 0.05)',
|
|
paddingTop: '1rem',
|
|
transition: 'background 0.15s, border-color 0.15s'
|
|
}}
|
|
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; }}
|
|
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
|
|
>
|
|
<div style={{
|
|
width: '36px', height: '36px', flexShrink: 0,
|
|
borderRadius: '0.375rem',
|
|
background: `${color}18`,
|
|
border: `1px solid ${color}40`,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|
}}>
|
|
<Icon style={{ width: '17px', height: '17px', color }} />
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{
|
|
fontFamily: 'monospace', fontSize: '0.8rem', fontWeight: '600',
|
|
color: active ? color : '#CBD5E1',
|
|
textTransform: 'uppercase', letterSpacing: '0.06em'
|
|
}}>
|
|
{label}
|
|
</div>
|
|
<div style={{ fontSize: '0.68rem', color: '#475569', marginTop: '1px' }}>
|
|
{description}
|
|
</div>
|
|
</div>
|
|
{active && (
|
|
<div style={{
|
|
width: '6px', height: '6px', borderRadius: '50%',
|
|
background: color, boxShadow: `0 0 8px ${color}`, flexShrink: 0
|
|
}} />
|
|
)}
|
|
</button>
|
|
);
|
|
})()}
|
|
</nav>
|
|
|
|
{/* Footer */}
|
|
<div style={{
|
|
marginTop: 'auto', paddingTop: '1rem',
|
|
borderTop: '1px solid rgba(255, 255, 255, 0.05)',
|
|
textAlign: 'center'
|
|
}}>
|
|
<div style={{ fontSize: '0.6rem', color: '#1E293B', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
|
NTS Threat Intelligence
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|