mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
269 lines
8.8 KiB
TypeScript
269 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import { ColumnDef, RowSelectionState } from "@tanstack/react-table";
|
|
import { ChevronRight } from "lucide-react";
|
|
|
|
import { Checkbox } from "@/components/shadcn";
|
|
import {
|
|
DataTableColumnHeader,
|
|
SeverityBadge,
|
|
StatusFindingBadge,
|
|
} from "@/components/shadcn/table";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/shadcn/tooltip";
|
|
import { cn } from "@/lib";
|
|
import {
|
|
canDrillDownFindingGroup,
|
|
getFilteredFindingGroupDelta,
|
|
getFindingGroupImpactedCounts,
|
|
isFindingGroupMuted,
|
|
} from "@/lib/findings-groups";
|
|
import { FindingGroupRow } from "@/types";
|
|
import { getProviderDisplayName } from "@/types/providers";
|
|
|
|
import { DataTableRowActions } from "./data-table-row-actions";
|
|
import { canMuteFindingGroup } from "./finding-group-selection";
|
|
import { ImpactedResourcesCell } from "./impacted-resources-cell";
|
|
import { NotificationIndicator } from "./notification-indicator";
|
|
import { ProviderIconCell } from "./provider-icon-cell";
|
|
|
|
interface GetColumnFindingGroupsOptions {
|
|
rowSelection: RowSelectionState;
|
|
selectableRowCount: number;
|
|
onDrillDown: (checkId: string, group: FindingGroupRow) => void;
|
|
expandedCheckId?: string | null;
|
|
/** True when the expanded group has individually selected resources */
|
|
hasResourceSelection?: boolean;
|
|
/** Active URL filters — used to make the delta indicator status-aware */
|
|
filters?: Record<string, string | string[] | undefined>;
|
|
}
|
|
|
|
const VISIBLE_DISABLED_CHECKBOX_CLASS =
|
|
"disabled:opacity-100 disabled:bg-bg-input-primary/60 disabled:border-border-input-primary/70";
|
|
|
|
export function getColumnFindingGroups({
|
|
rowSelection,
|
|
selectableRowCount,
|
|
onDrillDown,
|
|
expandedCheckId,
|
|
hasResourceSelection = false,
|
|
filters = {},
|
|
}: GetColumnFindingGroupsOptions): ColumnDef<FindingGroupRow>[] {
|
|
const selectedCount = Object.values(rowSelection).filter(Boolean).length;
|
|
const isAllSelected =
|
|
selectedCount > 0 && selectedCount === selectableRowCount;
|
|
const isSomeSelected =
|
|
selectedCount > 0 && selectedCount < selectableRowCount;
|
|
|
|
return [
|
|
// Combined column: notification + expand toggle + checkbox
|
|
{
|
|
id: "select",
|
|
header: ({ table }) => {
|
|
const headerChecked = isAllSelected
|
|
? true
|
|
: isSomeSelected
|
|
? "indeterminate"
|
|
: false;
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-2" />
|
|
<div className="w-4" />
|
|
<Checkbox
|
|
size="sm"
|
|
className={VISIBLE_DISABLED_CHECKBOX_CLASS}
|
|
checked={headerChecked}
|
|
onCheckedChange={(checked) =>
|
|
table.toggleAllPageRowsSelected(checked === true)
|
|
}
|
|
onClick={(e) => e.stopPropagation()}
|
|
aria-label="Select all"
|
|
disabled={selectableRowCount === 0}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
cell: ({ row }) => {
|
|
const group = row.original;
|
|
const allMuted = isFindingGroupMuted(group);
|
|
const isExpanded = expandedCheckId === group.checkId;
|
|
const deltaKey = getFilteredFindingGroupDelta(group, filters);
|
|
const canExpand = canDrillDownFindingGroup(group);
|
|
const canSelect = canMuteFindingGroup({
|
|
resourcesFail: group.resourcesFail,
|
|
resourcesTotal: group.resourcesTotal,
|
|
muted: group.muted,
|
|
mutedCount: group.mutedCount,
|
|
});
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<NotificationIndicator
|
|
delta={deltaKey}
|
|
isMuted={allMuted}
|
|
showDeltaWhenMuted
|
|
/>
|
|
{canExpand ? (
|
|
<button
|
|
type="button"
|
|
aria-label={`Expand ${group.checkTitle}`}
|
|
className="hover:bg-bg-neutral-tertiary flex size-4 shrink-0 items-center justify-center rounded-md transition-colors"
|
|
onClick={() => onDrillDown(group.checkId, group)}
|
|
>
|
|
<ChevronRight
|
|
className={cn(
|
|
"text-text-neutral-secondary size-4 transition-transform duration-200",
|
|
isExpanded && "rotate-90",
|
|
)}
|
|
/>
|
|
</button>
|
|
) : (
|
|
<div className="size-4 shrink-0" />
|
|
)}
|
|
<Checkbox
|
|
size="sm"
|
|
className={VISIBLE_DISABLED_CHECKBOX_CLASS}
|
|
checked={
|
|
rowSelection[row.id] && isExpanded && hasResourceSelection
|
|
? "indeterminate"
|
|
: !!rowSelection[row.id]
|
|
}
|
|
disabled={!canSelect}
|
|
onCheckedChange={(checked) => {
|
|
// When indeterminate (resources selected), clicking deselects the group
|
|
if (
|
|
rowSelection[row.id] &&
|
|
isExpanded &&
|
|
hasResourceSelection
|
|
) {
|
|
row.toggleSelected(false);
|
|
} else {
|
|
row.toggleSelected(checked === true);
|
|
}
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
aria-label="Select row"
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
},
|
|
// Status column — not sortable on finding-groups endpoint
|
|
{
|
|
accessorKey: "status",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Status" />
|
|
),
|
|
cell: ({ row }) => {
|
|
return <StatusFindingBadge status={row.original.status} />;
|
|
},
|
|
enableSorting: false,
|
|
},
|
|
// Finding title column
|
|
{
|
|
accessorKey: "finding",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader
|
|
column={column}
|
|
title="Finding Groups"
|
|
param="check_id"
|
|
/>
|
|
),
|
|
cell: ({ row }) => {
|
|
const group = row.original;
|
|
const canExpand = canDrillDownFindingGroup(group);
|
|
const provider = group.providers[0];
|
|
const providerName = provider
|
|
? getProviderDisplayName(provider)
|
|
: undefined;
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
{provider && providerName ? (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="shrink-0">
|
|
<ProviderIconCell
|
|
provider={provider}
|
|
size={20}
|
|
className="size-5 rounded-none bg-transparent"
|
|
/>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">{providerName}</TooltipContent>
|
|
</Tooltip>
|
|
) : null}
|
|
{/* Single line always: min room to stay readable, ellipsis beyond
|
|
the max, full title in the tooltip. */}
|
|
<div className="min-w-0">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
{canExpand ? (
|
|
<button
|
|
type="button"
|
|
className="text-text-neutral-primary hover:text-button-tertiary block max-w-[500px] min-w-[120px] cursor-pointer truncate border-none bg-transparent p-0 text-left text-sm hover:underline"
|
|
onClick={() => onDrillDown(group.checkId, group)}
|
|
>
|
|
{group.checkTitle}
|
|
</button>
|
|
) : (
|
|
<span className="text-text-neutral-primary block max-w-[500px] min-w-[120px] truncate text-left text-sm">
|
|
{group.checkTitle}
|
|
</span>
|
|
)}
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" maxWidth="md">
|
|
{group.checkTitle}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
// Severity column
|
|
{
|
|
accessorKey: "severity",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader
|
|
column={column}
|
|
title="Severity"
|
|
param="severity"
|
|
/>
|
|
),
|
|
cell: ({ row }) => <SeverityBadge severity={row.original.severity} />,
|
|
},
|
|
// Impacted Resources column
|
|
{
|
|
id: "impactedResources",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Impacted Resources" />
|
|
),
|
|
cell: ({ row }) => {
|
|
const group = row.original;
|
|
const counts = getFindingGroupImpactedCounts(group);
|
|
return (
|
|
<ImpactedResourcesCell
|
|
impacted={counts.impacted}
|
|
total={counts.total}
|
|
/>
|
|
);
|
|
},
|
|
enableSorting: false,
|
|
},
|
|
// Actions column
|
|
{
|
|
id: "actions",
|
|
header: () => <div className="w-10" />,
|
|
cell: ({ row }) => <DataTableRowActions row={row} />,
|
|
enableSorting: false,
|
|
},
|
|
];
|
|
}
|