Skip to content

Commit

Permalink
wizard: add user information step (HMS-4903)
Browse files Browse the repository at this point in the history
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.
[x] [wip> unit tests- in progress because I want to make sure that all new fields are in a good shape.
  • Loading branch information
mgold1234 committed Nov 26, 2024
1 parent 35e2706 commit c5ac229
Show file tree
Hide file tree
Showing 11 changed files with 778 additions and 8 deletions.
20 changes: 19 additions & 1 deletion src/Components/CreateImageWizard/CreateImageWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
useFirstBootValidation,
useDetailsValidation,
useRegistrationValidation,
useUserValidation,
} from './utilities/useValidation';
import {
isAwsAccountIdValid,
Expand Down Expand Up @@ -65,6 +66,9 @@ import {
selectGcpShareMethod,
selectImageTypes,
addImageType,
selectUserName,
selectUserSshKey,
selectUserPassword,
} from '../../store/wizardSlice';
import { resolveRelPath } from '../../Utilities/path';
import { useFlag } from '../../Utilities/useGetEnvironment';
Expand Down Expand Up @@ -205,6 +209,9 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const azureSubscriptionId = useAppSelector(selectAzureSubscriptionId);
const azureResourceGroup = useAppSelector(selectAzureResourceGroup);
const azureSource = useAppSelector(selectAzureSource);
const userName = useAppSelector(selectUserName);
const userPassword = useAppSelector(selectUserPassword);
const userSshKey = useAppSelector(selectUserSshKey);
// Registration
const registrationValidation = useRegistrationValidation();
// Snapshots
Expand All @@ -216,6 +223,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const firstBootValidation = useFirstBootValidation();
// Details
const detailsValidation = useDetailsValidation();
// User
const userValidation = useUserValidation();

let startIndex = 1; // default index
if (isEdit) {
Expand Down Expand Up @@ -448,7 +457,16 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
key="wizard-users"
isHidden={!isUsersEnabled}
footer={
<CustomWizardFooter disableNext={false} optional={true} />
<CustomWizardFooter
disableNext={
!(
userName === '' &&
userPassword === '' &&
userSshKey === ''
) && userValidation.disabledNext
}
optional={true}
/>
}
>
<UsersStep />
Expand Down
155 changes: 154 additions & 1 deletion src/Components/CreateImageWizard/ValidatedTextInput.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -30,6 +34,28 @@ interface HookValidatedTextInputPropTypes extends TextInputProps {
warning?: string;
}

interface HookValidatedTextAreaPropTypes extends TextAreaProps {
dataTestId?: string | undefined;
ariaLabel?: string;
value: string;
placeholder?: string;
stepValidation: StepValidation;
fieldName: string;
}

interface HookValidatedTextInputWithButtonPropTypes extends TextInputProps {
dataTestId?: string | undefined;
ouiaId?: string;
ariaLabel: string | undefined;
value: string;
placeholder?: string;
stepValidation: StepValidation;
fieldName: string;
togglePasswordVisibility: () => void;
isPasswordVisible: boolean;
isEmpty: boolean;
}

export const HookValidatedInput = ({
dataTestId,
ouiaId,
Expand Down Expand Up @@ -90,6 +116,76 @@ export const HookValidatedInput = ({
);
};

export const HookValidatedInputWithButton = ({
dataTestId,
ouiaId,
ariaLabel,
value,
placeholder,
onChange,
stepValidation,
fieldName,
type = 'text',
isEmpty,
togglePasswordVisibility,
isPasswordVisible,
}: HookValidatedTextInputWithButtonPropTypes) => {
const [isPristine, setIsPristine] = useState(!value ? true : false);
// Do not surface validation on pristine state components
// Allow step validation to be set on pristine state, when needed
const validated = isEmpty
? 'default'
: isPristine
? 'default'
: stepValidation.errors[fieldName] === 'default'
? 'default'
: stepValidation.errors[fieldName]
? 'error'
: 'success';

const handleBlur = () => {
setIsPristine(false);
};
return (
<>
<div
style={{ position: 'relative', display: 'flex', alignItems: 'center' }}
>
<TextInput
value={value}
data-testid={dataTestId}
ouiaId={ouiaId}
type={type}
onChange={onChange}
validated={validated}
aria-label={ariaLabel}
onBlur={handleBlur}
placeholder={placeholder}
style={{ paddingRight: '2rem' }}
/>
<Button
variant="plain"
onClick={togglePasswordVisibility}
aria-label="Toggle password visibility"
style={{
position: 'absolute',
right: '0.5rem',
}}
>
{isPasswordVisible ? <EyeSlashIcon /> : <EyeIcon />}
</Button>
</div>
{validated === 'error' && (
<HelperText>
<HelperTextItem variant="error" hasIcon>
{stepValidation.errors[fieldName]}
</HelperTextItem>
</HelperText>
)}
</>
);
};

export const ValidatedTextInput = ({
dataTestId,
ouiaId,
Expand Down Expand Up @@ -137,3 +233,60 @@ export const ValidatedTextInput = ({
</>
);
};

export const HookValidatedTextArea = ({
dataTestId,
ariaLabel,
value,
placeholder,
onChange,
stepValidation,
fieldName,
type = 'text',
isDisabled = false,
}: HookValidatedTextAreaPropTypes) => {
const [isPristine, setIsPristine] = useState(!value ? true : false);
// Do not surface validation on pristine state components
// Allow step validation to be set on pristine state, when needed
const validated = isDisabled
? 'default'
: isPristine
? 'default'
: stepValidation.errors[fieldName] === 'default'
? 'default'
: stepValidation.errors[fieldName]
? 'error'
: 'success';

const handleBlur = () => {
setIsPristine(false);
};
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
if (onChange) {
onChange(event, event.target.value);
}
};

return (
<>
<TextArea
value={value}
data-testid={dataTestId}
type={type}
onChange={handleChange}
validated={validated}
aria-label={ariaLabel}
onBlur={handleBlur}
placeholder={placeholder}
isDisabled={isDisabled}
/>
{validated === 'error' && (
<HelperText>
<HelperTextItem variant="error" hasIcon>
{stepValidation.errors[fieldName]}
</HelperTextItem>
</HelperText>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@ import {
} from '@patternfly/react-core';
import UserIcon from '@patternfly/react-icons/dist/esm/icons/user-icon';

const EmptyUserState = () => {
interface EmptyUserStateProps {
onAddUserClick: () => void;
}
const EmptyUserState = ({ onAddUserClick }: EmptyUserStateProps) => {
return (
<EmptyState variant={EmptyStateVariant.lg}>
<EmptyStateHeader
icon={<EmptyStateIcon icon={UserIcon} />}
headingLevel="h4"
/>
<EmptyStateFooter>
<Button variant="secondary" onClick={() => {}}>
<Button variant="secondary" onClick={onAddUserClick}>
Add a user
</Button>
</EmptyStateFooter>
</EmptyState>
);
};

export default EmptyUserState;
Loading

0 comments on commit c5ac229

Please sign in to comment.