Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: Improve password fields #19220

Merged
merged 4 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 70 additions & 23 deletions pkg/lib/cockpit-components-password.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@
*/
import cockpit from 'cockpit';
import React, { useState } from 'react';
import { Button } from '@patternfly/react-core/dist/esm/components/Button/index.js';
import { FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { InputGroup, InputGroupItem } from '@patternfly/react-core/dist/esm/components/InputGroup/index.js';
import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
import { Progress, ProgressMeasureLocation, ProgressSize } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { HelpIcon } from '@patternfly/react-icons';
import { EyeIcon, EyeSlashIcon, HelpIcon } from '@patternfly/react-icons';

import { FormHelper } from "cockpit-components-form-helper";

import './cockpit-components-password.scss';
import { Flex, FlexItem } from '@patternfly/react-core';

const _ = cockpit.gettext;

Expand Down Expand Up @@ -60,8 +63,10 @@ export const PasswordFormFields = ({
}) => {
const [password, setPassword] = useState(initial_password || "");
const [passwordConfirm, setConfirmPassword] = useState("");
const [passwordStrength, setPasswordStrength] = useState("");
const [passwordStrength, setPasswordStrength] = useState();
const [passwordMessage, setPasswordMessage] = useState("");
const [passwordHidden, setPasswordHidden] = useState(true);
const [passwordConfirmHidden, setPasswordConfirmHidden] = useState(true);

function onPasswordChanged(value) {
setPassword(value);
Expand All @@ -77,20 +82,34 @@ export const PasswordFormFields = ({
setPasswordMessage(strength.message);
});
} else {
setPasswordStrength("");
setPasswordStrength();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

setPasswordMessage("");
}
}

let variant;
if (passwordStrength === "")
variant = "default";
else if (passwordStrength > 66)
let message;
let messageColor;
if (passwordStrength > 66) {
variant = "success";
else if (passwordStrength > 33)
messageColor = "pf-v5-u-success-color-200";
message = _("Strong password");
} else if (passwordStrength > 33) {
variant = "warning";
else
messageColor = "pf-v5-u-warning-color-200";
message = _("Acceptable password");
} else {
variant = "danger";
messageColor = "pf-v5-u-danger-color-200";
message = _("Weak password");
}

if (!passwordMessage && message)
setPasswordMessage(message);

let passwordStrengthValue = Number.isInteger(passwordStrength) ? Number.parseInt(passwordStrength) : -1;
if (password !== "" && (passwordStrengthValue >= 0 && passwordStrengthValue < 25))
passwordStrengthValue = 25;

return (
<>
Expand All @@ -106,19 +125,35 @@ export const PasswordFormFields = ({
validated={error_password ? "warning" : "default"}
id={idPrefix + "-pw1-group"}
fieldId={idPrefix + "-pw1"}>
<TextInput className="check-passwords" type="password" id={idPrefix + "-pw1"}
autoComplete="new-password" value={password} onChange={(_event, value) => onPasswordChanged(value)}
validated={error_password ? "warning" : "default"} />
<div>
<Progress id={idPrefix + "-meter"}
className={"ct-password-strength-meter " + variant}
martinpitt marked this conversation as resolved.
Show resolved Hide resolved
title={_("password quality")}
size={ProgressSize.sm}
measureLocation={ProgressMeasureLocation.none}
variant={variant}
value={Number.isInteger(passwordStrength) ? passwordStrength : 0} />
<div id={idPrefix + "-password-meter-message"} className="pf-v5-c-form__helper-text" aria-live="polite">{passwordMessage}</div>
</div>
<InputGroup>
<InputGroupItem isFill>
<TextInput className="check-passwords" type={passwordHidden ? "password" : "text"} id={idPrefix + "-pw1"}
autoComplete="new-password" value={password} onChange={(_event, value) => onPasswordChanged(value)}
validated={error_password ? "warning" : "default"} />
</InputGroupItem>
<InputGroupItem>
<Button
variant="control"
onClick={() => setPasswordHidden(!passwordHidden)}
aria-label={passwordHidden ? _("Show password") : _("Hide password")}>
{passwordHidden ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</InputGroupItem>
</InputGroup>
{passwordStrengthValue >= 0 && <Flex spaceItems={{ default: 'spaceItemsSm' }}>
<FlexItem>
<Progress id={idPrefix + "-meter"}
className={"pf-v5-u-pt-xs ct-password-strength-meter " + variant}
title={_("password quality")}
size={ProgressSize.sm}
measureLocation={ProgressMeasureLocation.none}
variant={variant}
value={passwordStrengthValue} />
</FlexItem>
<FlexItem>
<div id={idPrefix + "-password-meter-message"} className={"pf-v5-c-form__helper-text " + messageColor} aria-live="polite">{passwordMessage}</div>
</FlexItem>
</Flex>}
{error_password && <FormHelperText>
<HelperText component="ul" aria-live="polite" id="password-error-message">
<HelperTextItem isDynamic variant="warning" component="li">
Expand All @@ -131,8 +166,20 @@ export const PasswordFormFields = ({
{password_confirm_label && <FormGroup label={password_confirm_label}
id={idPrefix + "-pw2-group"}
fieldId={idPrefix + "-pw2"}>
<TextInput type="password" id={idPrefix + "-pw2"} autoComplete="new-password"
value={passwordConfirm} onChange={(_event, value) => { setConfirmPassword(value); change("password_confirm", value) }} />
<InputGroup>
<InputGroupItem isFill>
<TextInput type={passwordConfirmHidden ? "password" : "text"} id={idPrefix + "-pw2"} autoComplete="new-password"
value={passwordConfirm} onChange={(_event, value) => { setConfirmPassword(value); change("password_confirm", value) }} />
</InputGroupItem>
<InputGroupItem>
<Button
variant="control"
onClick={() => setPasswordConfirmHidden(!passwordConfirmHidden)}
aria-label={passwordConfirmHidden ? _("Show confirmation password") : _("Hide confirmation password")}>
{passwordConfirmHidden ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</InputGroupItem>
</InputGroup>
<FormHelper fieldId={idPrefix + "-pw2"} helperTextInvalid={error_password_confirm} />
</FormGroup>}
</>
Expand Down
4 changes: 4 additions & 0 deletions pkg/lib/cockpit-components-password.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@import "global-variables";
@import "@patternfly/patternfly/utilities/Text/text.scss";

.ct-password-strength-meter {
grid-gap: var(--pf-v5-global--spacer--xs);
inline-size: var(--pf-v5-global--spacer--2xl);

.pf-v5-c-progress__description, .pf-v5-c-progress__status {
display: none;
Expand Down
14 changes: 14 additions & 0 deletions test/verify/check-users
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,20 @@ class TestAccounts(testlib.MachineCase):
b.set_input_text('#accounts-create-password-pw1', good_password)
b.wait_visible("#accounts-create-password-meter.success")

# Test password show/hide functionality
b.wait_visible('#accounts-create-password-pw1[type="password"]')
b.click("#accounts-create-password-pw1-group button[aria-label='Show password']")
b.wait_visible('#accounts-create-password-pw1[type="text"]')
b.click("#accounts-create-password-pw1-group button[aria-label='Hide password']")
b.wait_visible('#accounts-create-password-pw1[type="password"]')

# Test confirmation password show/hide functionality
b.wait_visible('#accounts-create-password-pw2[type="password"]')
b.click("#accounts-create-password-pw2-group button[aria-label='Show confirmation password']")
b.wait_visible('#accounts-create-password-pw2[type="text"]')
b.click("#accounts-create-password-pw2-group button[aria-label='Hide confirmation password']")
b.wait_visible('#accounts-create-password-pw2[type="password"]')

# wrong password confirmation
b.set_input_text('#accounts-create-password-pw2', good_password + 'b')
b.click('#accounts-create-dialog button.apply')
Expand Down