mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 14:54:30 +00:00
feat(ui): integrate Organizations flow into provider add workflow
Add AWS method selector step between provider type and UID form in ConnectAccountForm. Replace HeroUI VerticalSteps/Progress with custom icon-based stepper in WorkflowAddProvider. Remove HeroUI Spacer import from layout in favor of Tailwind utility class.
This commit is contained in:
4 files changed
+193
-114
No files matched your search
@@ -1,6 +1,5 @@
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
import React from "react";
|
||||
|
||||
import { WorkflowAddProvider } from "@/components/providers/workflow";
|
||||
@@ -18,7 +17,7 @@ export default function ProviderLayout({ children }: ProviderLayoutProps) {
|
||||
icon="icon-park-outline:close-small"
|
||||
href="/providers"
|
||||
/>
|
||||
<Spacer y={8} />
|
||||
<div className="mt-8" />
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12">
|
||||
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block">
|
||||
<WorkflowAddProvider />
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { addProvider } from "@/actions/providers/providers";
|
||||
import { AwsMethodSelector } from "@/components/providers/organizations/aws-method-selector";
|
||||
import { OrgWizardModal } from "@/components/providers/organizations/org-wizard-modal";
|
||||
import { ProviderTitleDocs } from "@/components/providers/workflow/provider-title-docs";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { useToast } from "@/components/ui";
|
||||
@@ -83,6 +85,8 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
|
||||
export const ConnectAccountForm = () => {
|
||||
const { toast } = useToast();
|
||||
const [prevStep, setPrevStep] = useState(1);
|
||||
const [awsMethod, setAwsMethod] = useState<"single" | null>(null);
|
||||
const [isOrgModalOpen, setIsOrgModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const formSchema = addProviderFormSchema;
|
||||
@@ -170,10 +174,19 @@ export const ConnectAccountForm = () => {
|
||||
};
|
||||
|
||||
const handleBackStep = () => {
|
||||
// If in UID form after choosing single, go back to method selector
|
||||
if (prevStep === 2 && awsMethod === "single") {
|
||||
setAwsMethod(null);
|
||||
form.setValue("providerUid", "");
|
||||
form.setValue("providerAlias", "");
|
||||
return;
|
||||
}
|
||||
|
||||
setPrevStep((prev) => prev - 1);
|
||||
//Deselect the providerType if the user is going back to the first step
|
||||
// Deselect the providerType if the user is going back to the first step
|
||||
if (prevStep === 2) {
|
||||
form.setValue("providerType", undefined as unknown as ProviderType);
|
||||
setAwsMethod(null);
|
||||
}
|
||||
// Reset the providerUid and providerAlias fields when going back
|
||||
form.setValue("providerUid", "");
|
||||
@@ -200,35 +213,50 @@ export const ConnectAccountForm = () => {
|
||||
errorMessage={form.formState.errors.providerType?.message}
|
||||
/>
|
||||
)}
|
||||
{/* Step 2: UID, alias, and credentials (if AWS) */}
|
||||
{prevStep === 2 && (
|
||||
{/* Step 2: AWS method selector (only for AWS, before choosing method) */}
|
||||
{prevStep === 2 && providerType === "aws" && awsMethod === null && (
|
||||
<>
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="providerUid"
|
||||
type="text"
|
||||
label={providerFieldDetails.label}
|
||||
labelPlacement="inside"
|
||||
placeholder={providerFieldDetails.placeholder}
|
||||
variant="bordered"
|
||||
isRequired
|
||||
<AwsMethodSelector
|
||||
onSelectSingle={() => setAwsMethod("single")}
|
||||
onSelectOrganizations={() => setIsOrgModalOpen(true)}
|
||||
/>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="providerAlias"
|
||||
type="text"
|
||||
label="Provider alias (optional)"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the provider alias"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
<OrgWizardModal
|
||||
open={isOrgModalOpen}
|
||||
onOpenChange={setIsOrgModalOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/* Step 2: UID, alias form (non-AWS or AWS single account) */}
|
||||
{prevStep === 2 &&
|
||||
(providerType !== "aws" || awsMethod === "single") && (
|
||||
<>
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="providerUid"
|
||||
type="text"
|
||||
label={providerFieldDetails.label}
|
||||
labelPlacement="inside"
|
||||
placeholder={providerFieldDetails.placeholder}
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="providerAlias"
|
||||
type="text"
|
||||
label="Provider alias (optional)"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the provider alias"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex w-full justify-end gap-4">
|
||||
{/* Show "Back" button only in Step 2 */}
|
||||
{/* Show "Back" button in Step 2 (skip when on AWS method selector) */}
|
||||
{prevStep === 2 && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -241,22 +269,23 @@ export const ConnectAccountForm = () => {
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{/* Show "Next" button in Step 2 */}
|
||||
{prevStep === 2 && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
size="lg"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<ChevronRightIcon size={24} />
|
||||
)}
|
||||
{isLoading ? "Loading" : "Next"}
|
||||
</Button>
|
||||
)}
|
||||
{/* Show "Next" button in Step 2 (only when UID form is visible) */}
|
||||
{prevStep === 2 &&
|
||||
(providerType !== "aws" || awsMethod === "single") && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
size="lg"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<ChevronRightIcon size={24} />
|
||||
)}
|
||||
{isLoading ? "Loading" : "Next"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./credentials-role-helper";
|
||||
export * from "./provider-title-docs";
|
||||
export * from "./skeleton-provider-workflow";
|
||||
export * from "./vertical-steps";
|
||||
export * from "./workflow-add-provider";
|
||||
@@ -1,97 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import { Progress } from "@heroui/progress";
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
import { FolderGit2, KeyRound, Shield } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { VerticalSteps } from "./vertical-steps";
|
||||
import { ProwlerShort } from "@/components/icons/prowler/ProwlerIcons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { IconComponent, IconSvgProps } from "@/types/components";
|
||||
|
||||
const steps = [
|
||||
interface StepConfig {
|
||||
label: string;
|
||||
description: string;
|
||||
icon: IconComponent;
|
||||
}
|
||||
|
||||
const STEPS: StepConfig[] = [
|
||||
{
|
||||
title: "Choose your Cloud Provider",
|
||||
label: "Link a Cloud Provider",
|
||||
description:
|
||||
"Select the cloud provider you wish to connect and specify your preferred authentication method from the supported options.",
|
||||
href: "/providers/connect-account",
|
||||
"Enter the provider details you would like to add in Prowler.",
|
||||
icon: FolderGit2,
|
||||
},
|
||||
{
|
||||
title: "Enter Authentication Details",
|
||||
label: "Authenticate Credentials",
|
||||
description:
|
||||
"Provide the necessary credentials to establish a secure connection to your selected cloud provider.",
|
||||
href: "/providers/add-credentials",
|
||||
"Authorize a secure connection between Prowler and your provider.",
|
||||
icon: KeyRound,
|
||||
},
|
||||
{
|
||||
title: "Verify Connection & Start Scan",
|
||||
label: "Validate Connection",
|
||||
description:
|
||||
"Ensure your credentials are correct and start scanning your cloud environment.",
|
||||
href: "/providers/test-connection",
|
||||
"Review provider resources and test the connection to Prowler.",
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
label: "Launch Scan",
|
||||
description: "Scan newly connected resources.",
|
||||
icon: ProwlerShort,
|
||||
},
|
||||
];
|
||||
|
||||
const ROUTE_CONFIG: Record<
|
||||
string,
|
||||
{
|
||||
stepIndex: number;
|
||||
stepOverride?: { index: number; title: string; description: string };
|
||||
}
|
||||
> = {
|
||||
"/providers/connect-account": { stepIndex: 0 },
|
||||
"/providers/add-credentials": { stepIndex: 1 },
|
||||
"/providers/test-connection": { stepIndex: 2 },
|
||||
"/providers/update-credentials": {
|
||||
stepIndex: 1,
|
||||
stepOverride: {
|
||||
index: 2,
|
||||
title: "Make sure the new credentials are valid",
|
||||
description: "Valid credentials will take you back to the providers page",
|
||||
},
|
||||
},
|
||||
const ROUTE_TO_STEP: Record<string, number> = {
|
||||
"/providers/connect-account": 0,
|
||||
"/providers/add-credentials": 1,
|
||||
"/providers/test-connection": 2,
|
||||
"/providers/update-credentials": 1,
|
||||
};
|
||||
|
||||
export const WorkflowAddProvider = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const config = ROUTE_CONFIG[pathname] || { stepIndex: 0 };
|
||||
const currentStep = config.stepIndex;
|
||||
|
||||
const updatedSteps = steps.map((step, index) => {
|
||||
if (config.stepOverride && index === config.stepOverride.index) {
|
||||
return { ...step, ...config.stepOverride };
|
||||
}
|
||||
return step;
|
||||
});
|
||||
const currentStep = ROUTE_TO_STEP[pathname] ?? 0;
|
||||
|
||||
return (
|
||||
<section className="max-w-sm">
|
||||
<h1 className="mb-2 text-xl font-medium" id="getting-started">
|
||||
Add a Cloud Provider
|
||||
</h1>
|
||||
<p className="text-small text-default-500 mb-5">
|
||||
Complete these steps to configure your cloud provider and initiate your
|
||||
first scan.
|
||||
</p>
|
||||
<Progress
|
||||
classNames={{
|
||||
base: "px-0.5 mb-5",
|
||||
label: "text-small",
|
||||
value: "text-small text-button-primary",
|
||||
indicator: "bg-button-primary",
|
||||
}}
|
||||
label="Steps"
|
||||
maxValue={steps.length - 1}
|
||||
minValue={0}
|
||||
showValueLabel={true}
|
||||
size="md"
|
||||
value={currentStep}
|
||||
valueLabel={`${currentStep + 1} of ${steps.length}`}
|
||||
/>
|
||||
<VerticalSteps
|
||||
hideProgressBars
|
||||
currentStep={currentStep}
|
||||
stepClassName="border border-border-neutral-primary aria-[current]:border-button-primary aria-[current]:text-text-neutral-primary cursor-default"
|
||||
steps={updatedSteps}
|
||||
/>
|
||||
<Spacer y={4} />
|
||||
</section>
|
||||
<nav aria-label="Progress" className="flex max-w-sm flex-col gap-0">
|
||||
{STEPS.map((step, index) => {
|
||||
const isComplete = index < currentStep;
|
||||
const isActive = index === currentStep;
|
||||
const isInactive = index > currentStep;
|
||||
|
||||
return (
|
||||
<div key={step.label} className="flex items-start gap-3">
|
||||
{/* Step indicator + connector line */}
|
||||
<div className="flex flex-col items-center">
|
||||
<StepCircle
|
||||
isComplete={isComplete}
|
||||
isActive={isActive}
|
||||
icon={step.icon}
|
||||
/>
|
||||
{/* Connector line */}
|
||||
{index < STEPS.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
"h-14 w-0 border-l border-dashed",
|
||||
isComplete ? "border-primary/40" : "border-[#202020]",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Label + description */}
|
||||
<div className="flex flex-col gap-1 pt-[10px]">
|
||||
<span
|
||||
className={cn(
|
||||
"text-lg leading-7 font-normal",
|
||||
isActive && "text-[#f4f4f5]",
|
||||
isComplete && "text-[#f4f4f5]",
|
||||
isInactive && "text-[#f4f4f5]/60",
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
<p className="text-xs leading-5 text-[#d4d4d8]">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
interface StepCircleProps {
|
||||
isComplete: boolean;
|
||||
isActive: boolean;
|
||||
icon: IconComponent;
|
||||
}
|
||||
|
||||
function StepCircle({ isComplete, isActive, icon: Icon }: StepCircleProps) {
|
||||
const borderColor =
|
||||
isComplete || isActive ? "border-primary" : "border-[#202020]";
|
||||
const iconColor = isComplete || isActive ? "text-primary" : "text-[#525252]";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-[44px] shrink-0 items-center justify-center rounded-full border bg-[#121110]",
|
||||
borderColor,
|
||||
)}
|
||||
>
|
||||
<StepIcon icon={Icon} className={iconColor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders either a Lucide icon (className-based) or a custom SVG icon
|
||||
* (size-prop-based like ProwlerShort) with consistent sizing.
|
||||
*/
|
||||
function StepIcon({
|
||||
icon: Icon,
|
||||
className,
|
||||
}: {
|
||||
icon: IconComponent;
|
||||
className: string;
|
||||
}) {
|
||||
if (isCustomSvgIcon(Icon)) {
|
||||
return <Icon size={24} className={className} />;
|
||||
}
|
||||
return <Icon className={cn("size-6", className)} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard: Lucide icons have a `displayName` property set by lucide-react.
|
||||
* Custom SVG icons (React.FC<IconSvgProps>) accept a `size` number prop.
|
||||
*/
|
||||
function isCustomSvgIcon(icon: IconComponent): icon is React.FC<IconSvgProps> {
|
||||
return !("displayName" in icon && typeof icon.displayName === "string");
|
||||
}
|
||||
Reference in new issue
Block a user