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.
320 lines
13 KiB
JavaScript
320 lines
13 KiB
JavaScript
// CompliancePage.jsx — full-page assembly of the AEO Compliance view.
|
||
// Rebuilt from frontend/src/components/pages/CompliancePage.js with
|
||
// inline-rendered chart placeholders that match Recharts visually.
|
||
|
||
const {
|
||
COLORS: PC, statusColor: pStatusColor, pctDisplay: pPct, cAlpha: pAlpha,
|
||
CompPageHeader, CompButton, TeamTabs,
|
||
MetricHealthCard, DeviceTable, DeviceTableToolbar, DeviceTableHeader, DeviceRow, CompEmpty,
|
||
ChartCard, ChartLegend, RollbackDialog, RollbackToast, CompIcon: PIcon,
|
||
} = window.COMP;
|
||
|
||
const { useState: useCompPageState } = React;
|
||
|
||
/* ── Sample data — what summary + items endpoints look like ── */
|
||
const SAMPLE_FAMILIES = [
|
||
{
|
||
metricId: 'VM-CRITICAL', category: 'Vulnerability Management', target: 0.95, worstStatus: 'Below 15% of Target',
|
||
entries: [
|
||
{ metric_id: 'VM-CRITICAL', priority: 'P1', compliance_pct: 0.74, status: 'Below 15% of Target' },
|
||
{ metric_id: 'VM-CRITICAL', priority: 'P2', compliance_pct: 0.91, status: 'Within 15% of Target' },
|
||
],
|
||
},
|
||
{
|
||
metricId: 'AUTH-MFA', category: 'Access & MFA', target: 0.98, worstStatus: 'Within 15% of Target',
|
||
entries: [{ metric_id: 'AUTH-MFA', compliance_pct: 0.94, status: 'Within 15% of Target' }],
|
||
},
|
||
{
|
||
metricId: 'LOG-COVERAGE', category: 'Logging & Monitoring', target: 0.90, worstStatus: 'Meets/Exceeds Target',
|
||
entries: [{ metric_id: 'LOG-COVERAGE', compliance_pct: 0.97, status: 'Meets/Exceeds Target' }],
|
||
},
|
||
{
|
||
metricId: 'EOL-OS', category: 'End-of-Life OS', target: 1.00, worstStatus: 'Below 15% of Target',
|
||
entries: [{ metric_id: 'EOL-OS', compliance_pct: 0.62, status: 'Below 15% of Target' }],
|
||
},
|
||
{
|
||
metricId: 'EDR-DEPLOY', category: 'Endpoint Protection', target: 0.95, worstStatus: 'Meets/Exceeds Target',
|
||
entries: [{ metric_id: 'EDR-DEPLOY', compliance_pct: 0.96, status: 'Meets/Exceeds Target' }],
|
||
},
|
||
];
|
||
|
||
const SAMPLE_DEVICES = [
|
||
{ hostname: 'app-prod-04.steam.internal', ip: '10.42.18.4', type: 'Linux server', failingMetrics: [{ metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }, { metric_id: 'EOL-OS', category: 'End-of-Life OS' }], seenCount: 5, hasNotes: true },
|
||
{ hostname: 'db-staging-01.steam.internal', ip: '10.42.20.11', type: 'Linux server', failingMetrics: [{ metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }], seenCount: 2, hasNotes: false },
|
||
{ hostname: 'fileshare-02.steam.internal', ip: '10.42.16.32', type: 'Windows server', failingMetrics: [{ metric_id: 'AUTH-MFA', category: 'Access & MFA' }], seenCount: 1, hasNotes: false },
|
||
{ hostname: 'jumpbox-east.steam.internal', ip: '10.42.4.7', type: 'Linux server', failingMetrics: [{ metric_id: 'AUTH-MFA', category: 'Access & MFA' }, { metric_id: 'VM-CRITICAL', category: 'Vulnerability Management' }], seenCount: 4, hasNotes: true },
|
||
{ hostname: 'legacy-billing.steam.internal', ip: '10.42.8.18', type: 'Windows server', failingMetrics: [{ metric_id: 'EOL-OS', category: 'End-of-Life OS' }], seenCount: 7, hasNotes: false },
|
||
];
|
||
|
||
/* ── Inline chart visuals — semantic stand-ins for Recharts. ── */
|
||
function NetworkScoreChart() {
|
||
const points = [82, 84, 81, 86, 85, 87, 88];
|
||
return (
|
||
<ChartSvg>
|
||
<Line points={points} color={PC.teal} fill={pAlpha(PC.teal, 0.15)} />
|
||
<YAxisLabels labels={['100%', '80%', '60%']} />
|
||
</ChartSvg>
|
||
);
|
||
}
|
||
function StatusDistributionChart() {
|
||
const data = [
|
||
{ meets: 62, within: 22, below: 16 },
|
||
{ meets: 65, within: 20, below: 15 },
|
||
{ meets: 67, within: 21, below: 12 },
|
||
{ meets: 72, within: 18, below: 10 },
|
||
];
|
||
return <StackedBars data={data} keys={['meets', 'within', 'below']} colors={[PC.green, PC.amber, PC.red]} />;
|
||
}
|
||
function TeamHealthChart() {
|
||
return (
|
||
<ChartSvg>
|
||
<Line points={[78, 80, 79, 83, 85, 88]} color={PC.teal} />
|
||
<Line points={[68, 70, 73, 71, 74, 76]} color={PC.amber} />
|
||
</ChartSvg>
|
||
);
|
||
}
|
||
function NewRecurringResolvedChart() {
|
||
const data = [
|
||
{ new_count: 12, recurring_count: 7, resolved_count: -10 },
|
||
{ new_count: 8, recurring_count: 9, resolved_count: -14 },
|
||
{ new_count: 14, recurring_count: 5, resolved_count: -8 },
|
||
{ new_count: 9, recurring_count: 6, resolved_count: -12 },
|
||
];
|
||
return (
|
||
<ChartSvg>
|
||
<ChartLegend items={[
|
||
{ label: 'New', color: PC.red },
|
||
{ label: 'Recurring', color: PC.amber },
|
||
{ label: 'Resolved', color: PC.green },
|
||
]} />
|
||
<StackedBars data={data} keys={['new_count', 'recurring_count', 'resolved_count']} colors={[PC.red, PC.amber, PC.green]} centered />
|
||
</ChartSvg>
|
||
);
|
||
}
|
||
function AvgDaysToResolveChart() {
|
||
const rows = [
|
||
{ label: 'AUTH-MFA', v: 4 },
|
||
{ label: 'VM-CRITICAL', v: 12 },
|
||
{ label: 'EOL-OS', v: 28 },
|
||
{ label: 'EDR-DEPLOY', v: 6 },
|
||
];
|
||
return <HorizontalBars rows={rows} max={32} color={PC.teal} unit="days" />;
|
||
}
|
||
function PersistentFindingsChart() {
|
||
const rows = [
|
||
{ label: 'legacy-billing', v: 7 },
|
||
{ label: 'app-prod-04', v: 5 },
|
||
{ label: 'jumpbox-east', v: 4 },
|
||
{ label: 'db-staging-01', v: 2 },
|
||
];
|
||
return <HorizontalBars rows={rows} max={8} color={PC.amber} unit="cycles" />;
|
||
}
|
||
|
||
/* Tiny SVG primitives — flat, deterministic, no library. */
|
||
function ChartSvg({ children, height = 180 }) {
|
||
return (
|
||
<div style={{ position: 'relative', width: '100%', height }}>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
function Line({ points, color, fill }) {
|
||
const max = Math.max(...points);
|
||
const min = Math.min(...points) * 0.85;
|
||
const range = max - min || 1;
|
||
const w = 100, h = 100;
|
||
const step = w / (points.length - 1);
|
||
const path = points.map((v, i) => `${i === 0 ? 'M' : 'L'} ${i * step} ${h - ((v - min) / range) * h}`).join(' ');
|
||
const fillPath = path + ` L ${w} ${h} L 0 ${h} Z`;
|
||
return (
|
||
<svg width="100%" height="100%" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ overflow: 'visible' }}>
|
||
{fill && <path d={fillPath} fill={fill} />}
|
||
<path d={path} fill="none" stroke={color} strokeWidth="1.5" />
|
||
{points.map((v, i) => (
|
||
<circle key={i} cx={i * step} cy={h - ((v - min) / range) * h} r="1.5" fill={color} />
|
||
))}
|
||
</svg>
|
||
);
|
||
}
|
||
function YAxisLabels({ labels }) {
|
||
return (
|
||
<div style={{
|
||
position: 'absolute', top: 0, bottom: 0, left: -2,
|
||
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
|
||
fontFamily: 'var(--font-mono)', fontSize: 9, color: 'var(--fg-disabled)',
|
||
pointerEvents: 'none',
|
||
}}>
|
||
{labels.map(l => <span key={l}>{l}</span>)}
|
||
</div>
|
||
);
|
||
}
|
||
function StackedBars({ data, keys, colors, centered = false }) {
|
||
const total = (d) => keys.reduce((s, k) => s + Math.abs(d[k]), 0);
|
||
const maxTotal = Math.max(...data.map(total));
|
||
return (
|
||
<div style={{ display: 'flex', alignItems: centered ? 'center' : 'flex-end', gap: 12, height: '100%', paddingTop: 8 }}>
|
||
{data.map((d, i) => {
|
||
const segs = keys.map((k, ki) => ({ v: d[k], color: colors[ki], k }));
|
||
return (
|
||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%' }}>
|
||
<div style={{ flex: 1, width: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
|
||
{segs.map((s, si) => (
|
||
<div key={si} style={{
|
||
width: '100%', height: `${(Math.abs(s.v) / maxTotal) * 100}%`,
|
||
background: s.color, opacity: 0.85,
|
||
borderTopLeftRadius: si === 0 ? 2 : 0,
|
||
borderTopRightRadius: si === 0 ? 2 : 0,
|
||
}} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
function HorizontalBars({ rows, max, color, unit }) {
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingTop: 8 }}>
|
||
{rows.map(r => (
|
||
<div key={r.label} style={{ display: 'grid', gridTemplateColumns: '120px 1fr 50px', gap: 8, alignItems: 'center' }}>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)', textAlign: 'right' }}>{r.label}</span>
|
||
<div style={{ height: 14, background: 'rgba(255,255,255,0.04)', borderRadius: 3, overflow: 'hidden' }}>
|
||
<div style={{ width: `${(r.v / max) * 100}%`, height: '100%', background: color, opacity: 0.85, borderRadius: 3 }} />
|
||
</div>
|
||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color, fontWeight: 600 }}>{r.v} {unit}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Page assembly ── */
|
||
function CompliancePage() {
|
||
const [team, setTeam] = useCompPageState('STEAM');
|
||
const [tab, setTab] = useCompPageState('active');
|
||
const [filter, setFilter] = useCompPageState(null);
|
||
const [search, setSearch] = useCompPageState('');
|
||
const [selected, setSelected] = useCompPageState(null);
|
||
const [rollback, setRollback] = useCompPageState(null);
|
||
|
||
const filteredDevices = SAMPLE_DEVICES
|
||
.filter(d => !filter || d.failingMetrics.some(m => filter.includes(m.metric_id)))
|
||
.filter(d => !search || d.hostname.toLowerCase().includes(search.toLowerCase()));
|
||
|
||
return (
|
||
<div data-screen-label="01 Compliance" style={{
|
||
position: 'relative',
|
||
padding: 32, minHeight: '100vh', background: 'var(--bg-page)',
|
||
fontFamily: 'var(--font-display)',
|
||
}}>
|
||
<CompPageHeader
|
||
lastReport="2026-04-21"
|
||
networkScore="88%"
|
||
verticalScore="84%"
|
||
isAdmin
|
||
onRollback={() => setRollback('confirm')}
|
||
/>
|
||
|
||
<TeamTabs teams={['STEAM', 'ACCESS-ENG']} active={team} onChange={setTeam} />
|
||
|
||
{/* Metric Health */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<div style={{
|
||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
|
||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em',
|
||
marginBottom: 10,
|
||
}}>
|
||
Metric Health — click to filter
|
||
{filter && (
|
||
<button onClick={() => setFilter(null)} style={{
|
||
marginLeft: 12, color: PC.teal, background: 'none', border: 'none',
|
||
cursor: 'pointer', fontSize: 10, fontFamily: 'var(--font-mono)',
|
||
}}>× clear filter</button>
|
||
)}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||
{SAMPLE_FAMILIES.map(family => {
|
||
const ids = family.entries.map(e => e.metric_id);
|
||
const isActive = filter !== null && filter.length === ids.length && ids.every(id => filter.includes(id));
|
||
return (
|
||
<div key={family.metricId} style={{ display: 'flex', flex: '1 1 0', minWidth: 160 }}>
|
||
<MetricHealthCard
|
||
family={family}
|
||
active={isActive}
|
||
onClick={() => setFilter(isActive ? null : ids)}
|
||
onInfoClick={() => {}}
|
||
/>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Charts */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginBottom: 24 }}>
|
||
<ChartCard title="Network Compliance" subtitle="Trailing 7 days">
|
||
<NetworkScoreChart />
|
||
</ChartCard>
|
||
<ChartCard title="Status Distribution" subtitle="Last 4 cycles">
|
||
<StatusDistributionChart />
|
||
</ChartCard>
|
||
<ChartCard title="Team Health" subtitle="STEAM vs ACCESS-ENG">
|
||
<TeamHealthChart />
|
||
</ChartCard>
|
||
<ChartCard title="New / Recurring / Resolved" subtitle="Per cycle" height={200}>
|
||
<NewRecurringResolvedChart />
|
||
</ChartCard>
|
||
<ChartCard title="Avg Days to Resolve" subtitle="By metric">
|
||
<AvgDaysToResolveChart />
|
||
</ChartCard>
|
||
<ChartCard title="Most Persistent Findings" subtitle="By cycles seen">
|
||
<PersistentFindingsChart />
|
||
</ChartCard>
|
||
</div>
|
||
|
||
{/* Device table */}
|
||
<DeviceTable>
|
||
<DeviceTableToolbar
|
||
tab={tab} onTabChange={setTab}
|
||
count={filteredDevices.length}
|
||
search={search} onSearchChange={e => setSearch(e.target.value)}
|
||
/>
|
||
<DeviceTableHeader />
|
||
{filteredDevices.length === 0 ? (
|
||
<CompEmpty>No non-compliant devices match the current filter</CompEmpty>
|
||
) : (
|
||
filteredDevices.map(d => (
|
||
<DeviceRow
|
||
key={d.hostname}
|
||
hostname={d.hostname} ip={d.ip} type={d.type}
|
||
failingMetrics={d.failingMetrics}
|
||
seenCount={d.seenCount} hasNotes={d.hasNotes}
|
||
selected={selected === d.hostname}
|
||
onClick={() => setSelected(selected === d.hostname ? null : d.hostname)}
|
||
/>
|
||
))
|
||
)}
|
||
</DeviceTable>
|
||
|
||
{rollback === 'confirm' && (
|
||
<RollbackDialog
|
||
reportLabel="2026-04-21"
|
||
onCancel={() => setRollback(null)}
|
||
onConfirm={() => setRollback('toast')}
|
||
/>
|
||
)}
|
||
{rollback === 'toast' && (
|
||
<RollbackToast
|
||
tone="success"
|
||
message="Upload rolled back"
|
||
detail="42 items deleted, 18 reactivated"
|
||
onDismiss={() => setRollback(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
window.COMP_PAGE = { CompliancePage };
|