From 71220b2696a678837bf8d19c7442ba12787d313e Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:28:03 +0100 Subject: [PATCH] fix(ui): replace HeroUI dropdowns with Radix ActionDropdown to fix overlay conflict (#9996) --- ui/CHANGELOG.md | 5 + .../simple/mute-rule-row-actions.tsx | 73 +++------ .../findings/table/data-table-row-actions.tsx | 27 +--- .../table/data-table-row-actions.tsx | 103 ++++-------- .../table/data-table-row-actions.tsx | 79 +++------- .../table/data-table-row-actions.tsx | 148 ++++++------------ .../resources/table/column-resources.tsx | 4 +- .../roles/table/data-table-row-actions.tsx | 79 +++------- .../table/scans/data-table-row-actions.tsx | 72 +++------ .../shadcn/dropdown/action-dropdown.tsx | 42 ++--- ui/components/shadcn/dropdown/index.ts | 3 +- .../api-keys/data-table-row-actions.tsx | 84 +++------- .../users/table/data-table-row-actions.tsx | 79 +++------- 13 files changed, 257 insertions(+), 541 deletions(-) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 55a8048182..d7fadac0de 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -8,9 +8,14 @@ All notable changes to the **Prowler UI** are documented in this file. - Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983) +--- + +## [1.18.2] (Prowler UNRELEASED) + ### 🐞 Fixed - ProviderTypeSelector crash when an unknown provider type is not present in PROVIDER_DATA [(#9991)](https://github.com/prowler-cloud/prowler/pull/9991) +- Infinite memory loop when opening modals from table row action dropdowns caused by HeroUI (React Aria) and Radix Dialog overlay conflict [(#9996)](https://github.com/prowler-cloud/prowler/pull/9996) --- diff --git a/ui/app/(prowler)/mutelist/_components/simple/mute-rule-row-actions.tsx b/ui/app/(prowler)/mutelist/_components/simple/mute-rule-row-actions.tsx index f57b9ffbe4..70f437d967 100644 --- a/ui/app/(prowler)/mutelist/_components/simple/mute-rule-row-actions.tsx +++ b/ui/app/(prowler)/mutelist/_components/simple/mute-rule-row-actions.tsx @@ -1,17 +1,15 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; import { Pencil, Trash2 } from "lucide-react"; import { MuteRuleData } from "@/actions/mute-rules/types"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; interface MuteRuleRowActionsProps { muteRule: MuteRuleData; @@ -26,11 +24,8 @@ export function MuteRuleRowActions({ }: MuteRuleRowActionsProps) { return (
- - + - - - - - } - onPress={() => onEdit(muteRule)} - > - Edit - - - } - onPress={() => onDelete(muteRule)} - > - Delete - - - - + } + > + } + label="Edit Mute Rule" + onSelect={() => onEdit(muteRule)} + /> + + } + label="Delete Mute Rule" + destructive + onSelect={() => onDelete(muteRule)} + /> + +
); } diff --git a/ui/components/findings/table/data-table-row-actions.tsx b/ui/components/findings/table/data-table-row-actions.tsx index 9055d7eb9a..752f9f5144 100644 --- a/ui/components/findings/table/data-table-row-actions.tsx +++ b/ui/components/findings/table/data-table-row-actions.tsx @@ -66,30 +66,13 @@ export function DataTableRowActions({ return [finding.id]; }; - const getMuteDescription = (): string => { - if (isMuted) { - return "This finding is already muted"; - } - const ids = getMuteIds(); - if (ids.length > 1) { - return `Mute ${ids.length} selected findings`; - } - return "Mute this finding"; - }; - const getMuteLabel = () => { if (isMuted) return "Muted"; - if (!isMuted && isCurrentSelected && hasMultipleSelected) { - return ( - <> - Mute - - ({selectedFindingIds.length}) - - - ); + const ids = getMuteIds(); + if (ids.length > 1) { + return `Mute ${ids.length} Findings`; } - return "Mute"; + return "Mute Finding"; }; const handleMuteComplete = () => { @@ -146,7 +129,6 @@ export function DataTableRowActions({ ) } label={getMuteLabel()} - description={getMuteDescription()} disabled={isMuted} onSelect={() => { setIsMuteModalOpen(true); @@ -155,7 +137,6 @@ export function DataTableRowActions({ } label="Send to Jira" - description="Create a Jira issue for this finding" onSelect={() => setIsJiraModalOpen(true)} /> diff --git a/ui/components/invitations/table/data-table-row-actions.tsx b/ui/components/invitations/table/data-table-row-actions.tsx index 4ec5f50e79..b643202f80 100644 --- a/ui/components/invitations/table/data-table-row-actions.tsx +++ b/ui/components/invitations/table/data-table-row-actions.tsx @@ -1,24 +1,17 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - AddNoteBulkIcon, - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Eye, Pencil, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { DeleteForm, EditForm } from "../forms"; @@ -27,7 +20,6 @@ interface DataTableRowActionsProps { row: Row; roles?: { id: string; name: string }[]; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -67,65 +59,36 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => - router.push(`/invitations/check-details?id=${invitationId}`) - } - > - Check Details - - - } - onPress={() => setIsEditOpen(true)} - isDisabled={invitationAccepted === "accepted"} - > - Edit Invitation - - - - - } - onPress={() => setIsDeleteOpen(true)} - isDisabled={invitationAccepted === "accepted"} - > - Revoke Invitation - - - - + } + > + } + label="Check Details" + onSelect={() => + router.push(`/invitations/check-details?id=${invitationId}`) + } + /> + } + label="Edit Invitation" + onSelect={() => setIsEditOpen(true)} + disabled={invitationAccepted === "accepted"} + /> + + } + label="Revoke Invitation" + destructive + onSelect={() => setIsDeleteOpen(true)} + disabled={invitationAccepted === "accepted"} + /> + +
); diff --git a/ui/components/manage-groups/table/data-table-row-actions.tsx b/ui/components/manage-groups/table/data-table-row-actions.tsx index 3ea2f9c9a1..519b0b05e2 100644 --- a/ui/components/manage-groups/table/data-table-row-actions.tsx +++ b/ui/components/manage-groups/table/data-table-row-actions.tsx @@ -1,23 +1,17 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { DeleteGroupForm } from "../forms"; @@ -25,7 +19,6 @@ import { DeleteGroupForm } from "../forms"; interface DataTableRowActionsProps { row: Row; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -47,51 +40,27 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => router.push(`/manage-groups?groupId=${groupId}`)} - > - Edit Provider Group - - - - - } - onPress={() => setIsDeleteOpen(true)} - > - Delete Provider Group - - - - + } + > + } + label="Edit Provider Group" + onSelect={() => router.push(`/manage-groups?groupId=${groupId}`)} + /> + + } + label="Delete Provider Group" + destructive + onSelect={() => setIsDeleteOpen(true)} + /> + +
); diff --git a/ui/components/providers/table/data-table-row-actions.tsx b/ui/components/providers/table/data-table-row-actions.tsx index 1866abe8ab..023f563006 100644 --- a/ui/components/providers/table/data-table-row-actions.tsx +++ b/ui/components/providers/table/data-table-row-actions.tsx @@ -1,25 +1,18 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - AddNoteBulkIcon, - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, PlugZap, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { checkConnectionProvider } from "@/actions/providers/providers"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { EditForm } from "../forms"; @@ -28,7 +21,6 @@ import { DeleteForm } from "../forms/delete-form"; interface DataTableRowActionsProps { row: Row; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -53,12 +45,6 @@ export function DataTableRowActions({ const hasSecret = (row.original as any).relationships?.secret?.data; - // Calculate disabled keys based on conditions - const disabledKeys = []; - if (!hasSecret || loading) { - disabledKeys.push("new"); - } - return ( <> ({
- - + - - - - } - onPress={() => - router.push( - `/providers/${hasSecret ? "update" : "add"}-credentials?type=${providerType}&id=${providerId}${providerSecretId ? `&secretId=${providerSecretId}` : ""}`, - ) - } - closeOnSelect={true} - > - {hasSecret ? "Update Credentials" : "Add Credentials"} - - } - onPress={handleTestConnection} - closeOnSelect={false} - > - {loading ? "Testing..." : "Test Connection"} - - } - onPress={() => setIsEditOpen(true)} - closeOnSelect={true} - > - Edit Provider Alias - - - - - } - onPress={() => setIsDeleteOpen(true)} - closeOnSelect={true} - > - Delete Provider - - - - + } + > + } + label={hasSecret ? "Update Credentials" : "Add Credentials"} + onSelect={() => + router.push( + `/providers/${hasSecret ? "update" : "add"}-credentials?type=${providerType}&id=${providerId}${providerSecretId ? `&secretId=${providerSecretId}` : ""}`, + ) + } + /> + } + label={loading ? "Testing..." : "Test Connection"} + description={ + hasSecret && !loading + ? "Check the provider connection" + : loading + ? "Checking provider connection" + : "Add credentials to test the connection" + } + onSelect={(e) => { + e.preventDefault(); + handleTestConnection(); + }} + disabled={!hasSecret || loading} + /> + } + label="Edit Provider Alias" + onSelect={() => setIsEditOpen(true)} + /> + + } + label="Delete Provider" + destructive + onSelect={() => setIsDeleteOpen(true)} + /> + +
); diff --git a/ui/components/resources/table/column-resources.tsx b/ui/components/resources/table/column-resources.tsx index e94021d2d0..cdbc000ff3 100644 --- a/ui/components/resources/table/column-resources.tsx +++ b/ui/components/resources/table/column-resources.tsx @@ -83,7 +83,6 @@ const FailedFindingsBadge = ({ count }: { count: number }) => { // Row actions dropdown const ResourceRowActions = ({ row }: { row: { original: ResourceProps } }) => { const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const resourceName = row.original.attributes?.name || "Resource"; return ( <> @@ -102,8 +101,7 @@ const ResourceRowActions = ({ row }: { row: { original: ResourceProps } }) => { > } - label="View details" - description={`View details for ${resourceName}`} + label="View Details" onSelect={() => setIsDrawerOpen(true)} /> diff --git a/ui/components/roles/table/data-table-row-actions.tsx b/ui/components/roles/table/data-table-row-actions.tsx index 0eb7864a51..70895817e3 100644 --- a/ui/components/roles/table/data-table-row-actions.tsx +++ b/ui/components/roles/table/data-table-row-actions.tsx @@ -1,30 +1,23 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { DeleteRoleForm } from "../workflow/forms"; interface DataTableRowActionsProps { row: Row; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -43,51 +36,27 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => router.push(`/roles/edit?roleId=${roleId}`)} - > - Edit Role - - - - - } - onPress={() => setIsDeleteOpen(true)} - > - Delete Role - - - - + } + > + } + label="Edit Role" + onSelect={() => router.push(`/roles/edit?roleId=${roleId}`)} + /> + + } + label="Delete Role" + destructive + onSelect={() => setIsDeleteOpen(true)} + /> + +
); diff --git a/ui/components/scans/table/scans/data-table-row-actions.tsx b/ui/components/scans/table/scans/data-table-row-actions.tsx index b9d16db9ab..e185d0d43d 100644 --- a/ui/components/scans/table/scans/data-table-row-actions.tsx +++ b/ui/components/scans/table/scans/data-table-row-actions.tsx @@ -1,22 +1,15 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - // DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import { DownloadIcon } from "lucide-react"; +import { Download, Pencil } from "lucide-react"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { useToast } from "@/components/ui"; import { downloadScanZip } from "@/lib/helper"; @@ -26,7 +19,6 @@ import { EditScanForm } from "../../forms"; interface DataTableRowActionsProps { row: Row; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -52,46 +44,26 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => downloadScanZip(scanId, toast)} - isDisabled={scanState !== "completed"} - > - Download .zip - - - - } - onPress={() => setIsEditOpen(true)} - > - Edit scan name - - - - + } + > + } + label="Download .zip" + description="Available only for completed scans" + onSelect={() => downloadScanZip(scanId, toast)} + disabled={scanState !== "completed"} + /> + } + label="Edit Scan Name" + onSelect={() => setIsEditOpen(true)} + /> +
); diff --git a/ui/components/shadcn/dropdown/action-dropdown.tsx b/ui/components/shadcn/dropdown/action-dropdown.tsx index d272872129..fe44da2757 100644 --- a/ui/components/shadcn/dropdown/action-dropdown.tsx +++ b/ui/components/shadcn/dropdown/action-dropdown.tsx @@ -9,7 +9,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown"; @@ -17,8 +16,6 @@ import { interface ActionDropdownProps { /** The dropdown trigger element. Defaults to a vertical dots icon button */ trigger?: ReactNode; - /** Label shown at the top of the dropdown */ - label?: string; /** Alignment of the dropdown content */ align?: "start" | "center" | "end"; /** Additional className for the content */ @@ -30,7 +27,6 @@ interface ActionDropdownProps { export function ActionDropdown({ trigger, - label = "Actions", align = "end", className, ariaLabel = "Open actions menu", @@ -52,16 +48,10 @@ export function ActionDropdown({ - {label && ( - <> - {label} - - - )} {children} @@ -91,8 +81,8 @@ export function ActionDropdownItem({ return ( svg]:size-5", - destructive && "text-destructive", + "text-muted-foreground mt-0.5 shrink-0 [&>svg]:size-4", + destructive && "text-text-error-primary", )} > {icon} @@ -113,7 +103,7 @@ export function ActionDropdownItem({ {description} @@ -124,8 +114,18 @@ export function ActionDropdownItem({ ); } -// Re-export commonly used components for convenience -export { - DropdownMenuLabel as ActionDropdownLabel, - DropdownMenuSeparator as ActionDropdownSeparator, -} from "./dropdown"; +export function ActionDropdownDangerZone({ + children, +}: { + children: ReactNode; +}) { + return ( + <> + + + Danger zone + + {children} + + ); +} diff --git a/ui/components/shadcn/dropdown/index.ts b/ui/components/shadcn/dropdown/index.ts index d21a2ff982..06169c0668 100644 --- a/ui/components/shadcn/dropdown/index.ts +++ b/ui/components/shadcn/dropdown/index.ts @@ -1,8 +1,7 @@ export { ActionDropdown, + ActionDropdownDangerZone, ActionDropdownItem, - ActionDropdownLabel, - ActionDropdownSeparator, } from "./action-dropdown"; export { DropdownMenu, diff --git a/ui/components/users/profile/api-keys/data-table-row-actions.tsx b/ui/components/users/profile/api-keys/data-table-row-actions.tsx index 51af064c8a..b47eaab0cc 100644 --- a/ui/components/users/profile/api-keys/data-table-row-actions.tsx +++ b/ui/components/users/profile/api-keys/data-table-row-actions.tsx @@ -1,21 +1,15 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, Trash2 } from "lucide-react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { EnrichedApiKey } from "./types"; @@ -25,8 +19,6 @@ interface DataTableRowActionsProps { onRevoke: (apiKey: EnrichedApiKey) => void; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; - export function DataTableRowActions({ row, onEdit, @@ -39,53 +31,29 @@ export function DataTableRowActions({ return (
- - + - - - - } - onPress={() => onEdit(apiKey)} - > - Edit name - - - {canRevoke ? ( - - - } - onPress={() => onRevoke(apiKey)} - > - Revoke - - - ) : null} - - + } + > + } + label="Edit API Key" + onSelect={() => onEdit(apiKey)} + /> + {canRevoke && ( + + } + label="Revoke API Key" + destructive + onSelect={() => onRevoke(apiKey)} + /> + + )} +
); } diff --git a/ui/components/users/table/data-table-row-actions.tsx b/ui/components/users/table/data-table-row-actions.tsx index 41f81290da..fafabb5011 100644 --- a/ui/components/users/table/data-table-row-actions.tsx +++ b/ui/components/users/table/data-table-row-actions.tsx @@ -1,22 +1,16 @@ "use client"; -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@heroui/dropdown"; -import { - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@heroui/shared-icons"; import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; +import { Pencil, Trash2 } from "lucide-react"; import { useState } from "react"; import { VerticalDotsIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; +import { + ActionDropdown, + ActionDropdownDangerZone, + ActionDropdownItem, +} from "@/components/shadcn/dropdown"; import { Modal } from "@/components/shadcn/modal"; import { DeleteForm, EditForm } from "../forms"; @@ -25,7 +19,6 @@ interface DataTableRowActionsProps { row: Row; roles?: { id: string; name: string }[]; } -const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0"; export function DataTableRowActions({ row, @@ -66,51 +59,27 @@ export function DataTableRowActions({
- - + - - - - } - onPress={() => setIsEditOpen(true)} - > - Edit User - - - - - } - onPress={() => setIsDeleteOpen(true)} - > - Delete User - - - - + } + > + } + label="Edit User" + onSelect={() => setIsEditOpen(true)} + /> + + } + label="Delete User" + destructive + onSelect={() => setIsDeleteOpen(true)} + /> + +
);