diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 3a667853a7..7ac2c70086 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **Prowler UI** are documented in this file. +## [1.18.1] (Prowler UNRELEASED) + +### 🐞 Fixed + +- Scans page polling now only refreshes scan table data instead of re-rendering the entire server component tree, eliminating redundant API calls to providers, findings, and compliance endpoints every 5 seconds + +--- + ## [1.18.0] (Prowler v5.18.0) ### 🔄 Changed diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index c3f1489714..2d0416f37c 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -1,21 +1,19 @@ import { Suspense } from "react"; import { getAllProviders } from "@/actions/providers"; -import { getScans, getScansByState } from "@/actions/scans"; +import { getScans } from "@/actions/scans"; import { auth } from "@/auth.config"; import { MutedFindingsConfigButton } from "@/components/providers"; import { - AutoRefresh, NoProvidersAdded, NoProvidersConnected, ScansFilters, } from "@/components/scans"; import { LaunchScanWorkflow } from "@/components/scans/launch-workflow"; import { SkeletonTableScans } from "@/components/scans/table"; -import { ColumnGetScans } from "@/components/scans/table/scans"; +import { ScansTableWithPolling } from "@/components/scans/table/scans"; import { ContentLayout } from "@/components/ui"; import { CustomBanner } from "@/components/ui/custom/custom-banner"; -import { DataTable } from "@/components/ui/table"; import { createProviderDetailsMapping, extractProviderUIDs, @@ -57,15 +55,6 @@ export default async function Scans({ const hasManageScansPermission = session?.user?.permissions?.manage_scans; - // Get scans data to check for executing scans - const scansData = await getScansByState(); - - const hasExecutingScan = scansData?.data?.some( - (scan: ScanProps) => - scan.attributes.state === "executing" || - scan.attributes.state === "available", - ); - // Extract provider UIDs and create provider details mapping for filtering const providerUIDs = providersData ? extractProviderUIDs(providersData) : []; const providerDetails = providersData @@ -82,7 +71,6 @@ export default async function Scans({ return ( - <> <> {!hasManageScansPermission ? ( @@ -177,11 +165,10 @@ const SSRDataTableScans = async ({ }) || []; return ( - ); }; diff --git a/ui/components/scans/table/scans/index.ts b/ui/components/scans/table/scans/index.ts index 2567c0098c..18c3035f7d 100644 --- a/ui/components/scans/table/scans/index.ts +++ b/ui/components/scans/table/scans/index.ts @@ -1,3 +1,4 @@ export * from "./column-get-scans"; export * from "./data-table-row-actions"; export * from "./data-table-row-details"; +export * from "./scans-table-with-polling"; diff --git a/ui/components/scans/table/scans/scans-table-with-polling.tsx b/ui/components/scans/table/scans/scans-table-with-polling.tsx new file mode 100644 index 0000000000..12c18b35d2 --- /dev/null +++ b/ui/components/scans/table/scans/scans-table-with-polling.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useCallback, useState } from "react"; + +import { getScans } from "@/actions/scans"; +import { AutoRefresh } from "@/components/scans"; +import { DataTable } from "@/components/ui/table"; +import { MetaDataProps, ScanProps, SearchParamsProps } from "@/types"; + +import { ColumnGetScans } from "./column-get-scans"; + +interface ScansTableWithPollingProps { + initialData: ScanProps[]; + initialMeta?: MetaDataProps; + searchParams: SearchParamsProps; +} + +const EXECUTING_STATES = ["executing", "available"] as const; + +function expandScansWithProviderInfo( + scans: ScanProps[], + included?: Array<{ type: string; id: string; attributes: any }>, +) { + return ( + scans?.map((scan) => { + const providerId = scan.relationships?.provider?.data?.id; + + if (!providerId) { + return { ...scan, providerInfo: undefined }; + } + + const providerData = included?.find( + (item) => item.type === "providers" && item.id === providerId, + ); + + if (!providerData) { + return { ...scan, providerInfo: undefined }; + } + + return { + ...scan, + providerInfo: { + provider: providerData.attributes.provider, + uid: providerData.attributes.uid, + alias: providerData.attributes.alias, + }, + }; + }) || [] + ); +} + +export function ScansTableWithPolling({ + initialData, + initialMeta, + searchParams, +}: ScansTableWithPollingProps) { + const [scansData, setScansData] = useState(initialData); + const [meta, setMeta] = useState(initialMeta); + + const hasExecutingScan = scansData.some((scan) => + EXECUTING_STATES.includes( + scan.attributes.state as (typeof EXECUTING_STATES)[number], + ), + ); + + const handleRefresh = useCallback(async () => { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10); + const sort = searchParams.sort?.toString(); + + const filters = Object.fromEntries( + Object.entries(searchParams).filter( + ([key]) => key.startsWith("filter[") && key !== "scanId", + ), + ); + + const query = (filters["filter[search]"] as string) || ""; + + const result = await getScans({ + query, + page, + sort, + filters, + pageSize, + include: "provider", + }); + + if (result?.data) { + const expanded = expandScansWithProviderInfo( + result.data, + result.included, + ); + setScansData(expanded); + + if (result && "meta" in result) { + setMeta(result.meta as MetaDataProps); + } + } + }, [searchParams]); + + return ( + <> + + + + ); +} diff --git a/ui/components/ui/custom/custom-table-link.tsx b/ui/components/ui/custom/custom-table-link.tsx index 80b6b000bf..9fba1e2c55 100644 --- a/ui/components/ui/custom/custom-table-link.tsx +++ b/ui/components/ui/custom/custom-table-link.tsx @@ -21,7 +21,9 @@ export const TableLink = ({ href, label, isDisabled }: TableLinkProps) => { return ( ); };