mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-02 01:15:31 +00:00
feat(ui): add provider wizard types, store, and stepper
This commit is contained in:
8 files changed
+323
-10
No files matched your search
@@ -0,0 +1,3 @@
|
||||
export * from "./provider-wizard-modal";
|
||||
export * from "./steps";
|
||||
export * from "./wizard-stepper";
|
||||
@@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AlertCircle,
|
||||
CircleCheckBig,
|
||||
FolderGit2,
|
||||
KeyRound,
|
||||
Rocket,
|
||||
} from "lucide-react";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
import { ProwlerShort } from "@/components/icons/prowler/ProwlerIcons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { IconComponent, IconSvgProps } from "@/types/components";
|
||||
|
||||
interface WizardStepperProps {
|
||||
currentStep: number;
|
||||
hasConnectionErrors?: boolean;
|
||||
stepOffset?: number;
|
||||
}
|
||||
|
||||
interface StepConfig {
|
||||
label: string;
|
||||
description: string;
|
||||
icon: IconComponent;
|
||||
}
|
||||
|
||||
const STEPS: StepConfig[] = [
|
||||
{
|
||||
label: "Link a Cloud Provider",
|
||||
description: "Enter the provider details you would like to add in Prowler.",
|
||||
icon: FolderGit2,
|
||||
},
|
||||
{
|
||||
label: "Authenticate Credentials",
|
||||
description:
|
||||
"Authorize a secure connection between Prowler and your provider.",
|
||||
icon: KeyRound,
|
||||
},
|
||||
{
|
||||
label: "Validate Connection",
|
||||
description:
|
||||
"Review provider resources and test the connection to Prowler.",
|
||||
icon: Rocket,
|
||||
},
|
||||
{
|
||||
label: "Launch Scan",
|
||||
description: "Scan newly connected resources.",
|
||||
icon: ProwlerShort,
|
||||
},
|
||||
];
|
||||
|
||||
export function WizardStepper({
|
||||
currentStep,
|
||||
hasConnectionErrors = false,
|
||||
stepOffset = 0,
|
||||
}: WizardStepperProps) {
|
||||
const activeVisualStep = Math.max(
|
||||
0,
|
||||
Math.min(currentStep + stepOffset, STEPS.length - 1),
|
||||
);
|
||||
|
||||
return (
|
||||
<nav aria-label="Wizard progress" className="flex flex-col gap-0">
|
||||
{STEPS.map((step, index) => {
|
||||
const isComplete = index < activeVisualStep;
|
||||
const isActive = index === activeVisualStep;
|
||||
const isInactive = index > activeVisualStep;
|
||||
const isErrorStep = index === 2 && hasConnectionErrors && isActive;
|
||||
|
||||
return (
|
||||
<div key={step.label} className="flex items-start gap-3">
|
||||
<div className="flex flex-col items-center">
|
||||
<StepCircle
|
||||
isComplete={isComplete}
|
||||
isActive={isActive}
|
||||
isError={isErrorStep}
|
||||
icon={step.icon}
|
||||
/>
|
||||
{index < STEPS.length - 1 && (
|
||||
<StepConnector isComplete={isComplete} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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",
|
||||
isErrorStep && "text-destructive",
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
<p className="text-xs leading-5 text-[#d4d4d8]">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
interface StepCircleProps {
|
||||
isComplete: boolean;
|
||||
isActive: boolean;
|
||||
isError: boolean;
|
||||
icon: IconComponent;
|
||||
}
|
||||
|
||||
function StepCircle({
|
||||
isComplete,
|
||||
isActive,
|
||||
isError,
|
||||
icon: Icon,
|
||||
}: StepCircleProps) {
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="border-destructive flex size-[44px] shrink-0 items-center justify-center rounded-full border bg-[#121110]">
|
||||
<AlertCircle className="text-destructive size-6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
return (
|
||||
<div className="bg-button-primary-press flex size-[44px] shrink-0 items-center justify-center rounded-full">
|
||||
<CircleCheckBig className="text-bg-neutral-primary size-6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
return (
|
||||
<div className="border-border-input-primary-pressed flex size-[44px] shrink-0 items-center justify-center rounded-full border bg-[#121110]">
|
||||
<StepIcon icon={Icon} className="text-border-input-primary-pressed" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-[44px] shrink-0 items-center justify-center rounded-full border border-[#202020] bg-[#121110]">
|
||||
<StepIcon icon={Icon} className="text-[#525252]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepConnector({ isComplete }: { isComplete: boolean }) {
|
||||
if (isComplete) {
|
||||
return <div className="bg-border-input-primary-pressed h-14 w-px" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-14 w-px"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"repeating-linear-gradient(to bottom, var(--color-bg-data-muted) 0px, var(--color-bg-data-muted) 4px, transparent 4px, transparent 8px)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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)} />;
|
||||
}
|
||||
|
||||
function isCustomSvgIcon(
|
||||
icon: IconComponent,
|
||||
): icon is (props: IconSvgProps) => ReactElement {
|
||||
return !("displayName" in icon && typeof icon.displayName === "string");
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./organizations/store";
|
||||
export * from "./provider-wizard/store";
|
||||
export * from "./ui/store";
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard";
|
||||
|
||||
import { useProviderWizardStore } from "./store";
|
||||
|
||||
describe("useProviderWizardStore", () => {
|
||||
it("stores provider identity and mode, then resets to defaults", () => {
|
||||
useProviderWizardStore.getState().reset();
|
||||
|
||||
useProviderWizardStore.getState().setProvider({
|
||||
id: "provider-1",
|
||||
type: "aws",
|
||||
uid: "123456789012",
|
||||
alias: "prod-account",
|
||||
});
|
||||
useProviderWizardStore.getState().setVia("role");
|
||||
useProviderWizardStore.getState().setSecretId("secret-1");
|
||||
useProviderWizardStore.getState().setMode(PROVIDER_WIZARD_MODE.UPDATE);
|
||||
|
||||
const afterSet = useProviderWizardStore.getState();
|
||||
expect(afterSet.providerId).toBe("provider-1");
|
||||
expect(afterSet.providerType).toBe("aws");
|
||||
expect(afterSet.providerUid).toBe("123456789012");
|
||||
expect(afterSet.providerAlias).toBe("prod-account");
|
||||
expect(afterSet.via).toBe("role");
|
||||
expect(afterSet.secretId).toBe("secret-1");
|
||||
expect(afterSet.mode).toBe(PROVIDER_WIZARD_MODE.UPDATE);
|
||||
|
||||
useProviderWizardStore.getState().reset();
|
||||
const afterReset = useProviderWizardStore.getState();
|
||||
|
||||
expect(afterReset.providerId).toBeNull();
|
||||
expect(afterReset.providerType).toBeNull();
|
||||
expect(afterReset.providerUid).toBeNull();
|
||||
expect(afterReset.providerAlias).toBeNull();
|
||||
expect(afterReset.via).toBeNull();
|
||||
expect(afterReset.secretId).toBeNull();
|
||||
expect(afterReset.mode).toBe(PROVIDER_WIZARD_MODE.ADD);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
import {
|
||||
PROVIDER_WIZARD_MODE,
|
||||
ProviderWizardIdentity,
|
||||
ProviderWizardMode,
|
||||
} from "@/types/provider-wizard";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
interface ProviderWizardState {
|
||||
providerId: string | null;
|
||||
providerType: ProviderType | null;
|
||||
providerUid: string | null;
|
||||
providerAlias: string | null;
|
||||
via: string | null;
|
||||
secretId: string | null;
|
||||
mode: ProviderWizardMode;
|
||||
setProvider: (provider: ProviderWizardIdentity) => void;
|
||||
setVia: (via: string | null) => void;
|
||||
setSecretId: (secretId: string | null) => void;
|
||||
setMode: (mode: ProviderWizardMode) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
providerId: null,
|
||||
providerType: null,
|
||||
providerUid: null,
|
||||
providerAlias: null,
|
||||
via: null,
|
||||
secretId: null,
|
||||
mode: PROVIDER_WIZARD_MODE.ADD,
|
||||
};
|
||||
|
||||
export const useProviderWizardStore = create<ProviderWizardState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...initialState,
|
||||
setProvider: (provider) =>
|
||||
set({
|
||||
providerId: provider.id,
|
||||
providerType: provider.type,
|
||||
providerUid: provider.uid,
|
||||
providerAlias: provider.alias,
|
||||
}),
|
||||
setVia: (via) => set({ via }),
|
||||
setSecretId: (secretId) => set({ secretId }),
|
||||
setMode: (mode) => set({ mode }),
|
||||
reset: () => set(initialState),
|
||||
}),
|
||||
{
|
||||
name: "provider-wizard-store",
|
||||
},
|
||||
),
|
||||
);
|
||||
+10
-10
@@ -80,55 +80,55 @@ export const addProviderFormSchema = z
|
||||
z.object({
|
||||
providerType: z.literal("aws"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(12, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("azure"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
awsCredentialsType: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("m365"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("gcp"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
awsCredentialsType: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("kubernetes"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
awsCredentialsType: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("github"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("iac"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("oraclecloud"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("mongodbatlas"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("alibabacloud"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from "./filters";
|
||||
export * from "./formSchemas";
|
||||
export * from "./organizations";
|
||||
export * from "./processors";
|
||||
export * from "./provider-wizard";
|
||||
export * from "./providers";
|
||||
export * from "./resources";
|
||||
export * from "./scans";
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ProviderType } from "./providers";
|
||||
|
||||
export const PROVIDER_WIZARD_STEP = {
|
||||
CONNECT: 0,
|
||||
CREDENTIALS: 1,
|
||||
TEST: 2,
|
||||
LAUNCH: 3,
|
||||
} as const;
|
||||
|
||||
export type ProviderWizardStep =
|
||||
(typeof PROVIDER_WIZARD_STEP)[keyof typeof PROVIDER_WIZARD_STEP];
|
||||
|
||||
export const PROVIDER_WIZARD_MODE = {
|
||||
ADD: "add",
|
||||
UPDATE: "update",
|
||||
} as const;
|
||||
|
||||
export type ProviderWizardMode =
|
||||
(typeof PROVIDER_WIZARD_MODE)[keyof typeof PROVIDER_WIZARD_MODE];
|
||||
|
||||
export interface ProviderWizardIdentity {
|
||||
id: string;
|
||||
type: ProviderType;
|
||||
uid: string | null;
|
||||
alias: string | null;
|
||||
}
|
||||
Reference in new issue
Block a user