application/components/admin/user.js
import React, {Component} from 'react';
import Immutable from 'immutable';
import {browserHistory} from 'react-router';
import generatePassword from 'password-generator';
import {List, ListItem} from 'material-ui/List';
import Subheader from 'material-ui/Subheader';
import Avatar from 'material-ui/Avatar';
import FontIcon from 'material-ui/FontIcon';
import Divider from 'material-ui/Divider';
import Checkbox from 'material-ui/Checkbox';
import LinearProgress from 'material-ui/LinearProgress';
import PersonIcon from 'material-ui/svg-icons/social/person-outline';
import ReloadIcon from 'material-ui/svg-icons/action/autorenew';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
import IconButton from 'material-ui/IconButton';
import TextField from 'material-ui/TextField';
import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table';
import {Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle} from 'material-ui/Toolbar';
import {connect} from 'react-redux';
import ExpandingCard from './../expanding-card';
import ReduxPaginator from './../common/redux-paginator';
import DeleteButton from './../common/delete-button';
import CurrentUser from './../../current-user';
import {
usersMaybeFetch,
usersFetchOne,
usersFetch,
usersRemove,
userSetSelected,
userUpdate,
userCreate
} from './../../redux/actions/user-actions';
import {
aclsMaybeFetch
} from './../../redux/actions/acl-actions';
import {
snackbarShowMessage
} from './../../redux/actions/snackbar-actions';
import {
alertShowMessage
} from './../../redux/actions/alert-actions';
import {Row, Col} from './../flexbox';
import request from './../../util/request';
import {ENTER_KEY} from './../../util/key-codes';
class UserCard extends Component {
constructor(props) {
super(props);
this.state = {
expanded: false,
hasMore: false,
hasLoaded: false,
modalOpen: false,
selected: []
};
this.loadScrollPage.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.openCard == 'user' && nextProps.openCard != this.props.openCard) {
this.handleToggle(true);
}
}
handleToggle(expanded) {
if (this.props.onToggle) {
this.props.onToggle('user', expanded);
}
if (expanded && !this.state.hasLoaded) {
const {dispatch} = this.props;
dispatch(usersMaybeFetch())
.then(() => dispatch(aclsMaybeFetch()))
.then(() => this.setState({hasLoaded: true}));
}
}
loadScrollPage(page = 0) {
const {dispatch} = this.props;
dispatch(usersMaybeFetch({page: page}));
}
handleUserClick(user, e) {
const {dispatch} = this.props;
dispatch(usersFetchOne(user.get('uuid')))
.then(() => dispatch(userSetSelected(user.get('uuid'))))
.then(() => {
this.setState({
'modalOpen': true
});
});
}
handleNewUser(e) {
const {dispatch} = this.props;
dispatch(userSetSelected(false));
this.setState({'modalOpen': true});
}
handleModalClose(e) {
const {dispatch} = this.props;
dispatch(userSetSelected(false));
this.setState({'modalOpen': false, 'selected': [], 'selectedItems': []});
// fixes selection state in 0.15.6.1
this.tableBody.setState({selectedRows: []});
}
handleRowSelection(selection) {
let selectedItems = [];
if (selection == 'all') {
selectedItems = this.props.users.items.map((item) => item.get('uuid'));
} else {
const items = this.props.users.items.toJS();
selectedItems = selection.map((i) =>items[i].uuid);
}
this.setState({
'selectedItems': selectedItems,
'selected': selection
});
}
handlePagination(page) {
if (this.props.users.pagination.current == page) {
return;
}
const keyword = (this.props.location && this.props.location.query.keyword)
? decodeURIComponent(this.props.location.query.keyword)
: null;
const q = request.setQuery({
'page': page
});
const this_url = request.getPath() + '?' + q;
browserHistory.push(this_url);
let params = {'page': page};
if (keyword) {
params['keyword'] = keyword;
params['includesnw'] = 1;
}
const {dispatch} = this.props;
dispatch(usersFetch(params));
this.setState({'selected': [], 'selectedItems': []});
// fixes selection state in 0.15.6.1
this.tableBody.setState({selectedRows: []});
}
handleKeyDown(e) {
const val = e.target.value;
if (e.keyCode !== ENTER_KEY) {
return;
}
const {dispatch} = this.props;
const q = request.setQuery({
'keyword': encodeURIComponent(val),
'page': 1
});
const this_url = request.getPath() + '?' + q;
browserHistory.push(this_url);
dispatch(usersFetch({page: 1, keyword: val, includesnw: 1}));
}
handleSearchReset() {
const {dispatch} = this.props;
const q = request.setQuery({
'keyword': '',
'page': 1
});
const this_url = request.getPath() + '?' + q;
browserHistory.push(this_url);
this.quickSearch.value = '';
dispatch(usersFetch({page: 1}));
}
handleDelete(next) {
const uuids = this.state.selectedItems.join(',');
const {dispatch} = this.props;
dispatch(usersRemove(uuids))
.then(() => dispatch(usersFetch()))
.then(() => { this.setState({'selected': [], 'selectedItems': []}) })
.then(() => { this.tableBody.setState({selectedRows: []}) })
.then(() => next());
return true;
}
render() {
let items = this.props.users.items.map((item, i) => {
let snw = false;
if (parseInt(item.get('is_snworks'))) {
snw = true;
}
const groups = item.get('roles').map((role, i) => {
return <span key={i}>{role.get('name')}{(i < item.get('roles').size-1) ? ', ' : ''}</span>
})
return (
<TableRow
key={i}
selected={this.state.selected == 'all' || this.state.selected.indexOf(i) != -1 ? true : false}
selectable={(!CurrentUser.isSnworks() && parseInt(item.get('is_snworks'))) ? false : true}
>
<TableRowColumn>
<Row middle={['xs']}>
<Col xs={1} start={['xs']}>
{snw ? <img style={{'maxWidth': '16px'}} src="/assets/img/cube-flat.png" /> : ''}
{parseInt(item.get('is_service')) ? <FontIcon tooltip='Service User' style={{'fontSize': '1em'}} className='fa fa-cog' /> : ''}
</Col>
<Col xs={11}>
{
(!CurrentUser.isSnworks() && parseInt(item.get('is_snworks')))
? (
<span>
{item.get('name')}
<br />
{item.get('email')}
</span>
)
: (
<a
onClick={this.handleUserClick.bind(this, item)}
>
{item.get('name')}
<br />
{item.get('email')}
</a>
)
}
</Col>
</Row>
</TableRowColumn>
<TableRowColumn>{groups}</TableRowColumn>
</TableRow>
);
});
return (
<ExpandingCard
onToggle={this.handleToggle.bind(this)}
title="Users"
subtitle="Manage user accounts"
expanded={this.props.openCard === 'user'}
>
<Row middle='xs'>
<Col xs={3} autoBox={false}>
<div className="box toolbar-search-wrap">
<FontIcon className='mui-icons'>search</FontIcon>
<input
ref={quickSearch => this.quickSearch = quickSearch}
className="search"
type="text"
placeholder="Search Users"
onKeyDown={this.handleKeyDown.bind(this)}
defaultValue={this.props.location.query.keyword ? decodeURIComponent(this.props.location.query.keyword) : ''}
/>
<FontIcon className='mui-icons clear-search' onClick={this.handleSearchReset.bind(this)}>close</FontIcon>
</div>
</Col>
<Col xs={4}>
<DeleteButton
disabled={this.state.selectedItems && this.state.selectedItems.length ? false : true}
onDelete={this.handleDelete.bind(this)}
/>
<RaisedButton
primary={true}
label="Add User"
onClick={this.handleNewUser.bind(this)}
/>
</Col>
<Col xs={5} end='xs'>
<ReduxPaginator
pagination={this.props.users.pagination}
onPaginate={this.handlePagination.bind(this)}
/>
</Col>
</Row>
<Table
fixedHeader={true}
selectable={true}
multiSelectable={true}
onRowSelection={this.handleRowSelection.bind(this)}
>
<TableHeader>
<TableRow>
<TableHeaderColumn>
<Row>
<Col xs={11} offsetXs={1}>
Name
</Col>
</Row>
</TableHeaderColumn>
<TableHeaderColumn>Groups</TableHeaderColumn>
</TableRow>
</TableHeader>
<TableBody
ref={tableBody => this.tableBody = tableBody}
deselectOnClickaway={false}
>
{items}
</TableBody>
</Table>
<UserModal onClose={this.handleModalClose.bind(this)} open={this.state.modalOpen} />
</ExpandingCard>
);
}
}
let mapStateToProps = (state) => {
return {
users: state.users,
acls: state.acls
};
}
export default connect(mapStateToProps)(UserCard);
class UnwrappedUserModal extends Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
changedPassword: false,
changedRoles: false,
passwordStrength: 0,
didUpdatePassword: false,
email_updated_password: false,
create_as_author: false,
user: {
'name': false,
'email': false,
'phone': false,
'roles': [],
'is_snworks': 0
}
};
}
// componentWillUpdate(nextProps, nextState) {
componentWillReceiveProps(nextProps) {
// this.setState({'user': {
// 'name': false,
// 'email': false,
// 'phone': false,
// 'roles': [],
// 'is_snworks': 0
// }});
if (!this.state.loaded || nextProps.users.selected != this.props.users.selected) {
const user = nextProps.users.items.find((item) => {
return item.get('uuid') == nextProps.users.selected;
});
if (user) {
console.log('SET USER');
this.setState({'user': user.toJS()});
}
this.setState({
'loaded': true,
'changedPassword': false,
'passwordStrength': 0,
'changedRoles': false,
didUpdatePassword: false,
email_updated_password: false,
create_as_author: false
})
}
}
handleClose(e) {
this.setState({'user': {
'name': false,
'email': false,
'phone': false,
'roles': [],
'is_snworks': 0,
'is_service': 0
}});
this.props.onClose(e);
}
handleSave(e) {
let user = this.state.user;
const {dispatch} = this.props;
let data = {
'name': user.name,
'email': user.email,
'phone': user.phone,
'is_service': user.is_service
};
if (CurrentUser.isSnworks()) {
data.is_snworks = user.is_snworks;
}
if (this.state.changedRoles) {
data.roles = [];
user.roles.map((item, i) => {
data.roles.push(item.name);
});
}
if (this.state.didUpdatePassword) {
data['password'] = this.state.changedPassword;
}
if (this.state.didUpdatePassword && this.state.email_updated_password) {
data['email_updated_password'] = 1;
}
if (!user.uuid && this.state.create_as_author) {
data['create_as_author'] = 1;
}
if (user.uuid) {
dispatch(userUpdate(user.uuid, data))
.then(() => dispatch(usersFetch()))
.then(() => dispatch(snackbarShowMessage('User updated')))
.then(() => dispatch(userSetSelected(false)))
.then(() => {
this.handleClose(e);
})
.catch((e) => {
e.response.json().then((json) => {
if (json.message.indexOf('email is already registered') !== -1) {
dispatch(alertShowMessage({
'title': 'Oops',
'message': 'A user with that email address already exists'
}));
} else {
dispatch(alertShowMessage({
'title': 'Oops',
'message': json.message
}));
}
});
});
} else {
dispatch(userCreate(data))
.then((resp) => {
const u = resp.payload.user;
if (u.get('is_service') && u.get('service_key')) {
dispatch(alertShowMessage({
'title': 'New Service User',
'message': (
<div>
<p>
Please copy this service user's access keys and store it in a safe place. <strong>This is the only time it will be shown.</strong>
</p>
<TextField
floatingLabelText='Public Key'
readOnly={true}
value={u.get('public_key')}
fullWidth={true}
/>
<TextField
floatingLabelText='Private Key'
readOnly={true}
value={u.get('service_key')}
fullWidth={true}
/>
</div>
)
}));
}
})
.then(() => dispatch(usersFetch()))
.then(() => dispatch(snackbarShowMessage('User created')))
.then(() => dispatch(userSetSelected(false)))
.then(() => {
this.handleClose(e);
})
.catch((e) => {
e.response.json().then((json) => {
if (json.message.indexOf('email is already registered') !== -1) {
dispatch(alertShowMessage({
'title': 'Oops',
'message': 'A user with that email address already exists'
}));
} else {
dispatch(alertShowMessage({
'title': 'Oops',
'message': json.message
}));
}
});
});
}
}
handleFieldUpdate(e, val) {
const prop = e.target.name;
let user = this.state.user;
user[prop] = val;
this.setState({'user': user});
}
handleSnworksUpdate(e, checked) {
let user = this.state.user;
user.is_snworks = checked ? 1 : 0;
this.setState({'user': user});
}
handleServiceUpdate(e, checked) {
let user = this.state.user;
user.is_service = checked ? 1 : 0;
this.setState({'user': user});
}
handleEmailPassUpdate(e, checked) {
this.setState({'email_updated_password': checked ? true : false});
}
handleCreateAsAuthor(e, checked) {
this.setState({'create_as_author': checked ? true : false});
}
handleRoleUpdate(e, checked) {
let user = this.state.user;
let role = e.target.name;
if (checked) {
user.roles.push({'name': role});
} else {
let roles = user.roles.map((r, i) => {
if (r.name != role) {
return r;
}
return false;
});
user.roles = roles;
}
this.setState({'user': user, 'changedRoles': true});
}
handlePasswordChange(e) {
this.setState({
'changedPassword': e.target.value,
'didUpdatePassword': true,
'passwordStrength': zxcvbn(e.target.value).score + 1
});
}
handleGeneratePassword() {
const p = generatePassword(12, false, /[\w\d\?\-]/);
this.setState({
'changedPassword': p,
'didUpdatePassword': true,
'passwordStrength': zxcvbn(p).score + 1,
'email_updated_password': true
});
}
getStrengthColor() {
let strengthColor = 'red';
switch (this.state.passwordStrength) {
case 0:
case 1:
strengthColor = 'red';
break;
case 2:
strengthColor = 'orange';
break;
case 3:
strengthColor = 'yellow';
break;
case 4:
case 5:
strengthColor = 'green';
break;
}
return strengthColor;
}
render() {
const actions = [
<FlatButton
label="Cancel"
primary={false}
onClick={this.handleClose.bind(this)}
/>,
<FlatButton
label="Save"
primary={true}
keyboardFocused={true}
onClick={this.handleSave.bind(this)}
/>
];
return (
<Dialog
title="Edit User"
actions={actions}
modal={false}
open={this.props.open}
onRequestClose={this.handleClose.bind(this)}
autoScrollBodyContent={true}
>
<p className='small'>
<strong>Do you need to a lot of users?</strong> Contact <a href="mailto:support@getsnworks.com">support@getsnworks.com</a> and we can help you out!
</p>
<TextField
floatingLabelText="Name"
value={this.state.user.name ? this.state.user.name : ''}
name='name'
fullWidth={true}
onChange={this.handleFieldUpdate.bind(this)}
/>
<TextField
floatingLabelText="Email"
value={this.state.user.email ? this.state.user.email : ''}
disabled={this.state.user.is_service ? true : false}
name='email'
fullWidth={true}
onChange={this.handleFieldUpdate.bind(this)}
/>
<TextField
floatingLabelText="Phone"
value={this.state.user.phone ? this.state.user.phone : ''}
disabled={this.state.user.is_service ? true : false}
name='phone'
fullWidth={true}
onChange={this.handleFieldUpdate.bind(this)}
className='clear-bottom'
/>
{
CurrentUser.isSnworks()
? (<Checkbox
label="SNworks User"
labelPosition="right"
name='snw'
disabled={this.state.user.is_service ? true : false}
checked={parseInt(this.state.user.is_snworks) ? true : false}
onCheck={this.handleSnworksUpdate.bind(this)}
/>)
: ''
}
{
!this.state.user.uuid
? (<Checkbox
label="Also create author"
labelPosition="right"
name='create_as_author'
disabled={this.state.user.is_service ? true : false}
checked={this.state.create_as_author}
onCheck={this.handleCreateAsAuthor.bind(this)}
/>)
: ''
}
<Checkbox
label="Service User"
labelPosition="right"
name='is_service'
checked={parseInt(this.state.user.is_service) ? true : false}
onCheck={this.handleServiceUpdate.bind(this)}
/>
<h3>Roles</h3>
{this.props.acls.items.map((item, i) => {
let checked = false;
this.state.user.roles.map((r, i) => {
if (r.name == item.get('name')) {
checked = true;
}
});
return <Checkbox
label={item.get('name')}
labelPosition="right"
name={item.get('name')}
key={i}
defaultChecked={checked}
onCheck={this.handleRoleUpdate.bind(this)}
/>;
})}
<Row bottom={['xs']}>
<Col xs={8}>
<h3>Password</h3>
<TextField
floatingLabelText="Enter a new password"
value={this.state.changedPassword ? this.state.changedPassword : ''}
fullWidth={true}
disabled={parseInt(this.state.user.is_service) ? true : false}
onChange={this.handlePasswordChange.bind(this)}
/>
<LinearProgress
mode="determinate"
value={this.state.passwordStrength}
color={this.getStrengthColor()}
max={5}
style={{backgroundColor: 'white'}}
/>
</Col>
<Col xs={4}>
<RaisedButton
onClick={this.handleGeneratePassword.bind(this)}
label='Generate a password'
disabled={parseInt(this.state.user.is_service) ? true : false}
secondary={true}
/>
</Col>
</Row>
<Row top={['xs']}>
<Col xs={12}>
<Checkbox
label='Send user new password notification'
labelPosition="right"
checked={this.state.email_updated_password}
onCheck={this.handleEmailPassUpdate.bind(this)}
disabled={!this.state.didUpdatePassword}
/>
</Col>
</Row>
</Dialog>
);
}
}
const UserModal = connect(mapStateToProps)(UnwrappedUserModal);