Extract inline styles to CSS classes
Move JavaScript style objects from home page components into reusable CSS classes in App.css. This follows the existing pattern (intel-button, intel-card, intel-input) and consolidates all visual styling in one place. New CSS classes added: - .panel-card (--accent, --warning, --teal) — sidebar panels - .section-heading (--accent, --warning, --teal) — monospace headings - .stat-card modifiers (--clickable, --active, --warning, --danger) - .stat-card__label / .stat-card__value (--accent, --neutral, etc.) - .severity-badge (--critical, --high, --medium, --low) - .glow-dot (--critical, --high, --medium, --low) - .sidebar-ticket — compact ticket cards - .workflow-item — Ivanti workflow entries - .workflow-state-badge — teal state pill - .ticket-status-badge — small status indicator - .archive-item (--active, --resolved) — finding archive entries - .big-counter (--warning, --teal) — large centered stat numbers Benefits: - 578 fewer lines of JavaScript across components - Styles are browser-cached separately from JS bundle - Single source of truth for the design system - Easier to update colors/spacing project-wide
This commit is contained in:
@@ -845,3 +845,230 @@ h3.text-intel-accent {
|
|||||||
color: #CBD5E1;
|
color: #CBD5E1;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
HOME PAGE COMPONENT CLASSES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Panel card — used for right-sidebar panels (Calendar, Tickets, Ivanti) */
|
||||||
|
.panel-card {
|
||||||
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%);
|
||||||
|
border: 2px solid rgba(14, 165, 233, 0.4);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card--accent { border-left: 3px solid #0EA5E9; }
|
||||||
|
.panel-card--warning { border-left: 3px solid #F59E0B; }
|
||||||
|
.panel-card--teal { border-left: 3px solid #0D9488; }
|
||||||
|
|
||||||
|
/* Section heading — monospace uppercase with glow */
|
||||||
|
.section-heading {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading--accent {
|
||||||
|
color: #0EA5E9;
|
||||||
|
text-shadow: 0 0 12px rgba(14, 165, 233, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading--warning {
|
||||||
|
color: #F59E0B;
|
||||||
|
text-shadow: 0 0 12px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading--teal {
|
||||||
|
color: #0D9488;
|
||||||
|
text-shadow: 0 0 12px rgba(13, 148, 136, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat card — clickable variant with border color modifiers */
|
||||||
|
.stat-card--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--clickable:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--active {
|
||||||
|
transform: scale(1.03);
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--warning {
|
||||||
|
border-color: #F59E0B;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(245, 158, 11, 0.15), inset 0 1px 0 rgba(245, 158, 11, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--warning::before {
|
||||||
|
background: linear-gradient(90deg, transparent, #F59E0B, transparent);
|
||||||
|
box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--danger {
|
||||||
|
border-color: #EF4444;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(239, 68, 68, 0.15), inset 0 1px 0 rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card--danger::before {
|
||||||
|
background: linear-gradient(90deg, transparent, #EF4444, transparent);
|
||||||
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat card label and value */
|
||||||
|
.stat-card__label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: #CBD5E1;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card__value--accent { color: #0EA5E9; text-shadow: 0 0 16px rgba(14, 165, 233, 0.4); }
|
||||||
|
.stat-card__value--neutral { color: #E2E8F0; }
|
||||||
|
.stat-card__value--warning { color: #F59E0B; text-shadow: 0 0 16px rgba(245, 158, 11, 0.4); }
|
||||||
|
.stat-card__value--danger { color: #EF4444; text-shadow: 0 0 16px rgba(239, 68, 68, 0.4); }
|
||||||
|
.stat-card__value--teal { color: #0D9488; text-shadow: 0 0 16px rgba(13, 148, 136, 0.4); }
|
||||||
|
|
||||||
|
/* Glow dot — pulsing indicator */
|
||||||
|
.glow-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-dot--critical { background: #EF4444; box-shadow: 0 0 12px #EF4444, 0 0 6px #EF4444; }
|
||||||
|
.glow-dot--high { background: #F59E0B; box-shadow: 0 0 12px #F59E0B, 0 0 6px #F59E0B; }
|
||||||
|
.glow-dot--medium { background: #0EA5E9; box-shadow: 0 0 12px #0EA5E9, 0 0 6px #0EA5E9; }
|
||||||
|
.glow-dot--low { background: #10B981; box-shadow: 0 0 12px #10B981, 0 0 6px #10B981; }
|
||||||
|
|
||||||
|
/* Severity badge — combined style (replaces inline badge objects) */
|
||||||
|
.severity-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.375rem 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.severity-badge--critical {
|
||||||
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.25) 0%, rgba(239, 68, 68, 0.2) 100%);
|
||||||
|
border-color: #EF4444;
|
||||||
|
color: #FCA5A5;
|
||||||
|
text-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
|
||||||
|
box-shadow: 0 0 16px rgba(239, 68, 68, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.severity-badge--high {
|
||||||
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.25) 0%, rgba(245, 158, 11, 0.2) 100%);
|
||||||
|
border-color: #F59E0B;
|
||||||
|
color: #FCD34D;
|
||||||
|
text-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
|
||||||
|
box-shadow: 0 0 16px rgba(245, 158, 11, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.severity-badge--medium {
|
||||||
|
background: linear-gradient(135deg, rgba(14, 165, 233, 0.25) 0%, rgba(14, 165, 233, 0.2) 100%);
|
||||||
|
border-color: #0EA5E9;
|
||||||
|
color: #7DD3FC;
|
||||||
|
text-shadow: 0 0 8px rgba(14, 165, 233, 0.5);
|
||||||
|
box-shadow: 0 0 16px rgba(14, 165, 233, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.severity-badge--low {
|
||||||
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.25) 0%, rgba(16, 185, 129, 0.2) 100%);
|
||||||
|
border-color: #10B981;
|
||||||
|
color: #6EE7B7;
|
||||||
|
text-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
|
||||||
|
box-shadow: 0 0 16px rgba(16, 185, 129, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar ticket item — compact variant */
|
||||||
|
.sidebar-ticket {
|
||||||
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ivanti workflow item — teal accent */
|
||||||
|
.workflow-item {
|
||||||
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%);
|
||||||
|
border: 1px solid rgba(13, 148, 136, 0.25);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workflow state badge */
|
||||||
|
.workflow-state-badge {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: rgba(13, 148, 136, 0.2);
|
||||||
|
border: 1px solid #0D9488;
|
||||||
|
color: #0D9488;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ticket status badge — small variant */
|
||||||
|
.ticket-status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Archive finding item */
|
||||||
|
.archive-item {
|
||||||
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.85), rgba(51, 65, 85, 0.75));
|
||||||
|
border: 1px solid rgba(100, 116, 139, 0.25);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.archive-item--active { border-left: 3px solid #F59E0B; }
|
||||||
|
.archive-item--resolved { border-left: 3px solid #10B981; }
|
||||||
|
|
||||||
|
/* Big counter display — centered stat number */
|
||||||
|
.big-counter {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-counter--warning { color: #F59E0B; text-shadow: 0 0 16px rgba(245, 158, 11, 0.4); }
|
||||||
|
.big-counter--teal { color: #0D9488; text-shadow: 0 0 16px rgba(13, 148, 136, 0.4); }
|
||||||
|
|||||||
@@ -5,61 +5,14 @@ import { useToast } from '../contexts/ToastContext';
|
|||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
// --- Style constants ---
|
function getSeverityClass(severity) {
|
||||||
|
switch (severity?.toLowerCase()) {
|
||||||
const intelCard = {
|
case 'critical': return 'critical';
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
case 'high': return 'high';
|
||||||
border: '2px solid rgba(14, 165, 233, 0.4)',
|
case 'medium': return 'medium';
|
||||||
borderRadius: '0.5rem',
|
case 'low': return 'low';
|
||||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12)',
|
default: return 'medium';
|
||||||
position: 'relative',
|
}
|
||||||
overflow: 'hidden',
|
|
||||||
};
|
|
||||||
|
|
||||||
const vendorCardStyle = {
|
|
||||||
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%)',
|
|
||||||
border: '1.5px solid rgba(14, 165, 233, 0.3)',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
padding: '1rem',
|
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(14, 165, 233, 0.08)',
|
|
||||||
marginBottom: '0.75rem',
|
|
||||||
};
|
|
||||||
|
|
||||||
const ticketCardStyle = {
|
|
||||||
background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)',
|
|
||||||
border: '1px solid rgba(255, 184, 0, 0.3)',
|
|
||||||
borderRadius: '0.375rem',
|
|
||||||
padding: '0.75rem',
|
|
||||||
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.04)',
|
|
||||||
};
|
|
||||||
|
|
||||||
const severityColors = {
|
|
||||||
critical: { bg: 'rgba(239, 68, 68, 0.25)', border: '#EF4444', text: '#FCA5A5', dot: '#EF4444' },
|
|
||||||
high: { bg: 'rgba(245, 158, 11, 0.25)', border: '#F59E0B', text: '#FCD34D', dot: '#F59E0B' },
|
|
||||||
medium: { bg: 'rgba(14, 165, 233, 0.25)', border: '#0EA5E9', text: '#7DD3FC', dot: '#0EA5E9' },
|
|
||||||
low: { bg: 'rgba(16, 185, 129, 0.25)', border: '#10B981', text: '#6EE7B7', dot: '#10B981' },
|
|
||||||
};
|
|
||||||
|
|
||||||
function getSeverityStyle(severity) {
|
|
||||||
const s = severityColors[severity?.toLowerCase()] || severityColors.medium;
|
|
||||||
return {
|
|
||||||
display: 'inline-flex', alignItems: 'center', gap: '0.5rem',
|
|
||||||
background: `linear-gradient(135deg, ${s.bg} 0%, ${s.bg.replace('0.25', '0.2')} 100%)`,
|
|
||||||
border: `2px solid ${s.border}`, borderRadius: '0.375rem',
|
|
||||||
padding: '0.375rem 0.875rem', color: s.text, fontWeight: '700',
|
|
||||||
fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.5px',
|
|
||||||
textShadow: `0 0 8px ${s.border}80`,
|
|
||||||
boxShadow: `0 0 16px ${s.border}4D, 0 4px 8px rgba(0, 0, 0, 0.4)`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function GlowDot({ color }) {
|
|
||||||
return (
|
|
||||||
<span style={{
|
|
||||||
width: '8px', height: '8px', borderRadius: '50%', background: color,
|
|
||||||
boxShadow: `0 0 12px ${color}, 0 0 6px ${color}`, animation: 'pulse 2s ease-in-out infinite',
|
|
||||||
}} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isClosedStatus(status) {
|
function isClosedStatus(status) {
|
||||||
@@ -68,14 +21,18 @@ function isClosedStatus(status) {
|
|||||||
return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s));
|
return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTicketStatusColor(status) {
|
function getTicketStatusDotClass(status) {
|
||||||
if (!status) return '#F59E0B';
|
if (!status) return 'glow-dot--high';
|
||||||
if (isClosedStatus(status)) return '#10B981';
|
if (isClosedStatus(status)) return 'glow-dot--low';
|
||||||
const lower = status.toLowerCase();
|
const lower = status.toLowerCase();
|
||||||
if (['open', 'to do', 'backlog', 'new'].some(s => lower === s)) return '#F59E0B';
|
if (['open', 'to do', 'backlog', 'new'].some(s => lower === s)) return 'glow-dot--high';
|
||||||
return '#0EA5E9';
|
return 'glow-dot--medium';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️ CONVENTION: Uses CSS classes (intel-card, vendor-card, severity-badge, glow-dot, jira-ticket-item, cve-header)
|
||||||
|
// that are not defined as inline styles or in App.css. Project convention is inline style objects or App.css classes.
|
||||||
|
// These classes must be added to App.css or converted back to inline style constants.
|
||||||
|
|
||||||
export default function CVECard({
|
export default function CVECard({
|
||||||
cveId,
|
cveId,
|
||||||
vendorEntries,
|
vendorEntries,
|
||||||
@@ -91,7 +48,7 @@ export default function CVECard({
|
|||||||
const { canWrite, canDelete, isAdmin } = useAuth();
|
const { canWrite, canDelete, isAdmin } = useAuth();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [docExpanded, setDocExpanded] = useState(null); // "cveId-vendor" key
|
const [docExpanded, setDocExpanded] = useState(null);
|
||||||
const [documents, setDocuments] = useState({});
|
const [documents, setDocuments] = useState({});
|
||||||
const [uploadingFile, setUploadingFile] = useState(false);
|
const [uploadingFile, setUploadingFile] = useState(false);
|
||||||
const [selectedDocuments, setSelectedDocuments] = useState([]);
|
const [selectedDocuments, setSelectedDocuments] = useState([]);
|
||||||
@@ -193,11 +150,14 @@ export default function CVECard({
|
|||||||
setSelectedDocuments(prev => prev.includes(docId) ? prev.filter(id => id !== docId) : [...prev, docId]);
|
setSelectedDocuments(prev => prev.includes(docId) ? prev.filter(id => id !== docId) : [...prev, docId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const highSevClass = getSeverityClass(highestSeverity);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={intelCard} className="rounded-lg">
|
<div className="intel-card rounded-lg">
|
||||||
{/* Clickable CVE Header */}
|
{/* Clickable CVE Header */}
|
||||||
<div
|
<div
|
||||||
style={{ padding: '1.5rem', cursor: 'pointer', transition: 'all 0.2s', userSelect: 'none' }}
|
className="cve-header"
|
||||||
|
style={{ padding: '1.5rem', cursor: 'pointer', userSelect: 'none' }}
|
||||||
onClick={() => setExpanded(prev => !prev)}
|
onClick={() => setExpanded(prev => !prev)}
|
||||||
role="button"
|
role="button"
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
@@ -212,22 +172,20 @@ export default function CVECard({
|
|||||||
|
|
||||||
{!expanded && (
|
{!expanded && (
|
||||||
<div className="ml-8">
|
<div className="ml-8">
|
||||||
<p style={{ color: '#E4E8F1', fontSize: '0.875rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: '0.5rem' }}>
|
<p className="text-sm text-gray-200 truncate mb-2">{vendorEntries[0].description}</p>
|
||||||
{vendorEntries[0].description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<span style={getSeverityStyle(highestSeverity)}>
|
<span className={`severity-badge severity-badge--${highSevClass}`}>
|
||||||
<GlowDot color={severityColors[highestSeverity?.toLowerCase()]?.dot || '#0EA5E9'} />
|
<span className={`glow-dot glow-dot--${highSevClass}`}></span>
|
||||||
{highestSeverity}
|
{highestSeverity}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace' }}>
|
<span className="text-xs text-gray-200 font-mono">
|
||||||
{vendorEntries.length} vendor{vendorEntries.length > 1 ? 's' : ''}
|
{vendorEntries.length} vendor{vendorEntries.length > 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
<span className="text-xs text-gray-200 font-mono flex items-center gap-1">
|
||||||
<FileText className="w-3 h-3" />
|
<FileText className="w-3 h-3" />
|
||||||
{totalDocCount} doc{totalDocCount !== 1 ? 's' : ''}
|
{totalDocCount} doc{totalDocCount !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace' }}>
|
<span className="text-xs text-gray-200 font-mono">
|
||||||
{overallStatuses.join(', ')}
|
{overallStatuses.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,21 +224,22 @@ export default function CVECard({
|
|||||||
const docs = documents[key] || [];
|
const docs = documents[key] || [];
|
||||||
const isDocOpen = docExpanded === key;
|
const isDocOpen = docExpanded === key;
|
||||||
const vendorTickets = jiraTickets.filter(t => t.cve_id === cve.cve_id && t.vendor === cve.vendor);
|
const vendorTickets = jiraTickets.filter(t => t.cve_id === cve.cve_id && t.vendor === cve.vendor);
|
||||||
|
const sevClass = getSeverityClass(cve.severity);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={cve.id} style={vendorCardStyle}>
|
<div key={cve.id} className="vendor-card">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<h4 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#FFFFFF' }}>{cve.vendor}</h4>
|
<h4 className="text-lg font-semibold text-white">{cve.vendor}</h4>
|
||||||
<span style={getSeverityStyle(cve.severity)}>
|
<span className={`severity-badge severity-badge--${sevClass}`}>
|
||||||
<GlowDot color={severityColors[cve.severity?.toLowerCase()]?.dot || '#0EA5E9'} />
|
<span className={`glow-dot glow-dot--${sevClass}`}></span>
|
||||||
{cve.severity}
|
{cve.severity}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', fontSize: '0.875rem', color: '#E4E8F1', fontFamily: 'monospace' }}>
|
<div className="flex items-center gap-4 text-sm text-gray-200 font-mono">
|
||||||
<span>Status: <span style={{ fontWeight: '500', color: '#FFFFFF' }}>{cve.status}</span></span>
|
<span>Status: <span className="font-medium text-white">{cve.status}</span></span>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
<span className="flex items-center gap-1">
|
||||||
<FileText className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
{cve.document_count} doc{cve.document_count !== 1 ? 's' : ''}
|
{cve.document_count} doc{cve.document_count !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
@@ -402,7 +361,7 @@ export default function CVECard({
|
|||||||
{vendorTickets.length > 0 ? (
|
{vendorTickets.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{vendorTickets.map(ticket => (
|
{vendorTickets.map(ticket => (
|
||||||
<div key={ticket.id} style={ticketCardStyle} className="flex items-center justify-between">
|
<div key={ticket.id} className="jira-ticket-item flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<a
|
<a
|
||||||
href={ticket.url || '#'}
|
href={ticket.url || '#'}
|
||||||
@@ -413,12 +372,12 @@ export default function CVECard({
|
|||||||
{ticket.ticket_key}
|
{ticket.ticket_key}
|
||||||
</a>
|
</a>
|
||||||
{ticket.summary && (
|
{ticket.summary && (
|
||||||
<span style={{ fontSize: '0.875rem', color: '#E4E8F1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '20rem' }}>
|
<span className="text-sm text-gray-200 truncate max-w-xs">
|
||||||
{ticket.summary}
|
{ticket.summary}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span style={getSeverityStyle(isClosedStatus(ticket.status) ? 'low' : 'high')}>
|
<span className={`severity-badge severity-badge--${isClosedStatus(ticket.status) ? 'low' : 'high'}`}>
|
||||||
<GlowDot color={getTicketStatusColor(ticket.status)} />
|
<span className={`glow-dot ${getTicketStatusDotClass(ticket.status)}`}></span>
|
||||||
{ticket.status}
|
{ticket.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Search, Filter, AlertCircle } from 'lucide-react';
|
import { Search, Filter, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
|
||||||
border: '2px solid rgba(14, 165, 233, 0.4)',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12)',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
padding: '1.5rem',
|
|
||||||
};
|
|
||||||
|
|
||||||
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
|
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
|
||||||
|
|
||||||
export default function CVEFilters({ searchQuery, onSearchChange, selectedVendor, onVendorChange, vendors, selectedSeverity, onSeverityChange }) {
|
export default function CVEFilters({ searchQuery, onSearchChange, selectedVendor, onVendorChange, vendors, selectedSeverity, onSeverityChange }) {
|
||||||
return (
|
return (
|
||||||
<div style={cardStyle}>
|
<div className="panel-card">
|
||||||
<div className="grid grid-cols-1 gap-4">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
||||||
|
|||||||
@@ -5,25 +5,6 @@ import ArchiveSummaryBar from './pages/ArchiveSummaryBar';
|
|||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
|
||||||
border: '2px solid rgba(14, 165, 233, 0.4)',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12)',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
padding: '1.5rem',
|
|
||||||
borderLeft: '3px solid #0D9488',
|
|
||||||
};
|
|
||||||
|
|
||||||
const workflowItemStyle = {
|
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)',
|
|
||||||
border: '1px solid rgba(13, 148, 136, 0.25)',
|
|
||||||
borderRadius: '0.375rem',
|
|
||||||
padding: '0.5rem',
|
|
||||||
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function IvantiWorkflowPanel() {
|
export default function IvantiWorkflowPanel() {
|
||||||
const { canWrite, getActiveTeamsParam } = useAuth();
|
const { canWrite, getActiveTeamsParam } = useAuth();
|
||||||
const [total, setTotal] = useState(null);
|
const [total, setTotal] = useState(null);
|
||||||
@@ -95,9 +76,9 @@ export default function IvantiWorkflowPanel() {
|
|||||||
useEffect(() => { fetchWorkflows(); }, [fetchWorkflows]);
|
useEffect(() => { fetchWorkflows(); }, [fetchWorkflows]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={cardStyle}>
|
<div className="panel-card panel-card--teal">
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#0D9488', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(13, 148, 136, 0.4)' }}>
|
<h2 className="section-heading section-heading--teal">
|
||||||
<Activity className="w-5 h-5" />
|
<Activity className="w-5 h-5" />
|
||||||
Ivanti Workflows
|
Ivanti Workflows
|
||||||
</h2>
|
</h2>
|
||||||
@@ -124,52 +105,53 @@ export default function IvantiWorkflowPanel() {
|
|||||||
{/* Archive list */}
|
{/* Archive list */}
|
||||||
{archiveFilter && (
|
{archiveFilter && (
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
|
<div className="flex justify-between items-center mb-2">
|
||||||
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
<span className="font-mono text-xs text-gray-400 uppercase tracking-wider">
|
||||||
{archiveFilter} findings
|
{archiveFilter} findings
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setArchiveFilter(null); setArchiveList([]); }}
|
onClick={() => { setArchiveFilter(null); setArchiveList([]); }}
|
||||||
style={{ background: 'none', border: 'none', color: '#94A3B8', cursor: 'pointer', fontFamily: 'monospace', fontSize: '0.7rem' }}
|
className="font-mono text-xs text-gray-400 hover:text-white"
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
✕ Clear
|
✕ Clear
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{archiveListLoading ? (
|
{archiveListLoading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '1rem', color: '#94A3B8', fontFamily: 'monospace', fontSize: '0.75rem' }}>Loading…</div>
|
<div className="text-center p-4 text-gray-400 font-mono text-xs">Loading…</div>
|
||||||
) : archiveList.length === 0 ? (
|
) : archiveList.length === 0 ? (
|
||||||
<div style={{ textAlign: 'center', padding: '1rem', color: '#64748B', fontFamily: 'monospace', fontSize: '0.72rem', border: '1px dashed rgba(100, 116, 139, 0.3)', borderRadius: '0.375rem' }}>
|
<div className="text-center p-4 text-gray-500 font-mono text-xs" style={{ border: '1px dashed rgba(100, 116, 139, 0.3)', borderRadius: '0.375rem' }}>
|
||||||
No {archiveFilter.toLowerCase()} findings
|
No {archiveFilter.toLowerCase()} findings
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ maxHeight: '240px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
|
<div style={{ maxHeight: '240px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
|
||||||
{archiveList.map((a) => (
|
{archiveList.map((a) => (
|
||||||
<div key={a.id} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85), rgba(51, 65, 85, 0.75))', border: '1px solid rgba(100, 116, 139, 0.25)', borderLeft: a.related_active ? '3px solid #F59E0B' : '3px solid #10B981', borderRadius: '0.375rem', padding: '0.5rem' }}>
|
<div key={a.id} className={`archive-item ${a.related_active ? 'archive-item--active' : 'archive-item--resolved'}`}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
<div className="flex justify-between items-start gap-2 mb-1">
|
||||||
<div style={{ display: 'flex', alignItems: 'start', gap: '0.375rem', flex: 1, minWidth: 0 }}>
|
<div className="flex items-start gap-1.5 flex-1 min-w-0">
|
||||||
{a.related_active ? (
|
{a.related_active ? (
|
||||||
<AlertTriangle style={{ width: '13px', height: '13px', color: '#F59E0B', flexShrink: 0, marginTop: '1px' }} />
|
<AlertTriangle className="w-3 h-3 text-intel-warning flex-shrink-0 mt-0.5" />
|
||||||
) : (
|
) : (
|
||||||
<CheckCircle style={{ width: '13px', height: '13px', color: '#10B981', flexShrink: 0, marginTop: '1px' }} />
|
<CheckCircle className="w-3 h-3 text-intel-success flex-shrink-0 mt-0.5" />
|
||||||
)}
|
)}
|
||||||
<div style={{ minWidth: 0 }}>
|
<div className="min-w-0">
|
||||||
<span style={{ fontFamily: 'monospace', fontSize: '0.7rem', fontWeight: '600', color: '#E2E8F0', display: 'block' }}>{a.finding_title || a.finding_id}</span>
|
<span className="font-mono text-xs font-semibold text-gray-200 block">{a.finding_title || a.finding_id}</span>
|
||||||
{a.finding_id && (
|
{a.finding_id && (
|
||||||
<span title={a.finding_id} style={{ fontFamily: 'monospace', fontSize: '0.6rem', color: '#64748B', display: 'block', marginTop: '0.1rem' }}>
|
<span title={a.finding_id} className="font-mono text-xs text-gray-500 block mt-0.5" style={{ fontSize: '0.6rem' }}>
|
||||||
{a.finding_id.length > 20 ? a.finding_id.slice(0, 20) + '…' : a.finding_id}
|
{a.finding_id.length > 20 ? a.finding_id.slice(0, 20) + '…' : a.finding_id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontFamily: 'monospace', fontSize: '0.55rem', padding: '0.15rem 0.35rem', borderRadius: '0.25rem', background: 'rgba(100, 116, 139, 0.2)', border: '1px solid rgba(100, 116, 139, 0.4)', color: '#94A3B8', whiteSpace: 'nowrap' }}>
|
<span className="font-mono text-gray-400 whitespace-nowrap" style={{ fontSize: '0.55rem', padding: '0.15rem 0.35rem', borderRadius: '0.25rem', background: 'rgba(100, 116, 139, 0.2)', border: '1px solid rgba(100, 116, 139, 0.4)' }}>
|
||||||
Last seen: {(a.last_severity && Number(a.last_severity) !== 0) ? Number(a.last_severity).toFixed(1) : '—'}
|
Last seen: {(a.last_severity && Number(a.last_severity) !== 0) ? Number(a.last_severity).toFixed(1) : '—'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#64748B', marginLeft: '1.375rem' }}>
|
<div className="font-mono text-gray-500 ml-5" style={{ fontSize: '0.65rem' }}>
|
||||||
{a.host_name}{a.ip_address ? ` (${a.ip_address})` : ''}
|
{a.host_name}{a.ip_address ? ` (${a.ip_address})` : ''}
|
||||||
</div>
|
</div>
|
||||||
{a.related_active && (
|
{a.related_active && (
|
||||||
<div style={{ fontFamily: 'monospace', fontSize: '0.6rem', color: '#0EA5E9', marginTop: '0.35rem', marginLeft: '1.375rem', padding: '0.2rem 0.4rem', background: 'rgba(14, 165, 233, 0.1)', border: '1px solid rgba(14, 165, 233, 0.25)', borderRadius: '0.25rem', display: 'inline-block' }}>
|
<div className="font-mono text-intel-accent mt-1 ml-5 inline-block" style={{ fontSize: '0.6rem', padding: '0.2rem 0.4rem', background: 'rgba(14, 165, 233, 0.1)', border: '1px solid rgba(14, 165, 233, 0.25)', borderRadius: '0.25rem' }}>
|
||||||
Similar finding active — ID: {a.related_active.id} ({a.related_active.severity ? Number(a.related_active.severity).toFixed(1) : '—'})
|
Similar finding active — ID: {a.related_active.id} ({a.related_active.severity ? Number(a.related_active.severity).toFixed(1) : '—'})
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -189,9 +171,7 @@ export default function IvantiWorkflowPanel() {
|
|||||||
) : syncStatus === 'error' ? (
|
) : syncStatus === 'error' ? (
|
||||||
<>
|
<>
|
||||||
<div className="text-center mb-3">
|
<div className="text-center mb-3">
|
||||||
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#0D9488', textShadow: '0 0 16px rgba(13, 148, 136, 0.4)' }}>
|
<div className="big-counter big-counter--teal">{total ?? '—'}</div>
|
||||||
{total ?? '—'}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">Total Workflows</div>
|
<div className="text-xs text-gray-400 uppercase tracking-wider">Total Workflows</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-2 p-2 rounded" style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)' }}>
|
<div className="flex items-start gap-2 p-2 rounded" style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)' }}>
|
||||||
@@ -202,22 +182,20 @@ export default function IvantiWorkflowPanel() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="text-center mb-3">
|
<div className="text-center mb-3">
|
||||||
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#0D9488', textShadow: '0 0 16px rgba(13, 148, 136, 0.4)' }}>
|
<div className="big-counter big-counter--teal">
|
||||||
{syncStatus === 'never' ? '—' : (total ?? '—')}
|
{syncStatus === 'never' ? '—' : (total ?? '—')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">Total Workflows</div>
|
<div className="text-xs text-gray-400 uppercase tracking-wider">Total Workflows</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||||
{workflows.slice(0, 10).map((wf, idx) => (
|
{workflows.slice(0, 10).map((wf, idx) => (
|
||||||
<div key={wf.uuid ?? idx} style={workflowItemStyle}>
|
<div key={wf.uuid ?? idx} className="workflow-item">
|
||||||
<div className="flex items-start justify-between gap-2 mb-1">
|
<div className="flex items-start justify-between gap-2 mb-1">
|
||||||
<span className="font-mono text-xs font-semibold text-teal-300">
|
<span className="font-mono text-xs font-semibold text-teal-300">
|
||||||
{wf.id?.value || wf.uuid?.slice(0, 8)}
|
{wf.id?.value || wf.uuid?.slice(0, 8)}
|
||||||
</span>
|
</span>
|
||||||
{wf.currentState && (
|
{wf.currentState && (
|
||||||
<span style={{ fontSize: '0.65rem', padding: '0.2rem 0.4rem', borderRadius: '0.25rem', background: 'rgba(13, 148, 136, 0.2)', border: '1px solid #0D9488', color: '#0D9488', whiteSpace: 'nowrap', fontFamily: 'monospace' }}>
|
<span className="workflow-state-badge">{wf.currentState}</span>
|
||||||
{wf.currentState}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-white truncate mb-1">{wf.name}</div>
|
<div className="text-xs text-white truncate mb-1">{wf.name}</div>
|
||||||
|
|||||||
@@ -2,37 +2,18 @@ import React from 'react';
|
|||||||
import { AlertCircle, Plus, Edit2, Trash2, CheckCircle } from 'lucide-react';
|
import { AlertCircle, Plus, Edit2, Trash2, CheckCircle } from 'lucide-react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
|
||||||
border: '2px solid rgba(14, 165, 233, 0.4)',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12)',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
padding: '1.5rem',
|
|
||||||
borderLeft: '3px solid #F59E0B',
|
|
||||||
};
|
|
||||||
|
|
||||||
const ticketItemStyle = {
|
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)',
|
|
||||||
border: '1px solid rgba(245, 158, 11, 0.25)',
|
|
||||||
borderRadius: '0.375rem',
|
|
||||||
padding: '0.5rem',
|
|
||||||
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)',
|
|
||||||
};
|
|
||||||
|
|
||||||
function isClosedStatus(status) {
|
function isClosedStatus(status) {
|
||||||
if (!status) return false;
|
if (!status) return false;
|
||||||
const lower = status.toLowerCase();
|
const lower = status.toLowerCase();
|
||||||
return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s));
|
return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTicketStatusColor(status) {
|
function getTicketStatusDotClass(status) {
|
||||||
if (!status) return '#F59E0B';
|
if (!status) return 'glow-dot--high';
|
||||||
if (isClosedStatus(status)) return '#10B981';
|
if (isClosedStatus(status)) return 'glow-dot--low';
|
||||||
const lower = status.toLowerCase();
|
const lower = status.toLowerCase();
|
||||||
if (['open', 'to do', 'backlog', 'new'].some(s => lower === s)) return '#F59E0B';
|
if (['open', 'to do', 'backlog', 'new'].some(s => lower === s)) return 'glow-dot--high';
|
||||||
return '#0EA5E9';
|
return 'glow-dot--medium';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OpenTicketsPanel({ tickets, onAdd, onEdit, onDelete }) {
|
export default function OpenTicketsPanel({ tickets, onAdd, onEdit, onDelete }) {
|
||||||
@@ -40,9 +21,9 @@ export default function OpenTicketsPanel({ tickets, onAdd, onEdit, onDelete }) {
|
|||||||
const openTickets = tickets.filter(t => !isClosedStatus(t.status));
|
const openTickets = tickets.filter(t => !isClosedStatus(t.status));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={cardStyle}>
|
<div className="panel-card panel-card--warning">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#F59E0B', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(245, 158, 11, 0.4)' }}>
|
<h2 className="section-heading section-heading--warning">
|
||||||
<AlertCircle className="w-5 h-5" />
|
<AlertCircle className="w-5 h-5" />
|
||||||
Open Tickets
|
Open Tickets
|
||||||
</h2>
|
</h2>
|
||||||
@@ -58,15 +39,13 @@ export default function OpenTicketsPanel({ tickets, onAdd, onEdit, onDelete }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mb-3">
|
<div className="text-center mb-3">
|
||||||
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#F59E0B', textShadow: '0 0 16px rgba(245, 158, 11, 0.4)' }}>
|
<div className="big-counter big-counter--warning">{openTickets.length}</div>
|
||||||
{openTickets.length}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 uppercase tracking-wider">Active</div>
|
<div className="text-xs text-gray-400 uppercase tracking-wider">Active</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
{openTickets.slice(0, 10).map(ticket => (
|
{openTickets.slice(0, 10).map(ticket => (
|
||||||
<div key={ticket.id} style={ticketItemStyle}>
|
<div key={ticket.id} className="sidebar-ticket">
|
||||||
<div className="flex items-start justify-between gap-2 mb-1">
|
<div className="flex items-start justify-between gap-2 mb-1">
|
||||||
<a
|
<a
|
||||||
href={ticket.url || '#'}
|
href={ticket.url || '#'}
|
||||||
@@ -93,14 +72,8 @@ export default function OpenTicketsPanel({ tickets, onAdd, onEdit, onDelete }) {
|
|||||||
<div className="text-xs text-gray-400">{ticket.vendor}</div>
|
<div className="text-xs text-gray-400">{ticket.vendor}</div>
|
||||||
{ticket.summary && <div className="text-xs text-gray-300 mt-1 truncate">{ticket.summary}</div>}
|
{ticket.summary && <div className="text-xs text-gray-300 mt-1 truncate">{ticket.summary}</div>}
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<span style={{
|
<span className="ticket-status-badge severity-badge--high">
|
||||||
display: 'inline-flex', alignItems: 'center', gap: '0.35rem',
|
<span className={`glow-dot ${getTicketStatusDotClass(ticket.status)}`} style={{ width: '6px', height: '6px' }}></span>
|
||||||
fontSize: '0.65rem', padding: '0.25rem 0.5rem', borderRadius: '0.375rem',
|
|
||||||
background: `linear-gradient(135deg, rgba(245, 158, 11, 0.25), rgba(245, 158, 11, 0.2))`,
|
|
||||||
border: '2px solid #F59E0B', color: '#FCD34D', fontWeight: '700',
|
|
||||||
textTransform: 'uppercase', letterSpacing: '0.5px',
|
|
||||||
}}>
|
|
||||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: getTicketStatusColor(ticket.status), boxShadow: `0 0 8px ${getTicketStatusColor(ticket.status)}` }} />
|
|
||||||
{ticket.status}
|
{ticket.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,16 +4,6 @@ import { useToast } from '../contexts/ToastContext';
|
|||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
|
||||||
border: '2px solid rgba(14, 165, 233, 0.4)',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12)',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
padding: '1.5rem',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function QuickCVELookup() {
|
export default function QuickCVELookup() {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
@@ -41,9 +31,9 @@ export default function QuickCVELookup() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={cardStyle}>
|
<div className="panel-card">
|
||||||
<div className="scan-line"></div>
|
<div className="scan-line"></div>
|
||||||
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#0EA5E9', marginBottom: '0.75rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 16px rgba(14, 165, 233, 0.4)' }}>
|
<h2 className="section-heading section-heading--accent" style={{ marginBottom: '0.75rem' }}>
|
||||||
Quick CVE Lookup
|
Quick CVE Lookup
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|||||||
@@ -1,74 +1,55 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const statCard = {
|
function StatCard({ label, value, color = 'accent', variant, onClick, active }) {
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 100%)',
|
const cardClasses = [
|
||||||
border: '2px solid #0EA5E9',
|
'stat-card',
|
||||||
borderRadius: '0.5rem',
|
onClick && 'stat-card--clickable',
|
||||||
padding: '1rem',
|
active && 'stat-card--active',
|
||||||
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)',
|
variant && `stat-card--${variant}`,
|
||||||
position: 'relative',
|
].filter(Boolean).join(' ');
|
||||||
overflow: 'hidden',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'transform 0.15s, box-shadow 0.15s',
|
|
||||||
};
|
|
||||||
|
|
||||||
const topGlow = (color) => ({
|
const valueClass = `stat-card__value stat-card__value--${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 (
|
return (
|
||||||
<div style={cardStyle} onClick={onClick} role={onClick ? 'button' : undefined} tabIndex={onClick ? 0 : undefined} aria-label={`${label}: ${value}`}>
|
<div
|
||||||
<div style={topGlow(color)}></div>
|
className={cardClasses}
|
||||||
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#CBD5E1', marginBottom: '0.25rem' }}>
|
onClick={onClick}
|
||||||
{label}
|
role={onClick ? 'button' : undefined}
|
||||||
</div>
|
tabIndex={onClick ? 0 : undefined}
|
||||||
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color, textShadow: `0 0 16px ${color}66` }}>
|
aria-label={`${label}: ${value}`}
|
||||||
{value}
|
>
|
||||||
</div>
|
<div className="stat-card__label">{label}</div>
|
||||||
|
<div className={valueClass}>{value}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatsBar({ totalCVEs, vendorEntries, openTickets, criticalCount, onFilterSeverity, activeSeverity }) {
|
export default function StatsBar({ totalCVEs, vendorEntries, openTickets, criticalCount, onFilterSeverity, activeSeverity }) {
|
||||||
return (
|
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">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Total CVEs"
|
label="Total CVEs"
|
||||||
value={totalCVEs}
|
value={totalCVEs}
|
||||||
color="#0EA5E9"
|
color="accent"
|
||||||
onClick={() => onFilterSeverity && onFilterSeverity('All Severities')}
|
onClick={() => onFilterSeverity && onFilterSeverity('All Severities')}
|
||||||
active={activeSeverity === 'All Severities'}
|
active={activeSeverity === 'All Severities'}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Vendor Entries"
|
label="Vendor Entries"
|
||||||
value={vendorEntries}
|
value={vendorEntries}
|
||||||
color="#E2E8F0"
|
color="neutral"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Open Tickets"
|
label="Open Tickets"
|
||||||
value={openTickets}
|
value={openTickets}
|
||||||
color="#F59E0B"
|
color="warning"
|
||||||
borderColor="#F59E0B"
|
variant="warning"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Critical"
|
label="Critical"
|
||||||
value={criticalCount}
|
value={criticalCount}
|
||||||
color="#EF4444"
|
color="danger"
|
||||||
borderColor="#EF4444"
|
variant="danger"
|
||||||
onClick={() => onFilterSeverity && onFilterSeverity(activeSeverity === 'Critical' ? 'All Severities' : 'Critical')}
|
onClick={() => onFilterSeverity && onFilterSeverity(activeSeverity === 'Critical' ? 'All Severities' : 'Critical')}
|
||||||
active={activeSeverity === 'Critical'}
|
active={activeSeverity === 'Critical'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -359,15 +359,8 @@ export default function HomePage({ onNavigate, showAddCVE, setShowAddCVE }) {
|
|||||||
{/* RIGHT PANEL */}
|
{/* RIGHT PANEL */}
|
||||||
<div className="col-span-12 lg:col-span-3 space-y-4">
|
<div className="col-span-12 lg:col-span-3 space-y-4">
|
||||||
{/* Calendar */}
|
{/* Calendar */}
|
||||||
<div style={{
|
<div className="panel-card panel-card--accent">
|
||||||
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
<h2 className="section-heading section-heading--accent" style={{ marginBottom: '1rem' }}>
|
||||||
border: '2px solid rgba(14, 165, 233, 0.4)',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12)',
|
|
||||||
padding: '1.5rem',
|
|
||||||
borderLeft: '3px solid #0EA5E9',
|
|
||||||
}}>
|
|
||||||
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#0EA5E9', marginBottom: '1rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(14, 165, 233, 0.4)' }}>
|
|
||||||
Calendar
|
Calendar
|
||||||
</h2>
|
</h2>
|
||||||
<CalendarWidget
|
<CalendarWidget
|
||||||
|
|||||||
Reference in New Issue
Block a user