mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
7d2a22c45a
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com> Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
242 lines
7.5 KiB
TypeScript
242 lines
7.5 KiB
TypeScript
"use client";
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { InfoIcon } from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import {
|
|
Controller,
|
|
DefaultValues,
|
|
useForm,
|
|
UseFormReturn,
|
|
useWatch,
|
|
} from "react-hook-form";
|
|
|
|
import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
|
|
import {
|
|
Form,
|
|
FormButtons,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/shadcn/form";
|
|
import { Input } from "@/components/shadcn/input/input";
|
|
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
|
|
import { Separator } from "@/components/shadcn/separator/separator";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/shadcn/tooltip";
|
|
import { useFormServerErrors } from "@/hooks/use-form-server-errors";
|
|
import { useManageProvidersUnlimitedVisibility } from "@/hooks/use-manage-providers-unlimited-visibility";
|
|
import {
|
|
getUnlimitedVisibilityField,
|
|
getVisiblePermissionFormFields,
|
|
} from "@/lib/role-permissions";
|
|
import { isCloud } from "@/lib/shared/env";
|
|
import { roleFormSchema, RoleFormValues } from "@/types";
|
|
|
|
import { UnlimitedVisibilityField } from "./unlimited-visibility-section";
|
|
|
|
export interface RoleGroupOption {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface RoleFormSubmitContext {
|
|
form: UseFormReturn<RoleFormValues>;
|
|
handleServerResponse: (data: unknown) => boolean;
|
|
}
|
|
|
|
interface RoleFormProps {
|
|
groups: RoleGroupOption[];
|
|
defaultValues: DefaultValues<RoleFormValues>;
|
|
submitText: string;
|
|
onSubmit: (
|
|
values: RoleFormValues,
|
|
ctx: RoleFormSubmitContext,
|
|
) => void | Promise<void>;
|
|
onCancel?: () => void;
|
|
}
|
|
|
|
export const RoleForm = ({
|
|
groups,
|
|
defaultValues,
|
|
submitText,
|
|
onSubmit,
|
|
onCancel,
|
|
}: RoleFormProps) => {
|
|
const router = useRouter();
|
|
|
|
const form = useForm<RoleFormValues>({
|
|
resolver: zodResolver(roleFormSchema),
|
|
defaultValues,
|
|
});
|
|
|
|
const isCloudEnvironment = isCloud();
|
|
const visiblePermissionFormFields =
|
|
getVisiblePermissionFormFields(isCloudEnvironment);
|
|
const showUnlimitedVisibilityField = !!getUnlimitedVisibilityField();
|
|
|
|
const { setPermissionValue, setUnlimitedVisibility } =
|
|
useManageProvidersUnlimitedVisibility(form);
|
|
const { handleServerResponse } = useFormServerErrors(form, {
|
|
"/data/attributes/name": "name",
|
|
});
|
|
|
|
// useWatch instead of form.watch: React Compiler can keep memoized JSX stale
|
|
// when getter reads happen during render.
|
|
const formValues = useWatch({ control: form.control });
|
|
const unlimitedVisibility = !!formValues.unlimited_visibility;
|
|
const isLoading = form.formState.isSubmitting;
|
|
|
|
const onSelectAllChange = (checked: boolean) => {
|
|
visiblePermissionFormFields.forEach(({ field }) => {
|
|
setPermissionValue(field, checked);
|
|
});
|
|
};
|
|
|
|
const handleCancel = onCancel ?? (() => router.push("/roles"));
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit((values) =>
|
|
onSubmit(values, { form, handleServerResponse }),
|
|
)}
|
|
className="flex flex-col gap-6"
|
|
>
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
Role Name <span className="text-text-error-primary">*</span>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="Enter role name"
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<span className="text-lg font-semibold">Admin Permissions</span>
|
|
|
|
{/* Select All Checkbox */}
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="select-all"
|
|
size="sm"
|
|
checked={visiblePermissionFormFields.every((perm) =>
|
|
Boolean(formValues[perm.field as keyof RoleFormValues]),
|
|
)}
|
|
onCheckedChange={(checked) => onSelectAllChange(Boolean(checked))}
|
|
/>
|
|
<label htmlFor="select-all" className="text-small">
|
|
Grant all admin permissions
|
|
</label>
|
|
</div>
|
|
|
|
{/* Permissions Grid */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{visiblePermissionFormFields.map(
|
|
({ field, label, description }) => (
|
|
<div key={field} className="flex items-center gap-2">
|
|
<Checkbox
|
|
id={field}
|
|
size="sm"
|
|
checked={!!formValues[field as keyof RoleFormValues]}
|
|
onCheckedChange={(checked) =>
|
|
setPermissionValue(field, Boolean(checked))
|
|
}
|
|
/>
|
|
<label htmlFor={field} className="text-small">
|
|
{label}
|
|
</label>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex w-fit items-center justify-center">
|
|
<InfoIcon
|
|
className="text-muted-foreground cursor-pointer"
|
|
aria-hidden="true"
|
|
width={16}
|
|
/>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{description}</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
),
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator className="my-4" />
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<span className="text-lg font-semibold">Visibility</span>
|
|
|
|
{showUnlimitedVisibilityField && (
|
|
<UnlimitedVisibilityField
|
|
isSelected={unlimitedVisibility}
|
|
onValueChange={setUnlimitedVisibility}
|
|
/>
|
|
)}
|
|
|
|
{!unlimitedVisibility && (
|
|
<>
|
|
<p className="text-small text-default-700 font-medium">
|
|
Select the groups this role will have access to. If no groups
|
|
are selected and unlimited visibility is not enabled, the role
|
|
will not have access to any accounts.
|
|
</p>
|
|
|
|
<Controller
|
|
name="groups"
|
|
control={form.control}
|
|
render={({ field }) => (
|
|
<div className="flex flex-col gap-2">
|
|
<EnhancedMultiSelect
|
|
options={groups.map((group) => ({
|
|
label: group.name,
|
|
value: group.id,
|
|
}))}
|
|
onValueChange={field.onChange}
|
|
defaultValue={field.value || []}
|
|
placeholder="Select groups"
|
|
searchable={true}
|
|
hideSelectAll={true}
|
|
emptyIndicator="No results found"
|
|
resetOnDefaultValueChange={true}
|
|
/>
|
|
</div>
|
|
)}
|
|
/>
|
|
|
|
{form.formState.errors.groups && (
|
|
<p className="mt-2 text-sm text-red-600">
|
|
{form.formState.errors.groups.message}
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
<FormButtons
|
|
submitText={submitText}
|
|
isDisabled={isLoading}
|
|
onCancel={handleCancel}
|
|
/>
|
|
</form>
|
|
</Form>
|
|
);
|
|
};
|