mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-01 05:37:14 +00:00
Compare commits
1 Commits
v5.18
...
backport/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7403912b6 |
@@ -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)
|
||||
- Filter navigations silently reverting in production due to shared `useTransition()` context wrapping `router.push()` — each filter now uses a local transition while signaling the shared context for the DataTable loading indicator [(#10017)](https://github.com/prowler-cloud/prowler/pull/10017)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -47,11 +47,9 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) {
|
||||
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;
|
||||
// Signal shared pending state for DataTable loading indicator
|
||||
const filterTransition = useFilterTransitionOptional();
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
const filterKey = "filter[provider_id__in]";
|
||||
const current = searchParams.get(filterKey) || "";
|
||||
@@ -105,6 +103,7 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) {
|
||||
params.set("page", "1");
|
||||
}
|
||||
|
||||
filterTransition?.signalFilterChange();
|
||||
startTransition(() => {
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
});
|
||||
|
||||
@@ -127,11 +127,9 @@ export const ProviderTypeSelector = ({
|
||||
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;
|
||||
// Signal shared pending state for DataTable loading indicator
|
||||
const filterTransition = useFilterTransitionOptional();
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
const currentProviders = searchParams.get("filter[provider_type__in]") || "";
|
||||
const selectedTypes = currentProviders
|
||||
@@ -157,6 +155,7 @@ export const ProviderTypeSelector = ({
|
||||
params.set("page", "1");
|
||||
}
|
||||
|
||||
filterTransition?.signalFilterChange();
|
||||
startTransition(() => {
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
});
|
||||
|
||||
@@ -17,11 +17,9 @@ 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;
|
||||
// Signal shared pending state for DataTable loading indicator
|
||||
const filterTransition = useFilterTransitionOptional();
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
// Get the current muted filter value from URL
|
||||
// Middleware ensures filter[muted] is always present when navigating to /findings
|
||||
@@ -49,6 +47,7 @@ export const CustomCheckboxMutedFindings = () => {
|
||||
params.set("page", "1");
|
||||
}
|
||||
|
||||
filterTransition?.signalFilterChange();
|
||||
startTransition(() => {
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
TransitionStartFunction,
|
||||
useContext,
|
||||
useTransition,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
interface FilterTransitionContextType {
|
||||
isPending: boolean;
|
||||
startTransition: TransitionStartFunction;
|
||||
signalFilterChange: () => void;
|
||||
}
|
||||
|
||||
const FilterTransitionContext = createContext<
|
||||
@@ -39,13 +40,33 @@ interface FilterTransitionProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a shared pending state for filter changes.
|
||||
*
|
||||
* Filter components signal the start of navigation via signalFilterChange(),
|
||||
* and use their own local useTransition() for the actual router.push().
|
||||
* This avoids a known Next.js production bug where a shared useTransition()
|
||||
* wrapping router.push() causes the navigation to be silently reverted.
|
||||
*
|
||||
* The pending state auto-resets when searchParams change (navigation completed).
|
||||
*/
|
||||
export const FilterTransitionProvider = ({
|
||||
children,
|
||||
}: FilterTransitionProviderProps) => {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const searchParams = useSearchParams();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
// Auto-reset pending state when searchParams change (navigation completed)
|
||||
useEffect(() => {
|
||||
setIsPending(false);
|
||||
}, [searchParams]);
|
||||
|
||||
const signalFilterChange = () => {
|
||||
setIsPending(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<FilterTransitionContext.Provider value={{ isPending, startTransition }}>
|
||||
<FilterTransitionContext.Provider value={{ isPending, signalFilterChange }}>
|
||||
{children}
|
||||
</FilterTransitionContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useTransition } from "react";
|
||||
import { useTransition } from "react";
|
||||
|
||||
import { useFilterTransitionOptional } from "@/contexts";
|
||||
|
||||
@@ -20,70 +20,64 @@ export const useUrlFilters = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Use shared context if available, otherwise fall back to local transition
|
||||
const sharedTransition = useFilterTransitionOptional();
|
||||
const [localIsPending, localStartTransition] = useTransition();
|
||||
// Signal shared pending state for DataTable loading indicator
|
||||
const filterTransition = useFilterTransitionOptional();
|
||||
const [localIsPending, startTransition] = useTransition();
|
||||
|
||||
const isPending = sharedTransition?.isPending ?? localIsPending;
|
||||
const startTransition =
|
||||
sharedTransition?.startTransition ?? localStartTransition;
|
||||
const isPending = filterTransition?.isPending ?? localIsPending;
|
||||
|
||||
const updateFilter = useCallback(
|
||||
(key: string, value: string | string[] | null) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
const updateFilter = (key: string, value: string | string[] | null) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`;
|
||||
const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`;
|
||||
|
||||
const currentValue = params.get(filterKey);
|
||||
const nextValue = Array.isArray(value)
|
||||
? value.length > 0
|
||||
? value.join(",")
|
||||
: null
|
||||
: value === null
|
||||
? null
|
||||
: value;
|
||||
const currentValue = params.get(filterKey);
|
||||
const nextValue = Array.isArray(value)
|
||||
? value.length > 0
|
||||
? value.join(",")
|
||||
: null
|
||||
: value === null
|
||||
? null
|
||||
: value;
|
||||
|
||||
// If effective value is unchanged, do nothing (avoids redundant fetches)
|
||||
if (currentValue === nextValue) return;
|
||||
// If effective value is unchanged, do nothing (avoids redundant fetches)
|
||||
if (currentValue === nextValue) return;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
filterTransition?.signalFilterChange();
|
||||
startTransition(() => {
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
});
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
filterTransition?.signalFilterChange();
|
||||
startTransition(() => {
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
Array.from(params.keys()).forEach((key) => {
|
||||
if (key.startsWith("filter[") || key === "sort") {
|
||||
@@ -93,17 +87,18 @@ export const useUrlFilters = () => {
|
||||
|
||||
params.delete("page");
|
||||
|
||||
filterTransition?.signalFilterChange();
|
||||
startTransition(() => {
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
});
|
||||
}, [router, searchParams, pathname, startTransition]);
|
||||
};
|
||||
|
||||
const hasFilters = useCallback(() => {
|
||||
const hasFilters = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
return Array.from(params.keys()).some(
|
||||
(key) => key.startsWith("filter[") || key === "sort",
|
||||
);
|
||||
}, [searchParams]);
|
||||
};
|
||||
|
||||
return {
|
||||
updateFilter,
|
||||
|
||||
Reference in New Issue
Block a user