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.
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
// CompPrimitives.jsx — primitives for the Compliance page kit.
|
||||
// Lifted directly from frontend/src/components/pages/CompliancePage.js.
|
||||
// Identity color is teal (#14B8A6); status colors map green/amber/red onto
|
||||
// "Meets/Exceeds Target", "Within 15% of Target", and "Below 15% of Target".
|
||||
|
||||
const { useState: useCompState, useRef: useCompRef } = React;
|
||||
|
||||
/* ── Tokens ──────────────────────────────────────────────────────
|
||||
Two layers:
|
||||
• Status — drives every percentage display + the worst-status
|
||||
ribbon on metric cards. Always one of three.
|
||||
• Category — owns the colored MetricBadge that flags which
|
||||
program a failing metric belongs to. */
|
||||
const C_COLORS = {
|
||||
teal: '#14B8A6',
|
||||
tealMid: '#5EEAD4',
|
||||
green: '#10B981',
|
||||
amber: '#F59E0B',
|
||||
red: '#EF4444',
|
||||
sky: '#0EA5E9',
|
||||
purple: '#8B5CF6',
|
||||
orange: '#F97316',
|
||||
slate: '#64748B',
|
||||
};
|
||||
|
||||
const STATUS_COLOR = {
|
||||
'Meets/Exceeds Target': C_COLORS.green,
|
||||
'Within 15% of Target': C_COLORS.amber,
|
||||
'Below 15% of Target': C_COLORS.red,
|
||||
};
|
||||
|
||||
const CATEGORY_COLORS = {
|
||||
'Vulnerability Management': C_COLORS.red,
|
||||
'Access & MFA': C_COLORS.amber,
|
||||
'Logging & Monitoring': C_COLORS.purple,
|
||||
'End-of-Life OS': C_COLORS.orange,
|
||||
'Decommissioned Assets': C_COLORS.slate,
|
||||
'Asset Data Quality': C_COLORS.slate,
|
||||
'Application Security': C_COLORS.sky,
|
||||
'Disaster Recovery': C_COLORS.teal,
|
||||
'Endpoint Protection': C_COLORS.orange,
|
||||
};
|
||||
|
||||
const statusColor = s => STATUS_COLOR[s] || C_COLORS.red;
|
||||
const pctDisplay = p => `${Math.round(p * 100)}%`;
|
||||
const cAlpha = (hex, a) => {
|
||||
const h = hex.replace('#', '');
|
||||
return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`;
|
||||
};
|
||||
|
||||
/* ── PageHeader ──────────────────────────────────────────────────
|
||||
AEO Compliance — title in teal w/ glow, last-report meta beneath,
|
||||
refresh + upload-report on the right. Mirrors the KB / Reporting
|
||||
header pattern but with teal instead of green. */
|
||||
function CompPageHeader({ title = 'AEO Compliance', lastReport, networkScore, verticalScore, onRefresh, onUpload, onRollback, isAdmin }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24, gap: 16 }}>
|
||||
<div>
|
||||
<h2 style={{
|
||||
margin: '0 0 6px 0',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 700,
|
||||
color: C_COLORS.teal, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
textShadow: `0 0 16px ${cAlpha(C_COLORS.teal, 0.4)}`,
|
||||
}}>{title}</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', fontFamily: 'var(--font-mono)', fontSize: 11 }}>
|
||||
{lastReport ? (
|
||||
<>
|
||||
<span style={{ color: 'var(--fg-disabled)' }}>
|
||||
Last report: <span style={{ color: 'var(--fg-2)' }}>{lastReport}</span>
|
||||
</span>
|
||||
{isAdmin && (
|
||||
<button onClick={onRollback} style={{
|
||||
background: 'transparent', border: '1px solid rgba(239,68,68,0.25)',
|
||||
borderRadius: 4, padding: '2px 6px', cursor: 'pointer',
|
||||
color: 'var(--fg-2)', display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
fontSize: 10, fontFamily: 'var(--font-mono)',
|
||||
}}>
|
||||
<CompIcon name="rotate" size={10} color="currentColor" /> Rollback
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span style={{ color: 'var(--fg-disabled)' }}>No reports uploaded</span>
|
||||
)}
|
||||
{networkScore != null && (
|
||||
<span style={{ color: 'var(--fg-2)' }}>Network: <span style={{ color: C_COLORS.teal }}>{networkScore}</span></span>
|
||||
)}
|
||||
{verticalScore != null && (
|
||||
<span style={{ color: 'var(--fg-2)' }}>Vertical: <span style={{ color: C_COLORS.teal }}>{verticalScore}</span></span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
||||
<CompIconButton icon="refresh" onClick={onRefresh} />
|
||||
<CompButton variant="primary" icon="upload" onClick={onUpload}>Upload Report</CompButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Buttons ───────────────────────────────────────────────────── */
|
||||
function CompButton({ variant = 'neutral', icon, size = 'md', children, ...rest }) {
|
||||
const [hover, setHover] = useCompState(false);
|
||||
const v = {
|
||||
primary: { bg: hover ? cAlpha(C_COLORS.teal, 0.28) : cAlpha(C_COLORS.teal, 0.18), bd: C_COLORS.teal, fg: C_COLORS.teal },
|
||||
neutral: { bg: hover ? cAlpha(C_COLORS.teal, 0.10) : 'transparent', bd: cAlpha(C_COLORS.teal, 0.30), fg: C_COLORS.teal },
|
||||
danger: { bg: hover ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.10)', bd: C_COLORS.red, fg: C_COLORS.red },
|
||||
ghost: { bg: hover ? 'rgba(255,255,255,0.04)' : 'transparent', bd: 'rgba(100,116,139,0.40)', fg: 'var(--fg-2)' },
|
||||
}[variant];
|
||||
const padX = size === 'sm' ? 10 : 16;
|
||||
const padY = size === 'sm' ? 4 : 8;
|
||||
const fs = size === 'sm' ? 11 : 12;
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: `${padY}px ${padX}px`, borderRadius: 6,
|
||||
background: v.bg, border: `1px solid ${v.bd}`, color: v.fg,
|
||||
fontFamily: 'var(--font-mono)', fontSize: fs, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
cursor: 'pointer', transition: 'all 160ms ease', whiteSpace: 'nowrap',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{icon && <CompIcon name={icon} size={fs + 2} color={v.fg} />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CompIconButton({ icon, onClick, color = C_COLORS.teal }) {
|
||||
const [hover, setHover] = useCompState(false);
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
background: hover ? cAlpha(color, 0.10) : 'transparent',
|
||||
border: `1px solid ${hover ? color : cAlpha(color, 0.25)}`,
|
||||
borderRadius: 6, padding: 8, cursor: 'pointer',
|
||||
color: hover ? color : 'var(--fg-2)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'all 160ms ease',
|
||||
}}>
|
||||
<CompIcon name={icon} size={16} color="currentColor" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── TeamTabs ──────────────────────────────────────────────────── */
|
||||
function TeamTabs({ teams, active, onChange }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 24 }}>
|
||||
{teams.map(team => {
|
||||
const on = active === team;
|
||||
return (
|
||||
<button key={team} onClick={() => onChange(team)} style={{
|
||||
padding: '8px 18px', cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.06em',
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${on ? C_COLORS.teal : cAlpha(C_COLORS.teal, 0.20)}`,
|
||||
background: on ? cAlpha(C_COLORS.teal, 0.18) : 'transparent',
|
||||
color: on ? C_COLORS.teal : 'var(--fg-disabled)',
|
||||
transition: 'all 160ms ease',
|
||||
}}>{team}</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── VariantPill ─────────────────────────────────────────────────
|
||||
The compliance % pill that lives inside MetricHealthCard. One per
|
||||
priority/variant within a metric family. Dot only shown when the
|
||||
variant isn't already meeting target — green pills stay quiet. */
|
||||
function VariantPill({ status, pct, label }) {
|
||||
const color = statusColor(status);
|
||||
const isOk = status === 'Meets/Exceeds Target';
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
padding: '2px 7px',
|
||||
background: cAlpha(color, 0.12),
|
||||
border: `1px solid ${cAlpha(color, 0.25)}`,
|
||||
borderRadius: 3,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
color: 'var(--fg-2)', whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{!isOk && (
|
||||
<span style={{
|
||||
display: 'inline-block', width: 4, height: 4, borderRadius: '50%',
|
||||
background: color, boxShadow: `0 0 5px ${color}`,
|
||||
}} />
|
||||
)}
|
||||
{label && <span style={{ color: 'var(--fg-disabled)' }}>{label}</span>}
|
||||
<span style={{ color, fontWeight: 600 }}>{pctDisplay(pct)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── StatusRibbon ────────────────────────────────────────────────
|
||||
The lozenge at the bottom of MetricHealthCard. "OK" when meeting,
|
||||
abbreviated status text otherwise. */
|
||||
function StatusRibbon({ status }) {
|
||||
const color = statusColor(status);
|
||||
const isOk = status === 'Meets/Exceeds Target';
|
||||
return (
|
||||
<div style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
textTransform: 'uppercase', letterSpacing: '0.04em',
|
||||
color, padding: '3px 9px',
|
||||
background: cAlpha(color, 0.10),
|
||||
borderRadius: 999,
|
||||
border: `1px solid ${cAlpha(color, 0.30)}`,
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'inline-block', width: 5, height: 5, borderRadius: '50%',
|
||||
background: color, boxShadow: isOk ? 'none' : `0 0 6px ${color}`,
|
||||
}} />
|
||||
{isOk ? 'OK' : status.replace(' of Target', '')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── MetricHealthCard ────────────────────────────────────────────
|
||||
The big clickable cards in the metric strip. Click to filter the
|
||||
device table; click the info "i" to open the metric definition
|
||||
panel. Border + ID color shift when active. */
|
||||
function MetricHealthCard({ family, active, onClick, onInfoClick, onHover, onLeave }) {
|
||||
const [h, setH] = useCompState(false);
|
||||
const color = statusColor(family.worstStatus);
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
onMouseEnter={(e) => { setH(true); onHover && onHover(e.currentTarget); }}
|
||||
onMouseLeave={() => { setH(false); onLeave && onLeave(); }}
|
||||
style={{
|
||||
position: 'relative', textAlign: 'left', cursor: 'pointer',
|
||||
background: active
|
||||
? cAlpha(color, 0.15)
|
||||
: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: `1.5px solid ${active ? color : (h ? cAlpha(color, 0.50) : cAlpha(color, 0.25))}`,
|
||||
borderRadius: 8,
|
||||
padding: '14px 16px',
|
||||
minWidth: 160, flex: '1 1 0',
|
||||
transition: 'all 160ms ease',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onClick={(e) => { e.stopPropagation(); onInfoClick && onInfoClick(family.metricId); }}
|
||||
style={{
|
||||
position: 'absolute', top: 8, right: 8,
|
||||
display: 'inline-flex', cursor: 'pointer', padding: 2,
|
||||
color: 'var(--fg-disabled)', borderRadius: 3,
|
||||
}}
|
||||
>
|
||||
<CompIcon name="info" size={13} color="currentColor" />
|
||||
</span>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700,
|
||||
color: active ? color : 'var(--fg-1)', marginBottom: 4, paddingRight: 20,
|
||||
}}>{family.metricId}</div>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
marginBottom: 8,
|
||||
}}>{family.category}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: 8 }}>
|
||||
{family.entries.map((e, i) => (
|
||||
<VariantPill
|
||||
key={e.metric_id + '-' + i}
|
||||
status={e.status} pct={e.compliance_pct}
|
||||
label={family.entries.length > 1 ? (e.priority || `#${i + 1}`) : null}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
color: 'var(--fg-disabled)', marginBottom: 8,
|
||||
}}>target {pctDisplay(family.target)}</div>
|
||||
<StatusRibbon status={family.worstStatus} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── MetricBadge ─────────────────────────────────────────────────
|
||||
Compact category-tinted ID chip used in device-row "Failing Metrics"
|
||||
columns and inside detail panels. */
|
||||
function MetricBadge({ metricId, category }) {
|
||||
const color = CATEGORY_COLORS[category] || C_COLORS.slate;
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center',
|
||||
padding: '2px 7px',
|
||||
background: cAlpha(color, 0.12),
|
||||
border: `1px solid ${cAlpha(color, 0.30)}`,
|
||||
borderRadius: 3, color,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>{metricId}</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── SeenBadge ───────────────────────────────────────────────────
|
||||
"1×" / "3×" / "5×" — how many cycles a host has been failing the
|
||||
same set of metrics. Color escalates: slate → amber → red. */
|
||||
function SeenBadge({ count }) {
|
||||
const color = count > 3 ? C_COLORS.red : count > 1 ? C_COLORS.amber : C_COLORS.slate;
|
||||
return (
|
||||
<span style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
|
||||
color, padding: '2px 7px',
|
||||
background: cAlpha(color, 0.10),
|
||||
border: `1px solid ${cAlpha(color, 0.30)}`,
|
||||
borderRadius: 3, whiteSpace: 'nowrap',
|
||||
}}>{count}×</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── DeviceTable + DeviceRow ─────────────────────────────────────
|
||||
The non-compliant host list. Toolbar has Active/Resolved tabs +
|
||||
hostname search. Rows show hostname, IP, type, failing metric
|
||||
badges, seen count, and a notes indicator. */
|
||||
function DeviceTable({ children }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: '1px solid rgba(20,184,166,0.15)',
|
||||
borderRadius: 8, overflow: 'hidden',
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceTableToolbar({ tab, onTabChange, count, search, onSearchChange }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '14px 16px', borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{['active', 'resolved'].map(t => {
|
||||
const on = tab === t;
|
||||
return (
|
||||
<button key={t} onClick={() => onTabChange(t)} style={{
|
||||
padding: '6px 14px', cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.04em',
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${on ? cAlpha(C_COLORS.teal, 0.40) : 'transparent'}`,
|
||||
background: on ? cAlpha(C_COLORS.teal, 0.10) : 'transparent',
|
||||
color: on ? C_COLORS.teal : 'var(--fg-disabled)',
|
||||
}}>
|
||||
{t}
|
||||
{on && <span style={{ marginLeft: 6, color: 'var(--fg-2)' }}>({count})</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<CompSearchInput value={search} onChange={onSearchChange} placeholder="Search hostname…" width={220} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CompSearchInput({ value, onChange, placeholder, width = 240 }) {
|
||||
const [focus, setFocus] = useCompState(false);
|
||||
return (
|
||||
<input
|
||||
value={value} onChange={onChange} placeholder={placeholder}
|
||||
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
||||
style={{
|
||||
background: 'rgba(15,23,42,0.85)',
|
||||
border: `1px solid ${focus ? cAlpha(C_COLORS.teal, 0.60) : cAlpha(C_COLORS.teal, 0.20)}`,
|
||||
borderRadius: 4, color: 'var(--fg-1)', outline: 'none',
|
||||
padding: '6px 12px', fontSize: 12, fontFamily: 'var(--font-mono)',
|
||||
width, transition: 'border-color 160ms ease',
|
||||
boxShadow: focus ? `0 0 0 3px ${cAlpha(C_COLORS.teal, 0.10)}` : 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const DEVICE_GRID = '2.5fr 1.1fr 1fr 2fr 0.5fr 0.4fr';
|
||||
|
||||
function DeviceTableHeader() {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'grid', gridTemplateColumns: DEVICE_GRID,
|
||||
padding: '8px 16px',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
}}>
|
||||
<span>Hostname</span><span>IP Address</span><span>Type</span>
|
||||
<span>Failing Metrics</span><span>Seen</span><span></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceRow({ hostname, ip, type, failingMetrics, seenCount, hasNotes, selected, onClick }) {
|
||||
const [hover, setHover] = useCompState(false);
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'grid', gridTemplateColumns: DEVICE_GRID,
|
||||
padding: '10px 16px',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
cursor: 'pointer',
|
||||
background: selected ? cAlpha(C_COLORS.teal, 0.08) : (hover ? 'rgba(255,255,255,0.025)' : 'transparent'),
|
||||
borderLeft: selected ? `2px solid ${C_COLORS.teal}` : '2px solid transparent',
|
||||
transition: 'all 160ms ease', alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
color: selected ? C_COLORS.teal : 'var(--fg-1)',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>{hostname}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>{ip || '—'}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--fg-disabled)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{type || '—'}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
{failingMetrics.map(m => <MetricBadge key={m.metric_id} metricId={m.metric_id} category={m.category} />)}
|
||||
</div>
|
||||
<div><SeenBadge count={seenCount} /></div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{hasNotes && <CompIcon name="message" size={13} color={cAlpha(C_COLORS.teal, 0.7)} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── EmptyState — for table body when there's nothing to show. ── */
|
||||
function CompEmpty({ children }) {
|
||||
return (
|
||||
<div style={{
|
||||
padding: 48, textAlign: 'center',
|
||||
color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── ChartCard — wrapper around any of the 6 charts on the page. ── */
|
||||
function ChartCard({ title, subtitle, children, height = 240 }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: '1px solid rgba(20,184,166,0.15)',
|
||||
borderRadius: 8, padding: 16,
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
marginBottom: subtitle ? 2 : 12,
|
||||
}}>{title}</div>
|
||||
{subtitle && (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10,
|
||||
color: 'var(--fg-disabled)', marginBottom: 12,
|
||||
}}>{subtitle}</div>
|
||||
)}
|
||||
<div style={{ height }}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── ChartLegend — shared legend row used at the top of stacked charts. ── */
|
||||
function ChartLegend({ items }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 8 }}>
|
||||
{items.map(it => (
|
||||
<span key={it.label} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)',
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'inline-block', width: 10, height: 10, borderRadius: 2,
|
||||
background: it.color,
|
||||
}} />
|
||||
{it.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── DefinitionTooltip ───────────────────────────────────────────
|
||||
The hover popover that surfaces a metric's title + business
|
||||
justification + data sources. */
|
||||
function DefinitionTooltip({ title, justification, sources }) {
|
||||
return (
|
||||
<div style={{
|
||||
width: 300,
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||
border: `1px solid ${cAlpha(C_COLORS.teal, 0.30)}`,
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
|
||||
padding: '12px 14px',
|
||||
}}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700, color: 'var(--fg-1)', marginBottom: 6, lineHeight: 1.3 }}>{title}</div>
|
||||
{justification && (
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)', lineHeight: 1.4, marginBottom: 6 }}>{justification}</div>
|
||||
)}
|
||||
{sources && (
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)' }}>Sources: {sources}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── RollbackDialog ──────────────────────────────────────────────
|
||||
Centered modal w/ red identity. "Reverses the most recent upload"
|
||||
message + danger confirm. */
|
||||
function RollbackDialog({ reportLabel, onCancel, onConfirm, loading }) {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, zIndex: 60,
|
||||
background: 'rgba(10,14,39,0.92)', backdropFilter: 'blur(8px)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||
border: '1px solid rgba(239,68,68,0.30)', borderRadius: 12,
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.7)',
|
||||
width: '100%', maxWidth: 420, padding: 28,
|
||||
}}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 15, fontWeight: 700, color: C_COLORS.red, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 12 }}>
|
||||
Rollback Upload
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.5, marginBottom: 8, fontFamily: 'var(--font-display)' }}>
|
||||
This will reverse the most recent upload:
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)',
|
||||
background: 'rgba(15,23,42,0.6)', borderRadius: 6,
|
||||
padding: '10px 12px', marginBottom: 18,
|
||||
border: '1px solid rgba(239,68,68,0.15)',
|
||||
}}>
|
||||
<div><span style={{ color: 'var(--fg-disabled)' }}>File:</span> {reportLabel}</div>
|
||||
<div style={{ marginTop: 4, fontSize: 10, color: 'var(--fg-disabled)' }}>
|
||||
New items will be deleted, resolved items will be reactivated, and the upload record will be removed.
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<CompButton variant="ghost" onClick={onCancel} style={{ flex: 1, justifyContent: 'center' }}>Cancel</CompButton>
|
||||
<button onClick={onConfirm} disabled={loading} style={{
|
||||
flex: 2, padding: 10,
|
||||
background: loading ? 'rgba(239,68,68,0.05)' : 'rgba(239,68,68,0.10)',
|
||||
border: `1px solid ${C_COLORS.red}`, borderRadius: 6,
|
||||
color: C_COLORS.red, cursor: loading ? 'wait' : 'pointer',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
opacity: loading ? 0.6 : 1,
|
||||
}}>
|
||||
<CompIcon name="rotate" size={13} color="currentColor" />
|
||||
{loading ? 'Rolling back…' : 'Confirm Rollback'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── RollbackToast — bottom-right confirmation/error toast. ── */
|
||||
function RollbackToast({ tone = 'success', message, detail, onDismiss }) {
|
||||
const c = tone === 'error' ? C_COLORS.red : C_COLORS.green;
|
||||
return (
|
||||
<div onClick={onDismiss} style={{
|
||||
position: 'absolute', bottom: 24, right: 24, zIndex: 70,
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.98) 0%, rgba(15,23,42,0.99) 100%)',
|
||||
border: `1px solid ${cAlpha(c, 0.40)}`, borderRadius: 8,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
|
||||
padding: '14px 20px', maxWidth: 360,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, color: c, cursor: 'pointer',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: detail ? 4 : 0 }}>
|
||||
<CompIcon name={tone === 'error' ? 'alert' : 'rotate'} size={14} color="currentColor" />
|
||||
{message}
|
||||
</div>
|
||||
{detail && <div style={{ fontSize: 10, color: 'var(--fg-2)' }}>{detail}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── CompIcon — every icon used by the compliance page. ── */
|
||||
function CompIcon({ name, size = 16, color = 'currentColor' }) {
|
||||
const p = {
|
||||
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
|
||||
stroke: color, strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
||||
style: { display: 'inline-block', verticalAlign: 'middle' },
|
||||
};
|
||||
switch (name) {
|
||||
case 'upload': return <svg {...p}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>;
|
||||
case 'refresh': return <svg {...p}><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></svg>;
|
||||
case 'rotate': return <svg {...p}><path d="M3 12a9 9 0 1 0 9-9"/><polyline points="3 4 3 9 8 9"/></svg>;
|
||||
case 'message': return <svg {...p}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>;
|
||||
case 'info': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>;
|
||||
case 'alert': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>;
|
||||
case 'check': return <svg {...p}><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>;
|
||||
case 'loader': return <svg {...p}><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg>;
|
||||
case 'x': return <svg {...p}><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>;
|
||||
default: return <svg {...p}><circle cx="12" cy="12" r="10"/></svg>;
|
||||
}
|
||||
}
|
||||
|
||||
window.COMP = {
|
||||
COLORS: C_COLORS, STATUS_COLOR, CATEGORY_COLORS,
|
||||
statusColor, pctDisplay, cAlpha,
|
||||
CompPageHeader, CompButton, CompIconButton, TeamTabs,
|
||||
VariantPill, StatusRibbon, MetricHealthCard, MetricBadge, SeenBadge,
|
||||
DeviceTable, DeviceTableToolbar, DeviceTableHeader, DeviceRow, CompSearchInput, CompEmpty,
|
||||
ChartCard, ChartLegend,
|
||||
DefinitionTooltip, RollbackDialog, RollbackToast,
|
||||
CompIcon,
|
||||
};
|
||||
@@ -0,0 +1,319 @@
|
||||
// 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 };
|
||||
363
docs/design-system-redesign/ui_kits/compliance/KitDocs.jsx
Normal file
363
docs/design-system-redesign/ui_kits/compliance/KitDocs.jsx
Normal file
@@ -0,0 +1,363 @@
|
||||
// KitDocs.jsx — browseable docs page for the Compliance kit.
|
||||
|
||||
const { useState: useDocsCompState } = React;
|
||||
const {
|
||||
COLORS: DCC, statusColor: dStatus, pctDisplay: dPct, cAlpha: dA,
|
||||
CompPageHeader: DHeader, CompButton: DBtn, TeamTabs: DTabs,
|
||||
VariantPill: DVPill, StatusRibbon: DRibbon, MetricHealthCard: DMHC,
|
||||
MetricBadge: DMB, SeenBadge: DSB,
|
||||
DeviceTable: DDT, DeviceTableToolbar: DDTT, DeviceTableHeader: DDTH, DeviceRow: DDR,
|
||||
CompEmpty: DEmpty, ChartCard: DChart, ChartLegend: DLegend,
|
||||
DefinitionTooltip: DTip, RollbackDialog: DRoll, RollbackToast: DToast,
|
||||
CompIcon: DIcon,
|
||||
} = window.COMP;
|
||||
const { CompliancePage: DPage } = window.COMP_PAGE;
|
||||
|
||||
function CSection({ id, eyebrow, title, blurb, children }) {
|
||||
return (
|
||||
<section id={id} style={{ paddingTop: 32, scrollMarginTop: 120 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{eyebrow && (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 700,
|
||||
color: DCC.teal, textTransform: 'uppercase', letterSpacing: '0.18em',
|
||||
marginBottom: 6,
|
||||
}}>{eyebrow}</div>
|
||||
)}
|
||||
<h2 style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700,
|
||||
color: 'var(--fg-1)', margin: 0, letterSpacing: '0.02em',
|
||||
}}>{title}</h2>
|
||||
{blurb && (
|
||||
<p style={{
|
||||
fontFamily: 'var(--font-display)', fontSize: 14, lineHeight: 1.6,
|
||||
color: 'var(--fg-muted)', maxWidth: 660, margin: '8px 0 0 0',
|
||||
}}>{blurb}</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CCode({ children }) {
|
||||
return (
|
||||
<code style={{
|
||||
display: 'inline-block', padding: '2px 6px', borderRadius: 3,
|
||||
background: dA(DCC.teal, 0.10), border: `1px solid ${dA(DCC.teal, 0.18)}`,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: DCC.tealMid,
|
||||
}}>{children}</code>
|
||||
);
|
||||
}
|
||||
|
||||
function CSwatch({ name, value, role }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'grid', gridTemplateColumns: '52px 1fr auto', alignItems: 'center', gap: 14,
|
||||
padding: '8px 0', borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
}}>
|
||||
<div style={{ height: 36, borderRadius: 6, background: value, border: '1px solid rgba(255,255,255,0.08)' }} />
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-1)', fontWeight: 600 }}>{name}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)' }}>{role}</div>
|
||||
</div>
|
||||
<CCode>{value}</CCode>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CSpec({ label, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'grid', gridTemplateColumns: '160px 1fr', gap: 16,
|
||||
padding: '10px 0', borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
}}>
|
||||
<div style={{ color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.06em', fontSize: 10, fontWeight: 600 }}>{label}</div>
|
||||
<div style={{ color: 'var(--fg-2)' }}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CSpecimen({ children, padding = 24 }) {
|
||||
return (
|
||||
<div style={{
|
||||
padding,
|
||||
background: 'rgba(15,23,42,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.05)', borderRadius: 8,
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'tokens', label: 'Tokens' },
|
||||
{ id: 'components', label: 'Components' },
|
||||
{ id: 'assemblies', label: 'Assemblies' },
|
||||
{ id: 'reference', label: 'Reference Page' },
|
||||
];
|
||||
|
||||
const subhead = {
|
||||
margin: '32px 0 6px 0',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700,
|
||||
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
};
|
||||
const subblurb = {
|
||||
margin: '0 0 12px 0',
|
||||
fontFamily: 'var(--font-display)', fontSize: 13, lineHeight: 1.55,
|
||||
color: 'var(--fg-muted)', maxWidth: 720,
|
||||
};
|
||||
|
||||
const SAMPLE_FAMILY_BAD = {
|
||||
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' },
|
||||
],
|
||||
};
|
||||
const SAMPLE_FAMILY_OK = {
|
||||
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' }],
|
||||
};
|
||||
|
||||
function CKitDocs() {
|
||||
const [active, setActive] = useDocsCompState('overview');
|
||||
const handle = (id) => {
|
||||
setActive(id);
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - 80;
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'var(--bg-page)' }}>
|
||||
<header style={{ padding: '40px 48px 0 48px', maxWidth: 1280, margin: '0 auto' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: DCC.teal, textTransform: 'uppercase', letterSpacing: '0.18em', marginBottom: 8 }}>
|
||||
STEAM Security · UI Kit
|
||||
</div>
|
||||
<h1 style={{
|
||||
margin: 0, fontFamily: 'var(--font-mono)', fontSize: 36, fontWeight: 700,
|
||||
color: DCC.teal, textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
textShadow: `0 0 24px ${dA(DCC.teal, 0.30)}`,
|
||||
}}>Compliance</h1>
|
||||
<p style={{
|
||||
margin: '12px 0 0 0', maxWidth: 720, fontSize: 15, lineHeight: 1.6,
|
||||
color: 'var(--fg-muted)', fontFamily: 'var(--font-display)',
|
||||
}}>
|
||||
The AEO Compliance view: per-team metric health, six trend charts, and a non-compliant device
|
||||
drilldown. Identity color is teal — distinct from the green-titled CVE pages — with status colors
|
||||
that map green/amber/red onto target adherence.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav style={{
|
||||
position: 'sticky', top: 0, zIndex: 10, marginTop: 28,
|
||||
background: 'rgba(2,6,23,0.85)', backdropFilter: 'blur(8px)',
|
||||
borderBottom: `1px solid ${dA(DCC.teal, 0.15)}`,
|
||||
}}>
|
||||
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px', display: 'flex', gap: 4 }}>
|
||||
{TABS.map(t => {
|
||||
const on = active === t.id;
|
||||
return (
|
||||
<button key={t.id} onClick={() => handle(t.id)} style={{
|
||||
padding: '14px 16px', background: 'transparent', border: 'none',
|
||||
borderBottom: `2px solid ${on ? DCC.teal : 'transparent'}`,
|
||||
color: on ? DCC.teal : 'var(--fg-2)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
cursor: 'pointer', transition: 'all 160ms ease',
|
||||
}}>{t.label}</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px 96px 48px' }}>
|
||||
|
||||
<CSection id="overview" eyebrow="01 — Overview" title="Why this kit exists" blurb="Compliance has its own visual identity inside the suite — teal page title, status colors driven by target adherence, and a metric-card pattern that does double duty as a filter. This kit captures the vocabulary so other audit-style views can reuse it.">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<CSpecimen>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: DCC.teal, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 12, fontWeight: 600 }}>Identity</div>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.6, fontFamily: 'var(--font-display)' }}>
|
||||
Teal owns the page header, the active team tab, the upload CTA, the active device row,
|
||||
and any "neutral compliance signal" surface. Status colors (green/amber/red) own
|
||||
everything that represents target adherence — never decorative.
|
||||
</p>
|
||||
</CSpecimen>
|
||||
<CSpecimen>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: DCC.teal, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 12, fontWeight: 600 }}>Layout</div>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'var(--fg-1)', lineHeight: 1.6, fontFamily: 'var(--font-display)' }}>
|
||||
Page header → team tabs → metric health row (one card per metric family) →
|
||||
3×2 chart grid → device table with active/resolved tabs and hostname search.
|
||||
Selecting a metric card filters the table; selecting a row opens a detail panel.
|
||||
</p>
|
||||
</CSpecimen>
|
||||
</div>
|
||||
</CSection>
|
||||
|
||||
<CSection id="tokens" eyebrow="02 — Tokens" title="Status, category, and identity color" blurb="Status colors are reserved for target adherence. Category colors tag failing-metric badges by program area so a host's failure mix is scannable.">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 32 }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8, fontWeight: 600 }}>Status (target adherence)</div>
|
||||
<CSwatch name="green" value={DCC.green} role="Meets/Exceeds Target · success" />
|
||||
<CSwatch name="amber" value={DCC.amber} role="Within 15% of Target · attention" />
|
||||
<CSwatch name="red" value={DCC.red} role="Below 15% of Target · critical" />
|
||||
<div style={{ marginTop: 24, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8, fontWeight: 600 }}>Identity</div>
|
||||
<CSwatch name="teal" value={DCC.teal} role="Page title · CTA · selected row" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8, fontWeight: 600 }}>Category</div>
|
||||
<CSwatch name="red" value={DCC.red} role="Vulnerability Management" />
|
||||
<CSwatch name="amber" value={DCC.amber} role="Access & MFA" />
|
||||
<CSwatch name="purple" value={DCC.purple} role="Logging & Monitoring" />
|
||||
<CSwatch name="orange" value={DCC.orange} role="End-of-Life OS · Endpoint Protection" />
|
||||
<CSwatch name="sky" value={DCC.sky} role="Application Security" />
|
||||
<CSwatch name="slate" value={DCC.slate} role="Asset Data Quality · Decommissioned" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<CSpec label="Card chrome">background <CCode>linear-gradient(135deg, rgba(30,41,59,.95), rgba(15,23,42,.98))</CCode></CSpec>
|
||||
<CSpec label="Metric card border">resting <CCode>1.5px solid {`{statusColor}`} @ 0.25</CCode> · hover <CCode>0.50</CCode> · active <CCode>1.0</CCode> + 15% bg fill</CSpec>
|
||||
<CSpec label="Title type"><CCode>var(--font-mono)</CCode> · 24 / 700 · uppercase · 0.1em tracking · 16px text-shadow glow</CSpec>
|
||||
<CSpec label="Worst-status logic">A family's <CCode>worstStatus</CCode> is the lowest-severity entry across all variants — drives card border + ribbon</CSpec>
|
||||
</div>
|
||||
</CSection>
|
||||
|
||||
<CSection id="components" eyebrow="03 — Components" title="The pieces" blurb="Every primitive used by the page assembly. All are exported on window.COMP.">
|
||||
<h3 style={subhead}>CompPageHeader</h3>
|
||||
<p style={subblurb}>Teal title with glow, last-report meta + optional rollback button, network/vertical scores, and a refresh + upload CTA on the right.</p>
|
||||
<CSpecimen>
|
||||
<DHeader lastReport="2026-04-21" networkScore="88%" verticalScore="84%" isAdmin onRollback={() => {}} />
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>TeamTabs</h3>
|
||||
<p style={subblurb}>Two-team toggle pinned above the metric strip. The active tab fills with teal at 18% alpha.</p>
|
||||
<CSpecimen>
|
||||
<DTabs teams={['STEAM', 'ACCESS-ENG']} active="STEAM" onChange={() => {}} />
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>CompButton</h3>
|
||||
<p style={subblurb}>Four variants. Primary is the lone teal CTA (Upload Report). Danger fronts the rollback flow.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
<DBtn variant="primary" icon="upload">Upload Report</DBtn>
|
||||
<DBtn variant="neutral" icon="refresh">Refresh</DBtn>
|
||||
<DBtn variant="danger" icon="rotate">Rollback</DBtn>
|
||||
<DBtn variant="ghost">Cancel</DBtn>
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>MetricHealthCard</h3>
|
||||
<p style={subblurb}>The big clickable card in the metric strip. Border + ID color follow the family's <em>worst</em> status, so a single bad variant turns the whole family red. Click filters the device table; the info "i" opens a definition panel.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_BAD} active={false} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_BAD} active={true} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_OK} active={false} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>VariantPill · StatusRibbon</h3>
|
||||
<p style={subblurb}>Atoms inside MetricHealthCard. VariantPill = one priority's % readout. StatusRibbon = the bottom lozenge.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<DVPill status="Meets/Exceeds Target" pct={0.97} />
|
||||
<DVPill status="Within 15% of Target" pct={0.91} label="P2" />
|
||||
<DVPill status="Below 15% of Target" pct={0.74} label="P1" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<DRibbon status="Meets/Exceeds Target" />
|
||||
<DRibbon status="Within 15% of Target" />
|
||||
<DRibbon status="Below 15% of Target" />
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>MetricBadge · SeenBadge</h3>
|
||||
<p style={subblurb}>Row-level chips in the device table. MetricBadge tints by category; SeenBadge escalates slate→amber→red as repeat-failure count grows.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<DMB metricId="VM-CRITICAL" category="Vulnerability Management" />
|
||||
<DMB metricId="AUTH-MFA" category="Access & MFA" />
|
||||
<DMB metricId="LOG-COVERAGE" category="Logging & Monitoring" />
|
||||
<DMB metricId="EOL-OS" category="End-of-Life OS" />
|
||||
<DMB metricId="EDR-DEPLOY" category="Endpoint Protection" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<DSB count={1} /><DSB count={3} /><DSB count={5} /><DSB count={7} />
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>DeviceRow</h3>
|
||||
<p style={subblurb}>One non-compliant host per row. Selected state shifts the left border + hostname color to teal.</p>
|
||||
<CSpecimen padding={0}>
|
||||
<DDT>
|
||||
<DDTH />
|
||||
<DDR 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} selected={true} onClick={() => {}} />
|
||||
<DDR 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} onClick={() => {}} />
|
||||
</DDT>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>ChartCard</h3>
|
||||
<p style={subblurb}>Wrapper for any of the six trend charts. Title in mono uppercase, optional subtitle in disabled grey, 240px chart well by default.</p>
|
||||
<CSpecimen>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<DChart title="Network Compliance" subtitle="Trailing 7 days" height={120}>
|
||||
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--fg-disabled)', fontFamily: 'var(--font-mono)', fontSize: 11 }}>chart well</div>
|
||||
</DChart>
|
||||
<DChart title="Status Distribution" subtitle="Last 4 cycles" height={120}>
|
||||
<DLegend items={[{ label: 'Meets', color: DCC.green }, { label: 'Within 15%', color: DCC.amber }, { label: 'Below 15%', color: DCC.red }]} />
|
||||
</DChart>
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>DefinitionTooltip</h3>
|
||||
<p style={subblurb}>Hover popover used to surface a metric's title, business justification, and data sources.</p>
|
||||
<CSpecimen>
|
||||
<DTip title="VM-CRITICAL — Critical Vulnerabilities Patched" justification="Track the percentage of critical CVEs patched within the SLA window. Below-target performance creates exploitable risk on production assets." sources="Tenable, Atlas, JIRA" />
|
||||
</CSpecimen>
|
||||
</CSection>
|
||||
|
||||
<CSection id="assemblies" eyebrow="04 — Assemblies" title="How the parts compose">
|
||||
<h3 style={subhead}>Metric health row</h3>
|
||||
<p style={subblurb}>One MetricHealthCard per family, flexed evenly. Click a card to filter the device table to only its IDs; an "× clear filter" button appears in the section label when active.</p>
|
||||
<CSpecimen>
|
||||
<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
|
||||
<span style={{ marginLeft: 12, color: DCC.teal }}>× clear filter</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_BAD} active={true} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
<div style={{ flex: 1 }}><DMHC family={SAMPLE_FAMILY_OK} active={false} onClick={() => {}} onInfoClick={() => {}} /></div>
|
||||
</div>
|
||||
</CSpecimen>
|
||||
|
||||
<h3 style={subhead}>Device table</h3>
|
||||
<p style={subblurb}>Toolbar (active/resolved tabs + hostname search) → header row → DeviceRows. Empty/loading/error states are centered messages inside the same chrome.</p>
|
||||
<CSpecimen padding={0}>
|
||||
<DDT>
|
||||
<DDTT tab="active" onTabChange={() => {}} count={3} search="" onSearchChange={() => {}} />
|
||||
<DDTH />
|
||||
<DDR 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} onClick={() => {}} />
|
||||
<DDR hostname="jumpbox-east.steam.internal" ip="10.42.4.7" type="Linux server" failingMetrics={[{ metric_id: 'AUTH-MFA', category: 'Access & MFA' }]} seenCount={4} hasNotes={true} onClick={() => {}} />
|
||||
<DDR 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} onClick={() => {}} />
|
||||
</DDT>
|
||||
</CSpecimen>
|
||||
</CSection>
|
||||
|
||||
<CSection id="reference" eyebrow="05 — Reference" title="Full Compliance page" blurb="Every primitive composed exactly as CompliancePage.js renders. The frame below is scrollable.">
|
||||
<div className="sample-frame" style={{
|
||||
border: `1px solid ${dA(DCC.teal, 0.20)}`, borderRadius: 12,
|
||||
overflow: 'hidden', maxHeight: 900, overflowY: 'auto',
|
||||
background: 'var(--bg-page)',
|
||||
}}>
|
||||
<DPage />
|
||||
</div>
|
||||
</CSection>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.COMP_DOCS = { CKitDocs };
|
||||
36
docs/design-system-redesign/ui_kits/compliance/README.md
Normal file
36
docs/design-system-redesign/ui_kits/compliance/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Compliance UI Kit
|
||||
|
||||
Visual vocabulary for the AEO Compliance view (`CompliancePage.js`).
|
||||
|
||||
## Files
|
||||
- `index.html` — entry point.
|
||||
- `CompPrimitives.jsx` — `CompPageHeader`, `CompButton`, `TeamTabs`, `MetricHealthCard`, `VariantPill`, `StatusRibbon`, `MetricBadge`, `SeenBadge`, `DeviceTable`/`DeviceRow`, `ChartCard`, `DefinitionTooltip`, `RollbackDialog`, `RollbackToast`, `CompIcon`.
|
||||
- `CompliancePage.jsx` — full-page assembly.
|
||||
- `KitDocs.jsx` — Overview · Tokens · Components · Assemblies · Reference.
|
||||
|
||||
## Identity
|
||||
| Surface | Color | Hex |
|
||||
|----------------------|--------|-----------|
|
||||
| Page title + glow | teal | `#14B8A6` |
|
||||
| Active team tab | teal | `#14B8A6` |
|
||||
| Upload Report CTA | teal | `#14B8A6` |
|
||||
| Selected device row | teal | `#14B8A6` |
|
||||
|
||||
## Status colors (target adherence)
|
||||
| Status | Color | Hex |
|
||||
|-------------------------|--------|-----------|
|
||||
| Meets/Exceeds Target | green | `#10B981` |
|
||||
| Within 15% of Target | amber | `#F59E0B` |
|
||||
| Below 15% of Target | red | `#EF4444` |
|
||||
|
||||
## Category colors (badge tinting)
|
||||
red · Vulnerability Management — amber · Access & MFA — purple · Logging & Monitoring — orange · End-of-Life OS / Endpoint Protection — sky · Application Security — slate · Asset Data Quality / Decommissioned
|
||||
|
||||
## Layout
|
||||
Page header → team tabs → metric health row → 3×2 chart grid → device table.
|
||||
|
||||
## Page-level rules
|
||||
1. Status colors are reserved for target adherence; never decorative.
|
||||
2. A family's `worstStatus` (lowest-severity variant) drives card border + ribbon — one bad variant turns the whole family red.
|
||||
3. Clicking a metric card filters the device table to its IDs; an "× clear filter" button is the only escape hatch shown inline in the section label.
|
||||
4. SeenBadge escalates slate (1×) → amber (2–3×) → red (4×+).
|
||||
30
docs/design-system-redesign/ui_kits/compliance/index.html
Normal file
30
docs/design-system-redesign/ui_kits/compliance/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>STEAM Security · Compliance UI Kit</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="../../colors_and_type.css" />
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; }
|
||||
.page-bg { min-height: 100vh; background: var(--bg-page); }
|
||||
:target { scroll-margin-top: 120px; }
|
||||
.sample-frame::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.sample-frame::-webkit-scrollbar-thumb { background: rgba(20,184,166,0.25); border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="page-bg"></div>
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<script type="text/babel" src="CompPrimitives.jsx"></script>
|
||||
<script type="text/babel" src="CompliancePage.jsx"></script>
|
||||
<script type="text/babel" src="KitDocs.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const { CKitDocs } = window.COMP_DOCS;
|
||||
function App() { return <main data-screen-label="Compliance Kit"><CKitDocs /></main>; }
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user