diff --git a/ui/components/providers/organizations/org-setup-form.tsx b/ui/components/providers/organizations/org-setup-form.tsx index 46571b66be..0a62efedb7 100644 --- a/ui/components/providers/organizations/org-setup-form.tsx +++ b/ui/components/providers/organizations/org-setup-form.tsx @@ -12,11 +12,12 @@ import { WIZARD_FOOTER_ACTION_TYPE, WizardFooterConfig, } from "@/components/providers/wizard/steps/footer-controls"; +import { WizardInputField } from "@/components/providers/workflow/forms/fields"; import { Alert, AlertDescription } from "@/components/shadcn/alert"; import { Button } from "@/components/shadcn/button/button"; import { Checkbox } from "@/components/shadcn/checkbox/checkbox"; -import { Input } from "@/components/shadcn/input/input"; import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner"; +import { Form } from "@/components/ui/form"; import { getAWSCredentialsTemplateLinks } from "@/lib"; import { ORG_SETUP_PHASE, OrgSetupPhase } from "@/types/organizations"; @@ -68,14 +69,7 @@ export function OrgSetupForm({ const [setupPhase, setSetupPhase] = useState(initialPhase); const formId = "org-wizard-setup-form"; - const { - control, - register, - handleSubmit, - formState: { errors, isSubmitting, isValid }, - setError, - watch, - } = useForm({ + const form = useForm({ resolver: zodResolver(orgSetupSchema), mode: "onChange", reValidateMode: "onChange", @@ -86,10 +80,13 @@ export function OrgSetupForm({ stackSetDeployed: false, }, }); - const awsOrgIdField = register("awsOrgId", { - setValueAs: (value: unknown) => - typeof value === "string" ? value.toLowerCase() : value, - }); + const { + control, + handleSubmit, + formState: { errors, isSubmitting, isValid }, + setError, + watch, + } = form; const awsOrgId = watch("awsOrgId") || ""; const isOrgIdValid = /^o-[a-z0-9]{10,32}$/.test(awsOrgId.trim()); @@ -180,228 +177,209 @@ export function OrgSetupForm({ }, [apiError, formId]); return ( -
- {setupPhase === ORG_SETUP_PHASE.DETAILS && ( -
-
- -

- Amazon Web Services (AWS) / Organization Details -

+ + + {setupPhase === ORG_SETUP_PHASE.DETAILS && ( +
+
+ +

+ Amazon Web Services (AWS) / Organization Details +

+
+ +

+ Enter the Organization ID for the accounts you want to add to + Prowler. +

+ )} -

- Enter the Organization ID for the accounts you want to add to - Prowler. -

-
- )} - - {setupPhase === ORG_SETUP_PHASE.ACCESS && ( -
-
- -

- Amazon Web Services (AWS) / Authentication Details -

+ {setupPhase === ORG_SETUP_PHASE.ACCESS && ( +
+
+ +

+ Amazon Web Services (AWS) / Authentication Details +

+
-
- )} + )} - {setupPhase === ORG_SETUP_PHASE.ACCESS && isSubmitting && ( -
-
- -

Gathering AWS Accounts...

+ {setupPhase === ORG_SETUP_PHASE.ACCESS && isSubmitting && ( +
+
+ +

Gathering AWS Accounts...

+
-
- )} + )} - {apiError && ( - - - {apiError} - - - )} + {apiError && ( + + + {apiError} + + + )} - {setupPhase === ORG_SETUP_PHASE.DETAILS && ( -
-
- - + value.toLowerCase()} autoCapitalize="none" autoCorrect="off" spellCheck={false} - {...awsOrgIdField} - onInput={(event) => { - const loweredValue = event.currentTarget.value.toLowerCase(); - if (event.currentTarget.value !== loweredValue) { - event.currentTarget.value = loweredValue; - } - }} /> - {errors.awsOrgId && ( - - {errors.awsOrgId.message} - - )} -
-
- - - {errors.organizationName && ( - - {errors.organizationName.message} - - )} -
-

- If left blank, Prowler will use the Organization name stored in AWS. -

-
- )} - - {setupPhase === ORG_SETUP_PHASE.ACCESS && !isSubmitting && ( -
-
-

- 1) Launch the Prowler CloudFormation StackSet in your AWS Console. +

+ If left blank, Prowler will use the Organization name stored in + AWS.

- -
- -
-

- 2) Use the following Prowler External ID parameter in the - StackSet. -

-
- - External ID: - -
- - {stackSetExternalId || "Loading organization external ID..."} - - + + + Prowler CloudFormation StackSet for AWS Organizations + + + +
+ +
+

+ 2) Use the following Prowler External ID parameter in the + StackSet. +

+
+ + External ID: + +
+ + {stackSetExternalId || + "Loading organization external ID..."} + + +
-
-
-

- 3) Copy the Prowler IAM Role ARN from AWS and confirm the StackSet - is successfully deployed by clicking the checkbox below. -

-
+
+

+ 3) Copy the Prowler IAM Role ARN from AWS and confirm the + StackSet is successfully deployed by clicking the checkbox + below. +

+
-
- - - {errors.roleArn && ( + +

+ * It may take up to 60 seconds for AWS to generate the IAM Role + ARN +

+ +
+ ( + <> + + field.onChange(Boolean(checked)) + } + /> + + + )} + /> +
+ {errors.stackSetDeployed && ( - {errors.roleArn.message} + {errors.stackSetDeployed.message} )}
- -

- * It may take up to 60 seconds for AWS to generate the IAM Role ARN -

- -
- ( - <> - - field.onChange(Boolean(checked)) - } - /> - - - )} - /> -
- {errors.stackSetDeployed && ( - - {errors.stackSetDeployed.message} - - )} -
- )} - + )} + + ); } diff --git a/ui/components/providers/workflow/credentials-role-helper.tsx b/ui/components/providers/workflow/credentials-role-helper.tsx index 46554fbacf..0ded59beee 100644 --- a/ui/components/providers/workflow/credentials-role-helper.tsx +++ b/ui/components/providers/workflow/credentials-role-helper.tsx @@ -2,7 +2,7 @@ import { IdIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; -import { SnippetChip } from "@/components/ui/entities"; +import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { IntegrationType } from "@/types/integrations"; interface CredentialsRoleHelperProps { @@ -95,7 +95,7 @@ export const CredentialsRoleHelper = ({ External ID: - } /> + } />
diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx index 9e0a44a53d..959749b8b7 100644 --- a/ui/components/providers/workflow/forms/base-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx @@ -1,11 +1,11 @@ "use client"; -import { Divider } from "@heroui/divider"; import { ChevronLeftIcon, ChevronRightIcon, Loader2 } from "lucide-react"; import { useEffect } from "react"; import { Control, UseFormSetValue } from "react-hook-form"; import { Button } from "@/components/shadcn"; +import { Separator } from "@/components/shadcn/separator/separator"; import { Form } from "@/components/ui/form"; import { useCredentialsForm } from "@/hooks/use-credentials-form"; import { getAWSCredentialsTemplateLinks } from "@/lib"; @@ -149,7 +149,7 @@ export const BaseCredentialsForm = ({ - + {providerType === "aws" && effectiveVia === "role" && ( - - { + control: Control; + name: FieldPath; + label?: string; + labelPlacement?: "inside" | "outside"; + variant?: "flat" | "bordered" | "underlined" | "faded"; + size?: "sm" | "md" | "lg"; + type?: string; + placeholder?: string; + password?: boolean; + confirmPassword?: boolean; + defaultValue?: string; + isReadOnly?: boolean; + isRequired?: boolean; + isDisabled?: boolean; + normalizeValue?: (value: string) => string; + autoCapitalize?: InputHTMLAttributes["autoCapitalize"]; + autoCorrect?: InputHTMLAttributes["autoCorrect"]; + spellCheck?: boolean; + requiredIndicator?: boolean; +} + +export const WizardInputField = ({ + control, + name, + type = "text", + label = name, + labelPlacement = "inside", + variant, + size, + placeholder, + confirmPassword = false, + password = false, + defaultValue, + isReadOnly = false, + isRequired = true, + isDisabled = false, + normalizeValue, + autoCapitalize, + autoCorrect, + spellCheck, + requiredIndicator, +}: WizardInputFieldProps) => { + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] = + useState(false); + void variant; + void size; + + const inputLabel = confirmPassword + ? "Confirm Password" + : password + ? "Password" + : label; + + const inputPlaceholder = confirmPassword + ? "Confirm Password" + : password + ? "Password" + : placeholder; + + const isMaskedInput = type === "password" || password || confirmPassword; + const inputType = isMaskedInput + ? isPasswordVisible || isConfirmPasswordVisible + ? "text" + : "password" + : type; + const inputIsRequired = password || confirmPassword ? true : isRequired; + const showRequiredIndicator = requiredIndicator ?? inputIsRequired; + + const toggleVisibility = () => { + if (password || type === "password") { + setIsPasswordVisible((current) => !current); + return; + } + if (confirmPassword) { + setIsConfirmPasswordVisible((current) => !current); + } + }; + + return ( + { + const value = field.value ?? defaultValue ?? ""; + + return ( +
+ + +
+ { + if (!normalizeValue) { + field.onChange(event); + return; + } + const normalizedValue = normalizeValue(event.target.value); + field.onChange(normalizedValue); + }} + value={value} + /> + {isMaskedInput && ( + + )} +
+
+ +
+ ); + }} + /> + ); +}; diff --git a/ui/components/providers/workflow/forms/fields/wizard-radio-card.tsx b/ui/components/providers/workflow/forms/fields/wizard-radio-card.tsx new file mode 100644 index 0000000000..00b8440030 --- /dev/null +++ b/ui/components/providers/workflow/forms/fields/wizard-radio-card.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { RadioGroupItem } from "@/components/shadcn/radio-group/radio-group"; +import { cn } from "@/lib/utils"; + +interface WizardRadioCardProps { + value: string; + children: React.ReactNode; + isInvalid?: boolean; +} + +export const WizardRadioCard = ({ + value, + children, + isInvalid = false, +}: WizardRadioCardProps) => { + return ( +
+ + +
+ ); +}; diff --git a/ui/components/providers/workflow/forms/fields/wizard-textarea-field.tsx b/ui/components/providers/workflow/forms/fields/wizard-textarea-field.tsx new file mode 100644 index 0000000000..05768b0e04 --- /dev/null +++ b/ui/components/providers/workflow/forms/fields/wizard-textarea-field.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Control, FieldPath, FieldValues } from "react-hook-form"; + +import { Textarea } from "@/components/shadcn/textarea/textarea"; +import { FormControl, FormField, FormMessage } from "@/components/ui/form"; +import { cn } from "@/lib/utils"; + +interface WizardTextareaFieldProps { + control: Control; + name: FieldPath; + label?: string; + labelPlacement?: "inside" | "outside" | "outside-left"; + variant?: "flat" | "bordered" | "underlined" | "faded"; + size?: "sm" | "md" | "lg"; + placeholder?: string; + defaultValue?: string; + isRequired?: boolean; + minRows?: number; + maxRows?: number; + fullWidth?: boolean; + disableAutosize?: boolean; + description?: React.ReactNode; + requiredIndicator?: boolean; +} + +export const WizardTextareaField = ({ + control, + name, + label = name, + labelPlacement = "inside", + variant, + size, + placeholder, + defaultValue, + isRequired = false, + minRows = 3, + maxRows, + fullWidth = true, + disableAutosize = false, + description, + requiredIndicator, +}: WizardTextareaFieldProps) => { + void variant; + void size; + void fullWidth; + void disableAutosize; + const showRequiredIndicator = requiredIndicator ?? isRequired; + + return ( + { + const value = field.value ?? defaultValue ?? ""; + + return ( +
+ + +