diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index 7d3647f1cf..e6813bc234 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -46,6 +46,7 @@ export const getScans = async ({ revalidatePath("/scans"); return parsedData; } catch (error) { + // eslint-disable-next-line no-console console.error("Error fetching scans:", error); return undefined; } diff --git a/ui/app/(prowler)/resources/page.tsx b/ui/app/(prowler)/resources/page.tsx index a1cd98416d..6a2bbbadab 100644 --- a/ui/app/(prowler)/resources/page.tsx +++ b/ui/app/(prowler)/resources/page.tsx @@ -9,7 +9,7 @@ import { SkeletonTableResources } from "@/components/resources/skeleton/skeleton import { ColumnResources } from "@/components/resources/table/column-resources"; import { ContentLayout } from "@/components/ui"; import { DataTable, DataTableFilterCustom } from "@/components/ui/table"; -import { createDict } from "@/lib"; +import { createDict, extractFiltersAndQuery, extractSortAndKey, hasDateOrScanFilter, replaceFilterFieldKey } from "@/lib"; import { ResourceProps, SearchParamsProps } from "@/types"; export default async function Resources({ @@ -20,46 +20,31 @@ export default async function Resources({ const searchParamsKey = JSON.stringify(searchParams || {}); // Check if the searchParams contain any date or filter - const hasDateOrScanFilter = Object.keys(searchParams).some((key) => - key.includes("inserted_at"), - ); + const hasDateOrScan = hasDateOrScanFilter(searchParams); - // Default filters for getFindings - const defaultFilters: Record = hasDateOrScanFilter - ? {} // Do not apply default filters if there are date or filters - : { "page[size]": "100" }; // TODO: Remove page[size] 100 when metadata endpoint implemented + const { filters } = extractFiltersAndQuery(searchParams); + filters["page[size]"] = "100"; // TODO: Remove page[size] 100 when metadata endpoint implemented - const filters: Record = { - ...defaultFilters, - ...Object.fromEntries( - Object.entries(searchParams) - .filter(([key]) => key.startsWith("filter[")) - .map(([key, value]) => [ - key, - Array.isArray(value) ? value.join(",") : value?.toString() || "", - ]), - ), - }; + if (!hasDateOrScan) { + const scansData = await getScans({ + filters: { + "fields[scans]": "inserted_at", + }, + }); - const scansData = await getScans({ - filters: { - "filter[state]": "completed", - "fields[scans]": "inserted_at", - }, - }); - - if (scansData.data?.length !== 0) { - const latestScandate = scansData.data[0].attributes.inserted_at; - const formattedDate = format(parseISO(latestScandate), "yyyy-MM-dd"); - if (!hasDateOrScanFilter) { - filters["filter[inserted_at]"] = formattedDate; + if (scansData.data?.length !== 0) { + const latestScandate = scansData.data?.[0]?.attributes?.inserted_at; + const formattedDate = format(parseISO(latestScandate), "yyyy-MM-dd"); + filters["filter[updated_at]"] = formattedDate; } } + const outputFilters = replaceFilterFieldKey(filters, 'inserted_at', 'updated_at'); + // Resource call for filters const resourcesData = await getResourceFields( "name,type,region,service", - filters, + outputFilters, ); let resourceNameList: string[] = []; @@ -71,14 +56,14 @@ export default async function Resources({ resourceNameList = Array.from( new Set( resourcesData.data.map((item: ResourceProps) => item.attributes.name) || - [], + [], ), ); typeList = Array.from( new Set( resourcesData.data.map((item: ResourceProps) => item.attributes.type) || - [], + [], ), ); @@ -142,49 +127,40 @@ const SSRDataTable = async ({ searchParams: SearchParamsProps; }) => { const page = parseInt(searchParams.page?.toString() || "1", 10); - + const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); const defaultSort = "name"; - const sort = searchParams.sort?.toString() || defaultSort; - - // Check if the searchParams contain any date or filter - const hasDateOrScanFilter = Object.keys(searchParams).some((key) => - key.includes("inserted_at"), - ); - - const filters: Record = { - ...Object.fromEntries( - Object.entries(searchParams) - .filter(([key]) => key.startsWith("filter[")) - .map(([key, value]) => [ - key, - Array.isArray(value) ? value.join(",") : value?.toString() || "", - ]), - ), - }; - - // Fetch scans data latest date - const scansData = await getScans({ - filters: { - "filter[state]": "completed", - "fields[scans]": "inserted_at", - }, + const { encodedSort } = extractSortAndKey({ + ...searchParams, + sort: searchParams.sort ?? defaultSort, }); - if (scansData.data?.length !== 0) { - const latestScandate = scansData.data[0].attributes.inserted_at; - const formattedDate = format(parseISO(latestScandate), "yyyy-MM-dd"); - if (!hasDateOrScanFilter) { - filters["filter[inserted_at]"] = formattedDate; + // Check if the searchParams contain any date or filter + const hasDateOrScan = hasDateOrScanFilter(searchParams); + + const { filters, query } = extractFiltersAndQuery(searchParams); + + if (!hasDateOrScan) { + // Fetch scans data latest date + const scansData = await getScans({ + filters: { + "fields[scans]": "inserted_at", + }, + }); + + if (scansData.data?.length !== 0) { + const latestScandate = scansData?.data?.[0]?.attributes?.inserted_at; + const formattedDate = format(parseISO(latestScandate), "yyyy-MM-dd"); + filters["filter[updated_at]"] = formattedDate; } } - const query = filters["filter[search]"] || ""; + const outputFilters = replaceFilterFieldKey(filters, 'inserted_at', 'updated_at'); const resourcesData = await getResources({ query, page, - filters, - sort, - pageSize: 10, + filters: outputFilters, + sort: encodedSort, + pageSize, }); const findingsDict = createDict("findings", resourcesData); @@ -193,22 +169,22 @@ const SSRDataTable = async ({ // Expand each resources with its corresponding findings and provider const expandedResources = resourcesData?.data ? resourcesData.data.map((resource: ResourceProps) => { - const findings = { - meta: resource.relationships.findings.meta, - data: resource.relationships.findings.data?.map( - (finding) => findingsDict[finding.id], - ), - }; + const findings = { + meta: resource.relationships.findings.meta, + data: resource.relationships.findings.data?.map( + (finding) => findingsDict[finding.id], + ), + }; - const provider = { - data: providerDict[resource.relationships.provider.data.id], - }; + const provider = { + data: providerDict[resource.relationships.provider.data.id], + }; - return { - ...resource, - relationships: { findings, provider }, - }; - }) + return { + ...resource, + relationships: { findings, provider }, + }; + }) : []; const expandedResponse = { diff --git a/ui/components/resources/table/column-resources.tsx b/ui/components/resources/table/column-resources.tsx index 13ecc9d991..50c79f31ee 100644 --- a/ui/components/resources/table/column-resources.tsx +++ b/ui/components/resources/table/column-resources.tsx @@ -18,6 +18,12 @@ const getResourceData = ( return row.original.attributes?.[field] || `No ${field} found in resource`; }; +const getChipStyle = (count: number) => { + if (count > 10) return "bg-red-100 text-red-800"; + if (count > 1) return "bg-yellow-100 text-yellow-800"; + return "bg-green-100 text-green-800"; +}; + const getProviderData = ( row: { original: ResourceProps }, field: keyof ResourceProps["relationships"]["provider"]["data"]["attributes"], @@ -69,8 +75,14 @@ export const ColumnResources: ColumnDef[] = [ const resourceName = getResourceData(row, "name"); return ( <> -
- {typeof resourceName === "string" ? resourceName : "Invalid name"} +
+
+

+ {typeof resourceName === "string" + ? resourceName + : "Invalid name"} +

+
); @@ -78,14 +90,20 @@ export const ColumnResources: ColumnDef[] = [ }, { accessorKey: "failedFindings", - header: "Failed Findings", + header: () =>
Failed Findings
, cell: ({ row }) => { const count = row.original.relationships.findings.data.filter( (data) => data.attributes.status === "FAIL", ).length; return ( <> -
{count}
+

+ + {count} + +

); }, @@ -99,7 +117,7 @@ export const ColumnResources: ColumnDef[] = [ const region = getResourceData(row, "region"); return ( -
+
{typeof region === "string" ? region : "Invalid region"}
); @@ -114,7 +132,7 @@ export const ColumnResources: ColumnDef[] = [ const type = getResourceData(row, "type"); return ( -
+
{typeof type === "string" ? type : "Invalid type"}
); @@ -133,7 +151,7 @@ export const ColumnResources: ColumnDef[] = [ const service = getResourceData(row, "service"); return ( -
+
{typeof service === "string" ? service : "Invalid region"}
); diff --git a/ui/components/resources/table/resource-detail.tsx b/ui/components/resources/table/resource-detail.tsx index 6093d99748..c6bc20cd46 100644 --- a/ui/components/resources/table/resource-detail.tsx +++ b/ui/components/resources/table/resource-detail.tsx @@ -5,11 +5,11 @@ import { useRouter } from "next/navigation"; import { DateWithTime, - getProviderLogo, + EntityInfoShort, InfoField, } from "@/components/ui/entities"; import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table"; -import { ProviderType, ResourceApiResponse, ResourceProps } from "@/types"; +import { ResourceApiResponse, ResourceProps } from "@/types"; import { SkeletonFindingSummary } from "../skeleton/skeleton-finding-summary"; @@ -70,13 +70,35 @@ export const ResourceDetail = ({
{/* Resource Details section */}
- - - - {renderValue(resourceData?.attributes.uid)} - - - +
+ + + + {renderValue(resourceData?.attributes.uid)} + + + + + + +
+
{renderValue(resourceData.attributes.name)} @@ -109,28 +131,6 @@ export const ResourceDetail = ({
- {/* Provider Details section */} -
-
- {resourceData.relationships.provider.data.attributes.alias && ( - - {resourceData.relationships.provider.data.attributes.alias} - - )} - - {resourceData.relationships.provider.data.attributes.uid} - -
-
- - {getProviderLogo( - resourceData.relationships.provider.data.attributes - .provider as ProviderType, - )} - -
-
- {/* Finding associated with this resource section */}

diff --git a/ui/components/ui/entities/info-field.tsx b/ui/components/ui/entities/info-field.tsx index 0607efdf7b..0c2908b6e0 100644 --- a/ui/components/ui/entities/info-field.tsx +++ b/ui/components/ui/entities/info-field.tsx @@ -42,7 +42,7 @@ export const InfoField = ({ {variant === "simple" ? ( -
+
{children}
) : ( diff --git a/ui/lib/helper.ts b/ui/lib/helper.ts index db664e2a18..c29b037df5 100644 --- a/ui/lib/helper.ts +++ b/ui/lib/helper.ts @@ -269,3 +269,23 @@ export const permissionFormFields: PermissionInfo[] = [ description: "Provides access to billing settings and invoices", }, ]; + +export function replaceFilterFieldKey( + obj: Record, + oldField: string, + newField: string +): Record { + const fieldObj: Record = {}; + + for (const key in obj) { + const match = key.match(/^filter\[(.+)\]$/); + if (match && match[1] === oldField) { + const newKey = `filter[${newField}]`; + fieldObj[newKey] = obj[key]; + } else { + fieldObj[key] = obj[key]; + } + } + + return fieldObj; +} diff --git a/ui/types/components.ts b/ui/types/components.ts index 874c57e99c..e45913efc7 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -1,7 +1,7 @@ import { LucideIcon } from "lucide-react"; import { SVGProps } from "react"; -import { ProviderProps, ProviderType } from "./providers"; +import { ProviderType } from "./providers"; export type IconSvgProps = SVGProps & { size?: number;