mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-16 17:11:58 +00:00
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:
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user