feat: add delta attribute in findings detail view with and finding id to the url (#7654)

This commit is contained in:
Alejandro Bailo
2025-05-06 16:52:15 +02:00
committed by GitHub
parent 87951a8371
commit 3b17eb024c
8 changed files with 148 additions and 73 deletions

3
.gitignore vendored
View File

@@ -42,6 +42,9 @@ junit-reports/
# VSCode files
.vscode/
# Cursor files
.cursorignore
# Terraform
.terraform*
*.tfstate

View File

@@ -15,6 +15,7 @@ import {
import { FindingProps } from "@/types";
import { Muted } from "../muted";
import { DeltaIndicator } from "./delta-indicator";
const getFindingsData = (row: { original: FindingProps }) => {
return row.original;
@@ -44,21 +45,23 @@ const getProviderData = (
);
};
// const getScanData = (
// row: { original: FindingProps },
// field: keyof FindingProps["relationships"]["scan"]["attributes"],
// ) => {
// return (
// row.original.relationships?.scan?.attributes?.[field] ||
// `No ${field} found in scan`
// );
// };
const FindingDetailsCell = ({ row }: { row: any }) => {
const searchParams = useSearchParams();
const findingId = searchParams.get("id");
const isOpen = findingId === row.original.id;
const handleOpenChange = (open: boolean) => {
const params = new URLSearchParams(searchParams);
if (open) {
params.set("id", row.original.id);
} else {
params.delete("id");
}
window.history.pushState({}, "", `?${params.toString()}`);
};
return (
<div className="flex justify-center">
<TriggerSheet
@@ -66,6 +69,7 @@ const FindingDetailsCell = ({ row }: { row: any }) => {
title="Finding Details"
description="View the finding details"
defaultOpen={isOpen}
onOpenChange={handleOpenChange}
>
<DataTableRowDetails
entityId={row.original.id}
@@ -96,11 +100,18 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
const {
attributes: { muted },
} = getFindingsData(row);
const { delta } = row.original.attributes;
return (
<div className="relative flex max-w-[410px] flex-row items-center gap-2 3xl:max-w-[660px]">
<p className="mr-7 whitespace-normal break-words text-sm">
{checktitle}
</p>
<div className="flex flex-row items-center gap-4">
{(delta === "new" || delta === "changed") && (
<DeltaIndicator delta={delta} />
)}
<p className="mr-7 whitespace-normal break-words text-sm">
{checktitle}
</p>
</div>
<span className="absolute -right-2 top-1/2 -translate-y-1/2">
<Muted isMuted={muted} />
</span>

View File

@@ -1,40 +1,14 @@
"use client";
// import { usePathname, useRouter, useSearchParams } from "next/navigation";
// import { useEffect } from "react";
import { FindingProps } from "@/types/components";
import { FindingDetail } from "./finding-detail";
export const DataTableRowDetails = ({
// entityId,
findingDetails,
}: {
entityId: string;
findingDetails: FindingProps;
}) => {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
// useEffect(() => {
// if (entityId) {
// const params = new URLSearchParams(searchParams.toString());
// params.set("id", entityId);
// router.push(`${pathname}?${params.toString()}`, { scroll: false });
// }
// return () => {
// if (entityId) {
// const cleanupParams = new URLSearchParams(searchParams.toString());
// cleanupParams.delete("id");
// router.push(`${pathname}?${cleanupParams.toString()}`, {
// scroll: false,
// });
// }
// };
// }, [entityId, pathname, router, searchParams]);
return <FindingDetail findingDetails={findingDetails} />;
};

View File

@@ -0,0 +1,46 @@
import { Tooltip } from "@nextui-org/react";
import { CustomButton } from "@/components/ui/custom/custom-button";
import { cn } from "@/lib/utils";
interface DeltaIndicatorProps {
delta: string;
}
export const DeltaIndicator = ({ delta }: DeltaIndicatorProps) => {
return (
<Tooltip
className="pointer-events-auto"
content={
<div className="flex gap-1 text-xs">
<span>
{delta === "new"
? "New finding."
: "Status changed since the previous scan."}
</span>
<CustomButton
ariaLabel="Learn more about findings"
color="transparent"
size="sm"
className="h-auto min-w-0 p-0 text-primary"
asLink="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app/#step-8-analyze-the-findings"
target="_blank"
>
Learn more
</CustomButton>
</div>
}
>
<div
className={cn(
"h-2 w-2 min-w-2 cursor-pointer rounded-full",
delta === "new"
? "bg-system-severity-high"
: delta === "changed"
? "bg-system-severity-medium"
: "bg-gray-500",
)}
/>
</Tooltip>
);
};

View File

@@ -3,6 +3,7 @@
import { Snippet } from "@nextui-org/react";
import Link from "next/link";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { InfoField } from "@/components/ui/entities";
import { DateWithTime } from "@/components/ui/entities/date-with-time";
import {
@@ -13,6 +14,7 @@ import { SeverityBadge } from "@/components/ui/table/severity-badge";
import { FindingProps } from "@/types";
import { Muted } from "../muted";
import { DeltaIndicator } from "./delta-indicator";
const renderValue = (value: string | null | undefined) => {
return value && value.trim() !== "" ? value : "-";
@@ -87,13 +89,11 @@ export const FindingDetail = ({
{/* Check Metadata */}
<Section title="Finding Details">
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-4">
<div className="flex flex-wrap gap-4">
<InfoField label="Provider" variant="simple">
<div className="flex items-center gap-2">
{getProviderLogo(
attributes.check_metadata.provider as ProviderType,
)}
</div>
{getProviderLogo(
attributes.check_metadata.provider as ProviderType,
)}
</InfoField>
<InfoField label="Service">
{attributes.check_metadata.servicename}
@@ -102,21 +102,31 @@ export const FindingDetail = ({
<InfoField label="First Seen">
<DateWithTime inline dateTime={attributes.first_seen_at || "-"} />
</InfoField>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<InfoField label="Check ID" variant="simple">
<Snippet
className="max-w-full bg-gray-50 py-1 text-xs dark:bg-slate-800"
hideSymbol
{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"
>
{attributes.check_id}
</Snippet>
</InfoField>
<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>
</div>
<InfoField label="ID" variant="simple">
<CodeSnippet value={findingDetails.id} />
</InfoField>
<InfoField label="Check ID" variant="simple">
<CodeSnippet value={attributes.check_id} />
</InfoField>
<InfoField label="UID" variant="simple">
<CodeSnippet value={attributes.uid} />
</InfoField>
{attributes.status === "FAIL" && (
<InfoField label="Risk" variant="simple">

View File

@@ -0,0 +1,13 @@
import { Snippet } from "@nextui-org/react";
export const CodeSnippet = ({ value }: { value: string }) => (
<Snippet
className="w-full bg-gray-50 py-1 text-xs dark:bg-slate-800"
hideSymbol
classNames={{
pre: "w-full truncate",
}}
>
{value}
</Snippet>
);

View File

@@ -1,39 +1,55 @@
import { Tooltip } from "@nextui-org/react";
import clsx from "clsx";
import { InfoIcon } from "lucide-react";
interface InfoFieldProps {
label: string;
children: React.ReactNode;
variant?: "default" | "simple";
className?: string;
tooltipContent?: string;
}
<Tooltip
className="text-xs"
content="Download a ZIP file containing the JSON (OCSF), CSV, and HTML reports."
>
<div className="flex items-center gap-2">
<InfoIcon className="mb-1 text-primary" size={12} />
</div>
</Tooltip>;
export const InfoField = ({
label,
children,
variant = "default",
tooltipContent,
className,
}: InfoFieldProps) => {
if (variant === "simple") {
return (
<div className="flex flex-col gap-1">
<span className="text-xs font-bold text-gray-500 dark:text-prowler-theme-pale/70">
{label}
</span>
<div className="text-small text-gray-900 dark:text-prowler-theme-pale">
{children}
</div>
</div>
);
}
return (
<div className={clsx("flex flex-col gap-1", className)}>
<span className="text-xs font-bold text-gray-500 dark:text-prowler-theme-pale/70">
{label}
<span className="flex items-center gap-1">
{label}
{tooltipContent && (
<Tooltip className="text-xs" content={tooltipContent}>
<div className="flex cursor-pointer items-center gap-2">
<InfoIcon className="mb-1 text-primary" size={12} />
</div>
</Tooltip>
)}
</span>
</span>
<div className="rounded-lg bg-gray-50 px-3 py-2 text-small text-gray-900 dark:bg-slate-800 dark:text-prowler-theme-pale">
{children}
</div>
{variant === "simple" ? (
<div className="text-small text-gray-900 dark:text-prowler-theme-pale">
{children}
</div>
) : (
<div className="rounded-lg bg-gray-50 px-3 py-2 text-small text-gray-900 dark:bg-slate-800 dark:text-prowler-theme-pale">
{children}
</div>
)}
</div>
);
};

View File

@@ -13,6 +13,7 @@ interface TriggerSheetProps {
description: string;
children: React.ReactNode;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function TriggerSheet({
@@ -21,9 +22,10 @@ export function TriggerSheet({
description,
children,
defaultOpen = false,
onOpenChange,
}: TriggerSheetProps) {
return (
<Sheet defaultOpen={defaultOpen}>
<Sheet defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
<SheetTrigger className="flex items-center gap-2">
{triggerComponent}
</SheetTrigger>