From 883c5d4e56d6d1b4470026527393f570c11f5294 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:43:20 +0200 Subject: [PATCH] feat: client side validation (#8161) --- .../integrations/forms/saml-config-form.tsx | 95 +++++++++++++++---- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/ui/components/integrations/forms/saml-config-form.tsx b/ui/components/integrations/forms/saml-config-form.tsx index 31c63bf4e0..ecea01343f 100644 --- a/ui/components/integrations/forms/saml-config-form.tsx +++ b/ui/components/integrations/forms/saml-config-form.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { useFormState } from "react-dom"; +import { z } from "zod"; import { createSamlConfig, updateSamlConfig } from "@/actions/integrations"; import { AddIcon } from "@/components/icons"; @@ -26,13 +27,40 @@ export const SamlConfigForm = ({ const [emailDomain, setEmailDomain] = useState( samlConfig?.attributes?.email_domain || "", ); - const [uploadedFile, setUploadedFile] = useState<{ - name: string; - uploaded: boolean; - }>({ name: "", uploaded: false }); + const [uploadedFile, setUploadedFile] = useState(null); + const [clientErrors, setClientErrors] = useState<{ + email_domain?: string | null; + metadata_xml?: string | null; + }>({}); const formRef = useRef(null); const { toast } = useToast(); + // Client-side validation function + const validateFields = (email: string, hasFile: boolean) => { + // Validar cada campo por separado para poder limpiarlos individualmente + const emailValidation = z + .string() + .trim() + .min(1, { message: "Email domain is required" }) + .safeParse(email); + const metadataValidation = z + .string() + .trim() + .min(1, { message: "Metadata XML is required" }) + .safeParse(hasFile ? "dummy_xml_content" : ""); + + const newErrors = { + email_domain: emailValidation.success + ? null + : emailValidation.error.issues[0]?.message, + metadata_xml: metadataValidation.success + ? null + : metadataValidation.error.issues[0]?.message, + }; + + setClientErrors(newErrors); + }; + useEffect(() => { if (state?.success) { toast({ @@ -52,7 +80,8 @@ export const SamlConfigForm = ({ const handleFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) { - setUploadedFile({ name: "", uploaded: false }); + setUploadedFile(null); + validateFields(emailDomain, false); return; } @@ -70,7 +99,8 @@ export const SamlConfigForm = ({ }); // Clear the file input event.target.value = ""; - setUploadedFile({ name: "", uploaded: false }); + setUploadedFile(null); + validateFields(emailDomain, false); return; } @@ -88,7 +118,8 @@ export const SamlConfigForm = ({ }); // Clear the file input event.target.value = ""; - setUploadedFile({ name: "", uploaded: false }); + setUploadedFile(null); + validateFields(emailDomain, false); return; } @@ -100,8 +131,8 @@ export const SamlConfigForm = ({ xmlInput.value = content; } - // Update file state - setUploadedFile({ name: file.name, uploaded: true }); + setUploadedFile(file); + validateFields(emailDomain, true); toast({ title: "File uploaded successfully", @@ -117,7 +148,8 @@ export const SamlConfigForm = ({ }); // Clear the file input event.target.value = ""; - setUploadedFile({ name: "", uploaded: false }); + setUploadedFile(null); + validateFields(emailDomain, false); }; reader.readAsText(file); @@ -137,11 +169,21 @@ export const SamlConfigForm = ({ labelPlacement="outside" variant="bordered" isRequired={true} - isInvalid={!!state?.errors?.email_domain} - errorMessage={state?.errors?.email_domain} + isInvalid={ + !!(clientErrors.email_domain === null + ? undefined + : clientErrors.email_domain || state?.errors?.email_domain) + } + errorMessage={ + clientErrors.email_domain === null + ? undefined + : clientErrors.email_domain || state?.errors?.email_domain + } value={emailDomain} onChange={(e: React.ChangeEvent) => { - setEmailDomain(e.target.value); + const newValue = e.target.value; + setEmailDomain(newValue); + validateFields(newValue, !!uploadedFile); }} /> @@ -173,6 +215,15 @@ export const SamlConfigForm = ({ /> +
+ + Name ID Format: + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + +
+
Supported Assertion Attributes: @@ -214,15 +265,19 @@ export const SamlConfigForm = ({ }} startContent={} className={`h-10 justify-start rounded-medium border-2 text-default-500 ${ - state?.errors?.metadata_xml + ( + clientErrors.metadata_xml === null + ? undefined + : clientErrors.metadata_xml || state?.errors?.metadata_xml + ) ? "border-red-500" - : uploadedFile.uploaded + : uploadedFile ? "border-green-500 bg-green-50 dark:bg-green-900/20" : "border-default-200" }`} > - {uploadedFile.uploaded ? ( + {uploadedFile ? ( {uploadedFile.name} @@ -246,7 +301,13 @@ export const SamlConfigForm = ({ Upload your Identity Provider's SAML metadata XML file

- {state?.errors?.metadata_xml} + {(() => { + const finalError = + clientErrors.metadata_xml === null + ? undefined + : clientErrors.metadata_xml || state?.errors?.metadata_xml; + return finalError; + })()}