Skip to content

Commit

Permalink
Adds Users list, forms and details. Adds password form field.
Browse files Browse the repository at this point in the history
  • Loading branch information
mabashian committed Nov 11, 2019
1 parent 4746bc7 commit deb6e58
Show file tree
Hide file tree
Showing 36 changed files with 2,054 additions and 34 deletions.
91 changes: 91 additions & 0 deletions awx/ui_next/src/components/FormField/PasswordField.jsx
@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Field } from 'formik';
import {
Button,
ButtonVariant,
FormGroup,
InputGroup,
TextInput,
Tooltip,
} from '@patternfly/react-core';
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';

function PasswordField(props) {
const { id, name, label, validate, isRequired, i18n } = props;
const [inputType, setInputType] = useState('password');

const handlePasswordToggle = () => {
setInputType(inputType === 'text' ? 'password' : 'text');
};

return (
<Field
name={name}
validate={validate}
render={({ field, form }) => {
const isValid =
form && (!form.touched[field.name] || !form.errors[field.name]);
return (
<FormGroup
fieldId={id}
helperTextInvalid={form.errors[field.name]}
isRequired={isRequired}
isValid={isValid}
label={label}
>
<InputGroup>
<Tooltip
content={
inputType === 'password' ? i18n._(t`Show`) : i18n._(t`Hide`)
}
>
<Button
variant={ButtonVariant.control}
aria-label={i18n._(t`Toggle Password`)}
onClick={handlePasswordToggle}
>
{inputType === 'password' && <EyeSlashIcon />}
{inputType === 'text' && <EyeIcon />}
</Button>
</Tooltip>
<TextInput
id={id}
isRequired={isRequired}
isValid={isValid}
type={inputType}
{...field}
onChange={(value, event) => {
field.onChange(event);
}}
/>
</InputGroup>
</FormGroup>
);
}}
/>
);
}

PasswordField.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
type: PropTypes.string,
validate: PropTypes.func,
isRequired: PropTypes.bool,
tooltip: PropTypes.node,
tooltipMaxWidth: PropTypes.string,
};

PasswordField.defaultProps = {
type: 'text',
validate: () => {},
isRequired: false,
tooltip: null,
tooltipMaxWidth: '',
};

export default withI18n()(PasswordField);
42 changes: 42 additions & 0 deletions awx/ui_next/src/components/FormField/PasswordField.test.jsx
@@ -0,0 +1,42 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
import { Formik } from 'formik';
import PasswordField from './PasswordField';

describe('PasswordField', () => {
test('renders the expected content', () => {
const wrapper = mountWithContexts(
<Formik
initialValues={{
password: '',
}}
render={() => (
<PasswordField id="test-password" name="password" label="Password" />
)}
/>
);
expect(wrapper).toHaveLength(1);
});

test('properly responds to show/hide toggles', async () => {
const wrapper = mountWithContexts(
<Formik
initialValues={{
password: '',
}}
render={() => (
<PasswordField id="test-password" name="password" label="Password" />
)}
/>
);
expect(wrapper.find('input').prop('type')).toBe('password');
expect(wrapper.find('EyeSlashIcon').length).toBe(1);
expect(wrapper.find('EyeIcon').length).toBe(0);
wrapper.find('button').simulate('click');
await sleep(1);
expect(wrapper.find('input').prop('type')).toBe('text');
expect(wrapper.find('EyeSlashIcon').length).toBe(0);
expect(wrapper.find('EyeIcon').length).toBe(1);
});
});
1 change: 1 addition & 0 deletions awx/ui_next/src/components/FormField/index.js
@@ -1,3 +1,4 @@
export { default } from './FormField';
export { default as CheckboxField } from './CheckboxField';
export { default as FieldTooltip } from './FieldTooltip';
export { default as PasswordField } from './PasswordField';
@@ -1,5 +1,13 @@
import React, { Fragment } from 'react';
import { func, bool, number, string, arrayOf, shape } from 'prop-types';
import {
func,
bool,
number,
string,
arrayOf,
shape,
checkPropTypes,
} from 'prop-types';
import { Button, Tooltip } from '@patternfly/react-core';
import { TrashAltIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
Expand All @@ -22,9 +30,40 @@ const DeleteButton = styled(Button)`
}
`;

const requireNameOrUsername = props => {
const { name, username } = props;
if (!name && !username) {
return new Error(
`One of 'name' or 'username' is required by ItemToDelete component.`
);
}
if (name) {
checkPropTypes(
{
name: string,
},
{ name: props.name },
'prop',
'ItemToDelete'
);
}
if (username) {
checkPropTypes(
{
username: string,
},
{ username: props.username },
'prop',
'ItemToDelete'
);
}
return null;
};

const ItemToDelete = shape({
id: number.isRequired,
name: string.isRequired,
name: requireNameOrUsername,
username: requireNameOrUsername,
summary_fields: shape({
user_capabilities: shape({
delete: bool.isRequired,
Expand Down Expand Up @@ -148,7 +187,7 @@ class ToolbarDeleteButton extends React.Component {
<br />
{itemsToDelete.map(item => (
<span key={item.id}>
<strong>{item.name}</strong>
<strong>{item.name || item.username}</strong>
<br />
</span>
))}
Expand Down
2 changes: 1 addition & 1 deletion awx/ui_next/src/components/Sort/Sort.jsx
Expand Up @@ -127,7 +127,7 @@ class Sort extends React.Component {

return (
<React.Fragment>
{sortDropdownItems.length > 1 && (
{sortDropdownItems.length > 0 && (
<React.Fragment>
<SortBy>{i18n._(t`Sort By`)}</SortBy>
<Dropdown
Expand Down
Expand Up @@ -7,7 +7,7 @@ import { CredentialTypesAPI, ProjectsAPI } from '@api';

jest.mock('@api');

describe('<ProjectAdd />', () => {
describe('<ProjectForm />', () => {
let wrapper;
const mockData = {
name: 'foo',
Expand Down

0 comments on commit deb6e58

Please sign in to comment.