mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Password Field Improvements (#36160)
* Password: added show password functionality added autcomplete props created password component Fixes #28721 * addressed review changes and added unit tests * wrapped passwordField component in forwardRef * fix validation and tests
This commit is contained in:
parent
dc8874cd2e
commit
c26bd6c52f
@ -1,7 +1,8 @@
|
|||||||
import React, { FC, SyntheticEvent } from 'react';
|
import React, { FC, SyntheticEvent } from 'react';
|
||||||
import { Tooltip, Form, Field, Input, VerticalGroup, Button } from '@grafana/ui';
|
import { Tooltip, Form, Field, VerticalGroup, Button } from '@grafana/ui';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { submitButton } from '../Login/LoginForm';
|
import { submitButton } from '../Login/LoginForm';
|
||||||
|
import { PasswordField } from '../PasswordField/PasswordField';
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: (pw: string) => void;
|
onSubmit: (pw: string) => void;
|
||||||
onSkip?: (event?: SyntheticEvent) => void;
|
onSkip?: (event?: SyntheticEvent) => void;
|
||||||
@ -21,21 +22,19 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
|
|||||||
{({ errors, register, getValues }) => (
|
{({ errors, register, getValues }) => (
|
||||||
<>
|
<>
|
||||||
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
|
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
|
||||||
<Input
|
<PasswordField
|
||||||
autoFocus
|
|
||||||
id="new-password"
|
id="new-password"
|
||||||
type="password"
|
autoFocus
|
||||||
{...register('newPassword', {
|
autoComplete="new-password"
|
||||||
required: 'New password is required',
|
{...register('newPassword', { required: 'New Password is required' })}
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
||||||
<Input
|
<PasswordField
|
||||||
id="confirm-new-password"
|
id="confirm-new-password"
|
||||||
type="password"
|
autoComplete="new-password"
|
||||||
{...register('confirmNew', {
|
{...register('confirmNew', {
|
||||||
required: 'Confirmed password is required',
|
required: 'Confirmed Password is required',
|
||||||
validate: (v: string) => v === getValues().newPassword || 'Passwords must match!',
|
validate: (v: string) => v === getValues().newPassword || 'Passwords must match!',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -48,8 +48,8 @@ describe('ChangePassword Page', () => {
|
|||||||
render(<ChangePasswordPage {...props} />);
|
render(<ChangePasswordPage {...props} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
|
||||||
expect(await screen.findByText('New password is required')).toBeInTheDocument();
|
expect(await screen.findByText('New Password is required')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Confirmed password is required')).toBeInTheDocument();
|
expect(screen.getByText('Confirmed Password is required')).toBeInTheDocument();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await userEvent.type(screen.getByLabelText('New password'), 'admin');
|
await userEvent.type(screen.getByLabelText('New password'), 'admin');
|
||||||
|
@ -4,6 +4,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { FormModel } from './LoginCtrl';
|
import { FormModel } from './LoginCtrl';
|
||||||
import { Button, Form, Input, Field } from '@grafana/ui';
|
import { Button, Form, Input, Field } from '@grafana/ui';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { PasswordField } from '../PasswordField/PasswordField';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
@ -39,11 +40,11 @@ export const LoginForm: FC<Props> = ({ children, onSubmit, isLoggingIn, password
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
|
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}>
|
||||||
<Input
|
<PasswordField
|
||||||
|
id="current-password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
passwordHint={passwordHint}
|
||||||
{...register('password', { required: 'Password is required' })}
|
{...register('password', { required: 'Password is required' })}
|
||||||
type="password"
|
|
||||||
placeholder={passwordHint}
|
|
||||||
aria-label={selectors.pages.Login.password}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Button aria-label={selectors.pages.Login.submit} className={submitButton} disabled={isLoggingIn}>
|
<Button aria-label={selectors.pages.Login.submit} className={submitButton} disabled={isLoggingIn}>
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { PasswordField } from './PasswordField';
|
||||||
|
|
||||||
|
describe('PasswordField', () => {
|
||||||
|
const props = {
|
||||||
|
id: 'password',
|
||||||
|
placeholder: 'enter password',
|
||||||
|
'data-testid': 'password-field',
|
||||||
|
};
|
||||||
|
it('should renders correctly', () => {
|
||||||
|
render(<PasswordField {...props} />);
|
||||||
|
expect(screen.getByTestId('password-field')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should able to show password value if clicked on password-reveal icon', () => {
|
||||||
|
render(<PasswordField {...props} />);
|
||||||
|
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'password');
|
||||||
|
fireEvent.click(screen.getByRole('button'));
|
||||||
|
expect(screen.getByTestId('password-field')).toHaveProperty('type', 'text');
|
||||||
|
});
|
||||||
|
});
|
44
public/app/core/components/PasswordField/PasswordField.tsx
Normal file
44
public/app/core/components/PasswordField/PasswordField.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
|
import { Input, IconButton } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
autoFocus?: boolean;
|
||||||
|
autoComplete?: string;
|
||||||
|
id?: string;
|
||||||
|
passwordHint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PasswordField: FC<Props> = React.forwardRef<HTMLInputElement, Props>(
|
||||||
|
({ autoComplete, autoFocus, id, passwordHint, ...props }, ref) => {
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
autoComplete={autoComplete}
|
||||||
|
{...props}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
placeholder={passwordHint}
|
||||||
|
aria-label={selectors.pages.Login.password}
|
||||||
|
ref={ref}
|
||||||
|
suffix={
|
||||||
|
<IconButton
|
||||||
|
name={showPassword ? 'eye-slash' : 'eye'}
|
||||||
|
surface="header"
|
||||||
|
aria-controls={id}
|
||||||
|
aria-expanded={showPassword}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
PasswordField.displayName = 'PasswordField';
|
@ -6,6 +6,7 @@ import appEvents from 'app/core/app_events';
|
|||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
import { InnerBox, LoginLayout } from '../Login/LoginLayout';
|
import { InnerBox, LoginLayout } from '../Login/LoginLayout';
|
||||||
|
import { PasswordField } from '../PasswordField/PasswordField';
|
||||||
|
|
||||||
interface SignupDTO {
|
interface SignupDTO {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -90,23 +91,21 @@ export const SignupPage: FC<Props> = (props) => {
|
|||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
|
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
|
||||||
<Input
|
<PasswordField
|
||||||
id="new-password"
|
id="new-password"
|
||||||
{...register('password', {
|
|
||||||
required: 'Password is required',
|
|
||||||
})}
|
|
||||||
autoFocus
|
autoFocus
|
||||||
type="password"
|
autoComplete="new-password"
|
||||||
|
{...register('password', { required: 'Password is required' })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
|
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
|
||||||
<Input
|
<PasswordField
|
||||||
id="confirm-new-password"
|
id="confirm-new-password"
|
||||||
|
autoComplete="new-password"
|
||||||
{...register('confirm', {
|
{...register('confirm', {
|
||||||
required: 'Confirmed password is required',
|
required: 'Confirmed password is required',
|
||||||
validate: (v) => v === getValues().password || 'Passwords must match!',
|
validate: (v) => v === getValues().password || 'Passwords must match!',
|
||||||
})}
|
})}
|
||||||
type="password"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Button, Field, Form, HorizontalGroup, Input, LinkButton } from '@grafana/ui';
|
import { Button, Field, Form, HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||||
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { UserDTO } from 'app/types';
|
import { UserDTO } from 'app/types';
|
||||||
import { ChangePasswordFields } from './types';
|
import { ChangePasswordFields } from './types';
|
||||||
|
import { PasswordField } from '../../core/components/PasswordField/PasswordField';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
user: UserDTO;
|
user: UserDTO;
|
||||||
@ -34,17 +35,17 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Field label="Old password" invalid={!!errors.oldPassword} error={errors?.oldPassword?.message}>
|
<Field label="Old password" invalid={!!errors.oldPassword} error={errors?.oldPassword?.message}>
|
||||||
<Input
|
<PasswordField
|
||||||
id="old-password"
|
id="current-password"
|
||||||
type="password"
|
autoComplete="current-password"
|
||||||
{...register('oldPassword', { required: 'Old password is required' })}
|
{...register('oldPassword', { required: 'Old password is required' })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
|
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
|
||||||
<Input
|
<PasswordField
|
||||||
id="new-password"
|
id="new-password"
|
||||||
type="password"
|
autoComplete="new-password"
|
||||||
{...register('newPassword', {
|
{...register('newPassword', {
|
||||||
required: 'New password is required',
|
required: 'New password is required',
|
||||||
validate: {
|
validate: {
|
||||||
@ -56,9 +57,9 @@ export const ChangePasswordForm: FC<Props> = ({ user, onChangePassword, isSaving
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Confirm password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
<Field label="Confirm password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
|
||||||
<Input
|
<PasswordField
|
||||||
id="confirm-new-password"
|
id="confirm-new-password"
|
||||||
type="password"
|
autoComplete="new-password"
|
||||||
{...register('confirmNew', {
|
{...register('confirmNew', {
|
||||||
required: 'New password confirmation is required',
|
required: 'New password confirmation is required',
|
||||||
validate: (v) => v === getValues().newPassword || 'Passwords must match',
|
validate: (v) => v === getValues().newPassword || 'Passwords must match',
|
||||||
|
Loading…
Reference in New Issue
Block a user