From a8be4ecbdd0730facf9dff8b8ad9857584842ea3 Mon Sep 17 00:00:00 2001 From: Michal Gold Date: Sun, 10 Nov 2024 11:23:02 +0200 Subject: [PATCH] wizard: add user information step (HMS-4903) This commit introduces the user information step with the following fields: (*) `userName` field with validation according to Red Hat guidelines: https://access.redhat.com/solutions/30164 (*) `password` field with validation using the pam_pwquality module, which enforces strict password policies for Linux systems. This module enhances security by enforcing minimum length, complexity, and avoiding weak passwords. More info: https://access.redhat.com/solutions/6979714 Validation is configured based on the requirements defined for our systems: https://github.com/osbuild/osbuild-composer/tree/main/test/data/manifests https://github.com/osbuild/images/pkg/distro/rhel/rhel10 (*) `confirm password` field that check if confirm password equal to password (*) `ssh key` field - implement the same validation as we have in edge. (*) unit tests- in progress because I want to make sure that all new fields are in a good shape. (*) - user validation will be done in a following pr. --- .../CreateImageWizard/CreateImageWizard.tsx | 1 - .../CreateImageWizard/ValidatedTextInput.tsx | 200 +++++++++++++++--- .../steps/Users/component/Empty.tsx | 9 +- .../steps/Users/component/UserInfo.tsx | 183 ++++++++++++++++ .../CreateImageWizard/steps/Users/index.tsx | 8 +- .../utilities/requestMapper.ts | 37 +++- .../CreateImageWizard/validators.ts | 32 +++ src/store/wizardSlice.ts | 110 ++++++++++ .../steps/Users/Users.test.tsx | 107 ++++++++++ 9 files changed, 658 insertions(+), 29 deletions(-) create mode 100644 src/Components/CreateImageWizard/steps/Users/component/UserInfo.tsx create mode 100644 src/test/Components/CreateImageWizard/steps/Users/Users.test.tsx diff --git a/src/Components/CreateImageWizard/CreateImageWizard.tsx b/src/Components/CreateImageWizard/CreateImageWizard.tsx index f1c5b38d5..5f2f6ec4c 100644 --- a/src/Components/CreateImageWizard/CreateImageWizard.tsx +++ b/src/Components/CreateImageWizard/CreateImageWizard.tsx @@ -225,7 +225,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { const firstBootValidation = useFirstBootValidation(); // Details const detailsValidation = useDetailsValidation(); - let startIndex = 1; // default index if (isEdit) { startIndex = 20; diff --git a/src/Components/CreateImageWizard/ValidatedTextInput.tsx b/src/Components/CreateImageWizard/ValidatedTextInput.tsx index 607450349..ea24d6035 100644 --- a/src/Components/CreateImageWizard/ValidatedTextInput.tsx +++ b/src/Components/CreateImageWizard/ValidatedTextInput.tsx @@ -1,11 +1,15 @@ -import React, { useState } from 'react'; +import React, { ChangeEvent, useState } from 'react'; import { HelperText, HelperTextItem, TextInput, TextInputProps, + Button, + TextAreaProps, + TextArea, } from '@patternfly/react-core'; +import { EyeSlashIcon, EyeIcon } from '@patternfly/react-icons'; import type { StepValidation } from './utilities/useValidation'; @@ -30,6 +34,62 @@ interface HookValidatedTextInputPropTypes extends TextInputProps { warning?: string; } +interface HookValidatedTextAreaPropTypes extends TextAreaProps { + dataTestId?: string | undefined; + ariaLabel: string | undefined; + value: string; + placeholder?: string; + stepValidation: StepValidation; + fieldName: string; + isEmpty: boolean; +} + +interface HookValidatedTextInputWithButtonPropTypes + extends HookValidatedTextInputPropTypes { + togglePasswordVisibility: () => void; + isPasswordVisible: boolean; + isEmpty: boolean; +} + +const getValidationState = ( + value: string, + stepValidation: StepValidation, + fieldName: string, + isPristine: boolean, + isEmpty?: boolean +): 'default' | 'error' | 'success' => { + if (isEmpty) return 'default'; + if (isPristine) return 'default'; + if (stepValidation.errors[fieldName] === 'default') return 'default'; + return stepValidation.errors[fieldName] ? 'error' : 'success'; +}; + +const ErrorHelperText = ({ + errorMessage, +}: { + errorMessage?: string | undefined; +}) => { + if (!errorMessage) return null; + + return ( + + + {errorMessage} + + + ); +}; + +const usePristineState = (initialValue: string) => { + const [isPristine, setIsPristine] = useState(!initialValue); + + const handleBlur = () => { + setIsPristine(false); + }; + + return { isPristine, handleBlur }; +}; + export const HookValidatedInput = ({ dataTestId, ouiaId, @@ -43,20 +103,21 @@ export const HookValidatedInput = ({ type = 'text', warning = undefined, }: HookValidatedTextInputPropTypes) => { - const [isPristine, setIsPristine] = useState(!value ? true : false); + const { isPristine, handleBlur } = usePristineState(value); // Do not surface validation on pristine state components // Allow step validation to be set on pristine state, when needed - const validated = isPristine - ? 'default' - : stepValidation.errors[fieldName] === 'default' - ? 'default' - : stepValidation.errors[fieldName] - ? 'error' - : 'success'; - const handleBlur = () => { - setIsPristine(false); - }; + const errorMessage = + !isPristine && stepValidation.errors[fieldName] + ? stepValidation.errors[fieldName] + : undefined; + + const validationState = getValidationState( + value, + stepValidation, + fieldName, + isPristine + ); return ( <> @@ -66,19 +127,13 @@ export const HookValidatedInput = ({ ouiaId={ouiaId || ''} type={type} onChange={onChange!} - validated={validated} + validated={validationState} aria-label={ariaLabel || ''} onBlur={handleBlur} placeholder={placeholder || ''} isDisabled={isDisabled || false} /> - {validated === 'error' && ( - - - {stepValidation.errors[fieldName]} - - - )} + {warning !== undefined && ( @@ -90,6 +145,57 @@ export const HookValidatedInput = ({ ); }; +export const HookValidatedInputWithPasswordVisibilityButton = ( + props: HookValidatedTextInputWithButtonPropTypes +) => { + const { togglePasswordVisibility, isPasswordVisible, ...restProps } = props; + const { isPristine, handleBlur } = usePristineState(restProps.value); + + const errorMessage = + !isPristine && restProps.stepValidation.errors[restProps.fieldName] + ? restProps.stepValidation.errors[restProps.fieldName] + : undefined; + + const validationState = getValidationState( + restProps.value, + restProps.stepValidation, + restProps.fieldName, + isPristine + ); + return ( + <> +
+ + +
+ + + ); +}; + export const ValidatedTextInput = ({ dataTestId, ouiaId, @@ -100,11 +206,7 @@ export const ValidatedTextInput = ({ placeholder, onChange, }: ValidatedTextInputPropTypes) => { - const [isPristine, setIsPristine] = useState(!value ? true : false); - - const handleBlur = () => { - setIsPristine(false); - }; + const { isPristine, handleBlur } = usePristineState(value); const handleValidation = () => { // Prevent premature validation during user's first entry @@ -137,3 +239,51 @@ export const ValidatedTextInput = ({ ); }; + +export const HookValidatedTextArea = ({ + dataTestId, + ariaLabel, + value, + placeholder, + onChange, + stepValidation, + fieldName, + type = 'text', + isDisabled = false, + isEmpty, +}: HookValidatedTextAreaPropTypes) => { + const { isPristine, handleBlur } = usePristineState(value); + const validationState = getValidationState( + value, + stepValidation, + fieldName, + isPristine, + isEmpty + ); + const handleChange = (event: ChangeEvent) => { + if (onChange) { + onChange(event, event.target.value); + } + }; + const errorMessage = + !isPristine && stepValidation.errors[fieldName] + ? stepValidation.errors[fieldName] + : undefined; + + return ( + <> +