diff --git a/resources/frontend_client/app/admin/people/actions.js b/resources/frontend_client/app/admin/people/actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..376d88978e3dd601d5d725fba272b5667d3a2285
--- /dev/null
+++ b/resources/frontend_client/app/admin/people/actions.js
@@ -0,0 +1,126 @@
+"use strict";
+
+import _ from "underscore";
+import { createAction } from "redux-actions";
+import moment from "moment";
+import { normalize, Schema, arrayOf } from "normalizr";
+
+
+// HACK: just use our Angular resources for now
+function AngularResourceProxy(serviceName, methods) {
+    methods.forEach((methodName) => {
+        this[methodName] = function(...args) {
+            let service = angular.element(document.querySelector("body")).injector().get(serviceName);
+            return service[methodName](...args).$promise;
+        }
+    });
+}
+
+// similar to createAction but accepts a (redux-thunk style) thunk and dispatches based on whether
+// the promise returned from the thunk resolves or rejects, similar to redux-promise
+function createThunkAction(actionType, actionThunkCreator) {
+    return function(...actionArgs) {
+        var thunk = actionThunkCreator(...actionArgs);
+        return async function(dispatch, getState) {
+            try {
+                let payload = await thunk(dispatch, getState);
+                dispatch({ type: actionType, payload });
+            } catch (error) {
+                dispatch({ type: actionType, payload: error, error: true });
+                throw error;
+            }
+        }
+    }
+}
+
+const user = new Schema('user');
+
+
+// resource wrappers
+const UserApi = new AngularResourceProxy("User", ["list", "update", "create", "delete"]);
+
+
+// action constants
+export const CREATE_USER = 'CREATE_USER';
+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 UPDATE_USER = 'UPDATE_USER';
+
+
+// action creators
+
+export const showAddPersonModal = createAction(SHOW_ADD_PERSON);
+export const showEditDetailsModal = createAction(SHOW_EDIT_DETAILS);
+
+export const createUser = createThunkAction(CREATE_USER, function(user) {
+    return async function(dispatch, getState) {
+        // apply any user defaults here
+        user.is_superuser = false;
+
+        let newUser = await UserApi.create(user);
+        newUser.last_login = (newUser.last_login) ? moment(newUser.last_login) : null;
+
+        return newUser;
+    };
+});
+
+export const deleteUser = createThunkAction(DELETE_USER, function(user) {
+    return async function(dispatch, getState) {
+        let resp = await UserApi.delete({
+            userId: user.id
+        });
+        return user;
+    };
+});
+
+export const fetchUsers = createThunkAction(FETCH_USERS, function() {
+    return async function(dispatch, getState) {
+        let users = await UserApi.list();
+
+        for (var u of users) {
+            u.last_login = (u.last_login) ? moment(u.last_login) : null;
+        }
+
+        return normalize(users, arrayOf(user));
+    };
+});
+
+export const grantAdmin = createThunkAction(GRANT_ADMIN, function(user) {
+    return async function(dispatch, getState) {
+        // give this user admin perms
+        user.is_superuser = true;
+
+        // do the update
+        let updatedUser = await UserApi.update(user);
+        updatedUser.last_login = (updatedUser.last_login) ? moment(updatedUser.last_login) : null;
+
+        return updatedUser;
+    };
+});
+
+export const revokeAdmin = createThunkAction(REVOKE_ADMIN, function(user) {
+    return async function(dispatch, getState) {
+        // remove user admin perms
+        user.is_superuser = false;
+
+        // do the update
+        let updatedUser = await UserApi.update(user);
+        updatedUser.last_login = (updatedUser.last_login) ? moment(updatedUser.last_login) : null;
+
+        return updatedUser;
+    };
+});
+
+export const updateUser = createThunkAction(UPDATE_USER, function(user) {
+    return async function(dispatch, getState) {
+        let updatedUser = await UserApi.update(user);
+
+        updatedUser.last_login = (updatedUser.last_login) ? moment(updatedUser.last_login) : null;
+
+        return updatedUser;
+    };
+});
diff --git a/resources/frontend_client/app/admin/people/components/AdminPeople.react.js b/resources/frontend_client/app/admin/people/components/AdminPeople.react.js
new file mode 100644
index 0000000000000000000000000000000000000000..46380360670b5bfe5aa7ced76efda3ee6db70807
--- /dev/null
+++ b/resources/frontend_client/app/admin/people/components/AdminPeople.react.js
@@ -0,0 +1,156 @@
+"use strict";
+
+import React, { Component, PropTypes } from "react";
+import _ from "underscore";
+
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.react";
+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 UserActionsSelect from "./UserActionsSelect.react";
+import UserRoleSelect from "./UserRoleSelect.react";
+import { createUser,
+         fetchUsers,
+         grantAdmin,
+         revokeAdmin,
+         showAddPersonModal,
+         showEditDetailsModal,
+         updateUser } from "../actions";
+
+
+export default class AdminPeople extends Component {
+
+    constructor(props) {
+        super(props);
+
+        this.state = { error: null };
+    }
+
+    async componentDidMount() {
+        try {
+            await this.props.dispatch(fetchUsers());
+        } catch (error) {
+            this.setState({ error });
+        }
+    }
+
+    onRoleChange(user, roleDef) {
+        if (roleDef.id === "user" && user.is_superuser) {
+            this.props.dispatch(revokeAdmin(user));
+        } else if (roleDef.id === "admin" && !user.is_superuser) {
+            this.props.dispatch(grantAdmin(user));
+        }
+    }
+
+    onAddPerson(user) {
+        // close the modal no matter what
+        this.props.dispatch(showAddPersonModal(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
+
+            this.props.dispatch(createUser(user));
+        }
+    }
+
+    renderAddPersonModal() {
+        if (!this.props.showAddPersonModal) return false;
+
+        return (
+            <Modal>
+                <ModalContent title="Add Person"
+                              closeFn={() => this.props.dispatch(showAddPersonModal(false))}>
+                    <EditUserForm
+                        buttonText="Add Person"
+                        submitFn={this.onAddPerson.bind(this)} />
+                </ModalContent>
+            </Modal>
+        );
+    }
+
+    onEditDetails(user) {
+        // close the modal no matter what
+        this.props.dispatch(showEditDetailsModal(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>
+        );
+    }
+
+    render() {
+        let users = _.values(this.props.users);
+        let { error } = this.state;
+
+        return (
+            <LoadingAndErrorWrapper loading={!users} error={error}>
+            {() =>
+                <div className="wrapper">
+                    {this.renderAddPersonModal()}
+                    {this.renderEditDetailsModal()}
+
+                    <section className="PageHeader clearfix">
+                        <a className="Button Button--primary float-right" href="#" onClick={() => this.props.dispatch(showAddPersonModal(true))}>Add person</a>
+                        <h2 className="PageTitle">People</h2>
+                    </section>
+
+                    <section>
+                        <table className="ContentTable">
+                            <thead>
+                                <tr>
+                                    <th>Name</th>
+                                    <th>Email</th>
+                                    <th>Role</th>
+                                    <th>Last Seen</th>
+                                    <th></th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                { users.map(user =>
+                                <tr>
+                                    <td><span className="text-white inline-block"><UserAvatar background={(user.is_superuser) ? "bg-purple" : "bg-brand"} user={user} /></span> <span className="ml2 text-bold">{user.common_name}</span></td>
+                                    <td>{user.email}</td>
+                                    <td>
+                                        <UserRoleSelect
+                                            user={user}
+                                            onChangeFn={this.onRoleChange.bind(this)} />
+                                    </td>
+                                    <td>{user.last_login.fromNow()}</td>
+                                    <td className="text-right">
+                                        <UserActionsSelect user={user} dispatch={this.props.dispatch} />
+                                    </td>
+                                </tr>
+                                )}
+                            </tbody>
+                        </table>
+                    </section>
+                </div>
+            }
+            </LoadingAndErrorWrapper>
+        );
+    }
+}
+
+AdminPeople.propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    users: PropTypes.array
+};
diff --git a/resources/frontend_client/app/admin/people/components/EditUserForm.react.js b/resources/frontend_client/app/admin/people/components/EditUserForm.react.js
new file mode 100644
index 0000000000000000000000000000000000000000..10fb63d80ddf3bcf75a55e7d0b8a1073765146b4
--- /dev/null
+++ b/resources/frontend_client/app/admin/people/components/EditUserForm.react.js
@@ -0,0 +1,115 @@
+"use strict";
+
+import React, { Component, PropTypes } from "react";
+import _ from "underscore";
+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";
+
+
+export default class EditUserForm extends Component {
+
+    constructor(props) {
+        super(props);
+        this.state = { formError: null, valid: false }
+    }
+
+    validateForm() {
+        let { valid } = this.state;
+        let isValid = true;
+
+        // required: first_name, last_name, email
+        for (var fieldName in this.refs) {
+            let node = React.findDOMNode(this.refs[fieldName]);
+            if (node.required && MetabaseUtils.isEmpty(node.value)) isValid = false;
+        };
+
+        if(isValid !== valid) {
+            this.setState({
+                'valid': isValid
+            });
+        }
+    }
+
+    onChange() {
+        this.validateForm();
+    }
+
+    formSubmitted(e) {
+        e.preventDefault();
+
+        this.setState({
+            formError: null
+        });
+
+        let formErrors = {data:{errors:{}}};
+
+        // validate email address
+        if (!MetabaseUtils.validEmail(React.findDOMNode(this.refs.email).value)) {
+            formErrors.data.errors.email = "Not a valid formatted email address";
+        }
+
+        if (_.keys(formErrors.data.errors).length > 0) {
+            this.setState({
+                formError: formErrors
+            });
+            return;
+        }
+
+        let user = (this.props.user) ? _.clone(this.props.user) : {};
+
+        user.first_name = React.findDOMNode(this.refs.firstName).value;
+        user.last_name = React.findDOMNode(this.refs.lastName).value;
+        user.email = React.findDOMNode(this.refs.email).value;
+
+        this.props.submitFn(user);
+    }
+
+    cancel() {
+        this.props.submitFn(null);
+    }
+
+    render() {
+        const { buttonText, user } = this.props;
+        const { formError, valid } = this.state;
+
+        return (
+            <form className="Form-new" name="userForm" onSubmit={this.formSubmitted.bind(this)} noValidate className="mt2">
+                <FormField fieldName="first_name" formError={formError}>
+                    <FormLabel title="First name" fieldName="first_name" formError={formError}></FormLabel>
+                    <input ref="firstName" className="Form-input Form-offset full" name="name" defaultValue={(user) ? user.first_name : null} placeholder="Johnny" onChange={this.onChange.bind(this)} />
+                    <span className="Form-charm"></span>
+                </FormField>
+
+                <FormField fieldName="last_name" formError={formError}>
+                    <FormLabel title="Last name" fieldName="last_name" formError={formError}></FormLabel>
+                    <input ref="lastName" className="Form-input Form-offset" name="name" defaultValue={(user) ? user.last_name : null} placeholder="Appleseed" required onChange={this.onChange.bind(this)} />
+                    <span className="Form-charm"></span>
+                </FormField>
+
+                <FormField fieldName="email" formError={formError}>
+                    <FormLabel title="Email address" fieldName="email" formError={formError}></FormLabel>
+                    <input ref="email" className="Form-input Form-offset full" name="email" defaultValue={(user) ? user.email : null} placeholder="youlooknicetoday@email.com" required onChange={this.onChange.bind(this)} />
+                    <span className="Form-charm"></span>
+                </FormField>
+
+                <div className="Form-actions">
+                    <button className={cx("Button", "mr2", {"Button--primary": valid})} disabled={!valid}>
+                        { buttonText ? buttonText : "Save Changes" }
+                    </button>
+                    or <a className="link ml1 text-bold" href="" onClick={this.cancel.bind(this)}>Cancel</a>
+                </div>
+            </form>
+        );
+    }
+}
+
+EditUserForm.propTypes = {
+    buttonText: PropTypes.string,
+    submitFn: PropTypes.func.isRequired,
+    user: PropTypes.object
+}
diff --git a/resources/frontend_client/app/admin/people/components/UserActionsSelect.react.js b/resources/frontend_client/app/admin/people/components/UserActionsSelect.react.js
new file mode 100644
index 0000000000000000000000000000000000000000..5c61372f117703291132e28cab9dcbb7949eea4c
--- /dev/null
+++ b/resources/frontend_client/app/admin/people/components/UserActionsSelect.react.js
@@ -0,0 +1,76 @@
+"use strict";
+
+import React, { Component, PropTypes } from "react";
+
+import Icon from "metabase/components/Icon.react";
+import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
+
+import { deleteUser, showEditDetailsModal } from "../actions";
+
+
+export default class UserActionsSelect extends Component {
+
+    constructor(props) {
+        super(props);
+
+        this.styles = {
+            menuList: {
+                minWidth: "90px"
+            }
+        };
+    }
+
+    onEditDetails() {
+        this.props.dispatch(showEditDetailsModal(this.props.user));
+        this.refs.popover.toggle();
+    }
+
+    onResendInvite() {
+
+    }
+
+    onResetPassword() {
+
+    }
+
+    onRemoveUser() {
+        this.props.dispatch(deleteUser(this.props.user));
+        this.refs.popover.toggle();
+    }
+
+    render() {
+        let { user } = this.props;
+
+        var triggerElement = (
+            <span className="text-grey-1"><Icon name={'ellipsis'}></Icon></span>
+        );
+
+        var tetherOptions = {
+            attachment: 'top right',
+            targetAttachment: 'bottom right',
+            targetOffset: '5px 0',
+            constraints: [{ to: 'window', attachment: 'together', pin: ['top', 'bottom']}]
+        };
+
+        return (
+            <PopoverWithTrigger ref="popover"
+                                className={"PopoverBody PopoverBody--withArrow block"}
+                                tetherOptions={tetherOptions}
+                                triggerElement={triggerElement}>
+                <ul className="UserActionsSelect">
+                    <li onClick={this.onEditDetails.bind(this)}>Edit Details</li>
+                    { user.last_login === null ?
+                        <li onClick={this.onResendInvite.bind(this)}>Re-send Invite</li>
+                    :
+                        <li onClick={this.onResetPassword.bind(this)}>Reset Password</li>
+                    }
+                    <li className="Remove" onClick={this.onRemoveUser.bind(this)}>Remove</li>
+                </ul>
+            </PopoverWithTrigger>
+        );
+    }
+}
+
+UserActionsSelect.propTypes = {
+    user: React.PropTypes.object.isRequired
+};
diff --git a/resources/frontend_client/app/admin/people/components/UserRoleSelect.react.js b/resources/frontend_client/app/admin/people/components/UserRoleSelect.react.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7d8337ab541818922c2a11d2086bf3cf01440ac
--- /dev/null
+++ b/resources/frontend_client/app/admin/people/components/UserRoleSelect.react.js
@@ -0,0 +1,76 @@
+"use strict";
+
+import React, { Component, PropTypes } from "react";
+import cx from "classnames";
+
+import ColumnarSelector from "metabase/components/ColumnarSelector.react";
+import Icon from "metabase/components/Icon.react";
+import MetabaseCore from "metabase/lib/core";
+import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
+
+
+export default class UserRoleSelect extends Component {
+
+    toggle () {
+        this.refs.popover.toggle();
+    }
+
+    render() {
+        let { user, onChangeFn } = this.props;
+        const roleDef = (user.is_superuser) ? MetabaseCore.user_roles[1] : MetabaseCore.user_roles[0];
+
+        var triggerElement = (
+            <div className={"flex align-center"}>
+                <span className="mr1">{roleDef.name}</span>
+                <Icon className="text-grey-2" name="chevrondown"  width="10" height="10"/>
+            </div>
+        );
+
+        var sections = {};
+        MetabaseCore.user_roles.forEach(function (option) {
+            var sectionName = option.section || "";
+            sections[sectionName] = sections[sectionName] || { title: sectionName || undefined, items: [] };
+            sections[sectionName].items.push(option);
+        });
+        sections = Object.keys(sections).map((sectionName) => sections[sectionName]);
+
+        var columns = [
+            {
+                selectedItem: roleDef,
+                sections: sections,
+                itemTitleFn: (item) => item.name,
+                itemDescriptionFn: (item) => item.description,
+                itemSelectFn: (item) => {
+                    onChangeFn(user, item);
+                    this.toggle();
+                }
+            }
+        ];
+
+        var tetherOptions = {
+            attachment: 'top center',
+            targetAttachment: 'bottom center',
+            targetOffset: '5px 0',
+            constraints: [{ to: 'window', attachment: 'together', pin: ['top', 'bottom']}]
+        };
+
+        return (
+            <PopoverWithTrigger ref="popover"
+                                className={"PopoverBody PopoverBody--withArrow UserRolePopover block"}
+                                tetherOptions={tetherOptions}
+                                triggerElement={triggerElement}
+                                triggerClasses={cx("AdminSelectBorderless", "py1", {"text-purple": user.is_superuser, "text-brand": !user.is_superuser})}>
+                <ColumnarSelector columns={columns}/>
+            </PopoverWithTrigger>
+        );
+    }
+}
+
+UserRoleSelect.defaultProps = {
+    isInitiallyOpen: false
+};
+
+UserRoleSelect.propTypes = {
+    user: React.PropTypes.object.isRequired,
+    onChangeFn: React.PropTypes.func.isRequired
+};
diff --git a/resources/frontend_client/app/admin/people/containers/AdminPeopleApp.react.js b/resources/frontend_client/app/admin/people/containers/AdminPeopleApp.react.js
new file mode 100644
index 0000000000000000000000000000000000000000..575c1c2882cfa068517d6d8f85552e42d79b6573
--- /dev/null
+++ b/resources/frontend_client/app/admin/people/containers/AdminPeopleApp.react.js
@@ -0,0 +1,14 @@
+"use strict";
+
+import React, { Component, PropTypes } from "react";
+import { connect } from "react-redux";
+
+import AdminPeople from "../components/AdminPeople.react";
+import { adminPeopleSelectors } from "../selectors";
+
+@connect(adminPeopleSelectors)
+export default class AdminPeopleApp extends Component {
+    render() {
+        return <AdminPeople {...this.props} />;
+    }
+}
diff --git a/resources/frontend_client/app/admin/people/partials/people.html b/resources/frontend_client/app/admin/people/partials/people.html
deleted file mode 100644
index bef4b0ea2da779d1c27efa271107f54946dfc62b..0000000000000000000000000000000000000000
--- a/resources/frontend_client/app/admin/people/partials/people.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<div class="wrapper">
-    <section class="PageHeader clearfix">
-        <a class="Button Button--primary float-right" href="/admin/people/add">Add person</a>
-        <h2 class="PageTitle">People</h2>
-    </section>
-
-    <section>
-        <table class="ContentTable">
-            <thead>
-                <tr>
-                    <th>Name</th>
-                    <th></th>
-                </tr>
-            </thead>
-            <tbody>
-                <tr ng-repeat="user in people">
-                    <td>
-                        {{user.common_name}} <span ng-show="user.is_superuser" class="AdminBadge">Admin</span>
-                    </td>
-                    <td class="Table-actions">
-                        <button class="Button" ng-click="toggle(user.id)">
-                            <span ng-show="user.is_superuser">Revoke</span>
-                            <span ng-show="!user.is_superuser">Grant</span>
-                            admin
-                        </button>
-                        <button class="Button Button--danger" ng-click="delete(user.id)" delete-confirm>Remove</button>
-                    </td>
-                </tr>
-            </tbody>
-        </table>
-    </section>
-</div>
diff --git a/resources/frontend_client/app/admin/people/partials/people_add.html b/resources/frontend_client/app/admin/people/partials/people_add.html
deleted file mode 100644
index d4818c349204144eef78f0bd64f639c003dbdf6a..0000000000000000000000000000000000000000
--- a/resources/frontend_client/app/admin/people/partials/people_add.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<div class="wrapper">
-    <section class="Breadcrumbs">
-        <a class="Breadcrumb Breadcrumb--path" href="/admin/people/">People</a>
-        <mb-icon name="chevronright" class="Breadcrumb-divider" width="12px" height="12px"></mb-icon>
-        <h2 class="Breadcrumb Breadcrumb--page">Add person</h2>
-    </section>
-
-    <section class="Grid Grid--gutters Grid--full Grid--2of3">
-        <div class="Grid-cell Cell--2of3">
-            <form class="Form-new bordered rounded shadowed" name="form" novalidate>
-                <div class="Form-field" mb-form-field="first_name">
-                    <mb-form-label display-name="First name" field-name="first_name"></mb-form-label>
-                    <input class="Form-input Form-offset full" name="first_name" placeholder="John" ng-model="newUser.first_name" required autofocus />
-                    <span class="Form-charm"></span>
-                </div>
-
-                <div class="Form-field" mb-form-field="last_name">
-                    <mb-form-label display-name="Last name" field-name="last_name"></mb-form-label>
-                    <input class="Form-input Form-offset full" name="last_name" placeholder="Doe" ng-model="newUser.last_name" required />
-                    <span class="Form-charm"></span>
-                </div>
-
-                <div class="Form-field" mb-form-field="email">
-                    <mb-form-label display-name="Email" field-name="email"></mb-form-label>
-                    <input class="Form-input Form-offset full" name="email" placeholder="johndoe@mycompany.com" ng-model="newUser.email" required />
-                    <span class="Form-charm"></span>
-                </div>
-
-                <div class="Form-actions">
-                    <button class="Button" ng-class="{'Button--primary': form.$valid}" ng-click="save(newUser)" ng-disabled="!form.$valid">
-                        Create
-                    </button>
-                </div>
-            </form>
-        </div>
-    </section>
-</div>
diff --git a/resources/frontend_client/app/admin/people/people.controllers.js b/resources/frontend_client/app/admin/people/people.controllers.js
index 41087c81ae2872f5758a27ebd10086e355927153..3d1fd28b8fe6d1a0f2ff99f43a664f1d883697a1 100644
--- a/resources/frontend_client/app/admin/people/people.controllers.js
+++ b/resources/frontend_client/app/admin/people/people.controllers.js
@@ -1,100 +1,36 @@
 'use strict';
 
-var PeopleControllers = angular.module('metabaseadmin.people.controllers', [
-    'metabase.services',
-    'metabase.forms'
-]);
-
-PeopleControllers.controller('PeopleList', ['$scope', 'User',
-    function($scope, User) {
-
-        // grant superuser permission for a given user
-        var grant = function(user) {
-            user.is_superuser = true;
-
-            User.update(user, function (result) {
-                $scope.people.forEach(function (u) {
-                    if (u.id === user.id) {
-                        u.is_superuser = true;
-                    }
-                });
-            }, function (error) {
-                console.log('error', error);
-                $scope.alertError('failed to grant superuser to user');
-            });
-        };
-
-        // revoke superuser permission for a given user
-        var revoke = function(user) {
-            user.is_superuser = false;
+import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
+import promiseMiddleware from 'redux-promise';
+import thunkMidleware from "redux-thunk";
 
-            User.update(user, function (result) {
-                $scope.people.forEach(function (u) {
-                    if (u.id === user.id) {
-                        u.is_superuser = false;
-                    }
-                });
-            }, function (error) {
-                console.log('error', error);
-                $scope.alertError('failed to revoke superuser from user');
-            });
-        };
-
-        // toggle superuser permission for a given user
-        $scope.toggle = function(userId) {
-            $scope.people.forEach(function (user) {
-                if (user.id === userId) {
-                    if (user.is_superuser) {
-                        revoke(user);
-                    } else {
-                        grant(user);
-                    }
-                }
-            });
-        };
+import AdminPeopleApp from './containers/AdminPeopleApp.react';
+import * as reducers from './reducers';
 
-        // completely remove a given user
-        // TODO: we need this api function now
-        $scope.delete = function(userId) {
-            User.delete({
-                userId: userId
-            }, function(result) {
-                for (var i = 0; i < $scope.people.length; i++) {
-                    if($scope.people[i].id === userId) {
-                        $scope.people.splice(i, 1);
-                        break;
-                    }
-                }
-            }, function (error) {
-                console.log('error', error);
-                $scope.alertError('failed to remove user');
-            });
-        };
 
-        User.list(function (result) {
-            $scope.people = result;
-        }, function (error) {
-            console.log('error', error);
-        });
-    }
-]);
+const finalCreateStore = compose(
+  applyMiddleware(
+      thunkMidleware,
+      promiseMiddleware
+  ),
+  createStore
+);
 
+const reducer = combineReducers(reducers);
 
-PeopleControllers.controller('PeopleAdd', ['$scope', '$location', 'User',
-    function($scope, $location, User) {
 
-        $scope.save = function(newUser) {
-            $scope.$broadcast("form:reset");
+var PeopleControllers = angular.module('metabaseadmin.people.controllers', ['metabase.services']);
 
-            newUser.is_superuser = false;
+PeopleControllers.controller('PeopleList', ['$scope', '$location', '$route', '$routeParams',
+    function($scope, $location, $route, $routeParams) {
 
-            // TODO: we need this function!!
-            User.create(newUser, function (result) {
-                // just go back to people listing page for now
-                $location.path('/admin/people/');
-            }, function (error) {
-                $scope.$broadcast("form:api-error", error);
-            });
+        $scope.Component = AdminPeopleApp;
+        $scope.props = {
+            user: $scope.user,
+            onChangeLocation: function(url) {
+                $scope.$apply(() => $location.url(url));
+            }
         };
+        $scope.store = finalCreateStore(reducer, {});
     }
 ]);
diff --git a/resources/frontend_client/app/admin/people/people.module.js b/resources/frontend_client/app/admin/people/people.module.js
index b3cca254f6e4c46c5ffd502ef225ec5c835be719..51719964a3deda4fd81f0ce7a41c0c78dea39079 100644
--- a/resources/frontend_client/app/admin/people/people.module.js
+++ b/resources/frontend_client/app/admin/people/people.module.js
@@ -6,12 +6,7 @@ var AdminPeople = angular.module('metabaseadmin.people', [
 
 AdminPeople.config(['$routeProvider', function ($routeProvider) {
     $routeProvider.when('/admin/people/', {
-        templateUrl: '/app/admin/people/partials/people.html',
+        template: '<div mb-redux-component class="flex flex-column flex-full" />',
         controller: 'PeopleList'
     });
-
-    $routeProvider.when('/admin/people/add', {
-        templateUrl: '/app/admin/people/partials/people_add.html',
-        controller: 'PeopleAdd'
-    });
 }]);
diff --git a/resources/frontend_client/app/admin/people/reducers.js b/resources/frontend_client/app/admin/people/reducers.js
new file mode 100644
index 0000000000000000000000000000000000000000..db006aa17f9fac5a853e3b6fded81052f68bc39c
--- /dev/null
+++ b/resources/frontend_client/app/admin/people/reducers.js
@@ -0,0 +1,36 @@
+"use strict";
+
+import _ from "underscore";
+
+import { handleActions } from 'redux-actions';
+
+import {
+    CREATE_USER,
+    DELETE_USER,
+    FETCH_USERS,
+    GRANT_ADMIN,
+    REVOKE_ADMIN,
+    SHOW_ADD_PERSON,
+    SHOW_EDIT_DETAILS,
+    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 }
+}, null);
+
+// {  let newState = { ...state }; delete newState[user.id]; return newState; }
+
+export const users = handleActions({
+    [FETCH_USERS]: { next: (state, { payload }) => ({ ...payload.entities.user }) },
+    [CREATE_USER]: { next: (state, { payload: user }) => ({ ...state, [user.id]: user }) },
+    [DELETE_USER]: { next: (state, { payload: user }) => _.omit(state, user.id) },
+    [GRANT_ADMIN]: { next: (state, { payload: user }) => ({ ...state, [user.id]: user }) },
+    [REVOKE_ADMIN]: { next: (state, { payload: user }) => ({ ...state, [user.id]: user }) },
+    [UPDATE_USER]: { next: (state, { payload: user }) => ({ ...state, [user.id]: user }) },
+}, null);
diff --git a/resources/frontend_client/app/admin/people/selectors.js b/resources/frontend_client/app/admin/people/selectors.js
new file mode 100644
index 0000000000000000000000000000000000000000..57d649119f8be64b0b437df7e3239d71a6ff6dee
--- /dev/null
+++ b/resources/frontend_client/app/admin/people/selectors.js
@@ -0,0 +1,18 @@
+"use strict";
+
+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})
+);
\ No newline at end of file
diff --git a/resources/frontend_client/app/components/UserAvatar.react.js b/resources/frontend_client/app/components/UserAvatar.react.js
index b0a95418e5482c844e83791915ff71bda55acf62..42a1e4ffa1a3b11c4de1f05b46aa95cdb6b54430 100644
--- a/resources/frontend_client/app/components/UserAvatar.react.js
+++ b/resources/frontend_client/app/components/UserAvatar.react.js
@@ -1,6 +1,6 @@
 'use strict';
 
-import React, { Component } from 'react';
+import React, { Component, PropTypes } from 'react';
 import cx from 'classnames';
 
 export default class UserAvatar extends Component {
@@ -51,5 +51,10 @@ export default class UserAvatar extends Component {
 }
 
 UserAvatar.defaultProps = {
-    background: 'bg-brand',
+    background: 'bg-brand'
 }
+
+UserAvatar.propTypes = {
+    background: PropTypes.string,
+    user: PropTypes.object.isRequired
+}
\ No newline at end of file
diff --git a/resources/frontend_client/app/css/admin.css b/resources/frontend_client/app/css/admin.css
index 0332dd7175ad46a369c7ea7dc158a5f1498fca6a..5270179c6db0bfd6959daff8c90b17488e52a40c 100644
--- a/resources/frontend_client/app/css/admin.css
+++ b/resources/frontend_client/app/css/admin.css
@@ -284,6 +284,13 @@
     min-width: 90px;
 }
 
+.AdminSelectBorderless {
+    display: inline-block;
+    font-size: 14px;
+    font-weight: 700;
+    margin-bottom: 3px;
+}
+
 
 .MetadataTable-title {
     background-color: #FCFCFC;
@@ -410,3 +417,33 @@
 .SettingsPassword {
     width: 200px;
 }
+
+.UserRolePopover .ColumnarSelector-column {
+    min-height: 180px;
+}
+
+.UserActionsSelect {
+    padding-top: 1em;
+    min-width: 180px;
+}
+
+.UserActionsSelect li {
+    padding: var(--padding-1) var(--padding-2) var(--padding-1) var(--padding-2);
+}
+
+.UserActionsSelect li:hover {
+    background-color: var(--brand-color);
+    color: #fff;
+}
+
+.UserActionsSelect li.Remove {
+    margin-top: var(--padding-1);
+    padding: var(--padding-2);
+    border-top: 1px solid #EAEAEA;
+    color: var(--error-color);
+}
+
+.UserActionsSelect li.Remove:hover {
+    background-color: var(--error-color);
+    color: #fff;
+}
diff --git a/resources/frontend_client/app/icon_paths.js b/resources/frontend_client/app/icon_paths.js
index cac939dc6e64fe1912957567a843dcb51cffd44d..e238f46fb20fa37c24420c5b4a0c6fa3f581f470 100644
--- a/resources/frontend_client/app/icon_paths.js
+++ b/resources/frontend_client/app/icon_paths.js
@@ -48,6 +48,7 @@ export var ICON_PATHS = {
         path: 'M4,8 L4,0 L7,0 L7,8 L10,8 L5.5,13.25 L1,8 L4,8 Z M11,14 L0,14 L0,17 L11,17 L11,14 Z',
         attrs: { viewBox: '0 0 11 17' }
     },
+    ellipsis: 'M26.1111111,19 C27.7066004,19 29,17.6568542 29,16 C29,14.3431458 27.7066004,13 26.1111111,13 C24.5156218,13 23.2222222,14.3431458 23.2222222,16 C23.2222222,17.6568542 24.5156218,19 26.1111111,19 Z M5.88888889,19 C7.48437817,19 8.77777778,17.6568542 8.77777778,16 C8.77777778,14.3431458 7.48437817,13 5.88888889,13 C4.29339961,13 3,14.3431458 3,16 C3,17.6568542 4.29339961,19 5.88888889,19 Z M16,19 C17.5954893,19 18.8888889,17.6568542 18.8888889,16 C18.8888889,14.3431458 17.5954893,13 16,13 C14.4045107,13 13.1111111,14.3431458 13.1111111,16 C13.1111111,17.6568542 14.4045107,19 16,19 Z',
     expand: 'M29,13.6720571 L29,8.26132482e-16 L15.3279429,8.64083276e-16 L20.3471502,5.01920738 L15.0015892,10.3647684 L18.6368207,14 L23.9823818,8.65443894 L29,13.6720571 Z M0.00158917013,15.3279429 L0.00158917013,29 L13.6736463,29 L8.65443894,23.9807926 L14,18.6352316 L10.3647684,15 L5.01920738,20.3455611 L0.00158917013,15.3279429 Z',
     explore: 'M16.4796545,16.298957 L16.4802727,23.0580389 L16.4802727,23.0580389 C17.3528782,23.2731238 18,24.0609902 18,25 C18,26.1045695 17.1045695,27 16,27 C14.8954305,27 14,26.1045695 14,25 C14,24.0751922 14.6276951,23.2969904 15.4802906,23.0681896 L15.4796772,16.3617812 L15.4796772,16.3617812 L9.42693239,19.2936488 C9.54250354,19.9090101 9.36818637,20.5691625 8.90013616,21.0538426 C8.13283771,21.8484034 6.86670062,21.8705039 6.07213982,21.1032055 C5.27757902,20.335907 5.25547851,19.06977 6.02277696,18.2752092 C6.79007541,17.4806484 8.0562125,17.4585478 8.8507733,18.2258463 C8.90464955,18.277874 8.95497425,18.3321952 9.00174214,18.3885073 L14.8957415,15.5335339 L8.95698016,12.663638 C8.54316409,13.1288103 7.91883307,13.3945629 7.25239963,13.3245179 C6.15388108,13.2090589 5.35695382,12.2249357 5.47241277,11.1264172 C5.58787172,10.0278986 6.57199493,9.23097136 7.67051349,9.34643031 C8.76903204,9.46188927 9.5659593,10.4460125 9.45050035,11.544531 C9.44231425,11.6224166 9.42976147,11.6987861 9.41311084,11.7734218 L15.4795257,14.705006 L15.4789062,7.93143834 C14.6270158,7.70216703 14,6.9243072 14,6 C14,4.8954305 14.8954305,4 16,4 C17.1045695,4 18,4.8954305 18,6 C18,6.93950562 17.3521946,7.72770818 16.4788902,7.94230133 L16.4795143,14.7663758 L22.5940736,11.8045661 C22.4397082,11.1620316 22.6068068,10.4567329 23.0998638,9.94615736 C23.8671623,9.15159656 25.1332994,9.12949606 25.9278602,9.8967945 C26.722421,10.664093 26.7445215,11.93023 25.977223,12.7247908 C25.2099246,13.5193516 23.9437875,13.5414522 23.1492267,12.7741537 C23.120046,12.7459743 23.0919072,12.717122 23.0648111,12.687645 L17.1917924,15.5324558 L23.0283963,18.3529842 C23.4420438,17.8775358 24.073269,17.604607 24.7476004,17.6754821 C25.8461189,17.7909411 26.6430462,18.7750643 26.5275872,19.8735828 C26.4121283,20.9721014 25.4280051,21.7690286 24.3294865,21.6535697 C23.230968,21.5381107 22.4340407,20.5539875 22.5494996,19.455469 C22.5569037,19.3850239 22.56788,19.315819 22.5822296,19.2480155 L16.4796545,16.298957 Z M16.0651172,6.99791382 C16.5870517,6.96436642 17,6.53040783 17,6 C17,5.44771525 16.5522847,5 16,5 C15.4477153,5 15,5.44771525 15,6 C15,6.53446591 15.4192913,6.9710011 15.9468816,6.99861337 L16.0651172,6.99791382 L16.0651172,6.99791382 Z M16,26 C16.5522847,26 17,25.5522847 17,25 C17,24.4477153 16.5522847,24 16,24 C15.4477153,24 15,24.4477153 15,25 C15,25.5522847 15.4477153,26 16,26 Z M6.56266251,20.102897 C6.80476821,20.5992873 7.40343746,20.8054256 7.89982771,20.5633199 C8.39621795,20.3212142 8.60235631,19.722545 8.36025061,19.2261547 C8.11814491,18.7297645 7.51947566,18.5236261 7.02308541,18.7657318 C6.52669517,19.0078375 6.32055681,19.6065068 6.56266251,20.102897 Z M23.6397494,11.7738453 C23.8818551,12.2702355 24.4805243,12.4763739 24.9769146,12.2342682 C25.4733048,11.9921625 25.6794432,11.3934932 25.4373375,10.897103 C25.1952318,10.4007127 24.5965625,10.1945744 24.1001723,10.4366801 C23.603782,10.6787858 23.3976437,11.277455 23.6397494,11.7738453 Z M25.4373375,20.102897 C25.6794432,19.6065068 25.4733048,19.0078375 24.9769146,18.7657318 C24.4805243,18.5236261 23.8818551,18.7297645 23.6397494,19.2261547 C23.3976437,19.722545 23.603782,20.3212142 24.1001723,20.5633199 C24.5965625,20.8054256 25.1952318,20.5992873 25.4373375,20.102897 Z M8.36025061,11.7738453 C8.60235631,11.277455 8.39621795,10.6787858 7.89982771,10.4366801 C7.40343746,10.1945744 6.80476821,10.4007127 6.56266251,10.897103 C6.32055681,11.3934932 6.52669517,11.9921625 7.02308541,12.2342682 C7.51947566,12.4763739 8.11814491,12.2702355 8.36025061,11.7738453 Z',
     filter: 'M6.57883011,7.57952565 L1.18660637e-12,-4.86721774e-13 L16,-4.92050845e-13 L9.42116989,7.57952565 L9.42116989,13.5542169 L6.57883011,15 L6.57883011,7.57952565 Z',
diff --git a/resources/frontend_client/app/lib/core.js b/resources/frontend_client/app/lib/core.js
index ecdfd70428679134cb1e35ec1ae9701340a89e0b..fa3557d853e4b2d1023439398173b5b6a2112c58 100644
--- a/resources/frontend_client/app/lib/core.js
+++ b/resources/frontend_client/app/lib/core.js
@@ -5,6 +5,16 @@ import _ from "underscore";
 
 (function() {
 
+    this.user_roles = [{
+        'id': 'user',
+        'name': 'User',
+        'description': 'Can do everything except access the Admin Panel.'
+    }, {
+        'id': 'admin',
+        'name': 'Admin',
+        'description': "Can access the Admin Panel to add or remove users and modify database settings."
+    }];
+
     this.perms = [{
         'id': 0,
         'name': 'Private'