refactor(ui): remove discovery loader and simplify wizard

This commit is contained in:
alejandrobailo committed 2026-02-23 16:50:16 +01:00
1 parent f38908c1d9
commit 5f22761d7d
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(