From 400109f37ef7e7df6481efa061e56bdc4bfbcc0d Mon Sep 17 00:00:00 2001 From: Allen Gilliland <agilliland@gmail.com> Date: Tue, 29 Sep 2015 22:29:58 -0700 Subject: [PATCH] second batch of updates for new people admin screens which includes all the new modals for add user scenarios (unstyled at the moment). --- package.json | 1 + .../app/admin/people/actions.js | 9 +- .../people/components/AdminPeople.react.js | 151 +++++++++++++----- .../people/components/EditUserForm.react.js | 2 - .../people/components/PasswordReveal.react.js | 43 +++++ .../app/admin/people/reducers.js | 12 +- .../app/admin/people/selectors.js | 13 +- resources/frontend_client/app/lib/settings.js | 5 + resources/frontend_client/app/lib/utils.js | 8 + src/metabase/api/user.clj | 4 +- src/metabase/models/user.clj | 6 +- 11 files changed, 184 insertions(+), 70 deletions(-) create mode 100644 resources/frontend_client/app/admin/people/components/PasswordReveal.react.js diff --git a/package.json b/package.json index ff05a346824..09f99ecdd18 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "jquery": "^2.1.4", "moment": "^2.10.6", "normalizr": "^0.1.3", + "password-generator": "^2.0.1", "react": "^0.13.3", "react-grid-layout": "^0.8.5", "react-onclickout": "^1.1.0", diff --git a/resources/frontend_client/app/admin/people/actions.js b/resources/frontend_client/app/admin/people/actions.js index 376d88978e3..26315c4eb2f 100644 --- a/resources/frontend_client/app/admin/people/actions.js +++ b/resources/frontend_client/app/admin/people/actions.js @@ -1,6 +1,5 @@ "use strict"; -import _ from "underscore"; import { createAction } from "redux-actions"; import moment from "moment"; import { normalize, Schema, arrayOf } from "normalizr"; @@ -46,15 +45,13 @@ export const DELETE_USER = 'DELETE_USER'; export const FETCH_USERS = 'FETCH_USERS'; export const GRANT_ADMIN = 'GRANT_ADMIN'; export const REVOKE_ADMIN = 'REVOKE_ADMIN'; -export const SHOW_ADD_PERSON = 'SHOW_ADD_PERSON'; -export const SHOW_EDIT_DETAILS = 'SHOW_EDIT_DETAILS'; +export const SHOW_MODAL = 'SHOW_MODAL'; export const UPDATE_USER = 'UPDATE_USER'; // action creators -export const showAddPersonModal = createAction(SHOW_ADD_PERSON); -export const showEditDetailsModal = createAction(SHOW_EDIT_DETAILS); +export const showModal = createAction(SHOW_MODAL); export const createUser = createThunkAction(CREATE_USER, function(user) { return async function(dispatch, getState) { @@ -70,7 +67,7 @@ export const createUser = createThunkAction(CREATE_USER, function(user) { export const deleteUser = createThunkAction(DELETE_USER, function(user) { return async function(dispatch, getState) { - let resp = await UserApi.delete({ + await UserApi.delete({ userId: user.id }); return user; diff --git a/resources/frontend_client/app/admin/people/components/AdminPeople.react.js b/resources/frontend_client/app/admin/people/components/AdminPeople.react.js index 46380360670..04b816f58b4 100644 --- a/resources/frontend_client/app/admin/people/components/AdminPeople.react.js +++ b/resources/frontend_client/app/admin/people/components/AdminPeople.react.js @@ -4,22 +4,30 @@ import React, { Component, PropTypes } from "react"; import _ from "underscore"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.react"; +import MetabaseSettings from "metabase/lib/settings"; +import MetabaseUtils from "metabase/lib/utils"; import Modal from "metabase/components/Modal.react"; import ModalContent from "metabase/components/ModalContent.react"; import UserAvatar from "metabase/components/UserAvatar.react"; import EditUserForm from "./EditUserForm.react"; +import PasswordReveal from "./PasswordReveal.react"; import UserActionsSelect from "./UserActionsSelect.react"; import UserRoleSelect from "./UserRoleSelect.react"; import { createUser, fetchUsers, grantAdmin, revokeAdmin, - showAddPersonModal, - showEditDetailsModal, + showModal, updateUser } from "../actions"; +const MODAL_ADD_PERSON = 'MODAL_ADD_PERSON'; +const MODAL_EDIT_DETAILS = 'MODAL_EDIT_DETAILS'; +const MODAL_USER_ADDED_WITH_INVITE = 'MODAL_USER_ADDED_WITH_INVITE'; +const MODAL_USER_ADDED_WITH_PASSWORD = 'MODAL_USER_ADDED_WITH_PASSWORD'; + + export default class AdminPeople extends Component { constructor(props) { @@ -44,72 +52,139 @@ export default class AdminPeople extends Component { } } - onAddPerson(user) { + async onAddPerson(user) { // close the modal no matter what - this.props.dispatch(showAddPersonModal(null)); + this.props.dispatch(showModal(null)); if (user) { // time to create a new user! - // if email is not setup -> generate temp password and allow user to retrieve it - // when email available -> confirm that invitation was sent + if (false && MetabaseSettings.isEmailConfigured()) { + // when email available -> confirm that invitation was sent - this.props.dispatch(createUser(user)); - } - } + this.props.dispatch(createUser(user)); - renderAddPersonModal() { - if (!this.props.showAddPersonModal) return false; + this.props.dispatch(showModal({ + type: MODAL_USER_ADDED_WITH_INVITE, + details: { + user: user + } + })); - return ( - <Modal> - <ModalContent title="Add Person" - closeFn={() => this.props.dispatch(showAddPersonModal(false))}> - <EditUserForm - buttonText="Add Person" - submitFn={this.onAddPerson.bind(this)} /> - </ModalContent> - </Modal> - ); + } else { + // if email is not setup -> generate temp password and allow user to retrieve it + let autoPassword = MetabaseUtils.generatePassword(); + user.password = autoPassword; + + this.props.dispatch(createUser(user)); + + this.props.dispatch(showModal({ + type: MODAL_USER_ADDED_WITH_PASSWORD, + details: { + user: user + } + })); + } + } } onEditDetails(user) { // close the modal no matter what - this.props.dispatch(showEditDetailsModal(null)); + this.props.dispatch(showModal(null)); if (user) { this.props.dispatch(updateUser(user)); } } - renderEditDetailsModal() { - if (!this.props.showEditDetailsModal) return false; - - return ( - <Modal> - <ModalContent title="Edit Details" - closeFn={() => this.props.dispatch(showEditDetailsModal(null))}> - <EditUserForm - user={this.props.showEditDetailsModal} - submitFn={this.onEditDetails.bind(this)} /> - </ModalContent> - </Modal> - ); + renderModal(modalType, modalDetails) { + + if (modalType === MODAL_ADD_PERSON) { + + return ( + <Modal> + <ModalContent title="Add Person" + closeFn={() => this.props.dispatch(showModal(null))}> + <EditUserForm + buttonText="Add Person" + submitFn={this.onAddPerson.bind(this)} /> + </ModalContent> + </Modal> + ); + + } else if (modalType === MODAL_EDIT_DETAILS) { + let { user } = modalDetails; + + return ( + <Modal> + <ModalContent title="Edit Details" + closeFn={() => this.props.dispatch(showModal(null))}> + <EditUserForm + user={user} + submitFn={this.onEditDetails.bind(this)} /> + </ModalContent> + </Modal> + ); + + } else if (modalType === MODAL_USER_ADDED_WITH_PASSWORD) { + let { user } = modalDetails; + + return ( + <Modal> + <ModalContent title={user.first_name+" has been added"} + closeFn={() => this.props.dispatch(showModal(null))}> + <div> + <p>We couldn’t send them an email invitation, + so make sure to tell them to log in using <span className="text-bold">{user.email}</span> + and this password we’ve generated for them:</p> + + <PasswordReveal password={user.password}></PasswordReveal> + + <p>If you want to be able to send email invites, just go to the <a className="link" href="/admin/settings">Email Settings</a> page.</p> + + <div className="Form-actions"> + <button className="Button Button--primary mr2" onClick={() => this.props.dispatch(showModal(null))}>Done</button> + or <a className="link ml1 text-bold" href="" onClick={() => this.props.dispatch(showModal({type: MODAL_ADD_PERSON}))}>Add another person</a> + </div> + </div> + </ModalContent> + </Modal> + ); + + } else if (modalType === MODAL_USER_ADDED_WITH_INVITE) { + let { user } = modalDetails; + + return ( + <Modal> + <ModalContent title={user.first_name+" has been added"} + closeFn={() => this.props.dispatch(showModal(null))}> + <div> + <p>We’ve sent an invite to <span className="text-bold">{user.email}</span> with instructions to set their password.</p> + + <div className="Form-actions"> + <button className="Button Button--primary mr2" onClick={() => this.props.dispatch(showModal(null))}>Done</button> + or <a className="link ml1 text-bold" href="" onClick={() => this.props.dispatch(showModal({type: MODAL_ADD_PERSON}))}>Add another person</a> + </div> + </div> + </ModalContent> + </Modal> + ); + } } render() { let users = _.values(this.props.users); + let { modal } = this.props; let { error } = this.state; return ( <LoadingAndErrorWrapper loading={!users} error={error}> {() => <div className="wrapper"> - {this.renderAddPersonModal()} - {this.renderEditDetailsModal()} + { modal ? this.renderModal(modal.type, modal.details) : null } <section className="PageHeader clearfix"> - <a className="Button Button--primary float-right" href="#" onClick={() => this.props.dispatch(showAddPersonModal(true))}>Add person</a> + <a className="Button Button--primary float-right" href="#" onClick={() => this.props.dispatch(showModal({type: MODAL_ADD_PERSON}))}>Add person</a> <h2 className="PageTitle">People</h2> </section> diff --git a/resources/frontend_client/app/admin/people/components/EditUserForm.react.js b/resources/frontend_client/app/admin/people/components/EditUserForm.react.js index 10fb63d80dd..f6849ad19a0 100644 --- a/resources/frontend_client/app/admin/people/components/EditUserForm.react.js +++ b/resources/frontend_client/app/admin/people/components/EditUserForm.react.js @@ -6,8 +6,6 @@ import cx from "classnames"; import FormField from "metabase/components/form/FormField.react"; import FormLabel from "metabase/components/form/FormLabel.react"; -import FormMessage from "metabase/components/form/FormMessage.react"; -import MetabaseSettings from "metabase/lib/settings"; import MetabaseUtils from "metabase/lib/utils"; diff --git a/resources/frontend_client/app/admin/people/components/PasswordReveal.react.js b/resources/frontend_client/app/admin/people/components/PasswordReveal.react.js new file mode 100644 index 00000000000..30aedf446b1 --- /dev/null +++ b/resources/frontend_client/app/admin/people/components/PasswordReveal.react.js @@ -0,0 +1,43 @@ +"use strict"; + +import React, { Component, PropTypes } from "react"; + + +export default class PasswordReveal extends Component { + + constructor(props) { + super(props); + this.state = { visible: false }; + } + + onToggleVisibility() { + this.setState({ + visible: !this.state.visible + }); + } + + render() { + const { password } = this.props; + const { visible } = this.state; + + return ( + <div> + { visible ? + <input type="text" value={password} /> + : + <input type="password" value={password} /> + } + + { visible ? + <a href="#" className="link" onClick={this.onToggleVisibility.bind(this)}>Hide</a> + : + <a href="#" className="link" onClick={this.onToggleVisibility.bind(this)}>Show</a> + } + </div> + ); + } +} + +PasswordReveal.propTypes = { + password: PropTypes.string.isRequired +} diff --git a/resources/frontend_client/app/admin/people/reducers.js b/resources/frontend_client/app/admin/people/reducers.js index db006aa17f9..82b824521d3 100644 --- a/resources/frontend_client/app/admin/people/reducers.js +++ b/resources/frontend_client/app/admin/people/reducers.js @@ -10,21 +10,15 @@ import { FETCH_USERS, GRANT_ADMIN, REVOKE_ADMIN, - SHOW_ADD_PERSON, - SHOW_EDIT_DETAILS, + SHOW_MODAL, UPDATE_USER } from './actions'; -export const showAddPersonModal = handleActions({ - [SHOW_ADD_PERSON]: { next: (state, { payload }) => payload } -}, false); - -export const showEditDetailsModal = handleActions({ - [SHOW_EDIT_DETAILS]: { next: (state, { payload }) => payload } +export const modal = handleActions({ + [SHOW_MODAL]: { next: (state, { payload }) => payload } }, null); -// { let newState = { ...state }; delete newState[user.id]; return newState; } export const users = handleActions({ [FETCH_USERS]: { next: (state, { payload }) => ({ ...payload.entities.user }) }, diff --git a/resources/frontend_client/app/admin/people/selectors.js b/resources/frontend_client/app/admin/people/selectors.js index 57d649119f8..efbe60e67b2 100644 --- a/resources/frontend_client/app/admin/people/selectors.js +++ b/resources/frontend_client/app/admin/people/selectors.js @@ -4,15 +4,6 @@ import { createSelector } from 'reselect'; // our master selector which combines all of our partial selectors above export const adminPeopleSelectors = createSelector( - [state => state.showAddPersonModal, - state => state.showEditDetailsModal, - state => state.users], - - (showAddPersonModal, - showEditDetailsModal, - users) => - - ({showAddPersonModal, - showEditDetailsModal, - users}) + [state => state.modal, state => state.users], + (modal, users) => ({modal, users}) ); \ No newline at end of file diff --git a/resources/frontend_client/app/lib/settings.js b/resources/frontend_client/app/lib/settings.js index 7e2ae48bfa8..de1834544f7 100644 --- a/resources/frontend_client/app/lib/settings.js +++ b/resources/frontend_client/app/lib/settings.js @@ -22,6 +22,11 @@ const MetabaseSettings = { // these are all special accessors which provide a lookup of a property plus some additional help + isEmailConfigured: function() { + // for now just assume that if they set an SMTP host that they are running with emails + return (mb_settings['email-smtp-host'] !== undefined && mb_settings['email-smtp-host'] !== null); + }, + hasSetupToken: function() { return (mb_settings.setup_token !== undefined && mb_settings.setup_token !== null); }, diff --git a/resources/frontend_client/app/lib/utils.js b/resources/frontend_client/app/lib/utils.js index 7e303076763..a4dcfab8c17 100644 --- a/resources/frontend_client/app/lib/utils.js +++ b/resources/frontend_client/app/lib/utils.js @@ -1,7 +1,15 @@ 'use strict'; +import generatePassword from "password-generator"; + + // provides functions for building urls to things we care about var MetabaseUtils = { + generatePassword: function(length) { + const len = length || 14; + return generatePassword(len, false); + }, + isEmpty: function(str) { return (!str || 0 === str.length); }, diff --git a/src/metabase/api/user.clj b/src/metabase/api/user.clj index d2ef029148a..24c876a40b0 100644 --- a/src/metabase/api/user.clj +++ b/src/metabase/api/user.clj @@ -24,7 +24,7 @@ (defendpoint POST "/" "Create a new `User`." - [:as {{:keys [first_name last_name email]} :body :as request}] + [:as {{:keys [first_name last_name email password]} :body :as request}] {first_name [Required NonEmptyString] last_name [Required NonEmptyString] email [Required Email]} @@ -32,7 +32,7 @@ (let [existing-user (sel :one [User :id :is_active] :email email)] (-> (cond ;; new user account, so create it - (nil? existing-user) (create-user first_name last_name email :send-welcome true :invitor @*current-user*) + (nil? existing-user) (create-user first_name last_name email :password password :send-welcome true :invitor @*current-user*) ;; this user already exists but is inactive, so simply reactivate the account (not (:is_active existing-user)) (do (upd User (:id existing-user) diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj index 33764008815..b6c15e03fee 100644 --- a/src/metabase/models/user.clj +++ b/src/metabase/models/user.clj @@ -62,7 +62,7 @@ (defn create-user "Convenience function for creating a new `User` and sending out the welcome email." - [first-name last-name email-address & {:keys [send-welcome invitor] + [first-name last-name email-address & {:keys [send-welcome invitor password] :or {send-welcome false}}] {:pre [(string? first-name) (string? last-name) @@ -71,7 +71,9 @@ :email email-address :first_name first-name :last_name last-name - :password (str (java.util.UUID/randomUUID)))] + :password (if (not (nil? password)) + password + (str (java.util.UUID/randomUUID))))] (when send-welcome (let [reset-token (set-user-password-reset-token (:id new-user)) ;; NOTE: the new user join url is just a password reset with an indicator that this is a first time user -- GitLab