Skip to content

Commit

Permalink
User management extras (#141)
Browse files Browse the repository at this point in the history
* Add password confirmation.

* Move user form page to components.

* Add settings page to update active user's profile.

* Switch app user management to use context.

* Use contexts to update current user after their profile is updated
  • Loading branch information
newswangerd authored May 29, 2020
1 parent b10f985 commit d112347
Show file tree
Hide file tree
Showing 17 changed files with 259 additions and 51 deletions.
8 changes: 6 additions & 2 deletions src/api/active-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class API extends BaseAPI {
});
} else if (DEPLOYMENT_MODE === Constants.STANDALONE_DEPLOYMENT_MODE) {
return new Promise((resolve, reject) => {
super
.list()
this.http
.get(this.apiPath)
.then(result => {
resolve(result.data);
})
Expand All @@ -30,6 +30,10 @@ class API extends BaseAPI {
}
}

saveUser(data) {
return this.http.put(this.apiPath, data);
}

// insights has some asinine way of loading tokens that involves forcing the
// page to refresh before loading the token that can't be done witha single
// API request.
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export {
export { APIButton } from './api-button/api-button';
export { Main } from './patternfly-wrappers/main';
export { UserForm } from './user-form/user-form';
export { UserFormPage } from './user-form/user-form-page';
1 change: 1 addition & 0 deletions src/components/patternfly-wrappers/compound-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export class CompoundFilter extends React.Component<IProps, IState> {
default:
return (
<TextInput
aria-label={selectedFilter.id}
placeholder={
selectedFilter.placeholder || `Filter by ${selectedFilter.title}`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ interface IProps {
onCancel?: () => void;
}

export function mapErrorMessages(err) {
const messages: any = {};
for (const e of err.response.data.errors) {
messages[e.source.parameter] = e.detail;
}
return messages;
}

export class UserFormPage extends React.Component<IProps> {
public static defaultProps = {
extraControls: null,
Expand Down
62 changes: 58 additions & 4 deletions src/components/user-form/user-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,51 @@ interface IProps {
onCancel?: () => void;
}

export class UserForm extends React.Component<IProps> {
interface IState {
passwordConfirm: string;
}

export class UserForm extends React.Component<IProps, IState> {
public static defaultProps = {
isReadonly: false,
requiredFields: ['username', 'password'],
};

constructor(props) {
super(props);

this.state = {
passwordConfirm: '',
};
}

render() {
const { user, errorMessages, isReadonly, saveUser, onCancel } = this.props;
const {
user,
errorMessages,
isReadonly,
saveUser,
onCancel,
requiredFields,
} = this.props;
const { passwordConfirm } = this.state;
const formFields = [
{ id: 'first_name', title: 'First name' },
{ id: 'last_name', title: 'Last name' },
{ id: 'email', title: 'Email' },
{ id: 'username', title: 'Username' },
{ id: 'password', title: 'Password', type: 'password' },
{
id: 'password',
title: 'Password',
type: 'password',
placeholder: '••••••••••••••••••••••',
},
];
return (
<Form>
{formFields.map(v => (
<FormGroup
isRequired={requiredFields.includes(v.id)}
key={v.id}
fieldId={v.id}
label={v.title}
Expand All @@ -59,15 +87,37 @@ export class UserForm extends React.Component<IProps> {
<TextInput
isDisabled={isReadonly}
id={v.id}
placeholder={v.placeholder}
value={user[v.id]}
onChange={this.updateField}
type={(v.type as any) || 'text'}
/>
</FormGroup>
))}
<FormGroup
fieldId={'password-confirm'}
label={'Password confirmation'}
helperTextInvalid={'Passwords do not match'}
isValid={this.isPassSame(user.password, passwordConfirm)}
>
<TextInput
isDisabled={isReadonly}
id={'password-confirm'}
value={passwordConfirm}
onChange={(value, event) => {
this.setState({ passwordConfirm: value });
}}
type='password'
/>
</FormGroup>
{!isReadonly && (
<ActionGroup>
<Button onClick={() => saveUser()}>Save</Button>
<Button
isDisabled={!this.isPassSame(user.password, passwordConfirm)}
onClick={() => saveUser()}
>
Save
</Button>
<Button onClick={() => onCancel()} variant='link'>
Cancel
</Button>
Expand All @@ -77,6 +127,10 @@ export class UserForm extends React.Component<IProps> {
);
}

private isPassSame(pass, confirm) {
return !pass || pass === '' || pass === confirm;
}

private updateField = (value, event) => {
const update = { ...this.props.user };
update[event.target.id] = value;
Expand Down
9 changes: 2 additions & 7 deletions src/containers/edit-namespace/edit-namespace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Constants } from '../../constants';
import { Form, ActionGroup, Button } from '@patternfly/react-core';

import { Paths, formatPath } from '../../paths';
import { ParamHelper } from '../../utilities/param-helper';
import { ParamHelper, mapErrorMessages } from '../../utilities';

interface IState {
namespace: NamespaceType;
Expand Down Expand Up @@ -198,13 +198,8 @@ class EditNamespace extends React.Component<RouteComponentProps, IState> {
.catch(error => {
const result = error.response;
if (result.status === 400) {
const messages: any = {};
for (const e of result.data.errors) {
messages[e.source.parameter] = e.detail;
}

this.setState({
errorMessages: messages,
errorMessages: mapErrorMessages(error),
saving: false,
});
} else if (result.status === 404) {
Expand Down
1 change: 1 addition & 0 deletions src/containers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export { default as UserList } from './user-management/user-list';
export { default as EditUser } from './user-management/user-edit';
export { default as UserDetail } from './user-management/user-detail';
export { default as UserCreate } from './user-management/user-create';
export { default as UserProfile } from './settings/user-profile';
124 changes: 124 additions & 0 deletions src/containers/settings/user-profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as React from 'react';
import { withRouter, RouteComponentProps, Link } from 'react-router-dom';

import { Button } from '@patternfly/react-core';

import {
LoadingPageWithHeader,
UserFormPage,
AlertType,
AlertList,
closeAlertMixin,
} from '../../components';
import { UserType, ActiveUserAPI } from '../../api';
import { Paths } from '../../paths';
import { mapErrorMessages } from '../../utilities';
import { AppContext } from '../../loaders/standalone/app-context';

interface IState {
user: UserType;
errorMessages: object;
inEditMode: boolean;
alerts: AlertType[];
}

class UserProfile extends React.Component<RouteComponentProps, IState> {
private initialState: UserType;

constructor(props) {
super(props);

this.state = {
user: undefined,
errorMessages: {},
inEditMode: false,
alerts: [],
};
}

componentDidMount() {
const id = this.props.match.params['userID'];
ActiveUserAPI.getUser()
.then(result => {
// The api doesn't return a value for the password, so set a blank one here
// to keep react from getting confused
result.password = '';
this.initialState = { ...result };
this.setState({ user: result });
})
.catch(() => this.props.history.push(Paths.notFound));
}

render() {
const { user, errorMessages, inEditMode, alerts } = this.state;

if (!user) {
return <LoadingPageWithHeader></LoadingPageWithHeader>;
}
return (
<>
<AlertList
alerts={alerts}
closeAlert={i => this.closeAlert(i)}
></AlertList>
<UserFormPage
user={user}
breadcrumbs={[{ name: 'Settings' }, { name: 'My profile' }]}
title='My profile'
errorMessages={errorMessages}
updateUser={user => this.setState({ user: user })}
saveUser={this.saveUser}
isReadonly={!inEditMode}
onCancel={() =>
this.setState({
user: this.initialState,
inEditMode: false,
errorMessages: {},
})
}
extraControls={
!inEditMode && (
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div>
<Button onClick={() => this.setState({ inEditMode: true })}>
Edit
</Button>
</div>
</div>
)
}
></UserFormPage>
</>
);
}

private saveUser = () => {
const { user } = this.state;
ActiveUserAPI.saveUser(user)
.then(result => {
this.setState(
{
inEditMode: false,
alerts: this.state.alerts.concat([
{ variant: 'success', title: 'Profile saved.' },
]),
},
() => this.context.setUser(result.data),
);
})
.catch(err => {
this.setState({ errorMessages: mapErrorMessages(err) });
});
};

private get closeAlert() {
return closeAlertMixin('alerts');
}
}

export default withRouter(UserProfile);

// For some reason react complains about setting context type in the class itself.
// I think that it happens because withRouter confuses react into thinking that the
// component is a functional compent when it's actually a class component.
UserProfile.contextType = AppContext;
3 changes: 2 additions & 1 deletion src/containers/user-management/user-create.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';

import { UserFormPage, mapErrorMessages } from './user-form-page';
import { UserFormPage } from '../../components';
import { mapErrorMessages } from '../../utilities';
import { UserType, UserAPI } from '../../api';
import { Paths } from '../../paths';

Expand Down
2 changes: 1 addition & 1 deletion src/containers/user-management/user-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
AlertType,
AlertList,
closeAlertMixin,
UserFormPage,
} from '../../components';
import { UserFormPage } from './user-form-page';
import { UserType, UserAPI } from '../../api';
import { Paths, formatPath } from '../../paths';
import { DeleteUserModal } from './delete-user-modal';
Expand Down
4 changes: 2 additions & 2 deletions src/containers/user-management/user-edit.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';

import { LoadingPageWithHeader } from '../../components';
import { UserFormPage, mapErrorMessages } from './user-form-page';
import { LoadingPageWithHeader, UserFormPage } from '../../components';
import { mapErrorMessages } from '../../utilities';
import { UserType, UserAPI } from '../../api';
import { Paths, formatPath } from '../../paths';

Expand Down
9 changes: 9 additions & 0 deletions src/loaders/standalone/app-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';
import { UserType } from '../../api';

interface IAppContextType {
user: UserType;
setUser: (user: UserType) => void;
}

export const AppContext = React.createContext<IAppContextType>(undefined);
Loading

0 comments on commit d112347

Please sign in to comment.