fix(ui): reapply filter transition opacity overlay on filter changes (#10037)

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:25:46 +01:00
committed by GitHub
parent b424eb302e
commit 0e79b70fee
5 changed files with 87 additions and 108 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { ReactNode, useTransition } from "react";
import { useSearchParams } from "next/navigation";
import { ReactNode } from "react";
import {
AlibabaCloudProviderBadge,
@@ -22,7 +22,7 @@ import {
MultiSelectTrigger,
MultiSelectValue,
} from "@/components/shadcn/select/multiselect";
import { useFilterTransitionOptional } from "@/contexts";
import { useUrlFilters } from "@/hooks/use-url-filters";
import type { ProviderProps, ProviderType } from "@/types/providers";
const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
@@ -43,15 +43,8 @@ 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 { navigateWithParams } = useUrlFilters();
const filterKey = "filter[provider_id__in]";
const current = searchParams.get(filterKey) || "";
@@ -69,44 +62,36 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) {
);
const handleMultiValueChange = (ids: string[]) => {
const params = new URLSearchParams(searchParams.toString());
params.delete(filterKey);
navigateWithParams((params) => {
params.delete(filterKey);
if (ids.length > 0) {
params.set(filterKey, ids.join(","));
}
// Auto-deselect provider types that no longer have any selected accounts
if (selectedTypesList.length > 0) {
// Get provider types of currently selected accounts
const selectedProviders = providers.filter((p) => ids.includes(p.id));
const selectedProviderTypes = new Set(
selectedProviders.map((p) => p.attributes.provider),
);
// Keep only provider types that still have selected accounts
const remainingProviderTypes = selectedTypesList.filter((type) =>
selectedProviderTypes.has(type as ProviderType),
);
// Update provider_type__in filter
if (remainingProviderTypes.length > 0) {
params.set(
"filter[provider_type__in]",
remainingProviderTypes.join(","),
);
} else {
params.delete("filter[provider_type__in]");
if (ids.length > 0) {
params.set(filterKey, ids.join(","));
}
}
// Reset to page 1 when changing filter
if (params.has("page")) {
params.set("page", "1");
}
// Auto-deselect provider types that no longer have any selected accounts
if (selectedTypesList.length > 0) {
// Get provider types of currently selected accounts
const selectedProviders = providers.filter((p) => ids.includes(p.id));
const selectedProviderTypes = new Set(
selectedProviders.map((p) => p.attributes.provider),
);
startTransition(() => {
router.push(`${pathname}?${params.toString()}`, { scroll: false });
// Keep only provider types that still have selected accounts
const remainingProviderTypes = selectedTypesList.filter((type) =>
selectedProviderTypes.has(type as ProviderType),
);
// Update provider_type__in filter
if (remainingProviderTypes.length > 0) {
params.set(
"filter[provider_type__in]",
remainingProviderTypes.join(","),
);
} else {
params.delete("filter[provider_type__in]");
}
}
});
};

View File

@@ -1,7 +1,7 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { lazy, Suspense, useTransition } from "react";
import { useSearchParams } from "next/navigation";
import { lazy, Suspense } from "react";
import {
MultiSelect,
@@ -10,7 +10,7 @@ import {
MultiSelectTrigger,
MultiSelectValue,
} from "@/components/shadcn/select/multiselect";
import { useFilterTransitionOptional } from "@/contexts";
import { useUrlFilters } from "@/hooks/use-url-filters";
import { type ProviderProps, ProviderType } from "@/types/providers";
const AWSProviderBadge = lazy(() =>
@@ -123,15 +123,8 @@ type ProviderTypeSelectorProps = {
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 { navigateWithParams } = useUrlFilters();
const currentProviders = searchParams.get("filter[provider_type__in]") || "";
const selectedTypes = currentProviders
@@ -139,26 +132,17 @@ export const ProviderTypeSelector = ({
: [];
const handleMultiValueChange = (values: string[]) => {
const params = new URLSearchParams(searchParams.toString());
navigateWithParams((params) => {
// Update provider_type__in
if (values.length > 0) {
params.set("filter[provider_type__in]", values.join(","));
} else {
params.delete("filter[provider_type__in]");
}
// Update provider_type__in
if (values.length > 0) {
params.set("filter[provider_type__in]", values.join(","));
} else {
params.delete("filter[provider_type__in]");
}
// Clear account selection when changing provider types
// User should manually select accounts if they want to filter by specific accounts
params.delete("filter[provider_id__in]");
// Reset to page 1 when changing filter
if (params.has("page")) {
params.set("page", "1");
}
startTransition(() => {
router.push(`${pathname}?${params.toString()}`, { scroll: false });
// Clear account selection when changing provider types
// User should manually select accounts if they want to filter by specific accounts
params.delete("filter[provider_id__in]");
});
};

View File

@@ -1,10 +1,9 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useTransition } from "react";
import { useSearchParams } from "next/navigation";
import { Checkbox } from "@/components/shadcn";
import { useFilterTransitionOptional } from "@/contexts";
import { useUrlFilters } from "@/hooks/use-url-filters";
// Constants for muted filter URL values
const MUTED_FILTER_VALUES = {
@@ -13,18 +12,10 @@ const MUTED_FILTER_VALUES = {
} as const;
export const CustomCheckboxMutedFindings = () => {
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 { navigateWithParams } = useUrlFilters();
// 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]");
// URL states:
@@ -34,23 +25,15 @@ export const CustomCheckboxMutedFindings = () => {
const handleMutedChange = (checked: boolean | "indeterminate") => {
const isChecked = checked === true;
const params = new URLSearchParams(searchParams.toString());
if (isChecked) {
// Include muted: set special value (API will ignore invalid value and show all)
params.set("filter[muted]", MUTED_FILTER_VALUES.INCLUDE);
} else {
// Exclude muted: apply filter to show only non-muted
params.set("filter[muted]", MUTED_FILTER_VALUES.EXCLUDE);
}
// Reset to page 1 when changing filter
if (params.has("page")) {
params.set("page", "1");
}
startTransition(() => {
router.push(`${pathname}?${params.toString()}`, { scroll: false });
navigateWithParams((params) => {
if (isChecked) {
// Include muted: set special value (API will ignore invalid value and show all)
params.set("filter[muted]", MUTED_FILTER_VALUES.INCLUDE);
} else {
// Exclude muted: apply filter to show only non-muted
params.set("filter[muted]", MUTED_FILTER_VALUES.EXCLUDE);
}
});
};

View File

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

View File

@@ -2,6 +2,8 @@
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useFilterTransitionOptional } from "@/contexts";
const FINDINGS_PATH = "/findings";
const DEFAULT_MUTED_FILTER = "false";
@@ -16,6 +18,7 @@ export const useUrlFilters = () => {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const filterTransition = useFilterTransitionOptional();
const isPending = false;
const ensureFindingsDefaultMuted = (params: URLSearchParams) => {
@@ -29,7 +32,10 @@ export const useUrlFilters = () => {
ensureFindingsDefaultMuted(params);
const queryString = params.toString();
if (queryString === searchParams.toString()) return;
const targetUrl = queryString ? `${pathname}?${queryString}` : pathname;
filterTransition?.signalFilterChange();
router.push(targetUrl, { scroll: false });
};