diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 6418e73304..8fbc4d51d5 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to the **Prowler UI** are documented in this file. - 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) +- All filters on `/findings` silently reverting on first click in production [(#10034)](https://github.com/prowler-cloud/prowler/pull/10034) --- diff --git a/ui/hooks/use-url-filters.ts b/ui/hooks/use-url-filters.ts index 911aa61558..3c2ca7004b 100644 --- a/ui/hooks/use-url-filters.ts +++ b/ui/hooks/use-url-filters.ts @@ -1,89 +1,84 @@ "use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useTransition } from "react"; -import { useFilterTransitionOptional } from "@/contexts"; +const FINDINGS_PATH = "/findings"; +const DEFAULT_MUTED_FILTER = "false"; /** * Custom hook to handle URL filters and automatically reset * pagination when filters change. * - * Uses useTransition to prevent full page reloads when filters change, - * keeping the current UI visible while the new data loads. - * - * When used within a FilterTransitionProvider, the transition state is shared - * across all components using this hook, enabling coordinated loading indicators. + * Uses client-side router navigation to update query params without + * full page reloads when filters change. */ export const useUrlFilters = () => { const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); + const isPending = false; - // Use shared context if available, otherwise fall back to local transition - const sharedTransition = useFilterTransitionOptional(); - const [localIsPending, localStartTransition] = useTransition(); + const ensureFindingsDefaultMuted = (params: URLSearchParams) => { + // Findings defaults to excluding muted findings unless user sets it explicitly. + if (pathname === FINDINGS_PATH && !params.has("filter[muted]")) { + params.set("filter[muted]", DEFAULT_MUTED_FILTER); + } + }; - const isPending = sharedTransition?.isPending ?? localIsPending; - const startTransition = - sharedTransition?.startTransition ?? localStartTransition; + const navigate = (params: URLSearchParams) => { + ensureFindingsDefaultMuted(params); - const updateFilter = useCallback( - (key: string, value: string | string[] | null) => { - const params = new URLSearchParams(searchParams.toString()); + const queryString = params.toString(); + const targetUrl = queryString ? `${pathname}?${queryString}` : pathname; + router.push(targetUrl, { scroll: false }); + }; - const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; + const updateFilter = (key: string, value: string | string[] | null) => { + const params = new URLSearchParams(searchParams.toString()); - const currentValue = params.get(filterKey); - const nextValue = Array.isArray(value) - ? value.length > 0 - ? value.join(",") - : null - : value === null - ? null - : value; + const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; - // If effective value is unchanged, do nothing (avoids redundant fetches) - if (currentValue === nextValue) return; + const currentValue = params.get(filterKey); + const nextValue = Array.isArray(value) + ? value.length > 0 + ? value.join(",") + : null + : value === null + ? null + : value; - // Only reset page to 1 if page parameter already exists - if (params.has("page")) { - params.set("page", "1"); - } + // If effective value is unchanged, do nothing (avoids redundant fetches) + if (currentValue === nextValue) return; - if (nextValue === null) { - params.delete(filterKey); - } else { - params.set(filterKey, nextValue); - } - - startTransition(() => { - router.push(`${pathname}?${params.toString()}`, { scroll: false }); - }); - }, - [router, searchParams, pathname, startTransition], - ); - - const clearFilter = useCallback( - (key: string) => { - const params = new URLSearchParams(searchParams.toString()); - const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; + // Only reset page to 1 if page parameter already exists + if (params.has("page")) { + params.set("page", "1"); + } + if (nextValue === null) { params.delete(filterKey); + } else { + params.set(filterKey, nextValue); + } - // Only reset page to 1 if page parameter already exists - if (params.has("page")) { - params.set("page", "1"); - } + navigate(params); + }; - startTransition(() => { - router.push(`${pathname}?${params.toString()}`, { scroll: false }); - }); - }, - [router, searchParams, pathname, startTransition], - ); + const clearFilter = (key: string) => { + const params = new URLSearchParams(searchParams.toString()); + const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`; - const clearAllFilters = useCallback(() => { + params.delete(filterKey); + + // Only reset page to 1 if page parameter already exists + if (params.has("page")) { + params.set("page", "1"); + } + + navigate(params); + }; + + const clearAllFilters = () => { const params = new URLSearchParams(searchParams.toString()); Array.from(params.keys()).forEach((key) => { if (key.startsWith("filter[") || key === "sort") { @@ -93,17 +88,33 @@ export const useUrlFilters = () => { params.delete("page"); - startTransition(() => { - router.push(`${pathname}?${params.toString()}`, { scroll: false }); - }); - }, [router, searchParams, pathname, startTransition]); + navigate(params); + }; - const hasFilters = useCallback(() => { + const hasFilters = () => { const params = new URLSearchParams(searchParams.toString()); return Array.from(params.keys()).some( (key) => key.startsWith("filter[") || key === "sort", ); - }, [searchParams]); + }; + + /** + * Low-level navigation function for complex filter updates that need + * to modify multiple params atomically (e.g., setting provider_type + * while clearing provider_id). The modifier receives a mutable + * URLSearchParams; page is auto-reset if already present. + */ + const navigateWithParams = (modifier: (params: URLSearchParams) => void) => { + const params = new URLSearchParams(searchParams.toString()); + modifier(params); + + // Only reset page to 1 if page parameter already exists + if (params.has("page")) { + params.set("page", "1"); + } + + navigate(params); + }; return { updateFilter, @@ -111,5 +122,6 @@ export const useUrlFilters = () => { clearAllFilters, hasFilters, isPending, + navigateWithParams, }; }; diff --git a/ui/proxy.ts b/ui/proxy.ts index 5827f39844..98b0725dea 100644 --- a/ui/proxy.ts +++ b/ui/proxy.ts @@ -50,20 +50,6 @@ export default auth((req: NextRequest & { auth: any }) => { } } - // Redirect /findings to include default muted filter if not present - if ( - pathname === "/findings" && - !req.nextUrl.searchParams.has("filter[muted]") - ) { - const findingsUrl = new URL("/findings", req.url); - // Preserve existing search params - req.nextUrl.searchParams.forEach((value, key) => { - findingsUrl.searchParams.set(key, value); - }); - findingsUrl.searchParams.set("filter[muted]", "false"); - return NextResponse.redirect(findingsUrl); - } - return NextResponse.next(); });