mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
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:
@@ -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]");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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]");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user