Add ability to add a role

pull/992/head
Jared Scheib 2017-03-09 19:06:48 -08:00 committed by Andrew Watkins
parent e572c301c3
commit 51a7eae445
11 changed files with 286 additions and 59 deletions

View File

@ -3,8 +3,9 @@ import {
getRoles as getRolesAJAX,
getPermissions as getPermissionsAJAX,
createUser as createUserAJAX,
deleteRole as deleteRoleAJAX,
createRole as createRoleAJAX,
deleteUser as deleteUserAJAX,
deleteRole as deleteRoleAJAX,
updateRoleUsers as updateRoleUsersAJAX,
updateRolePermissions as updateRolePermissionsAJAX,
} from 'src/admin/apis'
@ -38,6 +39,10 @@ export const addUser = () => ({
type: 'ADD_USER',
})
export const addRole = () => ({
type: 'ADD_ROLE',
})
export const createUserSuccess = (user, createdUser) => ({
type: 'CREATE_USER_SUCCESS',
payload: {
@ -46,6 +51,14 @@ export const createUserSuccess = (user, createdUser) => ({
},
})
export const createRoleSuccess = (role, createdRole) => ({
type: 'CREATE_ROLE_SUCCESS',
payload: {
role,
createdRole,
},
})
export const editUser = (user, updates) => ({
type: 'EDIT_USER',
payload: {
@ -54,6 +67,14 @@ export const editUser = (user, updates) => ({
},
})
export const editRole = (role, updates) => ({
type: 'EDIT_ROLE',
payload: {
role,
updates,
},
})
export const killQuery = (queryID) => ({
type: 'KILL_QUERY',
payload: {
@ -75,13 +96,6 @@ export const loadQueries = (queries) => ({
},
})
export const deleteRole = (role) => ({
type: 'DELETE_ROLE',
payload: {
role,
},
})
export const deleteUser = (user) => ({
type: 'DELETE_USER',
payload: {
@ -89,10 +103,10 @@ export const deleteUser = (user) => ({
},
})
export const filterRoles = (text) => ({
type: 'FILTER_ROLES',
export const deleteRole = (role) => ({
type: 'DELETE_ROLE',
payload: {
text,
role,
},
})
@ -103,6 +117,13 @@ export const filterUsers = (text) => ({
},
})
export const filterRoles = (text) => ({
type: 'FILTER_ROLES',
payload: {
text,
},
})
// async actions
export const loadUsersAsync = (url) => async (dispatch) => {
const {data} = await getUsersAJAX(url)
@ -131,6 +152,18 @@ export const createUserAsync = (url, user) => async (dispatch) => {
}
}
export const createRoleAsync = (url, role) => async (dispatch) => {
try {
const {data} = await createRoleAJAX(url, role)
dispatch(publishNotification('success', 'Role created successfully'))
dispatch(createRoleSuccess(role, data))
} catch (error) {
// undo optimistic update
dispatch(publishNotification('error', `Failed to create role: ${error.data.message}`))
setTimeout(() => dispatch(deleteRole(role)), ADMIN_NOTIFICATION_DELAY)
}
}
export const killQueryAsync = (source, queryID) => (dispatch) => {
// optimistic update
dispatch(killQuery(queryID))

View File

@ -45,6 +45,18 @@ export const createUser = async (url, user) => {
}
}
export const createRole = async (url, role) => {
try {
return await AJAX({
method: 'POST',
url,
data: role,
})
} catch (error) {
throw error
}
}
export const deleteRole = async (url, addFlashMessage, rolename) => {
try {
const response = await AJAX({

View File

@ -11,11 +11,14 @@ const AdminTabs = ({
source,
hasRoles,
isEditingUsers,
isEditingRoles,
onClickCreate,
onEditUser,
onSaveUser,
onCancelEdit,
addFlashMessage,
onCancelEditUser,
onEditRole,
onSaveRole,
onCancelEditRole,
onDeleteRole,
onDeleteUser,
onFilterRoles,
@ -32,12 +35,11 @@ const AdminTabs = ({
allRoles={roles}
hasRoles={hasRoles}
permissions={permissions}
isEditingUsers={isEditingUsers}
isEditing={isEditingUsers}
onSave={onSaveUser}
onCancel={onCancelEditUser}
onClickCreate={onClickCreate}
onEdit={onEditUser}
onSave={onSaveUser}
onCancel={onCancelEdit}
addFlashMessage={addFlashMessage}
onDelete={onDeleteUser}
onFilter={onFilterUsers}
/>
@ -50,6 +52,11 @@ const AdminTabs = ({
roles={roles}
permissions={permissions}
allUsers={users}
isEditing={isEditingRoles}
onClickCreate={onClickCreate}
onEdit={onEditRole}
onSave={onSaveRole}
onCancel={onCancelEditRole}
onDelete={onDeleteRole}
onFilter={onFilterRoles}
onUpdateRoleUsers={onUpdateRoleUsers}
@ -102,11 +109,14 @@ AdminTabs.propTypes = {
source: shape(),
permissions: arrayOf(string),
isEditingUsers: bool,
isEditingRoles: bool,
onClickCreate: func.isRequired,
onEditUser: func.isRequired,
onSaveUser: func.isRequired,
onCancelEdit: func.isRequired,
addFlashMessage: func.isRequired,
onCancelEditUser: func.isRequired,
onEditRole: func.isRequired,
onSaveRole: func.isRequired,
onCancelEditRole: func.isRequired,
onDeleteRole: func.isRequired,
onDeleteUser: func.isRequired,
onFilterRoles: func.isRequired,

View File

@ -0,0 +1,56 @@
import React, {Component, PropTypes} from 'react'
class RoleEditingRow extends Component {
constructor(props) {
super(props)
this.handleKeyPress = ::this.handleKeyPress
this.handleEdit = ::this.handleEdit
}
handleKeyPress(role) {
return (e) => {
if (e.key === 'Enter') {
this.props.onSave(role)
}
}
}
handleEdit(role) {
return (e) => {
this.props.onEdit(role, {[e.target.name]: e.target.value})
}
}
render() {
const {role} = this.props
return (
<td>
<input
name="name"
type="text"
value={role.name || ''}
placeholder="role name"
onChange={this.handleEdit(role)}
onKeyPress={this.handleKeyPress(role)}
autoFocus={true}
/>
</td>
)
}
}
const {
bool,
func,
shape,
} = PropTypes
RoleEditingRow.propTypes = {
role: shape().isRequired,
isNew: bool,
onEdit: func.isRequired,
onSave: func.isRequired,
}
export default RoleEditingRow

View File

@ -1,7 +1,9 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
import RoleEditingRow from 'src/admin/components/RoleEditingRow'
import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
import ConfirmButtons from 'src/admin/components/ConfirmButtons'
import DeleteRow from 'src/admin/components/DeleteRow'
// TODO: replace with permissions list from server
@ -31,6 +33,11 @@ const RoleRow = ({
role: {name, permissions, users},
role,
allUsers,
isNew,
isEditing,
onEdit,
onSave,
onCancel,
onDelete,
onUpdateRoleUsers,
onUpdateRolePermissions,
@ -47,7 +54,11 @@ const RoleRow = ({
return (
<tr>
<td>{name}</td>
{
isEditing ?
<RoleEditingRow role={role} onEdit={onEdit} onSave={onSave} isNew={isNew} /> :
<td>{name}</td>
}
<td>
{
permissions && permissions.length ?
@ -71,7 +82,11 @@ const RoleRow = ({
}
</td>
<td className="text-right" style={{width: "85px"}}>
<DeleteRow onDelete={onDelete} item={role} />
{
isEditing ?
<ConfirmButtons item={role} onConfirm={onSave} onCancel={onCancel} /> :
<DeleteRow onDelete={onDelete} item={role} />
}
</td>
</tr>
)
@ -79,6 +94,7 @@ const RoleRow = ({
const {
arrayOf,
bool,
func,
shape,
string,
@ -94,6 +110,11 @@ RoleRow.propTypes = {
name: string,
})),
}).isRequired,
isNew: bool,
isEditing: bool,
onCancel: func,
onEdit: func,
onSave: func,
onDelete: func.isRequired,
allUsers: arrayOf(shape()),
onUpdateRoleUsers: func.isRequired,

View File

@ -6,13 +6,18 @@ import FilterBar from 'src/admin/components/FilterBar'
const RolesTable = ({
roles,
allUsers,
isEditing,
onClickCreate,
onEdit,
onSave,
onCancel,
onDelete,
onFilter,
onUpdateRoleUsers,
onUpdateRolePermissions,
}) => (
<div className="panel panel-info">
<FilterBar type="roles" onFilter={onFilter} />
<FilterBar type="roles" onFilter={onFilter} isEditing={isEditing} onClickCreate={onClickCreate} />
<div className="panel-body">
<table className="table v-center admin-table">
<thead>
@ -31,9 +36,14 @@ const RolesTable = ({
key={role.name}
allUsers={allUsers}
role={role}
onEdit={onEdit}
onSave={onSave}
onCancel={onCancel}
onDelete={onDelete}
onUpdateRoleUsers={onUpdateRoleUsers}
onUpdateRolePermissions={onUpdateRolePermissions}
isEditing={role.isEditing}
isNew={role.isNew}
/>
) : <EmptyRow tableName={'Roles'} />
}
@ -45,6 +55,7 @@ const RolesTable = ({
const {
arrayOf,
bool,
func,
shape,
string,
@ -61,6 +72,11 @@ RolesTable.propTypes = {
name: string,
})),
})),
isEditing: bool,
onClickCreate: func.isRequired,
onEdit: func.isRequired,
onSave: func.isRequired,
onCancel: func.isRequired,
onDelete: func.isRequired,
onFilter: func,
allUsers: arrayOf(shape()),

View File

@ -1,6 +1,6 @@
import React, {Component, PropTypes} from 'react'
class EditingRow extends Component {
class UserEditingRow extends Component {
constructor(props) {
super(props)
@ -62,11 +62,11 @@ const {
shape,
} = PropTypes
EditingRow.propTypes = {
UserEditingRow.propTypes = {
user: shape().isRequired,
isNew: bool,
onEdit: func.isRequired,
onSave: func.isRequired,
}
export default EditingRow
export default UserEditingRow

View File

@ -1,7 +1,7 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
import EditingRow from 'src/admin/components/EditingRow'
import UserEditingRow from 'src/admin/components/UserEditingRow'
import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
import ConfirmButtons from 'src/admin/components/ConfirmButtons'
import DeleteRow from 'src/admin/components/DeleteRow'
@ -23,7 +23,7 @@ const UserRow = ({
<tr className={classNames("", {"admin-table--edit-row": isEditing})}>
{
isEditing ?
<EditingRow user={user} onEdit={onEdit} onSave={onSave} isNew={isNew} /> :
<UserEditingRow user={user} onEdit={onEdit} onSave={onSave} isNew={isNew} /> :
<td>{name}</td>
}
{
@ -51,9 +51,10 @@ const UserRow = ({
}
</td>
<td className="text-right" style={{width: "85px"}}>
{isEditing ?
<ConfirmButtons item={user} onConfirm={onSave} onCancel={onCancel} /> :
<DeleteRow onDelete={onDelete} item={user} />
{
isEditing ?
<ConfirmButtons item={user} onConfirm={onSave} onCancel={onCancel} /> :
<DeleteRow onDelete={onDelete} item={user} />
}
</td>
</tr>

View File

@ -9,7 +9,7 @@ const UsersTable = ({
allRoles,
hasRoles,
permissions,
isEditingUsers,
isEditing,
onClickCreate,
onEdit,
onSave,
@ -18,7 +18,7 @@ const UsersTable = ({
onFilter,
}) => (
<div className="panel panel-info">
<FilterBar type="users" onFilter={onFilter} isEditing={isEditingUsers} onClickCreate={onClickCreate} />
<FilterBar type="users" onFilter={onFilter} isEditing={isEditing} onClickCreate={onClickCreate} />
<div className="panel-body">
<table className="table v-center admin-table">
<thead>
@ -73,7 +73,7 @@ UsersTable.propTypes = {
scope: string.isRequired,
})),
})),
isEditingUsers: bool,
isEditing: bool,
onClickCreate: func.isRequired,
onEdit: func.isRequired,
onSave: func.isRequired,

View File

@ -6,31 +6,43 @@ import {
loadRolesAsync,
loadPermissionsAsync,
addUser,
addRole,
deleteUser, // TODO rename to removeUser throughout + tests
deleteRole, // TODO rename to removeUser throughout + tests
editUser,
editRole,
createUserAsync,
deleteRoleAsync,
createRoleAsync,
deleteUserAsync,
deleteRoleAsync,
updateRoleUsersAsync,
updateRolePermissionsAsync,
filterRoles as filterRolesAction,
filterUsers as filterUsersAction,
filterRoles as filterRolesAction,
} from 'src/admin/actions'
import AdminTabs from 'src/admin/components/AdminTabs'
const isValid = (user) => {
const isValidUser = (user) => {
const minLen = 3
return (user.name.length >= minLen && user.password.length >= minLen)
}
const isValidRole = (role) => {
const minLen = 3
return (role.name.length >= minLen)
}
class AdminPage extends Component {
constructor(props) {
super(props)
this.handleClickCreate = ::this.handleClickCreate
this.handleEditUser = ::this.handleEditUser
this.handleEditRole = ::this.handleEditRole
this.handleSaveUser = ::this.handleSaveUser
this.handleCancelEdit = ::this.handleCancelEdit
this.handleSaveRole = ::this.handleSaveRole
this.handleCancelEditUser = ::this.handleCancelEditUser
this.handleCancelEditRole = ::this.handleCancelEditRole
this.handleDeleteRole = ::this.handleDeleteRole
this.handleDeleteUser = ::this.handleDeleteUser
this.handleUpdateRoleUsers = ::this.handleUpdateRoleUsers
@ -50,6 +62,8 @@ class AdminPage extends Component {
handleClickCreate(type) {
if (type === 'users') {
this.props.addUser()
} else if (type === 'roles') {
this.props.addRole()
}
}
@ -57,8 +71,12 @@ class AdminPage extends Component {
this.props.editUser(user, updates)
}
handleEditRole(role, updates) {
this.props.editRole(role, updates)
}
async handleSaveUser(user) {
if (!isValid(user)) {
if (!isValidUser(user)) {
this.props.addFlashMessage({type: 'error', text: 'Username and/or password too short'})
return
}
@ -69,10 +87,27 @@ class AdminPage extends Component {
}
}
handleCancelEdit(user) {
async handleSaveRole(role) {
if (!isValidRole(role)) {
this.props.addFlashMessage({type: 'error', text: 'Role name too short'})
return
}
if (role.isNew) {
this.props.createRole(this.props.source.links.roles, role)
} else {
// TODO update role
// console.log('update')
}
}
handleCancelEditUser(user) {
this.props.removeUser(user)
}
handleCancelEditRole(role) {
this.props.removeRole(role)
}
handleDeleteRole(role) {
this.props.deleteRole(role, this.props.addFlashMessage)
}
@ -90,7 +125,7 @@ class AdminPage extends Component {
}
render() {
const {users, roles, source, permissions, filterUsers, filterRoles, addFlashMessage} = this.props
const {users, roles, source, permissions, filterUsers, filterRoles} = this.props
const hasRoles = !!source.links.roles
const globalPermissions = permissions.find((p) => p.scope === 'all')
const allowed = globalPermissions ? globalPermissions.allowed : []
@ -118,15 +153,18 @@ class AdminPage extends Component {
permissions={allowed}
hasRoles={hasRoles}
isEditingUsers={users.some(u => u.isEditing)}
isEditingRoles={roles.some(r => r.isEditing)}
onClickCreate={this.handleClickCreate}
onEditUser={this.handleEditUser}
onEditRole={this.handleEditRole}
onSaveUser={this.handleSaveUser}
onCancelEdit={this.handleCancelEdit}
onDeleteRole={this.handleDeleteRole}
onSaveRole={this.handleSaveRole}
onCancelEditUser={this.handleCancelEditUser}
onCancelEditRole={this.handleCancelEditRole}
onDeleteUser={this.handleDeleteUser}
onDeleteRole={this.handleDeleteRole}
onFilterUsers={filterUsers}
onFilterRoles={filterRoles}
addFlashMessage={addFlashMessage}
onUpdateRoleUsers={this.handleUpdateRoleUsers}
onUpdateRolePermissions={this.handleUpdateRolePermissions}
/> :
@ -161,9 +199,13 @@ AdminPage.propTypes = {
loadRoles: func,
loadPermissions: func,
addUser: func,
addRole: func,
removeUser: func,
removeRole: func,
editUser: func,
editRole: func,
createUser: func,
createRole: func,
deleteRole: func,
deleteUser: func,
addFlashMessage: func,
@ -184,13 +226,17 @@ const mapDispatchToProps = (dispatch) => ({
loadRoles: bindActionCreators(loadRolesAsync, dispatch),
loadPermissions: bindActionCreators(loadPermissionsAsync, dispatch),
addUser: bindActionCreators(addUser, dispatch),
addRole: bindActionCreators(addRole, dispatch),
removeUser: bindActionCreators(deleteUser, dispatch),
removeRole: bindActionCreators(deleteRole, dispatch),
editUser: bindActionCreators(editUser, dispatch),
editRole: bindActionCreators(editRole, dispatch),
createUser: bindActionCreators(createUserAsync, dispatch),
deleteRole: bindActionCreators(deleteRoleAsync, dispatch),
createRole: bindActionCreators(createRoleAsync, dispatch),
deleteUser: bindActionCreators(deleteUserAsync, dispatch),
filterRoles: bindActionCreators(filterRolesAction, dispatch),
deleteRole: bindActionCreators(deleteRoleAsync, dispatch),
filterUsers: bindActionCreators(filterUsersAction, dispatch),
filterRoles: bindActionCreators(filterRolesAction, dispatch),
updateRoleUsers: bindActionCreators(updateRoleUsersAsync, dispatch),
updateRolePermissions: bindActionCreators(updateRolePermissionsAsync, dispatch),
})

View File

@ -8,6 +8,13 @@ const newDefaultUser = {
links: {self: ''},
isNew: true,
}
const newDefaultRole = {
name: '',
permissions: [],
users: [],
links: {self: ''},
isNew: true,
}
const initialState = {
users: [],
@ -42,6 +49,17 @@ export default function admin(state = initialState, action) {
}
}
case 'ADD_ROLE': {
const newRole = {...newDefaultRole, isEditing: true}
return {
...state,
roles: [
newRole,
...state.roles,
],
}
}
case 'CREATE_USER_SUCCESS': {
const {user, createdUser} = action.payload
const newState = {
@ -50,6 +68,14 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
case 'CREATE_ROLE_SUCCESS': {
const {role, createdRole} = action.payload
const newState = {
roles: state.roles.map(r => r.links.self === role.links.self ? {...createdRole} : r),
}
return {...state, ...newState}
}
case 'EDIT_USER': {
const {user, updates} = action.payload
const newState = {
@ -58,12 +84,11 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
case 'DELETE_ROLE': {
const {role} = action.payload
case 'EDIT_ROLE': {
const {role, updates} = action.payload
const newState = {
roles: state.roles.filter(r => r.name !== role.name),
roles: state.roles.map(r => r.links.self === role.links.self ? {...r, ...updates} : r),
}
return {...state, ...newState}
}
@ -76,22 +101,19 @@ export default function admin(state = initialState, action) {
return {...state, ...newState}
}
case 'LOAD_QUERIES': {
return {...state, ...action.payload}
}
case 'FILTER_ROLES': {
const {text} = action.payload
case 'DELETE_ROLE': {
const {role} = action.payload
const newState = {
roles: state.roles.map(r => {
r.hidden = !r.name.toLowerCase().includes(text)
return r
}),
roles: state.roles.filter(r => r.links.self !== role.links.self),
}
return {...state, ...newState}
}
case 'LOAD_QUERIES': {
return {...state, ...action.payload}
}
case 'FILTER_USERS': {
const {text} = action.payload
const newState = {
@ -100,7 +122,17 @@ export default function admin(state = initialState, action) {
return u
}),
}
return {...state, ...newState}
}
case 'FILTER_ROLES': {
const {text} = action.payload
const newState = {
roles: state.roles.map(r => {
r.hidden = !r.name.toLowerCase().includes(text)
return r
}),
}
return {...state, ...newState}
}