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>
This commit is contained in:
Prowler Bot
2026-02-11 11:31:29 +01:00
committed by GitHub
parent 366f10cf0c
commit f15cf20b4f
7 changed files with 70 additions and 10 deletions

View File

@@ -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)
---

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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<ProviderType, ReactNode> = {
@@ -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 = () => {

View File

@@ -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(

View File

@@ -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 (

View File

@@ -59,6 +59,14 @@ export function ScansTableWithPolling({
const [scansData, setScansData] = useState<ScanProps[]>(initialData);
const [meta, setMeta] = useState<MetaDataProps | undefined>(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],