mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 10:46:51 +00:00
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:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user