Files
cve-dashboard/docs/design-system-redesign/ui_kits/home/HomePage.jsx
root 27192dd69f WIP: Dashboard redesign — design system overhaul and component updates
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.
2026-04-29 14:20:23 +00:00

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 };