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 && (
-
-
-
-
-
+
)}
-
-
+
{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