mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat: add delta attribute in findings detail view with and finding id to the url (#7654)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,6 +42,9 @@ junit-reports/
|
||||
# VSCode files
|
||||
.vscode/
|
||||
|
||||
# Cursor files
|
||||
.cursorignore
|
||||
|
||||
# Terraform
|
||||
.terraform*
|
||||
*.tfstate
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
46
ui/components/findings/table/delta-indicator.tsx
Normal file
46
ui/components/findings/table/delta-indicator.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
13
ui/components/ui/code-snippet/code-snippet.tsx
Normal file
13
ui/components/ui/code-snippet/code-snippet.tsx
Normal 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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user