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

View 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).

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

View 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 };

View 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>