Skip to content
Snippets Groups Projects
Commit 400109f3 authored by Allen Gilliland's avatar Allen Gilliland
Browse files

second batch of updates for new people admin screens which includes all the...

second batch of updates for new people admin screens which includes all the new modals for add user scenarios (unstyled at the moment).
parent 52000ee9
No related branches found
No related tags found
No related merge requests found
Showing with 184 additions and 70 deletions
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
"jquery": "^2.1.4", "jquery": "^2.1.4",
"moment": "^2.10.6", "moment": "^2.10.6",
"normalizr": "^0.1.3", "normalizr": "^0.1.3",
"password-generator": "^2.0.1",
"react": "^0.13.3", "react": "^0.13.3",
"react-grid-layout": "^0.8.5", "react-grid-layout": "^0.8.5",
"react-onclickout": "^1.1.0", "react-onclickout": "^1.1.0",
......
"use strict"; "use strict";
import _ from "underscore";
import { createAction } from "redux-actions"; import { createAction } from "redux-actions";
import moment from "moment"; import moment from "moment";
import { normalize, Schema, arrayOf } from "normalizr"; import { normalize, Schema, arrayOf } from "normalizr";
...@@ -46,15 +45,13 @@ export const DELETE_USER = 'DELETE_USER'; ...@@ -46,15 +45,13 @@ export const DELETE_USER = 'DELETE_USER';
export const FETCH_USERS = 'FETCH_USERS'; export const FETCH_USERS = 'FETCH_USERS';
export const GRANT_ADMIN = 'GRANT_ADMIN'; export const GRANT_ADMIN = 'GRANT_ADMIN';
export const REVOKE_ADMIN = 'REVOKE_ADMIN'; export const REVOKE_ADMIN = 'REVOKE_ADMIN';
export const SHOW_ADD_PERSON = 'SHOW_ADD_PERSON'; export const SHOW_MODAL = 'SHOW_MODAL';
export const SHOW_EDIT_DETAILS = 'SHOW_EDIT_DETAILS';
export const UPDATE_USER = 'UPDATE_USER'; export const UPDATE_USER = 'UPDATE_USER';
// action creators // action creators
export const showAddPersonModal = createAction(SHOW_ADD_PERSON); export const showModal = createAction(SHOW_MODAL);
export const showEditDetailsModal = createAction(SHOW_EDIT_DETAILS);
export const createUser = createThunkAction(CREATE_USER, function(user) { export const createUser = createThunkAction(CREATE_USER, function(user) {
return async function(dispatch, getState) { return async function(dispatch, getState) {
...@@ -70,7 +67,7 @@ export const createUser = createThunkAction(CREATE_USER, function(user) { ...@@ -70,7 +67,7 @@ export const createUser = createThunkAction(CREATE_USER, function(user) {
export const deleteUser = createThunkAction(DELETE_USER, function(user) { export const deleteUser = createThunkAction(DELETE_USER, function(user) {
return async function(dispatch, getState) { return async function(dispatch, getState) {
let resp = await UserApi.delete({ await UserApi.delete({
userId: user.id userId: user.id
}); });
return user; return user;
......
...@@ -4,22 +4,30 @@ import React, { Component, PropTypes } from "react"; ...@@ -4,22 +4,30 @@ import React, { Component, PropTypes } from "react";
import _ from "underscore"; import _ from "underscore";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.react"; 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 Modal from "metabase/components/Modal.react";
import ModalContent from "metabase/components/ModalContent.react"; import ModalContent from "metabase/components/ModalContent.react";
import UserAvatar from "metabase/components/UserAvatar.react"; import UserAvatar from "metabase/components/UserAvatar.react";
import EditUserForm from "./EditUserForm.react"; import EditUserForm from "./EditUserForm.react";
import PasswordReveal from "./PasswordReveal.react";
import UserActionsSelect from "./UserActionsSelect.react"; import UserActionsSelect from "./UserActionsSelect.react";
import UserRoleSelect from "./UserRoleSelect.react"; import UserRoleSelect from "./UserRoleSelect.react";
import { createUser, import { createUser,
fetchUsers, fetchUsers,
grantAdmin, grantAdmin,
revokeAdmin, revokeAdmin,
showAddPersonModal, showModal,
showEditDetailsModal,
updateUser } from "../actions"; 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 { export default class AdminPeople extends Component {
constructor(props) { constructor(props) {
...@@ -44,72 +52,139 @@ export default class AdminPeople extends Component { ...@@ -44,72 +52,139 @@ export default class AdminPeople extends Component {
} }
} }
onAddPerson(user) { async onAddPerson(user) {
// close the modal no matter what // close the modal no matter what
this.props.dispatch(showAddPersonModal(null)); this.props.dispatch(showModal(null));
if (user) { if (user) {
// time to create a new user! // time to create a new user!
// if email is not setup -> generate temp password and allow user to retrieve it if (false && MetabaseSettings.isEmailConfigured()) {
// when email available -> confirm that invitation was sent // when email available -> confirm that invitation was sent
this.props.dispatch(createUser(user)); this.props.dispatch(createUser(user));
}
}
renderAddPersonModal() { this.props.dispatch(showModal({
if (!this.props.showAddPersonModal) return false; type: MODAL_USER_ADDED_WITH_INVITE,
details: {
user: user
}
}));
return ( } else {
<Modal> // if email is not setup -> generate temp password and allow user to retrieve it
<ModalContent title="Add Person" let autoPassword = MetabaseUtils.generatePassword();
closeFn={() => this.props.dispatch(showAddPersonModal(false))}> user.password = autoPassword;
<EditUserForm
buttonText="Add Person" this.props.dispatch(createUser(user));
submitFn={this.onAddPerson.bind(this)} />
</ModalContent> this.props.dispatch(showModal({
</Modal> type: MODAL_USER_ADDED_WITH_PASSWORD,
); details: {
user: user
}
}));
}
}
} }
onEditDetails(user) { onEditDetails(user) {
// close the modal no matter what // close the modal no matter what
this.props.dispatch(showEditDetailsModal(null)); this.props.dispatch(showModal(null));
if (user) { if (user) {
this.props.dispatch(updateUser(user)); this.props.dispatch(updateUser(user));
} }
} }
renderEditDetailsModal() { renderModal(modalType, modalDetails) {
if (!this.props.showEditDetailsModal) return false;
if (modalType === MODAL_ADD_PERSON) {
return (
<Modal> return (
<ModalContent title="Edit Details" <Modal>
closeFn={() => this.props.dispatch(showEditDetailsModal(null))}> <ModalContent title="Add Person"
<EditUserForm closeFn={() => this.props.dispatch(showModal(null))}>
user={this.props.showEditDetailsModal} <EditUserForm
submitFn={this.onEditDetails.bind(this)} /> buttonText="Add Person"
</ModalContent> submitFn={this.onAddPerson.bind(this)} />
</Modal> </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 couldnt 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 weve 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>Weve 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() { render() {
let users = _.values(this.props.users); let users = _.values(this.props.users);
let { modal } = this.props;
let { error } = this.state; let { error } = this.state;
return ( return (
<LoadingAndErrorWrapper loading={!users} error={error}> <LoadingAndErrorWrapper loading={!users} error={error}>
{() => {() =>
<div className="wrapper"> <div className="wrapper">
{this.renderAddPersonModal()} { modal ? this.renderModal(modal.type, modal.details) : null }
{this.renderEditDetailsModal()}
<section className="PageHeader clearfix"> <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> <h2 className="PageTitle">People</h2>
</section> </section>
......
...@@ -6,8 +6,6 @@ import cx from "classnames"; ...@@ -6,8 +6,6 @@ import cx from "classnames";
import FormField from "metabase/components/form/FormField.react"; import FormField from "metabase/components/form/FormField.react";
import FormLabel from "metabase/components/form/FormLabel.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"; import MetabaseUtils from "metabase/lib/utils";
......
"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
}
...@@ -10,21 +10,15 @@ import { ...@@ -10,21 +10,15 @@ import {
FETCH_USERS, FETCH_USERS,
GRANT_ADMIN, GRANT_ADMIN,
REVOKE_ADMIN, REVOKE_ADMIN,
SHOW_ADD_PERSON, SHOW_MODAL,
SHOW_EDIT_DETAILS,
UPDATE_USER UPDATE_USER
} from './actions'; } from './actions';
export const showAddPersonModal = handleActions({ export const modal = handleActions({
[SHOW_ADD_PERSON]: { next: (state, { payload }) => payload } [SHOW_MODAL]: { next: (state, { payload }) => payload }
}, false);
export const showEditDetailsModal = handleActions({
[SHOW_EDIT_DETAILS]: { next: (state, { payload }) => payload }
}, null); }, null);
// { let newState = { ...state }; delete newState[user.id]; return newState; }
export const users = handleActions({ export const users = handleActions({
[FETCH_USERS]: { next: (state, { payload }) => ({ ...payload.entities.user }) }, [FETCH_USERS]: { next: (state, { payload }) => ({ ...payload.entities.user }) },
......
...@@ -4,15 +4,6 @@ import { createSelector } from 'reselect'; ...@@ -4,15 +4,6 @@ import { createSelector } from 'reselect';
// our master selector which combines all of our partial selectors above // our master selector which combines all of our partial selectors above
export const adminPeopleSelectors = createSelector( export const adminPeopleSelectors = createSelector(
[state => state.showAddPersonModal, [state => state.modal, state => state.users],
state => state.showEditDetailsModal, (modal, users) => ({modal, users})
state => state.users],
(showAddPersonModal,
showEditDetailsModal,
users) =>
({showAddPersonModal,
showEditDetailsModal,
users})
); );
\ No newline at end of file
...@@ -22,6 +22,11 @@ const MetabaseSettings = { ...@@ -22,6 +22,11 @@ const MetabaseSettings = {
// these are all special accessors which provide a lookup of a property plus some additional help // 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() { hasSetupToken: function() {
return (mb_settings.setup_token !== undefined && mb_settings.setup_token !== null); return (mb_settings.setup_token !== undefined && mb_settings.setup_token !== null);
}, },
......
'use strict'; 'use strict';
import generatePassword from "password-generator";
// provides functions for building urls to things we care about // provides functions for building urls to things we care about
var MetabaseUtils = { var MetabaseUtils = {
generatePassword: function(length) {
const len = length || 14;
return generatePassword(len, false);
},
isEmpty: function(str) { isEmpty: function(str) {
return (!str || 0 === str.length); return (!str || 0 === str.length);
}, },
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
(defendpoint POST "/" (defendpoint POST "/"
"Create a new `User`." "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] {first_name [Required NonEmptyString]
last_name [Required NonEmptyString] last_name [Required NonEmptyString]
email [Required Email]} email [Required Email]}
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
(let [existing-user (sel :one [User :id :is_active] :email email)] (let [existing-user (sel :one [User :id :is_active] :email email)]
(-> (cond (-> (cond
;; new user account, so create it ;; 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 ;; this user already exists but is inactive, so simply reactivate the account
(not (:is_active existing-user)) (do (not (:is_active existing-user)) (do
(upd User (:id existing-user) (upd User (:id existing-user)
......
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
(defn create-user (defn create-user
"Convenience function for creating a new `User` and sending out the welcome email." "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}}] :or {send-welcome false}}]
{:pre [(string? first-name) {:pre [(string? first-name)
(string? last-name) (string? last-name)
...@@ -71,7 +71,9 @@ ...@@ -71,7 +71,9 @@
:email email-address :email email-address
:first_name first-name :first_name first-name
:last_name last-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 (when send-welcome
(let [reset-token (set-user-password-reset-token (:id new-user)) (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 ;; NOTE: the new user join url is just a password reset with an indicator that this is a first time user
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment