application/components/admin/group.js
import _ from 'lodash';
import React, {Component} from 'react';
import Immutable from 'immutable';
import generatePassword from 'password-generator';
import {List, ListItem} from 'material-ui/List';
import Subheader from 'material-ui/Subheader';
import Avatar from 'material-ui/Avatar';
import Divider from 'material-ui/Divider';
import Checkbox from 'material-ui/Checkbox';
import LinearProgress from 'material-ui/LinearProgress';
import PersonIcon from 'material-ui/svg-icons/social/person-outline';
import ReloadIcon from 'material-ui/svg-icons/action/autorenew';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import RaisedButton from 'material-ui/RaisedButton';
import IconButton from 'material-ui/IconButton';
import TextField from 'material-ui/TextField';
import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table';
import {Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle} from 'material-ui/Toolbar';
import {connect} from 'react-redux';
import ExpandingCard from './../expanding-card';
import ReduxPaginator from './../common/redux-paginator';
import DeleteButton from './../common/delete-button';
import CurrentUser from './../../current-user';
import DescriptiveAcls from './../../data/descriptive-acls';
import {
aclsMaybeFetch,
aclsFetch,
aclsFetchOne,
aclSetSelected,
aclsFetchResources,
aclUpdate,
aclsRemove
} from './../../redux/actions/acl-actions';
import {
snackbarShowMessage
} from './../../redux/actions/snackbar-actions';
import {Row, Col} from './../flexbox';
class GroupCard extends Component {
constructor(props) {
super(props);
this.state = {
hasMore: false,
hasLoaded: false,
modalOpen: false,
selected: []
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.openCard == 'group' && nextProps.openCard != this.props.openCard) {
this.handleToggle(true);
}
}
handleToggle(expanded) {
if (this.props.onToggle) {
this.props.onToggle('group', expanded);
}
if (expanded && !this.state.hasLoaded) {
const {dispatch} = this.props;
dispatch(aclsMaybeFetch())
.then(() => this.setState({hasLoaded: true}));
}
}
handleAclClick(acl, e) {
const {dispatch} = this.props;
dispatch(aclsFetchOne(acl.get('name')))
.then(() => dispatch(aclsFetchResources()))
.then(() => dispatch(aclSetSelected(acl.get('name'))))
.then(() => {
this.setState({
'modalOpen': true
});
});
}
handleNewAcl(e) {
const {dispatch} = this.props;
dispatch(aclsFetchResources())
.then(() => dispatch(aclSetSelected(false)))
.then(() => this.setState({'modalOpen': true}));
}
handleModalClose(e) {
this.setState({'modalOpen': false, 'selected': [], 'selectedItems': []});
this.tableBody.setState({selectedRows: []});
}
handleRowSelection(selection) {
let selectedItems = [];
if (selection == 'all') {
selectedItems = this.props.acls.items.map((item) => item.get('name'));
} else {
const items = this.props.acls.items.toJS();
selectedItems = selection.map((i) =>items[i].name);
}
this.setState({
'selectedItems': selectedItems,
'selected': selection
});
}
handlePagination(page) {
if (this.props.acls.pagination.current == page) {
return;
}
const {dispatch} = this.props;
dispatch(aclsFetch({page: page}));
}
handleDelete(next) {
const names = this.state.selectedItems.join(',');
const {dispatch} = this.props;
dispatch(aclsRemove(names))
.then(() => dispatch(aclsFetch()))
.then(() => this.tableBody.setState({selectedRows: []}))
.then(() => next());
return true;
}
render() {
let items = this.props.acls.items.map((item, i) => {
return (
<TableRow
key={i}
selected={this.state.selected == 'all' || this.state.selected.indexOf(i) != -1 ? true : false}
selectable={item.get('name') != 'Administrator' ? true : false}
>
<TableRowColumn>{item.get('name') != 'Administrator' ? (<a onClick={this.handleAclClick.bind(this, item)}>{item.get('name')}</a>) : item.get('name')}</TableRowColumn>
<TableRowColumn></TableRowColumn>
</TableRow>
);
});
return (
<ExpandingCard
onToggle={this.handleToggle.bind(this)}
title="Groups"
subtitle="Manage group and ACL permissions"
expanded={this.props.openCard === 'group'}
>
<Row middle='xs'>
<Col start='xs'>
<DeleteButton
disabled={this.state.selectedItems && this.state.selectedItems.length ? false : true}
onDelete={this.handleDelete.bind(this)}
/>
<RaisedButton
primary={true}
label="Add Group"
onClick={this.handleNewAcl.bind(this)}
/>
</Col>
</Row>
<Table
fixedHeader={true}
selectable={true}
multiSelectable={true}
onRowSelection={this.handleRowSelection.bind(this)}
>
<TableHeader>
<TableRow>
<TableHeaderColumn>Name</TableHeaderColumn>
<TableHeaderColumn></TableHeaderColumn>
</TableRow>
</TableHeader>
<TableBody
ref={tableBody => this.tableBody = tableBody}
deselectOnClickaway={false}
>
{items}
</TableBody>
</Table>
{
this.state.modalOpen
? <AclModal onClose={this.handleModalClose.bind(this)} open={this.state.modalOpen} />
: ''
}
</ExpandingCard>
);
}
}
let mapStateToProps = (state) => {
return {
acls: state.acls
};
}
export default connect(mapStateToProps)(GroupCard);
class UnwrappedAclModal extends Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
acl: Immutable.fromJS({
'name': false,
'description': false,
'accesses': {}
}),
descriptiveAcls: Immutable.fromJS(DescriptiveAcls)
};
}
componentWillMount() {
const acl = this.props.acls.items.find((item) => {
return item.get('name') == this.props.acls.selected;
});
if (acl) {
this.setState({'acl': acl, 'loaded': true});
} else {
this.setState({'acl': Immutable.fromJS({
'name': false,
'description': false,
'accesses': {},
}), 'loaded': true});
}
}
handleClose(e) {
this.props.onClose(e);
}
handleSave(e) {
let acl = this.state.acl;
const {dispatch} = this.props;
let data = {
'name': acl.get('name'),
'description': acl.get('description'),
'accesses': acl.get('accesses').toJS()
};
dispatch(aclUpdate(data))
.then(() => dispatch(aclsFetch()))
.then(() => dispatch(snackbarShowMessage('Group updated')))
.then(() => {
this.props.onClose(e);
});
}
handleFieldUpdate(e, val) {
const prop = e.target.name;
let acl = this.state.acl;
this.setState({'acl': acl.set(prop, val)});
}
handleCheck(e, checked) {
let currentAcls = this.state.acl.get('accesses');
let groupLabel = e.target.name.substr(0, e.target.name.indexOf(' :: '));
let actionLabel = e.target.name.substr(e.target.name.indexOf(' :: ')+4);
// first find the acls for the targeted action
const allAcls = this.state.descriptiveAcls;
const group = allAcls.find((item) => item.get('label') == groupLabel);
const action = group.get('actions').find((item) => item.get('label') == actionLabel);
// if it's empty, set the default list
if (!currentAcls || !currentAcls.size) {
currentAcls = allAcls.find((item) => item.get('label') == 'defaultSet').get('acls');
}
// convert to a javascript object to make this easier to work with
currentAcls = currentAcls.toJS();
if (checked) {
// first grab the ACLs for the new items
const newAcls = action.get('acls').toJS();
for (let i in newAcls) {
let newAclGroup = newAcls[i];
if (!_.has(currentAcls, newAclGroup.group)) {
currentAcls[newAclGroup.group] = newAclGroup.actions;
continue;
}
for (let j in newAclGroup.actions) {
let newAction = newAclGroup.actions[j];
currentAcls[newAclGroup.group].push(newAction);
}
currentAcls[newAclGroup.group] = _.uniq(currentAcls[newAclGroup.group]);
}
} else {
// sorta the opposite.
const removeAcls = action.get('acls').toJS();
for (let i in removeAcls) {
let removeAclGroup = removeAcls[i];
// doesn't exist, which is odd...
if (!_.has(currentAcls, removeAclGroup.group)) {
currentAcls[removeAclGroup.group] = removeAclGroup.actions;
continue;
}
for (let j in removeAclGroup.actions) {
let existingAction = removeAclGroup.actions[j];
for (let k in currentAcls[removeAclGroup.group]) {
console.log(currentAcls[removeAclGroup.group][k].action, existingAction.action);
if (currentAcls[removeAclGroup.group][k].action == existingAction.action) {
currentAcls[removeAclGroup.group][k].allowed = "0";
}
}
}
currentAcls[removeAclGroup.group] = _.uniq(currentAcls[removeAclGroup.group]);
}
}
this.setState({'acl': this.state.acl.set('accesses', Immutable.fromJS(currentAcls))});
return;
}
render() {
const actions = [
<FlatButton
label="Cancel"
primary={false}
onClick={this.handleClose.bind(this)}
/>,
<FlatButton
label="Save"
primary={true}
keyboardFocused={true}
onClick={this.handleSave.bind(this)}
/>
];
let resources = [];
const accesses = this.state.acl.get('accesses').toJS();
const allAcls = this.state.descriptiveAcls.toJS();
// seems more complicated than it is.
// looping through the descriptive acls.
for (let i in allAcls) {
let accessGroup = allAcls[i];
// skip the default set
if (accessGroup.label == 'defaultSet') {
continue;
}
let options = [];
for (let j in accessGroup.actions) {
let accessAction = accessGroup.actions[j];
// assume it's unchecked
let isChecked = false;
for (let k in accessAction.acls) {
let aclSet = accessAction.acls[k];
const currentAcls = accesses[aclSet.group];
if (!currentAcls) {
continue;
}
const total = aclSet.actions.length;
let count = 0;
_.find(currentAcls, (acl) => {
if (acl.action == '*' && acl.allowed == "1") {
count = total;
return true;
}
for (let i in aclSet.actions) {
if (acl.action == aclSet.actions[i].action
&& acl.allowed == aclSet.actions[i].allowed) {
count++;
}
}
});
if (count == total) {
isChecked = true;
}
}
options.push(
<Checkbox label={accessAction.label} name={accessGroup.label + ' :: ' + accessAction.label} defaultChecked={isChecked} onCheck={this.handleCheck.bind(this)} />
);
}
resources.push(
<div>
<h4>{accessGroup.label}</h4>
{options}
</div>
);
}
return (
<Dialog
title="Edit Group"
actions={actions}
modal={false}
open={this.props.open}
onRequestClose={this.handleClose.bind(this)}
autoScrollBodyContent={true}
>
<TextField
floatingLabelText="Name"
value={this.state.acl.get('name') ? this.state.acl.get('name') : ''}
name='name'
disabled={this.props.acls.selected ? true : false}
fullWidth={true}
onChange={this.handleFieldUpdate.bind(this)}
/>
<TextField
floatingLabelText="Description"
value={this.state.acl.get('description') ? this.state.acl.get('description') : ''}
name='description'
multiline={true}
fullWidth={true}
onChange={this.handleFieldUpdate.bind(this)}
/>
<Divider />
<h3>Resources</h3>
{resources}
<p className='align-center small'>
If your publication requires more granular access controls, please <a href="mailto:support@getsnworks.com">contact support</a>.
</p>
</Dialog>
);
}
}
const AclModal = connect(mapStateToProps)(UnwrappedAclModal);