fix(ui): replace HeroUI dropdowns with Radix ActionDropdown to fix overlay conflict (#9996)

This commit is contained in:
Alejandro Bailo
2026-02-10 10:28:03 +01:00
committed by GitHub
parent dd730eec94
commit 71220b2696
13 changed files with 257 additions and 541 deletions

View File

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

View File

@@ -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 (
<div className="flex items-center justify-center px-2">
<Dropdown
className="border-border-neutral-secondary bg-bg-neutral-secondary border shadow-xl"
placement="bottom"
>
<DropdownTrigger>
<ActionDropdown
trigger={
<Button
variant="outline"
size="icon-sm"
@@ -41,44 +36,22 @@ export function MuteRuleRowActions({
className="text-text-neutral-secondary"
/>
</Button>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="Mute rule actions"
color="default"
variant="flat"
>
<DropdownSection title="Actions">
<DropdownItem
key="edit"
description="Edit rule name and reason"
textValue="Edit"
startContent={
<Pencil className="text-default-500 pointer-events-none size-4 shrink-0" />
}
onPress={() => onEdit(muteRule)}
>
Edit
</DropdownItem>
<DropdownItem
key="delete"
description="Delete this mute rule"
textValue="Delete"
className="text-danger"
color="danger"
classNames={{
description: "text-danger",
}}
startContent={
<Trash2 className="pointer-events-none size-4 shrink-0" />
}
onPress={() => onDelete(muteRule)}
>
Delete
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
}
>
<ActionDropdownItem
icon={<Pencil />}
label="Edit Mute Rule"
onSelect={() => onEdit(muteRule)}
/>
<ActionDropdownDangerZone>
<ActionDropdownItem
icon={<Trash2 />}
label="Delete Mute Rule"
destructive
onSelect={() => onDelete(muteRule)}
/>
</ActionDropdownDangerZone>
</ActionDropdown>
</div>
);
}

View File

@@ -66,30 +66,13 @@ export function DataTableRowActions<T extends FindingRowData>({
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
<span className="ml-1 text-xs text-slate-500">
({selectedFindingIds.length})
</span>
</>
);
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<T extends FindingRowData>({
)
}
label={getMuteLabel()}
description={getMuteDescription()}
disabled={isMuted}
onSelect={() => {
setIsMuteModalOpen(true);
@@ -155,7 +137,6 @@ export function DataTableRowActions<T extends FindingRowData>({
<ActionDropdownItem
icon={<JiraIcon size={20} />}
label="Send to Jira"
description="Create a Jira issue for this finding"
onSelect={() => setIsJiraModalOpen(true)}
/>
</ActionDropdown>

View File

@@ -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<InvitationProps> {
row: Row<InvitationProps>;
roles?: { id: string; name: string }[];
}
const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0";
export function DataTableRowActions<InvitationProps>({
row,
@@ -67,65 +59,36 @@ export function DataTableRowActions<InvitationProps>({
</Modal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown
className="border-border-neutral-secondary bg-bg-neutral-secondary border shadow-xl"
placement="bottom"
>
<DropdownTrigger>
<ActionDropdown
trigger={
<Button variant="ghost" size="icon-sm" className="rounded-full">
<VerticalDotsIcon className="text-slate-400" />
</Button>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="Actions"
color="default"
variant="flat"
>
<DropdownSection title="Actions">
<DropdownItem
key="check-details"
description="View invitation details"
textValue="Check Details"
startContent={<AddNoteBulkIcon className={iconClasses} />}
onPress={() =>
router.push(`/invitations/check-details?id=${invitationId}`)
}
>
Check Details
</DropdownItem>
<DropdownItem
key="edit"
description="Allows you to edit the invitation"
textValue="Edit Invitation"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
onPress={() => setIsEditOpen(true)}
isDisabled={invitationAccepted === "accepted"}
>
Edit Invitation
</DropdownItem>
</DropdownSection>
<DropdownSection title="Danger zone">
<DropdownItem
key="delete"
className="text-text-error"
color="danger"
description="Delete the invitation permanently"
textValue="Delete Invitation"
startContent={
<DeleteDocumentBulkIcon
className={clsx(iconClasses, "!text-text-error")}
/>
}
onPress={() => setIsDeleteOpen(true)}
isDisabled={invitationAccepted === "accepted"}
>
Revoke Invitation
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
}
>
<ActionDropdownItem
icon={<Eye />}
label="Check Details"
onSelect={() =>
router.push(`/invitations/check-details?id=${invitationId}`)
}
/>
<ActionDropdownItem
icon={<Pencil />}
label="Edit Invitation"
onSelect={() => setIsEditOpen(true)}
disabled={invitationAccepted === "accepted"}
/>
<ActionDropdownDangerZone>
<ActionDropdownItem
icon={<Trash2 />}
label="Revoke Invitation"
destructive
onSelect={() => setIsDeleteOpen(true)}
disabled={invitationAccepted === "accepted"}
/>
</ActionDropdownDangerZone>
</ActionDropdown>
</div>
</>
);

View File

@@ -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<ProviderProps> {
row: Row<ProviderProps>;
}
const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0";
export function DataTableRowActions<ProviderProps>({
row,
@@ -47,51 +40,27 @@ export function DataTableRowActions<ProviderProps>({
</Modal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown
className="border-border-neutral-secondary bg-bg-neutral-secondary border shadow-xl"
placement="bottom"
>
<DropdownTrigger>
<ActionDropdown
trigger={
<Button variant="ghost" size="icon-sm" className="rounded-full">
<VerticalDotsIcon className="text-slate-400" />
</Button>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="Actions"
color="default"
variant="flat"
>
<DropdownSection title="Actions">
<DropdownItem
key="edit"
description="Allows you to edit the provider group"
textValue="Edit Provider Group"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
onPress={() => router.push(`/manage-groups?groupId=${groupId}`)}
>
Edit Provider Group
</DropdownItem>
</DropdownSection>
<DropdownSection title="Danger zone">
<DropdownItem
key="delete"
className="text-text-error"
color="danger"
description="Delete the provider group permanently"
textValue="Delete Provider Group"
startContent={
<DeleteDocumentBulkIcon
className={clsx(iconClasses, "!text-text-error")}
/>
}
onPress={() => setIsDeleteOpen(true)}
>
Delete Provider Group
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
}
>
<ActionDropdownItem
icon={<Pencil />}
label="Edit Provider Group"
onSelect={() => router.push(`/manage-groups?groupId=${groupId}`)}
/>
<ActionDropdownDangerZone>
<ActionDropdownItem
icon={<Trash2 />}
label="Delete Provider Group"
destructive
onSelect={() => setIsDeleteOpen(true)}
/>
</ActionDropdownDangerZone>
</ActionDropdown>
</div>
</>
);

View File

@@ -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<ProviderProps> {
row: Row<ProviderProps>;
}
const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0";
export function DataTableRowActions<ProviderProps>({
row,
@@ -53,12 +45,6 @@ export function DataTableRowActions<ProviderProps>({
const hasSecret = (row.original as any).relationships?.secret?.data;
// Calculate disabled keys based on conditions
const disabledKeys = [];
if (!hasSecret || loading) {
disabledKeys.push("new");
}
return (
<>
<Modal
@@ -82,88 +68,52 @@ export function DataTableRowActions<ProviderProps>({
</Modal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown
className="border-border-neutral-secondary bg-bg-neutral-secondary border shadow-xl"
placement="bottom"
>
<DropdownTrigger>
<ActionDropdown
trigger={
<Button variant="ghost" size="icon-sm" className="rounded-full">
<VerticalDotsIcon className="text-slate-400" />
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Actions"
color="default"
variant="flat"
disabledKeys={disabledKeys}
closeOnSelect={false}
>
<DropdownSection title="Actions">
<DropdownItem
key={hasSecret ? "update" : "add"}
description={
hasSecret
? "Update the provider credentials"
: "Add the provider credentials"
}
textValue={hasSecret ? "Update Credentials" : "Add Credentials"}
startContent={<EditDocumentBulkIcon className={iconClasses} />}
onPress={() =>
router.push(
`/providers/${hasSecret ? "update" : "add"}-credentials?type=${providerType}&id=${providerId}${providerSecretId ? `&secretId=${providerSecretId}` : ""}`,
)
}
closeOnSelect={true}
>
{hasSecret ? "Update Credentials" : "Add Credentials"}
</DropdownItem>
<DropdownItem
key="new"
description={
hasSecret && !loading
? "Check the provider connection"
: loading
? "Checking provider connection"
: "Add credentials to test the connection"
}
textValue="Check Connection"
startContent={<AddNoteBulkIcon className={iconClasses} />}
onPress={handleTestConnection}
closeOnSelect={false}
>
{loading ? "Testing..." : "Test Connection"}
</DropdownItem>
<DropdownItem
key="edit"
description="Allows you to edit the provider"
textValue="Edit Provider"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
onPress={() => setIsEditOpen(true)}
closeOnSelect={true}
>
Edit Provider Alias
</DropdownItem>
</DropdownSection>
<DropdownSection title="Danger zone">
<DropdownItem
key="delete"
className="text-text-error"
color="danger"
description="Delete the provider permanently"
textValue="Delete Provider"
startContent={
<DeleteDocumentBulkIcon
className={clsx(iconClasses, "!text-text-error")}
/>
}
onPress={() => setIsDeleteOpen(true)}
closeOnSelect={true}
>
Delete Provider
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
}
>
<ActionDropdownItem
icon={<Pencil />}
label={hasSecret ? "Update Credentials" : "Add Credentials"}
onSelect={() =>
router.push(
`/providers/${hasSecret ? "update" : "add"}-credentials?type=${providerType}&id=${providerId}${providerSecretId ? `&secretId=${providerSecretId}` : ""}`,
)
}
/>
<ActionDropdownItem
icon={<PlugZap />}
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}
/>
<ActionDropdownItem
icon={<Pencil />}
label="Edit Provider Alias"
onSelect={() => setIsEditOpen(true)}
/>
<ActionDropdownDangerZone>
<ActionDropdownItem
icon={<Trash2 />}
label="Delete Provider"
destructive
onSelect={() => setIsDeleteOpen(true)}
/>
</ActionDropdownDangerZone>
</ActionDropdown>
</div>
</>
);

View File

@@ -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 } }) => {
>
<ActionDropdownItem
icon={<Eye className="size-5" />}
label="View details"
description={`View details for ${resourceName}`}
label="View Details"
onSelect={() => setIsDrawerOpen(true)}
/>
</ActionDropdown>

View File

@@ -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<RoleProps> {
row: Row<RoleProps>;
}
const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0";
export function DataTableRowActions<RoleProps>({
row,
@@ -43,51 +36,27 @@ export function DataTableRowActions<RoleProps>({
<DeleteRoleForm roleId={roleId} setIsOpen={setIsDeleteOpen} />
</Modal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown
className="border-border-neutral-secondary bg-bg-neutral-secondary border shadow-xl"
placement="bottom"
>
<DropdownTrigger>
<ActionDropdown
trigger={
<Button variant="ghost" size="icon-sm" className="rounded-full">
<VerticalDotsIcon className="text-slate-400" />
</Button>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="Actions"
color="default"
variant="flat"
>
<DropdownSection title="Actions">
<DropdownItem
key="edit"
description="Edit the role details"
textValue="Edit Role"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
onPress={() => router.push(`/roles/edit?roleId=${roleId}`)}
>
Edit Role
</DropdownItem>
</DropdownSection>
<DropdownSection title="Danger zone">
<DropdownItem
key="delete"
className="text-text-error"
color="danger"
description="Delete the role permanently"
textValue="Delete Role"
startContent={
<DeleteDocumentBulkIcon
className={clsx(iconClasses, "!text-text-error")}
/>
}
onPress={() => setIsDeleteOpen(true)}
>
Delete Role
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
}
>
<ActionDropdownItem
icon={<Pencil />}
label="Edit Role"
onSelect={() => router.push(`/roles/edit?roleId=${roleId}`)}
/>
<ActionDropdownDangerZone>
<ActionDropdownItem
icon={<Trash2 />}
label="Delete Role"
destructive
onSelect={() => setIsDeleteOpen(true)}
/>
</ActionDropdownDangerZone>
</ActionDropdown>
</div>
</>
);

View File

@@ -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<ScanProps> {
row: Row<ScanProps>;
}
const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0";
export function DataTableRowActions<ScanProps>({
row,
@@ -52,46 +44,26 @@ export function DataTableRowActions<ScanProps>({
</Modal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown
className="border-border-neutral-secondary bg-bg-neutral-secondary border shadow-xl"
placement="bottom"
>
<DropdownTrigger>
<ActionDropdown
trigger={
<Button variant="ghost" size="icon-sm" className="rounded-full">
<VerticalDotsIcon className="text-slate-400" />
</Button>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="Actions"
color="default"
variant="flat"
>
<DropdownSection title="Download reports">
<DropdownItem
key="export"
description="Available only for completed scans"
textValue="Download .zip"
startContent={<DownloadIcon className={iconClasses} />}
onPress={() => downloadScanZip(scanId, toast)}
isDisabled={scanState !== "completed"}
>
Download .zip
</DropdownItem>
</DropdownSection>
<DropdownSection title="Actions">
<DropdownItem
key="edit"
description="Allows you to edit the scan name"
textValue="Edit Scan Name"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
onPress={() => setIsEditOpen(true)}
>
Edit scan name
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
}
>
<ActionDropdownItem
icon={<Download />}
label="Download .zip"
description="Available only for completed scans"
onSelect={() => downloadScanZip(scanId, toast)}
disabled={scanState !== "completed"}
/>
<ActionDropdownItem
icon={<Pencil />}
label="Edit Scan Name"
onSelect={() => setIsEditOpen(true)}
/>
</ActionDropdown>
</div>
</>
);

View File

@@ -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({
<DropdownMenuContent
align={align}
className={cn(
"border-border-neutral-secondary bg-bg-neutral-secondary w-56",
"border-border-neutral-secondary bg-bg-neutral-secondary w-56 rounded-xl",
className,
)}
>
{label && (
<>
<DropdownMenuLabel>{label}</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
{children}
</DropdownMenuContent>
</DropdownMenu>
@@ -91,8 +81,8 @@ export function ActionDropdownItem({
return (
<DropdownMenuItem
className={cn(
"flex cursor-pointer items-center gap-2",
destructive && "text-destructive focus:text-destructive",
"flex cursor-pointer items-start gap-2",
destructive && "text-text-error-primary focus:text-text-error-primary",
className,
)}
{...props}
@@ -100,8 +90,8 @@ export function ActionDropdownItem({
{icon && (
<span
className={cn(
"text-muted-foreground shrink-0 [&>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({
<span
className={cn(
"text-muted-foreground text-xs",
destructive && "text-destructive/70",
destructive && "text-text-error-primary/70",
)}
>
{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 (
<>
<DropdownMenuSeparator />
<span className="text-text-neutral-tertiary px-2 py-1.5 text-xs">
Danger zone
</span>
{children}
</>
);
}

View File

@@ -1,8 +1,7 @@
export {
ActionDropdown,
ActionDropdownDangerZone,
ActionDropdownItem,
ActionDropdownLabel,
ActionDropdownSeparator,
} from "./action-dropdown";
export {
DropdownMenu,

View File

@@ -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 (
<div className="relative flex items-center justify-end gap-2">
<Dropdown
className="border-border-neutral-secondary bg-bg-neutral-secondary border shadow-xl"
placement="bottom"
>
<DropdownTrigger>
<ActionDropdown
trigger={
<Button variant="ghost" size="icon-sm" className="rounded-full">
<VerticalDotsIcon className="text-slate-400" />
</Button>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="API Key actions"
color="default"
variant="flat"
>
<DropdownSection title="Actions">
<DropdownItem
key="edit"
description="Edit the API key name"
textValue="Edit name"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
onPress={() => onEdit(apiKey)}
>
Edit name
</DropdownItem>
</DropdownSection>
{canRevoke ? (
<DropdownSection title="Danger zone">
<DropdownItem
key="revoke"
className="text-text-error"
color="danger"
description="Revoke this API key permanently"
textValue="Revoke"
startContent={
<DeleteDocumentBulkIcon
className={clsx(iconClasses, "!text-text-error")}
/>
}
onPress={() => onRevoke(apiKey)}
>
Revoke
</DropdownItem>
</DropdownSection>
) : null}
</DropdownMenu>
</Dropdown>
}
>
<ActionDropdownItem
icon={<Pencil />}
label="Edit API Key"
onSelect={() => onEdit(apiKey)}
/>
{canRevoke && (
<ActionDropdownDangerZone>
<ActionDropdownItem
icon={<Trash2 />}
label="Revoke API Key"
destructive
onSelect={() => onRevoke(apiKey)}
/>
</ActionDropdownDangerZone>
)}
</ActionDropdown>
</div>
);
}

View File

@@ -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<UserProps> {
row: Row<UserProps>;
roles?: { id: string; name: string }[];
}
const iconClasses = "text-2xl text-default-500 pointer-events-none shrink-0";
export function DataTableRowActions<UserProps>({
row,
@@ -66,51 +59,27 @@ export function DataTableRowActions<UserProps>({
</Modal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown
className="border-border-neutral-secondary bg-bg-neutral-secondary border shadow-xl"
placement="bottom"
>
<DropdownTrigger>
<ActionDropdown
trigger={
<Button variant="ghost" size="icon-sm" className="rounded-full">
<VerticalDotsIcon className="text-slate-400" />
</Button>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="Actions"
color="default"
variant="flat"
>
<DropdownSection title="Actions">
<DropdownItem
key="edit"
description="Allows you to edit the user"
textValue="Edit User"
startContent={<EditDocumentBulkIcon className={iconClasses} />}
onPress={() => setIsEditOpen(true)}
>
Edit User
</DropdownItem>
</DropdownSection>
<DropdownSection title="Danger zone">
<DropdownItem
key="delete"
className="text-text-error"
color="danger"
description="Delete the user permanently"
textValue="Delete User"
startContent={
<DeleteDocumentBulkIcon
className={clsx(iconClasses, "!text-text-error")}
/>
}
onPress={() => setIsDeleteOpen(true)}
>
Delete User
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
}
>
<ActionDropdownItem
icon={<Pencil />}
label="Edit User"
onSelect={() => setIsEditOpen(true)}
/>
<ActionDropdownDangerZone>
<ActionDropdownItem
icon={<Trash2 />}
label="Delete User"
destructive
onSelect={() => setIsDeleteOpen(true)}
/>
</ActionDropdownDangerZone>
</ActionDropdown>
</div>
</>
);