Files
prowler/ui/components/findings/table/column-findings.tsx
2025-12-18 16:06:45 +01:00

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,
},
];
}