"use client"; import { RotateCcw } from "lucide-react"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { saveDynamicProviderCredentials } from "@/actions/providers/dynamic-provider-credentials"; import { getProviderSchemas } from "@/actions/providers/provider-schemas"; import { RegistryCredentialFields } from "@/components/providers/workflow/provider-credential-fields"; import { Button } from "@/components/shadcn/button/button"; import { Field, FieldLabel } from "@/components/shadcn/field/field"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/shadcn/select/select"; import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; import { useToast } from "@/components/shadcn/toast"; import { StatusAlert } from "@/components/shared/status-alert"; import { parseRegistryCredentialVariants, type RegistryCredentialSchema, } from "@/lib/provider-credentials/provider-credential-schema"; import { getCredentialDefaults, validateCredentialValues, } from "@/lib/provider-credentials/provider-credential-values"; import { formatRegistryLabel } from "@/lib/registry/labels"; import { useProviderWizardStore } from "@/store/provider-wizard/store"; import type { ProviderSchemasResult, ProviderSecretTypes, } from "@/types/provider-schema"; import { WIZARD_FOOTER_ACTION_TYPE, type WizardFooterConfig, } from "./footer-controls"; interface DynamicCredentialsStepProps { providerId: string; providerType: string; onNext: () => void; onBack: () => void; onFooterChange: (config: WizardFooterConfig) => void; } interface CredentialMethod { id: string; secretType: string; label: string; schema: RegistryCredentialSchema | null; } // Each variant of a secret type's schema is a method of its own. function listCredentialMethods( secretTypes: ProviderSecretTypes, ): CredentialMethod[] { return Object.entries(secretTypes).flatMap(([secretType, value]) => { const label = formatRegistryLabel(secretType); const variants = parseRegistryCredentialVariants(value); if (!variants || variants.length === 1) { return [ { id: secretType, secretType, label, schema: variants?.[0].schema ?? null, }, ]; } return variants.map((variant, index) => ({ id: `${secretType}/${index}`, secretType, label: variant.label ?? `${label} ${index + 1}`, schema: variant.schema, })); }); } function credentialFormError(status: ProviderSchemasResult["status"]) { switch (status) { case "access_denied": return { title: "Access required", description: "Your session may have expired or you may not have permission. Sign in again or contact your administrator.", }; case "unavailable": return { title: "Provider installation unavailable", description: "Install this provider's artifact again in Registry, then try again.", }; case "not_found": return { title: "Credential form unavailable", description: "This provider does not provide a credential form. Contact its publisher or your administrator.", }; case "success": case "malformed": return { title: "Credential form not supported", description: "We could not display this provider's credential form. Contact its publisher or your administrator.", }; default: return { title: "Could not load credential form", description: "Check your connection and try again.", }; } } function DynamicCredentialForm({ providerId, secretType, schema, onNext, onBack, onFooterChange, onLoadingChange, }: Omit & { secretType: string; schema: RegistryCredentialSchema; onLoadingChange: (value: boolean) => void; }) { const { toast } = useToast(); const setSecretId = useProviderWizardStore((state) => state.setSecretId); // Credentials belong only to this form. A new account or authentication // method mounts a fresh instance; no values enter the persisted wizard store. const [values, setValues] = useState(() => getCredentialDefaults(schema)); const [errors, setErrors] = useState>({}); const [saving, setSaving] = useState(false); const inFlight = useRef(false); const mounted = useRef(true); const formId = "provider-wizard-dynamic-credentials-form"; const valid = validateCredentialValues(schema, values).valid; useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); useEffect(() => { onFooterChange({ showBack: true, backLabel: "Back", backDisabled: saving, onBack, showAction: true, actionLabel: "Authenticate", actionDisabled: saving || !valid, actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT, actionFormId: formId, }); }, [onBack, onFooterChange, saving, valid]); return (
{ event.preventDefault(); if (inFlight.current) return; const validation = validateCredentialValues(schema, values); setErrors(validation.errors); if (!validation.valid) return; inFlight.current = true; setSaving(true); onLoadingChange(true); try { const result = await saveDynamicProviderCredentials({ providerId, secretType, secret: validation.secret, }); if (!mounted.current) return; if (result.status === "saved") { setValues({}); setSecretId(result.secretId); toast({ title: "Credentials saved", description: "Test the provider connection to continue.", }); onNext(); } else if (result.status === "invalid") { setErrors(result.errors); } else { const description = result.status === "schema_unavailable" ? "The credential schema is unavailable. Check the installed artifact in Registry and reload the form." : result.status === "access_denied" ? "You no longer have permission to update these credentials. Contact an administrator." : "Check your credentials and try again. Your provider account is already created."; setErrors({ _form: description }); toast({ variant: "destructive", title: "Credentials could not be saved", description, }); } } catch { if (mounted.current) { const description = "Could not save the credentials. Check your connection and retry."; setErrors({ _form: description }); toast({ variant: "destructive", title: "Credentials could not be saved", description, }); } } finally { inFlight.current = false; if (mounted.current) { setSaving(false); onLoadingChange(false); } } }} >
{errors._form && ( {errors._form} )} { setValues((current) => ({ ...current, [name]: value })); setErrors((current) => { const next = { ...current }; delete next[name]; return next; }); }} />
); } function DynamicCredentialsContent(props: DynamicCredentialsStepProps) { const { providerType, onBack, onFooterChange } = props; const [schemas, setSchemas] = useState(null); const [selectedMethod, setSelectedMethod] = useState(""); const [attempt, setAttempt] = useState(0); const [saving, setSaving] = useState(false); useEffect(() => { let active = true; setSchemas(null); setSelectedMethod(""); getProviderSchemas(providerType) .then((result) => { if (active) setSchemas(result); }) .catch(() => { if (active) setSchemas({ status: "error" }); }); return () => { active = false; }; }, [providerType, attempt]); const methods = schemas?.status === "success" ? listCredentialMethods(schemas.secretTypes) : []; const method = methods.find(({ id }) => id === selectedMethod) ?? methods[0]; const schema = method?.schema ?? null; useEffect(() => { if (!schema) onFooterChange({ showBack: true, backLabel: "Back", onBack, showAction: false, actionLabel: "Authenticate", actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, }); }, [schema, onBack, onFooterChange]); if (!schemas) return (
); const error = credentialFormError( schemas.status === "success" && methods.length === 0 ? "not_found" : schemas.status, ); return (
{methods.length > 1 && ( Authentication method )} {method && schema ? ( ) : (
{error.description}
)}
); } export function DynamicCredentialsStep(props: DynamicCredentialsStepProps) { return ( ); }