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