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>
|
||||
151
docs/design-system-redesign/ui_kits/cve-dashboard/AppShell.jsx
Normal file
151
docs/design-system-redesign/ui_kits/cve-dashboard/AppShell.jsx
Normal file
@@ -0,0 +1,151 @@
|
||||
// AppShell.jsx — top bar, nav drawer, user menu for the STEAM Security Dashboard.
|
||||
const { useState: useState_AS } = React;
|
||||
const { Icon: I_AS, GroupBadge: GB_AS } = window.SDS;
|
||||
|
||||
function TopBar({ user, currentPage, onNav, onMenuClick }) {
|
||||
return (
|
||||
<header style={{
|
||||
height: 56, position: 'sticky', top: 0, zIndex: 50,
|
||||
background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-1)',
|
||||
display: 'flex', alignItems: 'center', padding: '0 20px', gap: 16,
|
||||
}}>
|
||||
<button onClick={onMenuClick} style={{
|
||||
background: 'transparent', border: 'none', color: 'var(--fg-2)',
|
||||
cursor: 'pointer', padding: 6, display: 'flex', alignItems: 'center',
|
||||
}}><I_AS.Menu size={20} /></button>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<I_AS.Shield size={22} style={{ color: 'var(--accent)' }} />
|
||||
<div>
|
||||
<div style={{ font: '700 15px var(--font-ui)', color: 'var(--fg-1)', letterSpacing: '0.02em', lineHeight: 1 }}>STEAM</div>
|
||||
<div style={{ font: '500 9px var(--font-ui)', color: 'var(--fg-muted)', letterSpacing: '0.18em', marginTop: 2 }}>SECURITY</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav style={{ display: 'flex', gap: 2, marginLeft: 24 }}>
|
||||
{['Home', 'Reporting', 'Compliance', 'Knowledge Base', 'Exports'].map(p => (
|
||||
<NavTab key={p} label={p} active={currentPage === p} onClick={() => onNav(p)} />
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
<UserMenu user={user} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function NavTab({ label, active, onClick }) {
|
||||
const [hover, setHover] = useState_AS(false);
|
||||
return (
|
||||
<button onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
background: active ? 'var(--accent-soft)' : (hover ? 'var(--bg-elevated)' : 'transparent'),
|
||||
color: active ? 'var(--accent)' : 'var(--fg-2)',
|
||||
border: 'none', borderRadius: 6,
|
||||
padding: '8px 12px', fontFamily: 'var(--font-ui)', fontWeight: active ? 600 : 500, fontSize: 13,
|
||||
cursor: 'pointer', transition: 'background 150ms, color 150ms',
|
||||
}}>{label}</button>
|
||||
);
|
||||
}
|
||||
|
||||
function UserMenu({ user }) {
|
||||
const [open, setOpen] = useState_AS(false);
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button onClick={() => setOpen(!open)} style={{
|
||||
background: 'transparent', border: '1px solid var(--border-1)', borderRadius: 6,
|
||||
padding: '6px 10px', display: 'flex', alignItems: 'center', gap: 10,
|
||||
color: 'var(--fg-1)', cursor: 'pointer', fontFamily: 'var(--font-ui)', fontSize: 13,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 26, height: 26, borderRadius: '50%', background: 'var(--accent-soft)',
|
||||
color: 'var(--accent)', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 11,
|
||||
}}>{user.name.split(' ').map(p => p[0]).join('').slice(0, 2)}</div>
|
||||
<span>{user.name}</span>
|
||||
<I_AS.ChevronD size={14} />
|
||||
</button>
|
||||
{open && (
|
||||
<div style={{
|
||||
position: 'absolute', right: 0, top: '110%', minWidth: 240,
|
||||
background: 'var(--bg-surface)', border: '1px solid var(--border-1)',
|
||||
borderRadius: 8, boxShadow: 'var(--shadow-popover)', padding: 8, zIndex: 60,
|
||||
}}>
|
||||
<div style={{ padding: '8px 10px', borderBottom: '1px solid var(--border-1)', marginBottom: 6 }}>
|
||||
<div style={{ font: '600 13px var(--font-ui)', color: 'var(--fg-1)' }}>{user.name}</div>
|
||||
<div style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)', marginTop: 2 }}>{user.email}</div>
|
||||
<div style={{ marginTop: 8 }}><GB_AS group={user.group} /></div>
|
||||
</div>
|
||||
{['Manage Users', 'Audit Log', 'Settings', 'Sign Out'].map((it, i) => (
|
||||
<MenuItem key={it} label={it} danger={i === 3} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem({ label, danger }) {
|
||||
const [hover, setHover] = useState_AS(false);
|
||||
return <button
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
width: '100%', textAlign: 'left',
|
||||
background: hover ? 'var(--bg-elevated)' : 'transparent',
|
||||
color: danger ? 'var(--sev-critical)' : 'var(--fg-2)',
|
||||
border: 'none', borderRadius: 4, padding: '7px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 13, cursor: 'pointer',
|
||||
}}>{label}</button>;
|
||||
}
|
||||
|
||||
function NavDrawer({ open, onClose, currentPage, onNav, isAdmin }) {
|
||||
if (!open) return null;
|
||||
const items = [
|
||||
{ label: 'Home', icon: I_AS.Activity },
|
||||
{ label: 'Reporting', icon: I_AS.FileText },
|
||||
{ label: 'Compliance', icon: I_AS.Shield },
|
||||
{ label: 'Knowledge Base', icon: I_AS.Folder },
|
||||
{ label: 'Exports', icon: I_AS.Download },
|
||||
...(isAdmin ? [{ label: 'Admin Panel', icon: I_AS.Users }] : []),
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div onClick={onClose} style={{
|
||||
position: 'fixed', inset: 0, background: 'var(--bg-overlay)',
|
||||
backdropFilter: 'blur(4px)', zIndex: 60,
|
||||
}} />
|
||||
<aside style={{
|
||||
position: 'fixed', left: 0, top: 0, bottom: 0, width: 240, zIndex: 61,
|
||||
background: 'var(--bg-surface)', borderRight: '1px solid var(--border-1)',
|
||||
padding: 16, display: 'flex', flexDirection: 'column', gap: 4,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, padding: '4px 6px' }}>
|
||||
<span style={{ font: '600 11px var(--font-ui)', color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Navigation</span>
|
||||
<button onClick={onClose} style={{ background: 'transparent', border: 'none', color: 'var(--fg-muted)', cursor: 'pointer', display: 'flex' }}><I_AS.X size={16} /></button>
|
||||
</div>
|
||||
{items.map(it => (
|
||||
<DrawerItem key={it.label} {...it} active={currentPage === it.label}
|
||||
onClick={() => { onNav(it.label); onClose(); }} />
|
||||
))}
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerItem({ label, icon: IcCmp, active, onClick }) {
|
||||
const [hover, setHover] = useState_AS(false);
|
||||
return <button onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
background: active ? 'var(--accent-soft)' : (hover ? 'var(--bg-elevated)' : 'transparent'),
|
||||
color: active ? 'var(--accent)' : 'var(--fg-2)',
|
||||
border: 'none', borderRadius: 6, padding: '9px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontWeight: active ? 600 : 500, fontSize: 13,
|
||||
cursor: 'pointer', textAlign: 'left',
|
||||
}}><IcCmp size={16} />{label}</button>;
|
||||
}
|
||||
|
||||
window.SDS_Shell = { TopBar, NavDrawer };
|
||||
@@ -0,0 +1,351 @@
|
||||
// KnowledgeBase.jsx — recreation of the Knowledge Base page.
|
||||
const { useState: useState_KB, useMemo: useMemo_KB } = React;
|
||||
const { Button: Btn_KB, Card: Card_KB, Field: F_KB, Input: In_KB, Select: Sel_KB,
|
||||
EmptyState: ES_KB, Icon: I_KB } = window.SDS;
|
||||
|
||||
const KB_ARTICLES = [
|
||||
{ id: 1, title: 'NVD CVE Triage Runbook', category: 'Runbooks',
|
||||
description: 'Standard procedure for triaging incoming NVD-sourced CVEs across vendor pairs.',
|
||||
type: 'pdf', size: '412 KB', date: '2026-04-22', author: 'jramos', exts: ['pdf'] },
|
||||
{ id: 2, title: 'FP Workflow Submission Guide', category: 'Runbooks',
|
||||
description: 'How to compile evidence and submit False Positive workflows through the Ivanti Queue.',
|
||||
type: 'md', size: '24 KB', date: '2026-04-18', author: 'mhall' },
|
||||
{ id: 3, title: 'Cisco IOS-XE Advisory · cisco-sa-2024-0341', category: 'Vendor Advisories',
|
||||
description: 'Vendor advisory for Cisco IOS-XE Web UI privilege escalation. Linked to 12 host findings.',
|
||||
type: 'pdf', size: '1.3 MB', date: '2026-04-15', author: 'jramos' },
|
||||
{ id: 4, title: 'AEO Compliance Schema Reference', category: 'Policies',
|
||||
description: 'Authoritative metric ID list for the NTS_AEO weekly report. Used by the drift checker.',
|
||||
type: 'md', size: '38 KB', date: '2026-04-09', author: 'kpatel' },
|
||||
{ id: 5, title: 'Archer Risk Acceptance Process', category: 'Policies',
|
||||
description: 'EXC ticket lifecycle, required documentation, and standard SLAs for risk acceptance.',
|
||||
type: 'docx', size: '186 KB', date: '2026-04-02', author: 'mhall' },
|
||||
{ id: 6, title: 'Q2 Vulnerability Posture Briefing', category: 'Reports',
|
||||
description: 'Leadership briefing on Critical/High remediation throughput for FY26-Q2.',
|
||||
type: 'pptx', size: '4.7 MB', date: '2026-03-30', author: 'jramos' },
|
||||
{ id: 7, title: 'Ivanti / RiskSense API Integration Notes', category: 'Internal Docs',
|
||||
description: 'Authentication, BU filters, severity range tuning, and rate-limit notes.',
|
||||
type: 'md', size: '11 KB', date: '2026-03-21', author: 'kpatel' },
|
||||
{ id: 8, title: 'CVSS Severity Cascade Rules', category: 'Internal Docs',
|
||||
description: 'How v3.1 → v3.0 → v2.0 fallback is applied when scoring CVEs from NVD.',
|
||||
type: 'md', size: '6 KB', date: '2026-03-14', author: 'mhall' },
|
||||
];
|
||||
|
||||
const CATEGORIES = ['All', 'Runbooks', 'Vendor Advisories', 'Policies', 'Reports', 'Internal Docs'];
|
||||
|
||||
const TYPE_COLORS = {
|
||||
pdf: { c: '#EF4444', label: 'PDF' },
|
||||
md: { c: '#38BDF8', label: 'MD' },
|
||||
docx: { c: '#7DD3FC', label: 'DOCX' },
|
||||
pptx: { c: '#F59E0B', label: 'PPTX' },
|
||||
xlsx: { c: '#10B981', label: 'XLSX' },
|
||||
};
|
||||
|
||||
function FileTypeChip({ type }) {
|
||||
const v = TYPE_COLORS[type] || { c: 'var(--fg-muted)', label: type.toUpperCase() };
|
||||
return <span style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 36, height: 36, borderRadius: 6,
|
||||
background: 'var(--bg-elevated)', border: `1px solid ${v.c}`,
|
||||
color: v.c, fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10,
|
||||
flexShrink: 0,
|
||||
}}>{v.label}</span>;
|
||||
}
|
||||
|
||||
function ArticleRow({ article, onOpen, onDownload }) {
|
||||
const [hover, setHover] = useState_KB(false);
|
||||
return (
|
||||
<div onClick={onOpen}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
padding: '14px 18px',
|
||||
background: hover
|
||||
? 'linear-gradient(90deg, rgba(14,165,233,0.10) 0%, rgba(14,165,233,0.04) 100%)'
|
||||
: 'transparent',
|
||||
borderBottom: '1px solid var(--border-subtle)',
|
||||
boxShadow: hover ? 'inset 3px 0 0 var(--intel-accent)' : 'none',
|
||||
cursor: 'pointer', transition: 'all 200ms cubic-bezier(0.4,0,0.2,1)',
|
||||
}}>
|
||||
<FileTypeChip type={article.type} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ font: '600 14px var(--font-ui)', color: 'var(--fg-1)', marginBottom: 3,
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{article.title}</div>
|
||||
<div style={{ font: '400 12px var(--font-ui)', color: 'var(--fg-muted)',
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{article.description}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4, minWidth: 110 }}>
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 4, background: 'var(--bg-elevated)',
|
||||
color: 'var(--fg-2)', font: '500 11px var(--font-ui)',
|
||||
}}>{article.category}</span>
|
||||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)' }}>{article.date}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4, minWidth: 90 }}>
|
||||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)' }}>{article.size}</span>
|
||||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-disabled)' }}>{article.author}</span>
|
||||
</div>
|
||||
<button onClick={(e) => { e.stopPropagation(); onDownload(article); }} style={{
|
||||
background: 'transparent', border: '1px solid var(--border-1)', borderRadius: 6,
|
||||
padding: 7, cursor: 'pointer', color: 'var(--fg-2)', display: 'flex',
|
||||
}} title="Download"><I_KB.Download size={14} /></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryPill({ label, active, count, onClick }) {
|
||||
const [hover, setHover] = useState_KB(false);
|
||||
return <button onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 14px', borderRadius: 6,
|
||||
background: active
|
||||
? 'linear-gradient(135deg, rgba(14,165,233,0.20) 0%, rgba(14,165,233,0.12) 100%)'
|
||||
: (hover ? 'rgba(14,165,233,0.08)' : 'transparent'),
|
||||
color: active ? 'var(--intel-accent-bright)' : 'var(--fg-2)',
|
||||
border: `1px solid ${active ? 'var(--intel-accent)' : 'var(--border-default)'}`,
|
||||
font: `700 11px var(--font-mono)`, textTransform: 'uppercase', letterSpacing: '0.5px',
|
||||
textShadow: active ? '0 0 8px rgba(14,165,233,0.4)' : 'none',
|
||||
boxShadow: active ? '0 0 16px rgba(14,165,233,0.20)' : 'none',
|
||||
cursor: 'pointer', transition: 'all 200ms cubic-bezier(0.4,0,0.2,1)',
|
||||
}}>{label}<span style={{
|
||||
font: '700 10px var(--font-mono)', color: active ? 'var(--intel-accent-bright)' : 'var(--fg-muted)',
|
||||
padding: '1px 6px', borderRadius: 999,
|
||||
background: active ? 'rgba(14,165,233,0.15)' : 'rgba(148,163,184,0.10)',
|
||||
}}>{count}</span></button>;
|
||||
}
|
||||
|
||||
function KnowledgeBaseViewer({ article, onClose }) {
|
||||
return (
|
||||
<>
|
||||
<div onClick={onClose} style={{
|
||||
position: 'fixed', inset: 0, background: 'var(--bg-overlay)',
|
||||
backdropFilter: 'blur(4px)', zIndex: 100,
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'fixed', right: 0, top: 0, bottom: 0, width: 'min(640px, 100vw)',
|
||||
background: 'var(--bg-surface)', borderLeft: '1px solid var(--border-1)',
|
||||
boxShadow: 'var(--shadow-modal)', zIndex: 101,
|
||||
display: 'flex', flexDirection: 'column', animation: 'slideIn 240ms cubic-bezier(0.16,1,0.3,1)',
|
||||
}}>
|
||||
<header style={{
|
||||
padding: '20px 24px', borderBottom: '1px solid var(--border-1)',
|
||||
display: 'flex', alignItems: 'flex-start', gap: 14,
|
||||
}}>
|
||||
<FileTypeChip type={article.type} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ font: '600 16px var(--font-ui)', color: 'var(--fg-1)' }}>{article.title}</div>
|
||||
<div style={{ font: '400 12px var(--font-mono)', color: 'var(--fg-muted)', marginTop: 4 }}>
|
||||
{article.category} · {article.size} · {article.date} · {article.author}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} style={{
|
||||
background: 'transparent', border: 'none', color: 'var(--fg-muted)',
|
||||
cursor: 'pointer', padding: 6, display: 'flex',
|
||||
}}><I_KB.X size={18} /></button>
|
||||
</header>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '24px' }}>
|
||||
<div style={{ font: '500 11px var(--font-ui)', color: 'var(--fg-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 8 }}>Description</div>
|
||||
<div style={{ font: '400 14px/1.6 var(--font-ui)', color: 'var(--fg-2)', marginBottom: 24 }}>
|
||||
{article.description}
|
||||
</div>
|
||||
|
||||
<div style={{ font: '500 11px var(--font-ui)', color: 'var(--fg-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 8 }}>Preview</div>
|
||||
<div style={{
|
||||
background: 'var(--bg-page)', border: '1px solid var(--border-1)',
|
||||
borderRadius: 8, padding: 24, minHeight: 320,
|
||||
font: '400 13px/1.7 var(--font-ui)', color: 'var(--fg-2)',
|
||||
}}>
|
||||
<h3 style={{ font: '600 18px var(--font-ui)', color: 'var(--fg-1)', margin: '0 0 12px' }}>
|
||||
{article.title}
|
||||
</h3>
|
||||
<p style={{ margin: '0 0 12px' }}>
|
||||
This document is rendered inline in a sandboxed iframe (PDF) or as sanitised HTML
|
||||
from the <code style={{ font: '500 12px var(--font-mono)', color: 'var(--accent)',
|
||||
background: 'var(--bg-elevated)', padding: '1px 6px', borderRadius: 3 }}>
|
||||
react-markdown</code> + <code style={{ font: '500 12px var(--font-mono)', color: 'var(--accent)',
|
||||
background: 'var(--bg-elevated)', padding: '1px 6px', borderRadius: 3 }}>rehype-sanitize</code> pipeline.
|
||||
</p>
|
||||
<p style={{ margin: '0 0 12px' }}>
|
||||
Authenticated users in any group may view; only Admin and Standard_User may upload or delete.
|
||||
</p>
|
||||
<ul style={{ margin: '0 0 12px 18px', padding: 0 }}>
|
||||
<li>Allowed types: PDF, MD, TXT, Office, HTML, JSON, YAML, images</li>
|
||||
<li>10 MB per-file limit · file extension allowlist</li>
|
||||
<li>Standard_User can delete only articles they created</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style={{
|
||||
padding: '16px 24px', borderTop: '1px solid var(--border-1)',
|
||||
display: 'flex', gap: 8, justifyContent: 'flex-end',
|
||||
}}>
|
||||
<Btn_KB variant="ghost" icon={<I_KB.External size={14} />}>Open in tab</Btn_KB>
|
||||
<Btn_KB variant="primary" icon={<I_KB.Download size={14} />}>Download</Btn_KB>
|
||||
</footer>
|
||||
</div>
|
||||
<style>{`@keyframes slideIn{from{transform:translateX(100%)}to{transform:translateX(0)}}`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadModal({ onClose }) {
|
||||
const [drag, setDrag] = useState_KB(false);
|
||||
return (
|
||||
<>
|
||||
<div onClick={onClose} style={{
|
||||
position: 'fixed', inset: 0, background: 'var(--bg-overlay)',
|
||||
backdropFilter: 'blur(4px)', zIndex: 100,
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%, -50%)',
|
||||
width: 'min(560px, 92vw)', background: 'var(--bg-surface)',
|
||||
border: '1px solid var(--border-1)', borderRadius: 12,
|
||||
boxShadow: 'var(--shadow-modal)', zIndex: 101,
|
||||
}}>
|
||||
<header style={{
|
||||
padding: '20px 24px', borderBottom: '1px solid var(--border-1)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<div style={{ font: '600 16px var(--font-ui)', color: 'var(--fg-1)' }}>Upload Article</div>
|
||||
<button onClick={onClose} style={{
|
||||
background: 'transparent', border: 'none', color: 'var(--fg-muted)',
|
||||
cursor: 'pointer', padding: 6, display: 'flex',
|
||||
}}><I_KB.X size={18} /></button>
|
||||
</header>
|
||||
|
||||
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<F_KB label="Title"><In_KB placeholder="e.g. Cisco IOS-XE Advisory · cisco-sa-2024-0341" /></F_KB>
|
||||
<F_KB label="Category">
|
||||
<Sel_KB defaultValue="Runbooks">
|
||||
{CATEGORIES.filter(c => c !== 'All').map(c => <option key={c}>{c}</option>)}
|
||||
</Sel_KB>
|
||||
</F_KB>
|
||||
<F_KB label="Description">
|
||||
<textarea placeholder="Short description for the library list…" rows={3} style={{
|
||||
background: 'var(--bg-input)', color: 'var(--fg-1)',
|
||||
border: '1px solid var(--border-1)', borderRadius: 6, padding: '8px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 13, outline: 'none', resize: 'vertical',
|
||||
}} />
|
||||
</F_KB>
|
||||
|
||||
<div
|
||||
onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
|
||||
onDragLeave={() => setDrag(false)}
|
||||
onDrop={(e) => { e.preventDefault(); setDrag(false); }}
|
||||
style={{
|
||||
border: `2px dashed ${drag ? 'var(--accent)' : 'var(--border-2)'}`,
|
||||
borderRadius: 8, padding: 24, textAlign: 'center',
|
||||
background: drag ? 'var(--accent-soft)' : 'var(--bg-page)',
|
||||
transition: 'all 150ms',
|
||||
}}>
|
||||
<div style={{ color: drag ? 'var(--accent)' : 'var(--fg-muted)', display: 'flex', justifyContent: 'center', marginBottom: 8 }}>
|
||||
<I_KB.Upload size={28} />
|
||||
</div>
|
||||
<div style={{ font: '600 13px var(--font-ui)', color: 'var(--fg-1)' }}>Drop file or click to browse</div>
|
||||
<div style={{ font: '400 11px var(--font-mono)', color: 'var(--fg-muted)', marginTop: 4 }}>
|
||||
PDF · MD · DOCX · XLSX · PPTX · TXT — max 10 MB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style={{
|
||||
padding: '16px 24px', borderTop: '1px solid var(--border-1)',
|
||||
display: 'flex', gap: 8, justifyContent: 'flex-end',
|
||||
}}>
|
||||
<Btn_KB variant="ghost" onClick={onClose}>Cancel</Btn_KB>
|
||||
<Btn_KB variant="primary" icon={<I_KB.Upload size={14} />}>Upload</Btn_KB>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function KnowledgeBasePage() {
|
||||
const [search, setSearch] = useState_KB('');
|
||||
const [category, setCategory] = useState_KB('All');
|
||||
const [active, setActive] = useState_KB(null);
|
||||
const [uploading, setUploading] = useState_KB(false);
|
||||
|
||||
const counts = useMemo_KB(() => {
|
||||
const c = { All: KB_ARTICLES.length };
|
||||
KB_ARTICLES.forEach(a => { c[a.category] = (c[a.category] || 0) + 1; });
|
||||
return c;
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo_KB(() => KB_ARTICLES.filter(a => {
|
||||
if (category !== 'All' && a.category !== category) return false;
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
return a.title.toLowerCase().includes(q) || a.description.toLowerCase().includes(q);
|
||||
}
|
||||
return true;
|
||||
}), [search, category]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px 24px 48px', maxWidth: 1280, margin: '0 auto' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
|
||||
<div>
|
||||
<h1 style={{
|
||||
font: '700 24px var(--font-mono)', color: 'var(--intel-accent-bright)',
|
||||
margin: 0, textTransform: 'uppercase', letterSpacing: '0.10em',
|
||||
textShadow: 'var(--glow-heading)',
|
||||
}}>Knowledge Base</h1>
|
||||
<div style={{ font: '400 13px var(--font-ui)', color: 'var(--fg-muted)', marginTop: 6 }}>
|
||||
Internal reference material — runbooks, advisories, policies
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Btn_KB variant="ghost" icon={<I_KB.Download size={14} />}>Export List</Btn_KB>
|
||||
<Btn_KB variant="primary" icon={<I_KB.FilePlus size={14} />} onClick={() => setUploading(true)}>Upload Article</Btn_KB>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ flex: 1, maxWidth: 420 }}>
|
||||
<In_KB icon={<I_KB.Search size={14} />}
|
||||
placeholder="Search title or description…"
|
||||
value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
</div>
|
||||
<Sel_KB defaultValue="newest" style={{ minWidth: 160 }}>
|
||||
<option value="newest">Newest first</option>
|
||||
<option value="oldest">Oldest first</option>
|
||||
<option value="title">Title A–Z</option>
|
||||
</Sel_KB>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 16 }}>
|
||||
{CATEGORIES.map(c => (
|
||||
<CategoryPill key={c} label={c} count={counts[c] || 0}
|
||||
active={category === c} onClick={() => setCategory(c)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card_KB padding={0}>
|
||||
{filtered.length === 0 ? (
|
||||
<ES_KB icon={<I_KB.FileText size={32} />}
|
||||
title="No articles match"
|
||||
message="Try clearing the search box or selecting a different category." />
|
||||
) : filtered.map((a, i) => (
|
||||
<ArticleRow key={a.id} article={a}
|
||||
onOpen={() => setActive(a)}
|
||||
onDownload={(art) => console.log('download', art.title)} />
|
||||
))}
|
||||
</Card_KB>
|
||||
|
||||
<div style={{ marginTop: 12, font: '400 12px var(--font-mono)', color: 'var(--fg-muted)' }}>
|
||||
{filtered.length} article{filtered.length === 1 ? '' : 's'}
|
||||
{category !== 'All' && <> · filtered to <span style={{ color: 'var(--accent)' }}>{category}</span></>}
|
||||
</div>
|
||||
|
||||
{active && <KnowledgeBaseViewer article={active} onClose={() => setActive(null)} />}
|
||||
{uploading && <UploadModal onClose={() => setUploading(false)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.SDS_KB = { KnowledgeBasePage };
|
||||
250
docs/design-system-redesign/ui_kits/cve-dashboard/Primitives.jsx
Normal file
250
docs/design-system-redesign/ui_kits/cve-dashboard/Primitives.jsx
Normal file
@@ -0,0 +1,250 @@
|
||||
// Primitives.jsx — shared low-level UI for the STEAM Security Dashboard kit.
|
||||
// Plain inline styles + token CSS variables. No external libs.
|
||||
|
||||
const { useState } = React;
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────── */
|
||||
function Button({ variant = 'secondary', size = 'md', icon, children, onClick, disabled, style, ...rest }) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const sizing = size === 'sm'
|
||||
? { padding: '6px 12px', fontSize: 11 }
|
||||
: { padding: '10px 18px', fontSize: 13 };
|
||||
const variants = {
|
||||
primary: {
|
||||
bgRest: 'linear-gradient(135deg, rgba(14,165,233,0.15) 0%, rgba(14,165,233,0.10) 100%)',
|
||||
bgHover: 'linear-gradient(135deg, rgba(14,165,233,0.25) 0%, rgba(14,165,233,0.20) 100%)',
|
||||
bd: '#0EA5E9', fg: '#38BDF8',
|
||||
glow: '0 0 20px rgba(14,165,233,0.25)', tshadow: '0 0 6px rgba(14,165,233,0.2)',
|
||||
},
|
||||
success: {
|
||||
bgRest: 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(16,185,129,0.10) 100%)',
|
||||
bgHover: 'linear-gradient(135deg, rgba(16,185,129,0.25) 0%, rgba(16,185,129,0.20) 100%)',
|
||||
bd: '#10B981', fg: '#34D399',
|
||||
glow: '0 0 20px rgba(16,185,129,0.25)', tshadow: '0 0 6px rgba(16,185,129,0.2)',
|
||||
},
|
||||
danger: {
|
||||
bgRest: 'linear-gradient(135deg, rgba(239,68,68,0.15) 0%, rgba(239,68,68,0.10) 100%)',
|
||||
bgHover: 'linear-gradient(135deg, rgba(239,68,68,0.25) 0%, rgba(239,68,68,0.20) 100%)',
|
||||
bd: '#EF4444', fg: '#F87171',
|
||||
glow: '0 0 20px rgba(239,68,68,0.25)', tshadow: '0 0 6px rgba(239,68,68,0.2)',
|
||||
},
|
||||
secondary: {
|
||||
bgRest: 'transparent', bgHover: 'rgba(14,165,233,0.08)',
|
||||
bd: 'rgba(14,165,233,0.30)', fg: 'var(--fg-2)',
|
||||
glow: 'none', tshadow: 'none',
|
||||
},
|
||||
ghost: {
|
||||
bgRest: 'transparent', bgHover: 'rgba(14,165,233,0.06)',
|
||||
bd: 'transparent', fg: 'var(--fg-3)',
|
||||
glow: 'none', tshadow: 'none',
|
||||
},
|
||||
};
|
||||
const v = variants[variant] || variants.secondary;
|
||||
return (
|
||||
<button onClick={onClick} disabled={disabled}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: disabled ? 'var(--bg-elevated)' : (hover ? v.bgHover : v.bgRest),
|
||||
color: disabled ? 'var(--fg-disabled)' : v.fg,
|
||||
border: `1px solid ${disabled ? 'var(--border-1)' : v.bd}`,
|
||||
borderRadius: 6,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.5px',
|
||||
textShadow: v.tshadow,
|
||||
boxShadow: hover && !disabled
|
||||
? `${v.glow}, 0 4px 12px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.10)`
|
||||
: '0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.10)',
|
||||
transform: hover && !disabled ? 'translateY(-1px)' : 'translateY(0)',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 300ms cubic-bezier(0.4,0,0.2,1)',
|
||||
...sizing, ...style,
|
||||
}} {...rest}>
|
||||
{icon}{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Severity badge — gradient + pulse-glow dot ──────────────── */
|
||||
if (typeof document !== 'undefined' && !document.getElementById('sds-pulse-glow')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'sds-pulse-glow';
|
||||
s.textContent = '@keyframes sds-pulse-glow{0%,100%{opacity:1}50%{opacity:0.7}}';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
function SeverityBadge({ level, score }) {
|
||||
const map = {
|
||||
Critical: { c: '#EF4444', text: '#FCA5A5', glow: '0 0 12px rgba(239,68,68,0.6), 0 0 6px rgba(239,68,68,0.4)' },
|
||||
High: { c: '#F59E0B', text: '#FCD34D', glow: '0 0 12px rgba(245,158,11,0.6), 0 0 6px rgba(245,158,11,0.4)' },
|
||||
Medium: { c: '#0EA5E9', text: '#7DD3FC', glow: '0 0 12px rgba(14,165,233,0.6), 0 0 6px rgba(14,165,233,0.4)' },
|
||||
Low: { c: '#10B981', text: '#6EE7B7', glow: '0 0 12px rgba(16,185,129,0.6), 0 0 6px rgba(16,185,129,0.4)' },
|
||||
};
|
||||
const v = map[level] || map.Medium;
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 14px', borderRadius: 6,
|
||||
background: `linear-gradient(135deg, ${v.c}33 0%, ${v.c}26 100%)`,
|
||||
color: v.text, border: `2px solid ${v.c}99`,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 11,
|
||||
letterSpacing: '0.5px', textTransform: 'uppercase',
|
||||
textShadow: `0 0 8px ${v.c}66`,
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.4)',
|
||||
}}>
|
||||
<span style={{
|
||||
width: 8, height: 8, borderRadius: '50%', background: v.c,
|
||||
boxShadow: v.glow, animation: 'sds-pulse-glow 2s ease-in-out infinite',
|
||||
}} />
|
||||
{level.toUpperCase()}{score && <span style={{ marginLeft: 4 }}>{score}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── SLA pill ────────────────────────────────────────────────── */
|
||||
function SlaPill({ status }) {
|
||||
const map = {
|
||||
OVERDUE: { c: 'var(--sev-critical)', bg: 'var(--sev-critical-bg)' },
|
||||
AT_RISK: { c: 'var(--sev-high)', bg: 'var(--sev-high-bg)' },
|
||||
WITHIN_SLA: { c: 'var(--sev-low)', bg: 'var(--sev-low-bg)' },
|
||||
};
|
||||
const v = map[status] || map.WITHIN_SLA;
|
||||
return <span style={{
|
||||
padding: '2px 9px', borderRadius: 999, background: v.bg, color: v.c,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10, letterSpacing: '.05em',
|
||||
}}>{status}</span>;
|
||||
}
|
||||
|
||||
/* ── Group badge ─────────────────────────────────────────────── */
|
||||
function GroupBadge({ group }) {
|
||||
const map = {
|
||||
Admin: { c: 'var(--group-admin)', bg: 'rgba(239,68,68,0.10)' },
|
||||
Standard_User: { c: 'var(--group-standard)', bg: 'rgba(56,189,248,0.10)' },
|
||||
Leadership: { c: 'var(--group-leadership)', bg: 'rgba(245,158,11,0.10)' },
|
||||
Read_Only: { c: 'var(--group-readonly)', bg: 'rgba(148,163,184,0.10)' },
|
||||
};
|
||||
const v = map[group] || map.Read_Only;
|
||||
const label = group.replace('_', ' ');
|
||||
return <span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '3px 9px', borderRadius: 999,
|
||||
color: v.c, background: v.bg, border: `1px solid ${v.c}`,
|
||||
fontFamily: 'var(--font-ui)', fontWeight: 600, fontSize: 11,
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: v.c }} />
|
||||
{label}
|
||||
</span>;
|
||||
}
|
||||
|
||||
/* ── Field / Input ───────────────────────────────────────────── */
|
||||
function Field({ label, children, style }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, ...style }}>
|
||||
{label && <label style={{
|
||||
fontFamily: 'var(--font-ui)', fontWeight: 500, fontSize: 11,
|
||||
color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: '0.06em',
|
||||
}}>{label}</label>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Input({ icon, ...rest }) {
|
||||
const [focus, setFocus] = useState(false);
|
||||
return (
|
||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
||||
{icon && <span style={{ position: 'absolute', left: 10, color: 'var(--fg-muted)', display: 'flex' }}>{icon}</span>}
|
||||
<input
|
||||
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
||||
style={{
|
||||
width: '100%', boxSizing: 'border-box',
|
||||
background: 'var(--bg-input)', color: 'var(--fg-1)',
|
||||
border: `1px solid ${focus ? 'var(--border-focus)' : 'var(--border-1)'}`,
|
||||
boxShadow: focus ? 'var(--shadow-focus)' : 'none',
|
||||
borderRadius: 6, padding: icon ? '8px 10px 8px 32px' : '8px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 13, outline: 'none',
|
||||
transition: 'border 150ms',
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Select({ children, ...rest }) {
|
||||
const [focus, setFocus] = useState(false);
|
||||
return (
|
||||
<select
|
||||
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
||||
style={{
|
||||
background: 'var(--bg-input)', color: 'var(--fg-1)',
|
||||
border: `1px solid ${focus ? 'var(--border-focus)' : 'var(--border-1)'}`,
|
||||
boxShadow: focus ? 'var(--shadow-focus)' : 'none',
|
||||
borderRadius: 6, padding: '8px 10px',
|
||||
fontFamily: 'var(--font-ui)', fontSize: 13, outline: 'none',
|
||||
appearance: 'none',
|
||||
}}
|
||||
{...rest}>
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Card ────────────────────────────────────────────────────── */
|
||||
function Card({ children, style, padding = 20 }) {
|
||||
return <div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(51,65,85,0.9) 50%, rgba(30,41,59,0.95) 100%)',
|
||||
border: '1.5px solid rgba(14,165,233,0.30)',
|
||||
borderRadius: 8, padding,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4), 0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(14,165,233,0.10)',
|
||||
...style,
|
||||
}}>{children}</div>;
|
||||
}
|
||||
|
||||
/* ── Empty state ─────────────────────────────────────────────── */
|
||||
function EmptyState({ icon, title, message, action }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
gap: 12, padding: '48px 24px', color: 'var(--fg-muted)', textAlign: 'center',
|
||||
}}>
|
||||
{icon && <div style={{ color: 'var(--fg-disabled)' }}>{icon}</div>}
|
||||
<div style={{ fontFamily: 'var(--font-ui)', fontWeight: 600, fontSize: 16, color: 'var(--fg-2)' }}>{title}</div>
|
||||
{message && <div style={{ fontFamily: 'var(--font-ui)', fontSize: 13, maxWidth: 360 }}>{message}</div>}
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Lucide icons (inline SVG, currentColor) ─────────────────── */
|
||||
const ic = (path) => ({ size = 16, strokeWidth = 1.75, ...rest }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ flexShrink: 0 }} {...rest}>{path}</svg>
|
||||
);
|
||||
const Icon = {
|
||||
Shield: ic(<><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></>),
|
||||
Search: ic(<><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></>),
|
||||
Filter: ic(<><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></>),
|
||||
Sync: ic(<><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></>),
|
||||
Download: ic(<><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></>),
|
||||
Upload: ic(<><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"/></>),
|
||||
File: ic(<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></>),
|
||||
FileText: ic(<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="13" y2="17"/></>),
|
||||
FilePlus: ic(<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="12" x2="12" y2="18"/><line x1="9" y1="15" x2="15" y2="15"/></>),
|
||||
Folder: ic(<><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></>),
|
||||
Eye: ic(<><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></>),
|
||||
X: ic(<><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></>),
|
||||
ChevronD: ic(<><polyline points="6 9 12 15 18 9"/></>),
|
||||
ChevronR: ic(<><polyline points="9 18 15 12 9 6"/></>),
|
||||
Plus: ic(<><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></>),
|
||||
Menu: ic(<><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></>),
|
||||
Trash: ic(<><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></>),
|
||||
External: ic(<><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></>),
|
||||
Calendar: ic(<><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></>),
|
||||
Activity: ic(<><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></>),
|
||||
Users: ic(<><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></>),
|
||||
Scroll: ic(<><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/></>),
|
||||
Bell: ic(<><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></>),
|
||||
};
|
||||
|
||||
window.SDS = { Button, SeverityBadge, SlaPill, GroupBadge, Field, Input, Select, Card, EmptyState, Icon };
|
||||
30
docs/design-system-redesign/ui_kits/cve-dashboard/README.md
Normal file
30
docs/design-system-redesign/ui_kits/cve-dashboard/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# CVE Dashboard UI Kit
|
||||
|
||||
High-fidelity recreation of the **STEAM Security Dashboard** chrome plus a focused build of the **Knowledge Base** page.
|
||||
|
||||
## Files
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `index.html` | Mounts the kit. Opens to the Knowledge Base page; top-bar nav switches between pages. |
|
||||
| `Primitives.jsx` | `Button`, `Field`, `Input`, `Select`, `Card`, `SeverityBadge`, `SlaPill`, `GroupBadge`, `EmptyState`, `Icon` (lucide line icons inlined as SVG). |
|
||||
| `AppShell.jsx` | Top bar (brand mark + nav + UserMenu), NavDrawer overlay. |
|
||||
| `KnowledgeBase.jsx` | Knowledge Base page · article rows · category filter · upload modal · slide-out viewer. |
|
||||
|
||||
## How to use
|
||||
|
||||
1. Open `index.html` in a browser.
|
||||
2. The header nav lets you switch pages — `Knowledge Base` is fully built; the other tabs render a placeholder.
|
||||
3. Click any article row to open the **viewer panel** (slide-out from the right).
|
||||
4. Click **Upload Article** to open the upload modal.
|
||||
|
||||
## What is NOT built
|
||||
|
||||
This kit intentionally cuts the scope to one page (the Knowledge Base) plus the chrome. Reporting, Compliance, Home, Admin, and Exports are placeholders — the primitives in `Primitives.jsx` and the shell in `AppShell.jsx` are sufficient to compose those surfaces in a few hours.
|
||||
|
||||
## Conventions used
|
||||
|
||||
- All colour, type, spacing, radius, elevation pulls from `../../colors_and_type.css`.
|
||||
- No external icon library — lucide icons are inlined as SVG inside `Icon.*`.
|
||||
- Hover states are JS-driven here (mirrors the legacy dashboard pattern); production code should migrate these to CSS `:hover`.
|
||||
- All data is fake. Network calls are stubbed.
|
||||
66
docs/design-system-redesign/ui_kits/cve-dashboard/index.html
Normal file
66
docs/design-system-redesign/ui_kits/cve-dashboard/index.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>STEAM Security · CVE Dashboard 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); }
|
||||
</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="Primitives.jsx"></script>
|
||||
<script type="text/babel" src="AppShell.jsx"></script>
|
||||
<script type="text/babel" src="KnowledgeBase.jsx"></script>
|
||||
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
const { TopBar, NavDrawer } = window.SDS_Shell;
|
||||
const { KnowledgeBasePage } = window.SDS_KB;
|
||||
const { Card, EmptyState, Icon } = window.SDS;
|
||||
|
||||
const USER = { name: 'J. Ramos', email: 'jramos@steam.local', group: 'Admin' };
|
||||
|
||||
function Placeholder({ name }) {
|
||||
return (
|
||||
<div style={{ padding: '48px 24px', maxWidth: 1280, margin: '0 auto' }}>
|
||||
<h1 style={{ font: '600 24px var(--font-ui)', color: 'var(--fg-1)', margin: '0 0 4px' }}>{name}</h1>
|
||||
<div style={{ font: '400 13px var(--font-ui)', color: 'var(--fg-muted)', marginBottom: 24 }}>
|
||||
This surface is intentionally not built out in the UI kit — primitives + shell are sufficient to compose it.
|
||||
</div>
|
||||
<Card padding={0}>
|
||||
<EmptyState icon={<Icon.FileText size={32} />}
|
||||
title={`${name} placeholder`}
|
||||
message="Open the Knowledge Base tab to see the focused page recreation." />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [page, setPage] = useState('Knowledge Base');
|
||||
const [drawer, setDrawer] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<TopBar user={USER} currentPage={page} onNav={setPage} onMenuClick={() => setDrawer(true)} />
|
||||
<NavDrawer open={drawer} onClose={() => setDrawer(false)}
|
||||
currentPage={page} onNav={setPage} isAdmin={USER.group === 'Admin'} />
|
||||
<main data-screen-label={page}>
|
||||
{page === 'Knowledge Base' ? <KnowledgeBasePage /> : <Placeholder name={page} />}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
371
docs/design-system-redesign/ui_kits/home/HomePage.jsx
Normal file
371
docs/design-system-redesign/ui_kits/home/HomePage.jsx
Normal file
@@ -0,0 +1,371 @@
|
||||
// HomePage.jsx — full-page assembly of the CVE Dashboard Home view.
|
||||
// Rebuilt 1:1 from frontend/src/App.js (currentPage === 'home').
|
||||
//
|
||||
// Layout: top stat row (4 metric cards) → 12-col grid below
|
||||
// • col-span-9 (left): Quick CVE Lookup → Search/Filter → CVE list
|
||||
// • col-span-3 (right): Calendar → Open Tickets → Archer → Ivanti
|
||||
|
||||
const {
|
||||
COLORS: HC, StatCard, HomeCard, CardTitle, HomeButton, SeverityBadge, StatusBadge,
|
||||
HomeInput, HomeSelect, FieldLabel, ResultBanner,
|
||||
BigStat, MiniTicket, CVERow, VendorEntry,
|
||||
HomeIcon: HI, CalendarMini, ArchiveSummary, ScrollList, EmptyState,
|
||||
withAlpha: hAlpha,
|
||||
} = window.HOME;
|
||||
|
||||
const { useState: useHomePageState } = React;
|
||||
|
||||
/* ── Sample data — close to what App.js renders against ──────── */
|
||||
const SAMPLE_CVES = [
|
||||
{
|
||||
id: 'CVE-2025-1014',
|
||||
severity: 'Critical',
|
||||
description: 'Heap-based buffer overflow in the libnetfilter_queue user-space packet handler permits a remote attacker to execute arbitrary code via crafted ICMP traffic.',
|
||||
statuses: ['Open', 'In Progress'],
|
||||
vendors: [
|
||||
{ vendor: 'Red Hat', severity: 'Critical', status: 'Open', docCount: 4 },
|
||||
{ vendor: 'Ubuntu', severity: 'Critical', status: 'In Progress', docCount: 2 },
|
||||
{ vendor: 'SUSE', severity: 'High', status: 'Resolved', docCount: 3 },
|
||||
],
|
||||
tickets: [
|
||||
{ key: 'SEC-4821', summary: 'Patch netfilter on prod ingress fleet', status: 'In Progress' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'CVE-2025-0944',
|
||||
severity: 'High',
|
||||
description: 'Authentication bypass in admin console allows unauthenticated access to telemetry exports.',
|
||||
statuses: ['Addressed'],
|
||||
vendors: [
|
||||
{ vendor: 'Cisco', severity: 'High', status: 'Addressed', docCount: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'CVE-2024-9912',
|
||||
severity: 'Medium',
|
||||
description: 'Improper cert validation in the JIRA Server REST client could lead to MITM under attacker-controlled DNS.',
|
||||
statuses: ['Resolved'],
|
||||
vendors: [
|
||||
{ vendor: 'Atlassian', severity: 'Medium', status: 'Resolved', docCount: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const SAMPLE_OPEN_TICKETS = [
|
||||
{ key: 'SEC-4821', cveId: 'CVE-2025-1014', vendor: 'Red Hat', status: 'In Progress', summary: 'Patch netfilter ingress' },
|
||||
{ key: 'SEC-4794', cveId: 'CVE-2025-0944', vendor: 'Cisco', status: 'Open', summary: 'Roll admin-console hotfix' },
|
||||
{ key: 'SEC-4760', cveId: 'CVE-2024-9912', vendor: 'Atlassian', status: 'Open', summary: 'Validate cert chain' },
|
||||
];
|
||||
|
||||
const SAMPLE_ARCHER = [
|
||||
{ key: 'EXC-08291', cveId: 'CVE-2025-1014', vendor: 'SUSE', status: 'Pending Review' },
|
||||
{ key: 'EXC-08214', cveId: 'CVE-2024-9912', vendor: 'Adobe', status: 'Draft' },
|
||||
];
|
||||
|
||||
const SAMPLE_IVANTI = [
|
||||
{ id: 'WF-1042', name: 'Quarterly compliance scan', state: 'In Review', type: 'compliance audit', when: 'Apr 24' },
|
||||
{ id: 'WF-1038', name: 'Endpoint patch rollout — Linux fleet', state: 'In Progress', type: 'patch deploy', when: 'Apr 22' },
|
||||
{ id: 'WF-1034', name: 'Identity provider rotation', state: 'Approved', type: 'access change', when: 'Apr 21' },
|
||||
];
|
||||
|
||||
const ARCHIVE_SUMMARY = [
|
||||
{ label: 'In Review', count: 12, tone: 'amber' },
|
||||
{ label: 'In Progress', count: 8, tone: 'sky' },
|
||||
{ label: 'Approved', count: 17, tone: 'green' },
|
||||
{ label: 'Closed', count: 41, tone: 'neutral' },
|
||||
];
|
||||
|
||||
/* ── Page ────────────────────────────────────────────────────── */
|
||||
function HomePage() {
|
||||
const [expanded, setExpanded] = useHomePageState(SAMPLE_CVES[0].id);
|
||||
const [scanResult, setScanResult] = useHomePageState({ tone: 'success', text: 'CVE-2025-1014 addressed (3 vendors)' });
|
||||
const [search, setSearch] = useHomePageState('');
|
||||
const [vendor, setVendor] = useHomePageState('All Vendors');
|
||||
const [severity, setSeverity] = useHomePageState('All Severities');
|
||||
|
||||
return (
|
||||
<div data-screen-label="01 Home" style={{
|
||||
padding: 32, minHeight: '100vh', background: 'var(--bg-page)',
|
||||
fontFamily: 'var(--font-display)',
|
||||
}}>
|
||||
{/* ── Top: 4-up stats ── */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
||||
<StatCard label="Total CVEs" value="247" tone="sky" />
|
||||
<StatCard label="Vendor Entries" value="412" tone="neutral" />
|
||||
<StatCard label="Open Tickets" value="18" tone="amber" />
|
||||
<StatCard label="Critical" value="6" tone="red" />
|
||||
</div>
|
||||
|
||||
{/* ── 12-col body ── */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: 24 }}>
|
||||
|
||||
{/* LEFT (col-span-9) */}
|
||||
<div style={{ gridColumn: 'span 9', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
|
||||
{/* Quick CVE Lookup */}
|
||||
<HomeCard>
|
||||
<CardTitle color={HC.sky} icon="search">Quick CVE Lookup</CardTitle>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<HomeInput placeholder="Enter CVE ID (e.g., CVE-2025-1014)" />
|
||||
<HomeButton variant="primary" icon="search" onClick={() => setScanResult({ tone: 'success', text: 'CVE-2025-1014 addressed (3 vendors)' })}>
|
||||
Scan
|
||||
</HomeButton>
|
||||
</div>
|
||||
{scanResult && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<ResultBanner tone={scanResult.tone} title={scanResult.text}>
|
||||
<div style={{ display: 'grid', gap: 10, marginTop: 8 }}>
|
||||
{SAMPLE_CVES[0].vendors.map(v => (
|
||||
<div key={v.vendor} style={{
|
||||
padding: 12, background: 'rgba(15,23,42,0.7)',
|
||||
border: '1px solid rgba(14,165,233,0.30)', borderRadius: 6,
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 600, color: 'var(--fg-1)', marginBottom: 6 }}>{v.vendor}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>
|
||||
<span><strong style={{ color: 'var(--fg-1)' }}>Sev:</strong> {v.severity}</span>
|
||||
<span><strong style={{ color: 'var(--fg-1)' }}>Status:</strong> {v.status}</span>
|
||||
<span><strong style={{ color: 'var(--fg-1)' }}>Docs:</strong> {v.docCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ResultBanner>
|
||||
</div>
|
||||
)}
|
||||
</HomeCard>
|
||||
|
||||
{/* Search + Filter */}
|
||||
<HomeCard>
|
||||
<div style={{ display: 'grid', gap: 16 }}>
|
||||
<div>
|
||||
<FieldLabel icon="search">Search CVEs</FieldLabel>
|
||||
<HomeInput value={search} onChange={e => setSearch(e.target.value)} placeholder="CVE ID or description…" />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<div>
|
||||
<FieldLabel icon="filter">Vendor</FieldLabel>
|
||||
<HomeSelect value={vendor} onChange={e => setVendor(e.target.value)} options={['All Vendors', 'Red Hat', 'Cisco', 'Ubuntu', 'SUSE', 'Atlassian', 'Adobe']} />
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel icon="alert">Severity</FieldLabel>
|
||||
<HomeSelect value={severity} onChange={e => setSeverity(e.target.value)} options={['All Severities', 'Critical', 'High', 'Medium', 'Low']} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HomeCard>
|
||||
|
||||
{/* Results summary */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<p style={{ margin: 0, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
|
||||
<strong style={{ color: HC.sky, fontWeight: 700 }}>{SAMPLE_CVES.length}</strong> CVEs
|
||||
<span style={{ color: 'var(--fg-disabled)', margin: '0 8px' }}>•</span>
|
||||
<span style={{ color: 'var(--fg-1)' }}>{SAMPLE_CVES.reduce((n, c) => n + c.vendors.length, 0)}</span> vendor entries
|
||||
</p>
|
||||
<HomeButton variant="primary" icon="download">Export 2 Docs</HomeButton>
|
||||
</div>
|
||||
|
||||
{/* CVE list */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{SAMPLE_CVES.map(cve => (
|
||||
<CVERow
|
||||
key={cve.id}
|
||||
cveId={cve.id}
|
||||
severity={cve.severity}
|
||||
description={cve.description}
|
||||
vendorCount={cve.vendors.length}
|
||||
docCount={cve.vendors.reduce((s, v) => s + v.docCount, 0)}
|
||||
statuses={cve.statuses}
|
||||
expanded={expanded === cve.id}
|
||||
onToggle={() => setExpanded(expanded === cve.id ? null : cve.id)}
|
||||
>
|
||||
{/* meta row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
|
||||
<span>Published: 2025-03-12</span>
|
||||
<span style={{ color: HC.sky }}>•</span>
|
||||
<span>{cve.vendors.length} affected vendor{cve.vendors.length !== 1 ? 's' : ''}</span>
|
||||
{cve.vendors.length >= 2 && (
|
||||
<HomeButton variant="danger" icon="trash" size="sm" style={{ marginLeft: 8 }}>Delete All</HomeButton>
|
||||
)}
|
||||
</div>
|
||||
{/* vendor sub-cards */}
|
||||
{cve.vendors.map((v, i) => (
|
||||
<VendorEntry
|
||||
key={`${cve.id}-${v.vendor}`}
|
||||
vendor={v.vendor}
|
||||
severity={v.severity}
|
||||
status={v.status}
|
||||
docCount={v.docCount}
|
||||
onView={() => {}}
|
||||
onEdit={() => {}}
|
||||
onDelete={() => {}}
|
||||
>
|
||||
{/* For the first vendor of the first CVE, demonstrate the doc + ticket inset */}
|
||||
{i === 0 && cve.id === SAMPLE_CVES[0].id && (
|
||||
<>
|
||||
<DocInset />
|
||||
{cve.tickets && <TicketInset tickets={cve.tickets} />}
|
||||
</>
|
||||
)}
|
||||
</VendorEntry>
|
||||
))}
|
||||
</CVERow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT (col-span-3) */}
|
||||
<div style={{ gridColumn: 'span 3', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* Calendar */}
|
||||
<HomeCard padding={20} leftRail={HC.sky}>
|
||||
<CardTitle color={HC.sky} icon="calendar">Calendar</CardTitle>
|
||||
<CalendarMini today={26} markedDays={{ 14: 'red', 18: 'amber', 22: 'sky', 24: 'amber' }} />
|
||||
</HomeCard>
|
||||
|
||||
{/* Open Tickets */}
|
||||
<HomeCard padding={20} leftRail={HC.amber}>
|
||||
<CardTitle
|
||||
color={HC.amber}
|
||||
icon="alert"
|
||||
action={<HomeButton variant="warning" icon="plus" size="sm" />}
|
||||
>Open Tickets</CardTitle>
|
||||
<BigStat value={SAMPLE_OPEN_TICKETS.length} label="Active" color={HC.amber} />
|
||||
<ScrollList maxHeight={280}>
|
||||
{SAMPLE_OPEN_TICKETS.map(t => (
|
||||
<MiniTicket key={t.key} keyText={t.key} cveId={t.cveId} vendor={t.vendor} summary={t.summary} status={t.status} tone="amber" onEdit={() => {}} onDelete={() => {}} />
|
||||
))}
|
||||
</ScrollList>
|
||||
</HomeCard>
|
||||
|
||||
{/* Archer Risk */}
|
||||
<HomeCard padding={20} leftRail={HC.purple}>
|
||||
<CardTitle
|
||||
color={HC.purple}
|
||||
icon="shield"
|
||||
action={<button style={{ background: hAlpha(HC.purple, 0.18), border: `1px solid ${HC.purple}`, color: HC.purple, padding: '4px 8px', borderRadius: 4, fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }}><HI name="plus" size={12} color={HC.purple} /></button>}
|
||||
>Archer Risk Tickets</CardTitle>
|
||||
<BigStat value={SAMPLE_ARCHER.length} label="Active" color={HC.purple} />
|
||||
<ScrollList maxHeight={220}>
|
||||
{SAMPLE_ARCHER.map(t => (
|
||||
<MiniTicket key={t.key} keyText={t.key} cveId={t.cveId} vendor={t.vendor} status={t.status} tone="purple" onEdit={() => {}} onDelete={() => {}} />
|
||||
))}
|
||||
</ScrollList>
|
||||
</HomeCard>
|
||||
|
||||
{/* Ivanti Workflows */}
|
||||
<HomeCard padding={20} leftRail={HC.teal}>
|
||||
<CardTitle
|
||||
color={HC.teal}
|
||||
icon="activity"
|
||||
action={<button style={{ background: hAlpha(HC.teal, 0.18), border: `1px solid ${HC.teal}`, color: HC.teal, padding: '4px 8px', borderRadius: 4, fontFamily: 'var(--font-mono)', fontSize: 11, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4 }}><HI name="refresh" size={12} color={HC.teal} /> Sync</button>}
|
||||
>Ivanti Workflows</CardTitle>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)', marginBottom: 12 }}>
|
||||
Synced Apr 26 · 9:42 AM
|
||||
</div>
|
||||
<ArchiveSummary items={ARCHIVE_SUMMARY} />
|
||||
<BigStat value="78" label="Total Workflows" color={HC.teal} />
|
||||
<ScrollList maxHeight={240}>
|
||||
{SAMPLE_IVANTI.map(wf => (
|
||||
<div key={wf.id} style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.85), rgba(51,65,85,0.75))',
|
||||
border: `1px solid ${hAlpha(HC.teal, 0.25)}`, borderRadius: 6,
|
||||
padding: 10,
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 6, marginBottom: 4 }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: '#5EEAD4' }}>{wf.id}</span>
|
||||
<StatusBadge tone="teal" size="sm">{wf.state}</StatusBadge>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: 4 }}>{wf.name}</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)' }}>
|
||||
<span>{wf.type}</span>
|
||||
<span style={{ color: 'var(--fg-disabled)' }}>{wf.when}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollList>
|
||||
</HomeCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Insets used inside the first VendorEntry ────────────────── */
|
||||
function DocInset() {
|
||||
return (
|
||||
<div>
|
||||
<h5 style={{
|
||||
margin: '0 0 12px 0', display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
}}>
|
||||
<HI name="doc" size={13} color={HC.sky} />
|
||||
Documents (4)
|
||||
</h5>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{[
|
||||
{ name: 'rh-advisory-2025-1014.pdf', meta: 'advisory · 220 KB' },
|
||||
{ name: 'patch-notes-rhel9.pdf', meta: 'patch · 85 KB · approved by sec-eng' },
|
||||
].map(d => (
|
||||
<div key={d.name} style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '10px 12px', borderRadius: 4,
|
||||
background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(14,165,233,0.15)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0 }}>
|
||||
<input type="checkbox" style={{ accentColor: HC.sky }} />
|
||||
<HI name="doc" size={16} color={HC.sky} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-1)', fontWeight: 500 }}>{d.name}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)' }}>{d.meta}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<HomeButton variant="neutral" size="sm">View</HomeButton>
|
||||
<HomeButton variant="danger" size="sm">Del</HomeButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<HomeButton variant="neutral" icon="upload" size="sm" style={{ marginTop: 12 }}>Upload Doc</HomeButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TicketInset({ tickets }) {
|
||||
return (
|
||||
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid rgba(245,158,11,0.30)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<h5 style={{
|
||||
margin: 0, display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--fg-1)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
}}>
|
||||
<HI name="alert" size={13} color={HC.amber} />
|
||||
JIRA Tickets ({tickets.length})
|
||||
</h5>
|
||||
<HomeButton variant="primary" icon="plus" size="sm">Add Ticket</HomeButton>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{tickets.map(t => (
|
||||
<div key={t.key} style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '10px 12px', borderRadius: 6,
|
||||
background: 'linear-gradient(135deg, rgba(19,25,55,0.85), rgba(30,39,73,0.75))',
|
||||
border: '1px solid rgba(255,184,0,0.30)',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.04)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, minWidth: 0 }}>
|
||||
<a href="#" onClick={e => e.preventDefault()} style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600, color: HC.sky, textDecoration: 'none' }}>{t.key}</a>
|
||||
<span style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-1)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.summary}</span>
|
||||
<StatusBadge tone="amber" size="sm">{t.status}</StatusBadge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.HOME_PAGE = { HomePage };
|
||||
662
docs/design-system-redesign/ui_kits/home/HomePrimitives.jsx
Normal file
662
docs/design-system-redesign/ui_kits/home/HomePrimitives.jsx
Normal file
@@ -0,0 +1,662 @@
|
||||
// HomePrimitives.jsx — primitives for the CVE Dashboard Home page kit.
|
||||
// Lifted directly from frontend/src/App.js (the home view), normalized to
|
||||
// match the same vocabulary the Reporting + Knowledge Base kits use.
|
||||
//
|
||||
// Exported on window.HOME for the assembly + docs files to consume.
|
||||
|
||||
const { useState: useHomeState } = React;
|
||||
|
||||
/* ── Tokens ──────────────────────────────────────────────────────
|
||||
Identical palette to Reporting + KB. Home adds purple (Archer)
|
||||
and teal (Ivanti) — both used as left-rail / title-glow colors
|
||||
on the right-side panel stack. */
|
||||
const H_COLORS = {
|
||||
sky: '#0EA5E9',
|
||||
skySoft: '#7DD3FC',
|
||||
green: '#10B981',
|
||||
amber: '#F59E0B',
|
||||
amberSoft: '#FCD34D',
|
||||
red: '#EF4444',
|
||||
redSoft: '#FCA5A5',
|
||||
purple: '#8B5CF6',
|
||||
teal: '#0D9488',
|
||||
};
|
||||
|
||||
/* Card chrome shared with the rest of the system. One chrome, every panel. */
|
||||
const CARD_BG = 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)';
|
||||
const CARD_BORDER = '1.5px solid rgba(14,165,233,0.12)';
|
||||
const CARD_BORDER_HOVER = '1.5px solid rgba(14,165,233,0.35)';
|
||||
|
||||
/* ── StatCard ────────────────────────────────────────────────────
|
||||
Top-of-page metric tile. Color-coded by tone — sky for neutral
|
||||
counts, amber for "needs attention", red for critical. Top edge
|
||||
has a soft horizontal glow line in the same color. */
|
||||
function StatCard({ label, value, tone = 'sky', mono = true }) {
|
||||
const c = H_COLORS[tone] || H_COLORS.sky;
|
||||
const isAccent = tone !== 'neutral';
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative', overflow: 'hidden',
|
||||
background: CARD_BG,
|
||||
border: isAccent ? `2px solid ${c}` : CARD_BORDER,
|
||||
borderRadius: 8, padding: 16,
|
||||
boxShadow: isAccent
|
||||
? `0 4px 16px rgba(0,0,0,0.5), 0 0 20px ${withAlpha(c, 0.15)}, inset 0 1px 0 ${withAlpha(c, 0.15)}`
|
||||
: '0 4px 16px rgba(0,0,0,0.5)',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute', top: 0, left: 0, right: 0, height: 2,
|
||||
background: `linear-gradient(90deg, transparent, ${c}, transparent)`,
|
||||
boxShadow: `0 0 8px ${withAlpha(c, 0.5)}`,
|
||||
}} />
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: mono ? 'var(--font-mono)' : 'var(--font-display)',
|
||||
fontSize: 24, fontWeight: 700, color: c,
|
||||
textShadow: isAccent ? `0 0 16px ${withAlpha(c, 0.4)}` : 'none',
|
||||
lineHeight: 1,
|
||||
}}>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── HomeCard ────────────────────────────────────────────────────
|
||||
Same chrome as Reporting's KbCard but without a label slot —
|
||||
the home cards put their title inline above the body. Used as
|
||||
the wrapper for Quick Lookup, the filter row, and CVE rows. */
|
||||
function HomeCard({ children, padding = 24, hover = true, leftRail, style }) {
|
||||
const [h, setH] = useHomeState(false);
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => hover && setH(true)}
|
||||
onMouseLeave={() => setH(false)}
|
||||
style={{
|
||||
background: CARD_BG,
|
||||
border: h ? CARD_BORDER_HOVER : CARD_BORDER,
|
||||
borderLeft: leftRail ? `3px solid ${leftRail}` : (h ? CARD_BORDER_HOVER : CARD_BORDER).split(' ').slice(0).join(' '),
|
||||
borderRadius: 8,
|
||||
padding,
|
||||
transition: 'border-color 200ms ease, box-shadow 200ms ease',
|
||||
position: 'relative',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── CardTitle ───────────────────────────────────────────────────
|
||||
Mono uppercase, glow color matches the card's identity (sky for
|
||||
neutral, amber for tickets, purple for Archer, teal for Ivanti). */
|
||||
function CardTitle({ color = H_COLORS.sky, icon, children, action }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, gap: 12 }}>
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 600,
|
||||
color, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
textShadow: `0 0 12px ${withAlpha(color, 0.4)}`,
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
{icon && <HomeIcon name={icon} size={16} color={color} />}
|
||||
{children}
|
||||
</h3>
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── HomeButton ──────────────────────────────────────────────────
|
||||
Wraps the four button variants the Home page uses, keeping the
|
||||
exact same tinted-fill / outlined treatment as the Reporting kit
|
||||
so all pages feel consistent. */
|
||||
function HomeButton({ variant = 'neutral', icon, children, size = 'md', ...rest }) {
|
||||
const [hover, setHover] = useHomeState(false);
|
||||
const v = {
|
||||
primary: { bg: hover ? 'rgba(16,185,129,0.18)' : 'rgba(16,185,129,0.10)', bd: H_COLORS.green, fg: H_COLORS.green },
|
||||
neutral: { bg: hover ? 'rgba(14,165,233,0.10)' : 'transparent', bd: 'rgba(14,165,233,0.5)', fg: H_COLORS.sky },
|
||||
subtle: { bg: hover ? 'rgba(14,165,233,0.16)' : 'rgba(14,165,233,0.08)', bd: 'rgba(14,165,233,0.30)', fg: H_COLORS.sky },
|
||||
danger: { bg: hover ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.10)', bd: 'rgba(239,68,68,0.5)', fg: H_COLORS.red },
|
||||
warning: { bg: hover ? 'rgba(245,158,11,0.18)' : 'rgba(245,158,11,0.10)', bd: 'rgba(245,158,11,0.5)', fg: H_COLORS.amber },
|
||||
}[variant];
|
||||
const padX = size === 'sm' ? 10 : 14;
|
||||
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.06em',
|
||||
cursor: 'pointer', transition: 'all 160ms ease', whiteSpace: 'nowrap',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{icon && <HomeIcon name={icon} size={fs + 2} color={v.fg} />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── SeverityBadge ───────────────────────────────────────────────
|
||||
Strong tinted-fill badge used in CVE rows. Critical/High/Medium/Low. */
|
||||
function SeverityBadge({ level }) {
|
||||
const map = {
|
||||
Critical: { c: H_COLORS.red, text: H_COLORS.redSoft },
|
||||
High: { c: H_COLORS.amber, text: H_COLORS.amberSoft },
|
||||
Medium: { c: H_COLORS.sky, text: H_COLORS.skySoft },
|
||||
Low: { c: H_COLORS.green, text: '#6EE7B7' },
|
||||
}[level] || { c: H_COLORS.sky, text: H_COLORS.skySoft };
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: `linear-gradient(135deg, ${withAlpha(map.c, 0.25)}, ${withAlpha(map.c, 0.20)})`,
|
||||
border: `2px solid ${map.c}`, borderRadius: 6,
|
||||
padding: '4px 10px',
|
||||
color: map.text, fontWeight: 700, fontSize: 11,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
textShadow: `0 0 8px ${withAlpha(map.c, 0.5)}`,
|
||||
boxShadow: `0 0 16px ${withAlpha(map.c, 0.25)}, 0 4px 8px rgba(0,0,0,0.4)`,
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'inline-block', width: 6, height: 6, borderRadius: '50%',
|
||||
background: map.c, boxShadow: `0 0 8px ${map.c}`,
|
||||
}} />
|
||||
{level}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── StatusBadge ─────────────────────────────────────────────────
|
||||
Tone-coded text badge used for ticket statuses (Open / In Progress /
|
||||
Closed / Draft / Accepted). Smaller and lighter than SeverityBadge. */
|
||||
function StatusBadge({ tone = 'sky', children, size = 'md' }) {
|
||||
const c = H_COLORS[tone] || H_COLORS.sky;
|
||||
const fs = size === 'sm' ? 10 : 11;
|
||||
const pad = size === 'sm' ? '3px 7px' : '4px 9px';
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: withAlpha(c, 0.18),
|
||||
border: `1px solid ${c}`, borderRadius: 4,
|
||||
padding: pad, color: c,
|
||||
fontFamily: 'var(--font-mono)', fontSize: fs, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'inline-block', width: 6, height: 6, borderRadius: '50%',
|
||||
background: c, boxShadow: `0 0 6px ${c}`,
|
||||
}} />
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── HomeInput / HomeSelect ──────────────────────────────────────
|
||||
The intel-input look: dark fill + sky border on focus. */
|
||||
function HomeInput({ icon, ...rest }) {
|
||||
const [focus, setFocus] = useHomeState(false);
|
||||
return (
|
||||
<div style={{ position: 'relative', flex: 1 }}>
|
||||
{icon && (
|
||||
<div style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: H_COLORS.sky }}>
|
||||
<HomeIcon name={icon} size={14} color={H_COLORS.sky} />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
|
||||
style={{
|
||||
width: '100%', boxSizing: 'border-box',
|
||||
background: 'rgba(15,23,42,0.85)',
|
||||
border: `1px solid ${focus ? H_COLORS.sky : 'rgba(14,165,233,0.25)'}`,
|
||||
borderRadius: 6,
|
||||
padding: icon ? '9px 12px 9px 34px' : '9px 12px',
|
||||
color: 'var(--fg-1)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13,
|
||||
outline: 'none', transition: 'border-color 160ms ease',
|
||||
boxShadow: focus ? `0 0 0 3px ${withAlpha(H_COLORS.sky, 0.15)}` : 'none',
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeSelect({ value, onChange, options }) {
|
||||
return (
|
||||
<select value={value} onChange={onChange} style={{
|
||||
width: '100%', boxSizing: 'border-box',
|
||||
background: 'rgba(15,23,42,0.85)',
|
||||
border: '1px solid rgba(14,165,233,0.25)', borderRadius: 6,
|
||||
padding: '9px 12px', color: 'var(--fg-1)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13,
|
||||
outline: 'none', appearance: 'none',
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230EA5E9' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat', backgroundPosition: 'right 12px center', paddingRight: 32,
|
||||
}}>
|
||||
{options.map(o => <option key={o} value={o}>{o}</option>)}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({ icon, children }) {
|
||||
return (
|
||||
<label style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
marginBottom: 8,
|
||||
}}>
|
||||
{icon && <HomeIcon name={icon} size={13} color="currentColor" />}
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── ResultBanner ────────────────────────────────────────────────
|
||||
Sub-card used in Quick Lookup to surface scan results.
|
||||
Tones: success (CVE addressed), warning (not found), error. */
|
||||
function ResultBanner({ tone = 'success', icon, title, children }) {
|
||||
const map = {
|
||||
success: { c: H_COLORS.green, bg: 'rgba(16,185,129,0.10)', bd: 'rgba(16,185,129,0.30)' },
|
||||
warning: { c: H_COLORS.amber, bg: 'rgba(245,158,11,0.10)', bd: 'rgba(245,158,11,0.30)' },
|
||||
error: { c: H_COLORS.red, bg: 'rgba(239,68,68,0.10)', bd: 'rgba(239,68,68,0.30)' },
|
||||
}[tone];
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 12,
|
||||
padding: 16, borderRadius: 6,
|
||||
background: map.bg, border: `1px solid ${map.bd}`,
|
||||
}}>
|
||||
<div style={{ color: map.c, marginTop: 1 }}>
|
||||
<HomeIcon name={icon || tone} size={18} color={map.c} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600,
|
||||
color: map.c, marginBottom: children ? 8 : 0,
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── BigStat ─────────────────────────────────────────────────────
|
||||
The centered "active count + label" shown at the top of each
|
||||
right-rail panel (Open Tickets · Archer · Ivanti). */
|
||||
function BigStat({ value, label, color = H_COLORS.sky }) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', marginBottom: 12 }}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 32, fontWeight: 700,
|
||||
color, textShadow: `0 0 16px ${withAlpha(color, 0.4)}`, lineHeight: 1,
|
||||
}}>
|
||||
{value}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600,
|
||||
color: 'var(--fg-2)', textTransform: 'uppercase', letterSpacing: '0.12em',
|
||||
marginTop: 6,
|
||||
}}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── MiniTicket ──────────────────────────────────────────────────
|
||||
Compact card shown inside the right-rail scrollable lists.
|
||||
Color-coded by category via the `tone` prop (amber/purple/teal). */
|
||||
function MiniTicket({ keyText, cveId, vendor, status, tone = 'amber', summary, onEdit, onDelete }) {
|
||||
const c = H_COLORS[tone] || H_COLORS.amber;
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.85), rgba(51,65,85,0.75))',
|
||||
border: `1px solid ${withAlpha(c, 0.25)}`, borderRadius: 6,
|
||||
padding: 10,
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.03)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 6, marginBottom: 4 }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600, color: H_COLORS.sky }}>
|
||||
{keyText}
|
||||
</span>
|
||||
{(onEdit || onDelete) && (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{onEdit && <button onClick={onEdit} style={iconBtn(H_COLORS.amber)}><HomeIcon name="edit" size={11} color="currentColor" /></button>}
|
||||
{onDelete && <button onClick={onDelete} style={iconBtn(H_COLORS.red)}><HomeIcon name="trash" size={11} color="currentColor" /></button>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-1)', marginBottom: 2 }}>{cveId}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-2)' }}>{vendor}</div>
|
||||
{summary && (
|
||||
<div style={{
|
||||
fontSize: 11, color: 'var(--fg-2)', marginTop: 4,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
fontFamily: 'var(--font-display)',
|
||||
}}>{summary}</div>
|
||||
)}
|
||||
{status && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<StatusBadge tone={tone} size="sm">{status}</StatusBadge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const iconBtn = (color) => ({
|
||||
background: 'transparent', border: 'none', color: 'var(--fg-2)',
|
||||
cursor: 'pointer', padding: 2, display: 'inline-flex', alignItems: 'center',
|
||||
transition: 'color 120ms ease',
|
||||
});
|
||||
|
||||
/* ── CVERow ──────────────────────────────────────────────────────
|
||||
The main "row" in the home feed. Collapsed = chevron · CVE-ID ·
|
||||
description · meta row (severity badge, vendor count, doc count,
|
||||
statuses). Expanded = full description + admin actions slot. */
|
||||
function CVERow({ cveId, severity, description, vendorCount, docCount, statuses, expanded, onToggle, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: CARD_BG, border: CARD_BORDER, borderRadius: 8,
|
||||
transition: 'border-color 200ms ease',
|
||||
}}>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
width: '100%', textAlign: 'left',
|
||||
background: 'transparent', border: 'none',
|
||||
padding: 24, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 6 }}>
|
||||
<span style={{
|
||||
display: 'inline-block', transform: expanded ? 'rotate(0)' : 'rotate(-90deg)',
|
||||
transition: 'transform 200ms ease', color: H_COLORS.sky,
|
||||
}}>
|
||||
<HomeIcon name="chevron" size={18} color={H_COLORS.sky} />
|
||||
</span>
|
||||
<h3 style={{
|
||||
margin: 0, fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700,
|
||||
color: H_COLORS.sky, letterSpacing: '-0.01em',
|
||||
}}>{cveId}</h3>
|
||||
</div>
|
||||
<div style={{ marginLeft: 30 }}>
|
||||
<p style={{
|
||||
margin: '0 0 8px 0',
|
||||
color: 'var(--fg-1)', fontSize: 13, lineHeight: 1.5,
|
||||
fontFamily: 'var(--font-display)',
|
||||
display: '-webkit-box', WebkitLineClamp: expanded ? 'unset' : 1,
|
||||
WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
{description}
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
|
||||
<SeverityBadge level={severity} />
|
||||
<span style={metaText}>{vendorCount} vendor{vendorCount !== 1 ? 's' : ''}</span>
|
||||
<span style={{ ...metaText, display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<HomeIcon name="doc" size={11} color="currentColor" />
|
||||
{docCount} doc{docCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span style={metaText}>{statuses.join(', ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{expanded && children && (
|
||||
<div style={{ padding: '0 24px 24px', marginLeft: 30 }}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const metaText = {
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)',
|
||||
};
|
||||
|
||||
/* ── VendorEntry ─────────────────────────────────────────────────
|
||||
Sub-card inside an expanded CVE row, one per vendor that filed
|
||||
the CVE. Holds vendor name, severity, status, doc count, and
|
||||
inline action buttons. */
|
||||
function VendorEntry({ vendor, severity, status, docCount, children, onView, onEdit, onDelete }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(15,23,42,0.95) 0%, rgba(30,41,59,0.9) 100%)',
|
||||
border: '1.5px solid rgba(14,165,233,0.30)', borderRadius: 6,
|
||||
padding: 16, marginBottom: 12,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.4), inset 0 1px 0 rgba(14,165,233,0.08)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
|
||||
<h4 style={{ margin: 0, fontFamily: 'var(--font-display)', fontSize: 15, fontWeight: 600, color: 'var(--fg-1)' }}>{vendor}</h4>
|
||||
<SeverityBadge level={severity} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>
|
||||
<span>Status: <strong style={{ color: 'var(--fg-1)', fontWeight: 500 }}>{status}</strong></span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
<HomeIcon name="doc" size={13} color="currentColor" />
|
||||
{docCount} doc{docCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
|
||||
{onView && <HomeButton variant="neutral" icon="eye" size="sm" onClick={onView}>View</HomeButton>}
|
||||
{onEdit && <HomeButton variant="warning" icon="edit" size="sm" onClick={onEdit} />}
|
||||
{onDelete && <HomeButton variant="danger" icon="trash" size="sm" onClick={onDelete} />}
|
||||
</div>
|
||||
</div>
|
||||
{children && (
|
||||
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid rgba(14,165,233,0.20)' }}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── HomeIcon ────────────────────────────────────────────────────
|
||||
Inline SVGs covering every icon used on the home page so the kit
|
||||
has no external icon-font dependency. Keys mirror lucide-react names. */
|
||||
function HomeIcon({ 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 'search': return <svg {...p}><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>;
|
||||
case 'filter': return <svg {...p}><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></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':
|
||||
case 'success': return <svg {...p}><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>;
|
||||
case 'warning': 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 'error':
|
||||
case 'x': return <svg {...p}><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>;
|
||||
case 'shield': return <svg {...p}><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>;
|
||||
case 'activity': return <svg {...p}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>;
|
||||
case 'doc': return <svg {...p}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>;
|
||||
case 'eye': return <svg {...p}><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>;
|
||||
case 'edit': return <svg {...p}><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>;
|
||||
case 'trash': return <svg {...p}><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>;
|
||||
case 'plus': return <svg {...p}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></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 'chevron': return <svg {...p}><polyline points="6 9 12 15 18 9"/></svg>;
|
||||
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 'download': return <svg {...p}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>;
|
||||
case 'calendar': return <svg {...p}><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>;
|
||||
default: return <svg {...p}><circle cx="12" cy="12" r="10"/></svg>;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── CalendarMini ────────────────────────────────────────────────
|
||||
Minimal calendar surface for the right rail. Static — accepts a
|
||||
`today` index and an optional `markedDays` map for severity dots. */
|
||||
function CalendarMini({ month = 'April 2026', today = 26, markedDays = {} }) {
|
||||
const dows = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
// April 2026 starts on Wednesday — empty cells for S/M/T
|
||||
const startOffset = 3;
|
||||
const daysInMonth = 30;
|
||||
const cells = [...Array(startOffset).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)];
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
}}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-1)', fontWeight: 600 }}>{month}</span>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button style={navBtn}>‹</button>
|
||||
<button style={navBtn}>›</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2 }}>
|
||||
{dows.map((d, i) => (
|
||||
<div key={`dow-${i}`} style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-disabled)',
|
||||
textAlign: 'center', padding: '4px 0', fontWeight: 600,
|
||||
}}>{d}</div>
|
||||
))}
|
||||
{cells.map((day, i) => {
|
||||
if (day === null) return <div key={`empty-${i}`} />;
|
||||
const mark = markedDays[day];
|
||||
const isToday = day === today;
|
||||
return (
|
||||
<button key={`day-${day}`} style={{
|
||||
position: 'relative',
|
||||
padding: '6px 0', borderRadius: 4,
|
||||
background: isToday ? withAlpha(H_COLORS.sky, 0.20) : 'transparent',
|
||||
border: isToday ? `1px solid ${H_COLORS.sky}` : '1px solid transparent',
|
||||
color: isToday ? H_COLORS.sky : 'var(--fg-1)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: isToday ? 700 : 500,
|
||||
cursor: 'pointer', transition: 'background 120ms ease',
|
||||
}}>
|
||||
{day}
|
||||
{mark && (
|
||||
<span style={{
|
||||
position: 'absolute', bottom: 2, left: '50%', transform: 'translateX(-50%)',
|
||||
width: 4, height: 4, borderRadius: '50%',
|
||||
background: H_COLORS[mark] || H_COLORS.amber,
|
||||
}} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const navBtn = {
|
||||
background: 'transparent', border: '1px solid rgba(14,165,233,0.25)',
|
||||
color: H_COLORS.sky, borderRadius: 4, width: 22, height: 22,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, cursor: 'pointer',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
};
|
||||
|
||||
/* ── ArchiveSummary ──────────────────────────────────────────────
|
||||
The bar of state pills that lives at the top of the Ivanti card.
|
||||
Each pill shows an Ivanti workflow state + count, color-coded. */
|
||||
function ArchiveSummary({ items, activeFilter, onSelect }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
|
||||
{items.map(it => {
|
||||
const c = H_COLORS[it.tone] || H_COLORS.teal;
|
||||
const active = activeFilter === it.label;
|
||||
return (
|
||||
<button
|
||||
key={it.label}
|
||||
onClick={() => onSelect && onSelect(active ? null : it.label)}
|
||||
style={{
|
||||
flex: '1 1 60px',
|
||||
padding: '8px 10px',
|
||||
background: active ? withAlpha(c, 0.20) : withAlpha(c, 0.08),
|
||||
border: `1px solid ${active ? c : withAlpha(c, 0.30)}`,
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: 16, fontWeight: 700, color: c,
|
||||
textShadow: active ? `0 0 8px ${withAlpha(c, 0.5)}` : 'none', lineHeight: 1,
|
||||
}}>{it.count}</div>
|
||||
<div style={{
|
||||
fontSize: 9, color: 'var(--fg-2)', textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em', marginTop: 4, fontWeight: 600,
|
||||
}}>{it.label}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── ScrollList ──────────────────────────────────────────────────
|
||||
Generic max-height scroll wrapper for the right-rail panels. */
|
||||
function ScrollList({ maxHeight = 300, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
maxHeight, overflowY: 'auto',
|
||||
display: 'flex', flexDirection: 'column', gap: 8,
|
||||
paddingRight: 4,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── EmptyState ──────────────────────────────────────────────────
|
||||
Center-aligned check-circle + caption, used inside ScrollList
|
||||
when a panel has no items. */
|
||||
function EmptyState({ icon = 'check', tone = 'green', children }) {
|
||||
const c = H_COLORS[tone] || H_COLORS.green;
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '24px 0' }}>
|
||||
<div style={{ display: 'inline-flex', marginBottom: 8 }}>
|
||||
<HomeIcon name={icon} size={32} color={c} />
|
||||
</div>
|
||||
<p style={{
|
||||
margin: 0, fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
color: 'var(--fg-2)', fontStyle: 'italic',
|
||||
}}>{children}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── helpers ─────────────────────────────────────────────────── */
|
||||
function withAlpha(hex, a) {
|
||||
const h = hex.replace('#', '');
|
||||
const r = parseInt(h.slice(0, 2), 16);
|
||||
const g = parseInt(h.slice(2, 4), 16);
|
||||
const b = parseInt(h.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
window.HOME = {
|
||||
COLORS: H_COLORS,
|
||||
StatCard, HomeCard, CardTitle, HomeButton, SeverityBadge, StatusBadge,
|
||||
HomeInput, HomeSelect, FieldLabel, ResultBanner,
|
||||
BigStat, MiniTicket, CVERow, VendorEntry,
|
||||
HomeIcon, CalendarMini, ArchiveSummary, ScrollList, EmptyState,
|
||||
withAlpha,
|
||||
};
|
||||
443
docs/design-system-redesign/ui_kits/home/KitDocs.jsx
Normal file
443
docs/design-system-redesign/ui_kits/home/KitDocs.jsx
Normal file
@@ -0,0 +1,443 @@
|
||||
// KitDocs.jsx — browseable docs page for the Home kit.
|
||||
// Sections: Overview · Tokens · Components · Assemblies · Reference page.
|
||||
|
||||
const { useState: useDocsHomeState } = React;
|
||||
const {
|
||||
COLORS: DHC, StatCard: DStatCard, HomeCard: DHomeCard, CardTitle: DCardTitle,
|
||||
HomeButton: DBtn, SeverityBadge: DSev, StatusBadge: DStatus,
|
||||
HomeInput: DInput, HomeSelect: DSelect, FieldLabel: DLabel, ResultBanner: DBanner,
|
||||
BigStat: DBigStat, MiniTicket: DMini, CVERow: DCVERow, VendorEntry: DVendor,
|
||||
HomeIcon: DIcon, CalendarMini: DCal, ArchiveSummary: DArchive, ScrollList: DScroll,
|
||||
EmptyState: DEmpty, withAlpha: dAlpha,
|
||||
} = window.HOME;
|
||||
const { HomePage: DHomePage } = window.HOME_PAGE;
|
||||
|
||||
/* ── Layout primitives (same vocabulary as the Reporting kit docs) ── */
|
||||
function HSection({ 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: DHC.sky, 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 HSpec({ 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 HCode({ children }) {
|
||||
return (
|
||||
<code style={{
|
||||
display: 'inline-block', padding: '2px 6px', borderRadius: 3,
|
||||
background: 'rgba(14,165,233,0.10)', border: '1px solid rgba(14,165,233,0.18)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: DHC.skySoft,
|
||||
}}>{children}</code>
|
||||
);
|
||||
}
|
||||
|
||||
function HSwatch({ 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>
|
||||
<HCode>{value}</HCode>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HSpecimen({ children, padding = 24, dark = true, style }) {
|
||||
return (
|
||||
<div style={{
|
||||
padding,
|
||||
background: dark ? 'rgba(15,23,42,0.5)' : 'transparent',
|
||||
border: '1px solid rgba(255,255,255,0.05)', borderRadius: 8,
|
||||
...style,
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Sticky tab strip ─────────────────────────────────────────── */
|
||||
const TABS = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'tokens', label: 'Tokens' },
|
||||
{ id: 'components', label: 'Components' },
|
||||
{ id: 'assemblies', label: 'Assemblies' },
|
||||
{ id: 'reference', label: 'Reference Page' },
|
||||
];
|
||||
|
||||
function HKitDocs() {
|
||||
const [active, setActive] = useDocsHomeState('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 */}
|
||||
<header style={{
|
||||
padding: '40px 48px 0 48px', maxWidth: 1280, margin: '0 auto',
|
||||
}}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: DHC.sky, 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: DHC.green, textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
textShadow: '0 0 24px rgba(16,185,129,0.30)',
|
||||
}}>
|
||||
Home
|
||||
</h1>
|
||||
<p style={{
|
||||
margin: '12px 0 0 0', maxWidth: 720, fontSize: 15, lineHeight: 1.6,
|
||||
color: 'var(--fg-muted)', fontFamily: 'var(--font-display)',
|
||||
}}>
|
||||
The "command center" landing view of the CVE Dashboard. Pulls four signals into one screen:
|
||||
a top metric strip, a CVE feed with vendor sub-rows, and a right-rail stack of
|
||||
Calendar · JIRA · Archer · Ivanti. Built from the same chrome and tokens as the Reporting kit.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Tab strip */}
|
||||
<nav style={{
|
||||
position: 'sticky', top: 0, zIndex: 10,
|
||||
marginTop: 28,
|
||||
background: 'rgba(2,6,23,0.85)', backdropFilter: 'blur(8px)',
|
||||
borderBottom: '1px solid rgba(14,165,233,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 ? DHC.sky : 'transparent'}`,
|
||||
color: on ? DHC.sky : '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>
|
||||
|
||||
{/* Body */}
|
||||
<main style={{ maxWidth: 1280, margin: '0 auto', padding: '0 48px 96px 48px' }}>
|
||||
|
||||
{/* OVERVIEW */}
|
||||
<HSection id="overview" eyebrow="01 — Overview" title="Why this kit exists" blurb="Documents the visual + behavioral vocabulary of the home view so other dashboards in the suite can re-use the right-rail stack, the CVE row pattern, and the four-up stat strip without re-deriving them.">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<HSpecimen>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: DHC.sky, 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)' }}>
|
||||
Green appears in exactly one place: the page title in the chrome. Sky is the workhorse — borders,
|
||||
section titles, neutral buttons. Amber, red, purple, teal are reserved for specific data domains
|
||||
(tickets, critical, Archer, Ivanti) and never used decoratively.
|
||||
</p>
|
||||
</HSpecimen>
|
||||
<HSpecimen>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: DHC.sky, 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)' }}>
|
||||
Top: 4-up stat strip. Body: 12-column grid, left 9 / right 3. Left holds the lookup → filter → CVE
|
||||
feed flow. Right is a vertical stack of color-rail panels, each with a left-border identity color
|
||||
and a centered big-number metric.
|
||||
</p>
|
||||
</HSpecimen>
|
||||
</div>
|
||||
</HSection>
|
||||
|
||||
{/* TOKENS */}
|
||||
<HSection id="tokens" eyebrow="02 — Tokens" title="Color, type, and the right-rail palette" blurb="The four data domains on the home view each have an owned color used as: card left-rail border, card title color + glow, big-number value color, and badge tint.">
|
||||
<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 }}>Right-rail identity</div>
|
||||
<HSwatch name="sky" value={DHC.sky} role="Calendar · neutral surfaces · default" />
|
||||
<HSwatch name="amber" value={DHC.amber} role="Open Tickets · 'needs attention'" />
|
||||
<HSwatch name="purple" value={DHC.purple} role="Archer Risk Tickets" />
|
||||
<HSwatch name="teal" value={DHC.teal} role="Ivanti Workflows" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 8, fontWeight: 600 }}>Severity / status</div>
|
||||
<HSwatch name="green" value={DHC.green} role="Page identity glow · Low · success" />
|
||||
<HSwatch name="red" value={DHC.red} role="Critical · destructive" />
|
||||
<HSwatch name="amber" value={DHC.amber} role="High · in-progress" />
|
||||
<HSwatch name="sky" value={DHC.sky} role="Medium · neutral status" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<HSpec label="Card chrome">background <HCode>linear-gradient(135deg, rgba(30,41,59,.95) 0%, rgba(15,23,42,.98) 100%)</HCode></HSpec>
|
||||
<HSpec label="Card border">resting <HCode>1.5px solid rgba(14,165,233,0.12)</HCode> · hover <HCode>0.35</HCode></HSpec>
|
||||
<HSpec label="Card radius"><HCode>8px</HCode></HSpec>
|
||||
<HSpec label="Title type"><HCode>var(--font-mono)</HCode> · 14 / 600 · uppercase · 0.1em tracking · 12px text-shadow glow in title color</HSpec>
|
||||
<HSpec label="Big stat type"><HCode>var(--font-mono)</HCode> · 32 / 700 · 16px text-shadow glow at 0.4 alpha</HSpec>
|
||||
<HSpec label="Stat label type"><HCode>var(--font-mono)</HCode> · 10 / 600 · uppercase · 0.12em tracking · fg-2</HSpec>
|
||||
</div>
|
||||
</HSection>
|
||||
|
||||
{/* COMPONENTS */}
|
||||
<HSection id="components" eyebrow="03 — Components" title="The pieces" blurb="Every primitive used by the page assembly. All are exported on window.HOME so other pages in the dashboard can pull from the same vocabulary.">
|
||||
|
||||
{/* StatCard */}
|
||||
<h3 style={subhead}>StatCard</h3>
|
||||
<p style={subblurb}>Top-of-page metric tile. Color tone drives the 2px border, top-edge glow line, value color, and the inset highlight. Use <HCode>tone="neutral"</HCode> to suppress the colored treatment.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 12 }}>
|
||||
<DStatCard label="Total CVEs" value="247" tone="sky" />
|
||||
<DStatCard label="Vendor Entries" value="412" tone="neutral" />
|
||||
<DStatCard label="Open Tickets" value="18" tone="amber" />
|
||||
<DStatCard label="Critical" value="6" tone="red" />
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* Buttons */}
|
||||
<h3 style={subhead}>HomeButton</h3>
|
||||
<p style={subblurb}>Five variants. <strong style={{ color: DHC.green }}>Primary</strong> is reserved for the lone green CTA on each card. <strong style={{ color: DHC.sky }}>Neutral</strong> is the default for table-row + view actions. <strong style={{ color: DHC.amber }}>Warning</strong> = edit, <strong style={{ color: DHC.red }}>Danger</strong> = delete.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<DBtn variant="primary" icon="search">Scan</DBtn>
|
||||
<DBtn variant="neutral" icon="eye">View</DBtn>
|
||||
<DBtn variant="subtle" icon="download">Export</DBtn>
|
||||
<DBtn variant="warning" icon="edit">Edit</DBtn>
|
||||
<DBtn variant="danger" icon="trash">Delete</DBtn>
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* Badges */}
|
||||
<h3 style={subhead}>SeverityBadge · StatusBadge</h3>
|
||||
<p style={subblurb}>Severity is heavy: 2px solid border + glow + dot. Status is light: 1px border, smaller, used inside dense list cards.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 16 }}>
|
||||
<DSev level="Critical" /><DSev level="High" /><DSev level="Medium" /><DSev level="Low" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<DStatus tone="amber">In Progress</DStatus>
|
||||
<DStatus tone="red">Open</DStatus>
|
||||
<DStatus tone="green">Closed</DStatus>
|
||||
<DStatus tone="purple">Pending Review</DStatus>
|
||||
<DStatus tone="teal">Approved</DStatus>
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* Inputs */}
|
||||
<h3 style={subhead}>HomeInput · HomeSelect · FieldLabel</h3>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'grid', gap: 16 }}>
|
||||
<div>
|
||||
<DLabel icon="search">Search CVEs</DLabel>
|
||||
<DInput placeholder="CVE ID or description…" />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
<div>
|
||||
<DLabel icon="filter">Vendor</DLabel>
|
||||
<DSelect value="All Vendors" onChange={() => {}} options={['All Vendors', 'Red Hat', 'Cisco', 'Ubuntu']} />
|
||||
</div>
|
||||
<div>
|
||||
<DLabel icon="alert">Severity</DLabel>
|
||||
<DSelect value="All Severities" onChange={() => {}} options={['All Severities', 'Critical', 'High', 'Medium', 'Low']} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* ResultBanner */}
|
||||
<h3 style={subhead}>ResultBanner</h3>
|
||||
<p style={subblurb}>Sub-card surfaced inside the Quick CVE Lookup card after a scan. Three tones map to the three terminal states.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
<DBanner tone="success" title="✓ CVE Addressed (3 vendors)">
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)' }}>
|
||||
Red Hat (Open · 4 docs) · Ubuntu (In Progress · 2 docs) · SUSE (Resolved · 3 docs)
|
||||
</div>
|
||||
</DBanner>
|
||||
<DBanner tone="warning" title="Not Found">
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)' }}>This CVE has not been addressed yet. No entry exists in the database.</div>
|
||||
</DBanner>
|
||||
<DBanner tone="error" title="Error">
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, color: 'var(--fg-2)' }}>NVD lookup failed: rate-limited (429). Retry in 30s.</div>
|
||||
</DBanner>
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* BigStat */}
|
||||
<h3 style={subhead}>BigStat</h3>
|
||||
<p style={subblurb}>The centered "active count + label" shown at the top of every right-rail panel. Color follows panel identity.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
|
||||
<DBigStat value="3" label="Active" color={DHC.amber} />
|
||||
<DBigStat value="2" label="Active" color={DHC.purple} />
|
||||
<DBigStat value="78" label="Total Workflows" color={DHC.teal} />
|
||||
<DBigStat value="—" label="Never Synced" color={DHC.sky} />
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* MiniTicket */}
|
||||
<h3 style={subhead}>MiniTicket</h3>
|
||||
<p style={subblurb}>Compact card used inside right-rail scroll lists. Tone tints the border + status pill to match its parent panel's identity color.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 12 }}>
|
||||
<DMini keyText="SEC-4821" cveId="CVE-2025-1014" vendor="Red Hat" status="In Progress" tone="amber" onEdit={() => {}} />
|
||||
<DMini keyText="EXC-08291" cveId="CVE-2025-1014" vendor="SUSE" status="Pending Review" tone="purple" onEdit={() => {}} />
|
||||
<DMini keyText="WF-1042" cveId="—" vendor="Compliance scan" status="In Review" tone="teal" />
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* Calendar */}
|
||||
<h3 style={subhead}>CalendarMini</h3>
|
||||
<p style={subblurb}>Right-rail calendar surface. Day cells accept a marker color so SLA / due-date dots can be projected onto the month.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ maxWidth: 280 }}>
|
||||
<DCal today={26} markedDays={{ 14: 'red', 18: 'amber', 22: 'sky', 24: 'amber' }} />
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* ArchiveSummary */}
|
||||
<h3 style={subhead}>ArchiveSummary</h3>
|
||||
<p style={subblurb}>State-pill bar that lives at the top of the Ivanti card. Each pill is a click target that filters the workflows below.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ maxWidth: 320 }}>
|
||||
<DArchive items={[
|
||||
{ label: 'In Review', count: 12, tone: 'amber' },
|
||||
{ label: 'In Progress', count: 8, tone: 'sky' },
|
||||
{ label: 'Approved', count: 17, tone: 'green' },
|
||||
{ label: 'Closed', count: 41, tone: 'neutral' },
|
||||
]} activeFilter="In Review" />
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
{/* CVERow + VendorEntry */}
|
||||
<h3 style={subhead}>CVERow · VendorEntry</h3>
|
||||
<p style={subblurb}>The collapsible CVE feed cards. Collapsed = chevron + ID + truncated description + meta row. Expanded = vendor sub-cards, optionally with a doc inset and a JIRA inset under each vendor.</p>
|
||||
<HSpecimen padding={16}>
|
||||
<DCVERow
|
||||
cveId="CVE-2025-1014" severity="Critical"
|
||||
description="Heap-based buffer overflow in libnetfilter_queue permits remote code execution via crafted ICMP traffic."
|
||||
vendorCount={3} docCount={9} statuses={['Open', 'In Progress']}
|
||||
expanded={true} onToggle={() => {}}
|
||||
>
|
||||
<DVendor vendor="Red Hat" severity="Critical" status="Open" docCount={4} onView={() => {}} />
|
||||
<DVendor vendor="Ubuntu" severity="Critical" status="In Progress" docCount={2} onEdit={() => {}} />
|
||||
</DCVERow>
|
||||
</HSpecimen>
|
||||
|
||||
{/* EmptyState */}
|
||||
<h3 style={subhead}>EmptyState</h3>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,1fr)', gap: 16 }}>
|
||||
<DEmpty>No open tickets</DEmpty>
|
||||
<DEmpty icon="alert" tone="amber">Click Sync to load workflow data</DEmpty>
|
||||
</div>
|
||||
</HSpecimen>
|
||||
</HSection>
|
||||
|
||||
{/* ASSEMBLIES */}
|
||||
<HSection id="assemblies" eyebrow="04 — Assemblies" title="How the parts compose" blurb="Three patterns that other dashboards in the suite should reuse verbatim.">
|
||||
|
||||
<h3 style={subhead}>Right-rail panel</h3>
|
||||
<p style={subblurb}>HomeCard with a colored left-rail + matching CardTitle + BigStat + ScrollList of MiniTickets. The identity color owns all four.</p>
|
||||
<HSpecimen>
|
||||
<div style={{ maxWidth: 320 }}>
|
||||
<DHomeCard padding={20} leftRail={DHC.amber}>
|
||||
<DCardTitle color={DHC.amber} icon="alert" action={<DBtn variant="warning" icon="plus" size="sm" />}>Open Tickets</DCardTitle>
|
||||
<DBigStat value="3" label="Active" color={DHC.amber} />
|
||||
<DScroll maxHeight={220}>
|
||||
<DMini keyText="SEC-4821" cveId="CVE-2025-1014" vendor="Red Hat" status="In Progress" tone="amber" onEdit={() => {}} onDelete={() => {}} summary="Patch netfilter ingress" />
|
||||
<DMini keyText="SEC-4794" cveId="CVE-2025-0944" vendor="Cisco" status="Open" tone="amber" onEdit={() => {}} summary="Roll admin-console hotfix" />
|
||||
</DScroll>
|
||||
</DHomeCard>
|
||||
</div>
|
||||
</HSpecimen>
|
||||
|
||||
<h3 style={subhead}>Quick lookup → result banner</h3>
|
||||
<HSpecimen>
|
||||
<DHomeCard>
|
||||
<DCardTitle color={DHC.sky} icon="search">Quick CVE Lookup</DCardTitle>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<DInput placeholder="Enter CVE ID (e.g., CVE-2025-1014)" />
|
||||
<DBtn variant="primary" icon="search">Scan</DBtn>
|
||||
</div>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<DBanner tone="success" title="✓ CVE Addressed (3 vendors)">
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-2)' }}>Red Hat · Ubuntu · SUSE</div>
|
||||
</DBanner>
|
||||
</div>
|
||||
</DHomeCard>
|
||||
</HSpecimen>
|
||||
|
||||
<h3 style={subhead}>4-up stat strip</h3>
|
||||
<HSpecimen>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16 }}>
|
||||
<DStatCard label="Total CVEs" value="247" tone="sky" />
|
||||
<DStatCard label="Vendor Entries" value="412" tone="neutral" />
|
||||
<DStatCard label="Open Tickets" value="18" tone="amber" />
|
||||
<DStatCard label="Critical" value="6" tone="red" />
|
||||
</div>
|
||||
</HSpecimen>
|
||||
</HSection>
|
||||
|
||||
{/* REFERENCE */}
|
||||
<HSection id="reference" eyebrow="05 — Reference" title="Full Home page" blurb="Every primitive on this kit, composed exactly as App.js renders the home view. The frame below is a faithful reproduction — you can scroll inside it.">
|
||||
<div className="sample-frame" style={{
|
||||
border: '1px solid rgba(14,165,233,0.20)', borderRadius: 12,
|
||||
overflow: 'hidden', maxHeight: 900, overflowY: 'auto',
|
||||
background: 'var(--bg-page)',
|
||||
}}>
|
||||
<DHomePage />
|
||||
</div>
|
||||
</HSection>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
window.HOME_DOCS = { HKitDocs };
|
||||
37
docs/design-system-redesign/ui_kits/home/README.md
Normal file
37
docs/design-system-redesign/ui_kits/home/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Home UI Kit
|
||||
|
||||
Visual vocabulary for the CVE Dashboard home view (`currentPage === 'home'` in `frontend/src/App.js`).
|
||||
|
||||
## Files
|
||||
- `index.html` — entry point.
|
||||
- `HomePrimitives.jsx` — `StatCard`, `HomeCard`, `CardTitle`, `HomeButton`, `SeverityBadge`, `StatusBadge`, `HomeInput`, `HomeSelect`, `FieldLabel`, `ResultBanner`, `BigStat`, `MiniTicket`, `CVERow`, `VendorEntry`, `CalendarMini`, `ArchiveSummary`, `ScrollList`, `EmptyState`, `HomeIcon`.
|
||||
- `HomePage.jsx` — full-page assembly (`HomePage`).
|
||||
- `KitDocs.jsx` — browseable docs (Overview · Tokens · Components · Assemblies · Reference).
|
||||
|
||||
## Right-rail identity colors
|
||||
Each right-side panel owns one color, applied consistently to four surfaces:
|
||||
|
||||
| Panel | Color | Hex | Used for |
|
||||
|-------------------|----------|-----------|----------------------------------------------|
|
||||
| Calendar | sky | `#0EA5E9` | left-rail, title glow, today cell, day dots |
|
||||
| Open Tickets | amber | `#F59E0B` | left-rail, title glow, big stat, mini badges |
|
||||
| Archer Risk | purple | `#8B5CF6` | left-rail, title glow, big stat, mini badges |
|
||||
| Ivanti Workflows | teal | `#0D9488` | left-rail, title glow, big stat, mini badges |
|
||||
|
||||
## Layout
|
||||
- **Top:** 4-up stat strip (sky · neutral · amber · red).
|
||||
- **Body:** 12-col grid. Left 9 = Quick Lookup → Search/Filter → Results summary → CVE feed. Right 3 = vertical stack of right-rail panels.
|
||||
|
||||
## Card chrome (matches Reporting + KB)
|
||||
```
|
||||
background: linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)
|
||||
border: 1.5px solid rgba(14,165,233,0.12) /* 0.35 on hover */
|
||||
left-rail: 3px solid <identity-color> /* right-rail panels only */
|
||||
radius: 8px
|
||||
```
|
||||
|
||||
## Page-level rules
|
||||
1. Green appears in **one** place: the page title in the chrome (and as the lone primary CTA when present, e.g. "Scan").
|
||||
2. The four StatCard tones (sky/neutral/amber/red) map to (volume / inventory / attention / urgent). Don't reassign.
|
||||
3. Severity uses the heavy 2px-border SeverityBadge; ticket statuses use the 1px-border StatusBadge.
|
||||
4. Right-rail panels always lead with a BigStat. The number IS the headline.
|
||||
39
docs/design-system-redesign/ui_kits/home/index.html
Normal file
39
docs/design-system-redesign/ui_kits/home/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>STEAM Security · Home 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(14,165,233,0.2); 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="HomePrimitives.jsx"></script>
|
||||
<script type="text/babel" src="HomePage.jsx"></script>
|
||||
<script type="text/babel" src="KitDocs.jsx"></script>
|
||||
|
||||
<script type="text/babel">
|
||||
const { HKitDocs } = window.HOME_DOCS;
|
||||
function App() {
|
||||
return (
|
||||
<main data-screen-label="Home Kit">
|
||||
<HKitDocs />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
481
docs/design-system-redesign/ui_kits/reporting/KitDocs.jsx
Normal file
481
docs/design-system-redesign/ui_kits/reporting/KitDocs.jsx
Normal file
@@ -0,0 +1,481 @@
|
||||
// KitDocs.jsx — browseable docs page for the Reporting kit.
|
||||
// Sections: Overview · Tokens · Components · Assemblies · Reference page.
|
||||
|
||||
const { useState: useDocsState } = React;
|
||||
const {
|
||||
COLORS: DC, PageHeader, RptButton, KbCard, PillTab, FilterChip, StatusBanner,
|
||||
ToolbarLabel, SeverityDot, SlaPill, WorkflowBadge, DonutSample, RptIcon: DI,
|
||||
} = window.RPT;
|
||||
const { ReportingPage } = window.RPT_PAGE;
|
||||
|
||||
/* ── Layout primitives ─────────────────────────────────────────── */
|
||||
function Section({ 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: DC.sky, 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: 640, margin: '8px 0 0 0',
|
||||
}}>{blurb}</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Spec({ 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 CodeChip({ children }) {
|
||||
return (
|
||||
<code style={{
|
||||
display: 'inline-block', padding: '2px 6px', borderRadius: 3,
|
||||
background: 'rgba(14,165,233,0.10)', border: '1px solid rgba(14,165,233,0.18)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: DC.skySoft,
|
||||
}}>{children}</code>
|
||||
);
|
||||
}
|
||||
|
||||
function SwatchRow({ 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>
|
||||
<CodeChip>{value}</CodeChip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Sticky tab nav ─────────────────────────────────────────────── */
|
||||
function TabNav({ active, onChange }) {
|
||||
const items = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'tokens', label: 'Tokens' },
|
||||
{ id: 'components', label: 'Components' },
|
||||
{ id: 'assemblies', label: 'Assemblies' },
|
||||
{ id: 'reference', label: 'Reference page' },
|
||||
];
|
||||
return (
|
||||
<div style={{
|
||||
position: 'sticky', top: 0, zIndex: 20,
|
||||
background: 'rgba(15,23,42,0.92)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
borderBottom: '1px solid rgba(14,165,233,0.12)',
|
||||
padding: '14px 24px',
|
||||
}}>
|
||||
<div style={{ maxWidth: 1280, margin: '0 auto', display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700,
|
||||
color: DC.green, textTransform: 'uppercase', letterSpacing: '0.12em',
|
||||
textShadow: '0 0 12px rgba(16,185,129,0.25)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
Reporting Kit
|
||||
</div>
|
||||
<div style={{ width: 1, height: 18, background: 'rgba(255,255,255,0.08)' }} />
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{items.map((it) => (
|
||||
<PillTab key={it.id} active={active === it.id} onClick={() => onChange(it.id)}>
|
||||
{it.label}
|
||||
</PillTab>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Overview ───────────────────────────────────────────────────── */
|
||||
function OverviewSection() {
|
||||
return (
|
||||
<Section
|
||||
id="overview"
|
||||
eyebrow="01 · Overview"
|
||||
title="Reporting page UI kit"
|
||||
blurb="The visual vocabulary used by /reporting. Aligned to the Knowledge Base pattern: green-glow page identity, sky-blue surface accents, mono uppercase labels, Knowledge-Base card chrome on every panel."
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 14 }}>
|
||||
<KbCard label="Page identity" hover={false}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 18, fontWeight: 700,
|
||||
color: DC.green, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
textShadow: '0 0 12px rgba(16,185,129,0.25)',
|
||||
}}>Reporting</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)' }}>
|
||||
Green is reserved for the page title + the lone primary action (Sync). Everything else is sky.
|
||||
</div>
|
||||
</div>
|
||||
</KbCard>
|
||||
<KbCard label="Surface accent" hover={false}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{
|
||||
padding: 10, borderRadius: 6,
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: '1.5px solid rgba(14,165,233,0.35)',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: DC.skySoft,
|
||||
}}>
|
||||
KB card · sky border · 0.12 → 0.35 on hover
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)' }}>
|
||||
Same chrome for donuts, trend, and findings panel. No more colored left-rails.
|
||||
</div>
|
||||
</div>
|
||||
</KbCard>
|
||||
<KbCard label="Type" hover={false}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||
Card label · 11 / 600 / 0.1em
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14, color: 'var(--fg-1)' }}>JetBrains Mono · everywhere</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, color: 'var(--fg-muted)' }}>Outfit · prose only (blurbs)</div>
|
||||
</div>
|
||||
</KbCard>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Tokens ─────────────────────────────────────────────────────── */
|
||||
function TokensSection() {
|
||||
return (
|
||||
<Section
|
||||
id="tokens"
|
||||
eyebrow="02 · Tokens"
|
||||
title="Color roles, type, spacing"
|
||||
blurb="Reporting uses the dashboard token set. These are the specific roles the page leans on."
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: 14 }}>
|
||||
<KbCard label="Color roles" hover={false}>
|
||||
<SwatchRow name="--accent (sky-500)" value="#0EA5E9" role="Surfaces · pills · table headers · neutral btn" />
|
||||
<SwatchRow name="--intel-success" value="#10B981" role="Page title glow · primary Sync button" />
|
||||
<SwatchRow name="--intel-warning" value="#F59E0B" role="Filter active · anomaly · At-Risk SLA" />
|
||||
<SwatchRow name="--intel-danger" value="#EF4444" role="Errors · Critical sev · Overdue SLA" />
|
||||
<SwatchRow name="--text-disabled" value="#64748B" role="Card labels · meta text" />
|
||||
<SwatchRow name="--text-faint" value="#475569" role="Subtitle · separator counts" />
|
||||
</KbCard>
|
||||
<KbCard label="Card chrome" hover={false}>
|
||||
<Spec label="Background"><CodeChip>linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)</CodeChip></Spec>
|
||||
<Spec label="Border (rest)"><CodeChip>1.5px solid rgba(14,165,233,0.12)</CodeChip></Spec>
|
||||
<Spec label="Border (hover)"><CodeChip>1.5px solid rgba(14,165,233,0.35)</CodeChip></Spec>
|
||||
<Spec label="Radius"><CodeChip>8px</CodeChip></Spec>
|
||||
<Spec label="Padding"><CodeChip>16px (donuts) / 20px (panels)</CodeChip></Spec>
|
||||
<Spec label="Label divider"><CodeChip>1px solid rgba(255,255,255,0.04)</CodeChip></Spec>
|
||||
</KbCard>
|
||||
<KbCard label="Type scale" hover={false}>
|
||||
<Spec label="Page title">JetBrains Mono · 24 / 700 · 0.1em · uppercase · green glow</Spec>
|
||||
<Spec label="Subtitle / meta">Mono · 12 / 400 · slate-muted</Spec>
|
||||
<Spec label="Card label">Mono · 11 / 600 · 0.1em · uppercase · slate-disabled</Spec>
|
||||
<Spec label="Toolbar label">Mono · 11 / 700 · 0.1em · uppercase · sky</Spec>
|
||||
<Spec label="Button">Mono · 12 / 600 · 0.05em · uppercase</Spec>
|
||||
<Spec label="Pill tab">Mono · 11 / 600 · 0.05em · uppercase</Spec>
|
||||
<Spec label="Table cell">Mono · 11 / 400</Spec>
|
||||
</KbCard>
|
||||
<KbCard label="Spacing & motion" hover={false}>
|
||||
<Spec label="Page gap"><CodeChip>20px</CodeChip> between major sections</Spec>
|
||||
<Spec label="Donut grid"><CodeChip>repeat(auto-fill, minmax(220px, 1fr))</CodeChip> · gap 14</Spec>
|
||||
<Spec label="Toolbar gap">8px between buttons · 6px subtle group</Spec>
|
||||
<Spec label="Hover transition"><CodeChip>border-color 150ms cubic-bezier(0.4,0,0.2,1)</CodeChip></Spec>
|
||||
<Spec label="Spinner"><CodeChip>1s linear infinite</CodeChip></Spec>
|
||||
</KbCard>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Components ─────────────────────────────────────────────────── */
|
||||
function ComponentsSection() {
|
||||
const [tab, setTab] = useDocsState('ivanti');
|
||||
return (
|
||||
<Section
|
||||
id="components"
|
||||
eyebrow="03 · Components"
|
||||
title="Primitives"
|
||||
blurb="Each component is a thin wrapper around the inline-style pattern used in ReportingPage.js. Drop into other pages that need to inherit the same vocabulary."
|
||||
>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 14 }}>
|
||||
{/* Buttons */}
|
||||
<KbCard label="Buttons" hover={false}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', padding: '4px 0 12px' }}>
|
||||
<RptButton variant="primary" icon={<DI.Refresh size={13} />}>Sync</RptButton>
|
||||
<RptButton variant="neutral" icon={<DI.Atlas size={13} />}>Atlas</RptButton>
|
||||
<RptButton variant="subtle" icon={<DI.Download size={12} />}>Export</RptButton>
|
||||
<RptButton variant="danger" icon={<DI.AlertCircle size={12} />}>Reset</RptButton>
|
||||
<RptButton variant="neutral" disabled icon={<DI.Loader size={13} />}>Disabled</RptButton>
|
||||
</div>
|
||||
<Spec label="primary">Green tinted-fill · the only primary on the page (Sync)</Spec>
|
||||
<Spec label="neutral">Sky outlined · transparent · for Atlas, Prev/Next, etc.</Spec>
|
||||
<Spec label="subtle">Sky tinted-fill · for in-toolbar actions (Export, Queue, Columns)</Spec>
|
||||
<Spec label="danger">Red tinted-fill · destructive only</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Pill tabs */}
|
||||
<KbCard label="Pill tabs (metric switcher)" hover={false}>
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center', padding: '4px 0 12px' }}>
|
||||
<DI.PieChart size={14} style={{ color: '#334155', marginRight: 4 }} />
|
||||
<PillTab active={tab === 'ivanti'} onClick={() => setTab('ivanti')}>Ivanti Findings</PillTab>
|
||||
<PillTab active={tab === 'atlas'} onClick={() => setTab('atlas')}>Atlas Coverage</PillTab>
|
||||
<PillTab active={tab === 'sla'} onClick={() => setTab('sla')}>SLA</PillTab>
|
||||
</div>
|
||||
<Spec label="Active">sky border + sky-15% fill + sky text</Spec>
|
||||
<Spec label="Hover (inactive)">subtle white-10% border, slate-300 text</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Filter chips */}
|
||||
<KbCard label="Filter chips" hover={false}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', padding: '4px 0 12px' }}>
|
||||
<FilterChip color={DC.amber}>Severity: Critical</FilterChip>
|
||||
<FilterChip color={DC.sky}>Action: Patch</FilterChip>
|
||||
<FilterChip color={DC.red}>SLA: Overdue</FilterChip>
|
||||
</div>
|
||||
<Spec label="Color">Tinted to the dimension being filtered</Spec>
|
||||
<Spec label="Click">Clears the filter</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Status banners */}
|
||||
<KbCard label="Status banners" hover={false}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: '4px 0 12px' }}>
|
||||
<StatusBanner tone="error">Atlas: connection refused — retry in 30s</StatusBanner>
|
||||
<StatusBanner tone="warn">Sync stale (last success 4 hours ago)</StatusBanner>
|
||||
<StatusBanner tone="info">12 findings reassigned to platform-team</StatusBanner>
|
||||
</div>
|
||||
<Spec label="Placement">Header-level for system errors; inline above target for action results</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Severity / SLA / Workflow badges */}
|
||||
<KbCard label="Cell badges" hover={false}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, padding: '4px 0 12px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<SeverityDot level="Critical" />
|
||||
<SeverityDot level="High" />
|
||||
<SeverityDot level="Medium" />
|
||||
<SeverityDot level="Low" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<SlaPill status="OVERDUE" />
|
||||
<SlaPill status="AT_RISK" />
|
||||
<SlaPill status="WITHIN_SLA" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<WorkflowBadge state="OPEN" />
|
||||
<WorkflowBadge state="FP" />
|
||||
<WorkflowBadge state="EXC" />
|
||||
<WorkflowBadge state="REMEDIATED" />
|
||||
</div>
|
||||
</div>
|
||||
<Spec label="Severity">Dot + glow + soft-text label · fixed semantic colors</Spec>
|
||||
<Spec label="SLA">Pill · OVERDUE/AT_RISK/WITHIN_SLA</Spec>
|
||||
<Spec label="Workflow">Tagged badge · OPEN/FP/EXC/REMEDIATED/ARCHIVED</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* KB card itself */}
|
||||
<KbCard label="KB Card" hover={false}>
|
||||
<KbCard label="Open vs Closed" style={{ marginBottom: 10 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '10px 0' }}>
|
||||
<DonutSample
|
||||
segments={[
|
||||
{ label: 'Open', value: 184, color: DC.sky },
|
||||
{ label: 'Closed', value: 712, color: DC.green },
|
||||
]}
|
||||
size={110}
|
||||
centerLabel="TOTAL" centerValue="896" />
|
||||
</div>
|
||||
</KbCard>
|
||||
<Spec label="Container">KB card chrome + label divider</Spec>
|
||||
<Spec label="Body">Centered donut · 170 min-height · responsive auto-fill grid</Spec>
|
||||
</KbCard>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Assemblies ─────────────────────────────────────────────────── */
|
||||
function AssembliesSection() {
|
||||
return (
|
||||
<Section
|
||||
id="assemblies"
|
||||
eyebrow="04 · Assemblies"
|
||||
title="Page-level patterns"
|
||||
blurb="Three combinations the Reporting page is built from. Reuse them as-is on related pages (e.g. dashboards, audit logs)."
|
||||
>
|
||||
{/* Header assembly */}
|
||||
<KbCard label="① Page header + meta + actions" hover={false} style={{ marginBottom: 14 }}>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<PageHeader
|
||||
title="Reporting"
|
||||
meta={
|
||||
<>
|
||||
Last sync: 2 minutes ago
|
||||
<span style={{ marginLeft: 10, color: '#334155' }}>· 184 of 896 findings</span>
|
||||
<span style={{ marginLeft: 8, color: DC.amber }}>(3 filters active)</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<RptButton variant="neutral" icon={<DI.Atlas size={13} />}>Atlas</RptButton>
|
||||
<RptButton variant="primary" icon={<DI.Refresh size={13} />}>Sync</RptButton>
|
||||
</PageHeader>
|
||||
</div>
|
||||
<Spec label="Title">Mono uppercase · green glow · 24px</Spec>
|
||||
<Spec label="Meta line">Sync timestamp → record count → active filter count (amber)</Spec>
|
||||
<Spec label="Actions">Right-aligned · neutral secondaries → primary on far right</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Donut grid assembly */}
|
||||
<KbCard label="② Metric tabs + donut grid" hover={false} style={{ marginBottom: 14 }}>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center', marginBottom: 12 }}>
|
||||
<DI.PieChart size={14} style={{ color: '#334155', marginRight: 4 }} />
|
||||
<PillTab active onClick={() => {}}>Ivanti Findings</PillTab>
|
||||
<PillTab active={false} onClick={() => {}}>Atlas Coverage</PillTab>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 12 }}>
|
||||
{[
|
||||
{ label: 'Open vs Closed', segs: [{ label: 'Open', value: 184, color: DC.sky }, { label: 'Closed', value: 712, color: DC.green }], cl: 'TOTAL', cv: '896' },
|
||||
{ label: 'Action Coverage', segs: [{ label: 'Patch', value: 96, color: DC.sky }, { label: 'Mitigate', value: 42, color: DC.green }, { label: 'Accept', value: 28, color: '#A78BFA' }], cl: 'ASSIGNED', cv: '184' },
|
||||
{ label: 'FP Status', segs: [{ label: 'Pending', value: 14, color: DC.amber }, { label: 'Approved', value: 31, color: DC.green }, { label: 'Rejected', value: 6, color: DC.red }], cl: 'FINDINGS', cv: '51' },
|
||||
].map((d) => (
|
||||
<KbCard key={d.label} label={d.label}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', minHeight: 150 }}>
|
||||
<DonutSample size={100} segments={d.segs} centerLabel={d.cl} centerValue={d.cv} />
|
||||
</div>
|
||||
</KbCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Spec label="Tabs">Pill row sits above grid · scopes which donuts render</Spec>
|
||||
<Spec label="Grid">Auto-fill, 220px min · each donut is its own KB card</Spec>
|
||||
</KbCard>
|
||||
|
||||
{/* Findings panel chrome */}
|
||||
<KbCard label="③ Findings panel chrome (toolbar + filters + table)" hover={false}>
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: '1.5px solid rgba(14,165,233,0.12)', borderRadius: 8, padding: 16,
|
||||
marginTop: 8,
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
paddingBottom: 10, marginBottom: 10,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
}}>
|
||||
<ToolbarLabel count="184 of 896">Host Findings</ToolbarLabel>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<RptButton variant="subtle" icon={<DI.Download size={12} />}>Export</RptButton>
|
||||
<RptButton variant="subtle" icon={<DI.ListTodo size={12} />}>Queue</RptButton>
|
||||
<RptButton variant="subtle" icon={<DI.Settings size={12} />}>Columns</RptButton>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<FilterChip color={DC.amber}>Severity: Critical, High</FilterChip>
|
||||
<FilterChip color={DC.sky}>Action: Patch</FilterChip>
|
||||
<FilterChip color={DC.red}>SLA: Overdue</FilterChip>
|
||||
</div>
|
||||
</div>
|
||||
<Spec label="Toolbar">Mono uppercase label + count · subtle action buttons right</Spec>
|
||||
<Spec label="Filter row">Tinted chips, click-to-clear</Spec>
|
||||
<Spec label="Header migration">Sync/Atlas no longer live here — they're in the page header</Spec>
|
||||
</KbCard>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Reference page ─────────────────────────────────────────────── */
|
||||
function ReferenceSection() {
|
||||
return (
|
||||
<Section
|
||||
id="reference"
|
||||
eyebrow="05 · Reference page"
|
||||
title="Full Reporting page"
|
||||
blurb="Static mock of /reporting using only kit primitives. Use this to verify any change you make to a primitive flows through the page intact."
|
||||
>
|
||||
<div style={{
|
||||
background: 'var(--bg-page)',
|
||||
border: '1px solid rgba(14,165,233,0.12)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<ReportingPage />
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Top-level docs page ─────────────────────────────────────────── */
|
||||
function KitDocs() {
|
||||
const [active, setActive] = useDocsState('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' });
|
||||
}
|
||||
};
|
||||
|
||||
// observe scroll position to update active tab
|
||||
React.useEffect(() => {
|
||||
const sections = ['overview', 'tokens', 'components', 'assemblies', 'reference']
|
||||
.map((id) => document.getElementById(id))
|
||||
.filter(Boolean);
|
||||
const onScroll = () => {
|
||||
const y = window.scrollY + 160;
|
||||
let cur = sections[0]?.id;
|
||||
for (const s of sections) {
|
||||
if (s.offsetTop <= y) cur = s.id;
|
||||
}
|
||||
setActive(cur);
|
||||
};
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
onScroll();
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TabNav active={active} onChange={handle} />
|
||||
<div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 24px 80px' }}>
|
||||
<OverviewSection />
|
||||
<TokensSection />
|
||||
<ComponentsSection />
|
||||
<AssembliesSection />
|
||||
<ReferenceSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.RPT_DOCS = { KitDocs };
|
||||
36
docs/design-system-redesign/ui_kits/reporting/README.md
Normal file
36
docs/design-system-redesign/ui_kits/reporting/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Reporting UI Kit
|
||||
|
||||
The visual vocabulary used by `/reporting` after the Knowledge Base alignment pass.
|
||||
|
||||
## Files
|
||||
- `index.html` — entry point. Loads the kit docs page.
|
||||
- `ReportPrimitives.jsx` — `PageHeader`, `RptButton`, `KbCard`, `PillTab`, `FilterChip`, `StatusBanner`, `ToolbarLabel`, `SeverityDot`, `SlaPill`, `WorkflowBadge`, `DonutSample`, `RptIcon`.
|
||||
- `ReportingPage.jsx` — full-page reference assembly (`ReportingPage`).
|
||||
- `KitDocs.jsx` — browseable docs (Overview · Tokens · Components · Assemblies · Reference).
|
||||
|
||||
## Color roles
|
||||
- **Sky `#0EA5E9`** — surface accent (panel borders, tab pill active, donut highlight, table header text, neutral secondary buttons).
|
||||
- **Green `#10B981`** — page identity only: title glow + the lone primary action (Sync).
|
||||
- **Amber `#F59E0B`** — filter active, anomaly callout, At-Risk SLA.
|
||||
- **Red `#EF4444`** — error / Critical / Overdue.
|
||||
|
||||
## Card chrome (one chrome, every panel)
|
||||
```
|
||||
background: linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)
|
||||
border: 1.5px solid rgba(14,165,233,0.12) /* 0.35 on hover */
|
||||
radius: 8px
|
||||
label: mono · 11 / 600 · 0.1em · uppercase · slate-disabled
|
||||
divider: 1px solid rgba(255,255,255,0.04) under the label
|
||||
```
|
||||
|
||||
## Button hierarchy
|
||||
- `primary` (green tinted-fill) — **only** Sync uses this.
|
||||
- `neutral` (sky outlined transparent) — Atlas, Prev/Next, refresh.
|
||||
- `subtle` (sky tinted-fill) — Export, Queue, Columns, Rows.
|
||||
- `danger` (red tinted-fill) — destructive only.
|
||||
|
||||
## Page-level rules
|
||||
1. `Sync` and `Atlas` live in the **page header**, not the findings panel toolbar.
|
||||
2. The page title is the only place green appears as identity. Anywhere else, green = success state.
|
||||
3. Every metric panel is a KB card. No more colored left-rails.
|
||||
4. Filter chips tint to the dimension being filtered (severity → amber, SLA → red, action → sky).
|
||||
@@ -0,0 +1,393 @@
|
||||
// ReportPrimitives.jsx — Reporting-specific UI vocabulary.
|
||||
// All inline styles + tokens from ../../colors_and_type.css.
|
||||
// Mirrors the live Reporting page (frontend/src/components/pages/ReportingPage.js)
|
||||
// after the Knowledge-Base alignment pass.
|
||||
|
||||
const { useState: useRPTState } = React;
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
COLOR ROLE MAP (Reporting)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Sky-blue (#0EA5E9) → primary surface accent (panel borders,
|
||||
tab pill active, donut highlight, table
|
||||
header text, neutral secondary buttons)
|
||||
Green (#10B981) → page identity (header glow + primary
|
||||
Sync button)
|
||||
Amber (#F59E0B) → filter-active indicator, anomaly callout
|
||||
Red (#EF4444) → error / overdue
|
||||
Slate stack → muted text + dividers (#475569 → #334155)
|
||||
──────────────────────────────────────────────────────────────── */
|
||||
|
||||
const COLORS = {
|
||||
sky: '#0EA5E9',
|
||||
skySoft: '#7DD3FC',
|
||||
green: '#10B981',
|
||||
amber: '#F59E0B',
|
||||
red: '#EF4444',
|
||||
redSoft: '#FCA5A5',
|
||||
};
|
||||
|
||||
/* ── Page header ─────────────────────────────────────────────────
|
||||
Big mono uppercase title in green w/ glow + count subtitle.
|
||||
Right side: neutral icon-tinted secondaries + tinted-fill primary.
|
||||
Lifted from the existing Knowledge Base page header pattern. */
|
||||
function PageHeader({ title = 'Reporting', meta, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16 }}>
|
||||
<div>
|
||||
<h2 style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 24, fontWeight: 700,
|
||||
color: COLORS.green, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
textShadow: '0 0 16px rgba(16,185,129,0.25)',
|
||||
margin: '0 0 4px 0',
|
||||
}}>
|
||||
{title}
|
||||
</h2>
|
||||
{meta && (
|
||||
<div style={{ fontSize: 12, color: 'var(--fg-muted)', fontFamily: 'var(--font-mono)' }}>
|
||||
{meta}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexShrink: 0, alignItems: 'center' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────
|
||||
THREE variants documented for Reporting:
|
||||
• primary — green tinted-fill (lone primary action: Sync)
|
||||
• neutral — sky outlined transparent (Atlas, refresh, etc.)
|
||||
• subtle — sky tinted-fill (Export, Queue, Column manager)
|
||||
*/
|
||||
function RptButton({ variant = 'neutral', icon, children, disabled, ...rest }) {
|
||||
const [hover, setHover] = useRPTState(false);
|
||||
const v = {
|
||||
primary: {
|
||||
bgRest: 'rgba(16,185,129,0.18)',
|
||||
bgHover: 'rgba(16,185,129,0.26)',
|
||||
bd: COLORS.green, fg: COLORS.green,
|
||||
},
|
||||
neutral: {
|
||||
bgRest: 'transparent',
|
||||
bgHover: 'rgba(14,165,233,0.06)',
|
||||
bd: 'rgba(14,165,233,0.25)', fg: COLORS.sky,
|
||||
},
|
||||
subtle: {
|
||||
bgRest: 'rgba(14,165,233,0.08)',
|
||||
bgHover: 'rgba(14,165,233,0.16)',
|
||||
bd: 'rgba(14,165,233,0.35)', fg: COLORS.sky,
|
||||
},
|
||||
danger: {
|
||||
bgRest: 'rgba(239,68,68,0.08)',
|
||||
bgHover: 'rgba(239,68,68,0.16)',
|
||||
bd: 'rgba(239,68,68,0.30)', fg: COLORS.red,
|
||||
},
|
||||
}[variant];
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: hover && !disabled ? v.bgHover : v.bgRest,
|
||||
border: `1px solid ${hover && !disabled && variant === 'neutral' ? 'rgba(14,165,233,0.55)' : v.bd}`,
|
||||
color: disabled ? 'var(--fg-disabled)' : v.fg,
|
||||
padding: '8px 14px', borderRadius: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
transition: 'all 150ms cubic-bezier(0.4,0,0.2,1)',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{icon}{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── KB-style card (sky) — used for donuts + findings panel ──── */
|
||||
function KbCard({ children, padding = 16, label, labelExtra, hover = true, style }) {
|
||||
const [h, setH] = useRPTState(false);
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => hover && setH(true)} onMouseLeave={() => setH(false)}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: `1.5px solid ${h ? 'rgba(14,165,233,0.35)' : 'rgba(14,165,233,0.12)'}`,
|
||||
borderRadius: 8, padding,
|
||||
display: 'flex', flexDirection: 'column', gap: 10,
|
||||
transition: 'border-color 150ms cubic-bezier(0.4,0,0.2,1)',
|
||||
...style,
|
||||
}}>
|
||||
{label && (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
paddingBottom: 8,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<span>{label}</span>
|
||||
{labelExtra}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Pill tab (Ivanti / Atlas) ───────────────────────────────── */
|
||||
function PillTab({ active, color = COLORS.sky, onClick, children }) {
|
||||
const [hover, setHover] = useRPTState(false);
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||
cursor: 'pointer', borderRadius: 4,
|
||||
border: `1px solid ${active ? color : (hover ? 'rgba(255,255,255,0.10)' : 'transparent')}`,
|
||||
background: active ? `${color}26` : 'transparent',
|
||||
color: active ? color : (hover ? '#94A3B8' : 'var(--fg-muted)'),
|
||||
transition: 'all 120ms',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Filter chip (active filter pin in the toolbar) ──────────── */
|
||||
function FilterChip({ color = COLORS.amber, onClear, children }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClear}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '6px 12px',
|
||||
background: `${color}14`,
|
||||
border: `1px solid ${color}4D`,
|
||||
borderRadius: 6,
|
||||
color, cursor: 'pointer',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
}}
|
||||
>
|
||||
<RptIcon.Filter size={11} />
|
||||
{children}
|
||||
<span style={{ marginLeft: 2, opacity: 0.7 }}>×</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Status banner (error / Atlas error / sync error) ────────── */
|
||||
function StatusBanner({ tone = 'error', children }) {
|
||||
const tones = {
|
||||
error: { bg: 'rgba(239,68,68,0.08)', bd: 'rgba(239,68,68,0.25)', fg: COLORS.redSoft, icon: COLORS.red },
|
||||
warn: { bg: 'rgba(245,158,11,0.10)', bd: 'rgba(245,158,11,0.28)', fg: '#FCD34D', icon: COLORS.amber },
|
||||
info: { bg: 'rgba(14,165,233,0.08)', bd: 'rgba(14,165,233,0.25)', fg: COLORS.skySoft, icon: COLORS.sky },
|
||||
};
|
||||
const t = tones[tone];
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 8,
|
||||
padding: '10px 14px', background: t.bg, border: `1px solid ${t.bd}`,
|
||||
borderRadius: 8,
|
||||
}}>
|
||||
<RptIcon.AlertCircle size={15} style={{ color: t.icon, flexShrink: 0, marginTop: 1 }} />
|
||||
<span style={{ fontSize: 12, color: t.fg, fontFamily: 'var(--font-mono)' }}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Toolbar label (small mono uppercase, used inside findings panel) ── */
|
||||
function ToolbarLabel({ children, accent = COLORS.sky, count }) {
|
||||
return (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700,
|
||||
color: accent, textTransform: 'uppercase', letterSpacing: '0.1em',
|
||||
}}>
|
||||
{children}
|
||||
{count != null && (
|
||||
<span style={{ marginLeft: 10, color: '#334155', fontWeight: 400 }}>
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Severity dot (used in table rows) ───────────────────────── */
|
||||
function SeverityDot({ level }) {
|
||||
const map = {
|
||||
Critical: { c: COLORS.red, text: '#FCA5A5' },
|
||||
High: { c: COLORS.amber, text: '#FCD34D' },
|
||||
Medium: { c: COLORS.sky, text: '#7DD3FC' },
|
||||
Low: { c: COLORS.green, text: '#6EE7B7' },
|
||||
Info: { c: '#94A3B8', text: '#CBD5E1' },
|
||||
};
|
||||
const v = map[level] || map.Info;
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
|
||||
color: v.text, letterSpacing: '0.04em',
|
||||
}}>
|
||||
<span style={{
|
||||
width: 7, height: 7, borderRadius: '50%', background: v.c,
|
||||
boxShadow: `0 0 6px ${v.c}99`,
|
||||
}} />
|
||||
{level}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── SLA pill (table cell) ───────────────────────────────────── */
|
||||
function SlaPill({ status }) {
|
||||
const map = {
|
||||
OVERDUE: { c: COLORS.red, bg: 'rgba(239,68,68,0.16)' },
|
||||
AT_RISK: { c: COLORS.amber, bg: 'rgba(245,158,11,0.16)' },
|
||||
WITHIN_SLA: { c: COLORS.green, bg: 'rgba(16,185,129,0.16)' },
|
||||
};
|
||||
const v = map[status] || map.WITHIN_SLA;
|
||||
return (
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 999,
|
||||
background: v.bg, color: v.c,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10,
|
||||
letterSpacing: '0.05em',
|
||||
}}>
|
||||
{status.replace('_', ' ')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Workflow badge (table cell) ─────────────────────────────── */
|
||||
function WorkflowBadge({ state }) {
|
||||
const map = {
|
||||
OPEN: { c: COLORS.sky, bg: 'rgba(14,165,233,0.14)' },
|
||||
FP: { c: COLORS.amber, bg: 'rgba(245,158,11,0.14)' },
|
||||
EXC: { c: '#A78BFA', bg: 'rgba(167,139,250,0.14)' },
|
||||
REMEDIATED:{ c: COLORS.green, bg: 'rgba(16,185,129,0.14)' },
|
||||
ARCHIVED: { c: '#94A3B8', bg: 'rgba(148,163,184,0.14)' },
|
||||
};
|
||||
const v = map[state] || { c: 'var(--fg-muted)', bg: 'rgba(148,163,184,0.10)' };
|
||||
return (
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 4,
|
||||
background: v.bg, color: v.c, border: `1px solid ${v.c}55`,
|
||||
fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: 10,
|
||||
letterSpacing: '0.05em',
|
||||
}}>
|
||||
{state}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Donut placeholder — semantic stand-in for the real recharts donut ── */
|
||||
function DonutSample({ size = 130, segments, centerLabel, centerValue }) {
|
||||
// segments: [{ label, value, color }]
|
||||
const total = segments.reduce((s, x) => s + x.value, 0);
|
||||
const cx = size / 2, cy = size / 2;
|
||||
const outerR = size / 2 - 4, innerR = outerR - 16;
|
||||
let angle = -90;
|
||||
const arcs = segments.map((seg) => {
|
||||
const sweep = (seg.value / total) * 360;
|
||||
const a0 = (angle * Math.PI) / 180;
|
||||
const a1 = ((angle + sweep) * Math.PI) / 180;
|
||||
const large = sweep > 180 ? 1 : 0;
|
||||
const x0 = cx + outerR * Math.cos(a0), y0 = cy + outerR * Math.sin(a0);
|
||||
const x1 = cx + outerR * Math.cos(a1), y1 = cy + outerR * Math.sin(a1);
|
||||
const xi1 = cx + innerR * Math.cos(a1), yi1 = cy + innerR * Math.sin(a1);
|
||||
const xi0 = cx + innerR * Math.cos(a0), yi0 = cy + innerR * Math.sin(a0);
|
||||
const d = `M ${x0} ${y0} A ${outerR} ${outerR} 0 ${large} 1 ${x1} ${y1}
|
||||
L ${xi1} ${yi1} A ${innerR} ${innerR} 0 ${large} 0 ${xi0} ${yi0} Z`;
|
||||
angle += sweep;
|
||||
return { d, color: seg.color };
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<svg width={size} height={size}>
|
||||
{arcs.map((a, i) => (
|
||||
<path key={i} d={a.d} fill={a.color} stroke="rgba(15,23,42,0.95)" strokeWidth="1" />
|
||||
))}
|
||||
</svg>
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 700,
|
||||
color: 'var(--fg-1)', lineHeight: 1,
|
||||
}}>{centerValue}</div>
|
||||
{centerLabel && (
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 9, fontWeight: 600,
|
||||
color: 'var(--fg-disabled)', textTransform: 'uppercase', letterSpacing: '0.12em',
|
||||
marginTop: 4,
|
||||
}}>
|
||||
{centerLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Legend */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px 10px', justifyContent: 'center', maxWidth: size + 32 }}>
|
||||
{segments.map((s) => (
|
||||
<div key={s.label} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--fg-muted)',
|
||||
letterSpacing: '0.04em',
|
||||
}}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 2, background: s.color, flexShrink: 0 }} />
|
||||
<span>{s.label} <span style={{ color: 'var(--fg-disabled)' }}>{s.value}</span></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Inline lucide icons (Reporting subset) ──────────────────── */
|
||||
const _ic = (path) => ({ size = 14, strokeWidth = 1.75, ...rest }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ flexShrink: 0 }} {...rest}>{path}</svg>
|
||||
);
|
||||
const RptIcon = {
|
||||
Refresh: _ic(<><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></>),
|
||||
PieChart: _ic(<><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></>),
|
||||
Filter: _ic(<><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></>),
|
||||
Download: _ic(<><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></>),
|
||||
ChevronD: _ic(<><polyline points="6 9 12 15 18 9"/></>),
|
||||
ChevronUp: _ic(<><polyline points="18 15 12 9 6 15"/></>),
|
||||
ChevronUpDn:_ic(<><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></>),
|
||||
ListTodo: _ic(<><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></>),
|
||||
Settings: _ic(<><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>),
|
||||
Eye: _ic(<><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></>),
|
||||
EyeOff: _ic(<><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" y1="2" x2="22" y2="22"/></>),
|
||||
AlertCircle:_ic(<><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"/></>),
|
||||
AlertTri: _ic(<><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></>),
|
||||
Atlas: _ic(<><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10"/><path d="M12 2a15.3 15.3 0 0 0-4 10 15.3 15.3 0 0 0 4 10"/></>),
|
||||
Search: _ic(<><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></>),
|
||||
Square: _ic(<><rect x="3" y="3" width="18" height="18" rx="2"/></>),
|
||||
CheckSq: _ic(<><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></>),
|
||||
Loader: _ic(<><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"/></>),
|
||||
TrendUp: _ic(<><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></>),
|
||||
};
|
||||
|
||||
window.RPT = {
|
||||
COLORS,
|
||||
PageHeader, RptButton, KbCard, PillTab, FilterChip, StatusBanner,
|
||||
ToolbarLabel, SeverityDot, SlaPill, WorkflowBadge, DonutSample,
|
||||
RptIcon,
|
||||
};
|
||||
299
docs/design-system-redesign/ui_kits/reporting/ReportingPage.jsx
Normal file
299
docs/design-system-redesign/ui_kits/reporting/ReportingPage.jsx
Normal file
@@ -0,0 +1,299 @@
|
||||
// ReportingPage.jsx — full-page assembly using only RPT primitives.
|
||||
// Mirrors frontend/src/components/pages/ReportingPage.js after the KB pass.
|
||||
|
||||
const { useState: useRPSt } = React;
|
||||
const {
|
||||
COLORS: RC,
|
||||
PageHeader, RptButton, KbCard, PillTab, FilterChip, StatusBanner,
|
||||
ToolbarLabel, SeverityDot, SlaPill, WorkflowBadge, DonutSample,
|
||||
RptIcon: RI,
|
||||
} = window.RPT;
|
||||
|
||||
/* Sample findings rows. Static — purely for layout. */
|
||||
const SAMPLE_ROWS = [
|
||||
{ id: 'F-10241', host: 'web-prod-04.steam.local', os: 'Ubuntu 22.04', sev: 'Critical', cve: 'CVE-2024-3094', age: 4, sla: 'OVERDUE', state: 'OPEN', action: 'Patch', owner: 'platform' },
|
||||
{ id: 'F-10238', host: 'kafka-broker-2.steam.local',os: 'RHEL 9.3', sev: 'Critical', cve: 'CVE-2024-21626', age: 11, sla: 'OVERDUE', state: 'FP', action: 'Investigate',owner: 'data-eng' },
|
||||
{ id: 'F-10202', host: 'auth-prod-01.steam.local', os: 'Ubuntu 22.04', sev: 'High', cve: 'CVE-2024-1086', age: 3, sla: 'AT_RISK', state: 'OPEN', action: 'Patch', owner: 'platform' },
|
||||
{ id: 'F-10197', host: 'edge-cdn-09.steam.local', os: 'Alpine 3.19', sev: 'High', cve: 'CVE-2024-23222', age: 2, sla: 'AT_RISK', state: 'EXC', action: 'Accept', owner: 'edge' },
|
||||
{ id: 'F-10185', host: 'analytics-w-3.steam.local',os: 'Ubuntu 20.04', sev: 'Medium', cve: 'CVE-2023-50387', age: 14, sla: 'WITHIN_SLA', state: 'OPEN', action: 'Mitigate', owner: 'analytics' },
|
||||
{ id: 'F-10180', host: 'mail-relay-1.steam.local', os: 'Debian 12', sev: 'Medium', cve: 'CVE-2024-22195', age: 9, sla: 'WITHIN_SLA', state: 'REMEDIATED', action: 'Patch', owner: 'platform' },
|
||||
{ id: 'F-10164', host: 'jumphost-2.steam.local', os: 'Ubuntu 22.04', sev: 'Low', cve: 'CVE-2023-45288', age: 22, sla: 'WITHIN_SLA', state: 'OPEN', action: 'Defer', owner: 'sre' },
|
||||
];
|
||||
|
||||
/* Tiny anomaly bar chart placeholder for the trend section. */
|
||||
function TrendChartPlaceholder() {
|
||||
const data = [22, 28, 21, 24, 30, 27, 26, 25, 31, 38, 42, 45, 41, 36, 33];
|
||||
const closed = [10, 14, 12, 13, 18, 20, 22, 21, 24, 26, 28, 30, 31, 30, 29];
|
||||
const max = Math.max(...data);
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 120, padding: '4px 0' }}>
|
||||
{data.map((d, i) => (
|
||||
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 2 }}>
|
||||
<div style={{
|
||||
height: `${(d / max) * 100}%`,
|
||||
background: 'linear-gradient(180deg, rgba(14,165,233,0.85), rgba(14,165,233,0.45))',
|
||||
borderRadius: '2px 2px 0 0',
|
||||
}} />
|
||||
<div style={{
|
||||
height: `${(closed[i] / max) * 60}%`,
|
||||
background: 'rgba(16,185,129,0.55)',
|
||||
borderRadius: '0 0 2px 2px',
|
||||
}} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReportingPage() {
|
||||
const [tab, setTab] = useRPSt('ivanti');
|
||||
const [actionFilter, setActionFilter] = useRPSt(null);
|
||||
|
||||
/* Donut data (illustrative) */
|
||||
const ivantiDonuts = [
|
||||
{
|
||||
label: 'Open vs Closed',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Open', value: 184, color: RC.sky },
|
||||
{ label: 'Closed', value: 712, color: RC.green },
|
||||
]}
|
||||
centerLabel="TOTAL" centerValue="896" />,
|
||||
},
|
||||
{
|
||||
label: 'Action Coverage',
|
||||
labelExtra: actionFilter && (
|
||||
<span style={{ color: RC.amber, fontSize: 9 }}>● filtered</span>
|
||||
),
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Patch', value: 96, color: RC.sky },
|
||||
{ label: 'Mitigate', value: 42, color: RC.green },
|
||||
{ label: 'Accept', value: 28, color: '#A78BFA' },
|
||||
{ label: 'Investigate', value: 18, color: RC.amber },
|
||||
]}
|
||||
centerLabel="ASSIGNED" centerValue="184" />,
|
||||
},
|
||||
{
|
||||
label: 'FP Finding Status',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Pending', value: 14, color: RC.amber },
|
||||
{ label: 'Approved', value: 31, color: RC.green },
|
||||
{ label: 'Rejected', value: 6, color: RC.red },
|
||||
]}
|
||||
centerLabel="FINDINGS" centerValue="51" />,
|
||||
},
|
||||
{
|
||||
label: 'FP Workflow Status',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'In Review', value: 8, color: RC.sky },
|
||||
{ label: 'Closed', value: 22, color: RC.green },
|
||||
{ label: 'Escalated', value: 4, color: RC.red },
|
||||
]}
|
||||
centerLabel="FP TICKETS" centerValue="34" />,
|
||||
},
|
||||
];
|
||||
|
||||
const atlasDonuts = [
|
||||
{
|
||||
label: 'Host Coverage',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'With Plans', value: 312, color: RC.green },
|
||||
{ label: 'Without Plans', value: 88, color: RC.amber },
|
||||
]}
|
||||
centerLabel="HOSTS" centerValue="400" />,
|
||||
},
|
||||
{
|
||||
label: 'Plan Types',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Patch', value: 142, color: RC.sky },
|
||||
{ label: 'Mitigate', value: 68, color: RC.green },
|
||||
{ label: 'Accept', value: 31, color: '#A78BFA' },
|
||||
]}
|
||||
centerLabel="PLANS" centerValue="241" />,
|
||||
},
|
||||
{
|
||||
label: 'Plan Status',
|
||||
donut: <DonutSample
|
||||
segments={[
|
||||
{ label: 'Active', value: 184, color: RC.green },
|
||||
{ label: 'Pending', value: 42, color: RC.amber },
|
||||
{ label: 'Stalled', value: 15, color: RC.red },
|
||||
]}
|
||||
centerLabel="STATUS" centerValue="241" />,
|
||||
},
|
||||
];
|
||||
|
||||
const donuts = tab === 'ivanti' ? ivantiDonuts : atlasDonuts;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 20,
|
||||
padding: 24, maxWidth: 1280, margin: '0 auto',
|
||||
}}>
|
||||
{/* Page header */}
|
||||
<PageHeader
|
||||
title="Reporting"
|
||||
meta={
|
||||
<>
|
||||
Last sync: 2 minutes ago
|
||||
<span style={{ marginLeft: 10, color: '#334155' }}>· 184 of 896 findings</span>
|
||||
<span style={{ marginLeft: 8, color: RC.amber }}>(3 filters active)</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<RptButton variant="neutral" icon={<RI.Atlas size={13} />}>Atlas</RptButton>
|
||||
<RptButton variant="primary" icon={<RI.Refresh size={13} />}>Sync</RptButton>
|
||||
</PageHeader>
|
||||
|
||||
{/* Header-level error */}
|
||||
<StatusBanner tone="error">Atlas: connection refused — retry in 30s</StatusBanner>
|
||||
|
||||
{/* Metrics tabs */}
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
<RI.PieChart size={14} style={{ color: '#334155', marginRight: 4 }} />
|
||||
<PillTab active={tab === 'ivanti'} onClick={() => setTab('ivanti')}>Ivanti Findings</PillTab>
|
||||
<PillTab active={tab === 'atlas'} onClick={() => setTab('atlas')}>Atlas Coverage</PillTab>
|
||||
</div>
|
||||
|
||||
{/* Donut grid */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
|
||||
gap: 14,
|
||||
}}>
|
||||
{donuts.map((d) => (
|
||||
<KbCard key={d.label} label={d.label} labelExtra={d.labelExtra}>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 170 }}>
|
||||
{d.donut}
|
||||
</div>
|
||||
</KbCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Trend section */}
|
||||
<KbCard label="Open vs Closed · last 30 days" labelExtra={
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, color: RC.amber, fontSize: 10 }}>
|
||||
<RI.AlertTri size={11} /> spike detected day 12
|
||||
</span>
|
||||
}>
|
||||
<TrendChartPlaceholder />
|
||||
<div style={{ display: 'flex', gap: 14, fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-muted)', justifyContent: 'center' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||
<span style={{ width: 9, height: 9, background: RC.sky, borderRadius: 2 }} /> Open
|
||||
</span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||
<span style={{ width: 9, height: 9, background: 'rgba(16,185,129,0.55)', borderRadius: 2 }} /> Closed
|
||||
</span>
|
||||
</div>
|
||||
</KbCard>
|
||||
|
||||
{/* Findings table panel */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%)',
|
||||
border: '1.5px solid rgba(14,165,233,0.12)',
|
||||
borderRadius: 8, padding: 20,
|
||||
}}>
|
||||
{/* Toolbar */}
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
marginBottom: 12, paddingBottom: 10,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
||||
}}>
|
||||
<ToolbarLabel count="184 of 896">Host Findings</ToolbarLabel>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<RptButton variant="subtle" icon={<RI.Download size={12} />}>Export</RptButton>
|
||||
<RptButton variant="subtle" icon={<RI.ListTodo size={12} />}>Queue</RptButton>
|
||||
<RptButton variant="subtle" icon={<RI.Settings size={12} />}>Columns</RptButton>
|
||||
<RptButton variant="subtle" icon={<RI.EyeOff size={12} />}>Rows</RptButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search + filter chip row */}
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{
|
||||
position: 'relative', flex: '1 1 280px', maxWidth: 360,
|
||||
}}>
|
||||
<RI.Search size={13} style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--fg-disabled)' }} />
|
||||
<input
|
||||
defaultValue="kafka"
|
||||
placeholder="Search host, CVE, owner…"
|
||||
style={{
|
||||
width: '100%', padding: '8px 10px 8px 30px',
|
||||
background: 'rgba(15,23,42,0.6)',
|
||||
border: '1px solid rgba(14,165,233,0.18)',
|
||||
borderRadius: 6,
|
||||
color: 'var(--fg-1)', fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FilterChip color={RC.amber}>Severity: Critical, High</FilterChip>
|
||||
<FilterChip color={RC.sky}>Action: Patch</FilterChip>
|
||||
<FilterChip color={RC.red}>SLA: Overdue</FilterChip>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div style={{ overflow: 'auto', borderRadius: 6, border: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-mono)', fontSize: 11 }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'rgba(14,165,233,0.06)' }}>
|
||||
{['ID', 'Host', 'OS', 'Severity', 'CVE', 'Age', 'SLA', 'State', 'Action', 'Owner'].map((h) => (
|
||||
<th key={h} style={{
|
||||
textAlign: 'left', padding: '8px 12px',
|
||||
color: RC.sky, textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
fontWeight: 700, fontSize: 10,
|
||||
borderBottom: '1px solid rgba(14,165,233,0.18)',
|
||||
}}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||
{h}
|
||||
<RI.ChevronUpDn size={10} style={{ opacity: 0.5 }} />
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{SAMPLE_ROWS.map((r, i) => (
|
||||
<tr key={r.id} style={{
|
||||
background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.015)',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.03)',
|
||||
}}>
|
||||
<td style={{ padding: '10px 12px', color: RC.sky, fontWeight: 600 }}>{r.id}</td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-1)' }}>{r.host}</td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.os}</td>
|
||||
<td style={{ padding: '10px 12px' }}><SeverityDot level={r.sev} /></td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-2)' }}>{r.cve}</td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.age}d</td>
|
||||
<td style={{ padding: '10px 12px' }}><SlaPill status={r.sla} /></td>
|
||||
<td style={{ padding: '10px 12px' }}><WorkflowBadge state={r.state} /></td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-2)' }}>{r.action}</td>
|
||||
<td style={{ padding: '10px 12px', color: 'var(--fg-muted)' }}>{r.owner}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination footer */}
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
paddingTop: 12, marginTop: 4,
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-muted)',
|
||||
}}>
|
||||
<span>Showing 1–{SAMPLE_ROWS.length} of 184</span>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<RptButton variant="neutral">‹ Prev</RptButton>
|
||||
<RptButton variant="neutral">Next ›</RptButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
window.RPT_PAGE = { ReportingPage };
|
||||
46
docs/design-system-redesign/ui_kits/reporting/index.html
Normal file
46
docs/design-system-redesign/ui_kits/reporting/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>STEAM Security · Reporting 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); }
|
||||
|
||||
/* Anchor scroll offset under the sticky tab strip */
|
||||
:target { scroll-margin-top: 120px; }
|
||||
|
||||
/* Hide scrollbars on the in-page sample regions */
|
||||
.sample-frame::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.sample-frame::-webkit-scrollbar-thumb { background: rgba(14,165,233,0.2); 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="ReportPrimitives.jsx"></script>
|
||||
<script type="text/babel" src="ReportingPage.jsx"></script>
|
||||
<script type="text/babel" src="KitDocs.jsx"></script>
|
||||
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
const { KitDocs } = window.RPT_DOCS;
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<main data-screen-label="Reporting Kit">
|
||||
<KitDocs />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user