From 6417e6bbba529fd8284f1e3598e09a52acc21acf Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 16 May 2025 08:18:12 +0200 Subject: [PATCH] feat: use getFindingsLatest when no scan or date filters are applied (#7756) --- ui/CHANGELOG.md | 1 + ui/actions/findings/findings.ts | 83 +++++++++++++++++++++++++++++ ui/app/(prowler)/findings/page.tsx | 85 +++++++++--------------------- ui/lib/helper-filters.ts | 46 ++++++++++++++++ ui/lib/index.ts | 1 + 5 files changed, 155 insertions(+), 61 deletions(-) create mode 100644 ui/lib/helper-filters.ts diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index b6fc46f57b..1e80a6d44b 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Added `Accordion` component. [(#7700)](https://github.com/prowler-cloud/prowler/pull/7700) - Improve `Provider UID` filter by adding more context and enhancing the UI/UX. [(#7741)](https://github.com/prowler-cloud/prowler/pull/7741) - Added an AWS CloudFormation Quick Link to the IAM Role credentials step [(#7735)](https://github.com/prowler-cloud/prowler/pull/7735) +– Use `getLatestFindings` on findings page when no scan or date filters are applied. [(#7756)](https://github.com/prowler-cloud/prowler/pull/7756) ### 🐞 Fixes diff --git a/ui/actions/findings/findings.ts b/ui/actions/findings/findings.ts index 3d7f1fec37..0f2359a421 100644 --- a/ui/actions/findings/findings.ts +++ b/ui/actions/findings/findings.ts @@ -44,6 +44,47 @@ export const getFindings = async ({ } }; +export const getLatestFindings = async ({ + page = 1, + pageSize = 10, + query = "", + sort = "", + filters = {}, +}) => { + const headers = await getAuthHeaders({ contentType: false }); + + if (isNaN(Number(page)) || page < 1) + redirect("findings?include=resources,scan.provider"); + + const url = new URL( + `${apiBaseUrl}/findings/latest?include=resources,scan.provider`, + ); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); + + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + Object.entries(filters).forEach(([key, value]) => { + url.searchParams.append(key, String(value)); + }); + + try { + const findings = await fetch(url.toString(), { + headers, + }); + const data = await findings.json(); + const parsedData = parseStringify(data); + revalidatePath("/findings"); + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching findings:", error); + return undefined; + } +}; + export const getMetadataInfo = async ({ query = "", sort = "", @@ -85,3 +126,45 @@ export const getMetadataInfo = async ({ return undefined; } }; + +export const getLatestMetadataInfo = async ({ + query = "", + sort = "", + filters = {}, +}) => { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/findings/metadata/latest`); + + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + Object.entries(filters).forEach(([key, value]) => { + // Define filters to exclude + const excludedFilters = ["region__in", "service__in", "resource_type__in"]; + if ( + key !== "filter[search]" && + !excludedFilters.some((filter) => key.includes(filter)) + ) { + url.searchParams.append(key, String(value)); + } + }); + + try { + const response = await fetch(url.toString(), { + headers, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch metadata info: ${response.statusText}`); + } + + const parsedData = parseStringify(await response.json()); + + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching metadata info:", error); + return undefined; + } +}; diff --git a/ui/app/(prowler)/findings/page.tsx b/ui/app/(prowler)/findings/page.tsx index a894210fa8..3615c77b3b 100644 --- a/ui/app/(prowler)/findings/page.tsx +++ b/ui/app/(prowler)/findings/page.tsx @@ -1,8 +1,12 @@ import { Spacer } from "@nextui-org/react"; -import { format, subDays } from "date-fns"; import React, { Suspense } from "react"; -import { getFindings, getMetadataInfo } from "@/actions/findings"; +import { + getFindings, + getLatestFindings, + getLatestMetadataInfo, + getMetadataInfo, +} from "@/actions/findings"; import { getProviders } from "@/actions/providers"; import { getScans } from "@/actions/scans"; import { filterFindings } from "@/components/filters/data-filters"; @@ -13,7 +17,12 @@ import { } from "@/components/findings/table"; import { ContentLayout } from "@/components/ui"; import { DataTable, DataTableFilterCustom } from "@/components/ui/table"; -import { createDict } from "@/lib"; +import { + createDict, + extractFiltersAndQuery, + extractSortAndKey, + hasDateOrScanFilter, +} from "@/lib"; import { ProviderAccountProps, ProviderProps } from "@/types"; import { FindingProps, ScanProps, SearchParamsProps } from "@/types/components"; @@ -22,41 +31,14 @@ export default async function Findings({ }: { searchParams: SearchParamsProps; }) { - const searchParamsKey = JSON.stringify(searchParams || {}); - const sort = searchParams.sort?.toString(); - - // Make sure the sort is correctly encoded - const encodedSort = sort?.replace(/^\+/, ""); - - const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd"); + const { searchParamsKey, encodedSort } = extractSortAndKey(searchParams); + const { filters, query } = extractFiltersAndQuery(searchParams); // Check if the searchParams contain any date or scan filter - const hasDateOrScanFilter = Object.keys(searchParams).some( - (key) => key.includes("inserted_at") || key.includes("scan__in"), - ); - - // Default filters for getMetadataInfo - const defaultFilters: Record = hasDateOrScanFilter - ? {} // Do not apply default filters if there are date or scan filters - : { "filter[inserted_at__gte]": twoDaysAgo }; - - // Extract all filter parameters and combine with default filters - 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() || "", - ]), - ), - }; - - const query = filters["filter[search]"] || ""; + const hasDateOrScan = hasDateOrScanFilter(searchParams); const [metadataInfoData, providersData, scansData] = await Promise.all([ - getMetadataInfo({ + (hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo)({ query, sort: encodedSort, filters, @@ -163,38 +145,19 @@ 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 sort = searchParams.sort?.toString() || defaultSort; - // Make sure the sort is correctly encoded - const encodedSort = sort.replace(/^\+/, ""); - - const twoDaysAgo = format(subDays(new Date(), 2), "yyyy-MM-dd"); + const { encodedSort } = extractSortAndKey({ + ...searchParams, + sort: searchParams.sort ?? defaultSort, + }); + const { filters, query } = extractFiltersAndQuery(searchParams); // Check if the searchParams contain any date or scan filter - const hasDateOrScanFilter = Object.keys(searchParams).some( - (key) => key.includes("inserted_at") || key.includes("scan__in"), - ); + const hasDateOrScan = hasDateOrScanFilter(searchParams); - // Default filters for getFindings - const defaultFilters: Record = hasDateOrScanFilter - ? {} // Do not apply default filters if there are date or scan filters - : { "filter[inserted_at__gte]": twoDaysAgo }; + const fetchFindings = hasDateOrScan ? getFindings : getLatestFindings; - 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() || "", - ]), - ), - }; - - const query = filters["filter[search]"] || ""; - - const findingsData = await getFindings({ + const findingsData = await fetchFindings({ query, page, sort: encodedSort, diff --git a/ui/lib/helper-filters.ts b/ui/lib/helper-filters.ts new file mode 100644 index 0000000000..fe17185746 --- /dev/null +++ b/ui/lib/helper-filters.ts @@ -0,0 +1,46 @@ +/** + * Extracts normalized filters and search query from the URL search params. + * Used Server Side Rendering (SSR). There is a hook (useUrlFilters) for client side. + */ +export const extractFiltersAndQuery = ( + searchParams: Record, +) => { + const filters: Record = { + ...Object.fromEntries( + Object.entries(searchParams) + .filter(([key]) => key.startsWith("filter[")) + .map(([key, value]) => [ + key, + Array.isArray(value) ? value.join(",") : value?.toString() || "", + ]), + ), + }; + + const query = filters["filter[search]"] || ""; + return { filters, query }; +}; + +/** + * Returns true if there are any scan or inserted_at filters in the search params. + * Used to determine whether to call the full findings endpoint. + */ +export const hasDateOrScanFilter = (searchParams: Record) => + Object.keys(searchParams).some( + (key) => key.includes("inserted_at") || key.includes("scan__in"), + ); + +/** + * Encodes sort strings by removing leading "+" symbols. + */ +export const encodeSort = (sort?: string) => sort?.replace(/^\+/, "") || ""; + +/** + * Extracts the sort string and the stable key to use in Suspense boundaries. + */ +export const extractSortAndKey = (searchParams: Record) => { + const searchParamsKey = JSON.stringify(searchParams || {}); + const rawSort = searchParams.sort?.toString(); + const encodedSort = encodeSort(rawSort); + + return { searchParamsKey, rawSort, encodedSort }; +}; diff --git a/ui/lib/index.ts b/ui/lib/index.ts index 6e1761c9d6..6e17065079 100644 --- a/ui/lib/index.ts +++ b/ui/lib/index.ts @@ -1,4 +1,5 @@ export * from "./external-urls"; export * from "./helper"; +export * from "./helper-filters"; export * from "./menu-list"; export * from "./utils";