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

78 lines
2.7 KiB
JavaScript
Raw Normal View History

Refactor home page: extract components, add toast system, debounce search Major restructuring of the monolithic App.js (2484 lines) into focused, testable components: Architecture: - App.js is now a 189-line routing shell (header, nav, page switching) - HomePage.js orchestrates all home page state and layout - Each visual section is its own component with clear props API Extracted components: - StatsBar: clickable stat cards that filter by severity - QuickCVELookup: CVE existence check with inline results - CVEFilters: search + vendor/severity dropdowns - CVECard: expandable CVE with vendor entries, docs, tickets - OpenTicketsPanel: right sidebar open JIRA tickets - IvantiWorkflowPanel: right sidebar Ivanti workflow status + archive Extracted modals: - AddCVEModal: self-contained add form with NVD auto-fill - EditCVEModal: self-contained edit form with NVD update - JiraTicketModal: unified add/edit JIRA ticket modal - ArcherTicketModal: unified add/edit Archer ticket modal Performance optimizations: - Debounced search (300ms) via useDebounce hook — eliminates redundant API calls on every keystroke - Memoized groupedCVEs, openTicketCount, criticalCount via useMemo - Proper state updates (no direct mutation of cveDocuments) - useCallback on fetch functions to stabilize effect dependencies UX improvements: - Toast notification system replaces all alert() calls - Stat cards are now clickable to filter CVE list by severity - onKeyDown replaces deprecated onKeyPress - aria-labels added to interactive elements Infrastructure: - ToastContext with auto-dismiss, typed toasts (success/error/warning/info) - useDebounce custom hook for reuse across the app - Toast slide-in animation in App.css
2026-06-23 11:46:39 -06:00
import React from 'react';
const statCard = {
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 100%)',
border: '2px solid #0EA5E9',
borderRadius: '0.5rem',
padding: '1rem',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.15)',
position: 'relative',
overflow: 'hidden',
cursor: 'pointer',
transition: 'transform 0.15s, box-shadow 0.15s',
};
const topGlow = (color) => ({
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '2px',
background: `linear-gradient(90deg, transparent, ${color}, transparent)`,
boxShadow: `0 0 8px ${color}80`,
});
function StatCard({ label, value, color = '#0EA5E9', borderColor, onClick, active }) {
const cardStyle = {
...statCard,
...(borderColor ? { border: `2px solid ${borderColor}`, boxShadow: `0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px ${borderColor}26, inset 0 1px 0 ${borderColor}26` } : {}),
...(active ? { transform: 'scale(1.03)', boxShadow: `0 4px 24px rgba(0, 0, 0, 0.6), 0 0 28px ${color}40` } : {}),
};
return (
<div style={cardStyle} onClick={onClick} role={onClick ? 'button' : undefined} tabIndex={onClick ? 0 : undefined} aria-label={`${label}: ${value}`}>
<div style={topGlow(color)}></div>
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#CBD5E1', marginBottom: '0.25rem' }}>
{label}
</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color, textShadow: `0 0 16px ${color}66` }}>
{value}
</div>
</div>
);
}
export default function StatsBar({ totalCVEs, vendorEntries, openTickets, criticalCount, onFilterSeverity, activeSeverity }) {
return (
// ⚠️ CONVENTION: Use inline styles or App.css classes instead of Tailwind utility classes (grid grid-cols-1 md:grid-cols-4 gap-4)
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard
label="Total CVEs"
value={totalCVEs}
color="#0EA5E9"
onClick={() => onFilterSeverity && onFilterSeverity('All Severities')}
active={activeSeverity === 'All Severities'}
/>
<StatCard
label="Vendor Entries"
value={vendorEntries}
color="#E2E8F0"
/>
<StatCard
label="Open Tickets"
value={openTickets}
color="#F59E0B"
borderColor="#F59E0B"
/>
<StatCard
label="Critical"
value={criticalCount}
color="#EF4444"
borderColor="#EF4444"
onClick={() => onFilterSeverity && onFilterSeverity(activeSeverity === 'Critical' ? 'All Severities' : 'Critical')}
active={activeSeverity === 'Critical'}
/>
</div>
);
}