feat(ui): connect findings page to finding-groups endpoint

- Replace getFindings with getFindingGroups in findings page
- Update skeleton columns to match new table layout
- Update exports and selection context for new components

Ref: PROWLER-881
This commit is contained in:
alejandrobailo
2026-03-18 14:07:06 +01:00
parent f0d1ec8edb
commit 9a85906db5
4 changed files with 44 additions and 79 deletions

View File

@@ -1,9 +1,12 @@
import { Suspense } from "react";
import {
adaptFindingGroupsResponse,
getFindingGroups,
getLatestFindingGroups,
} from "@/actions/finding-groups";
import {
getFindingById,
getFindings,
getLatestFindings,
getLatestMetadataInfo,
getMetadataInfo,
} from "@/actions/findings";
@@ -12,13 +15,12 @@ import { getScans } from "@/actions/scans";
import { FindingDetailsSheet } from "@/components/findings";
import { FindingsFilters } from "@/components/findings/findings-filters";
import {
FindingsTableWithSelection,
FindingsGroupTable,
SkeletonTableFindings,
} from "@/components/findings/table";
import { ContentLayout } from "@/components/ui";
import { FilterTransitionWrapper } from "@/contexts";
import {
createDict,
createScanDetailsMapping,
extractFiltersAndQuery,
extractSortAndKey,
@@ -60,13 +62,12 @@ export default async function Findings({
: Promise.resolve(null),
]);
// Process the finding data to match the expected structure
// Process the finding data to match the expected structure (for detail sheet)
const processedFinding = findingByIdData?.data
? (() => {
const finding = findingByIdData.data;
const included = findingByIdData.included || [];
// Build dictionaries from included data
type IncludedItem = {
type: string;
id: string;
@@ -179,65 +180,40 @@ const SSRDataTable = async ({
}) => {
const page = parseInt(searchParams.page?.toString() || "1", 10);
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10);
const defaultSort = "severity,status,-inserted_at";
const defaultSort = "-severity,-fail_count,-last_seen_at";
const { encodedSort } = extractSortAndKey({
...searchParams,
sort: searchParams.sort ?? defaultSort,
});
const { filters, query } = extractFiltersAndQuery(searchParams);
const { filters } = extractFiltersAndQuery(searchParams);
// Check if the searchParams contain any date or scan filter
const hasDateOrScan = hasDateOrScanFilter(searchParams);
const fetchFindings = hasDateOrScan ? getFindings : getLatestFindings;
const fetchFindingGroups = hasDateOrScan
? getFindingGroups
: getLatestFindingGroups;
const findingsData = await fetchFindings({
query,
const findingGroupsData = await fetchFindingGroups({
page,
sort: encodedSort,
filters,
pageSize,
});
// Create dictionaries for resources, scans, and providers
const resourceDict = createDict("resources", findingsData);
const scanDict = createDict("scans", findingsData);
const providerDict = createDict("providers", findingsData);
// Expand each finding with its corresponding resource, scan, and provider
const expandedFindings = findingsData?.data
? findingsData.data.map((finding: FindingProps) => {
const scan = scanDict[finding.relationships?.scan?.data?.id];
const resource =
resourceDict[finding.relationships?.resources?.data?.[0]?.id];
const provider = providerDict[scan?.relationships?.provider?.data?.id];
return {
...finding,
relationships: { scan, resource, provider },
};
})
: [];
// Create the new object while maintaining the original structure
const expandedResponse = {
...findingsData,
data: expandedFindings,
};
// Transform API response to FindingGroupRow[]
const groups = adaptFindingGroupsResponse(findingGroupsData);
return (
<>
{findingsData?.errors && (
{findingGroupsData?.errors && (
<div className="text-small mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-red-700">
<p className="mr-2 font-semibold">Error:</p>
<p>{findingsData.errors[0].detail}</p>
<p>{findingGroupsData.errors[0].detail}</p>
</div>
)}
<FindingsTableWithSelection
data={expandedResponse?.data || []}
metadata={findingsData?.meta}
/>
<FindingsGroupTable data={groups} metadata={findingGroupsData?.meta} />
</>
);
};

View File

@@ -2,11 +2,10 @@
import { createContext, useContext } from "react";
import { FindingProps } from "@/types";
interface FindingsSelectionContextValue {
selectedFindingIds: string[];
selectedFindings: FindingProps[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
selectedFindings: any[];
clearSelection: () => void;
isSelected: (id: string) => boolean;
}

View File

@@ -1,11 +1,15 @@
export * from "./column-finding-groups";
export * from "./column-finding-resources";
export * from "./column-findings";
export * from "./data-table-row-actions";
export * from "./data-table-row-details";
export * from "./finding-detail";
export * from "./findings-group-drill-down";
export * from "./findings-group-table";
export * from "./findings-selection-context";
export * from "./findings-table-with-selection";
// TODO: PROWLER-379 - Re-export when backend supports grouped findings
// export * from "./impacted-resources-cell";
export * from "./impacted-providers-cell";
export * from "./impacted-resources-cell";
export * from "./notification-indicator";
export * from "./provider-icon-cell";
export * from "./skeleton-table-findings";

View File

@@ -9,6 +9,10 @@ const SkeletonTableRow = () => {
<Skeleton className="size-1.5 rounded-full" />
</div>
</td>
{/* Expand chevron */}
<td className="px-1 py-4">
<Skeleton className="size-5 rounded" />
</td>
{/* Checkbox */}
<td className="px-2 py-4">
<div className="bg-bg-input-primary border-border-input-primary size-6 rounded-sm border shadow-[0_1px_2px_0_rgba(0,0,0,0.1)]" />
@@ -24,14 +28,6 @@ const SkeletonTableRow = () => {
<Skeleton className="h-4 w-4/5 rounded" />
</div>
</td>
{/* Resource name chip */}
<td className="px-3 py-4">
<div className="bg-bg-neutral-tertiary flex h-8 w-28 items-center gap-2 rounded-lg px-2">
<Skeleton className="size-4 rounded" />
<Skeleton className="h-3.5 w-16 rounded" />
<Skeleton className="ml-auto size-3.5 rounded" />
</div>
</td>
{/* Severity */}
<td className="px-3 py-4">
<div className="flex items-center gap-2">
@@ -39,21 +35,17 @@ const SkeletonTableRow = () => {
<Skeleton className="h-4 w-12 rounded" />
</div>
</td>
{/* Provider icon */}
{/* Provider icons */}
<td className="px-3 py-4">
<Skeleton className="size-9 rounded-lg" />
</td>
{/* Service */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-20 rounded" />
</td>
{/* Time */}
<td className="px-3 py-4">
<div className="space-y-1">
<Skeleton className="h-4 w-24 rounded" />
<Skeleton className="h-3 w-20 rounded" />
<div className="flex items-center gap-1">
<Skeleton className="size-7 rounded-md" />
<Skeleton className="size-7 rounded-md" />
</div>
</td>
{/* Resources badge */}
<td className="px-3 py-4">
<Skeleton className="h-6 w-16 rounded-md" />
</td>
{/* Actions */}
<td className="px-2 py-4">
<Skeleton className="size-6 rounded" />
@@ -81,6 +73,8 @@ export const SkeletonTableFindings = () => {
<tr className="border-border-neutral-secondary border-b">
{/* Notification - empty header */}
<th className="w-6 py-3" />
{/* Expand - empty header */}
<th className="w-8 py-3" />
{/* Checkbox */}
<th className="w-10 px-2 py-3">
<div className="bg-bg-input-primary border-border-input-primary size-6 rounded-sm border shadow-[0_1px_2px_0_rgba(0,0,0,0.1)]" />
@@ -93,25 +87,17 @@ export const SkeletonTableFindings = () => {
<th className="w-[300px] px-3 py-3 text-left">
<Skeleton className="h-4 w-14 rounded" />
</th>
{/* Resource name */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-24 rounded" />
</th>
{/* Severity */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-14 rounded" />
</th>
{/* Provider */}
{/* Providers */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-14 rounded" />
<Skeleton className="h-4 w-16 rounded" />
</th>
{/* Service */}
{/* Resources */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-12 rounded" />
</th>
{/* Time */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-10 rounded" />
<Skeleton className="h-4 w-16 rounded" />
</th>
{/* Actions - empty header */}
<th className="w-10 py-3" />