Files
cve-dashboard/docs/design-system-redesign/ui_kits/compliance/CompPrimitives.jsx
root 27192dd69f WIP: Dashboard redesign — design system overhaul and component updates
Frontend redesign in progress: updated styles, layout, and components
across all pages to align with new design system. Includes Jira API
compliance specs, property tests, and load test script.
2026-04-29 14:20:23 +00:00

619 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
};