Files

300 lines
13 KiB
React
Raw Permalink Normal View History

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