refactor(ui): migrate provider wizard forms from HeroUI to shadcn (#10259)

This commit is contained in:
Alejandro Bailo
2026-03-06 10:13:47 +01:00
committed by GitHub
parent cc02c6f880
commit b1c5fa4c46
43 changed files with 917 additions and 560 deletions

View File

@@ -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<OrgSetupPhase>(initialPhase);
const formId = "org-wizard-setup-form";
const {
control,
register,
handleSubmit,
formState: { errors, isSubmitting, isValid },
setError,
watch,
} = useForm<OrgSetupFormData>({
const form = useForm<OrgSetupFormData>({
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 (
<form
id={formId}
onSubmit={handleFormSubmit}
className="flex flex-col gap-5"
>
{setupPhase === ORG_SETUP_PHASE.DETAILS && (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-4">
<AWSProviderBadge size={32} />
<h3 className="text-base font-semibold">
Amazon Web Services (AWS) / Organization Details
</h3>
<Form {...form}>
<form
id={formId}
onSubmit={handleFormSubmit}
className="flex flex-col gap-5"
>
{setupPhase === ORG_SETUP_PHASE.DETAILS && (
<div className="flex flex-col gap-6">
<div className="flex items-center gap-4">
<AWSProviderBadge size={32} />
<h3 className="text-base font-semibold">
Amazon Web Services (AWS) / Organization Details
</h3>
</div>
<p className="text-muted-foreground text-sm">
Enter the Organization ID for the accounts you want to add to
Prowler.
</p>
</div>
)}
<p className="text-muted-foreground text-sm">
Enter the Organization ID for the accounts you want to add to
Prowler.
</p>
</div>
)}
{setupPhase === ORG_SETUP_PHASE.ACCESS && (
<div className="flex flex-col gap-8">
<div className="flex items-center gap-4">
<AWSProviderBadge size={32} />
<h3 className="text-base font-semibold">
Amazon Web Services (AWS) / Authentication Details
</h3>
{setupPhase === ORG_SETUP_PHASE.ACCESS && (
<div className="flex flex-col gap-8">
<div className="flex items-center gap-4">
<AWSProviderBadge size={32} />
<h3 className="text-base font-semibold">
Amazon Web Services (AWS) / Authentication Details
</h3>
</div>
</div>
</div>
)}
)}
{setupPhase === ORG_SETUP_PHASE.ACCESS && isSubmitting && (
<div className="flex min-h-[220px] items-center justify-center">
<div className="flex items-center gap-3 py-2">
<TreeSpinner className="size-6" />
<p className="text-sm font-medium">Gathering AWS Accounts...</p>
{setupPhase === ORG_SETUP_PHASE.ACCESS && isSubmitting && (
<div className="flex min-h-[220px] items-center justify-center">
<div className="flex items-center gap-3 py-2">
<TreeSpinner className="size-6" />
<p className="text-sm font-medium">Gathering AWS Accounts...</p>
</div>
</div>
</div>
)}
)}
{apiError && (
<Alert variant="error">
<AlertDescription className="text-text-error-primary">
{apiError}
</AlertDescription>
</Alert>
)}
{apiError && (
<Alert variant="error">
<AlertDescription className="text-text-error-primary">
{apiError}
</AlertDescription>
</Alert>
)}
{setupPhase === ORG_SETUP_PHASE.DETAILS && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label htmlFor="awsOrgId" className="text-sm font-medium">
Organization ID
</label>
<Input
id="awsOrgId"
{setupPhase === ORG_SETUP_PHASE.DETAILS && (
<div className="flex flex-col gap-4">
<WizardInputField
control={control}
name="awsOrgId"
label="Organization ID"
labelPlacement="outside"
placeholder="e.g. o-123456789-abcdefg"
required
aria-required="true"
isRequired
normalizeValue={(value) => 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 && (
<span className="text-text-error-primary text-xs">
{errors.awsOrgId.message}
</span>
)}
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="organizationName" className="text-sm font-medium">
Name (optional)
</label>
<Input
id="organizationName"
<WizardInputField
control={control}
name="organizationName"
label="Name (optional)"
labelPlacement="outside"
placeholder=""
{...register("organizationName")}
isRequired={false}
/>
{errors.organizationName && (
<span className="text-text-error-primary text-xs">
{errors.organizationName.message}
</span>
)}
</div>
<p className="text-muted-foreground text-sm">
If left blank, Prowler will use the Organization name stored in AWS.
</p>
</div>
)}
{setupPhase === ORG_SETUP_PHASE.ACCESS && !isSubmitting && (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
1) Launch the Prowler CloudFormation StackSet in your AWS Console.
<p className="text-muted-foreground text-sm">
If left blank, Prowler will use the Organization name stored in
AWS.
</p>
<Button
variant="outline"
size="lg"
className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start"
disabled={!stackSetQuickLink}
asChild
>
<a
href={stackSetQuickLink || "#"}
target="_blank"
rel="noopener noreferrer"
</div>
)}
{setupPhase === ORG_SETUP_PHASE.ACCESS && !isSubmitting && (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
1) Launch the Prowler CloudFormation StackSet in your AWS
Console.
</p>
<Button
variant="outline"
size="lg"
className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start"
disabled={!stackSetQuickLink}
asChild
>
<ExternalLink className="size-5" />
<span>
Prowler CloudFormation StackSet for AWS Organizations
</span>
</a>
</Button>
</div>
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
2) Use the following Prowler External ID parameter in the
StackSet.
</p>
<div className="flex items-center gap-3">
<span className="text-text-neutral-tertiary text-xs">
External ID:
</span>
<div className="bg-bg-neutral-tertiary border-border-input-primary flex h-10 max-w-full items-center gap-3 rounded-full border px-4">
<span className="truncate text-xs font-medium">
{stackSetExternalId || "Loading organization external ID..."}
</span>
<button
type="button"
disabled={!stackSetExternalId}
onClick={async () => {
try {
await navigator.clipboard.writeText(stackSetExternalId);
setIsExternalIdCopied(true);
setTimeout(() => setIsExternalIdCopied(false), 1500);
} catch {
// Ignore clipboard errors (e.g., unsupported browser context).
}
}}
className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 transition-colors"
aria-label="Copy external ID"
<a
href={stackSetQuickLink || "#"}
target="_blank"
rel="noopener noreferrer"
>
{isExternalIdCopied ? (
<Check className="size-4" />
) : (
<Copy className="size-4" />
)}
</button>
<ExternalLink className="size-5" />
<span>
Prowler CloudFormation StackSet for AWS Organizations
</span>
</a>
</Button>
</div>
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
2) Use the following Prowler External ID parameter in the
StackSet.
</p>
<div className="flex items-center gap-3">
<span className="text-text-neutral-tertiary text-xs">
External ID:
</span>
<div className="bg-bg-neutral-tertiary border-border-input-primary flex h-10 max-w-full items-center gap-3 rounded-full border px-4">
<span className="truncate text-xs font-medium">
{stackSetExternalId ||
"Loading organization external ID..."}
</span>
<button
type="button"
disabled={!stackSetExternalId}
onClick={async () => {
try {
await navigator.clipboard.writeText(stackSetExternalId);
setIsExternalIdCopied(true);
setTimeout(() => setIsExternalIdCopied(false), 1500);
} catch {
// Ignore clipboard errors (e.g., unsupported browser context).
}
}}
className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 transition-colors"
aria-label="Copy external ID"
>
{isExternalIdCopied ? (
<Check className="size-4" />
) : (
<Copy className="size-4" />
)}
</button>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
3) Copy the Prowler IAM Role ARN from AWS and confirm the StackSet
is successfully deployed by clicking the checkbox below.
</p>
</div>
<div className="flex flex-col gap-4">
<p className="text-text-neutral-primary text-sm leading-7 font-normal">
3) Copy the Prowler IAM Role ARN from AWS and confirm the
StackSet is successfully deployed by clicking the checkbox
below.
</p>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="roleArn" className="text-sm font-medium">
Role ARN
</label>
<Input
id="roleArn"
<WizardInputField
control={control}
name="roleArn"
label="Role ARN"
labelPlacement="outside"
placeholder="e.g. arn:aws:iam::123456789012:role/ProwlerOrgRole"
{...register("roleArn")}
isRequired={false}
requiredIndicator
/>
{errors.roleArn && (
<p className="text-text-neutral-tertiary text-sm">
* It may take up to 60 seconds for AWS to generate the IAM Role
ARN
</p>
<div className="flex items-start gap-4">
<Controller
name="stackSetDeployed"
control={control}
render={({ field }) => (
<>
<Checkbox
id="stackSetDeployed"
className="mt-0.5"
checked={field.value}
onCheckedChange={(checked) =>
field.onChange(Boolean(checked))
}
/>
<label
htmlFor="stackSetDeployed"
className="text-text-neutral-tertiary text-xs leading-5 font-normal"
>
The StackSet has been successfully deployed in AWS
<span className="text-text-error-primary">*</span>
</label>
</>
)}
/>
</div>
{errors.stackSetDeployed && (
<span className="text-text-error-primary text-xs">
{errors.roleArn.message}
{errors.stackSetDeployed.message}
</span>
)}
</div>
<p className="text-text-neutral-tertiary text-sm">
* It may take up to 60 seconds for AWS to generate the IAM Role ARN
</p>
<div className="flex items-start gap-4">
<Controller
name="stackSetDeployed"
control={control}
render={({ field }) => (
<>
<Checkbox
id="stackSetDeployed"
className="mt-0.5"
checked={field.value}
onCheckedChange={(checked) =>
field.onChange(Boolean(checked))
}
/>
<label
htmlFor="stackSetDeployed"
className="text-text-neutral-primary text-sm leading-7 font-normal"
>
The StackSet has been successfully deployed in AWS
</label>
</>
)}
/>
</div>
{errors.stackSetDeployed && (
<span className="text-text-error-primary text-xs">
{errors.stackSetDeployed.message}
</span>
)}
</div>
)}
</form>
)}
</form>
</Form>
);
}

View File

@@ -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 = ({
<span className="text-default-500 block text-xs font-medium">
External ID:
</span>
<SnippetChip value={externalId} icon={<IdIcon size={16} />} />
<CodeSnippet value={externalId} icon={<IdIcon size={16} />} />
</div>
</div>
</div>

View File

@@ -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 = ({
<ProviderTitleDocs providerType={providerType} />
<Divider />
<Separator />
{providerType === "aws" && effectiveVia === "role" && (
<AWSRoleCredentialsForm

View File

@@ -9,10 +9,10 @@ import { z } from "zod";
import { addProvider } from "@/actions/providers/providers";
import { AwsMethodSelector } from "@/components/providers/organizations/aws-method-selector";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderTitleDocs } from "@/components/providers/workflow/provider-title-docs";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
import { CustomInput } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { addProviderFormSchema, ApiError, ProviderType } from "@/types";
@@ -346,7 +346,7 @@ export const ConnectAccountForm = ({
(providerType !== "aws" || awsMethod === "single") && (
<>
<ProviderTitleDocs providerType={providerType} />
<CustomInput
<WizardInputField
control={form.control}
name="providerUid"
type="text"
@@ -356,7 +356,7 @@ export const ConnectAccountForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={form.control}
name="providerAlias"
type="text"

View File

@@ -0,0 +1,3 @@
export { WizardInputField } from "./wizard-input-field";
export { WizardRadioCard } from "./wizard-radio-card";
export { WizardTextareaField } from "./wizard-textarea-field";

View File

@@ -0,0 +1,172 @@
"use client";
import { Icon } from "@iconify/react";
import { InputHTMLAttributes, useState } from "react";
import { Control, FieldPath, FieldValues } from "react-hook-form";
import { Input } from "@/components/shadcn/input/input";
import { FormControl, FormField, FormMessage } from "@/components/ui/form";
import { cn } from "@/lib/utils";
interface WizardInputFieldProps<T extends FieldValues> {
control: Control<T>;
name: FieldPath<T>;
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<HTMLInputElement>["autoCapitalize"];
autoCorrect?: InputHTMLAttributes<HTMLInputElement>["autoCorrect"];
spellCheck?: boolean;
requiredIndicator?: boolean;
}
export const WizardInputField = <T extends FieldValues>({
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<T>) => {
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 (
<FormField
control={control}
name={name}
render={({ field }) => {
const value = field.value ?? defaultValue ?? "";
return (
<div className="flex flex-col gap-1.5">
<label
htmlFor={name}
className={cn(
"text-text-neutral-tertiary text-xs",
labelPlacement === "outside"
? "font-medium"
: "font-light tracking-tight",
)}
>
{inputLabel}
{showRequiredIndicator && (
<span className="text-text-error-primary">*</span>
)}
</label>
<FormControl>
<div className="relative">
<Input
id={name}
aria-label={inputLabel}
placeholder={inputPlaceholder}
type={inputType}
required={inputIsRequired}
disabled={isDisabled}
readOnly={isReadOnly}
autoCapitalize={autoCapitalize}
autoCorrect={autoCorrect}
spellCheck={spellCheck}
className={cn(isMaskedInput && "pr-10")}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
onChange={(event) => {
if (!normalizeValue) {
field.onChange(event);
return;
}
const normalizedValue = normalizeValue(event.target.value);
field.onChange(normalizedValue);
}}
value={value}
/>
{isMaskedInput && (
<button
type="button"
onClick={toggleVisibility}
className="text-default-400 hover:text-default-500 absolute top-1/2 right-3 -translate-y-1/2"
aria-label={
inputType === "password"
? "Show password"
: "Hide password"
}
>
<Icon
className="pointer-events-none text-xl"
icon={
(password && isPasswordVisible) ||
(confirmPassword && isConfirmPasswordVisible) ||
(type === "password" && isPasswordVisible)
? "solar:eye-closed-linear"
: "solar:eye-bold"
}
/>
</button>
)}
</div>
</FormControl>
<FormMessage className="text-text-error max-w-full text-xs" />
</div>
);
}}
/>
);
};

View File

@@ -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 (
<div
className={cn(
"group inline-flex w-full cursor-pointer items-center justify-between gap-4 rounded-lg border-2 p-4",
"border-default hover:border-button-primary",
"has-[[data-state=checked]]:border-button-primary",
isInvalid && "border-bg-fail",
)}
>
<label
htmlFor={value}
className="flex flex-1 cursor-pointer items-center"
>
<span className="ml-2">{children}</span>
</label>
<RadioGroupItem value={value} id={value} />
</div>
);
};

View File

@@ -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<T extends FieldValues> {
control: Control<T>;
name: FieldPath<T>;
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 = <T extends FieldValues>({
control,
name,
label = name,
labelPlacement = "inside",
variant,
size,
placeholder,
defaultValue,
isRequired = false,
minRows = 3,
maxRows,
fullWidth = true,
disableAutosize = false,
description,
requiredIndicator,
}: WizardTextareaFieldProps<T>) => {
void variant;
void size;
void fullWidth;
void disableAutosize;
const showRequiredIndicator = requiredIndicator ?? isRequired;
return (
<FormField
control={control}
name={name}
render={({ field }) => {
const value = field.value ?? defaultValue ?? "";
return (
<div className="flex flex-col gap-1.5">
<label
htmlFor={name}
className={cn(
"text-text-neutral-tertiary text-xs",
labelPlacement === "outside"
? "font-medium"
: "font-light tracking-tight",
)}
>
{label}
{showRequiredIndicator && (
<span className="text-text-error-primary">*</span>
)}
</label>
<FormControl>
<Textarea
id={name}
aria-label={label}
placeholder={placeholder}
required={isRequired}
rows={maxRows ? Math.min(minRows, maxRows) : minRows}
className={cn(description && "mb-1")}
{...field}
value={value}
/>
</FormControl>
{description && (
<p className="text-text-neutral-tertiary max-w-full text-xs">
{description}
</p>
)}
<FormMessage className="text-text-error max-w-full text-xs" />
</div>
);
}}
/>
);
};

View File

@@ -0,0 +1,57 @@
import { ProviderType } from "@/types";
import {
ConnectionFalse,
ConnectionPending,
ConnectionTrue,
} from "../../../icons";
import { getProviderLogo } from "../../../ui/entities/get-provider-logo";
interface ProviderConnectionInfoProps {
connected: boolean | null;
provider: ProviderType;
providerAlias: string;
providerUID?: string;
}
export const ProviderConnectionInfo = ({
connected,
provider,
providerAlias,
providerUID,
}: ProviderConnectionInfoProps) => {
const getIcon = () => {
switch (connected) {
case true:
return (
<div className="rounded-medium border-system-success bg-system-success-lighter flex items-center justify-center border-2 p-1">
<ConnectionTrue className="text-system-success" size={24} />
</div>
);
case false:
return (
<div className="rounded-medium border-border-error flex items-center justify-center border-2 p-1">
<ConnectionFalse className="text-text-error-primary" size={24} />
</div>
);
case null:
return (
<div className="bg-info-lighter border-info-lighter rounded-medium flex items-center justify-center border p-1">
<ConnectionPending className="text-info" size={24} />
</div>
);
default:
return <ConnectionPending size={24} />;
}
};
return (
<div className="flex items-center text-sm">
<div className="flex items-center gap-4">
<div className="shrink-0">{getProviderLogo(provider)}</div>
{getIcon()}
<span className="font-medium">{providerAlias || providerUID}</span>
</div>
</div>
);
};

View File

@@ -1,7 +1,7 @@
import { Divider } from "@heroui/divider";
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { Separator } from "@/components/shadcn/separator/separator";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { AlibabaCloudCredentialsRole } from "@/types";
@@ -26,7 +26,7 @@ export const AlibabaCloudRoleCredentialsForm = ({
RAM Role to Assume
</span>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ROLE_ARN}
type="text"
@@ -37,13 +37,13 @@ export const AlibabaCloudRoleCredentialsForm = ({
isRequired
/>
<Divider />
<Separator />
<span className="text-default-500 text-xs font-bold">
Credentials for Role Assumption
</span>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID}
type="text"
@@ -53,7 +53,7 @@ export const AlibabaCloudRoleCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET}
type="password"
@@ -66,7 +66,7 @@ export const AlibabaCloudRoleCredentialsForm = ({
<span className="text-default-500 text-xs">Optional fields</span>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ROLE_SESSION_NAME}
type="text"

View File

@@ -1,6 +1,6 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { AlibabaCloudCredentials } from "@/types";
@@ -20,7 +20,7 @@ export const AlibabaCloudStaticCredentialsForm = ({
access to the resources you want Prowler to assess.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID}
type="text"
@@ -30,7 +30,7 @@ export const AlibabaCloudStaticCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET}
type="password"

View File

@@ -1,9 +1,9 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import { Control, Controller, FieldValues, Path } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { WizardRadioCard } from "@/components/providers/workflow/forms/fields";
import { RadioGroup } from "@/components/shadcn/radio-group/radio-group";
import { FormMessage } from "@/components/ui/form";
type RadioGroupAlibabaCloudViaCredentialsFormProps<T extends FieldValues> = {
@@ -25,47 +25,38 @@ export const RadioGroupAlibabaCloudViaCredentialsTypeForm = <
<Controller
name={"alibabacloudCredentialsType" as Path<T>}
control={control}
render={({ field }) => (
<>
<RadioGroup
className="flex flex-wrap"
isInvalid={isInvalid}
{...field}
value={field.value || ""}
onValueChange={(value) => {
field.onChange(value);
if (onChange) {
onChange(value);
}
}}
>
<div className="flex flex-col gap-4">
render={({ field }) => {
const currentValue = String(field.value ?? "");
return (
<>
<RadioGroup
name={field.name}
value={currentValue}
onValueChange={(value: string) => {
field.onChange(value);
onChange?.(value);
}}
>
<span className="text-default-500 text-sm">Using RAM Role</span>
<CustomRadio description="Connect assuming RAM Role" value="role">
<div className="flex items-center">
<span className="ml-2">Connect assuming RAM Role</span>
</div>
</CustomRadio>
<WizardRadioCard value="role" isInvalid={isInvalid}>
Connect assuming RAM Role
</WizardRadioCard>
<span className="text-default-500 text-sm">
Using Credentials
</span>
<CustomRadio
description="Connect via Access Keys"
value="credentials"
>
<div className="flex items-center">
<span className="ml-2">Connect via Access Keys</span>
</div>
</CustomRadio>
</div>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-text-error">
{errorMessage}
</FormMessage>
)}
</>
)}
<WizardRadioCard value="credentials" isInvalid={isInvalid}>
Connect via Access Keys
</WizardRadioCard>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-text-error">
{errorMessage}
</FormMessage>
)}
</>
);
}}
/>
);
};

View File

@@ -1,10 +1,10 @@
import { Chip } from "@heroui/chip";
import { Divider } from "@heroui/divider";
import { Switch } from "@heroui/switch";
import { useEffect, useState } from "react";
import { Control, UseFormSetValue, useWatch } from "react-hook-form";
import { CredentialsRoleHelper } from "@/components/providers/workflow";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { Badge } from "@/components/shadcn/badge/badge";
import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
import {
Select,
SelectContent,
@@ -12,7 +12,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/shadcn/select/select";
import { CustomInput } from "@/components/ui/custom";
import { Separator } from "@/components/shadcn/separator/separator";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { AWSCredentialsRole } from "@/types";
import { IntegrationType } from "@/types/integrations";
@@ -105,14 +105,9 @@ export const AWSRoleCredentialsForm = ({
: "AWS SDK Default"}
</span>
{isCloudEnv && (
<Chip
size="sm"
variant="flat"
color="success"
className="ml-2"
>
<Badge variant="tag" className="ml-2">
Recommended
</Chip>
</Badge>
)}
</div>
</SelectItem>
@@ -127,7 +122,7 @@ export const AWSRoleCredentialsForm = ({
{credentialsType === "access-secret-key" && (
<>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.AWS_ACCESS_KEY_ID}
type="password"
@@ -137,7 +132,7 @@ export const AWSRoleCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.AWS_SECRET_ACCESS_KEY}
type="password"
@@ -147,7 +142,7 @@ export const AWSRoleCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.AWS_SESSION_TOKEN}
type="password"
@@ -159,7 +154,7 @@ export const AWSRoleCredentialsForm = ({
/>
</>
)}
<Divider className="" />
<Separator />
{type === "providers" ? (
<span className="text-default-500 text-xs font-bold">Assume Role</span>
@@ -170,11 +165,11 @@ export const AWSRoleCredentialsForm = ({
? "Adding a role is required"
: "Optionally add a role"}
</span>
<Switch
size="sm"
isSelected={showRoleSection}
onValueChange={setShowOptionalRole}
isDisabled={isCloudEnv && credentialsType === "aws-sdk-default"}
<Checkbox
checked={showRoleSection}
onCheckedChange={(checked) => setShowOptionalRole(Boolean(checked))}
disabled={isCloudEnv && credentialsType === "aws-sdk-default"}
aria-label="Optionally add a role"
/>
</div>
)}
@@ -187,9 +182,9 @@ export const AWSRoleCredentialsForm = ({
integrationType={integrationType}
/>
<Divider />
<Separator />
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ROLE_ARN}
type="text"
@@ -199,7 +194,7 @@ export const AWSRoleCredentialsForm = ({
variant="bordered"
isRequired={showRoleSection}
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.EXTERNAL_ID}
type="text"
@@ -214,7 +209,7 @@ export const AWSRoleCredentialsForm = ({
<span className="text-default-500 text-xs">Optional fields</span>
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ROLE_SESSION_NAME}
type="text"
@@ -224,7 +219,7 @@ export const AWSRoleCredentialsForm = ({
variant="bordered"
isRequired={false}
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.SESSION_DURATION}
type="number"

View File

@@ -1,6 +1,6 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { AWSCredentials } from "@/types";
@@ -19,7 +19,7 @@ export const AWSStaticCredentialsForm = ({
Please provide the information for your AWS credentials.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.AWS_ACCESS_KEY_ID}
type="password"
@@ -29,7 +29,7 @@ export const AWSStaticCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.AWS_SECRET_ACCESS_KEY}
type="password"
@@ -39,7 +39,7 @@ export const AWSStaticCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.AWS_SESSION_TOKEN}
type="password"

View File

@@ -1,10 +1,9 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import React from "react";
import { Control, Controller } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { WizardRadioCard } from "@/components/providers/workflow/forms/fields";
import { RadioGroup } from "@/components/shadcn/radio-group/radio-group";
import { FormMessage } from "@/components/ui/form";
type RadioGroupAWSViaCredentialsFormProps = {
@@ -27,36 +26,21 @@ export const RadioGroupAWSViaCredentialsTypeForm = ({
render={({ field }) => (
<>
<RadioGroup
className="flex flex-wrap"
isInvalid={isInvalid}
{...field}
name={field.name}
value={field.value || ""}
onValueChange={(value) => {
onValueChange={(value: string) => {
field.onChange(value);
if (onChange) {
onChange(value);
}
onChange?.(value);
}}
>
<div className="flex flex-col gap-4">
<span className="text-default-500 text-sm">Using IAM Role</span>
<CustomRadio description="Connect assuming IAM Role" value="role">
<div className="flex items-center">
<span className="ml-2">Connect assuming IAM Role</span>
</div>
</CustomRadio>
<span className="text-default-500 text-sm">
Using Credentials
</span>
<CustomRadio
description="Connect via Credentials"
value="credentials"
>
<div className="flex items-center">
<span className="ml-2">Connect via Credentials</span>
</div>
</CustomRadio>
</div>
<span className="text-default-500 text-sm">Using IAM Role</span>
<WizardRadioCard value="role" isInvalid={isInvalid}>
Connect assuming IAM Role
</WizardRadioCard>
<span className="text-default-500 text-sm">Using Credentials</span>
<WizardRadioCard value="credentials" isInvalid={isInvalid}>
Connect via Credentials
</WizardRadioCard>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-text-error">

View File

@@ -2,7 +2,7 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { CloudflareApiKeyCredentials } from "@/types";
@@ -22,7 +22,7 @@ export const CloudflareApiKeyCredentialsForm = ({
associated with your Cloudflare account.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.CLOUDFLARE_API_EMAIL}
type="text"
@@ -32,7 +32,7 @@ export const CloudflareApiKeyCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.CLOUDFLARE_API_KEY}
type="password"

View File

@@ -2,7 +2,7 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { CloudflareTokenCredentials } from "@/types";
@@ -23,7 +23,7 @@ export const CloudflareApiTokenCredentialsForm = ({
method.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.CLOUDFLARE_API_TOKEN}
type="password"

View File

@@ -1,9 +1,9 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import { Control, Controller } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { WizardRadioCard } from "@/components/providers/workflow/forms/fields";
import { RadioGroup } from "@/components/shadcn/radio-group/radio-group";
import { FormMessage } from "@/components/ui/form";
type RadioGroupCloudflareViaCredentialsFormProps = {
@@ -26,38 +26,22 @@ export const RadioGroupCloudflareViaCredentialsTypeForm = ({
render={({ field }) => (
<>
<RadioGroup
className="flex flex-wrap"
isInvalid={isInvalid}
{...field}
name={field.name}
value={field.value || ""}
onValueChange={(value) => {
onValueChange={(value: string) => {
field.onChange(value);
if (onChange) {
onChange(value);
}
onChange?.(value);
}}
>
<div className="flex flex-col gap-4">
<span className="text-default-500 text-sm">
Select Authentication Method
</span>
<CustomRadio
description="Connect using a Cloudflare API Token (recommended)"
value="api_token"
>
<div className="flex items-center">
<span className="ml-2">API Token</span>
</div>
</CustomRadio>
<CustomRadio
description="Connect using Global API Key and Email"
value="api_key"
>
<div className="flex items-center">
<span className="ml-2">API Key + Email</span>
</div>
</CustomRadio>
</div>
<span className="text-default-500 text-sm">
Select Authentication Method
</span>
<WizardRadioCard value="api_token" isInvalid={isInvalid}>
API Token
</WizardRadioCard>
<WizardRadioCard value="api_key" isInvalid={isInvalid}>
API Key + Email
</WizardRadioCard>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-text-error">

View File

@@ -1,6 +1,6 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { GCPDefaultCredentials } from "@/types";
export const GCPDefaultCredentialsForm = ({
@@ -18,7 +18,7 @@ export const GCPDefaultCredentialsForm = ({
Please provide the information for your GCP credentials.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name="client_id"
type="text"
@@ -28,7 +28,7 @@ export const GCPDefaultCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name="client_secret"
type="password"
@@ -38,7 +38,7 @@ export const GCPDefaultCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name="refresh_token"
type="password"

View File

@@ -1,6 +1,6 @@
import { Control } from "react-hook-form";
import { CustomTextarea } from "@/components/ui/custom";
import { WizardTextareaField } from "@/components/providers/workflow/forms/fields";
import { GCPServiceAccountKey } from "@/types";
export const GCPServiceAccountKeyForm = ({
@@ -18,7 +18,7 @@ export const GCPServiceAccountKeyForm = ({
Please provide the service account key for your GCP credentials.
</div>
</div>
<CustomTextarea
<WizardTextareaField
control={control}
name="service_account_key"
label="Service Account Key"

View File

@@ -1,13 +1,12 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import React from "react";
import { Control, Controller } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { WizardRadioCard } from "@/components/providers/workflow/forms/fields";
import { RadioGroup } from "@/components/shadcn/radio-group/radio-group";
import { FormMessage } from "@/components/ui/form";
type RadioGroupAWSViaCredentialsFormProps = {
type RadioGroupGCPViaCredentialsFormProps = {
control: Control<any>;
isInvalid: boolean;
errorMessage?: string;
@@ -19,7 +18,7 @@ export const RadioGroupGCPViaCredentialsTypeForm = ({
isInvalid,
errorMessage,
onChange,
}: RadioGroupAWSViaCredentialsFormProps) => {
}: RadioGroupGCPViaCredentialsFormProps) => {
return (
<Controller
name="gcpCredentialsType"
@@ -27,43 +26,25 @@ export const RadioGroupGCPViaCredentialsTypeForm = ({
render={({ field }) => (
<>
<RadioGroup
className="flex flex-wrap"
isInvalid={isInvalid}
{...field}
name={field.name}
value={field.value || ""}
onValueChange={(value) => {
onValueChange={(value: string) => {
field.onChange(value);
if (onChange) {
onChange(value);
}
onChange?.(value);
}}
>
<div className="flex flex-col gap-4">
<span className="text-default-500 text-sm">
Using Service Account
</span>
<CustomRadio
description="Connect using Service Account"
value="service-account"
>
<div className="flex items-center">
<span className="ml-2">Connect via Service Account Key</span>
</div>
</CustomRadio>
<span className="text-default-500 text-sm">
Using Application Default Credentials
</span>
<CustomRadio
description="Connect via Credentials"
value="credentials"
>
<div className="flex items-center">
<span className="ml-2">
Connect via Application Default Credentials
</span>
</div>
</CustomRadio>
</div>
<span className="text-default-500 text-sm">
Using Service Account
</span>
<WizardRadioCard value="service-account" isInvalid={isInvalid}>
Connect via Service Account Key
</WizardRadioCard>
<span className="text-default-500 text-sm">
Using Application Default Credentials
</span>
<WizardRadioCard value="credentials" isInvalid={isInvalid}>
Connect via Application Default Credentials
</WizardRadioCard>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-text-error">

View File

@@ -2,7 +2,10 @@
import { Control } from "react-hook-form";
import { CustomInput, CustomTextarea } from "@/components/ui/custom";
import {
WizardInputField,
WizardTextareaField,
} from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
export const GitHubAppForm = ({ control }: { control: Control<any> }) => {
@@ -16,7 +19,7 @@ export const GitHubAppForm = ({ control }: { control: Control<any> }) => {
Please provide your GitHub App ID and private key.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.GITHUB_APP_ID}
type="text"
@@ -26,7 +29,7 @@ export const GitHubAppForm = ({ control }: { control: Control<any> }) => {
variant="bordered"
isRequired
/>
<CustomTextarea
<WizardTextareaField
control={control}
name={ProviderCredentialFields.GITHUB_APP_KEY}
label="GitHub App Private Key"

View File

@@ -2,7 +2,7 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
export const GitHubOAuthAppForm = ({ control }: { control: Control<any> }) => {
@@ -16,7 +16,7 @@ export const GitHubOAuthAppForm = ({ control }: { control: Control<any> }) => {
Please provide your GitHub OAuth App token.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.OAUTH_APP_TOKEN}
type="password"

View File

@@ -2,7 +2,7 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
export const GitHubPersonalAccessTokenForm = ({
@@ -20,7 +20,7 @@ export const GitHubPersonalAccessTokenForm = ({
Please provide your GitHub personal access token.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.PERSONAL_ACCESS_TOKEN}
type="password"

View File

@@ -1,10 +1,9 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import React from "react";
import { Control, Controller } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { WizardRadioCard } from "@/components/providers/workflow/forms/fields";
import { RadioGroup } from "@/components/shadcn/radio-group/radio-group";
import { FormMessage } from "@/components/ui/form";
type RadioGroupGitHubViaCredentialsFormProps = {
@@ -27,50 +26,32 @@ export const RadioGroupGitHubViaCredentialsTypeForm = ({
render={({ field }) => (
<>
<RadioGroup
className="flex flex-wrap"
isInvalid={isInvalid}
{...field}
name={field.name}
value={field.value || ""}
onValueChange={(value) => {
onValueChange={(value: string) => {
field.onChange(value);
if (onChange) {
onChange(value);
}
onChange?.(value);
}}
>
<div className="flex flex-col gap-4">
<span className="text-default-500 text-sm">
Personal Access Token
</span>
<CustomRadio
description="Use a personal access token for authentication"
value="personal_access_token"
>
<div className="flex items-center">
<span className="ml-2">Personal Access Token</span>
</div>
</CustomRadio>
<span className="text-default-500 text-sm">
Personal Access Token
</span>
<WizardRadioCard
value="personal_access_token"
isInvalid={isInvalid}
>
Personal Access Token
</WizardRadioCard>
<span className="text-default-500 text-sm">OAuth App</span>
<CustomRadio
description="Use OAuth App token for authentication"
value="oauth_app"
>
<div className="flex items-center">
<span className="ml-2">OAuth App Token</span>
</div>
</CustomRadio>
<span className="text-default-500 text-sm">OAuth App</span>
<WizardRadioCard value="oauth_app" isInvalid={isInvalid}>
OAuth App Token
</WizardRadioCard>
<span className="text-default-500 text-sm">GitHub App</span>
<CustomRadio
description="Use GitHub App ID and private key for authentication"
value="github_app"
>
<div className="flex items-center">
<span className="ml-2">GitHub App</span>
</div>
</CustomRadio>
</div>
<span className="text-default-500 text-sm">GitHub App</span>
<WizardRadioCard value="github_app" isInvalid={isInvalid}>
GitHub App
</WizardRadioCard>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-text-error">

View File

@@ -1,9 +1,12 @@
"use client";
import Link from "next/link";
import { Control } from "react-hook-form";
import { CustomInput, CustomTextarea } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import {
WizardInputField,
WizardTextareaField,
} from "@/components/providers/workflow/forms/fields";
import { M365CertificateCredentials } from "@/types";
export const M365CertificateCredentialsForm = ({
@@ -22,7 +25,7 @@ export const M365CertificateCredentialsForm = ({
certificate authentication.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name="tenant_id"
type="text"
@@ -32,7 +35,7 @@ export const M365CertificateCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name="client_id"
type="text"
@@ -42,7 +45,7 @@ export const M365CertificateCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomTextarea
<WizardTextareaField
control={control}
name="certificate_content"
label="Certificate Content"
@@ -56,12 +59,14 @@ export const M365CertificateCredentialsForm = ({
The certificate content must be base64 encoded from an unsigned
certificate. For detailed instructions on how to generate and encode
your certificate, please refer to the{" "}
<CustomLink
<Link
href="https://docs.prowler.com/user-guide/providers/microsoft365/authentication#generate-the-certificate"
size="sm"
target="_blank"
rel="noopener noreferrer"
className="text-button-tertiary p-0 text-sm"
>
certificate generation guide
</CustomLink>
</Link>
.
</p>
</>

View File

@@ -2,7 +2,7 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { M365ClientSecretCredentials } from "@/types";
export const M365ClientSecretCredentialsForm = ({
@@ -20,7 +20,7 @@ export const M365ClientSecretCredentialsForm = ({
Please provide your Microsoft 365 application credentials.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name="tenant_id"
type="text"
@@ -30,7 +30,7 @@ export const M365ClientSecretCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name="client_id"
type="text"
@@ -40,7 +40,7 @@ export const M365ClientSecretCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name="client_secret"
type="password"

View File

@@ -1,10 +1,9 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import React from "react";
import { Control, Controller } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { WizardRadioCard } from "@/components/providers/workflow/forms/fields";
import { RadioGroup } from "@/components/shadcn/radio-group/radio-group";
import { FormMessage } from "@/components/ui/form";
type RadioGroupM365ViaCredentialsFormProps = {
@@ -27,38 +26,22 @@ export const RadioGroupM365ViaCredentialsTypeForm = ({
render={({ field }) => (
<>
<RadioGroup
className="flex flex-wrap"
isInvalid={isInvalid}
{...field}
name={field.name}
value={field.value || ""}
onValueChange={(value) => {
onValueChange={(value: string) => {
field.onChange(value);
if (onChange) {
onChange(value);
}
onChange?.(value);
}}
>
<div className="flex flex-col gap-4">
<span className="text-default-500 text-sm">
Select Authentication Method
</span>
<CustomRadio
description="Connect using Application Client Secret"
value="app_client_secret"
>
<div className="flex items-center">
<span className="ml-2">App Client Secret Credentials</span>
</div>
</CustomRadio>
<CustomRadio
description="Connect using Application Certificate"
value="app_certificate"
>
<div className="flex items-center">
<span className="ml-2">App Certificate Credentials</span>
</div>
</CustomRadio>
</div>
<span className="text-default-500 text-sm">
Select Authentication Method
</span>
<WizardRadioCard value="app_client_secret" isInvalid={isInvalid}>
App Client Secret Credentials
</WizardRadioCard>
<WizardRadioCard value="app_certificate" isInvalid={isInvalid}>
App Certificate Credentials
</WizardRadioCard>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-text-error">

View File

@@ -22,7 +22,7 @@ import { checkTaskStatus } from "@/lib/helper";
import { ProviderType } from "@/types";
import { ApiError, testConnectionFormSchema } from "@/types";
import { ProviderInfo } from "../..";
import { ProviderConnectionInfo } from "./provider-connection-info";
type FormValues = z.input<typeof testConnectionFormSchema>;
@@ -243,7 +243,7 @@ export const TestConnectionForm = ({
</>
)}
<ProviderInfo
<ProviderConnectionInfo
connected={providerData.data.attributes.connection.connected}
provider={providerData.data.attributes.provider}
providerAlias={providerData.data.attributes.alias}

View File

@@ -1,6 +1,6 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { AzureCredentials } from "@/types";
export const AzureCredentialsForm = ({
@@ -18,7 +18,7 @@ export const AzureCredentialsForm = ({
Please provide the information for your Azure credentials.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name="client_id"
type="text"
@@ -28,7 +28,7 @@ export const AzureCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name="client_secret"
type="password"
@@ -38,7 +38,7 @@ export const AzureCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name="tenant_id"
type="text"

View File

@@ -1,6 +1,6 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { IacCredentials } from "@/types";
export const IacCredentialsForm = ({
@@ -18,7 +18,7 @@ export const IacCredentialsForm = ({
Provide an access token if the repository is private (optional).
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name="access_token"
label="Access Token (Optional)"

View File

@@ -1,6 +1,6 @@
import { Control } from "react-hook-form";
import { CustomTextarea } from "@/components/ui/custom";
import { WizardTextareaField } from "@/components/providers/workflow/forms/fields";
import { KubernetesCredentials } from "@/types";
export const KubernetesCredentialsForm = ({
@@ -18,7 +18,7 @@ export const KubernetesCredentialsForm = ({
Please provide the kubeconfig content for your Kubernetes credentials.
</div>
</div>
<CustomTextarea
<WizardTextareaField
control={control}
name="kubeconfig_content"
label="Kubeconfig Content"

View File

@@ -1,6 +1,6 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { MongoDBAtlasCredentials } from "@/types";
@@ -20,7 +20,7 @@ export const MongoDBAtlasCredentialsForm = ({
with read access to the resources you want Prowler to assess.
</div>
</div>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ATLAS_PUBLIC_KEY}
type="text"
@@ -30,7 +30,7 @@ export const MongoDBAtlasCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.ATLAS_PRIVATE_KEY}
type="password"

View File

@@ -1,6 +1,9 @@
import { Control } from "react-hook-form";
import { CustomInput, CustomTextarea } from "@/components/ui/custom";
import {
WizardInputField,
WizardTextareaField,
} from "@/components/providers/workflow/forms/fields";
import { OpenStackCredentials } from "@/types";
export const OpenStackCredentialsForm = ({
@@ -18,7 +21,7 @@ export const OpenStackCredentialsForm = ({
Please provide your OpenStack clouds.yaml content and the cloud name.
</div>
</div>
<CustomTextarea
<WizardTextareaField
control={control}
name="clouds_yaml_content"
label="Clouds YAML Content"
@@ -28,7 +31,7 @@ export const OpenStackCredentialsForm = ({
minRows={10}
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name="clouds_yaml_cloud"
type="text"

View File

@@ -1,6 +1,9 @@
import { Control, Controller } from "react-hook-form";
import { CustomInput, CustomTextarea } from "@/components/ui/custom";
import {
WizardInputField,
WizardTextareaField,
} from "@/components/providers/workflow/forms/fields";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { OCICredentials } from "@/types";
@@ -25,7 +28,7 @@ export const OracleCloudCredentialsForm = ({
name={ProviderCredentialFields.OCI_TENANCY}
render={({ field }) => <input type="hidden" {...field} />}
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.OCI_USER}
type="text"
@@ -35,7 +38,7 @@ export const OracleCloudCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.OCI_FINGERPRINT}
type="text"
@@ -45,7 +48,7 @@ export const OracleCloudCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.OCI_REGION}
type="text"
@@ -55,7 +58,7 @@ export const OracleCloudCredentialsForm = ({
variant="bordered"
isRequired
/>
<CustomTextarea
<WizardTextareaField
control={control}
name={ProviderCredentialFields.OCI_KEY_CONTENT}
label="Private Key Content"
@@ -65,7 +68,7 @@ export const OracleCloudCredentialsForm = ({
minRows={6}
isRequired
/>
<CustomInput
<WizardInputField
control={control}
name={ProviderCredentialFields.OCI_PASS_PHRASE}
type="password"

View File

@@ -1,33 +1,21 @@
import { Card, CardBody, CardHeader } from "@heroui/card";
import { Skeleton } from "@heroui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/shadcn/card/card";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
export const SkeletonProviderWorkflow = () => {
return (
<Card>
<Card variant="inner">
<CardHeader className="flex flex-col items-start gap-2">
<Skeleton className="h-6 w-2/3 rounded-lg">
<div className="bg-default-200 h-6"></div>
</Skeleton>
<Skeleton className="h-4 w-1/2 rounded-lg">
<div className="bg-default-200 h-4"></div>
</Skeleton>
<Skeleton className="h-6 w-2/3 rounded-lg" />
<Skeleton className="h-4 w-1/2 rounded-lg" />
</CardHeader>
<CardBody className="flex flex-col items-start gap-6">
<CardContent className="flex flex-col items-start gap-6">
<div className="flex gap-4">
<Skeleton className="h-12 w-12 rounded-lg">
<div className="bg-default-200 h-12 w-12"></div>
</Skeleton>
<Skeleton className="h-12 w-12 rounded-lg">
<div className="bg-default-200 h-12 w-12"></div>
</Skeleton>
<Skeleton className="h-12 w-12 rounded-lg" />
<Skeleton className="h-12 w-12 rounded-lg" />
</div>
<Skeleton className="h-5 w-3/4 rounded-lg">
<div className="bg-default-200 h-5"></div>
</Skeleton>
<Skeleton className="h-12 w-40 self-end rounded-lg">
<div className="bg-default-200 h-12"></div>
</Skeleton>
</CardBody>
<Skeleton className="h-5 w-3/4 rounded-lg" />
<Skeleton className="h-12 w-40 self-end rounded-lg" />
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,46 @@
"use client";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { cn } from "@/lib/utils";
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("flex flex-col gap-4", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-border-input-primary aspect-square size-4 shrink-0 rounded-full border shadow-[0_1px_2px_0_rgba(0,0,0,0.1)] transition-all outline-none",
"focus-visible:border-border-input-primary-press focus-visible:ring-border-input-primary-press/50 focus-visible:ring-2",
"data-[state=checked]:border-button-primary",
"disabled:cursor-not-allowed disabled:opacity-40",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="grid place-content-center"
>
<span className="bg-button-primary size-2 rounded-full" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };

View File

@@ -0,0 +1,50 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { ComponentProps, forwardRef } from "react";
import { cn } from "@/lib/utils";
const textareaVariants = cva(
"flex w-full rounded-lg border text-sm transition-all outline-none resize-none disabled:cursor-not-allowed disabled:opacity-50",
{
variants: {
variant: {
default:
"border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-inset focus:ring-border-input-primary-press placeholder:text-text-neutral-tertiary",
ghost:
"border-transparent bg-transparent hover:bg-bg-neutral-tertiary focus:bg-bg-neutral-tertiary placeholder:text-text-neutral-tertiary",
},
textareaSize: {
default: "min-h-16 px-4 py-3",
sm: "min-h-12 px-3 py-2 text-xs",
lg: "min-h-24 px-5 py-4",
},
},
defaultVariants: {
variant: "default",
textareaSize: "default",
},
},
);
export interface TextareaProps
extends Omit<ComponentProps<"textarea">, "size">,
VariantProps<typeof textareaVariants> {}
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, variant, textareaSize, ...props }, ref) => {
return (
<textarea
ref={ref}
data-slot="textarea"
className={cn(textareaVariants({ variant, textareaSize, className }))}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";
export { Textarea, textareaVariants };

View File

@@ -1,23 +0,0 @@
import { type ReactNode } from "react";
interface CustomSectionProps {
title: string | ReactNode;
children: ReactNode;
action?: ReactNode;
}
export const CustomSection = ({
title,
children,
action,
}: CustomSectionProps) => (
<div className="dark:bg-prowler-blue-400 flex flex-col gap-4 rounded-lg p-4 shadow">
<div className="flex items-center justify-between">
<h3 className="text-md dark:text-prowler-theme-pale/90 font-medium text-gray-800">
{title}
</h3>
{action && <div>{action}</div>}
</div>
{children}
</div>
);

View File

@@ -3,7 +3,6 @@ export * from "./custom-input";
export * from "./custom-link";
export * from "./custom-modal-buttons";
export * from "./custom-radio";
export * from "./custom-section";
export * from "./custom-server-input";
export * from "./custom-table-link";
export * from "./custom-textarea";

View File

@@ -159,6 +159,14 @@
"strategy": "installed",
"generatedAt": "2025-11-19T12:28:39.510Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-radio-group",
"from": "1.3.8",
"to": "1.3.8",
"strategy": "installed",
"generatedAt": "2026-03-05T14:12:59.626Z"
},
{
"section": "dependencies",
"name": "@radix-ui/react-scroll-area",

View File

@@ -47,6 +47,7 @@
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-scroll-area": "1.2.10",
"@radix-ui/react-select": "2.2.5",
"@radix-ui/react-separator": "1.1.7",

34
ui/pnpm-lock.yaml generated
View File

@@ -92,6 +92,9 @@ importers:
'@radix-ui/react-popover':
specifier: 1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-radio-group':
specifier: 1.3.8
version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-scroll-area':
specifier: 1.2.10
version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -3047,6 +3050,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-radio-group@1.3.8':
resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.10':
resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==}
peerDependencies:
@@ -13078,6 +13094,24 @@ snapshots:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
'@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.4)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.4)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.8)(react@19.2.4)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.4)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.4)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
'@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2