diff --git a/ui/spec/admin/reducers/adminSpec.js b/ui/spec/admin/reducers/adminSpec.js new file mode 100644 index 000000000..699150408 --- /dev/null +++ b/ui/spec/admin/reducers/adminSpec.js @@ -0,0 +1,57 @@ +import reducer from 'src/admin/reducers/admin' + +import { + loadRoles, + deleteRole, + deleteUser, +} from 'src/admin/actions' + +let state = undefined +const r1 = {name: 'role1'} +const r2 = {name: 'role2'} +const roles = [r1, r2] + +const u1 = {name: 'user1'} +const u2 = {name: 'user2'} +const users = [u1, u2] + +describe('Admin.Reducers', () => { + it('it can load the roles', () => { + const actual = reducer(state, loadRoles({roles})) + const expected = { + roles, + } + + expect(actual.roles).to.deep.equal(expected.roles) + }) + + it('it can delete a role', () => { + state = { + roles: [ + r1, + ] + } + + const actual = reducer(state, deleteRole(r1)) + const expected = { + roles: [], + } + + expect(actual.roles).to.deep.equal(expected.roles) + }) + + it('it can delete a user', () => { + state = { + users: [ + u1, + ] + } + + const actual = reducer(state, deleteUser(u1)) + const expected = { + users: [], + } + + expect(actual.users).to.deep.equal(expected.users) + }) +}) diff --git a/ui/src/admin/actions/index.js b/ui/src/admin/actions/index.js index 5b0db3331..bdb39ada7 100644 --- a/ui/src/admin/actions/index.js +++ b/ui/src/admin/actions/index.js @@ -1,4 +1,9 @@ -import {getUsers, getRoles} from 'src/admin/apis' +import { + getUsers, + getRoles, + deleteRole as deleteRoleAJAX, + deleteUser as deleteUserAJAX, +} from 'src/admin/apis' import {killQuery as killQueryProxy} from 'shared/apis/metaQuery' export const loadUsers = ({users}) => ({ @@ -29,11 +34,6 @@ export const loadQueries = (queries) => ({ }, }) -// async actions -export const loadUsersAsync = (url) => async (dispatch) => { - const {data} = await getUsers(url) - dispatch(loadUsers(data)) -} export const loadRoles = ({roles}) => ({ type: 'LOAD_ROLES', payload: { @@ -41,6 +41,26 @@ export const loadRoles = ({roles}) => ({ }, }) +export const deleteRole = (role) => ({ + type: 'DELETE_ROLE', + payload: { + role, + }, +}) + +export const deleteUser = (user) => ({ + type: 'DELETE_USER', + payload: { + user, + }, +}) + +// async actions +export const loadUsersAsync = (url) => async (dispatch) => { + const {data} = await getUsers(url) + dispatch(loadUsers(data)) +} + export const loadRolesAsync = (url) => async (dispatch) => { const {data} = await getRoles(url) dispatch(loadRoles(data)) @@ -54,3 +74,19 @@ export const killQueryAsync = (source, queryID) => (dispatch) => { // kill query on server killQueryProxy(source, queryID) } + +export const deleteRoleAsync = (role, addFlashMessage) => (dispatch) => { + // optimistic update + dispatch(deleteRole(role)) + + // delete role on server + deleteRoleAJAX(role.links.self, addFlashMessage, role.name) +} + +export const deleteUserAsync = (user, addFlashMessage) => (dispatch) => { + // optimistic update + dispatch(deleteUser(user)) + + // delete user on server + deleteUserAJAX(user.links.self, addFlashMessage, user.name) +} diff --git a/ui/src/admin/apis/index.js b/ui/src/admin/apis/index.js index 11ae3094b..b9126cd97 100644 --- a/ui/src/admin/apis/index.js +++ b/ui/src/admin/apis/index.js @@ -21,3 +21,43 @@ export const getRoles = async (url) => { console.error(error) // eslint-disable-line no-console } } + +export const deleteRole = async (url, addFlashMessage, rolename) => { + try { + const response = await AJAX({ + method: 'DELETE', + url, + }) + addFlashMessage({ + type: 'success', + text: `${rolename} successfully deleted.`, + }) + return response + } catch (error) { + console.error(error) // eslint-disable-line no-console + addFlashMessage({ + type: 'error', + text: `Error deleting: ${rolename}.`, + }) + } +} + +export const deleteUser = async (url, addFlashMessage, username) => { + try { + const response = await AJAX({ + method: 'DELETE', + url, + }) + addFlashMessage({ + type: 'success', + text: `${username} successfully deleted.`, + }) + return response + } catch (error) { + console.error(error) // eslint-disable-line no-console + addFlashMessage({ + type: 'error', + text: `Error deleting: ${username}.`, + }) + } +} diff --git a/ui/src/admin/components/AdminTabs.js b/ui/src/admin/components/AdminTabs.js index 80410a8f0..b32e5e954 100644 --- a/ui/src/admin/components/AdminTabs.js +++ b/ui/src/admin/components/AdminTabs.js @@ -22,7 +22,7 @@ class AdminTabs extends Component { } render() { - const {users, roles, source} = this.props + const {users, roles, source, onDeleteRole, onDeleteUser} = this.props return ( @@ -35,11 +35,13 @@ class AdminTabs extends Component { @@ -53,6 +55,7 @@ class AdminTabs extends Component { const { arrayOf, + func, shape, string, } = PropTypes @@ -66,6 +69,8 @@ AdminTabs.propTypes = { })), source: shape(), roles: arrayOf(shape()), + onDeleteRole: func.isRequired, + onDeleteUser: func.isRequired, } export default AdminTabs diff --git a/ui/src/admin/components/DeleteRow.js b/ui/src/admin/components/DeleteRow.js new file mode 100644 index 000000000..e92daf1b3 --- /dev/null +++ b/ui/src/admin/components/DeleteRow.js @@ -0,0 +1,92 @@ +import React, {PropTypes, Component} from 'react' +import OnClickOutside from 'shared/components/OnClickOutside' + +const DeleteButton = ({onConfirm}) => ( + +) + +const ConfirmButtons = ({onDelete, item, onCancel}) => ( +
+ + +
+) + +class DeleteRow extends Component { + constructor(props) { + super(props) + this.state = { + isConfirmed: false, + } + this.handleConfirm = ::this.handleConfirm + this.handleCancel = ::this.handleCancel + } + + handleConfirm() { + this.setState({isConfirmed: true}) + } + + handleCancel() { + this.setState({isConfirmed: false}) + } + + handleClickOutside() { + this.setState({isConfirmed: false}) + } + + render() { + const {onDelete, item} = this.props + const {isConfirmed} = this.state + + if (isConfirmed) { + return ( + + ) + } + + return ( + + ) + } +} + +const { + func, + shape, +} = PropTypes + +DeleteButton.propTypes = { + onConfirm: func.isRequired, +} + +ConfirmButtons.propTypes = { + onDelete: func.isRequired, + item: shape({}).isRequired, + onCancel: func.isRequired, +} + +DeleteRow.propTypes = { + item: shape({}), + onDelete: func.isRequired, +} + +export default OnClickOutside(DeleteRow) diff --git a/ui/src/admin/components/RoleRow.js b/ui/src/admin/components/RoleRow.js index 10bee300c..da19f305e 100644 --- a/ui/src/admin/components/RoleRow.js +++ b/ui/src/admin/components/RoleRow.js @@ -2,6 +2,7 @@ import React, {PropTypes} from 'react' import _ from 'lodash' import MultiSelectDropdown from 'shared/components/MultiSelectDropdown' +import DeleteRow from 'src/admin/components/DeleteRow' const PERMISSIONS = [ "NoPermissions", @@ -25,7 +26,7 @@ const PERMISSIONS = [ "KapacitorConfigAPI", ] -const RoleRow = ({role: {name, permissions, users}}) => ( +const RoleRow = ({role: {name, permissions, users}, role, onDelete}) => ( {name} @@ -43,21 +44,22 @@ const RoleRow = ({role: {name, permissions, users}}) => ( { users && users.length ? role.name)} + items={users.map((r) => r.name)} selectedItems={[]} label={'Select Users'} onApply={() => '//TODO'} /> : '\u2014' } - - + + ) const { arrayOf, + func, shape, string, } = PropTypes @@ -72,6 +74,7 @@ RoleRow.propTypes = { name: string, })), }).isRequired, + onDelete: func.isRequired, } export default RoleRow diff --git a/ui/src/admin/components/RolesTable.js b/ui/src/admin/components/RolesTable.js index acb045f02..8f02179e1 100644 --- a/ui/src/admin/components/RolesTable.js +++ b/ui/src/admin/components/RolesTable.js @@ -2,7 +2,7 @@ import React, {PropTypes} from 'react' import RoleRow from 'src/admin/components/RoleRow' import EmptyRow from 'src/admin/components/EmptyRow' -const RolesTable = ({roles}) => ( +const RolesTable = ({roles, onDelete}) => (
@@ -31,7 +31,7 @@ const RolesTable = ({roles}) => ( { roles.length ? roles.map((role) => - + ) : } @@ -42,6 +42,7 @@ const RolesTable = ({roles}) => ( const { arrayOf, + func, shape, string, } = PropTypes @@ -57,6 +58,7 @@ RolesTable.propTypes = { name: string, })), })), + onDelete: func.isRequired, } export default RolesTable diff --git a/ui/src/admin/components/UserRow.js b/ui/src/admin/components/UserRow.js index 671cc259d..7bdb0a8f6 100644 --- a/ui/src/admin/components/UserRow.js +++ b/ui/src/admin/components/UserRow.js @@ -1,7 +1,8 @@ import React, {PropTypes} from 'react' import MultiSelectDropdown from 'shared/components/MultiSelectDropdown' +import DeleteRow from 'src/admin/components/DeleteRow' -const UserRow = ({user: {name, roles, permissions}}) => ( +const UserRow = ({user: {name, roles, permissions}, user, onDelete}) => ( {name} @@ -26,11 +27,15 @@ const UserRow = ({user: {name, roles, permissions}}) => ( /> : '\u2014' } + + + ) const { arrayOf, + func, shape, string, } = PropTypes @@ -45,6 +50,7 @@ UserRow.propTypes = { name: string, })), }).isRequired, + onDelete: func.isRequired, } export default UserRow diff --git a/ui/src/admin/components/UsersTable.js b/ui/src/admin/components/UsersTable.js index e8af96c6b..886e5bacd 100644 --- a/ui/src/admin/components/UsersTable.js +++ b/ui/src/admin/components/UsersTable.js @@ -2,22 +2,23 @@ import React, {PropTypes} from 'react' import UserRow from 'src/admin/components/UserRow' import EmptyRow from 'src/admin/components/EmptyRow' -const UsersTable = ({users}) => ( -
+const UsersTable = ({users, onDelete}) => ( +
- +
+ { users.length ? users.map((user) => - + ) : } @@ -28,6 +29,7 @@ const UsersTable = ({users}) => ( const { arrayOf, + func, shape, string, } = PropTypes @@ -43,6 +45,7 @@ UsersTable.propTypes = { scope: string.isRequired, })), })), + onDelete: func.isRequired, } export default UsersTable diff --git a/ui/src/admin/containers/AdminPage.js b/ui/src/admin/containers/AdminPage.js index 491f046c7..1ed1e262e 100644 --- a/ui/src/admin/containers/AdminPage.js +++ b/ui/src/admin/containers/AdminPage.js @@ -1,12 +1,19 @@ import React, {Component, PropTypes} from 'react' -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; -import {loadUsersAsync, loadRolesAsync} from 'src/admin/actions' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' +import { + loadUsersAsync, + loadRolesAsync, + deleteRoleAsync, + deleteUserAsync, +} from 'src/admin/actions' import AdminTabs from 'src/admin/components/AdminTabs' class AdminPage extends Component { constructor(props) { super(props) + this.handleDeleteRole = ::this.handleDeleteRole + this.handleDeleteUser = ::this.handleDeleteUser } componentDidMount() { @@ -18,6 +25,14 @@ class AdminPage extends Component { } } + handleDeleteRole(role) { + this.props.deleteRole(role, this.props.addFlashMessage) + } + + handleDeleteUser(user) { + this.props.deleteUser(user, this.props.addFlashMessage) + } + render() { const {users, roles, source} = this.props @@ -36,7 +51,17 @@ class AdminPage extends Component {
- {users.length ? : Loading...} + { + users.length ? + : + Loading... + }
@@ -64,6 +89,9 @@ AdminPage.propTypes = { roles: arrayOf(shape()), loadUsers: func, loadRoles: func, + deleteRole: func, + deleteUser: func, + addFlashMessage: func, } const mapStateToProps = ({admin}) => ({ @@ -74,6 +102,8 @@ const mapStateToProps = ({admin}) => ({ const mapDispatchToProps = (dispatch) => ({ loadUsers: bindActionCreators(loadUsersAsync, dispatch), loadRoles: bindActionCreators(loadRolesAsync, dispatch), + deleteRole: bindActionCreators(deleteRoleAsync, dispatch), + deleteUser: bindActionCreators(deleteUserAsync, dispatch), }) -export default connect(mapStateToProps, mapDispatchToProps)(AdminPage); +export default connect(mapStateToProps, mapDispatchToProps)(AdminPage) diff --git a/ui/src/admin/reducers/admin.js b/ui/src/admin/reducers/admin.js index c23ecb6c0..727b27f45 100644 --- a/ui/src/admin/reducers/admin.js +++ b/ui/src/admin/reducers/admin.js @@ -17,6 +17,24 @@ export default function admin(state = initialState, action) { return {...state, ...action.payload} } + case 'DELETE_ROLE': { + const {role} = action.payload + const newState = { + roles: state.roles.filter(r => r.name !== role.name), + } + + return {...state, ...newState} + } + + case 'DELETE_USER': { + const {user} = action.payload + const newState = { + users: state.users.filter(u => u.name !== user.name), + } + + return {...state, ...newState} + } + case 'LOAD_QUERIES': { return {...state, ...action.payload} }
User Roles Permissions