diff --git a/ui/actions/roles/index.ts b/ui/actions/roles/index.ts new file mode 100644 index 0000000000..70310e7da4 --- /dev/null +++ b/ui/actions/roles/index.ts @@ -0,0 +1 @@ +export * from "./roles"; diff --git a/ui/actions/roles/roles.ts b/ui/actions/roles/roles.ts new file mode 100644 index 0000000000..39aac321eb --- /dev/null +++ b/ui/actions/roles/roles.ts @@ -0,0 +1,174 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +import { auth } from "@/auth.config"; +import { parseStringify } from "@/lib"; + +export const getRoles = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}) => { + const session = await auth(); + + if (isNaN(Number(page)) || page < 1) redirect("/roles"); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/roles`); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } + }); + + try { + const invitations = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + const data = await invitations.json(); + const parsedData = parseStringify(data); + revalidatePath("/roles"); + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching roles:", error); + return undefined; + } +}; + +// export const sendInvite = async (formData: FormData) => { +// const session = await auth(); +// const keyServer = process.env.API_BASE_URL; + +// const email = formData.get("email"); +// const url = new URL(`${keyServer}/tenants/invitations`); + +// const body = JSON.stringify({ +// data: { +// type: "invitations", +// attributes: { +// email, +// }, +// relationships: {}, +// }, +// }); + +// try { +// const response = await fetch(url.toString(), { +// method: "POST", +// headers: { +// "Content-Type": "application/vnd.api+json", +// Accept: "application/vnd.api+json", +// Authorization: `Bearer ${session?.accessToken}`, +// }, +// body, +// }); +// const data = await response.json(); + +// return parseStringify(data); +// } catch (error) { +// return { +// error: getErrorMessage(error), +// }; +// } +// }; + +// export const updateInvite = async (formData: FormData) => { +// const session = await auth(); +// const keyServer = process.env.API_BASE_URL; + +// const invitationId = formData.get("invitationId"); +// const invitationEmail = formData.get("invitationEmail"); +// const expiresAt = formData.get("expires_at"); + +// const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`); + +// try { +// const response = await fetch(url.toString(), { +// method: "PATCH", +// headers: { +// "Content-Type": "application/vnd.api+json", +// Accept: "application/vnd.api+json", +// Authorization: `Bearer ${session?.accessToken}`, +// }, +// body: JSON.stringify({ +// data: { +// type: "invitations", +// id: invitationId, +// attributes: { +// email: invitationEmail, +// ...(expiresAt && { expires_at: expiresAt }), +// }, +// }, +// }), +// }); +// const data = await response.json(); +// revalidatePath("/invitations"); +// return parseStringify(data); +// } catch (error) { +// // eslint-disable-next-line no-console +// console.error("Error updating invitation:", error); +// return { +// error: getErrorMessage(error), +// }; +// } +// }; + +// export const getInvitationInfoById = async (invitationId: string) => { +// const session = await auth(); +// const keyServer = process.env.API_BASE_URL; +// const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`); + +// try { +// const response = await fetch(url.toString(), { +// method: "GET", +// headers: { +// Accept: "application/vnd.api+json", +// Authorization: `Bearer ${session?.accessToken}`, +// }, +// }); + +// const data = await response.json(); +// return parseStringify(data); +// } catch (error) { +// return { +// error: getErrorMessage(error), +// }; +// } +// }; + +// export const revokeInvite = async (formData: FormData) => { +// const session = await auth(); +// const keyServer = process.env.API_BASE_URL; + +// const invitationId = formData.get("invitationId"); +// const url = new URL(`${keyServer}/tenants/invitations/${invitationId}`); +// try { +// const response = await fetch(url.toString(), { +// method: "DELETE", +// headers: { +// Authorization: `Bearer ${session?.accessToken}`, +// }, +// }); +// const data = await response.json(); +// await wait(1000); +// revalidatePath("/invitations"); +// return parseStringify(data); +// } catch (error) { +// return { +// error: getErrorMessage(error), +// }; +// } +// }; diff --git a/ui/app/(prowler)/roles/page.tsx b/ui/app/(prowler)/roles/page.tsx new file mode 100644 index 0000000000..07b77bf7e6 --- /dev/null +++ b/ui/app/(prowler)/roles/page.tsx @@ -0,0 +1,64 @@ +import { Spacer } from "@nextui-org/react"; +import { Suspense } from "react"; + +import { getRoles } from "@/actions/roles"; +import { FilterControls } from "@/components/filters"; +import { filterInvitations } from "@/components/filters/data-filters"; +import { AddRoleButton } from "@/components/roles"; +import { ColumnsRoles } from "@/components/roles/table"; +import { SkeletonTableRoles } from "@/components/roles/table"; +import { Header } from "@/components/ui"; +import { DataTable, DataTableFilterCustom } from "@/components/ui/table"; +import { SearchParamsProps } from "@/types"; + +export default async function Roles({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { + const searchParamsKey = JSON.stringify(searchParams || {}); + + return ( + <> +
+ + + + + + + + + }> + + + + ); +} + +const SSRDataTable = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const sort = searchParams.sort?.toString(); + + // Extract all filter parameters + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + // Extract query from filters + const query = (filters["filter[search]"] as string) || ""; + + const rolesData = await getRoles({ query, page, sort, filters }); + + return ( + + ); +}; diff --git a/ui/components/roles/add-role-button.tsx b/ui/components/roles/add-role-button.tsx new file mode 100644 index 0000000000..3ac3b67126 --- /dev/null +++ b/ui/components/roles/add-role-button.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { AddIcon } from "../icons"; +import { CustomButton } from "../ui/custom"; + +export const AddRoleButton = () => { + return ( +
+ } + > + Add Role + +
+ ); +}; diff --git a/ui/components/roles/index.ts b/ui/components/roles/index.ts new file mode 100644 index 0000000000..27454e73fd --- /dev/null +++ b/ui/components/roles/index.ts @@ -0,0 +1 @@ +export * from "./add-role-button"; diff --git a/ui/components/roles/table/column-roles.tsx b/ui/components/roles/table/column-roles.tsx new file mode 100644 index 0000000000..ba044e05a2 --- /dev/null +++ b/ui/components/roles/table/column-roles.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; + +import { DateWithTime } from "@/components/ui/entities"; +import { DataTableColumnHeader } from "@/components/ui/table"; +import { RolesProps } from "@/types"; + +const getRoleAttributes = (row: { original: RolesProps["data"][number] }) => { + return row.original.attributes; +}; + +const getRoleRelationships = (row: { + original: RolesProps["data"][number]; +}) => { + return row.original.relationships; +}; + +export const ColumnsRoles: ColumnDef[] = [ + { + accessorKey: "role", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const data = getRoleAttributes(row); + return ( +

+ {data.name[0].toUpperCase() + data.name.slice(1).toLowerCase()} +

+ ); + }, + }, + { + accessorKey: "users", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const relationships = getRoleRelationships(row); + const count = relationships.users.meta.count; + return ( +

+ {count === 0 + ? "No Users" + : `${count} ${count === 1 ? "User" : "Users"}`} +

+ ); + }, + }, + { + accessorKey: "invitations", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const relationships = getRoleRelationships(row); + return ( +

{relationships.invitations.meta.count}

+ ); + }, + }, + { + accessorKey: "updated_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { updated_at } = getRoleAttributes(row); + return ; + }, + }, + { + accessorKey: "inserted_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { inserted_at } = getRoleAttributes(row); + return ; + }, + }, +]; diff --git a/ui/components/roles/table/data-table-row-actions.tsx b/ui/components/roles/table/data-table-row-actions.tsx new file mode 100644 index 0000000000..4c7280cbfc --- /dev/null +++ b/ui/components/roles/table/data-table-row-actions.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { + Button, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownSection, + DropdownTrigger, +} from "@nextui-org/react"; +import { + AddNoteBulkIcon, + DeleteDocumentBulkIcon, + EditDocumentBulkIcon, +} from "@nextui-org/shared-icons"; +import { Row } from "@tanstack/react-table"; +import clsx from "clsx"; + +import { VerticalDotsIcon } from "@/components/icons"; + +// import { DeleteForm, EditForm } from "../forms"; + +interface DataTableRowActionsProps { + row: Row; +} +const iconClasses = + "text-2xl text-default-500 pointer-events-none flex-shrink-0"; + +export function DataTableRowActions({ + row, +}: DataTableRowActionsProps) { + // const [isEditOpen, setIsEditOpen] = useState(false); + // const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const invitationId = (row.original as { id: string }).id; + return ( + <> + {/* + + */} + {/* + + */} + +
+ + + + + + + } + > + Check Details + + + } + // onClick={() => setIsEditOpen(true)} + > + Edit Invitation + + + + + } + // onClick={() => setIsDeleteOpen(true)} + > + Revoke Invitation + + + + +
+ + ); +} diff --git a/ui/components/roles/table/index.ts b/ui/components/roles/table/index.ts new file mode 100644 index 0000000000..a446d9fa77 --- /dev/null +++ b/ui/components/roles/table/index.ts @@ -0,0 +1,3 @@ +export * from "./column-roles"; +export * from "./data-table-row-actions"; +export * from "./skeleton-table-roles"; diff --git a/ui/components/roles/table/skeleton-table-roles.tsx b/ui/components/roles/table/skeleton-table-roles.tsx new file mode 100644 index 0000000000..24fd218fa6 --- /dev/null +++ b/ui/components/roles/table/skeleton-table-roles.tsx @@ -0,0 +1,59 @@ +import { Card, Skeleton } from "@nextui-org/react"; +import React from "react"; + +export const SkeletonTableRoles = () => { + return ( + + {/* Table headers */} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* Table body */} +
+ {[...Array(10)].map((_, index) => ( +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ))} +
+
+ ); +}; diff --git a/ui/types/components.ts b/ui/types/components.ts index fdabefb683..77c8785747 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -227,6 +227,71 @@ export interface InvitationProps { self: string; }; } + +export interface RolesProps { + links: { + first: string; + last: string; + next: string | null; + prev: string | null; + }; + data: { + type: "role"; + id: string; + attributes: { + name: string; + manage_users: boolean; + manage_account: boolean; + manage_billing: boolean; + manage_providers: boolean; + manage_integrations: boolean; + manage_scans: boolean; + unlimited_visibility: boolean; + inserted_at: string; + updated_at: string; + }; + relationships: { + provider_groups: { + meta: { + count: number; + }; + data: { + type: string; + id: string; + }[]; + }; + users: { + meta: { + count: number; + }; + data: { + type: string; + id: string; + }[]; + }; + invitations: { + meta: { + count: number; + }; + data: { + type: string; + id: string; + }[]; + }; + }; + links: { + self: string; + }; + }[]; + meta: { + pagination: { + page: number; + pages: number; + count: number; + }; + version: string; + }; +} export interface UserProfileProps { data: { type: "users";