diff --git a/ui/app/(prowler)/roles/(add-role)/layout.tsx b/ui/app/(prowler)/roles/(add-role)/layout.tsx
new file mode 100644
index 0000000000..fb96134552
--- /dev/null
+++ b/ui/app/(prowler)/roles/(add-role)/layout.tsx
@@ -0,0 +1,32 @@
+import "@/styles/globals.css";
+
+import { Spacer } from "@nextui-org/react";
+import React from "react";
+
+import { WorkflowAddEditRole } from "@/components/roles/workflow";
+import { NavigationHeader } from "@/components/ui";
+
+interface RoleLayoutProps {
+ children: React.ReactNode;
+}
+
+export default function RoleLayout({ children }: RoleLayoutProps) {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/ui/app/(prowler)/roles/(add-role)/new/page.tsx b/ui/app/(prowler)/roles/(add-role)/new/page.tsx
new file mode 100644
index 0000000000..d35fa0b719
--- /dev/null
+++ b/ui/app/(prowler)/roles/(add-role)/new/page.tsx
@@ -0,0 +1,7 @@
+import React from "react";
+
+import { AddRoleForm } from "@/components/roles/workflow/forms/add-role-form";
+
+export default function AddRolePage() {
+ return ;
+}
diff --git a/ui/components/roles/table/column-roles.tsx b/ui/components/roles/table/column-roles.tsx
index ba044e05a2..ff1a8a8e03 100644
--- a/ui/components/roles/table/column-roles.tsx
+++ b/ui/components/roles/table/column-roles.tsx
@@ -60,24 +60,32 @@ export const ColumnsRoles: ColumnDef[] = [
cell: ({ row }) => {
const relationships = getRoleRelationships(row);
return (
- {relationships.invitations.meta.count}
+
+ {relationships.invitations.meta.count === 0
+ ? "No Invitations"
+ : `${relationships.invitations.meta.count} ${
+ relationships.invitations.meta.count === 1
+ ? "Invitation"
+ : "Invitations"
+ }`}
+
);
},
},
- {
- accessorKey: "updated_at",
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => {
- const { updated_at } = getRoleAttributes(row);
- return ;
- },
- },
+ // {
+ // accessorKey: "updated_at",
+ // header: ({ column }) => (
+ //
+ // ),
+ // cell: ({ row }) => {
+ // const { updated_at } = getRoleAttributes(row);
+ // return ;
+ // },
+ // },
{
accessorKey: "inserted_at",
header: ({ column }) => (
diff --git a/ui/components/roles/workflow/forms/add-role-form.tsx b/ui/components/roles/workflow/forms/add-role-form.tsx
new file mode 100644
index 0000000000..d1afd99a78
--- /dev/null
+++ b/ui/components/roles/workflow/forms/add-role-form.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Checkbox } from "@nextui-org/react";
+import { SaveIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { addRole } from "@/actions/roles/roles";
+import { useToast } from "@/components/ui";
+import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { addRoleFormSchema, ApiError } from "@/types";
+
+export type FormValues = z.infer;
+
+export const AddRoleForm = () => {
+ const { toast } = useToast();
+ const router = useRouter();
+
+ const form = useForm({
+ resolver: zodResolver(addRoleFormSchema),
+ defaultValues: {
+ name: "",
+ manage_users: false,
+ manage_account: false,
+ manage_billing: false,
+ manage_providers: false,
+ manage_integrations: false,
+ manage_scans: false,
+ unlimited_visibility: false,
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormValues) => {
+ const formData = new FormData();
+ formData.append("name", values.name);
+ formData.append("manage_users", String(values.manage_users));
+ formData.append("manage_account", String(values.manage_account));
+ formData.append("manage_billing", String(values.manage_billing));
+ formData.append("manage_providers", String(values.manage_providers));
+ formData.append("manage_integrations", String(values.manage_integrations));
+ formData.append("manage_scans", String(values.manage_scans));
+ formData.append(
+ "unlimited_visibility",
+ String(values.unlimited_visibility),
+ );
+
+ try {
+ const data = await addRole(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ switch (error.source.pointer) {
+ case "/data/attributes/name":
+ form.setError("name", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: errorMessage,
+ });
+ }
+ });
+ } else {
+ toast({
+ variant: "success",
+ title: "Role Added",
+ description: "The role was added successfully.",
+ });
+ router.push("/roles");
+ }
+ } catch (error) {
+ toast({
+ variant: "destructive",
+ title: "Error",
+ description: "An unexpected error occurred. Please try again.",
+ });
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/roles/workflow/forms/index.ts b/ui/components/roles/workflow/forms/index.ts
new file mode 100644
index 0000000000..c50e093cc0
--- /dev/null
+++ b/ui/components/roles/workflow/forms/index.ts
@@ -0,0 +1 @@
+export * from "./add-role-form";
diff --git a/ui/components/roles/workflow/index.ts b/ui/components/roles/workflow/index.ts
new file mode 100644
index 0000000000..4a4f199ff7
--- /dev/null
+++ b/ui/components/roles/workflow/index.ts
@@ -0,0 +1,3 @@
+export * from "./skeleton-role-form";
+export * from "./vertical-steps";
+export * from "./workflow-add-edit-role";
diff --git a/ui/components/roles/workflow/skeleton-role-form.tsx b/ui/components/roles/workflow/skeleton-role-form.tsx
new file mode 100644
index 0000000000..839efb1ba0
--- /dev/null
+++ b/ui/components/roles/workflow/skeleton-role-form.tsx
@@ -0,0 +1,65 @@
+import { Card, Skeleton } from "@nextui-org/react";
+import React from "react";
+
+export const SkeletonRoleForm = () => {
+ return (
+
+ {/* Table headers */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Table body */}
+
+ {[...Array(3)].map((_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/roles/workflow/vertical-steps.tsx b/ui/components/roles/workflow/vertical-steps.tsx
new file mode 100644
index 0000000000..74eaa32747
--- /dev/null
+++ b/ui/components/roles/workflow/vertical-steps.tsx
@@ -0,0 +1,291 @@
+"use client";
+
+import type { ButtonProps } from "@nextui-org/react";
+import { cn } from "@nextui-org/react";
+import { useControlledState } from "@react-stately/utils";
+import { domAnimation, LazyMotion, m } from "framer-motion";
+import type { ComponentProps } from "react";
+import React from "react";
+
+export type VerticalStepProps = {
+ className?: string;
+ description?: React.ReactNode;
+ title?: React.ReactNode;
+};
+
+export interface VerticalStepsProps
+ extends React.HTMLAttributes {
+ /**
+ * An array of steps.
+ *
+ * @default []
+ */
+ steps?: VerticalStepProps[];
+ /**
+ * The color of the steps.
+ *
+ * @default "primary"
+ */
+ color?: ButtonProps["color"];
+ /**
+ * The current step index.
+ */
+ currentStep?: number;
+ /**
+ * The default step index.
+ *
+ * @default 0
+ */
+ defaultStep?: number;
+ /**
+ * Whether to hide the progress bars.
+ *
+ * @default false
+ */
+ hideProgressBars?: boolean;
+ /**
+ * The custom class for the steps wrapper.
+ */
+ className?: string;
+ /**
+ * The custom class for the step.
+ */
+ stepClassName?: string;
+ /**
+ * Callback function when the step index changes.
+ */
+ onStepChange?: (stepIndex: number) => void;
+}
+
+function CheckIcon(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
+
+export const VerticalSteps = React.forwardRef<
+ HTMLButtonElement,
+ VerticalStepsProps
+>(
+ (
+ {
+ color = "primary",
+ steps = [],
+ defaultStep = 0,
+ onStepChange,
+ currentStep: currentStepProp,
+ hideProgressBars = false,
+ stepClassName,
+ className,
+ ...props
+ },
+ ref,
+ ) => {
+ const [currentStep, setCurrentStep] = useControlledState(
+ currentStepProp,
+ defaultStep,
+ onStepChange,
+ );
+
+ const colors = React.useMemo(() => {
+ let userColor;
+ let fgColor;
+
+ const colorsVars = [
+ "[--active-fg-color:var(--step-fg-color)]",
+ "[--active-border-color:var(--step-color)]",
+ "[--active-color:var(--step-color)]",
+ "[--complete-background-color:var(--step-color)]",
+ "[--complete-border-color:var(--step-color)]",
+ "[--inactive-border-color:hsl(var(--nextui-default-300))]",
+ "[--inactive-color:hsl(var(--nextui-default-300))]",
+ ];
+
+ switch (color) {
+ case "primary":
+ userColor = "[--step-color:hsl(var(--nextui-primary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]";
+ break;
+ case "secondary":
+ userColor = "[--step-color:hsl(var(--nextui-secondary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-secondary-foreground))]";
+ break;
+ case "success":
+ userColor = "[--step-color:hsl(var(--nextui-success))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-success-foreground))]";
+ break;
+ case "warning":
+ userColor = "[--step-color:hsl(var(--nextui-warning))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-warning-foreground))]";
+ break;
+ case "danger":
+ userColor = "[--step-color:hsl(var(--nextui-error))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-error-foreground))]";
+ break;
+ case "default":
+ userColor = "[--step-color:hsl(var(--nextui-default))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-default-foreground))]";
+ break;
+ default:
+ userColor = "[--step-color:hsl(var(--nextui-primary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]";
+ break;
+ }
+
+ if (!className?.includes("--step-fg-color")) colorsVars.unshift(fgColor);
+ if (!className?.includes("--step-color")) colorsVars.unshift(userColor);
+ if (!className?.includes("--inactive-bar-color"))
+ colorsVars.push(
+ "[--inactive-bar-color:hsl(var(--nextui-default-300))]",
+ );
+
+ return colorsVars;
+ }, [color, className]);
+
+ return (
+
+ );
+ },
+);
+
+VerticalSteps.displayName = "VerticalSteps";
diff --git a/ui/components/roles/workflow/workflow-add-edit-role.tsx b/ui/components/roles/workflow/workflow-add-edit-role.tsx
new file mode 100644
index 0000000000..319a027c17
--- /dev/null
+++ b/ui/components/roles/workflow/workflow-add-edit-role.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { Progress, Spacer } from "@nextui-org/react";
+import { usePathname } from "next/navigation";
+import React from "react";
+
+import { VerticalSteps } from "./vertical-steps";
+
+const steps = [
+ {
+ title: "Create a new role",
+ description: "Enter the name of the role you want to add.",
+ href: "/roles/new",
+ },
+ {
+ title: "Edit a existing role",
+ description:
+ "Review the role details and share the information required for the role.",
+ href: "/roles/edit",
+ },
+];
+
+export const WorkflowAddEditRole = () => {
+ const pathname = usePathname();
+
+ // Calculate current step based on pathname
+ const currentStepIndex = steps.findIndex((step) =>
+ pathname.endsWith(step.href),
+ );
+ const currentStep = currentStepIndex === -1 ? 0 : currentStepIndex;
+
+ return (
+
+
+ Manage Role Permissions
+
+
+ Define a new role with customized permissions or modify an existing one
+ to meet your needs.
+
+
+
+
+
+ );
+};
diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts
index 0b503fe26b..f51bae1e5c 100644
--- a/ui/types/formSchemas.ts
+++ b/ui/types/formSchemas.ts
@@ -1,5 +1,16 @@
import { z } from "zod";
+export const addRoleFormSchema = z.object({
+ name: z.string().min(1, "Name is required"),
+ manage_users: z.boolean().default(false),
+ manage_account: z.boolean().default(false),
+ manage_billing: z.boolean().default(false),
+ manage_providers: z.boolean().default(false),
+ manage_integrations: z.boolean().default(false),
+ manage_scans: z.boolean().default(false),
+ unlimited_visibility: z.boolean().default(false),
+});
+
export const editScanFormSchema = (currentName: string) =>
z.object({
scanName: z