Files
sencho/frontend/src/components/ui/SignalRail.tsx
T
Anso 083442d5ea fix: differentiate security action links and add suppression editing (#1500)
Security page UX fixes:

- Stop the CVSS x EPSS scatter chart from painting a full-plot "white
  rectangle" cursor on click (cursor disabled), and prevent click-drag
  selection on charts.
- Differentiate the overview action links: "fixable" links (masthead primary
  action, review-queue blocker, and the Fixable signal tile) now open the
  Images tab pre-filtered to fixable findings; the Stale and Failed signal
  tiles link to the History tab where those scans are listed; Secrets and
  Misconfigs tiles link to their tabs. The Images tab accepts an initialFilter
  and exposes a Fixable option in the severity dropdown.
- Fix the "Secrets / misconfigs" option wrapping and misaligning in the
  severity dropdown (single-line option labels, wider trigger).
- Add Edit for CVE suppressions and misconfig acknowledgements (reason, scope
  pattern, expiry), reusing the existing dialog and the existing PUT endpoints;
  the CVE/rule identity stays fixed.
2026-06-28 06:17:47 -04:00

84 lines
3.1 KiB
TypeScript

import { cn } from '@/lib/utils';
import { Sparkline } from '@/components/ui/sparkline';
export type SignalTone = 'value' | 'warn' | 'error' | 'subtitle';
export interface SignalTile {
kicker: string;
value: string;
tone?: SignalTone;
/** Optional series for a 64x20 sparkline stroked in brand cyan. */
spark?: number[];
/** When set, the tile renders as a button that navigates on click. */
onClick?: () => void;
}
interface SignalRailProps {
tiles: SignalTile[];
className?: string;
}
const toneClass: Record<SignalTone, string> = {
value: 'text-stat-value',
warn: 'text-warning',
error: 'text-destructive',
subtitle: 'text-stat-subtitle',
};
export function SignalRail({ tiles, className }: SignalRailProps) {
if (tiles.length === 0) return null;
return (
<div
className={cn(
'shrink-0 grid border-b border-card-border bg-card',
className,
)}
style={{ gridTemplateColumns: `repeat(${tiles.length}, minmax(0, 1fr))` }}
>
{tiles.map((tile, idx) => {
const inner = (
<>
<div className="flex min-w-0 flex-col gap-1">
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-stat-subtitle">
{tile.kicker}
</span>
<span
className={cn(
'font-mono tabular-nums tracking-tight text-2xl leading-none',
toneClass[tile.tone ?? 'value'],
)}
>
{tile.value}
</span>
</div>
{tile.spark && tile.spark.length > 1 ? (
<div className="h-5 w-16 shrink-0 opacity-90">
<Sparkline
points={tile.spark}
stroke="var(--brand)"
fill="var(--brand)"
strokeWidth={1.25}
/>
</div>
) : null}
</>
);
const cellClass = cn(
'flex items-center justify-between gap-4 px-5 py-[var(--density-tile-y)] text-left',
idx > 0 && 'border-l border-card-border',
tile.onClick && 'cursor-pointer transition-colors hover:bg-accent/5',
);
return tile.onClick ? (
<button key={tile.kicker} type="button" onClick={tile.onClick} className={cellClass}>
{inner}
</button>
) : (
<div key={tile.kicker} className={cellClass}>
{inner}
</div>
);
})}
</div>
);
}