Frontend redesign in progress: updated styles, layout, and components across all pages to align with new design system. Includes Jira API compliance specs, property tests, and load test script.
372 lines
18 KiB
JavaScript
372 lines
18 KiB
JavaScript
// HomePage.jsx — full-page assembly of the CVE Dashboard Home view.
|
|
// Rebuilt 1:1 from frontend/src/App.js (currentPage === 'home').
|
|
//
|
|
// Layout: top stat row (4 metric cards) → 12-col grid below
|
|
// • col-span-9 (left): Quick CVE Lookup → Search/Filter → CVE list
|
|
// • col-span-3 (right): Calendar → Open Tickets → Archer → Ivanti
|
|
|
|
const {
|
|
COLORS: HC, StatCard, HomeCard, CardTitle, HomeButton, SeverityBadge, StatusBadge,
|
|
HomeInput, HomeSelect, FieldLabel, ResultBanner,
|
|
BigStat, MiniTicket, CVERow, VendorEntry,
|
|
HomeIcon: HI, CalendarMini, ArchiveSummary, ScrollList, EmptyState,
|
|
withAlpha: hAlpha,
|
|
} = window.HOME;
|
|
|
|
const { useState: useHomePageState } = React;
|
|
|
|
/* ── Sample data — close to what App.js renders against ──────── */
|
|
const SAMPLE_CVES = [
|
|
{
|
|
id: 'CVE-2025-1014',
|
|
severity: 'Critical',
|
|
description: 'Heap-based buffer overflow in the libnetfilter_queue user-space packet handler permits a remote attacker to execute arbitrary code via crafted ICMP traffic.',
|
|
statuses: ['Open', 'In Progress'],
|
|
vendors: [
|
|
{ vendor: 'Red Hat', severity: 'Critical', status: 'Open', docCount: 4 },
|
|
{ vendor: 'Ubuntu', severity: 'Critical', status: 'In Progress', docCount: 2 },
|
|
{ vendor: 'SUSE', severity: 'High', status: 'Resolved', docCount: 3 },
|
|
],
|
|
tickets: [
|
|
{ key: 'SEC-4821', summary: 'Patch netfilter on prod ingress fleet', status: 'In Progress' },
|
|
],
|
|
},
|
|
{
|
|
id: 'CVE-2025-0944',
|
|
severity: 'High',
|
|
description: 'Authentication bypass in admin console allows unauthenticated access to telemetry exports.',
|
|
statuses: ['Addressed'],
|
|
vendors: [
|
|
{ vendor: 'Cisco', severity: 'High', status: 'Addressed', docCount: 2 },
|
|
],
|
|
},
|
|
{
|
|
id: 'CVE-2024-9912',
|
|
severity: 'Medium',
|
|
description: 'Improper cert validation in the JIRA Server REST client could lead to MITM under attacker-controlled DNS.',
|
|
statuses: ['Resolved'],
|
|
vendors: [
|
|
{ vendor: 'Atlassian', severity: 'Medium', status: 'Resolved', docCount: 1 },
|
|
],
|
|
},
|
|
];
|
|
|
|
const SAMPLE_OPEN_TICKETS = [
|
|
{ key: 'SEC-4821', cveId: 'CVE-2025-1014', vendor: 'Red Hat', status: 'In Progress', summary: 'Patch netfilter ingress' },
|
|
{ key: 'SEC-4794', cveId: 'CVE-2025-0944', vendor: 'Cisco', status: 'Open', summary: 'Roll admin-console hotfix' },
|
|
{ key: 'SEC-4760', cveId: 'CVE-2024-9912', vendor: 'Atlassian', status: 'Open', summary: 'Validate cert chain' },
|
|
];
|
|
|
|
const SAMPLE_ARCHER = [
|
|
{ key: 'EXC-08291', cveId: 'CVE-2025-1014', vendor: 'SUSE', status: 'Pending Review' },
|
|
{ key: 'EXC-08214', cveId: 'CVE-2024-9912', vendor: 'Adobe', status: 'Draft' },
|
|
];
|
|
|
|
const SAMPLE_IVANTI = [
|
|
{ id: 'WF-1042', name: 'Quarterly compliance scan', state: 'In Review', type: 'compliance audit', when: 'Apr 24' },
|
|
{ id: 'WF-1038', name: 'Endpoint patch rollout — Linux fleet', state: 'In Progress', type: 'patch deploy', when: 'Apr 22' },
|
|
{ id: 'WF-1034', name: 'Identity provider rotation', state: 'Approved', type: 'access change', when: 'Apr 21' },
|
|
];
|
|
|
|
const ARCHIVE_SUMMARY = [
|
|
{ label: 'In Review', count: 12, tone: 'amber' },
|
|
{ label: 'In Progress', count: 8, tone: 'sky' },
|
|
{ label: 'Approved', count: 17, tone: 'green' },
|
|
{ label: 'Closed', count: 41, tone: 'neutral' },
|
|
];
|
|
|
|
/* ── Page ────────────────────────────────────────────────────── */
|
|
function HomePage() {
|
|
const [expanded, setExpanded] = useHomePageState(SAMPLE_CVES[0].id);
|
|
const [scanResult, setScanResult] = useHomePageState({ tone: 'success', text: 'CVE-2025-1014 addressed (3 vendors)' });
|
|
const [search, setSearch] = useHomePageState('');
|
|
const [vendor, setVendor] = useHomePageState('All Vendors');
|
|
const [severity, setSeverity] = useHomePageState('All Severities');
|
|
|
|
return (
|
|
<div data-screen-label="01 Home" style={{
|
|
padding: 32, minHeight: '100vh', background: 'var(--bg-page)',
|
|
fontFamily: 'var(--font-display)',
|
|
}}>
|
|
{/* ── Top: 4-up stats ── */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
|
<StatCard label="Total CVEs" value="247" tone="sky" />
|
|
<StatCard label="Vendor Entries" value="412" tone="neutral" />
|
|
<StatCard label="Open Tickets" value="18" tone="amber" />
|
|
<StatCard label="Critical" value="6" tone="red" />
|
|
</div>
|
|
|
|
{/* ── 12-col body ── */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: 24 }}>
|
|
|
|
{/* LEFT (col-span-9) */}
|
|
<div style={{ gridColumn: 'span 9', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
|
|
{/* Quick CVE Lookup */}
|
|
<HomeCard>
|
|
<CardTitle color={HC.sky} icon="search">Quick CVE Lookup</CardTitle>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<HomeInput placeholder="Enter CVE ID (e.g., CVE-2025-1014)" />
|
|
<HomeButton variant="primary" icon="search" onClick={() => setScanResult({ tone: 'success', text: 'CVE-2025-1014 addressed (3 vendors)' })}>
|
|
Scan
|
|
</HomeButton>
|
|
</div>
|
|
{scanResult && (
|
|
<div style={{ marginTop: 16 }}>
|
|
<ResultBanner tone={scanResult.tone} title={scanResult.text}>
|
|
<div style={{ display: 'grid', gap: 10, marginTop: 8 }}>
|
|
{SAMPLE_CVES[0].vendors.map(v => (
|
|
<div key={v.vendor} style={{
|
|
padding: 12, background: 'rgba(15,23,42,0.7)',
|
|
border: '1px solid rgba(14,165,233,0.30)', borderRadius: 6,
|
|
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
|
}}>
|
|
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 600, color: 'var(--fg-1)', marginBottom: 6 }}>{v.vendor}</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>
|
|
<span><strong style={{ color: 'var(--fg-1)' }}>Sev:</strong> {v.severity}</span>
|
|
<span><strong style={{ color: 'var(--fg-1)' }}>Status:</strong> {v.status}</span>
|
|
<span><strong style={{ color: 'var(--fg-1)' }}>Docs:</strong> {v.docCount}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ResultBanner>
|
|
</div>
|
|
)}
|
|
</HomeCard>
|
|
|
|
{/* Search + Filter */}
|
|
<HomeCard>
|
|
<div style={{ display: 'grid', gap: 16 }}>
|
|
<div>
|
|
<FieldLabel icon="search">Search CVEs</FieldLabel>
|
|
<HomeInput value={search} onChange={e => setSearch(e.target.value)} placeholder="CVE ID or description…" />
|
|
</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
|
<div>
|
|
<FieldLabel icon="filter">Vendor</FieldLabel>
|
|
<HomeSelect value={vendor} onChange={e => setVendor(e.target.value)} options={['All Vendors', 'Red Hat', 'Cisco', 'Ubuntu', 'SUSE', 'Atlassian', 'Adobe']} />
|
|
</div>
|
|
<div>
|
|
<FieldLabel icon="alert">Severity</FieldLabel>
|
|
<HomeSelect value={severity} onChange={e => setSeverity(e.target.value)} options={['All Severities', 'Critical', 'High', 'Medium', 'Low']} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</HomeCard>
|
|
|
|
{/* Results summary */}
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<p style={{ margin: 0, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
|
|
<strong style={{ color: HC.sky, fontWeight: 700 }}>{SAMPLE_CVES.length}</strong> CVEs
|
|
<span style={{ color: 'var(--fg-disabled)', margin: '0 8px' }}>•</span>
|
|
<span style={{ color: 'var(--fg-1)' }}>{SAMPLE_CVES.reduce((n, c) => n + c.vendors.length, 0)}</span> vendor entries
|
|
</p>
|
|
<HomeButton variant="primary" icon="download">Export 2 Docs</HomeButton>
|
|
</div>
|
|
|
|
{/* CVE list */}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
{SAMPLE_CVES.map(cve => (
|
|
<CVERow
|
|
key={cve.id}
|
|
cveId={cve.id}
|
|
severity={cve.severity}
|
|
description={cve.description}
|
|
vendorCount={cve.vendors.length}
|
|
docCount={cve.vendors.reduce((s, v) => s + v.docCount, 0)}
|
|
statuses={cve.statuses}
|
|
expanded={expanded === cve.id}
|
|
onToggle={() => setExpanded(expanded === cve.id ? null : cve.id)}
|
|
>
|
|
{/* meta row */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
|
|
<span>Published: 2025-03-12</span>
|
|
<span style={{ color: HC.sky }}>•</span>
|
|
<span>{cve.vendors.length} affected vendor{cve.vendors.length !== 1 ? 's' : ''}</span>
|
|
{cve.vendors.length >= 2 && (
|
|
<HomeButton variant="danger" icon="trash" size="sm" style={{ marginLeft: 8 }}>Delete All</HomeButton>
|
|
)}
|
|
</div>
|
|
{/* vendor sub-cards */}
|
|
{cve.vendors.map((v, i) => (
|
|
<VendorEntry
|
|
key={`${cve.id}-${v.vendor}`}
|
|
vendor={v.vendor}
|
|
severity={v.severity}
|
|
status={v.status}
|
|
docCount={v.docCount}
|
|
onView={() => {}}
|
|
onEdit={() => {}}
|
|
onDelete={() => {}}
|
|
>
|
|
{/* For the first vendor of the first CVE, demonstrate the doc + ticket inset */}
|
|
{i === 0 && cve.id === SAMPLE_CVES[0].id && (
|
|
<>
|
|
<DocInset />
|
|
{cve.tickets && <TicketInset tickets={cve.tickets} />}
|
|
</>
|
|
)}
|
|
</VendorEntry>
|
|
))}
|
|
</CVERow>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* RIGHT (col-span-3) */}
|
|
<div style={{ gridColumn: 'span 3', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
{/* Calendar */}
|
|
<HomeCard padding={20} leftRail={HC.sky}>
|
|
<CardTitle color={HC.sky} icon="calendar">Calendar</CardTitle>
|
|
<CalendarMini today={26} markedDays={{ 14: 'red', 18: 'amber', 22: 'sky', 24: 'amber' }} />
|
|
</HomeCard>
|
|
|
|
{/* Open Tickets */}
|
|
<HomeCard padding={20} leftRail={HC.amber}>
|
|
<CardTitle
|
|
color={HC.amber}
|
|
icon="alert"
|
|
action={<HomeButton variant="warning" icon="plus" size="sm" />}
|
|
>Open Tickets</CardTitle>
|
|
<BigStat value={SAMPLE_OPEN_TICKETS.length} label="Active" color={HC.amber} />
|
|
<ScrollList maxHeight={280}>
|
|
{SAMPLE_OPEN_TICKETS.map(t => (
|
|
<MiniTicket key={t.key} keyText={t.key} cveId={t.cveId} vendor={t.vendor} summary={t.summary} status={t.status} tone="amber" onEdit={() => {}} onDelete={() => {}} />
|
|
))}
|
|
</ScrollList>
|
|
</HomeCard>
|
|
|
|
{/* Archer Risk */}
|
|
<HomeCard padding={20} leftRail={HC.purple}>
|
|
<CardTitle
|
|
color={HC.purple}
|
|
icon="shield"
|
|
action={<button style={{ background: hAlpha(HC.purple, 0.18), border: `1px solid ${HC.purple}`, color: HC.purple, padding: '4px 8px', borderRadius: 4, fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }}><HI name="plus" size={12} color={HC.purple} /></button>}
|
|
>Archer Risk Tickets</CardTitle>
|
|
<BigStat value={SAMPLE_ARCHER.length} label="Active" color={HC.purple} />
|
|
<ScrollList maxHeight={220}>
|
|
{SAMPLE_ARCHER.map(t => (
|
|
<MiniTicket key={t.key} keyText={t.key} cveId={t.cveId} vendor={t.vendor} status={t.status} tone="purple" onEdit={() => {}} onDelete={() => {}} />
|
|
))}
|
|
</ScrollList>
|
|
</HomeCard>
|
|
|
|
{/* Ivanti Workflows */}
|
|
<HomeCard padding={20} leftRail={HC.teal}>
|
|
<CardTitle
|
|
color={HC.teal}
|
|
icon="activity"
|
|
action={<button style={{ background: hAlpha(HC.teal, 0.18), border: `1px solid ${HC.teal}`, color: HC.teal, padding: '4px 8px', borderRadius: 4, fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }}><HI name="refresh" size={12} color={HC.teal} /> Sync</button>}
|
|
>Ivanti Workflows</CardTitle>
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)', marginBottom: 12 }}>
|
|
Synced Apr 26 · 9:42 AM
|
|
</div>
|
|
<ArchiveSummary items={ARCHIVE_SUMMARY} />
|
|
<BigStat value="78" label="Total Workflows" color={HC.teal} />
|
|
<ScrollList maxHeight={240}>
|
|
{SAMPLE_IVANTI.map(wf => (
|
|
<div key={wf.id} style={{
|
|
background: 'linear-gradient(135deg, rgba(30,41,59,0.85), rgba(51,65,85,0.75))',
|
|
border: `1px solid ${hAlpha(HC.teal, 0.25)}`, borderRadius: 6,
|
|
padding: 10,
|
|
boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03)',
|
|
}}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 6, marginBottom: 4 }}>
|
|
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: '#5EEAD4' }}>{wf.id}</span>
|
|
<StatusBadge tone="teal" size="sm">{wf.state}</StatusBadge>
|
|
</div>
|
|
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: 4 }}>{wf.name}</div>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)' }}>
|
|
<span>{wf.type}</span>
|
|
<span style={{ color: 'var(--fg-disabled)' }}>{wf.when}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</ScrollList>
|
|
</HomeCard>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ── Insets used inside the first VendorEntry ────────────────── */
|
|
function DocInset() {
|
|
return (
|
|
<div>
|
|
<h5 style={{
|
|
margin: '0 0 12px 0', display: 'flex', alignItems: 'center', gap: 6,
|
|
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
|
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
|
}}>
|
|
<HI name="doc" size={13} color={HC.sky} />
|
|
Documents (4)
|
|
</h5>
|
|
<div style={{ display: 'grid', gap: 8 }}>
|
|
{[
|
|
{ name: 'rh-advisory-2025-1014.pdf', meta: 'advisory · 220 KB' },
|
|
{ name: 'patch-notes-rhel9.pdf', meta: 'patch · 85 KB · approved by sec-eng' },
|
|
].map(d => (
|
|
<div key={d.name} style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
padding: '10px 12px', borderRadius: 4,
|
|
background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(14,165,233,0.15)',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0 }}>
|
|
<input type="checkbox" style={{ accentColor: HC.sky }} />
|
|
<HI name="doc" size={16} color={HC.sky} />
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-1)', fontWeight: 500 }}>{d.name}</div>
|
|
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)' }}>{d.meta}</div>
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
<HomeButton variant="neutral" size="sm">View</HomeButton>
|
|
<HomeButton variant="danger" size="sm">Del</HomeButton>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<HomeButton variant="neutral" icon="upload" size="sm" style={{ marginTop: 12 }}>Upload Doc</HomeButton>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TicketInset({ tickets }) {
|
|
return (
|
|
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid rgba(245,158,11,0.30)' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
|
<h5 style={{
|
|
margin: 0, display: 'flex', alignItems: 'center', gap: 6,
|
|
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
|
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
|
}}>
|
|
<HI name="alert" size={13} color={HC.amber} />
|
|
JIRA Tickets ({tickets.length})
|
|
</h5>
|
|
<HomeButton variant="primary" icon="plus" size="sm">Add Ticket</HomeButton>
|
|
</div>
|
|
<div style={{ display: 'grid', gap: 8 }}>
|
|
{tickets.map(t => (
|
|
<div key={t.key} style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
padding: '10px 12px', borderRadius: 6,
|
|
background: 'linear-gradient(135deg, rgba(19,25,55,0.85), rgba(30,39,73,0.75))',
|
|
border: '1px solid rgba(255,184,0,0.30)',
|
|
boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.04)',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, minWidth: 0 }}>
|
|
<a href="#" onClick={e => e.preventDefault()} style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600, color: HC.sky, textDecoration: 'none' }}>{t.key}</a>
|
|
<span style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.summary}</span>
|
|
<StatusBadge tone="amber" size="sm">{t.status}</StatusBadge>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
window.HOME_PAGE = { HomePage };
|