mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
Users Page - Add User Modal, Edit User Modal (#45)
* feat(users): Add in Add User modal, add in Label component * feat(users): Make adjustments to Add User modal and Edit user modal * feat(users): Populate data from mock API * feat(users): Permissions - do not show team info to users, redirect users
This commit is contained in:
@@ -3,8 +3,10 @@ import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getUsers } from "@/actions/users";
|
||||
import { auth } from "@/auth.config";
|
||||
import { Header } from "@/components/ui";
|
||||
import {
|
||||
AddUserModal,
|
||||
ColumnsUser,
|
||||
DataTableUser,
|
||||
SkeletonTableUser,
|
||||
@@ -12,11 +14,19 @@ import {
|
||||
import { searchParamsProps } from "@/types";
|
||||
|
||||
export default async function Users({ searchParams }: searchParamsProps) {
|
||||
const session = await auth();
|
||||
if (session?.user?.role !== "admin") {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="User Management" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<div className="flex flex-col items-end w-full">
|
||||
<div className="flex space-x-6">
|
||||
<AddUserModal />
|
||||
</div>
|
||||
<Spacer y={6} />
|
||||
<Suspense key={searchParams.page} fallback={<SkeletonTableUser />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
@@ -2,7 +2,9 @@ export * from "./action-card/ActionCard";
|
||||
export * from "./alert/Alert";
|
||||
export * from "./chart/Chart";
|
||||
export * from "./dialog/Dialog";
|
||||
export * from "./dropdown/Dropdown";
|
||||
export * from "./header/Header";
|
||||
export * from "./label/Label";
|
||||
export * from "./select/Select";
|
||||
export * from "./sidebar";
|
||||
export * from "./table/SeverityBadge";
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "../../icons/prowler/ProwlerIcons";
|
||||
import { ThemeSwitch } from "../../ThemeSwitch";
|
||||
import Sidebar from "./Sidebar";
|
||||
import { sectionItemsWithTeams } from "./SidebarItems";
|
||||
import { sectionItems, sectionItemsWithTeams } from "./SidebarItems";
|
||||
import { UserAvatar } from "./UserAvatar";
|
||||
|
||||
export const SidebarWrap = () => {
|
||||
@@ -81,7 +81,11 @@ export const SidebarWrap = () => {
|
||||
<Sidebar
|
||||
defaultSelectedKey="overview"
|
||||
isCompact={isCompact}
|
||||
items={sectionItemsWithTeams}
|
||||
items={
|
||||
session?.user?.role === "admin"
|
||||
? sectionItemsWithTeams
|
||||
: sectionItems
|
||||
}
|
||||
selectedKeys={[currentPath]}
|
||||
/>
|
||||
</ScrollShadow>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Input } from "@nextui-org/react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui";
|
||||
|
||||
import { ButtonAddUser } from "./ButtonAddUser";
|
||||
import { CustomSelectUser } from "./CustomSelectUser";
|
||||
|
||||
export const AddUserModal = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button aria-label="Add User" variant="ghost">
|
||||
Add User
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
aria-describedby={undefined}
|
||||
className="flex flex-col sm:max-w-md md:max-w-lg"
|
||||
>
|
||||
<DialogHeader className="mb-6 space-y-3">
|
||||
<DialogTitle className="text-2xl text-center">Add User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form ref={ref} onSubmit={() => setOpen(false)}>
|
||||
<div className="col-span-1 flex flex-col gap-y-2 my-auto">
|
||||
<Input
|
||||
type="text"
|
||||
name="email"
|
||||
label="Email"
|
||||
labelPlacement="outside"
|
||||
placeholder="Email"
|
||||
aria-label="Enter Email"
|
||||
size="md"
|
||||
radius="md"
|
||||
isRequired
|
||||
fullWidth
|
||||
classNames={{
|
||||
base: "h-12 mb-4",
|
||||
inputWrapper: "h-full",
|
||||
}}
|
||||
/>
|
||||
<CustomSelectUser />
|
||||
</div>
|
||||
<div className="col-span-2 flex justify-center mt-4">
|
||||
<ButtonAddUser />
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
|
||||
export const ButtonAddUser = () => {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<Button type="submit" area-disabled={pending}>
|
||||
{pending ? "Adding..." : "Invite user"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
|
||||
export const ButtonEditUser = () => {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<Button type="submit" area-disabled={pending}>
|
||||
{pending ? "Saving..." : "Edit user"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui";
|
||||
import { UserProps } from "@/types";
|
||||
|
||||
interface CustomSelectUserProps {
|
||||
userData?: UserProps;
|
||||
}
|
||||
|
||||
export const CustomSelectUser: React.FC<CustomSelectUserProps> = ({
|
||||
userData,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Label htmlFor="role">
|
||||
Select a role<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select>
|
||||
<SelectTrigger className="shadow-none h-12 rounded-xl bg-zinc-100 border-transparent text-foreground-500 focus:ring-2 focus:ring-blue-600 ring-0 hover:bg-zinc-200 focus:border-2 focus:border-white">
|
||||
<SelectValue
|
||||
placeholder={(userData && userData?.role) || "Select a role"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectGroup className="[&>[data-highlighted]]:bg-transparent">
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@nextui-org/react";
|
||||
import { useRef } from "react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui";
|
||||
import { UserProps } from "@/types";
|
||||
|
||||
import { ButtonEditUser } from "./ButtonEditUser";
|
||||
import { CustomSelectUser } from "./CustomSelectUser";
|
||||
|
||||
interface EditUserModalProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
userData: UserProps;
|
||||
}
|
||||
|
||||
export const EditUserModal: React.FC<EditUserModalProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
userData,
|
||||
}) => {
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent
|
||||
aria-describedby={undefined}
|
||||
className="flex flex-col sm:max-w-md md:max-w-lg"
|
||||
>
|
||||
<DialogHeader className="mb-6 space-y-3">
|
||||
<DialogTitle className="text-2xl text-center">Edit User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form ref={ref} onSubmit={() => setIsOpen(false)}>
|
||||
<div className="col-span-1 flex flex-col gap-y-2 my-auto">
|
||||
<Input
|
||||
type="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
labelPlacement="outside"
|
||||
placeholder={userData?.name}
|
||||
aria-label="Enter Name"
|
||||
size="md"
|
||||
radius="md"
|
||||
isRequired
|
||||
fullWidth
|
||||
classNames={{
|
||||
base: "h-12 mb-4",
|
||||
inputWrapper: "h-full",
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
name="email"
|
||||
label="Email"
|
||||
labelPlacement="outside"
|
||||
placeholder={userData?.email}
|
||||
aria-label="Enter Email"
|
||||
size="md"
|
||||
radius="md"
|
||||
isRequired
|
||||
fullWidth
|
||||
classNames={{
|
||||
base: "h-12 mb-4",
|
||||
inputWrapper: "h-full",
|
||||
}}
|
||||
/>
|
||||
<CustomSelectUser userData={userData} />
|
||||
</div>
|
||||
<div className="col-span-2 flex justify-center mt-4">
|
||||
<ButtonEditUser />
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,11 @@
|
||||
export * from "./AddUserModal";
|
||||
export * from "./ButtonAddUser";
|
||||
export * from "./ButtonEditUser";
|
||||
export * from "./CustomSelectUser";
|
||||
export * from "./EditUserModal";
|
||||
export * from "./table/ColumnsUser";
|
||||
export * from "./table/DataTableColumnHeader";
|
||||
export * from "./table/DataTablePagination";
|
||||
export * from "./table/DataTableUser";
|
||||
export * from "./table/SkeletonTableUser";
|
||||
export * from "./table/UserActions";
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
} from "@nextui-org/react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { VerticalDotsIcon } from "@/components/icons";
|
||||
import { DateWithTime } from "@/components/providers";
|
||||
import { StatusBadge } from "@/components/ui";
|
||||
import { UserActions } from "@/components/users";
|
||||
import { UserProps } from "@/types";
|
||||
|
||||
const getUserData = (row: { original: UserProps }) => {
|
||||
@@ -63,21 +56,9 @@ export const ColumnsUser: ColumnDef<UserProps>[] = [
|
||||
accessorKey: "actions",
|
||||
header: () => <div className="text-right">Actions</div>,
|
||||
id: "actions",
|
||||
cell: () => {
|
||||
return (
|
||||
<div className="relative flex justify-end items-center gap-2">
|
||||
<Dropdown className="bg-background border-1 border-default-200">
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly radius="full" size="sm" variant="light">
|
||||
<VerticalDotsIcon className="text-default-400" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem>Edit</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
cell: ({ row }) => {
|
||||
const userData = getUserData(row);
|
||||
return <UserActions userData={userData} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { VerticalDotsIcon } from "@/components/icons";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui";
|
||||
import { EditUserModal } from "@/components/users";
|
||||
import { UserProps } from "@/types";
|
||||
|
||||
interface UserActionsProps {
|
||||
userData: UserProps;
|
||||
}
|
||||
|
||||
export const UserActions: React.FC<UserActionsProps> = ({ userData }) => {
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex justify-end items-center gap-2">
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger>
|
||||
<VerticalDotsIcon
|
||||
size={28}
|
||||
className="text-default-400 p-0.5 hover:bg-gray-200 hover:rounded-full"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-white">
|
||||
<DropdownMenuItem
|
||||
className="hover:bg-gray-200 hover:cursor-pointer"
|
||||
onClick={() => setIsEditOpen(true)}
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="hover:bg-gray-200 hover:cursor-pointer">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{isEditOpen && (
|
||||
<EditUserModal
|
||||
isOpen={isEditOpen}
|
||||
setIsOpen={setIsEditOpen}
|
||||
userData={userData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Generated
+127
@@ -13,6 +13,7 @@
|
||||
"@nextui-org/system": "2.2.1",
|
||||
"@nextui-org/theme": "2.2.5",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
@@ -2886,6 +2887,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz",
|
||||
"integrity": "sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-menu": "2.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz",
|
||||
@@ -2953,6 +2983,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz",
|
||||
"integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0"
|
||||
},
|
||||
@@ -2971,6 +3002,71 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz",
|
||||
"integrity": "sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-collection": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.0",
|
||||
"@radix-ui/react-focus-guards": "1.1.0",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.0",
|
||||
"@radix-ui/react-portal": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-roving-focus": "1.1.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll": {
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
|
||||
"integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.4",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||
@@ -3070,6 +3166,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
|
||||
"integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-collection": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"@nextui-org/system": "2.2.1",
|
||||
"@nextui-org/theme": "2.2.5",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
|
||||
Reference in New Issue
Block a user