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.
300 lines
13 KiB
JavaScript
300 lines
13 KiB
JavaScript
// ReportingPage.jsx — full-page assembly using only RPT primitives.
|
||
// Mirrors frontend/src/components/pages/ReportingPage.js after the KB pass.
|
||
|
||
const { useState: useRPSt } = React;
|
||
const {
|
||
COLORS: RC,
|
||
PageHeader, RptButton, KbCard, PillTab, FilterChip, StatusBanner,
|
||
ToolbarLabel, SeverityDot, SlaPill, WorkflowBadge, DonutSample,
|
||
RptIcon: RI,
|
||
} = window.RPT;
|
||
|
||
/* Sample findings rows. Static — purely for layout. */
|
||
const SAMPLE_ROWS = [
|
||
{ id: 'F-10241', host: 'web-prod-04.steam.local', os: 'Ubuntu 22.04', sev: 'Critical', cve: 'CVE-2024-3094', age: 4, sla: 'OVERDUE', state: 'OPEN', action: 'Patch', owner: 'platform' },
|
||
{ id: 'F-10238', host: 'kafka-broker-2.steam.local',os: 'RHEL 9.3', sev: 'Critical', cve: 'CVE-2024-21626', age: 11, sla: 'OVERDUE', state: 'FP', action: 'Investigate',owner: 'data-eng' },
|
||
{ id: 'F-10202', host: 'auth-prod-01.steam.local', os: 'Ubuntu 22.04', sev: 'High', cve: 'CVE-2024-1086', age: 3, sla: 'AT_RISK', state: 'OPEN', action: 'Patch', owner: 'platform' },
|
||
{ id: 'F-10197', host: 'edge-cdn-09.steam.local', os: 'Alpine 3.19', sev: 'High', cve: 'CVE-2024-23222', age: 2, sla: 'AT_RISK', state: 'EXC', action: 'Accept', owner: 'edge' },
|
||
{ id: 'F-10185', host: 'analytics-w-3.steam.local',os: 'Ubuntu 20.04', sev: 'Medium', cve: 'CVE-2023-50387', age: 14, sla: 'WITHIN_SLA', state: 'OPEN', action: 'Mitigate', owner: 'analytics' },
|
||
{ id: 'F-10180', host: 'mail-relay-1.steam.local', os: 'Debian 12', sev: 'Medium', cve: 'CVE-2024-22195', age: 9, sla: 'WITHIN_SLA', state: 'REMEDIATED', action: 'Patch', owner: 'platform' },
|
||
{ id: 'F-10164', host: 'jumphost-2.steam.local', os: 'Ubuntu 22.04', sev: 'Low', cve: 'CVE-2023-45288', age: 22, sla: 'WITHIN_SLA', state: 'OPEN', action: 'Defer', owner: 'sre' },
|
||
];
|
||
|
||
/* Tiny anomaly bar chart placeholder for the trend section. */
|
||
function TrendChartPlaceholder() {
|
||
const data = [22, 28, 21, 24, 30, 27, 26, 25, 31, 38, 42, 45, 41, 36, 33];
|
||
const closed = [10, 14, 12, 13, 18, 20, 22, 21, 24, 26, 28, 30, 31, 30, 29];
|
||
const max = Math.max(...data);
|
||
return (
|
||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 120, padding: '4px 0' }}>
|
||
{data.map((d, i) => (
|
||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 2 }}>
|
||
<div style={{
|
||
height: `${(d / max) * 100}%`,
|
||
background: 'linear-gradient(180deg, rgba(14,165,233,0.85), rgba(14,165,233,0.45))',
|
||
borderRadius: '2px 2px 0 0',
|
||
}} />
|
||
<div style={{
|
||
height: `${(closed[i] / max) * 60}%`,
|
||
background: 'rgba(16,185,129,0.55)',
|
||
borderRadius: '0 0 2px 2px',
|
||
}} />
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ReportingPage() {
|
||
const [tab, setTab] = useRPSt('ivanti');
|
||
const [actionFilter, setActionFilter] = useRPSt(null);
|
||
|
||
/* Donut data (illustrative) */
|
||
const ivantiDonuts = [
|
||
{
|
||
label: 'Open vs Closed',
|
||
donut: <DonutSample
|
||
segments={[
|
||
{ label: 'Open', value: 184, color: RC.sky },
|
||
{ label: 'Closed', value: 712, color: RC.green },
|
||
]}
|
||
centerLabel="TOTAL" centerValue="896" />,
|
||
},
|
||
{
|
||
label: 'Action Coverage',
|
||
labelExtra: actionFilter && (
|
||
<span style={{ color: RC.amber, fontSize: 9 }}>● filtered</span>
|
||
),
|
||
donut: <DonutSample
|
||
segments={[
|
||
{ label: 'Patch', value: 96, color: RC.sky },
|
||
{ label: 'Mitigate', value: 42, color: RC.green },
|
||
{ label: 'Accept', value: 28, color: '#A78BFA' },
|
||
{ label: 'Investigate', value: 18, color: RC.amber },
|
||
]}
|
||
centerLabel="ASSIGNED" centerValue="184" />,
|
||
},
|
||
{
|
||
label: 'FP Finding Status',
|
||
donut: <DonutSample
|
||
segments={[
|
||
{ label: 'Pending', value: 14, color: RC.amber },
|
||
{ label: 'Approved', value: 31, color: RC.green },
|
||
{ label: 'Rejected', value: 6, color: RC.red },
|
||
]}
|
||
centerLabel="FINDINGS" centerValue="51" />,
|
||
},
|
||
{
|
||
label: 'FP Workflow Status',
|
||
donut: <DonutSample
|
||
segments={[
|
||
{ label: 'In Review', value: 8, color: RC.sky },
|
||
{ label: 'Closed', value: 22, color: RC.green },
|
||
{ label: 'Escalated', value: 4, color: RC.red },
|
||
]}
|
||
centerLabel="FP TICKETS" centerValue="34" />,
|
||
},
|
||
];
|
||
|
||
const atlasDonuts = [
|
||
{
|
||
label: 'Host Coverage',
|
||
donut: <DonutSample
|
||
segments={[
|
||
{ label: 'With Plans', value: 312, color: RC.green },
|
||
{ label: 'Without Plans', value: 88, color: RC.amber },
|
||
]}
|
||
centerLabel="HOSTS" centerValue="400" />,
|
||
},
|
||
{
|
||
label: 'Plan Types',
|
||
donut: <DonutSample
|
||
segments={[
|
||
{ label: 'Patch', value: 142, color: RC.sky },
|
||
{ label: 'Mitigate', value: 68, color: RC.green },
|
||
{ label: 'Accept', value: 31, color: '#A78BFA' },
|
||
]}
|
||
centerLabel="PLANS" centerValue="241" />,
|
||
},
|
||
{
|
||
label: 'Plan Status',
|
||
donut: <DonutSample
|
||
segments={[
|
||
{ label: 'Active', value: 184, color: RC.green },
|
||
{ label: 'Pending', value: 42, color: RC.amber },
|
||
{ label: 'Stalled', value: 15, color: RC.red },
|
||
]}
|
||
centerLabel="STATUS" centerValue="241" />,
|
||
},
|
||
];
|
||
|
||
const donuts = tab === 'ivanti' ? ivantiDonuts : atlasDonuts;
|
||
|
||
return (
|
||
<div style={{
|
||
display: 'flex', flexDirection: 'column', gap: 20,
|
||
padding: 24, maxWidth: 1280, margin: '0 auto',
|
||
}}>
|
||
{/* Page header */}
|
||
<PageHeader
|
||
title="Reporting"
|
||
meta={
|
||
<>
|
||
Last sync: 2 minutes ago
|
||
<span style={{ marginLeft: 10, color: '#334155' }}>· 184 of 896 findings</span>
|
||
<span style={{ marginLeft: 8, color: RC.amber }}>(3 filters active)</span>
|
||
</>
|
||
}
|
||
>
|
||
<RptButton variant="neutral" icon={<RI.Atlas size={13} />}>Atlas</RptButton>
|
||
<RptButton variant="primary" icon={<RI.Refresh size={13} />}>Sync</RptButton>
|
||
</PageHeader>
|
||
|
||
{/* Header-level error */}
|
||
<StatusBanner tone="error">Atlas: connection refused — retry in 30s</StatusBanner>
|
||
|
||
{/* Metrics tabs */}
|
||
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||
<RI.PieChart size={14} style={{ color: '#334155', marginRight: 4 }} />
|
||
<PillTab active={tab === 'ivanti'} onClick={() => setTab('ivanti')}>Ivanti Findings</PillTab>
|
||
<PillTab active={tab === 'atlas'} onClick={() => setTab('atlas')}>Atlas Coverage</PillTab>
|
||
</div>
|
||
|
||
{/* Donut grid */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
|
||
gap: 14,
|
||
}}>
|
||
{donuts.map((d) => (
|
||
<KbCard key={d.label} label={d.label} labelExtra={d.labelExtra}>
|
||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 170 }}>
|
||
{d.donut}
|
||
</div>
|
||
</KbCard>
|
||
))}
|
||
</div>
|
||
|
||
{/* Trend section */}
|
||
<KbCard label="Open vs Closed · last 30 days" labelExtra={
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, color: RC.amber, fontSize: 10 }}>
|
||
<RI.AlertTri size={11} /> spike detected day 12
|
||
</span>
|
||
}>
|
||
<TrendChartPlaceholder />
|
||
<div style={{ display: 'flex', gap: 14, fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-muted)', justifyContent: 'center' }}>
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||
<span style={{ width: 9, height: 9, background: RC.sky, borderRadius: 2 }} /> Open
|
||
</span>
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||
<span style={{ width: 9, height: 9, background: 'rgba(16,185,129,0.55)', borderRadius: 2 }} /> Closed
|
||
</span>
|
||
</div>
|
||
</KbCard>
|
||
|
||
{/* Findings table panel */}
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||
border: '1.5px solid rgba(14,165,233,0.12)',
|
||
borderRadius: 8, padding: 20,
|
||
}}>
|
||
{/* Toolbar */}
|
||
<div style={{
|
||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||
marginBottom: 12, paddingBottom: 10,
|
||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||
}}>
|
||
<ToolbarLabel count="184 of 896">Host Findings</ToolbarLabel>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
<RptButton variant="subtle" icon={<RI.Download size={12} />}>Export</RptButton>
|
||
<RptButton variant="subtle" icon={<RI.ListTodo size={12} />}>Queue</RptButton>
|
||
<RptButton variant="subtle" icon={<RI.Settings size={12} />}>Columns</RptButton>
|
||
<RptButton variant="subtle" icon={<RI.EyeOff size={12} />}>Rows</RptButton>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Search + filter chip row */}
|
||
<div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' }}>
|
||
<div style={{
|
||
position: 'relative', flex: '1 1 280px', maxWidth: 360,
|
||
}}>
|
||
<RI.Search size={13} style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-disabled)' }} />
|
||
<input
|
||
defaultValue="kafka"
|
||
placeholder="Search host, CVE, owner…"
|
||
style={{
|
||
width: '100%', padding: '8px 10px 8px 30px',
|
||
background: 'rgba(15,23,42,0.6)',
|
||
border: '1px solid rgba(14,165,233,0.18)',
|
||
borderRadius: 6,
|
||
color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: 12,
|
||
outline: 'none',
|
||
}}
|
||
/>
|
||
</div>
|
||
<FilterChip color={RC.amber}>Severity: Critical, High</FilterChip>
|
||
<FilterChip color={RC.sky}>Action: Patch</FilterChip>
|
||
<FilterChip color={RC.red}>SLA: Overdue</FilterChip>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<div style={{ overflow: 'auto', borderRadius: 6, border: '1px solid rgba(255,255,255,0.04)' }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-mono)', fontSize: 11 }}>
|
||
<thead>
|
||
<tr style={{ background: 'rgba(14,165,233,0.06)' }}>
|
||
{['ID', 'Host', 'OS', 'Severity', 'CVE', 'Age', 'SLA', 'State', 'Action', 'Owner'].map((h) => (
|
||
<th key={h} style={{
|
||
textAlign: 'left', padding: '8px 12px',
|
||
color: RC.sky, textTransform: 'uppercase', letterSpacing: '0.08em',
|
||
fontWeight: 700, fontSize: 10,
|
||
borderBottom: '1px solid rgba(14,165,233,0.18)',
|
||
}}>
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||
{h}
|
||
<RI.ChevronUpDn size={10} style={{ opacity: 0.5 }} />
|
||
</span>
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{SAMPLE_ROWS.map((r, i) => (
|
||
<tr key={r.id} style={{
|
||
background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.015)',
|
||
borderBottom: '1px solid rgba(255,255,255,0.03)',
|
||
}}>
|
||
<td style={{ padding: '10px 12px', color: RC.sky, fontWeight: 600 }}>{r.id}</td>
|
||
<td style={{ padding: '10px 12px', color: 'var(--fg-1)' }}>{r.host}</td>
|
||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.os}</td>
|
||
<td style={{ padding: '10px 12px' }}><SeverityDot level={r.sev} /></td>
|
||
<td style={{ padding: '10px 12px', color: 'var(--fg-2)' }}>{r.cve}</td>
|
||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.age}d</td>
|
||
<td style={{ padding: '10px 12px' }}><SlaPill status={r.sla} /></td>
|
||
<td style={{ padding: '10px 12px' }}><WorkflowBadge state={r.state} /></td>
|
||
<td style={{ padding: '10px 12px', color: 'var(--fg-2)' }}>{r.action}</td>
|
||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.owner}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination footer */}
|
||
<div style={{
|
||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||
paddingTop: 12, marginTop: 4,
|
||
fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)',
|
||
}}>
|
||
<span>Showing 1–{SAMPLE_ROWS.length} of 184</span>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
<RptButton variant="neutral">‹ Prev</RptButton>
|
||
<RptButton variant="neutral">Next ›</RptButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
window.RPT_PAGE = { ReportingPage };
|