diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx index 8c98b319be..dca9087745 100644 --- a/ui/components/findings/table/column-findings.tsx +++ b/ui/components/findings/table/column-findings.tsx @@ -6,11 +6,10 @@ import { useSearchParams } from "next/navigation"; import { DataTableRowActions, - DataTableRowDetails, + FindingDetail, } from "@/components/findings/table"; import { Checkbox } from "@/components/shadcn"; import { DateWithTime, SnippetChip } from "@/components/ui/entities"; -import { TriggerSheet } from "@/components/ui/sheet"; import { DataTableColumnHeader, SeverityBadge, @@ -51,7 +50,7 @@ const getProviderData = ( ); }; -// Component for finding title that opens the detail sheet +// Component for finding title that opens the detail drawer const FindingTitleCell = ({ row }: { row: { original: FindingProps } }) => { const searchParams = useSearchParams(); const findingId = searchParams.get("id"); @@ -71,24 +70,18 @@ const FindingTitleCell = ({ row }: { row: { original: FindingProps } }) => { }; return ( -

{checktitle}

} - title="Finding Details" - description="View the finding details" - defaultOpen={isOpen} - onOpenChange={handleOpenChange} - > - -
+ /> ); }; diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index b7dc18011a..62a2b17c37 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -1,20 +1,31 @@ "use client"; import { Snippet } from "@heroui/snippet"; -import { Tooltip } from "@heroui/tooltip"; -import { ExternalLink, Link } from "lucide-react"; +import { ExternalLink, Link, X } from "lucide-react"; +import { usePathname, useSearchParams } from "next/navigation"; +import type { ReactNode } from "react"; import ReactMarkdown from "react-markdown"; import { - Card, - CardAction, - CardContent, - CardHeader, - CardTitle, + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, + InfoField, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, } from "@/components/shadcn"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { CustomLink } from "@/components/ui/custom/custom-link"; -import { EntityInfo, InfoField } from "@/components/ui/entities"; +import { EntityInfo } from "@/components/ui/entities"; import { DateWithTime } from "@/components/ui/entities/date-with-time"; import { SeverityBadge } from "@/components/ui/table/severity-badge"; import { @@ -54,20 +65,35 @@ const formatDuration = (seconds: number) => { return parts.join(" "); }; +interface FindingDetailProps { + findingDetails: FindingProps; + trigger?: ReactNode; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; +} + export const FindingDetail = ({ findingDetails, -}: { - findingDetails: FindingProps; -}) => { + trigger, + open, + defaultOpen = false, + onOpenChange, +}: FindingDetailProps) => { const finding = findingDetails; const attributes = finding.attributes; const resource = finding.relationships.resource.attributes; const scan = finding.relationships.scan.attributes; const providerDetails = finding.relationships.provider.attributes; - const currentUrl = new URL(window.location.href); - const params = new URLSearchParams(currentUrl.search); - params.set("id", findingDetails.id); - const url = `${window.location.origin}${currentUrl.pathname}?${params.toString()}`; + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const copyFindingUrl = () => { + const params = new URLSearchParams(searchParams.toString()); + params.set("id", findingDetails.id); + const url = `${window.location.origin}${pathname}?${params.toString()}`; + navigator.clipboard.writeText(url); + }; // Build Git URL for IaC findings const gitUrl = @@ -80,39 +106,65 @@ export const FindingDetail = ({ ) : null; - return ( -
+ const content = ( +
{/* Header */} -
-
-

- {renderValue(attributes.check_metadata.checktitle)} - +
+ {/* Row 1: Status badges */} +
+ + + {attributes.delta && ( +
+ + + {attributes.delta} + +
+ )} + +
+ + {/* Row 2: Title with copy link */} +

+ {renderValue(attributes.check_metadata.checktitle)} + + - -

-
-
- + + Copy finding link to clipboard + +

+ + {/* Row 3: First Seen */} +
+
- {/* Check Metadata */} - - - Finding Details - - - + {/* Tabs */} + + + General + Resources + Scans + + +

+ Here is an overview of this finding: +

+ + {/* General Tab */} +
{resource.region} - - +
+ +
+ + - {attributes.delta && ( - -
- - {attributes.delta} -
-
- )} - - + + + + +
- - - - - - - - - - - - {attributes.status === "FAIL" && ( @@ -183,7 +217,7 @@ export const FindingDetail = ({ {attributes.check_metadata.remediation && (
-

+

Remediation Details

@@ -261,30 +295,32 @@ export const FindingDetail = ({ {attributes.check_metadata.categories?.join(", ") || "none"} - - + - {/* Resource Details */} - - - Resource Details + {/* Resources Tab */} + {providerDetails.provider === "iac" && gitUrl && ( - - - - - +
+ + + + + View in Repository + + + + Go to Resource in the Repository + - +
)} -
- +
{renderValue(resource.name)} @@ -310,9 +346,13 @@ export const FindingDetail = ({
+ + + + {resource.tags && Object.entries(resource.tags).length > 0 && (
-

+

Tags

@@ -333,15 +373,10 @@ export const FindingDetail = ({
- - - - {/* Add new Scan Details section */} - - - Scan Details - - + + + {/* Scans Tab */} +
{scan.name || "N/A"} @@ -377,8 +412,36 @@ export const FindingDetail = ({ )}
-
-
+ +
); + + // If no trigger, render content directly (inline mode) + if (!trigger) { + return content; + } + + // With trigger, wrap in Drawer + return ( + + {trigger} + + + Finding Details + View the finding details + + + + Close + + {content} + + + ); }; diff --git a/ui/components/shadcn/drawer.tsx b/ui/components/shadcn/drawer.tsx new file mode 100644 index 0000000000..4df034384f --- /dev/null +++ b/ui/components/shadcn/drawer.tsx @@ -0,0 +1,135 @@ +"use client"; + +import type { ComponentProps } from "react"; +import { Drawer as DrawerPrimitive } from "vaul"; + +import { cn } from "@/lib/utils"; + +function Drawer({ + ...props +}: ComponentProps) { + return ; +} + +function DrawerTrigger({ + ...props +}: ComponentProps) { + return ; +} + +function DrawerPortal({ + ...props +}: ComponentProps) { + return ; +} + +function DrawerClose({ + ...props +}: ComponentProps) { + return ; +} + +function DrawerOverlay({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +function DrawerContent({ + className, + children, + ...props +}: ComponentProps) { + return ( + + + +
+ {children} + + + ); +} + +function DrawerHeader({ className, ...props }: ComponentProps<"div">) { + return ( +
+ ); +} + +function DrawerFooter({ className, ...props }: ComponentProps<"div">) { + return ( +
+ ); +} + +function DrawerTitle({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +function DrawerDescription({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} + +export { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + DrawerPortal, + DrawerTitle, + DrawerTrigger, +}; diff --git a/ui/components/shadcn/index.ts b/ui/components/shadcn/index.ts index 88ff8d80ac..fdafd282b9 100644 --- a/ui/components/shadcn/index.ts +++ b/ui/components/shadcn/index.ts @@ -7,7 +7,9 @@ export * from "./card/resource-stats-card/resource-stats-card-content"; export * from "./card/resource-stats-card/resource-stats-card-header"; export * from "./checkbox/checkbox"; export * from "./combobox"; +export * from "./drawer"; export * from "./dropdown/dropdown"; +export * from "./info-field"; export * from "./input/input"; export * from "./search-input/search-input"; export * from "./select/multiselect"; diff --git a/ui/components/shadcn/info-field/index.ts b/ui/components/shadcn/info-field/index.ts new file mode 100644 index 0000000000..83097f6852 --- /dev/null +++ b/ui/components/shadcn/info-field/index.ts @@ -0,0 +1 @@ +export * from "./info-field"; diff --git a/ui/components/shadcn/info-field/info-field.tsx b/ui/components/shadcn/info-field/info-field.tsx new file mode 100644 index 0000000000..2f45957e99 --- /dev/null +++ b/ui/components/shadcn/info-field/info-field.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { InfoIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"; + +const INFO_FIELD_VARIANTS = { + default: "default", + simple: "simple", + transparent: "transparent", +} as const; + +type InfoFieldVariant = + (typeof INFO_FIELD_VARIANTS)[keyof typeof INFO_FIELD_VARIANTS]; + +interface InfoFieldProps { + label: string; + children: ReactNode; + variant?: InfoFieldVariant; + className?: string; + tooltipContent?: string; + inline?: boolean; +} + +export function InfoField({ + label, + children, + variant = "default", + tooltipContent, + className, + inline = false, +}: InfoFieldProps) { + const labelContent = ( + + {label} + {inline && ":"} + {tooltipContent && ( + + + + + + + {tooltipContent} + + )} + + ); + + if (inline) { + return ( +
+ + {labelContent} + +
{children}
+
+ ); + } + + return ( +
+ + {labelContent} + + + {variant === "simple" ? ( +
+ {children} +
+ ) : variant === "transparent" ? ( +
{children}
+ ) : ( +
+ {children} +
+ )} +
+ ); +} diff --git a/ui/dependency-log.json b/ui/dependency-log.json index 0d5277a1f9..57e8812482 100644 --- a/ui/dependency-log.json +++ b/ui/dependency-log.json @@ -479,6 +479,14 @@ "strategy": "installed", "generatedAt": "2025-12-15T11:18:25.093Z" }, + { + "section": "dependencies", + "name": "react-day-picker", + "from": "9.13.0", + "to": "9.13.0", + "strategy": "installed", + "generatedAt": "2026-01-05T16:21:23.125Z" + }, { "section": "dependencies", "name": "react-dom", @@ -607,6 +615,14 @@ "strategy": "installed", "generatedAt": "2025-10-22T12:36:37.962Z" }, + { + "section": "dependencies", + "name": "vaul", + "from": "1.1.2", + "to": "1.1.2", + "strategy": "installed", + "generatedAt": "2026-01-05T16:21:23.125Z" + }, { "section": "dependencies", "name": "world-atlas", diff --git a/ui/package.json b/ui/package.json index 9e1d3f4311..7f5caf7e91 100644 --- a/ui/package.json +++ b/ui/package.json @@ -101,6 +101,7 @@ "tw-animate-css": "1.4.0", "use-stick-to-bottom": "1.1.1", "uuid": "11.1.0", + "vaul": "1.1.2", "world-atlas": "2.0.2", "zod": "4.1.11", "zustand": "5.0.8" diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 192f0ec5d2..5480ef97c2 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -249,6 +249,9 @@ importers: uuid: specifier: 11.1.0 version: 11.1.0 + vaul: + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) world-atlas: specifier: 2.0.2 version: 2.0.2 @@ -8265,6 +8268,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -18664,6 +18673,15 @@ snapshots: vary@1.1.2: {} + vaul@1.1.2(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.2.2(react@19.2.2))(react@19.2.2): + dependencies: + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + react: 19.2.2 + react-dom: 19.2.2(react@19.2.2) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3