feat: introduce inline error messages in SamlConfiguration and RoleMappings forms

This commit is contained in:
kasia.oczkowska
2024-12-13 11:12:04 +00:00
parent 819f122106
commit e784215e3c
7 changed files with 314 additions and 174 deletions

View File

@@ -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';
@@ -13,6 +15,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
import useAdminCreateSamlAuthProvider from 'hooks/useAdminCreateSamlAuthProvider';
import useAdminUpdateSamlAuthProvider from 'hooks/useAdminUpdateSamlAuthProvider';
import useRoles from 'hooks/useRoles.ee';
import { getGeneralErrorMessage } from 'helpers/errors';
const defaultValues = {
active: false,
@@ -28,45 +31,126 @@ 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 handleSubmit = async (providerData) => {
const isSuccess =
isCreateSamlAuthProviderSuccess || isUpdateSamlAuthProviderSuccess;
const handleSubmit = async (providerData, e, setError) => {
try {
if (provider?.id) {
await updateSamlAuthProvider(providerData);
} else {
await createSamlAuthProvider(providerData);
}
} catch (error) {
const errors = error?.response?.data?.errors;
if (errors) {
const fieldNames = Object.keys(defaultValues);
Object.entries(errors).forEach(([fieldName, fieldErrors]) => {
if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) {
setError(fieldName, {
type: 'fieldRequestError',
message: fieldErrors.join(', '),
});
}
});
}
enqueueSnackbar(formatMessage('authenticationForm.successfullySaved'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-save-saml-provider-success',
},
const generalError = getGeneralErrorMessage({
error,
fallbackMessage: formatMessage('authenticationForm.error'),
});
} catch {
throw new Error('Failed while saving!');
if (generalError) {
setError('root.general', {
type: 'requestError',
message: generalError,
});
}
}
};
@@ -75,103 +159,145 @@ function SamlConfiguration({ provider, providerLoading }) {
}
return (
<Form defaultValues={provider || defaultValues} onSubmit={handleSubmit}>
<Stack direction="column" gap={2}>
<Switch
name="active"
label={formatMessage('authenticationForm.active')}
/>
<TextField
required={true}
name="name"
label={formatMessage('authenticationForm.name')}
fullWidth
/>
<TextField
required={true}
name="certificate"
label={formatMessage('authenticationForm.certificate')}
fullWidth
multiline
/>
<ControlledAutocomplete
name="signatureAlgorithm"
fullWidth
disablePortal
disableClearable={true}
options={[
{ label: 'SHA1', value: 'sha1' },
{ label: 'SHA256', value: 'sha256' },
{ label: 'SHA512', value: 'sha512' },
]}
renderInput={(params) => (
<MuiTextField
{...params}
label={formatMessage('authenticationForm.signatureAlgorithm')}
/>
<Form
defaultValues={provider || defaultValues}
onSubmit={handleSubmit}
noValidate
resolver={yupResolver(getValidationSchema(formatMessage))}
automaticValidation={false}
render={({ formState: { errors, isDirty } }) => (
<Stack direction="column" gap={2}>
<Switch
name="active"
label={formatMessage('authenticationForm.active')}
/>
<TextField
required={true}
name="name"
label={formatMessage('authenticationForm.name')}
fullWidth
error={!!errors?.name}
helperText={errors?.name?.message}
/>
<TextField
required={true}
name="certificate"
label={formatMessage('authenticationForm.certificate')}
fullWidth
multiline
error={!!errors?.certificate}
helperText={errors?.certificate?.message}
/>
<ControlledAutocomplete
name="signatureAlgorithm"
fullWidth
disablePortal
disableClearable={true}
options={[
{ label: 'SHA1', value: 'sha1' },
{ label: 'SHA256', value: 'sha256' },
{ label: 'SHA512', value: 'sha512' },
]}
showHelperText={false}
renderInput={(params) => (
<MuiTextField
{...params}
label={formatMessage('authenticationForm.signatureAlgorithm')}
required
error={!!errors?.signatureAlgorithm}
helperText={errors?.signatureAlgorithm?.message}
/>
)}
/>
<TextField
required={true}
name="issuer"
label={formatMessage('authenticationForm.issuer')}
fullWidth
error={!!errors?.issuer}
helperText={errors?.issuer?.message}
/>
<TextField
required={true}
name="entryPoint"
label={formatMessage('authenticationForm.entryPoint')}
fullWidth
error={!!errors?.entryPoint}
helperText={errors?.entryPoint?.message}
/>
<TextField
required={true}
name="firstnameAttributeName"
label={formatMessage('authenticationForm.firstnameAttributeName')}
fullWidth
error={!!errors?.firstnameAttributeName}
helperText={errors?.firstnameAttributeName?.message}
/>
<TextField
required={true}
name="surnameAttributeName"
label={formatMessage('authenticationForm.surnameAttributeName')}
fullWidth
error={!!errors?.surnameAttributeName}
helperText={errors?.surnameAttributeName?.message}
/>
<TextField
required={true}
name="emailAttributeName"
label={formatMessage('authenticationForm.emailAttributeName')}
fullWidth
error={!!errors?.emailAttributeName}
helperText={errors?.emailAttributeName?.message}
/>
<TextField
required={true}
name="roleAttributeName"
label={formatMessage('authenticationForm.roleAttributeName')}
fullWidth
error={!!errors?.roleAttributeName}
helperText={errors?.roleAttributeName?.message}
/>
<ControlledAutocomplete
name="defaultRoleId"
fullWidth
disablePortal
disableClearable={true}
options={generateRoleOptions(roles)}
showHelperText={false}
renderInput={(params) => (
<MuiTextField
{...params}
label={formatMessage('authenticationForm.defaultRole')}
required
error={!!errors?.defaultRoleId}
helperText={errors?.defaultRoleId?.message}
/>
)}
loading={isRolesLoading}
/>
{errors?.root?.general && (
<Alert data-test="error-alert" severity="error">
{errors.root.general.message}
</Alert>
)}
/>
<TextField
required={true}
name="issuer"
label={formatMessage('authenticationForm.issuer')}
fullWidth
/>
<TextField
required={true}
name="entryPoint"
label={formatMessage('authenticationForm.entryPoint')}
fullWidth
/>
<TextField
required={true}
name="firstnameAttributeName"
label={formatMessage('authenticationForm.firstnameAttributeName')}
fullWidth
/>
<TextField
required={true}
name="surnameAttributeName"
label={formatMessage('authenticationForm.surnameAttributeName')}
fullWidth
/>
<TextField
required={true}
name="emailAttributeName"
label={formatMessage('authenticationForm.emailAttributeName')}
fullWidth
/>
<TextField
required={true}
name="roleAttributeName"
label={formatMessage('authenticationForm.roleAttributeName')}
fullWidth
/>
<ControlledAutocomplete
name="defaultRoleId"
fullWidth
disablePortal
disableClearable={true}
options={generateRoleOptions(roles)}
renderInput={(params) => (
<MuiTextField
{...params}
label={formatMessage('authenticationForm.defaultRole')}
/>
{isSuccess && !isDirty && (
<Alert data-test="success-alert" severity="success">
{formatMessage('authenticationForm.successfullySaved')}
</Alert>
)}
loading={isRolesLoading}
/>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={isPending}
>
{formatMessage('authenticationForm.save')}
</LoadingButton>
</Stack>
</Form>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={isPending}
disabled={!isDirty}
>
{formatMessage('authenticationForm.save')}
</LoadingButton>
</Stack>
)}
/>
);
}