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:
root
2026-04-29 14:20:23 +00:00
parent 37119b9c8a
commit 27192dd69f
78 changed files with 9902 additions and 1368 deletions

View File

@@ -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,
};