mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
fix(ui): fix filter navigation and pagination bugs in findings and scans pages (#10013)
This commit is contained in:
@@ -16,6 +16,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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user