Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions ui/components/findings/table/column-findings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand All @@ -71,24 +70,18 @@ const FindingTitleCell = ({ row }: { row: { original: FindingProps } }) => {
};

return (
<TriggerSheet
triggerComponent={
<FindingDetail
findingDetails={row.original}
defaultOpen={isOpen}
onOpenChange={handleOpenChange}
trigger={
<div className="max-w-[500px]">
<p className="text-text-neutral-primary hover:text-button-tertiary cursor-pointer text-left text-sm break-words whitespace-normal hover:underline">
{checktitle}
</p>
</div>
}
title="Finding Details"
description="View the finding details"
defaultOpen={isOpen}
onOpenChange={handleOpenChange}
>
<DataTableRowDetails
entityId={row.original.id}
findingDetails={row.original}
/>
</TriggerSheet>
/>
);
};

Expand Down
241 changes: 149 additions & 92 deletions ui/components/findings/table/finding-detail.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
"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 type { ReactNode } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Line 87-90 uses window.location directly which can cause hydration mismatch in Next.js. Consider using usePathname() and useSearchParams() from Next.js:

const pathname = usePathname();
const searchParams = useSearchParams();

// Build URL using Next.js hooks instead of window.location
const params = new URLSearchParams(searchParams);
params.set('id', findingDetails.id);
const url = `${pathname}?${params.toString()}`;

This is pre-existing code, but worth addressing.

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 {
Expand Down Expand Up @@ -54,11 +64,21 @@ 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;
Expand All @@ -80,39 +100,65 @@ export const FindingDetail = ({
)
: null;

return (
<div className="flex flex-col gap-6 rounded-lg">
const content = (
<div className="flex min-w-0 flex-col gap-4 rounded-lg">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="dark:text-prowler-theme-pale/90 line-clamp-2 flex items-center gap-2 text-lg leading-tight font-medium text-gray-800">
{renderValue(attributes.check_metadata.checktitle)}
<Tooltip content="Copy finding link to clipboard" size="sm">
<div className="flex flex-col gap-2">
{/* Row 1: Status badges */}
<div className="flex flex-wrap items-center gap-4">
<StatusFindingBadge status={attributes.status as FindingStatus} />
<SeverityBadge severity={attributes.severity || "-"} />
{attributes.delta && (
<div className="flex items-center gap-1 capitalize">
<DeltaIndicator delta={attributes.delta} />
<span className="text-text-neutral-secondary text-xs">
{attributes.delta}
</span>
</div>
)}
<Muted
isMuted={attributes.muted}
mutedReason={attributes.muted_reason || ""}
/>
</div>

{/* Row 2: Title with copy link */}
<h2 className="dark:text-prowler-theme-pale/90 line-clamp-2 flex items-center gap-2 text-lg leading-tight font-medium text-gray-800">
{renderValue(attributes.check_metadata.checktitle)}
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => navigator.clipboard.writeText(url)}
className="text-bg-data-info inline-flex cursor-pointer transition-opacity hover:opacity-80"
aria-label="Copy finding link to clipboard"
>
<Link size={16} />
</button>
</Tooltip>
</h2>
</div>
<div className="flex items-center gap-x-4">
<Muted
isMuted={attributes.muted}
mutedReason={attributes.muted_reason || ""}
/>
</TooltipTrigger>
<TooltipContent>Copy finding link to clipboard</TooltipContent>
</Tooltip>
</h2>

{/* Row 3: First Seen */}
<div className="text-text-neutral-tertiary text-sm">
<DateWithTime inline dateTime={attributes.first_seen_at || "-"} />
</div>
</div>

{/* Check Metadata */}
<Card variant="base" padding="lg">
<CardHeader className="flex items-center justify-between">
<CardTitle>Finding Details</CardTitle>
<StatusFindingBadge status={attributes.status as FindingStatus} />
</CardHeader>
<CardContent className="flex flex-col gap-4">
{/* Tabs */}
<Tabs defaultValue="general" className="w-full">
<TabsList className="mb-4">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="resources">Resources</TabsTrigger>
<TabsTrigger value="scans">Scans</TabsTrigger>
</TabsList>

<p className="text-text-neutral-primary mb-4 text-sm">
Here is an overview of this finding:
</p>

{/* General Tab */}
<TabsContent value="general" className="flex flex-col gap-4">
<div className="flex flex-wrap gap-4">
<EntityInfo
cloudProvider={providerDetails.provider as ProviderType}
Expand All @@ -124,37 +170,19 @@ export const FindingDetail = ({
{attributes.check_metadata.servicename}
</InfoField>
<InfoField label="Region">{resource.region}</InfoField>
<InfoField label="First Seen">
<DateWithTime inline dateTime={attributes.first_seen_at || "-"} />
</div>

<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<InfoField label="Check ID" variant="simple">
<CodeSnippet value={attributes.check_id} />
</InfoField>
{attributes.delta && (
<InfoField
label="Delta"
tooltipContent="Indicates whether the finding is new (NEW), has changed status (CHANGED), or remains unchanged (NONE) compared to previous scans."
className="capitalize"
>
<div className="flex items-center gap-2">
<DeltaIndicator delta={attributes.delta} />
{attributes.delta}
</div>
</InfoField>
)}
<InfoField label="Severity" variant="simple">
<SeverityBadge severity={attributes.severity || "-"} />
<InfoField label="Finding ID" variant="simple">
<CodeSnippet value={findingDetails.id} />
</InfoField>
<InfoField label="Finding UID" variant="simple">
<CodeSnippet value={attributes.uid} />
</InfoField>
</div>
<InfoField label="Finding ID" variant="simple">
<CodeSnippet value={findingDetails.id} />
</InfoField>
<InfoField label="Check ID" variant="simple">
<CodeSnippet value={attributes.check_id} />
</InfoField>
<InfoField label="Finding UID" variant="simple">
<CodeSnippet value={attributes.uid} />
</InfoField>
<InfoField label="Resource ID" variant="simple">
<CodeSnippet value={resource.uid} />
</InfoField>

{attributes.status === "FAIL" && (
<InfoField label="Risk" variant="simple">
Expand Down Expand Up @@ -261,30 +289,32 @@ export const FindingDetail = ({
<InfoField label="Categories">
{attributes.check_metadata.categories?.join(", ") || "none"}
</InfoField>
</CardContent>
</Card>
</TabsContent>

{/* Resource Details */}
<Card variant="base" padding="lg">
<CardHeader>
<CardTitle>Resource Details</CardTitle>
{/* Resources Tab */}
<TabsContent value="resources" className="flex flex-col gap-4">
{providerDetails.provider === "iac" && gitUrl && (
<CardAction>
<Tooltip content="Go to Resource in the Repository" size="sm">
<a
href={gitUrl}
target="_blank"
rel="noopener noreferrer"
className="text-bg-data-info inline-flex cursor-pointer"
aria-label="Open resource in repository"
>
<ExternalLink size={16} className="inline" />
</a>
<div className="flex justify-end">
<Tooltip>
<TooltipTrigger asChild>
<a
href={gitUrl}
target="_blank"
rel="noopener noreferrer"
className="text-bg-data-info inline-flex items-center gap-1 text-sm"
aria-label="Open resource in repository"
>
<ExternalLink size={16} />
View in Repository
</a>
</TooltipTrigger>
<TooltipContent>
Go to Resource in the Repository
</TooltipContent>
</Tooltip>
</CardAction>
</div>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">

<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<InfoField label="Resource Name">
{renderValue(resource.name)}
Expand All @@ -310,6 +340,10 @@ export const FindingDetail = ({
</InfoField>
</div>

<InfoField label="Resource ID" variant="simple">
<CodeSnippet value={resource.uid} />
</InfoField>

{resource.tags && Object.entries(resource.tags).length > 0 && (
<div className="flex flex-col gap-4">
<h4 className="text-sm font-bold text-gray-500 dark:text-gray-400">
Expand All @@ -333,15 +367,10 @@ export const FindingDetail = ({
<DateWithTime inline dateTime={resource.updated_at || "-"} />
</InfoField>
</div>
</CardContent>
</Card>

{/* Add new Scan Details section */}
<Card variant="base" padding="lg">
<CardHeader>
<CardTitle>Scan Details</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
</TabsContent>

{/* Scans Tab */}
<TabsContent value="scans" className="flex flex-col gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<InfoField label="Scan Name">{scan.name || "N/A"}</InfoField>
<InfoField label="Resources Scanned">
Expand Down Expand Up @@ -377,8 +406,36 @@ export const FindingDetail = ({
</InfoField>
)}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);

// If no trigger, render content directly (inline mode)
if (!trigger) {
return content;
}

// With trigger, wrap in Drawer
return (
<Drawer
direction="right"
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="minimal-scrollbar 3xl:w-1/3 h-full w-full overflow-x-hidden overflow-y-auto p-6 md:w-1/2 md:max-w-none">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 3xl breakpoint is not standard Tailwind (only goes up to 2xl). Verify it's configured in tailwind.config.js, otherwise this won't work.

<DrawerHeader className="sr-only">
<DrawerTitle>Finding Details</DrawerTitle>
<DrawerDescription>View the finding details</DrawerDescription>
</DrawerHeader>
<DrawerClose className="ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none">
<X className="size-4" />
<span className="sr-only">Close</span>
</DrawerClose>
{content}
</DrawerContent>
</Drawer>
);
};
Loading