mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
370 lines
10 KiB
TypeScript
370 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { ColumnDef, RowSelectionState } from "@tanstack/react-table";
|
|
import { Database } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useSearchParams } from "next/navigation";
|
|
|
|
import { DataTableRowDetails } from "@/components/findings/table";
|
|
import { DataTableRowActions } from "@/components/findings/table/data-table-row-actions";
|
|
import { InfoIcon, MutedIcon } from "@/components/icons";
|
|
import {
|
|
Checkbox,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/shadcn";
|
|
import {
|
|
DateWithTime,
|
|
EntityInfo,
|
|
SnippetChip,
|
|
} from "@/components/ui/entities";
|
|
import { TriggerSheet } from "@/components/ui/sheet";
|
|
import {
|
|
DataTableColumnHeader,
|
|
SeverityBadge,
|
|
StatusFindingBadge,
|
|
} from "@/components/ui/table";
|
|
import { FindingProps, ProviderType } from "@/types";
|
|
|
|
import { DeltaIndicator } from "./delta-indicator";
|
|
|
|
const getFindingsData = (row: { original: FindingProps }) => {
|
|
return row.original;
|
|
};
|
|
|
|
const getFindingsMetadata = (row: { original: FindingProps }) => {
|
|
return row.original.attributes.check_metadata;
|
|
};
|
|
|
|
const getResourceData = (
|
|
row: { original: FindingProps },
|
|
field: keyof FindingProps["relationships"]["resource"]["attributes"],
|
|
) => {
|
|
return (
|
|
row.original.relationships?.resource?.attributes?.[field] ||
|
|
`No ${field} found in resource`
|
|
);
|
|
};
|
|
|
|
const getProviderData = (
|
|
row: { original: FindingProps },
|
|
field: keyof FindingProps["relationships"]["provider"]["attributes"],
|
|
) => {
|
|
return (
|
|
row.original.relationships?.provider?.attributes?.[field] ||
|
|
`No ${field} found in provider`
|
|
);
|
|
};
|
|
|
|
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 max-w-10 justify-center">
|
|
<TriggerSheet
|
|
triggerComponent={
|
|
<InfoIcon className="text-button-primary" size={16} />
|
|
}
|
|
title="Finding Details"
|
|
description="View the finding details"
|
|
defaultOpen={isOpen}
|
|
onOpenChange={handleOpenChange}
|
|
>
|
|
<DataTableRowDetails
|
|
entityId={row.original.id}
|
|
findingDetails={row.original}
|
|
/>
|
|
</TriggerSheet>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Function to generate columns with access to selection state
|
|
export function getColumnFindings(
|
|
rowSelection: RowSelectionState,
|
|
selectableRowCount: number,
|
|
): ColumnDef<FindingProps>[] {
|
|
// Calculate selection state from rowSelection for header checkbox
|
|
const selectedCount = Object.values(rowSelection).filter(Boolean).length;
|
|
const isAllSelected =
|
|
selectedCount > 0 && selectedCount === selectableRowCount;
|
|
const isSomeSelected =
|
|
selectedCount > 0 && selectedCount < selectableRowCount;
|
|
return [
|
|
{
|
|
id: "select",
|
|
header: ({ table }) => {
|
|
// Use state calculated from rowSelection to force re-render
|
|
const headerChecked = isAllSelected
|
|
? true
|
|
: isSomeSelected
|
|
? "indeterminate"
|
|
: false;
|
|
|
|
return (
|
|
<div className="flex w-6 items-center justify-center">
|
|
<Checkbox
|
|
checked={headerChecked}
|
|
onCheckedChange={(checked) =>
|
|
table.toggleAllPageRowsSelected(checked === true)
|
|
}
|
|
aria-label="Select all"
|
|
// Disable when no rows are selectable (all muted)
|
|
disabled={selectableRowCount === 0}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
cell: ({ row }) => {
|
|
const finding = row.original;
|
|
const isMuted = finding.attributes.muted;
|
|
const mutedReason = finding.attributes.muted_reason;
|
|
|
|
// Show muted icon with tooltip for muted findings
|
|
if (isMuted) {
|
|
const ruleName = mutedReason || "Unknown rule";
|
|
|
|
return (
|
|
<div className="flex w-6 items-center justify-center">
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="border-system-severity-critical/40 cursor-pointer rounded-full border p-0.5">
|
|
<MutedIcon className="text-system-severity-critical size-3.5" />
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<Link
|
|
href="/mutelist"
|
|
className="text-button-tertiary hover:text-button-tertiary-hover flex items-center gap-1 text-xs underline-offset-4"
|
|
>
|
|
<span className="text-text-neutral-primary">
|
|
Mute rule:
|
|
</span>
|
|
<span className="max-w-[150px] truncate">{ruleName}</span>
|
|
</Link>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Use rowSelection directly instead of row.getIsSelected()
|
|
// This ensures re-render when selection state changes
|
|
const isSelected = !!rowSelection[row.id];
|
|
|
|
return (
|
|
<div className="flex w-6 items-center justify-center">
|
|
<Checkbox
|
|
checked={isSelected}
|
|
onCheckedChange={(checked) =>
|
|
row.toggleSelected(checked === true)
|
|
}
|
|
aria-label="Select row"
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
},
|
|
{
|
|
id: "moreInfo",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Details" />
|
|
),
|
|
cell: ({ row }) => <FindingDetailsCell row={row} />,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: "check",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader
|
|
column={column}
|
|
title={"Finding"}
|
|
param="check_id"
|
|
/>
|
|
),
|
|
cell: ({ row }) => {
|
|
const { checktitle } = getFindingsMetadata(row);
|
|
const { delta } = row.original.attributes;
|
|
|
|
return (
|
|
<div className="3xl:max-w-[660px] flex max-w-[410px] flex-row items-center gap-2">
|
|
<div className="flex flex-row items-center gap-4">
|
|
{delta === "new" || delta === "changed" ? (
|
|
<DeltaIndicator delta={delta} />
|
|
) : null}
|
|
<p className="text-sm break-words whitespace-normal">
|
|
{checktitle}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "resourceName",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Resource name" />
|
|
),
|
|
cell: ({ row }) => {
|
|
const resourceName = getResourceData(row, "name");
|
|
|
|
return (
|
|
<SnippetChip
|
|
value={resourceName as string}
|
|
formatter={(value: string) => `...${value.slice(-10)}`}
|
|
icon={<Database size={16} />}
|
|
/>
|
|
);
|
|
},
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: "severity",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader
|
|
column={column}
|
|
title={"Severity"}
|
|
param="severity"
|
|
/>
|
|
),
|
|
cell: ({ row }) => {
|
|
const {
|
|
attributes: { severity },
|
|
} = getFindingsData(row);
|
|
return <SeverityBadge severity={severity} />;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "status",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader
|
|
column={column}
|
|
title={"Status"}
|
|
param="status"
|
|
/>
|
|
),
|
|
cell: ({ row }) => {
|
|
const {
|
|
attributes: { status },
|
|
} = getFindingsData(row);
|
|
|
|
return <StatusFindingBadge status={status} />;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "updated_at",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader
|
|
column={column}
|
|
title={"Last seen"}
|
|
param="updated_at"
|
|
/>
|
|
),
|
|
cell: ({ row }) => {
|
|
const {
|
|
attributes: { updated_at },
|
|
} = getFindingsData(row);
|
|
return (
|
|
<div className="w-[100px]">
|
|
<DateWithTime dateTime={updated_at} />
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
// {
|
|
// accessorKey: "scanName",
|
|
// header: "Scan Name",
|
|
// cell: ({ row }) => {
|
|
// const name = getScanData(row, "name");
|
|
|
|
// return (
|
|
// <p className="text-small">
|
|
// {typeof name === "string" || typeof name === "number"
|
|
// ? name
|
|
// : "Invalid data"}
|
|
// </p>
|
|
// );
|
|
// },
|
|
// },
|
|
{
|
|
accessorKey: "region",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Region" />
|
|
),
|
|
cell: ({ row }) => {
|
|
const region = getResourceData(row, "region");
|
|
|
|
return (
|
|
<div className="w-[80px] text-xs">
|
|
{typeof region === "string" ? region : "Invalid region"}
|
|
</div>
|
|
);
|
|
},
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: "service",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Service" />
|
|
),
|
|
cell: ({ row }) => {
|
|
const { servicename } = getFindingsMetadata(row);
|
|
return <p className="max-w-96 truncate text-xs">{servicename}</p>;
|
|
},
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: "cloudProvider",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Cloud Provider" />
|
|
),
|
|
cell: ({ row }) => {
|
|
const provider = getProviderData(row, "provider");
|
|
const alias = getProviderData(row, "alias");
|
|
const uid = getProviderData(row, "uid");
|
|
|
|
return (
|
|
<>
|
|
<EntityInfo
|
|
cloudProvider={provider as ProviderType}
|
|
entityAlias={alias as string}
|
|
entityId={uid as string}
|
|
/>
|
|
</>
|
|
);
|
|
},
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
id: "actions",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Actions" />
|
|
),
|
|
cell: ({ row }) => {
|
|
return <DataTableRowActions row={row} />;
|
|
},
|
|
enableSorting: false,
|
|
},
|
|
];
|
|
}
|