From c910167ff6e438f2af440bdce73ad5b3bd134512 Mon Sep 17 00:00:00 2001 From: Sophia Dao Date: Fri, 23 Aug 2024 09:44:48 -0500 Subject: [PATCH 1/2] Users Page - Table Row (#43) * feat(users): Add in Users page and sidebar * feat(users): Fix grammar, add in Users action * feat(users): Add in more API info * feat(users): Continue work on table, pass data through to table, style skeleton * feat(users): Format Status column * feat(users): Style table * feat(users): Change data, update Users to User --- actions/compliance.ts | 2 +- actions/providers.ts | 2 +- actions/services.ts | 2 +- actions/users.ts | 39 +++++++ app/(prowler)/users/page.tsx | 43 +++++++ app/api/users/route.ts | 10 ++ components/ui/sidebar/SidebarItems.tsx | 4 +- components/ui/table/StatusBadge.tsx | 6 +- components/users/index.ts | 5 + components/users/table/ColumnsUser.tsx | 83 ++++++++++++++ .../users/table/DataTableColumnHeader.tsx | 49 ++++++++ .../users/table/DataTablePagination.tsx | 85 ++++++++++++++ components/users/table/DataTableUser.tsx | 106 ++++++++++++++++++ components/users/table/SkeletonTableUser.tsx | 59 ++++++++++ dataUsers.json | 98 ++++++++++++++++ types/components.ts | 9 ++ 16 files changed, 596 insertions(+), 6 deletions(-) create mode 100644 actions/users.ts create mode 100644 app/(prowler)/users/page.tsx create mode 100644 app/api/users/route.ts create mode 100644 components/users/index.ts create mode 100644 components/users/table/ColumnsUser.tsx create mode 100644 components/users/table/DataTableColumnHeader.tsx create mode 100644 components/users/table/DataTablePagination.tsx create mode 100644 components/users/table/DataTableUser.tsx create mode 100644 components/users/table/SkeletonTableUser.tsx create mode 100644 dataUsers.json diff --git a/actions/compliance.ts b/actions/compliance.ts index 76064ebb33..379e71791f 100644 --- a/actions/compliance.ts +++ b/actions/compliance.ts @@ -33,7 +33,7 @@ export const getErrorMessage = (error: unknown): string => { } else if (typeof error === "string") { message = error; } else { - message = "Wops! Something when wrong."; + message = "Oops! Something went wrong."; } return message; }; diff --git a/actions/providers.ts b/actions/providers.ts index 6c6f3adee7..6892e8c922 100644 --- a/actions/providers.ts +++ b/actions/providers.ts @@ -119,7 +119,7 @@ export const getErrorMessage = (error: unknown): string => { } else if (typeof error === "string") { message = error; } else { - message = "Wops! Something when wrong."; + message = "Oops! Something went wrong."; } return message; }; diff --git a/actions/services.ts b/actions/services.ts index aeb9d7de07..dd19288a26 100644 --- a/actions/services.ts +++ b/actions/services.ts @@ -33,7 +33,7 @@ export const getErrorMessage = (error: unknown): string => { } else if (typeof error === "string") { message = error; } else { - message = "Wops! Something when wrong."; + message = "Oops! Something went wrong."; } return message; }; diff --git a/actions/users.ts b/actions/users.ts new file mode 100644 index 0000000000..02717f287c --- /dev/null +++ b/actions/users.ts @@ -0,0 +1,39 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +import { parseStringify } from "@/lib"; + +export const getUsers = async ({ page = 1 }) => { + if (isNaN(Number(page)) || page < 1) redirect("/users"); + const keyServer = process.env.LOCAL_SITE_URL; + + try { + const users = await fetch( + `${keyServer}/api/users?page%5Bnumber%5D=${page}`, + ); + const data = await users.json(); + const parsedData = parseStringify(data); + revalidatePath("/users"); + return parsedData; + } catch (error) { + console.error("Error fetching Users:", error); + return undefined; + } +}; + +export const getErrorMessage = (error: unknown): string => { + let message: string; + + if (error instanceof Error) { + message = error.message; + } else if (error && typeof error === "object" && "message" in error) { + message = String(error.message); + } else if (typeof error === "string") { + message = error; + } else { + message = "Oops! Something went wrong."; + } + return message; +}; diff --git a/app/(prowler)/users/page.tsx b/app/(prowler)/users/page.tsx new file mode 100644 index 0000000000..7c1549b2cf --- /dev/null +++ b/app/(prowler)/users/page.tsx @@ -0,0 +1,43 @@ +import { Spacer } from "@nextui-org/react"; +import { redirect } from "next/navigation"; +import { Suspense } from "react"; + +import { getUsers } from "@/actions/users"; +import { Header } from "@/components/ui"; +import { + ColumnsUser, + DataTableUser, + SkeletonTableUser, +} from "@/components/users"; +import { searchParamsProps } from "@/types"; + +export default async function Users({ searchParams }: searchParamsProps) { + return ( + <> +
+ +
+ + }> + + +
+ + ); +} + +const SSRDataTable = async ({ searchParams }: searchParamsProps) => { + const page = searchParams.page ? parseInt(searchParams.page) : 1; + const usersData = await getUsers({ page }); + const [users] = await Promise.all([usersData]); + + if (users?.errors) redirect("/users"); + + return ( + + ); +}; diff --git a/app/api/users/route.ts b/app/api/users/route.ts new file mode 100644 index 0000000000..9905b5c8c9 --- /dev/null +++ b/app/api/users/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server"; + +import data from "../../../dataUsers.json"; + +export async function GET() { + // Simulate fetching data with a delay + await new Promise((resolve) => setTimeout(resolve, 2000)); + + return NextResponse.json({ users: data }); +} diff --git a/components/ui/sidebar/SidebarItems.tsx b/components/ui/sidebar/SidebarItems.tsx index b4b896033d..20cf4d3533 100644 --- a/components/ui/sidebar/SidebarItems.tsx +++ b/components/ui/sidebar/SidebarItems.tsx @@ -213,9 +213,9 @@ export const sectionItemsWithTeams: SidebarItem[] = [ items: [ { key: "users", - href: "#", + href: "/users", title: "Users", - startContent: , + icon: "ci:users", }, { key: "roles", diff --git a/components/ui/table/StatusBadge.tsx b/components/ui/table/StatusBadge.tsx index c1f667f26e..99a2f52e60 100644 --- a/components/ui/table/StatusBadge.tsx +++ b/components/ui/table/StatusBadge.tsx @@ -7,7 +7,9 @@ type Status = | "cancelled" | "fail" | "success" - | "muted"; + | "muted" + | "active" + | "inactive"; const statusColorMap: Record< Status, @@ -19,6 +21,8 @@ const statusColorMap: Record< fail: "danger", success: "success", muted: "default", + active: "success", + inactive: "default", }; export const StatusBadge = ({ status }: { status: Status }) => { diff --git a/components/users/index.ts b/components/users/index.ts new file mode 100644 index 0000000000..a458fa1122 --- /dev/null +++ b/components/users/index.ts @@ -0,0 +1,5 @@ +export * from "./table/ColumnsUser"; +export * from "./table/DataTableColumnHeader"; +export * from "./table/DataTablePagination"; +export * from "./table/DataTableUser"; +export * from "./table/SkeletonTableUser"; diff --git a/components/users/table/ColumnsUser.tsx b/components/users/table/ColumnsUser.tsx new file mode 100644 index 0000000000..4dbf8001f0 --- /dev/null +++ b/components/users/table/ColumnsUser.tsx @@ -0,0 +1,83 @@ +"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 { UserProps } from "@/types"; + +const getUserData = (row: { original: UserProps }) => { + return row.original; +}; + +export const ColumnsUser: ColumnDef[] = [ + { + accessorKey: "email", + header: "Email", + cell: ({ row }) => { + const { email } = getUserData(row); + return

{email}

; + }, + }, + { + accessorKey: "name", + header: "Name", + cell: ({ row }) => { + const { name } = getUserData(row); + return

{name}

; + }, + }, + { + accessorKey: "role", + header: "Role", + cell: ({ row }) => { + const { role } = getUserData(row); + return

{role}

; + }, + }, + { + accessorKey: "added", + header: "Added", + cell: ({ row }) => { + const { dateAdded } = getUserData(row); + return ; + }, + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => { + const { status } = getUserData(row); + return ; + }, + }, + { + accessorKey: "actions", + header: () =>
Actions
, + id: "actions", + cell: () => { + return ( +
+ + + + + + Edit + + +
+ ); + }, + }, +]; diff --git a/components/users/table/DataTableColumnHeader.tsx b/components/users/table/DataTableColumnHeader.tsx new file mode 100644 index 0000000000..7fe6027527 --- /dev/null +++ b/components/users/table/DataTableColumnHeader.tsx @@ -0,0 +1,49 @@ +import { Button } from "@nextui-org/react"; +import { Column } from "@tanstack/react-table"; +import { + ArrowDownIcon, + ArrowUpIcon, + ChevronsLeftRightIcon, +} from "lucide-react"; +import { HTMLAttributes } from "react"; + +interface DataTableColumnHeaderProps + extends HTMLAttributes { + column: Column; + title: string; +} + +export const DataTableColumnHeader = ({ + column, + title, + className, +}: DataTableColumnHeaderProps) => { + const renderSortIcon = () => { + const sort = column.getIsSorted(); + if (!sort) { + return ; + } + return sort === "desc" ? ( + + ) : ( + + ); + }; + + if (!column.getCanSort()) { + return
{title}
; + } + return ( +
+ +
+ ); +}; diff --git a/components/users/table/DataTablePagination.tsx b/components/users/table/DataTablePagination.tsx new file mode 100644 index 0000000000..0e195f0507 --- /dev/null +++ b/components/users/table/DataTablePagination.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { + ChevronLeftIcon, + ChevronRightIcon, + DoubleArrowLeftIcon, + DoubleArrowRightIcon, +} from "@radix-ui/react-icons"; +import Link from "next/link"; +import { usePathname, useSearchParams } from "next/navigation"; + +import { extractPaginationInfo } from "@/lib"; +import { MetaDataProps } from "@/types"; + +interface DataTablePaginationProps { + pageSizeOptions?: number[]; + metadata?: MetaDataProps; +} + +export function DataTablePagination({ metadata }: DataTablePaginationProps) { + if (!metadata) return null; + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const { currentPage, totalPages, totalEntries } = + extractPaginationInfo(metadata); + + const createPageUrl = (pageNumber: number | string) => { + const params = new URLSearchParams(searchParams); + + if (pageNumber === "...") return `${pathname}?${params.toString()}`; + + if (+pageNumber > totalPages) { + return `${pathname}?${params.toString()}`; + } + + params.set("page", pageNumber.toString()); + return `${pathname}?${params.toString()}`; + }; + + return ( +
+
+ {totalEntries} entries in Total. +
+
+
+ Page {currentPage} of {totalPages} +
+
+ +
+
+
+ ); +} diff --git a/components/users/table/DataTableUser.tsx b/components/users/table/DataTableUser.tsx new file mode 100644 index 0000000000..7b5c6a5049 --- /dev/null +++ b/components/users/table/DataTableUser.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, +} from "@tanstack/react-table"; +import { useState } from "react"; + +import { DataTablePagination } from "@/components/providers"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui"; +import { MetaDataProps } from "@/types"; + +interface DataTableUserProps { + columns: ColumnDef[]; + data: TData[]; + metadata?: MetaDataProps; +} + +export function DataTableUser({ + columns, + data, + metadata, +}: DataTableUserProps) { + const [sorting, setSorting] = useState([]); + const table = useReactTable({ + data, + columns, + enableSorting: true, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + state: { + sorting, + }, + }); + return ( + <> +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+ +
+ + ); +} diff --git a/components/users/table/SkeletonTableUser.tsx b/components/users/table/SkeletonTableUser.tsx new file mode 100644 index 0000000000..cf4a584428 --- /dev/null +++ b/components/users/table/SkeletonTableUser.tsx @@ -0,0 +1,59 @@ +import { Card, Skeleton } from "@nextui-org/react"; +import React from "react"; + +export const SkeletonTableUser = () => { + return ( + + {/* Table headers */} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* Table body */} +
+ {[...Array(10)].map((_, index) => ( +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ))} +
+
+ ); +}; diff --git a/dataUsers.json b/dataUsers.json new file mode 100644 index 0000000000..26428a633a --- /dev/null +++ b/dataUsers.json @@ -0,0 +1,98 @@ +{ + "links": { + "first": "http://localhost:8080/api/v1/users?page%5Bnumber%5D=1", + "last": "http://localhost:8080/api/v1/users?page%5Bnumber%5D=1", + "next": null, + "prev": null + }, + "data": [ + { + "id": "001", + "email": "john.doe@example.com", + "name": "John Doe", + "role": "Admin", + "dateAdded": "2024-08-14T10:00:00.000000Z", + "status": "active" + }, + { + "id": "002", + "email": "jane.smith@example.com", + "name": "Jane Smith", + "role": "User", + "dateAdded": "2024-08-15T11:30:00.000000Z", + "status": "inactive" + }, + { + "id": "003", + "email": "will.johnson@example.com", + "name": "Will Johnson", + "role": "Admin", + "dateAdded": "2024-08-16T09:15:00.000000Z", + "status": "active" + }, + { + "id": "004", + "email": "emily.davis@example.com", + "name": "Emily Davis", + "role": "User", + "dateAdded": "2024-08-17T14:00:00.000000Z", + "status": "active" + }, + { + "id": "005", + "email": "michael.brown@example.com", + "name": "Michael Brown", + "role": "User", + "dateAdded": "2024-08-18T08:45:00.000000Z", + "status": "inactive" + }, + { + "id": "006", + "email": "sarah.miller@example.com", + "name": "Sarah Miller", + "role": "Admin", + "dateAdded": "2024-08-19T13:25:00.000000Z", + "status": "active" + }, + { + "id": "007", + "email": "david.wilson@example.com", + "name": "David Wilson", + "role": "User", + "dateAdded": "2024-08-20T10:50:00.000000Z", + "status": "active" + }, + { + "id": "008", + "email": "lisa.moore@example.com", + "name": "Lisa Moore", + "role": "Admin", + "dateAdded": "2024-08-21T07:30:00.000000Z", + "status": "inactive" + }, + { + "id": "009", + "email": "james.taylor@example.com", + "name": "James Taylor", + "role": "User", + "dateAdded": "2024-08-22T12:10:00.000000Z", + "status": "active" + }, + { + "id": "010", + "email": "anna.anderson@example.com", + "name": "Anna Anderson", + "role": "User", + "dateAdded": "2024-08-23T11:00:00.000000Z", + "status": "inactive" + } + ], + "meta": { + "pagination": { + "page": 1, + "pages": 1, + "count": 10 + }, + "version": "v1" + } +} diff --git a/types/components.ts b/types/components.ts index 5580d151eb..0fc88a2190 100644 --- a/types/components.ts +++ b/types/components.ts @@ -80,3 +80,12 @@ export interface MetaDataProps { }; version: string; } + +export interface UserProps { + id: string; + email: string; + name: string; + role: string; + dateAdded: string; + status: "active" | "inactive"; +} From 440e95515a01fbad63cd5616b4b807e3893198cb Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 26 Aug 2024 15:33:07 +0200 Subject: [PATCH 2/2] feat: add new items to the main menu --- app/(prowler)/categories/page.tsx | 12 ++++++++ .../{integration => integrations}/page.tsx | 4 +-- app/(prowler)/workloads/page.tsx | 12 ++++++++ components/ui/sidebar/SidebarItems.tsx | 30 +++++++++++++------ 4 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 app/(prowler)/categories/page.tsx rename app/(prowler)/{integration => integrations}/page.tsx (59%) create mode 100644 app/(prowler)/workloads/page.tsx diff --git a/app/(prowler)/categories/page.tsx b/app/(prowler)/categories/page.tsx new file mode 100644 index 0000000000..1a5650d8af --- /dev/null +++ b/app/(prowler)/categories/page.tsx @@ -0,0 +1,12 @@ +import { Spacer } from "@nextui-org/react"; + +import { Header } from "@/components/ui"; + +export default async function Categories() { + return ( + <> +
+ + + ); +} diff --git a/app/(prowler)/integration/page.tsx b/app/(prowler)/integrations/page.tsx similarity index 59% rename from app/(prowler)/integration/page.tsx rename to app/(prowler)/integrations/page.tsx index 576089371b..a930f1ed36 100644 --- a/app/(prowler)/integration/page.tsx +++ b/app/(prowler)/integrations/page.tsx @@ -2,10 +2,10 @@ import React from "react"; import { Header } from "@/components/ui"; -export default function Integration() { +export default function Integrations() { return ( <> -
+

Hi hi from Integration page

diff --git a/app/(prowler)/workloads/page.tsx b/app/(prowler)/workloads/page.tsx new file mode 100644 index 0000000000..3cadd0d1ca --- /dev/null +++ b/app/(prowler)/workloads/page.tsx @@ -0,0 +1,12 @@ +import { Spacer } from "@nextui-org/react"; + +import { Header } from "@/components/ui"; + +export default async function Workloads() { + return ( + <> +
+ + + ); +} diff --git a/components/ui/sidebar/SidebarItems.tsx b/components/ui/sidebar/SidebarItems.tsx index 20cf4d3533..43f01dfa66 100644 --- a/components/ui/sidebar/SidebarItems.tsx +++ b/components/ui/sidebar/SidebarItems.tsx @@ -126,12 +126,6 @@ export const sectionItems: SidebarItem[] = [ // /> // ), // }, - { - key: "services", - href: "/services", - icon: "material-symbols:linked-services-outline", - title: "Services", - }, { key: "compliance", href: "/compliance", @@ -143,6 +137,24 @@ export const sectionItems: SidebarItem[] = [ ), }, + { + key: "services", + href: "/services", + icon: "material-symbols:linked-services-outline", + title: "Services", + }, + { + key: "categories", + href: "/categories", + icon: "material-symbols:folder-open-outline", + title: "Categories", + }, + { + key: "workloads", + href: "/workloads", + icon: "lucide:tags", + title: "Workloads", + }, ], }, { @@ -190,10 +202,10 @@ export const sectionItems: SidebarItem[] = [ ), }, { - key: "integration", - href: "/integration", + key: "integrations", + href: "/integrations", icon: "tabler:puzzle", - title: "Integration", + title: "Integrations", }, { key: "settings",