diff --git a/packages/web/src/hooks/useAdminCreateSamlAuthProvider.js b/packages/web/src/hooks/useAdminCreateSamlAuthProvider.js index 5cf8c6bc..33fa7e79 100644 --- a/packages/web/src/hooks/useAdminCreateSamlAuthProvider.js +++ b/packages/web/src/hooks/useAdminCreateSamlAuthProvider.js @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import api from 'helpers/api'; -import { enqueueSnackbar } from 'notistack'; export default function useAdminCreateSamlAuthProvider() { const queryClient = useQueryClient(); @@ -16,20 +15,6 @@ export default function useAdminCreateSamlAuthProvider() { queryKey: ['admin', 'samlAuthProviders'], }); }, - onError: (error) => { - const errors = Object.entries( - error.response.data.errors || [['', 'Failed while saving!']], - ); - - for (const error of errors) { - enqueueSnackbar(`${error[0] ? error[0] + ': ' : ''} ${error[1]}`, { - variant: 'error', - SnackbarProps: { - 'data-test': 'snackbar-create-saml-auth-provider-error', - }, - }); - } - }, }); return query; diff --git a/packages/web/src/hooks/useAdminUpdateSamlAuthProvider.js b/packages/web/src/hooks/useAdminUpdateSamlAuthProvider.js index ab7dd2fa..9e6600f4 100644 --- a/packages/web/src/hooks/useAdminUpdateSamlAuthProvider.js +++ b/packages/web/src/hooks/useAdminUpdateSamlAuthProvider.js @@ -1,6 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import api from 'helpers/api'; -import { enqueueSnackbar } from 'notistack'; export default function useAdminUpdateSamlAuthProvider(samlAuthProviderId) { const queryClient = useQueryClient(); @@ -19,20 +18,6 @@ export default function useAdminUpdateSamlAuthProvider(samlAuthProviderId) { queryKey: ['admin', 'samlAuthProviders'], }); }, - onError: (error) => { - const errors = Object.entries( - error.response.data.errors || [['', 'Failed while saving!']], - ); - - for (const error of errors) { - enqueueSnackbar(`${error[0] ? error[0] + ': ' : ''} ${error[1]}`, { - variant: 'error', - SnackbarProps: { - 'data-test': 'snackbar-update-saml-auth-provider-error', - }, - }); - } - }, }); return query; diff --git a/packages/web/src/hooks/useSamlAuthProvider.js b/packages/web/src/hooks/useSamlAuthProvider.js index c293eafd..ee575beb 100644 --- a/packages/web/src/hooks/useSamlAuthProvider.js +++ b/packages/web/src/hooks/useSamlAuthProvider.js @@ -4,7 +4,7 @@ import api from 'helpers/api'; export default function useSamlAuthProvider({ samlAuthProviderId } = {}) { const query = useQuery({ - queryKey: ['samlAuthProviders', samlAuthProviderId], + queryKey: ['admin', 'samlAuthProviders', samlAuthProviderId], queryFn: async ({ signal }) => { const { data } = await api.get( `/v1/admin/saml-auth-providers/${samlAuthProviderId}`, diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 3307dfda..210f2390 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -290,12 +290,13 @@ "authenticationForm.defaultRole": "Default role", "authenticationForm.successfullySaved": "The provider has been saved.", "authenticationForm.save": "Save", + "authenticationForm.mandatoryInput": "{inputName} is required.", "roleMappingsForm.title": "Role mappings", "roleMappingsForm.remoteRoleName": "Remote role name", "roleMappingsForm.role": "Role", "roleMappingsForm.appendRoleMapping": "Append", "roleMappingsForm.save": "Save", - "roleMappingsForm.notFound": "No role mappings have found.", + "roleMappingsForm.notFound": "No role mappings have been found.", "roleMappingsForm.successfullySaved": "Role mappings have been saved.", "adminApps.title": "Apps", "adminApps.connections": "Connections", diff --git a/packages/web/src/pages/Authentication/RoleMappings.jsx b/packages/web/src/pages/Authentication/RoleMappings.jsx index 4440177b..f306a0f7 100644 --- a/packages/web/src/pages/Authentication/RoleMappings.jsx +++ b/packages/web/src/pages/Authentication/RoleMappings.jsx @@ -3,8 +3,8 @@ import LoadingButton from '@mui/lab/LoadingButton'; import Divider from '@mui/material/Divider'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; -import { useMemo } from 'react'; +import Alert from '@mui/material/Alert'; +import { useMemo, useState } from 'react'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; @@ -63,11 +63,11 @@ const getValidationSchema = (formatMessage) => function RoleMappings({ provider, providerLoading }) { const formatMessage = useFormatMessage(); - const enqueueSnackbar = useEnqueueSnackbar(); const { mutateAsync: updateRoleMappings, isPending: isUpdateRoleMappingsPending, + isSuccess: isUpdateRoleMappingsSuccess, } = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id); const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } = @@ -75,9 +75,12 @@ function RoleMappings({ provider, providerLoading }) { adminSamlAuthProviderId: provider?.id, }); const roleMappings = data?.data; + const fieldNames = ['remoteRoleName', 'roleId']; + const [fieldErrors, setFieldErrors] = useState(null); const handleRoleMappingsUpdate = async (values) => { try { + setFieldErrors(null); if (provider?.id) { await updateRoleMappings( values.roleMappings.map(({ roleId, remoteRoleName }) => ({ @@ -85,29 +88,20 @@ function RoleMappings({ provider, providerLoading }) { remoteRoleName, })), ); - - enqueueSnackbar(formatMessage('roleMappingsForm.successfullySaved'), { - variant: 'success', - SnackbarProps: { - 'data-test': 'snackbar-update-role-mappings-success', - }, - }); } } catch (error) { - const errors = Object.values( - error.response.data.errors || [['Failed while saving!']], - ); - - for (const [error] of errors) { - enqueueSnackbar(error, { - variant: 'error', - SnackbarProps: { - 'data-test': 'snackbar-update-role-mappings-error', - }, + const errors = error?.response?.data?.errors; + if (errors) { + Object.entries(errors).forEach(([fieldName, fieldErrors]) => { + if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { + setFieldErrors((prevErrors) => [ + ...(prevErrors || []), + `${fieldName}: ${fieldErrors.join(', ')}`, + ]); + } }); } - - throw new Error('Failed while saving!'); + throw errors || error; } }; @@ -118,6 +112,25 @@ function RoleMappings({ provider, providerLoading }) { [roleMappings], ); + const renderErrors = (errors) => { + const generalError = errors?.root?.general?.message; + if (fieldErrors) { + return fieldErrors.map((error, index) => ( + + {error} + + )); + } + + if (generalError) { + return ( + + {generalError} + + ); + } + }; + if ( providerLoading || !provider?.id || @@ -140,27 +153,35 @@ function RoleMappings({ provider, providerLoading }) { reValidateMode="onChange" noValidate automaticValidation={false} - > - - - - {formatMessage('roleMappingsForm.save')} - - - + render={({ formState: { errors, isDirty } }) => ( + + + {renderErrors(errors)} + {isUpdateRoleMappingsSuccess && !isDirty && ( + + {formatMessage('roleMappingsForm.successfullySaved')} + + )} + + {formatMessage('roleMappingsForm.save')} + + + )} + /> ); } RoleMappings.propTypes = { provider: PropTypes.shape({ - id: PropTypes.oneOf([PropTypes.number, PropTypes.string]).isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, }), providerLoading: PropTypes.bool, }; diff --git a/packages/web/src/pages/Authentication/SamlConfiguration.jsx b/packages/web/src/pages/Authentication/SamlConfiguration.jsx index 47488241..73ff8c2d 100644 --- a/packages/web/src/pages/Authentication/SamlConfiguration.jsx +++ b/packages/web/src/pages/Authentication/SamlConfiguration.jsx @@ -2,9 +2,11 @@ import PropTypes from 'prop-types'; import LoadingButton from '@mui/lab/LoadingButton'; import Stack from '@mui/material/Stack'; import MuiTextField from '@mui/material/TextField'; +import Alert from '@mui/material/Alert'; import * as React from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; -import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import ControlledAutocomplete from 'components/ControlledAutocomplete'; import Form from 'components/Form'; import Switch from 'components/Switch'; @@ -28,29 +30,94 @@ const defaultValues = { defaultRoleId: '', }; +const getValidationSchema = (formatMessage) => { + const getMandatoryFieldMessage = (fieldTranslationId) => + formatMessage('authenticationForm.mandatoryInput', { + inputName: formatMessage(fieldTranslationId), + }); + + return yup.object().shape({ + active: yup.boolean(), + name: yup + .string() + .trim() + .required(getMandatoryFieldMessage('authenticationForm.name')), + certificate: yup + .string() + .trim() + .required(getMandatoryFieldMessage('authenticationForm.certificate')), + signatureAlgorithm: yup + .string() + .trim() + .required( + getMandatoryFieldMessage('authenticationForm.signatureAlgorithm'), + ), + issuer: yup + .string() + .trim() + .required(getMandatoryFieldMessage('authenticationForm.issuer')), + entryPoint: yup + .string() + .trim() + .required(getMandatoryFieldMessage('authenticationForm.entryPoint')), + firstnameAttributeName: yup + .string() + .trim() + .required( + getMandatoryFieldMessage('authenticationForm.firstnameAttributeName'), + ), + surnameAttributeName: yup + .string() + .trim() + .required( + getMandatoryFieldMessage('authenticationForm.surnameAttributeName'), + ), + emailAttributeName: yup + .string() + .trim() + .required( + getMandatoryFieldMessage('authenticationForm.emailAttributeName'), + ), + roleAttributeName: yup + .string() + .trim() + .required( + getMandatoryFieldMessage('authenticationForm.roleAttributeName'), + ), + defaultRoleId: yup + .string() + .trim() + .required(getMandatoryFieldMessage('authenticationForm.defaultRole')), + }); +}; + function generateRoleOptions(roles) { return roles?.map(({ name: label, id: value }) => ({ label, value })); } function SamlConfiguration({ provider, providerLoading }) { const formatMessage = useFormatMessage(); - const { data, loading: isRolesLoading } = useRoles(); + const { data, isLoading: isRolesLoading } = useRoles(); const roles = data?.data; - const enqueueSnackbar = useEnqueueSnackbar(); const { mutateAsync: createSamlAuthProvider, isPending: isCreateSamlAuthProviderPending, + isSuccess: isCreateSamlAuthProviderSuccess, } = useAdminCreateSamlAuthProvider(); const { mutateAsync: updateSamlAuthProvider, isPending: isUpdateSamlAuthProviderPending, + isSuccess: isUpdateSamlAuthProviderSuccess, } = useAdminUpdateSamlAuthProvider(provider?.id); const isPending = isCreateSamlAuthProviderPending || isUpdateSamlAuthProviderPending; + const isSuccess = + isCreateSamlAuthProviderSuccess || isUpdateSamlAuthProviderSuccess; + const handleSubmit = async (providerData) => { try { if (provider?.id) { @@ -58,15 +125,9 @@ function SamlConfiguration({ provider, providerLoading }) { } else { await createSamlAuthProvider(providerData); } - - enqueueSnackbar(formatMessage('authenticationForm.successfullySaved'), { - variant: 'success', - SnackbarProps: { - 'data-test': 'snackbar-save-saml-provider-success', - }, - }); - } catch { - throw new Error('Failed while saving!'); + } catch (error) { + const errors = error?.response?.data?.errors; + throw errors || error; } }; @@ -75,103 +136,145 @@ function SamlConfiguration({ provider, providerLoading }) { } return ( -
- - - - - ( - + ( + + + + + ( + + )} + /> + + + + + + + ( + + )} + loading={isRolesLoading} + /> + {errors?.root?.general && ( + + {errors.root.general.message} + )} - /> - - - - - - - ( - + {isSuccess && !isDirty && ( + + {formatMessage('authenticationForm.successfullySaved')} + )} - loading={isRolesLoading} - /> - - {formatMessage('authenticationForm.save')} - - - + + {formatMessage('authenticationForm.save')} + +
+ )} + /> ); }