From f15cf20b4f440c53f2d4c6e1ae14a11e54a8ac87 Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Wed, 11 Feb 2026 11:31:29 +0100 Subject: [PATCH] fix(ui): fix filter navigation and pagination bugs in findings and scans pages (#10015) Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> --- ui/CHANGELOG.md | 3 +++ ui/actions/findings/findings.ts | 10 +++++++-- ui/actions/scans/scans.ts | 5 ++++- .../_components/accounts-selector.tsx | 21 ++++++++++++++++--- .../_components/provider-type-selector.tsx | 21 ++++++++++++++++--- .../custom-checkbox-muted-findings.tsx | 12 ++++++++++- .../table/scans/scans-table-with-polling.tsx | 8 +++++++ 7 files changed, 70 insertions(+), 10 deletions(-) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 59e3c18c86..6418e73304 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to the **Prowler UI** are documented in this file. - ProviderTypeSelector crash when an unknown provider type is not present in PROVIDER_DATA [(#9991)](https://github.com/prowler-cloud/prowler/pull/9991) - Infinite memory loop when opening modals from table row action dropdowns caused by HeroUI (React Aria) and Radix Dialog overlay conflict [(#9996)](https://github.com/prowler-cloud/prowler/pull/9996) +- Filter navigations not coordinating with Suspense boundaries due to missing startTransition in ProviderTypeSelector, AccountsSelector, and muted findings checkbox [(#10013)](https://github.com/prowler-cloud/prowler/pull/10013) +- Scans page pagination not updating table data because ScansTableWithPolling kept stale state from initial mount [(#10013)](https://github.com/prowler-cloud/prowler/pull/10013) +- Duplicate `filter[search]` parameter in findings and scans API calls [(#10013)](https://github.com/prowler-cloud/prowler/pull/10013) --- diff --git a/ui/actions/findings/findings.ts b/ui/actions/findings/findings.ts index 1fade543aa..d226795483 100644 --- a/ui/actions/findings/findings.ts +++ b/ui/actions/findings/findings.ts @@ -25,7 +25,10 @@ export const getFindings = async ({ if (sort) url.searchParams.append("sort", sort); Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); + // Skip filter[search] since it's already added via the `query` param above + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } }); try { @@ -63,7 +66,10 @@ export const getLatestFindings = async ({ if (sort) url.searchParams.append("sort", sort); Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); + // Skip filter[search] since it's already added via the `query` param above + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } }); try { diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index ccbd2448c0..a5f615bfe4 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -37,7 +37,10 @@ export const getScans = async ({ // Add dynamic filters (e.g., "filter[state]", "fields[scans]") Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); + // Skip filter[search] since it's already added via the `query` param above + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } }); try { diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx index dce5f049dc..fed62b7441 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx @@ -1,7 +1,7 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; -import { ReactNode } from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { ReactNode, useTransition } from "react"; import { AlibabaCloudProviderBadge, @@ -22,6 +22,7 @@ import { MultiSelectTrigger, MultiSelectValue, } from "@/components/shadcn/select/multiselect"; +import { useFilterTransitionOptional } from "@/contexts"; import type { ProviderProps, ProviderType } from "@/types/providers"; const PROVIDER_ICON: Record = { @@ -43,8 +44,15 @@ interface AccountsSelectorProps { export function AccountsSelector({ providers }: AccountsSelectorProps) { const router = useRouter(); + const pathname = usePathname(); const searchParams = useSearchParams(); + // Use shared transition context if available, otherwise fall back to local + const sharedTransition = useFilterTransitionOptional(); + const [, localStartTransition] = useTransition(); + const startTransition = + sharedTransition?.startTransition ?? localStartTransition; + const filterKey = "filter[provider_id__in]"; const current = searchParams.get(filterKey) || ""; const selectedTypes = searchParams.get("filter[provider_type__in]") || ""; @@ -92,7 +100,14 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) { } } - router.push(`?${params.toString()}`, { scroll: false }); + // Reset to page 1 when changing filter + if (params.has("page")) { + params.set("page", "1"); + } + + startTransition(() => { + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }); }; const selectedLabel = () => { diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx index e10fe361a9..1d10335334 100644 --- a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx @@ -1,7 +1,7 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; -import { lazy, Suspense } from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { lazy, Suspense, useTransition } from "react"; import { MultiSelect, @@ -10,6 +10,7 @@ import { MultiSelectTrigger, MultiSelectValue, } from "@/components/shadcn/select/multiselect"; +import { useFilterTransitionOptional } from "@/contexts"; import { type ProviderProps, ProviderType } from "@/types/providers"; const AWSProviderBadge = lazy(() => @@ -123,8 +124,15 @@ export const ProviderTypeSelector = ({ providers, }: ProviderTypeSelectorProps) => { const router = useRouter(); + const pathname = usePathname(); const searchParams = useSearchParams(); + // Use shared transition context if available, otherwise fall back to local + const sharedTransition = useFilterTransitionOptional(); + const [, localStartTransition] = useTransition(); + const startTransition = + sharedTransition?.startTransition ?? localStartTransition; + const currentProviders = searchParams.get("filter[provider_type__in]") || ""; const selectedTypes = currentProviders ? currentProviders.split(",").filter(Boolean) @@ -144,7 +152,14 @@ export const ProviderTypeSelector = ({ // User should manually select accounts if they want to filter by specific accounts params.delete("filter[provider_id__in]"); - router.push(`?${params.toString()}`, { scroll: false }); + // Reset to page 1 when changing filter + if (params.has("page")) { + params.set("page", "1"); + } + + startTransition(() => { + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }); }; const availableTypes = Array.from( diff --git a/ui/components/filters/custom-checkbox-muted-findings.tsx b/ui/components/filters/custom-checkbox-muted-findings.tsx index 9af7722957..74f94d96e5 100644 --- a/ui/components/filters/custom-checkbox-muted-findings.tsx +++ b/ui/components/filters/custom-checkbox-muted-findings.tsx @@ -1,8 +1,10 @@ "use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useTransition } from "react"; import { Checkbox } from "@/components/shadcn"; +import { useFilterTransitionOptional } from "@/contexts"; // Constants for muted filter URL values const MUTED_FILTER_VALUES = { @@ -15,6 +17,12 @@ export const CustomCheckboxMutedFindings = () => { const pathname = usePathname(); const searchParams = useSearchParams(); + // Use shared transition context if available, otherwise fall back to local + const sharedTransition = useFilterTransitionOptional(); + const [, localStartTransition] = useTransition(); + const startTransition = + sharedTransition?.startTransition ?? localStartTransition; + // Get the current muted filter value from URL // Middleware ensures filter[muted] is always present when navigating to /findings const mutedFilterValue = searchParams.get("filter[muted]"); @@ -41,7 +49,9 @@ export const CustomCheckboxMutedFindings = () => { params.set("page", "1"); } - router.push(`${pathname}?${params.toString()}`, { scroll: false }); + startTransition(() => { + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }); }; return ( diff --git a/ui/components/scans/table/scans/scans-table-with-polling.tsx b/ui/components/scans/table/scans/scans-table-with-polling.tsx index 5c59a5f267..3854871941 100644 --- a/ui/components/scans/table/scans/scans-table-with-polling.tsx +++ b/ui/components/scans/table/scans/scans-table-with-polling.tsx @@ -59,6 +59,14 @@ export function ScansTableWithPolling({ const [scansData, setScansData] = useState(initialData); const [meta, setMeta] = useState(initialMeta); + // Sync state with server data when props change (e.g., pagination or filter changes). + // useState only uses its argument on first mount, so without this effect, + // navigating to page 2 would change the URL but keep showing page 1 data. + useEffect(() => { + setScansData(initialData); + setMeta(initialMeta); + }, [initialData, initialMeta]); + const hasExecutingScan = scansData.some((scan) => EXECUTING_STATES.includes( scan.attributes.state as (typeof EXECUTING_STATES)[number],