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

initial work on new People admin section. still a WIP but we now have...

initial work on new People admin section.  still a WIP but we now have everything converted over react/redux and most of the core functionality is in place.
parent 48c989ef
No related branches found
No related tags found
No related merge requests found
Showing
with 696 additions and 164 deletions
"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;
};
});
"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
};
"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
}
"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
};
"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
};
"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} />;
}
}
<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>
<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>
'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, {});
}
]);
......@@ -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'
});
}]);
"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);
"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
'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
......@@ -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;
}
......@@ -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',
......
......@@ -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'
......
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