fix(ui): move default muted filter from middleware to client-side hook (#10035)

Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
Prowler Bot
2026-02-11 22:05:56 +01:00
committed by GitHub
parent f15cf20b4f
commit b424eb302e
3 changed files with 78 additions and 79 deletions
+1
View File
@@ -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)
---
+77 -65
View File
@@ -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,
};
};
-14
View File
@@ -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();
});