Home Reference Source

application/components/admin/user.js

  1. import React, {Component} from 'react';
  2. import Immutable from 'immutable';
  3. import {browserHistory} from 'react-router';
  4.  
  5. import generatePassword from 'password-generator';
  6.  
  7. import {List, ListItem} from 'material-ui/List';
  8. import Subheader from 'material-ui/Subheader';
  9. import Avatar from 'material-ui/Avatar';
  10. import FontIcon from 'material-ui/FontIcon';
  11. import Divider from 'material-ui/Divider';
  12. import Checkbox from 'material-ui/Checkbox';
  13. import LinearProgress from 'material-ui/LinearProgress';
  14.  
  15. import PersonIcon from 'material-ui/svg-icons/social/person-outline';
  16. import ReloadIcon from 'material-ui/svg-icons/action/autorenew';
  17.  
  18. import Dialog from 'material-ui/Dialog';
  19. import FlatButton from 'material-ui/FlatButton';
  20. import RaisedButton from 'material-ui/RaisedButton';
  21. import IconButton from 'material-ui/IconButton';
  22. import TextField from 'material-ui/TextField';
  23.  
  24. import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table';
  25. import {Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle} from 'material-ui/Toolbar';
  26.  
  27. import {connect} from 'react-redux';
  28. import ExpandingCard from './../expanding-card';
  29. import ReduxPaginator from './../common/redux-paginator';
  30. import DeleteButton from './../common/delete-button';
  31.  
  32. import CurrentUser from './../../current-user';
  33.  
  34. import {
  35. usersMaybeFetch,
  36. usersFetchOne,
  37. usersFetch,
  38. usersRemove,
  39. userSetSelected,
  40. userUpdate,
  41. userCreate
  42. } from './../../redux/actions/user-actions';
  43.  
  44. import {
  45. aclsMaybeFetch
  46. } from './../../redux/actions/acl-actions';
  47.  
  48. import {
  49. snackbarShowMessage
  50. } from './../../redux/actions/snackbar-actions';
  51. import {
  52. alertShowMessage
  53. } from './../../redux/actions/alert-actions';
  54.  
  55. import {Row, Col} from './../flexbox';
  56. import request from './../../util/request';
  57. import {ENTER_KEY} from './../../util/key-codes';
  58.  
  59. class UserCard extends Component {
  60. constructor(props) {
  61. super(props);
  62.  
  63. this.state = {
  64. expanded: false,
  65. hasMore: false,
  66. hasLoaded: false,
  67. modalOpen: false,
  68. selected: []
  69. };
  70.  
  71. this.loadScrollPage.bind(this);
  72. }
  73.  
  74. componentWillReceiveProps(nextProps) {
  75. if (nextProps.openCard == 'user' && nextProps.openCard != this.props.openCard) {
  76. this.handleToggle(true);
  77. }
  78. }
  79.  
  80. handleToggle(expanded) {
  81. if (this.props.onToggle) {
  82. this.props.onToggle('user', expanded);
  83. }
  84.  
  85. if (expanded && !this.state.hasLoaded) {
  86. const {dispatch} = this.props;
  87. dispatch(usersMaybeFetch())
  88. .then(() => dispatch(aclsMaybeFetch()))
  89. .then(() => this.setState({hasLoaded: true}));
  90. }
  91. }
  92.  
  93. loadScrollPage(page = 0) {
  94. const {dispatch} = this.props;
  95. dispatch(usersMaybeFetch({page: page}));
  96. }
  97.  
  98. handleUserClick(user, e) {
  99. const {dispatch} = this.props;
  100.  
  101. dispatch(usersFetchOne(user.get('uuid')))
  102. .then(() => dispatch(userSetSelected(user.get('uuid'))))
  103. .then(() => {
  104. this.setState({
  105. 'modalOpen': true
  106. });
  107. });
  108.  
  109. }
  110.  
  111. handleNewUser(e) {
  112. const {dispatch} = this.props;
  113. dispatch(userSetSelected(false));
  114. this.setState({'modalOpen': true});
  115. }
  116.  
  117. handleModalClose(e) {
  118. const {dispatch} = this.props;
  119. dispatch(userSetSelected(false));
  120. this.setState({'modalOpen': false, 'selected': [], 'selectedItems': []});
  121. // fixes selection state in 0.15.6.1
  122. this.tableBody.setState({selectedRows: []});
  123. }
  124.  
  125. handleRowSelection(selection) {
  126. let selectedItems = [];
  127.  
  128. if (selection == 'all') {
  129. selectedItems = this.props.users.items.map((item) => item.get('uuid'));
  130. } else {
  131. const items = this.props.users.items.toJS();
  132. selectedItems = selection.map((i) =>items[i].uuid);
  133. }
  134.  
  135. this.setState({
  136. 'selectedItems': selectedItems,
  137. 'selected': selection
  138. });
  139. }
  140.  
  141. handlePagination(page) {
  142. if (this.props.users.pagination.current == page) {
  143. return;
  144. }
  145. const keyword = (this.props.location && this.props.location.query.keyword)
  146. ? decodeURIComponent(this.props.location.query.keyword)
  147. : null;
  148. const q = request.setQuery({
  149. 'page': page
  150. });
  151. const this_url = request.getPath() + '?' + q;
  152. browserHistory.push(this_url);
  153.  
  154. let params = {'page': page};
  155. if (keyword) {
  156. params['keyword'] = keyword;
  157. params['includesnw'] = 1;
  158. }
  159.  
  160. const {dispatch} = this.props;
  161. dispatch(usersFetch(params));
  162. this.setState({'selected': [], 'selectedItems': []});
  163. // fixes selection state in 0.15.6.1
  164. this.tableBody.setState({selectedRows: []});
  165. }
  166.  
  167. handleKeyDown(e) {
  168. const val = e.target.value;
  169. if (e.keyCode !== ENTER_KEY) {
  170. return;
  171. }
  172.  
  173. const {dispatch} = this.props;
  174.  
  175. const q = request.setQuery({
  176. 'keyword': encodeURIComponent(val),
  177. 'page': 1
  178. });
  179. const this_url = request.getPath() + '?' + q;
  180. browserHistory.push(this_url);
  181.  
  182. dispatch(usersFetch({page: 1, keyword: val, includesnw: 1}));
  183. }
  184.  
  185. handleSearchReset() {
  186. const {dispatch} = this.props;
  187. const q = request.setQuery({
  188. 'keyword': '',
  189. 'page': 1
  190. });
  191. const this_url = request.getPath() + '?' + q;
  192. browserHistory.push(this_url);
  193. this.quickSearch.value = '';
  194.  
  195. dispatch(usersFetch({page: 1}));
  196. }
  197.  
  198. handleDelete(next) {
  199. const uuids = this.state.selectedItems.join(',');
  200. const {dispatch} = this.props;
  201.  
  202. dispatch(usersRemove(uuids))
  203. .then(() => dispatch(usersFetch()))
  204. .then(() => { this.setState({'selected': [], 'selectedItems': []}) })
  205. .then(() => { this.tableBody.setState({selectedRows: []}) })
  206. .then(() => next());
  207.  
  208. return true;
  209. }
  210.  
  211. render() {
  212. let items = this.props.users.items.map((item, i) => {
  213. let snw = false;
  214. if (parseInt(item.get('is_snworks'))) {
  215. snw = true;
  216. }
  217.  
  218. const groups = item.get('roles').map((role, i) => {
  219. return <span key={i}>{role.get('name')}{(i < item.get('roles').size-1) ? ', ' : ''}</span>
  220. })
  221.  
  222. return (
  223. <TableRow
  224. key={i}
  225. selected={this.state.selected == 'all' || this.state.selected.indexOf(i) != -1 ? true : false}
  226. selectable={(!CurrentUser.isSnworks() && parseInt(item.get('is_snworks'))) ? false : true}
  227. >
  228. <TableRowColumn>
  229. <Row middle={['xs']}>
  230. <Col xs={1} start={['xs']}>
  231. {snw ? <img style={{'maxWidth': '16px'}} src="/assets/img/cube-flat.png" /> : ''}
  232. {parseInt(item.get('is_service')) ? <FontIcon tooltip='Service User' style={{'fontSize': '1em'}} className='fa fa-cog' /> : ''}
  233. </Col>
  234. <Col xs={11}>
  235. {
  236. (!CurrentUser.isSnworks() && parseInt(item.get('is_snworks')))
  237. ? (
  238. <span>
  239. {item.get('name')}
  240. <br />
  241. {item.get('email')}
  242. </span>
  243. )
  244. : (
  245. <a
  246. onClick={this.handleUserClick.bind(this, item)}
  247. >
  248. {item.get('name')}
  249. <br />
  250. {item.get('email')}
  251. </a>
  252.  
  253. )
  254. }
  255. </Col>
  256. </Row>
  257. </TableRowColumn>
  258. <TableRowColumn>{groups}</TableRowColumn>
  259. </TableRow>
  260. );
  261. });
  262.  
  263. return (
  264. <ExpandingCard
  265. onToggle={this.handleToggle.bind(this)}
  266. title="Users"
  267. subtitle="Manage user accounts"
  268. expanded={this.props.openCard === 'user'}
  269. >
  270.  
  271. <Row middle='xs'>
  272. <Col xs={3} autoBox={false}>
  273. <div className="box toolbar-search-wrap">
  274. <FontIcon className='mui-icons'>search</FontIcon>
  275. <input
  276. ref={quickSearch => this.quickSearch = quickSearch}
  277. className="search"
  278. type="text"
  279. placeholder="Search Users"
  280. onKeyDown={this.handleKeyDown.bind(this)}
  281. defaultValue={this.props.location.query.keyword ? decodeURIComponent(this.props.location.query.keyword) : ''}
  282. />
  283. <FontIcon className='mui-icons clear-search' onClick={this.handleSearchReset.bind(this)}>close</FontIcon>
  284. </div>
  285. </Col>
  286. <Col xs={4}>
  287. <DeleteButton
  288. disabled={this.state.selectedItems && this.state.selectedItems.length ? false : true}
  289. onDelete={this.handleDelete.bind(this)}
  290. />
  291. <RaisedButton
  292. primary={true}
  293. label="Add User"
  294. onClick={this.handleNewUser.bind(this)}
  295. />
  296. </Col>
  297. <Col xs={5} end='xs'>
  298. <ReduxPaginator
  299. pagination={this.props.users.pagination}
  300. onPaginate={this.handlePagination.bind(this)}
  301. />
  302. </Col>
  303. </Row>
  304.  
  305. <Table
  306. fixedHeader={true}
  307. selectable={true}
  308. multiSelectable={true}
  309. onRowSelection={this.handleRowSelection.bind(this)}
  310. >
  311. <TableHeader>
  312. <TableRow>
  313. <TableHeaderColumn>
  314. <Row>
  315. <Col xs={11} offsetXs={1}>
  316. Name
  317. </Col>
  318. </Row>
  319. </TableHeaderColumn>
  320. <TableHeaderColumn>Groups</TableHeaderColumn>
  321. </TableRow>
  322. </TableHeader>
  323. <TableBody
  324. ref={tableBody => this.tableBody = tableBody}
  325. deselectOnClickaway={false}
  326. >
  327. {items}
  328. </TableBody>
  329. </Table>
  330.  
  331. <UserModal onClose={this.handleModalClose.bind(this)} open={this.state.modalOpen} />
  332. </ExpandingCard>
  333. );
  334. }
  335. }
  336.  
  337. let mapStateToProps = (state) => {
  338. return {
  339. users: state.users,
  340. acls: state.acls
  341. };
  342. }
  343.  
  344. export default connect(mapStateToProps)(UserCard);
  345.  
  346. class UnwrappedUserModal extends Component {
  347. constructor(props) {
  348. super(props);
  349.  
  350. this.state = {
  351. loaded: false,
  352. changedPassword: false,
  353. changedRoles: false,
  354. passwordStrength: 0,
  355. didUpdatePassword: false,
  356. email_updated_password: false,
  357. create_as_author: false,
  358. user: {
  359. 'name': false,
  360. 'email': false,
  361. 'phone': false,
  362. 'roles': [],
  363. 'is_snworks': 0
  364. }
  365. };
  366.  
  367. }
  368.  
  369. // componentWillUpdate(nextProps, nextState) {
  370. componentWillReceiveProps(nextProps) {
  371. // this.setState({'user': {
  372. // 'name': false,
  373. // 'email': false,
  374. // 'phone': false,
  375. // 'roles': [],
  376. // 'is_snworks': 0
  377. // }});
  378.  
  379. if (!this.state.loaded || nextProps.users.selected != this.props.users.selected) {
  380. const user = nextProps.users.items.find((item) => {
  381. return item.get('uuid') == nextProps.users.selected;
  382. });
  383.  
  384. if (user) {
  385. console.log('SET USER');
  386. this.setState({'user': user.toJS()});
  387. }
  388.  
  389. this.setState({
  390. 'loaded': true,
  391. 'changedPassword': false,
  392. 'passwordStrength': 0,
  393. 'changedRoles': false,
  394. didUpdatePassword: false,
  395. email_updated_password: false,
  396. create_as_author: false
  397. })
  398. }
  399. }
  400.  
  401. handleClose(e) {
  402. this.setState({'user': {
  403. 'name': false,
  404. 'email': false,
  405. 'phone': false,
  406. 'roles': [],
  407. 'is_snworks': 0,
  408. 'is_service': 0
  409. }});
  410. this.props.onClose(e);
  411. }
  412.  
  413. handleSave(e) {
  414. let user = this.state.user;
  415.  
  416. const {dispatch} = this.props;
  417.  
  418. let data = {
  419. 'name': user.name,
  420. 'email': user.email,
  421. 'phone': user.phone,
  422. 'is_service': user.is_service
  423. };
  424.  
  425. if (CurrentUser.isSnworks()) {
  426. data.is_snworks = user.is_snworks;
  427. }
  428.  
  429.  
  430. if (this.state.changedRoles) {
  431. data.roles = [];
  432.  
  433. user.roles.map((item, i) => {
  434. data.roles.push(item.name);
  435. });
  436. }
  437.  
  438. if (this.state.didUpdatePassword) {
  439. data['password'] = this.state.changedPassword;
  440. }
  441.  
  442. if (this.state.didUpdatePassword && this.state.email_updated_password) {
  443. data['email_updated_password'] = 1;
  444. }
  445.  
  446. if (!user.uuid && this.state.create_as_author) {
  447. data['create_as_author'] = 1;
  448. }
  449.  
  450. if (user.uuid) {
  451. dispatch(userUpdate(user.uuid, data))
  452. .then(() => dispatch(usersFetch()))
  453. .then(() => dispatch(snackbarShowMessage('User updated')))
  454. .then(() => dispatch(userSetSelected(false)))
  455. .then(() => {
  456. this.handleClose(e);
  457. })
  458. .catch((e) => {
  459. e.response.json().then((json) => {
  460. if (json.message.indexOf('email is already registered') !== -1) {
  461. dispatch(alertShowMessage({
  462. 'title': 'Oops',
  463. 'message': 'A user with that email address already exists'
  464. }));
  465. } else {
  466. dispatch(alertShowMessage({
  467. 'title': 'Oops',
  468. 'message': json.message
  469. }));
  470. }
  471. });
  472. });
  473. } else {
  474. dispatch(userCreate(data))
  475. .then((resp) => {
  476. const u = resp.payload.user;
  477. if (u.get('is_service') && u.get('service_key')) {
  478. dispatch(alertShowMessage({
  479. 'title': 'New Service User',
  480. 'message': (
  481. <div>
  482. <p>
  483. 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>
  484. </p>
  485. <TextField
  486. floatingLabelText='Public Key'
  487. readOnly={true}
  488. value={u.get('public_key')}
  489. fullWidth={true}
  490. />
  491. <TextField
  492. floatingLabelText='Private Key'
  493. readOnly={true}
  494. value={u.get('service_key')}
  495. fullWidth={true}
  496. />
  497. </div>
  498. )
  499. }));
  500. }
  501. })
  502. .then(() => dispatch(usersFetch()))
  503. .then(() => dispatch(snackbarShowMessage('User created')))
  504. .then(() => dispatch(userSetSelected(false)))
  505. .then(() => {
  506. this.handleClose(e);
  507. })
  508. .catch((e) => {
  509. e.response.json().then((json) => {
  510. if (json.message.indexOf('email is already registered') !== -1) {
  511. dispatch(alertShowMessage({
  512. 'title': 'Oops',
  513. 'message': 'A user with that email address already exists'
  514. }));
  515. } else {
  516. dispatch(alertShowMessage({
  517. 'title': 'Oops',
  518. 'message': json.message
  519. }));
  520. }
  521. });
  522. });
  523. }
  524. }
  525.  
  526. handleFieldUpdate(e, val) {
  527. const prop = e.target.name;
  528. let user = this.state.user;
  529.  
  530. user[prop] = val;
  531.  
  532. this.setState({'user': user});
  533. }
  534.  
  535. handleSnworksUpdate(e, checked) {
  536. let user = this.state.user;
  537. user.is_snworks = checked ? 1 : 0;
  538.  
  539. this.setState({'user': user});
  540. }
  541.  
  542. handleServiceUpdate(e, checked) {
  543. let user = this.state.user;
  544. user.is_service = checked ? 1 : 0;
  545.  
  546. this.setState({'user': user});
  547. }
  548.  
  549. handleEmailPassUpdate(e, checked) {
  550. this.setState({'email_updated_password': checked ? true : false});
  551. }
  552.  
  553. handleCreateAsAuthor(e, checked) {
  554. this.setState({'create_as_author': checked ? true : false});
  555. }
  556.  
  557. handleRoleUpdate(e, checked) {
  558. let user = this.state.user;
  559. let role = e.target.name;
  560.  
  561. if (checked) {
  562. user.roles.push({'name': role});
  563. } else {
  564. let roles = user.roles.map((r, i) => {
  565. if (r.name != role) {
  566. return r;
  567. }
  568.  
  569. return false;
  570. });
  571.  
  572. user.roles = roles;
  573. }
  574.  
  575. this.setState({'user': user, 'changedRoles': true});
  576. }
  577.  
  578. handlePasswordChange(e) {
  579. this.setState({
  580. 'changedPassword': e.target.value,
  581. 'didUpdatePassword': true,
  582. 'passwordStrength': zxcvbn(e.target.value).score + 1
  583. });
  584. }
  585.  
  586. handleGeneratePassword() {
  587. const p = generatePassword(12, false, /[\w\d\?\-]/);
  588. this.setState({
  589. 'changedPassword': p,
  590. 'didUpdatePassword': true,
  591. 'passwordStrength': zxcvbn(p).score + 1,
  592. 'email_updated_password': true
  593. });
  594. }
  595.  
  596.  
  597. getStrengthColor() {
  598. let strengthColor = 'red';
  599. switch (this.state.passwordStrength) {
  600. case 0:
  601. case 1:
  602. strengthColor = 'red';
  603. break;
  604. case 2:
  605. strengthColor = 'orange';
  606. break;
  607. case 3:
  608. strengthColor = 'yellow';
  609. break;
  610. case 4:
  611. case 5:
  612. strengthColor = 'green';
  613. break;
  614.  
  615. }
  616.  
  617. return strengthColor;
  618. }
  619.  
  620. render() {
  621. const actions = [
  622. <FlatButton
  623. label="Cancel"
  624. primary={false}
  625. onClick={this.handleClose.bind(this)}
  626. />,
  627. <FlatButton
  628. label="Save"
  629. primary={true}
  630. keyboardFocused={true}
  631. onClick={this.handleSave.bind(this)}
  632. />
  633. ];
  634.  
  635. return (
  636. <Dialog
  637. title="Edit User"
  638. actions={actions}
  639. modal={false}
  640. open={this.props.open}
  641. onRequestClose={this.handleClose.bind(this)}
  642. autoScrollBodyContent={true}
  643. >
  644. <p className='small'>
  645. <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!
  646. </p>
  647. <TextField
  648. floatingLabelText="Name"
  649. value={this.state.user.name ? this.state.user.name : ''}
  650. name='name'
  651. fullWidth={true}
  652. onChange={this.handleFieldUpdate.bind(this)}
  653. />
  654.  
  655. <TextField
  656. floatingLabelText="Email"
  657. value={this.state.user.email ? this.state.user.email : ''}
  658. disabled={this.state.user.is_service ? true : false}
  659. name='email'
  660. fullWidth={true}
  661. onChange={this.handleFieldUpdate.bind(this)}
  662. />
  663.  
  664. <TextField
  665. floatingLabelText="Phone"
  666. value={this.state.user.phone ? this.state.user.phone : ''}
  667. disabled={this.state.user.is_service ? true : false}
  668. name='phone'
  669. fullWidth={true}
  670. onChange={this.handleFieldUpdate.bind(this)}
  671. className='clear-bottom'
  672. />
  673.  
  674. {
  675. CurrentUser.isSnworks()
  676. ? (<Checkbox
  677. label="SNworks User"
  678. labelPosition="right"
  679. name='snw'
  680. disabled={this.state.user.is_service ? true : false}
  681. checked={parseInt(this.state.user.is_snworks) ? true : false}
  682. onCheck={this.handleSnworksUpdate.bind(this)}
  683. />)
  684. : ''
  685. }
  686. {
  687. !this.state.user.uuid
  688. ? (<Checkbox
  689. label="Also create author"
  690. labelPosition="right"
  691. name='create_as_author'
  692. disabled={this.state.user.is_service ? true : false}
  693. checked={this.state.create_as_author}
  694. onCheck={this.handleCreateAsAuthor.bind(this)}
  695. />)
  696. : ''
  697. }
  698. <Checkbox
  699. label="Service User"
  700. labelPosition="right"
  701. name='is_service'
  702. checked={parseInt(this.state.user.is_service) ? true : false}
  703. onCheck={this.handleServiceUpdate.bind(this)}
  704. />
  705.  
  706. <h3>Roles</h3>
  707.  
  708. {this.props.acls.items.map((item, i) => {
  709. let checked = false;
  710. this.state.user.roles.map((r, i) => {
  711. if (r.name == item.get('name')) {
  712. checked = true;
  713. }
  714. });
  715. return <Checkbox
  716. label={item.get('name')}
  717. labelPosition="right"
  718. name={item.get('name')}
  719. key={i}
  720. defaultChecked={checked}
  721. onCheck={this.handleRoleUpdate.bind(this)}
  722. />;
  723. })}
  724.  
  725.  
  726. <Row bottom={['xs']}>
  727. <Col xs={8}>
  728. <h3>Password</h3>
  729. <TextField
  730. floatingLabelText="Enter a new password"
  731. value={this.state.changedPassword ? this.state.changedPassword : ''}
  732. fullWidth={true}
  733. disabled={parseInt(this.state.user.is_service) ? true : false}
  734. onChange={this.handlePasswordChange.bind(this)}
  735. />
  736. <LinearProgress
  737. mode="determinate"
  738. value={this.state.passwordStrength}
  739. color={this.getStrengthColor()}
  740. max={5}
  741. style={{backgroundColor: 'white'}}
  742. />
  743. </Col>
  744. <Col xs={4}>
  745. <RaisedButton
  746. onClick={this.handleGeneratePassword.bind(this)}
  747. label='Generate a password'
  748. disabled={parseInt(this.state.user.is_service) ? true : false}
  749. secondary={true}
  750. />
  751. </Col>
  752. </Row>
  753. <Row top={['xs']}>
  754. <Col xs={12}>
  755. <Checkbox
  756. label='Send user new password notification'
  757. labelPosition="right"
  758. checked={this.state.email_updated_password}
  759. onCheck={this.handleEmailPassUpdate.bind(this)}
  760. disabled={!this.state.didUpdatePassword}
  761. />
  762. </Col>
  763. </Row>
  764. </Dialog>
  765. );
  766. }
  767. }
  768.  
  769. const UserModal = connect(mapStateToProps)(UnwrappedUserModal);