feat(frontend): SystemSheet §9.11 — security and scheduled sheets (PR 2/3) (#961)

* feat(frontend): add SystemSheet primitive and migrate mesh sheets to §9.11 chrome

DESIGN.md §9.11 codifies one canonical right-side detail-sheet shell (cyan
rail, mono crumb, italic serif name, mono meta, ESC chip + close glyph,
fixed three-slot toolbar, cyan-underline tabs, ScrollArea body, footer
freshness band). Today the 16 sheet consumers each render their own
header chrome with stock shadcn SheetHeader/SheetTitle.

Introduce <SystemSheet> + <SheetSection> in
frontend/src/components/ui/system-sheet.tsx, composing the existing
<Sheet>/<SheetContent> primitive. Add a backward-compatible showClose
prop to SheetContent so SystemSheet can render its own ESC chip + close
glyph instead of the stock cyan square close.

Migrate the four mesh sheets as the first batch:
* MeshActivitySheet: crumb Fleet › Mesh › Activity, footer freshness from
  most-recent event timestamp.
* MeshOptInSheet: crumb Fleet › Mesh › {nodeName}, meta of opted-in
  count, drops the redundant bottom Close button (ESC chip dismisses).
* MeshDiagnosticsSheet: removes the icon-prefixed title (forbidden by
  §9.11), lifts Refresh/Restart buttons from the body into the toolbar
  band, three SheetSection blocks for sidecar status, streams, cache.
* MeshRouteDetailSheet: adds Overview/Events/Raw tabs, lifts Test probe
  into the toolbar primary slot, footer surfaces last probe latency.

* feat(frontend): migrate security and scheduled sheets to §9.11 chrome

PR 2 of the System Sheet (§9.11) rollout, stacked on the SystemSheet
primitive PR. Migrates six more sheet consumers and merges the Stack
alert + auto-heal sheets into a single tabbed sheet per audit §17.

Sheets migrated:
* NodeUpdatesSheet: crumb Fleet > Updates, Recheck primary, Update-all
  secondary when applicable. Stat tiles and node rows lose their
  card-in-sheet wrapping (forbidden by §9.11) for flat dividers.
* StackAlertSheet (now the merged stack monitor): tabs Alerts /
  Auto-heal, crumb Stack > {name} > Monitor. New initialTab prop lets
  callers open directly to either tab. Auto-heal tab is hidden entirely
  for Community-tier users (matches the existing tier-gating on the
  context menu trigger and keyboard shortcut).
* StackAutoHealSheet.tsx: deleted. Its body became the Auto-heal tab
  inside the merged sheet.
* VulnerabilityScanSheet: removed the icon-prefixed title (forbidden by
  §9.11). Re-scan, Compare, CSV, SARIF lifted from body cards into the
  toolbar band. Tabs Vulnerabilities | Secrets | Misconfigs (counts on
  the tab labels). SBOM dropdown stays in the body summary section
  pending a primitive enhancement for dropdown-attached toolbar actions.
* ScanComparisonSheet: crumb Security > Scans > Compare, name Diff,
  meta with the +added/-removed delta.
* SecurityHistoryView: the inner sheet only. Crumb Security > Scan
  history. Compare (paid + 2 selected) and Refresh in toolbar.
* ScheduledOperationsView run-history sheet (lines ~847-935 only):
  crumb Schedules > {taskName} > Runs, Download CSV in toolbar, footer
  surfaces next-run timestamp.

Hook refactor:
* useOverlayState replaces three separate state vars (alertSheetOpen,
  alertSheetStack, autoHealStackName) with one stackMonitor object
  carrying { stackName, tab }. New helpers openAlertSheet(stackName),
  openAutoHeal(stackName), closeStackMonitor(). Tests rewritten and
  pass (11/11).
* useSidebarContextMenu and ShellOverlays updated for the new API. The
  three other call sites (useStackMenuItems, useStackKeyboardShortcuts)
  already use openAlertSheet/openAutoHeal and need no change.
This commit is contained in:
Anso
2026-05-06 23:19:16 -04:00
committed by GitHub
parent 4d9617a5c6
commit 3ec0a45ff0
11 changed files with 1307 additions and 1507 deletions
+226 -281
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { SystemSheet, SheetSection } from '@/components/ui/system-sheet';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import {
@@ -17,7 +17,6 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
ShieldCheck,
ShieldOff,
ExternalLink,
ChevronLeft,
@@ -27,11 +26,8 @@ import {
Loader2,
Check,
GitCompare,
KeyRound,
FileWarning,
} from 'lucide-react';
import { Combobox } from '@/components/ui/combobox';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Dialog,
DialogContent,
@@ -48,6 +44,7 @@ import { toast } from '@/components/ui/toast-store';
import { cn } from '@/lib/utils';
import { cveUrl } from '@/lib/cveUrl';
import { SEVERITY_ROW_TINT } from '@/lib/severityStyles';
import { formatTimeAgo } from '@/lib/relativeTime';
import type {
VulnerabilityScan,
VulnerabilityDetail,
@@ -371,40 +368,71 @@ export function VulnerabilityScanSheet({
}
}, [scan]);
return (
<Sheet open={scanId != null} onOpenChange={(open) => !open && onClose()}>
<SheetContent className="sm:max-w-2xl flex flex-col p-0">
<SheetHeader className="px-6 pt-6 pb-4 pr-14 border-b border-border space-y-2">
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">
Vulnerability scan · {scan?.triggered_by ?? '-'}
</div>
<SheetTitle className="flex items-center gap-2 font-display italic text-2xl">
<ShieldCheck className="w-5 h-5 text-muted-foreground" strokeWidth={1.5} />
<span className="font-mono text-base truncate not-italic">
{scan?.image_ref ?? 'Loading...'}
</span>
</SheetTitle>
<SheetDescription className="sr-only">
{scan
? `Vulnerability scan results for ${scan.image_ref}: ${scan.total_vulnerabilities} total findings.`
: 'Vulnerability scan details.'}
</SheetDescription>
</SheetHeader>
const meta = scan
? `${scan.total_vulnerabilities} vulns · ${scan.fixable_count} fixable · ${scan.triggered_by}`
: (loading ? 'Loading…' : 'No scan');
const footerContext = scan
? `Scanned ${formatTimeAgo(new Date(scan.scanned_at).getTime())}`
: undefined;
const secondaryActions = scan ? [
...(canCompare ? [{
label: 'Compare',
icon: compareLoading ? Loader2 : GitCompare,
onClick: openCompareMenu,
disabled: compareLoading,
}] : []),
...(details.length > 0 ? [{
label: 'CSV',
icon: Download,
onClick: exportCsv,
}] : []),
...(canGenerateSbom && scan.status === 'completed' ? [{
label: 'SARIF',
icon: Download,
onClick: () => { void exportSarif(); },
}] : []),
] : undefined;
return (
<>
<SystemSheet
open={scanId != null}
onOpenChange={(open) => !open && onClose()}
crumb={['Security', 'Scans', scan?.image_ref ?? '…']}
name={scan?.image_ref ?? 'Loading…'}
meta={meta}
primaryAction={onRescan && scan ? {
label: 'Re-scan',
icon: RefreshCw,
onClick: () => onRescan(scan.image_ref),
disabled: scan.status === 'in_progress',
} : undefined}
secondaryActions={secondaryActions}
tabs={scan ? [
{ id: 'vulns', label: 'Vulnerabilities', count: totalDetails },
{ id: 'secrets', label: 'Secrets', count: scan.secret_count ?? secrets.length },
{ id: 'misconfigs', label: 'Misconfigs', count: scan.misconfig_count ?? misconfigs.length },
] : undefined}
activeTab={tab}
onTabChange={(id) => setTab(id as FindingTab)}
footerContext={footerContext}
size="lg"
>
{loading && !scan && (
<div className="flex items-center justify-center flex-1">
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" strokeWidth={1.5} />
</div>
)}
{scan && (
<div className="flex flex-col flex-1 min-h-0">
{/* Summary stats */}
<div className="px-6 py-4 border-b space-y-3">
<>
<SheetSection title="Summary">
{scan.policy_evaluation?.violated && (
<div
role="alert"
className="relative rounded border border-destructive/40 bg-destructive/10 px-3 py-2 pl-4 shadow-card-bevel"
className="relative rounded border border-destructive/40 bg-destructive/10 px-3 py-2 pl-4 mb-3 shadow-card-bevel"
>
<span
aria-hidden="true"
@@ -427,7 +455,7 @@ export function VulnerabilityScanSheet({
</div>
</div>
)}
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 mb-3">
{scan.critical_count > 0 && (
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.CRITICAL)}>
{scan.critical_count} CRITICAL
@@ -476,19 +504,8 @@ export function VulnerabilityScanSheet({
</div>
</div>
<div className="flex flex-wrap items-center gap-2 pt-2">
{onRescan && (
<Button
variant="outline"
size="sm"
onClick={() => onRescan(scan.image_ref)}
disabled={scan.status === 'in_progress'}
>
<RefreshCw className="w-3.5 h-3.5 mr-1.5" strokeWidth={1.5} />
Re-scan
</Button>
)}
{canGenerateSbom && (
{canGenerateSbom && (
<div className="flex flex-wrap items-center gap-2 mt-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={downloadingSbom}>
@@ -509,43 +526,11 @@ export function VulnerabilityScanSheet({
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
<Button variant="outline" size="sm" onClick={exportCsv} disabled={details.length === 0}>
<Download className="w-3.5 h-3.5 mr-1.5" strokeWidth={1.5} />
CSV
</Button>
{canGenerateSbom && (
<Button
variant="outline"
size="sm"
onClick={exportSarif}
disabled={scan.status !== 'completed'}
title="Export findings as SARIF 2.1.0 for GitHub code scanning"
>
<Download className="w-3.5 h-3.5 mr-1.5" strokeWidth={1.5} />
SARIF
</Button>
)}
{canCompare && (
<Button
variant="outline"
size="sm"
onClick={openCompareMenu}
disabled={compareLoading}
title="Compare this scan to a previous one for the same image"
>
{compareLoading ? (
<Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" strokeWidth={1.5} />
) : (
<GitCompare className="w-3.5 h-3.5 mr-1.5" strokeWidth={1.5} />
)}
Compare
</Button>
)}
</div>
</div>
)}
{compareOpen && canCompare && (
<div className="pt-2 space-y-2">
<div className="pt-3 space-y-2">
{compareOptions.length === 0 && !compareLoading ? (
<div className="rounded border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
No other completed scans for this image yet. Run a second scan to enable comparison.
@@ -569,44 +554,11 @@ export function VulnerabilityScanSheet({
)}
</div>
)}
</div>
</SheetSection>
<Tabs
value={tab}
onValueChange={(v) => setTab(v as FindingTab)}
className="flex flex-col flex-1 min-h-0"
>
<div className="px-6 pt-3">
<TabsList>
<TabsTrigger value="vulns" className="gap-1.5">
<ShieldCheck className="w-3.5 h-3.5" strokeWidth={1.5} />
Vulnerabilities
<span className="font-mono tabular-nums text-stat-subtitle">
({totalDetails})
</span>
</TabsTrigger>
<TabsTrigger value="secrets" className="gap-1.5">
<KeyRound className="w-3.5 h-3.5" strokeWidth={1.5} />
Secrets
<span className="font-mono tabular-nums text-stat-subtitle">
({scan.secret_count ?? secrets.length})
</span>
</TabsTrigger>
<TabsTrigger value="misconfigs" className="gap-1.5">
<FileWarning className="w-3.5 h-3.5" strokeWidth={1.5} />
Misconfigs
<span className="font-mono tabular-nums text-stat-subtitle">
({scan.misconfig_count ?? misconfigs.length})
</span>
</TabsTrigger>
</TabsList>
</div>
<TabsContent
value="vulns"
className="flex flex-col flex-1 min-h-0 mt-0 data-[state=inactive]:hidden"
>
<div className="px-6 pt-3 flex items-center gap-1 flex-wrap">
{tab === 'vulns' && (
<SheetSection title={`Vulnerabilities · ${totalDetails}`}>
<div className="flex items-center gap-1 flex-wrap mb-3">
{(['ALL', 'CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as SeverityFilter[]).map((s) => (
<Button
key={s}
@@ -649,35 +601,34 @@ export function VulnerabilityScanSheet({
</div>
{totalDetails > details.length && (
<div className="px-6 pt-2 text-xs text-stat-subtitle font-mono">
<div className="text-xs text-stat-subtitle font-mono mb-2">
Showing first {details.length} of {totalDetails}. Export CSV for the complete list.
</div>
)}
<ScrollArea className="flex-1 min-h-0">
<div className="px-6 py-3">
{pageItems.length === 0 ? (
<div className="text-center text-sm text-muted-foreground py-12">
{details.length === 0
? 'No vulnerabilities found.'
: 'No vulnerabilities match the selected filter.'}
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[180px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">CVE</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Package</TableHead>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[110px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Installed</TableHead>
<TableHead className="w-[110px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Fixed</TableHead>
{canManageSuppressions && <TableHead className="w-[40px]" />}
</TableRow>
</TableHeader>
<TableBody>
{pageItems.map((d) => {
const href = cveUrl(d.vulnerability_id, d.primary_url);
return (
<ScrollArea block className="max-h-[60vh]">
{pageItems.length === 0 ? (
<div className="text-center text-sm text-muted-foreground py-12">
{details.length === 0
? 'No vulnerabilities found.'
: 'No vulnerabilities match the selected filter.'}
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[180px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">CVE</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Package</TableHead>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[110px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Installed</TableHead>
<TableHead className="w-[110px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Fixed</TableHead>
{canManageSuppressions && <TableHead className="w-[40px]" />}
</TableRow>
</TableHeader>
<TableBody>
{pageItems.map((d) => {
const href = cveUrl(d.vulnerability_id, d.primary_url);
return (
<TableRow
key={d.id}
className={cn(SEVERITY_ROW_TINT[d.severity], d.suppressed && 'opacity-60')}
@@ -742,21 +693,19 @@ export function VulnerabilityScanSheet({
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
)}
</div>
);
})}
</TableBody>
</Table>
)}
</ScrollArea>
</TabsContent>
</SheetSection>
)}
<TabsContent
value="secrets"
className="flex flex-col flex-1 min-h-0 mt-0 data-[state=inactive]:hidden"
>
{tab === 'secrets' && (
<SheetSection title={`Secrets · ${scan.secret_count ?? secrets.length}`}>
{secretsNeedsPagination && (
<div className="px-6 pt-3 flex items-center gap-1">
<div className="flex items-center gap-1 mb-3">
<div className="flex items-center gap-1 ml-auto">
<Button
variant="ghost"
@@ -784,70 +733,67 @@ export function VulnerabilityScanSheet({
</div>
</div>
)}
<ScrollArea className="flex-1 min-h-0">
<div className="px-6 py-3">
{secrets.length === 0 ? (
<div className="text-center text-sm text-muted-foreground py-12">
No secrets detected.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[160px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Rule</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Title</TableHead>
<TableHead className="w-[260px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Target</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{secretsPageItems.map((s) => (
<TableRow key={s.id} className={SEVERITY_ROW_TINT[s.severity]}>
<TableCell>
<SeverityChip severity={s.severity} />
</TableCell>
<TableCell className="font-mono text-xs truncate max-w-[160px]" title={s.rule_id}>
{s.rule_id}
</TableCell>
<TableCell className="text-xs">
<div className="truncate max-w-[320px]" title={s.title ?? undefined}>
{s.title || <span className="text-muted-foreground">-</span>}
<ScrollArea block className="max-h-[60vh]">
{secrets.length === 0 ? (
<div className="text-center text-sm text-muted-foreground py-12">
No secrets detected.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[160px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Rule</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Title</TableHead>
<TableHead className="w-[260px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Target</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{secretsPageItems.map((s) => (
<TableRow key={s.id} className={SEVERITY_ROW_TINT[s.severity]}>
<TableCell>
<SeverityChip severity={s.severity} />
</TableCell>
<TableCell className="font-mono text-xs truncate max-w-[160px]" title={s.rule_id}>
{s.rule_id}
</TableCell>
<TableCell className="text-xs">
<div className="truncate max-w-[320px]" title={s.title ?? undefined}>
{s.title || <span className="text-muted-foreground">-</span>}
</div>
{s.match_excerpt && (
<div
className="font-mono text-[11px] text-muted-foreground truncate max-w-[320px]"
title={s.match_excerpt}
>
{s.match_excerpt}
</div>
{s.match_excerpt && (
<div
className="font-mono text-[11px] text-muted-foreground truncate max-w-[320px]"
title={s.match_excerpt}
>
{s.match_excerpt}
</div>
)}
</TableCell>
<TableCell className="font-mono text-xs truncate max-w-[260px]" title={s.target}>
{s.target}
{s.start_line != null && (
<span className="text-muted-foreground">
:{s.start_line}
{s.end_line != null && s.end_line !== s.start_line
? `-${s.end_line}`
: ''}
</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
)}
</TableCell>
<TableCell className="font-mono text-xs truncate max-w-[260px]" title={s.target}>
{s.target}
{s.start_line != null && (
<span className="text-muted-foreground">
:{s.start_line}
{s.end_line != null && s.end_line !== s.start_line
? `-${s.end_line}`
: ''}
</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</ScrollArea>
</TabsContent>
</SheetSection>
)}
<TabsContent
value="misconfigs"
className="flex flex-col flex-1 min-h-0 mt-0 data-[state=inactive]:hidden"
>
{tab === 'misconfigs' && (
<SheetSection title={`Misconfigs · ${scan.misconfig_count ?? misconfigs.length}`}>
{misconfigsNeedsPagination && (
<div className="px-6 pt-3 flex items-center gap-1">
<div className="flex items-center gap-1 mb-3">
<div className="flex items-center gap-1 ml-auto">
<Button
variant="ghost"
@@ -877,78 +823,77 @@ export function VulnerabilityScanSheet({
</div>
</div>
)}
<ScrollArea className="flex-1 min-h-0">
<div className="px-6 py-3">
{misconfigs.length === 0 ? (
<div className="text-center text-sm text-muted-foreground py-12">
No misconfigurations detected.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[140px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Check</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Title</TableHead>
<TableHead className="w-[200px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Target</TableHead>
<TableHead className="w-[220px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Fix</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{misconfigsPageItems.map((m) => (
<TableRow key={m.id} className={SEVERITY_ROW_TINT[m.severity]}>
<TableCell>
<SeverityChip severity={m.severity} />
</TableCell>
<TableCell
className="font-mono text-xs truncate max-w-[140px]"
title={m.check_id ?? m.rule_id}
>
{m.check_id || m.rule_id}
</TableCell>
<TableCell className="text-xs">
<div className="truncate max-w-[320px]" title={m.title ?? undefined}>
{m.primary_url ? (
<a
href={m.primary_url}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-1 hover:underline"
>
{m.title || m.rule_id}
<ExternalLink className="w-3 h-3" strokeWidth={1.5} />
</a>
) : (
m.title || m.rule_id
)}
</div>
{m.message && (
<div
className="text-[11px] text-muted-foreground truncate max-w-[320px]"
title={m.message}
<ScrollArea block className="max-h-[60vh]">
{misconfigs.length === 0 ? (
<div className="text-center text-sm text-muted-foreground py-12">
No misconfigurations detected.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[140px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Check</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Title</TableHead>
<TableHead className="w-[200px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Target</TableHead>
<TableHead className="w-[220px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Fix</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{misconfigsPageItems.map((m) => (
<TableRow key={m.id} className={SEVERITY_ROW_TINT[m.severity]}>
<TableCell>
<SeverityChip severity={m.severity} />
</TableCell>
<TableCell
className="font-mono text-xs truncate max-w-[140px]"
title={m.check_id ?? m.rule_id}
>
{m.check_id || m.rule_id}
</TableCell>
<TableCell className="text-xs">
<div className="truncate max-w-[320px]" title={m.title ?? undefined}>
{m.primary_url ? (
<a
href={m.primary_url}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-1 hover:underline"
>
{m.message}
</div>
{m.title || m.rule_id}
<ExternalLink className="w-3 h-3" strokeWidth={1.5} />
</a>
) : (
m.title || m.rule_id
)}
</TableCell>
<TableCell className="font-mono text-xs truncate max-w-[200px]" title={m.target}>
{m.target}
</TableCell>
<TableCell className="text-xs truncate max-w-[220px]" title={m.resolution ?? undefined}>
{m.resolution || <span className="text-muted-foreground">-</span>}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</div>
{m.message && (
<div
className="text-[11px] text-muted-foreground truncate max-w-[320px]"
title={m.message}
>
{m.message}
</div>
)}
</TableCell>
<TableCell className="font-mono text-xs truncate max-w-[200px]" title={m.target}>
{m.target}
</TableCell>
<TableCell className="text-xs truncate max-w-[220px]" title={m.resolution ?? undefined}>
{m.resolution || <span className="text-muted-foreground">-</span>}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</ScrollArea>
</TabsContent>
</Tabs>
</div>
</SheetSection>
)}
</>
)}
</SheetContent>
</SystemSheet>
<ScanComparisonSheet
baselineScanId={compareBaselineId}
currentScanId={compareBaselineId != null ? scanId : null}
@@ -1031,7 +976,7 @@ export function VulnerabilityScanSheet({
</DialogFooter>
</DialogContent>
</Dialog>
</Sheet>
</>
);
}