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)/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)/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/(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/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..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",
@@ -213,9 +225,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";
+}