mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-25 12:54:18 +00:00
refactor(ui): remove discovery loader and simplify wizard
This commit is contained in:
4 files changed
+18
-213
No files matched your search
@@ -1,125 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { getDiscovery } from "@/actions/organizations/organizations";
|
||||
import {
|
||||
buildOrgTreeData,
|
||||
getSelectableAccountIds,
|
||||
} from "@/actions/organizations/organizations.adapter";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { useOrgSetupStore } from "@/store/organizations/store";
|
||||
import { DISCOVERY_STATUS, DiscoveryResult } from "@/types/organizations";
|
||||
|
||||
const POLL_INTERVAL_MS = 3000;
|
||||
const MAX_RETRIES = 60;
|
||||
|
||||
interface OrgDiscoveryLoaderProps {
|
||||
onDiscoveryComplete: () => void;
|
||||
}
|
||||
|
||||
export function OrgDiscoveryLoader({
|
||||
onDiscoveryComplete,
|
||||
}: OrgDiscoveryLoaderProps) {
|
||||
const { organizationId, discoveryId, setDiscovery, setSelectedAccountIds } =
|
||||
useOrgSetupStore();
|
||||
|
||||
const [status, setStatus] = useState<string>(DISCOVERY_STATUS.PENDING);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const retryCountRef = useRef(0);
|
||||
const pollRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const pollDiscovery = async () => {
|
||||
if (!organizationId || !discoveryId) return;
|
||||
|
||||
const result = await getDiscovery(organizationId, discoveryId);
|
||||
|
||||
if (result?.error) {
|
||||
setStatus(DISCOVERY_STATUS.FAILED);
|
||||
setErrorMessage(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const discoveryStatus = result.data.attributes.status;
|
||||
setStatus(discoveryStatus);
|
||||
|
||||
if (discoveryStatus === DISCOVERY_STATUS.SUCCEEDED) {
|
||||
const discoveryResult = result.data.attributes.result as DiscoveryResult;
|
||||
|
||||
// Store discovery result
|
||||
setDiscovery(discoveryId, discoveryResult);
|
||||
|
||||
// Pre-select all selectable accounts
|
||||
const selectableIds = getSelectableAccountIds(discoveryResult);
|
||||
setSelectedAccountIds(selectableIds);
|
||||
|
||||
// Pre-build tree data to ensure it's valid
|
||||
buildOrgTreeData(discoveryResult);
|
||||
|
||||
onDiscoveryComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
if (discoveryStatus === DISCOVERY_STATUS.FAILED) {
|
||||
setErrorMessage(
|
||||
result.data.attributes.error ?? "Discovery failed. Please try again.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Still pending or running — schedule next poll
|
||||
if (retryCountRef.current >= MAX_RETRIES) {
|
||||
setStatus(DISCOVERY_STATUS.FAILED);
|
||||
setErrorMessage("Discovery timed out. Please try again.");
|
||||
return;
|
||||
}
|
||||
retryCountRef.current += 1;
|
||||
pollRef.current = setTimeout(pollDiscovery, POLL_INTERVAL_MS);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
pollDiscovery();
|
||||
|
||||
return () => {
|
||||
if (pollRef.current) clearTimeout(pollRef.current);
|
||||
};
|
||||
// Only run on mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleRetry = () => {
|
||||
setStatus(DISCOVERY_STATUS.PENDING);
|
||||
setErrorMessage(null);
|
||||
retryCountRef.current = 0;
|
||||
pollDiscovery();
|
||||
};
|
||||
|
||||
if (status === DISCOVERY_STATUS.FAILED || errorMessage) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 py-12">
|
||||
<div className="border-destructive/50 bg-destructive/10 text-destructive rounded-md border px-6 py-4 text-center text-sm">
|
||||
{errorMessage ?? "An unknown error occurred during discovery."}
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleRetry}>
|
||||
<RefreshCw className="mr-2 size-4" />
|
||||
Retry Discovery
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 py-12">
|
||||
<Loader2 className="text-primary size-8 animate-spin" />
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-medium">
|
||||
Discovering your AWS Organization...
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
This may take a few moments
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Check, Copy, ExternalLink, Loader2 } from "lucide-react";
|
||||
import { Check, Copy, ExternalLink } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@@ -612,13 +612,6 @@ export function OrgSetupForm({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{setupPhase === ORG_SETUP_PHASE.DETAILS && isSubmitting && (
|
||||
<div className="text-muted-foreground flex items-center justify-end gap-2 text-sm">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Setting up organization...
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { OrgAccountSelection } from "@/components/providers/organizations/org-account-selection";
|
||||
import { OrgDiscoveryLoader } from "@/components/providers/organizations/org-discovery-loader";
|
||||
import { OrgLaunchScan } from "@/components/providers/organizations/org-launch-scan";
|
||||
import { OrgSetupForm } from "@/components/providers/organizations/org-setup-form";
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
@@ -47,13 +46,6 @@ const WIZARD_VARIANT = {
|
||||
|
||||
type WizardVariant = (typeof WIZARD_VARIANT)[keyof typeof WIZARD_VARIANT];
|
||||
|
||||
const VALIDATE_PHASE = {
|
||||
DISCOVERY: "discovery",
|
||||
SELECTION: "selection",
|
||||
} as const;
|
||||
|
||||
type ValidatePhase = (typeof VALIDATE_PHASE)[keyof typeof VALIDATE_PHASE];
|
||||
|
||||
const EMPTY_FOOTER_CONFIG: WizardFooterConfig = {
|
||||
showBack: false,
|
||||
backLabel: "Back",
|
||||
@@ -97,9 +89,6 @@ export function ProviderWizardModal({
|
||||
const [orgCurrentStep, setOrgCurrentStep] = useState<OrgWizardStep>(
|
||||
ORG_WIZARD_STEP.SETUP,
|
||||
);
|
||||
const [validatePhase, setValidatePhase] = useState<ValidatePhase>(
|
||||
VALIDATE_PHASE.DISCOVERY,
|
||||
);
|
||||
const [footerConfig, setFooterConfig] =
|
||||
useState<WizardFooterConfig>(EMPTY_FOOTER_CONFIG);
|
||||
const [providerTypeHint, setProviderTypeHint] = useState<ProviderType | null>(
|
||||
@@ -108,7 +97,7 @@ export function ProviderWizardModal({
|
||||
const [orgSetupPhase, setOrgSetupPhase] = useState<OrgSetupPhase>(
|
||||
ORG_SETUP_PHASE.DETAILS,
|
||||
);
|
||||
const scrollHintRefreshToken = `${wizardVariant}-${currentStep}-${orgCurrentStep}-${orgSetupPhase}-${validatePhase}`;
|
||||
const scrollHintRefreshToken = `${wizardVariant}-${currentStep}-${orgCurrentStep}-${orgSetupPhase}`;
|
||||
const { containerRef, showScrollHint, handleScroll } = useScrollHint({
|
||||
enabled: open,
|
||||
refreshToken: scrollHintRefreshToken,
|
||||
@@ -123,11 +112,7 @@ export function ProviderWizardModal({
|
||||
mode,
|
||||
providerType,
|
||||
} = useProviderWizardStore();
|
||||
const { connectionResults, reset: resetOrgWizard } = useOrgSetupStore();
|
||||
|
||||
const hasConnectionErrors = Object.values(connectionResults).some(
|
||||
(status) => status === "error",
|
||||
);
|
||||
const { reset: resetOrgWizard } = useOrgSetupStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -152,7 +137,6 @@ export function ProviderWizardModal({
|
||||
);
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.CREDENTIALS);
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(initialData.providerType);
|
||||
setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS);
|
||||
@@ -164,7 +148,6 @@ export function ProviderWizardModal({
|
||||
setWizardVariant(WIZARD_VARIANT.PROVIDER);
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT);
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(null);
|
||||
setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS);
|
||||
@@ -185,7 +168,6 @@ export function ProviderWizardModal({
|
||||
setWizardVariant(WIZARD_VARIANT.PROVIDER);
|
||||
setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT);
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(null);
|
||||
setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS);
|
||||
@@ -213,7 +195,6 @@ export function ProviderWizardModal({
|
||||
resetOrgWizard();
|
||||
setWizardVariant(WIZARD_VARIANT.ORGANIZATIONS);
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setValidatePhase(VALIDATE_PHASE.DISCOVERY);
|
||||
setFooterConfig(EMPTY_FOOTER_CONFIG);
|
||||
setProviderTypeHint(null);
|
||||
setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS);
|
||||
@@ -252,33 +233,6 @@ export function ProviderWizardModal({
|
||||
}
|
||||
: footerConfig;
|
||||
|
||||
useEffect(() => {
|
||||
if (isProviderFlow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
orgCurrentStep === ORG_WIZARD_STEP.VALIDATE &&
|
||||
validatePhase === VALIDATE_PHASE.DISCOVERY
|
||||
) {
|
||||
setFooterConfig({
|
||||
showBack: true,
|
||||
backLabel: "Back",
|
||||
onBack: () => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS);
|
||||
},
|
||||
showSecondaryAction: false,
|
||||
secondaryActionLabel: "",
|
||||
secondaryActionVariant: "outline",
|
||||
secondaryActionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
||||
showAction: false,
|
||||
actionLabel: "Next",
|
||||
actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON,
|
||||
});
|
||||
}
|
||||
}, [isProviderFlow, orgCurrentStep, validatePhase]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -313,7 +267,6 @@ export function ProviderWizardModal({
|
||||
orgCurrentStep,
|
||||
orgSetupPhase,
|
||||
)}
|
||||
hasConnectionErrors={hasConnectionErrors}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -362,7 +315,6 @@ export function ProviderWizardModal({
|
||||
onBack={backToProviderFlow}
|
||||
onNext={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.VALIDATE);
|
||||
setValidatePhase(VALIDATE_PHASE.SELECTION);
|
||||
}}
|
||||
onFooterChange={setFooterConfig}
|
||||
onPhaseChange={setOrgSetupPhase}
|
||||
@@ -370,40 +322,27 @@ export function ProviderWizardModal({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isProviderFlow &&
|
||||
orgCurrentStep === ORG_WIZARD_STEP.VALIDATE &&
|
||||
validatePhase === VALIDATE_PHASE.DISCOVERY && (
|
||||
<OrgDiscoveryLoader
|
||||
onDiscoveryComplete={() => {
|
||||
setValidatePhase(VALIDATE_PHASE.SELECTION);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isProviderFlow &&
|
||||
orgCurrentStep === ORG_WIZARD_STEP.VALIDATE &&
|
||||
validatePhase === VALIDATE_PHASE.SELECTION && (
|
||||
<OrgAccountSelection
|
||||
onBack={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setOrgSetupPhase(ORG_SETUP_PHASE.ACCESS);
|
||||
}}
|
||||
onNext={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.LAUNCH);
|
||||
}}
|
||||
onSkip={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.LAUNCH);
|
||||
}}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
)}
|
||||
{!isProviderFlow && orgCurrentStep === ORG_WIZARD_STEP.VALIDATE && (
|
||||
<OrgAccountSelection
|
||||
onBack={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.SETUP);
|
||||
setOrgSetupPhase(ORG_SETUP_PHASE.ACCESS);
|
||||
}}
|
||||
onNext={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.LAUNCH);
|
||||
}}
|
||||
onSkip={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.LAUNCH);
|
||||
}}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isProviderFlow && orgCurrentStep === ORG_WIZARD_STEP.LAUNCH && (
|
||||
<OrgLaunchScan
|
||||
onClose={handleClose}
|
||||
onBack={() => {
|
||||
setOrgCurrentStep(ORG_WIZARD_STEP.VALIDATE);
|
||||
setValidatePhase(VALIDATE_PHASE.SELECTION);
|
||||
}}
|
||||
onFooterChange={setFooterConfig}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { IconComponent, IconSvgProps } from "@/types/components";
|
||||
|
||||
interface WizardStepperProps {
|
||||
currentStep: number;
|
||||
hasConnectionErrors?: boolean;
|
||||
stepOffset?: number;
|
||||
}
|
||||
|
||||
@@ -46,7 +45,6 @@ const STEPS: StepConfig[] = [
|
||||
|
||||
export function WizardStepper({
|
||||
currentStep,
|
||||
hasConnectionErrors: _hasConnectionErrors = false,
|
||||
stepOffset = 0,
|
||||
}: WizardStepperProps) {
|
||||
const activeVisualStep = Math.max(
|
||||
|
||||
Reference in new issue
Block a user