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

Merge branch 'master' into sample_dataset_loading

parents 0fd0d25a e905e546
No related branches found
No related tags found
No related merge requests found
Showing
with 12056 additions and 10077 deletions
This diff is collapsed.
......@@ -29,6 +29,7 @@
"jquery": "^2.1.4",
"moment": "^2.10.6",
"normalizr": "^0.1.3",
"password-generator": "^2.0.1",
"react": "^0.13.3",
"react-grid-layout": "^0.8.5",
"react-onclickout": "^1.1.0",
......
"use strict";
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 SessionApi = new AngularResourceProxy("Session", ["forgot_password"]);
const UserApi = new AngularResourceProxy("User", ["list", "update", "create", "delete", "update_password", "send_invite"]);
// 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 RESEND_INVITE = 'RESEND_INVITE';
export const RESET_PASSWORD_EMAIL = 'RESET_PASSWORD_EMAIL';
export const RESET_PASSWORD_MANUAL = 'RESET_PASSWORD_MANUAL';
export const REVOKE_ADMIN = 'REVOKE_ADMIN';
export const SHOW_MODAL = 'SHOW_MODAL';
export const UPDATE_USER = 'UPDATE_USER';
// action creators
export const showModal = createAction(SHOW_MODAL);
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.date_joined = (newUser.date_joined) ? moment(newUser.date_joined) : null;
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) {
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.date_joined = (u.date_joined) ? moment(u.date_joined) : null;
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.date_joined = (updatedUser.date_joined) ? moment(updatedUser.date_joined) : null;
updatedUser.last_login = (updatedUser.last_login) ? moment(updatedUser.last_login) : null;
return updatedUser;
};
});
export const resendInvite = createThunkAction(RESEND_INVITE, function(user) {
return async function(dispatch, getState) {
return await UserApi.send_invite({id: user.id});
};
});
export const resetPasswordManually = createThunkAction(RESET_PASSWORD_MANUAL, function(user, password) {
return async function(dispatch, getState) {
return await UserApi.update_password({id: user.id, password: password});
};
});
export const resetPasswordViaEmail = createThunkAction(RESET_PASSWORD_EMAIL, function(user) {
return async function(dispatch, getState) {
return await SessionApi.forgot_password({email: user.email});
};
});
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.date_joined = (updatedUser.date_joined) ? moment(updatedUser.date_joined) : null;
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.date_joined = (updatedUser.date_joined) ? moment(updatedUser.date_joined) : null;
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 MetabaseSettings from "metabase/lib/settings";
import MetabaseUtils from "metabase/lib/utils";
import Modal from "metabase/components/Modal.react";
import ModalContent from "metabase/components/ModalContent.react";
import PasswordReveal from "metabase/components/PasswordReveal.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,
deleteUser,
fetchUsers,
grantAdmin,
resetPasswordManually,
resetPasswordViaEmail,
revokeAdmin,
showModal,
updateUser } from "../actions";
export const MODAL_ADD_PERSON = 'MODAL_ADD_PERSON';
export const MODAL_EDIT_DETAILS = 'MODAL_EDIT_DETAILS';
export const MODAL_INVITE_RESENT = 'MODAL_INVITE_RESENT';
export const MODAL_REMOVE_USER = 'MODAL_REMOVE_USER';
export const MODAL_RESET_PASSWORD = 'MODAL_RESET_PASSWORD';
export const MODAL_RESET_PASSWORD_MANUAL = 'MODAL_RESET_PASSWORD_MANUAL';
export const MODAL_RESET_PASSWORD_EMAIL = 'MODAL_RESET_PASSWORD_EMAIL';
export const MODAL_USER_ADDED_WITH_INVITE = 'MODAL_USER_ADDED_WITH_INVITE';
export const MODAL_USER_ADDED_WITH_PASSWORD = 'MODAL_USER_ADDED_WITH_PASSWORD';
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) {
// check that this isn't the last admin in the system
let admins = _.pick(this.props.users, function(value, key, object) {
return value.is_superuser;
});
if (admins && _.keys(admins).length > 1) {
this.props.dispatch(revokeAdmin(user));
}
} else if (roleDef.id === "admin" && !user.is_superuser) {
this.props.dispatch(grantAdmin(user));
}
}
async onAddPerson(user) {
// close the modal no matter what
this.props.dispatch(showModal(null));
if (user) {
let modal = MODAL_USER_ADDED_WITH_INVITE;
// we assume invite style creation and tweak as needed if email not available
if (!MetabaseSettings.isEmailConfigured()) {
modal = MODAL_USER_ADDED_WITH_PASSWORD;
user.password = MetabaseUtils.generatePassword();
}
// create the user
this.props.dispatch(createUser(user));
// carry on
this.props.dispatch(showModal({
type: modal,
details: {
user: user
}
}));
}
}
onEditDetails(user) {
// close the modal no matter what
this.props.dispatch(showModal(null));
if (user) {
this.props.dispatch(updateUser(user));
}
}
onPasswordResetConfirm(user) {
if (MetabaseSettings.isEmailConfigured()) {
// trigger password reset email
this.props.dispatch(resetPasswordViaEmail(user));
// show confirmation modal
this.props.dispatch(showModal({
type: MODAL_RESET_PASSWORD_EMAIL,
details: {user: user}
}));
} else {
// generate a password
const password = MetabaseUtils.generatePassword(14, MetabaseSettings.get('password_complexity'));
// trigger the reset
this.props.dispatch(resetPasswordManually(user, password));
// show confirmation modal
this.props.dispatch(showModal({
type: MODAL_RESET_PASSWORD_MANUAL,
details: {password: password, user: user}
}));
}
}
onRemoveUserConfirm(user) {
this.props.dispatch(showModal(null));
this.props.dispatch(deleteUser(user));
}
renderAddPersonModal(modalDetails) {
return (
<Modal>
<ModalContent title="Add Person"
closeFn={() => this.props.dispatch(showModal(null))}>
<EditUserForm
buttonText="Add Person"
submitFn={this.onAddPerson.bind(this)} />
</ModalContent>
</Modal>
);
}
renderEditDetailsModal(modalDetails) {
let { user } = modalDetails;
return (
<Modal>
<ModalContent title="Edit Details"
closeFn={() => this.props.dispatch(showModal(null))}>
<EditUserForm
user={user}
submitFn={this.onEditDetails.bind(this)} />
</ModalContent>
</Modal>
);
}
renderUserAddedWithPasswordModal(modalDetails) {
let { user } = modalDetails;
return (
<Modal className="Modal Modal--small">
<ModalContent title={user.first_name+" has been added"}
closeFn={() => this.props.dispatch(showModal(null))}
className="Modal-content Modal-content--small NewForm">
<div>
<div className="px4 pb4">
<div className="pb4">We couldnt send them an email invitation,
so make sure to tell them to log in using <span className="text-bold">{user.email} </span>
and this password weve generated for them:</div>
<PasswordReveal password={user.password} />
<div style={{paddingLeft: "5em", paddingRight: "5em"}} className="pt4 text-centered">If you want to be able to send email invites, just go to the <a className="link text-bold" href="/admin/settings/?section=Email">Email Settings</a> page.</div>
</div>
<div className="Form-actions">
<button className="Button Button--primary" onClick={() => this.props.dispatch(showModal(null))}>Done</button>
<span className="pl1">or<a className="link ml1 text-bold" href="" onClick={() => this.props.dispatch(showModal({type: MODAL_ADD_PERSON}))}>Add another person</a></span>
</div>
</div>
</ModalContent>
</Modal>
);
}
renderUserAddedWithInviteModal(modalDetails) {
let { user } = modalDetails;
return (
<Modal className="Modal Modal--small">
<ModalContent title={user.first_name+" has been added"}
closeFn={() => this.props.dispatch(showModal(null))}
className="Modal-content Modal-content--small NewForm">
<div>
<div style={{paddingLeft: "5em", paddingRight: "5em"}} className="pb4">Weve sent an invite to <span className="text-bold">{user.email}</span> with instructions to set their password.</div>
<div className="Form-actions">
<button className="Button Button--primary" onClick={() => this.props.dispatch(showModal(null))}>Done</button>
<span className="pl1">or<a className="link ml1 text-bold" href="" onClick={() => this.props.dispatch(showModal({type: MODAL_ADD_PERSON}))}>Add another person</a></span>
</div>
</div>
</ModalContent>
</Modal>
);
}
renderInviteResentModal(modalDetails) {
let { user } = modalDetails;
return (
<Modal className="Modal Modal--small">
<ModalContent title={"We've Re-sent "+user.first_name+"'s Invite"}
closeFn={() => this.props.dispatch(showModal(null))}
className="Modal-content Modal-content--small NewForm">
<div>
<div className="px4 pb4">Any previous email invites they have will no longer work.</div>
<div className="Form-actions">
<button className="Button Button--primary mr2" onClick={() => this.props.dispatch(showModal(null))}>Okay</button>
</div>
</div>
</ModalContent>
</Modal>
);
}
renderRemoveUserModal(modalDetails) {
let { user } = modalDetails;
return (
<Modal className="Modal Modal--small">
<ModalContent title={"Remove "+user.common_name}
closeFn={() => this.props.dispatch(showModal(null))}
className="Modal-content Modal-content--small NewForm">
<div>
<div className="px4 pb4">
Are you sure you want to do this? {user.first_name} won't be able to log in anymore. This can't be undone.
</div>
<div className="Form-actions">
<button className="Button Button--warning" onClick={() => this.onRemoveUserConfirm(user)}>Yes</button>
<button className="Button Button--primary ml2" onClick={() => this.props.dispatch(showModal(null))}>No</button>
</div>
</div>
</ModalContent>
</Modal>
);
}
renderResetPasswordModal(modalDetails) {
let { user } = modalDetails;
return (
<Modal className="Modal Modal--small">
<ModalContent title={"Reset "+user.first_name+"'s Password"}
closeFn={() => this.props.dispatch(showModal(null))}
className="Modal-content Modal-content--small NewForm">
<div>
<div className="px4 pb4">
Are you sure you want to do this?
</div>
<div className="Form-actions">
<button className="Button Button--warning" onClick={() => this.onPasswordResetConfirm(user)}>Yes</button>
<button className="Button Button--primary ml2" onClick={() => this.props.dispatch(showModal(null))}>No</button>
</div>
</div>
</ModalContent>
</Modal>
);
}
renderPasswordResetManuallyModal(modalDetails) {
let { user, password } = modalDetails;
return (
<Modal className="Modal Modal--small">
<ModalContent title={user.first_name+"'s Password Has Been Reset"}
closeFn={() => this.props.dispatch(showModal(null))}
className="Modal-content Modal-content--small NewForm">
<div>
<div className="px4 pb4">
<span className="pb3 block">Heres a temporary password they can use to log in and then change their password.</span>
<PasswordReveal password={password} />
</div>
<div className="Form-actions">
<button className="Button Button--primary mr2" onClick={() => this.props.dispatch(showModal(null))}>Done</button>
</div>
</div>
</ModalContent>
</Modal>
);
}
renderPasswordResetViaEmailModal(modalDetails) {
let { user } = modalDetails;
return (
<Modal className="Modal Modal--small">
<ModalContent title={user.first_name+"'s Password Has Been Reset"}
closeFn={() => this.props.dispatch(showModal(null))}
className="Modal-content Modal-content--small NewForm">
<div>
<div className="px4 pb4">We've sent them an email with instructions for creating a new password.</div>
<div className="Form-actions">
<button className="Button Button--primary mr2" onClick={() => this.props.dispatch(showModal(null))}>Done</button>
</div>
</div>
</ModalContent>
</Modal>
);
}
renderModal(modalType, modalDetails) {
switch(modalType) {
case MODAL_ADD_PERSON: return this.renderAddPersonModal(modalDetails);
case MODAL_EDIT_DETAILS: return this.renderEditDetailsModal(modalDetails);
case MODAL_USER_ADDED_WITH_PASSWORD: return this.renderUserAddedWithPasswordModal(modalDetails);
case MODAL_USER_ADDED_WITH_INVITE: return this.renderUserAddedWithInviteModal(modalDetails);
case MODAL_INVITE_RESENT: return this.renderInviteResentModal(modalDetails);
case MODAL_REMOVE_USER: return this.renderRemoveUserModal(modalDetails);
case MODAL_RESET_PASSWORD: return this.renderResetPasswordModal(modalDetails);
case MODAL_RESET_PASSWORD_MANUAL: return this.renderPasswordResetManuallyModal(modalDetails);
case MODAL_RESET_PASSWORD_EMAIL: return this.renderPasswordResetViaEmailModal(modalDetails);
}
return null;
}
render() {
let { modal, users } = this.props;
let { error } = this.state;
users = _.values(users).sort((a, b) => (b.date_joined - a.date_joined));
return (
<LoadingAndErrorWrapper loading={!users} error={error}>
{() =>
<div className="wrapper">
{ modal ? this.renderModal(modal.type, modal.details) : null }
<section className="PageHeader clearfix px2">
<a className="Button Button--primary float-right" href="#" onClick={() => this.props.dispatch(showModal({type: MODAL_ADD_PERSON}))}>Add person</a>
<h2 className="PageTitle">People</h2>
</section>
<section className="pb4">
<table className="ContentTable">
<thead style={{borderTop: "none"}}>
<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 ? user.last_login.fromNow() : "Never" }</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 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 onSubmit={this.formSubmitted.bind(this)} noValidate>
<div className="px4 pb2">
<FormField fieldName="first_name" formError={formError}>
<FormLabel title="First name" fieldName="first_name" formError={formError}></FormLabel>
<input ref="firstName" className="Form-input full" name="name" defaultValue={(user) ? user.first_name : null} placeholder="Johnny" onChange={this.onChange.bind(this)} />
</FormField>
<FormField fieldName="last_name" formError={formError}>
<FormLabel title="Last name" fieldName="last_name" formError={formError}></FormLabel>
<input ref="lastName" className="Form-input full" name="name" defaultValue={(user) ? user.last_name : null} placeholder="Appleseed" required onChange={this.onChange.bind(this)} />
</FormField>
<FormField fieldName="email" formError={formError}>
<FormLabel title="Email address" fieldName="email" formError={formError}></FormLabel>
<input ref="email" className="Form-input full" name="email" defaultValue={(user) ? user.email : null} placeholder="youlooknicetoday@email.com" required onChange={this.onChange.bind(this)} />
</FormField>
</div>
<div className="Form-actions">
<button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
{ buttonText ? buttonText : "Save Changes" }
</button>
<span className="pl1">or<a className="link ml1 text-bold" href="" onClick={this.cancel.bind(this)}>Cancel</a></span>
</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 MetabaseSettings from "metabase/lib/settings";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
import { MODAL_EDIT_DETAILS,
MODAL_INVITE_RESENT,
MODAL_REMOVE_USER,
MODAL_RESET_PASSWORD } from "./AdminPeople.react";
import { resendInvite, showModal } from "../actions";
export default class UserActionsSelect extends Component {
onEditDetails() {
this.props.dispatch(showModal({type: MODAL_EDIT_DETAILS, details: {user: this.props.user}}));
this.refs.popover.toggle();
}
onResendInvite() {
this.props.dispatch(resendInvite(this.props.user));
this.props.dispatch(showModal({type: MODAL_INVITE_RESENT, details: {user: this.props.user}}));
this.refs.popover.toggle();
}
onResetPassword() {
this.props.dispatch(showModal({type: MODAL_RESET_PASSWORD, details: {user: this.props.user}}));
this.refs.popover.toggle();
}
onRemoveUser() {
this.props.dispatch(showModal({type: MODAL_REMOVE_USER, details: {user: this.props.user}}));
this.refs.popover.toggle();
}
render() {
let { user } = this.props;
const 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={<span className="text-grey-1"><Icon name={'ellipsis'}></Icon></span>}>
<ul className="UserActionsSelect">
<li className="py1 px2 bg-brand-hover text-white-hover" onClick={this.onEditDetails.bind(this)}>Edit Details</li>
{ (user.last_login === null && MetabaseSettings.isEmailConfigured()) ?
<li className="py1 px2 bg-brand-hover text-white-hover" onClick={this.onResendInvite.bind(this)}>Re-send Invite</li>
:
<li className="py1 px2 bg-brand-hover text-white-hover" onClick={this.onResetPassword.bind(this)}>Reset Password</li>
}
<li className="mt1 p2 border-top bg-error-hover text-error text-white-hover" 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];
const triggerElement = (
<div className={"flex align-center"}>
<span className="mr1">{roleDef.name}</span>
<Icon className="text-grey-2" name="chevrondown" width="10" height="10"/>
</div>
);
let sections = {};
MetabaseCore.user_roles.forEach(function (option) {
let sectionName = option.section || "";
sections[sectionName] = sections[sectionName] || { title: sectionName || undefined, items: [] };
sections[sectionName].items.push(option);
});
sections = Object.keys(sections).map((sectionName) => sections[sectionName]);
const columns = [
{
selectedItem: roleDef,
sections: sections,
itemTitleFn: (item) => item.name,
itemDescriptionFn: (item) => item.description,
itemSelectFn: (item) => {
onChangeFn(user, item);
this.toggle();
}
}
];
const 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_MODAL,
UPDATE_USER
} from './actions';
export const modal = handleActions({
[SHOW_MODAL]: { next: (state, { payload }) => payload }
}, null);
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.modal, state => state.users],
(modal, users) => ({modal, users})
);
\ No newline at end of file
......@@ -10,6 +10,7 @@ import cx from 'classnames';
export default React.createClass({
displayName: "SettingsEditor",
propTypes: {
initialSection: React.string,
sections: React.PropTypes.object.isRequired,
updateSetting: React.PropTypes.func.isRequired
},
......@@ -20,6 +21,14 @@ export default React.createClass({
};
},
componentWillMount: function() {
if (this.props.initialSection) {
this.setState({
currentSection: this.props.initialSection
});
}
},
selectSection: function(section) {
this.setState({ currentSection: section });
},
......
......@@ -36,25 +36,31 @@ var EXTRA_SETTINGS_METADATA = {
"email-from-address": { display_name: "From Address", section: "Email", index: 5, type: "string" },
};
SettingsAdminControllers.controller('SettingsEditor', ['$scope', 'Settings', 'AppState', 'settings', function($scope, Settings, AppState, settings) {
$scope.SettingsEditor = SettingsEditor;
SettingsAdminControllers.controller('SettingsEditor', ['$scope', '$location', 'Settings', 'AppState', 'settings',
function($scope, $location, Settings, AppState, settings) {
$scope.SettingsEditor = SettingsEditor;
$scope.updateSetting = async function(setting) {
await Settings.put({ key: setting.key }, setting).$promise;
AppState.refreshSiteSettings();
}
if ('section' in $location.search()) {
$scope.initialSection = $location.search().section;
}
$scope.updateSetting = async function(setting) {
await Settings.put({ key: setting.key }, setting).$promise;
AppState.refreshSiteSettings();
}
$scope.sections = {};
settings.forEach(function(setting) {
var defaults = { display_name: keyToDisplayName(setting.key), placeholder: setting.default };
setting = _.extend(defaults, EXTRA_SETTINGS_METADATA[setting.key], setting);
var sectionName = setting.section || "Other";
$scope.sections[sectionName] = $scope.sections[sectionName] || [];
$scope.sections[sectionName].push(setting);
});
_.each($scope.sections, (section) => section.sort((a, b) => a.index - b.index))
$scope.sections = {};
settings.forEach(function(setting) {
var defaults = { display_name: keyToDisplayName(setting.key), placeholder: setting.default };
setting = _.extend(defaults, EXTRA_SETTINGS_METADATA[setting.key], setting);
var sectionName = setting.section || "Other";
$scope.sections[sectionName] = $scope.sections[sectionName] || [];
$scope.sections[sectionName].push(setting);
});
_.each($scope.sections, (section) => section.sort((a, b) => a.index - b.index))
function keyToDisplayName(key) {
return Humanize.capitalizeAll(key.replace(/-/g, " ")).trim();
function keyToDisplayName(key) {
return Humanize.capitalizeAll(key.replace(/-/g, " ")).trim();
}
}
}]);
]);
......@@ -7,7 +7,7 @@ import Icon from "metabase/components/Icon.react";
export default class ModalContent extends Component {
render() {
return (
<div className="Modal-content NewForm">
<div className={this.props.className}>
<div className="Modal-header Form-header flex align-center">
<h2 className="flex-full">{this.props.title}</h2>
<a href="#" className="text-grey-3 p1" onClick={this.props.closeFn}>
......@@ -22,6 +22,10 @@ export default class ModalContent extends Component {
}
}
ModalContent.defaultProps = {
className: "Modal-content NewForm"
};
ModalContent.propTypes = {
title: PropTypes.string.isRequired,
closeFn: PropTypes.func.isRequired
......
"use strict";
import React, { Component, PropTypes } from "react";
export default class PasswordReveal extends Component {
constructor(props) {
super(props);
this.state = { visible: false };
this.styles = {
container: {
borderWidth: "2px"
},
input: {
fontSize: '1.2rem',
letterSpacing: '2',
color: '#676C72'
},
label: {
top: "-12px"
}
}
}
onToggleVisibility() {
this.setState({
visible: !this.state.visible
});
}
render() {
const { password } = this.props;
const { visible } = this.state;
return (
<div style={this.styles.container} className="bordered rounded p3 relative">
<div style={this.styles.label} className="absolute text-centered left right">
<span className="px1 bg-white h6 text-bold text-grey-3 text-uppercase">Temporary Password</span>
</div>
{ visible ?
<span style={this.styles.input} className="text-grey-2 text-normal mr3">{password}</span>
:
<span style={this.styles.input} className="mr3">&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;</span>
}
{ visible ?
<a href="#" className="link text-bold" onClick={this.onToggleVisibility.bind(this)}>Hide</a>
:
<a href="#" className="link text-bold" onClick={this.onToggleVisibility.bind(this)}>Show</a>
}
</div>
);
}
}
PasswordReveal.propTypes = {
password: PropTypes.string.isRequired
}
'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
......@@ -5,6 +5,8 @@
--primary-button-border-color: #509EE3;
--primary-button-bg-color: #509EE3;
--warning-button-border-color: #E35050;
--warning-button-bg-color: #E35050;
--selected-button-bg-color: #F4F6F8;
--danger-button-bg-color: #EF8C8C;
......@@ -68,6 +70,18 @@
background-color: color(var(--primary-button-bg-color) shade(10%));
}
.Button--warning {
color: #fff;
background: var(--warning-button-bg-color);
border: 1px solid var(--warning-button-border-color);
}
.Button--warning:hover {
color: #fff;
border-color: color(var(--warning-button-border-color) shade(10%));
background-color: color(var(--warning-button-bg-color) shade(10%));
}
.Button--cancel {
border-radius: 99px;
}
......
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