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.
This commit is contained in:
Anso
2026-06-28 06:17:47 -04:00
committed by GitHub
parent ba57c67048
commit 083442d5ea
15 changed files with 316 additions and 115 deletions
+44 -32
View File
@@ -9,6 +9,8 @@ export interface SignalTile {
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 {
@@ -33,39 +35,49 @@ export function SignalRail({ tiles, className }: SignalRailProps) {
)}
style={{ gridTemplateColumns: `repeat(${tiles.length}, minmax(0, 1fr))` }}
>
{tiles.map((tile, idx) => (
<div
key={tile.kicker}
className={cn(
'flex items-center justify-between gap-4 px-5 py-[var(--density-tile-y)]',
idx > 0 && 'border-l border-card-border',
)}
>
<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}
/>
{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>
) : null}
</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>
);
}
+1 -1
View File
@@ -57,7 +57,7 @@ const ChartContainer = React.forwardRef<
data-chart={chartId}
ref={ref}
className={cn(
"flex justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
"flex select-none justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
+2 -2
View File
@@ -176,7 +176,7 @@ export function Combobox({
type="button"
onClick={() => handleSelect(option)}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground",
"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-accent hover:text-accent-foreground",
value === option.value && "bg-accent/50"
)}
>
@@ -187,7 +187,7 @@ export function Combobox({
)}
strokeWidth={1.5}
/>
{option.label}
<span className="min-w-0 truncate">{option.label}</span>
</button>
))
)}