diff --git a/app/(prowler)/users/page.tsx b/app/(prowler)/users/page.tsx index 7c1549b2cf..cff19b91e0 100644 --- a/app/(prowler)/users/page.tsx +++ b/app/(prowler)/users/page.tsx @@ -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 ( <>
+
+ +
}> diff --git a/components/ui/dropdown/Dropdown.tsx b/components/ui/dropdown/Dropdown.tsx new file mode 100644 index 0000000000..b88da41b59 --- /dev/null +++ b/components/ui/dropdown/Dropdown.tsx @@ -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, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; diff --git a/components/ui/index.ts b/components/ui/index.ts index f026779b96..70705ba015 100644 --- a/components/ui/index.ts +++ b/components/ui/index.ts @@ -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"; diff --git a/components/ui/label/Label.tsx b/components/ui/label/Label.tsx new file mode 100644 index 0000000000..9bdf1a5044 --- /dev/null +++ b/components/ui/label/Label.tsx @@ -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, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/components/ui/sidebar/SidebarWrap.tsx b/components/ui/sidebar/SidebarWrap.tsx index 201cecdecf..e584ca1043 100644 --- a/components/ui/sidebar/SidebarWrap.tsx +++ b/components/ui/sidebar/SidebarWrap.tsx @@ -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 = () => { diff --git a/components/users/AddUserModal.tsx b/components/users/AddUserModal.tsx new file mode 100644 index 0000000000..49d4b70dd9 --- /dev/null +++ b/components/users/AddUserModal.tsx @@ -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(null); + + return ( + + + + + + + Add User + +
setOpen(false)}> +
+ + +
+
+ +
+
+
+
+ ); +}; diff --git a/components/users/ButtonAddUser.tsx b/components/users/ButtonAddUser.tsx new file mode 100644 index 0000000000..e840dc0157 --- /dev/null +++ b/components/users/ButtonAddUser.tsx @@ -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 ( + + ); +}; diff --git a/components/users/ButtonEditUser.tsx b/components/users/ButtonEditUser.tsx new file mode 100644 index 0000000000..a0322b7a2d --- /dev/null +++ b/components/users/ButtonEditUser.tsx @@ -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 ( + + ); +}; diff --git a/components/users/CustomSelectUser.tsx b/components/users/CustomSelectUser.tsx new file mode 100644 index 0000000000..10acfc973c --- /dev/null +++ b/components/users/CustomSelectUser.tsx @@ -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 = ({ + userData, +}) => { + return ( + <> + + + + ); +}; diff --git a/components/users/EditUserModal.tsx b/components/users/EditUserModal.tsx new file mode 100644 index 0000000000..0d64c7f940 --- /dev/null +++ b/components/users/EditUserModal.tsx @@ -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>; + userData: UserProps; +} + +export const EditUserModal: React.FC = ({ + isOpen, + setIsOpen, + userData, +}) => { + const ref = useRef(null); + + return ( + + + + Edit User + +
setIsOpen(false)}> +
+ + + +
+
+ +
+
+
+
+ ); +}; diff --git a/components/users/index.ts b/components/users/index.ts index a458fa1122..36f6d8f557 100644 --- a/components/users/index.ts +++ b/components/users/index.ts @@ -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"; diff --git a/components/users/table/ColumnsUser.tsx b/components/users/table/ColumnsUser.tsx index 4dbf8001f0..70a05c189a 100644 --- a/components/users/table/ColumnsUser.tsx +++ b/components/users/table/ColumnsUser.tsx @@ -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[] = [ accessorKey: "actions", header: () =>
Actions
, id: "actions", - cell: () => { - return ( -
- - - - - - Edit - - -
- ); + cell: ({ row }) => { + const userData = getUserData(row); + return ; }, }, ]; diff --git a/components/users/table/UserActions.tsx b/components/users/table/UserActions.tsx new file mode 100644 index 0000000000..9ff00eda1d --- /dev/null +++ b/components/users/table/UserActions.tsx @@ -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 = ({ userData }) => { + const [isEditOpen, setIsEditOpen] = useState(false); + + return ( + <> +
+ + + + + + setIsEditOpen(true)} + > + Edit + + + Delete + + + +
+ {isEditOpen && ( + + )} + + ); +}; diff --git a/package-lock.json b/package-lock.json index 689b2cbfb6..320beae819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8646874078..07d2fb4d13 100644 --- a/package.json +++ b/package.json @@ -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",