client/app/pages/users/UsersList.jsx (298 lines of code) (raw):

import { isString, map, get, find } from "lodash"; import React from "react"; import PropTypes from "prop-types"; import Button from "antd/lib/button"; import Modal from "antd/lib/modal"; import routeWithUserSession from "@/components/ApplicationArea/routeWithUserSession"; import Link from "@/components/Link"; import Paginator from "@/components/Paginator"; import DynamicComponent from "@/components/DynamicComponent"; import { UserPreviewCard } from "@/components/PreviewCard"; import InputWithCopy from "@/components/InputWithCopy"; import { wrap as itemsList, ControllerType } from "@/components/items-list/ItemsList"; import { ResourceItemsSource } from "@/components/items-list/classes/ItemsSource"; import { UrlStateStorage } from "@/components/items-list/classes/StateStorage"; import LoadingState from "@/components/items-list/components/LoadingState"; import EmptyState from "@/components/items-list/components/EmptyState"; import * as Sidebar from "@/components/items-list/components/Sidebar"; import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTable"; import Layout from "@/components/layouts/ContentWithSidebar"; import wrapSettingsTab from "@/components/SettingsWrapper"; import { currentUser } from "@/services/auth"; import { policy } from "@/services/policy"; import User from "@/services/user"; import navigateTo from "@/components/ApplicationArea/navigateTo"; import notification from "@/services/notification"; import { absoluteUrl } from "@/services/utils"; import routes from "@/services/routes"; import CreateUserDialog from "./components/CreateUserDialog"; function UsersListActions({ user, enableUser, disableUser, deleteUser }) { if (user.id === currentUser.id) { return null; } if (user.is_invitation_pending) { return ( <Button type="danger" className="w-100" onClick={event => deleteUser(event, user)}> Delete </Button> ); } return user.is_disabled ? ( <Button type="primary" className="w-100" onClick={event => enableUser(event, user)}> Enable </Button> ) : ( <Button className="w-100" onClick={event => disableUser(event, user)}> Disable </Button> ); } UsersListActions.propTypes = { user: PropTypes.shape({ id: PropTypes.number, is_invitation_pending: PropTypes.bool, is_disabled: PropTypes.bool, }).isRequired, enableUser: PropTypes.func.isRequired, disableUser: PropTypes.func.isRequired, deleteUser: PropTypes.func.isRequired, }; class UsersList extends React.Component { static propTypes = { controller: ControllerType.isRequired, }; sidebarMenu = [ { key: "active", href: "users", title: "Active Users", }, { key: "pending", href: "users/pending", title: "Pending Invitations", }, { key: "disabled", href: "users/disabled", title: "Disabled Users", isAvailable: () => policy.canCreateUser(), }, ]; listColumns = [ Columns.custom.sortable((text, user) => <UserPreviewCard user={user} withLink />, { title: "Name", field: "name", width: null, }), Columns.custom.sortable( (text, user) => map(user.groups, group => ( <Link key={"group" + group.id} className="label label-tag" href={"groups/" + group.id}> {group.name} </Link> )), { title: "Groups", field: "groups", } ), Columns.timeAgo.sortable({ title: "Joined", field: "created_at", className: "text-nowrap", width: "1%", }), Columns.timeAgo.sortable({ title: "Last Active At", field: "active_at", className: "text-nowrap", width: "1%", }), Columns.custom( (text, user) => ( <UsersListActions user={user} enableUser={this.enableUser} disableUser={this.disableUser} deleteUser={this.deleteUser} /> ), { width: "1%", isAvailable: () => policy.canCreateUser(), } ), ]; componentDidMount() { if (this.props.controller.params.isNewUserPage) { this.showCreateUserDialog(); } } createUser = values => User.create(values) .then(user => { notification.success("Saved."); if (user.invite_link) { Modal.warning({ title: "Email not sent!", content: ( <React.Fragment> <p> The mail server is not configured, please send the following link to <b>{user.name}</b>: </p> <InputWithCopy value={absoluteUrl(user.invite_link)} aria-label="Invite link" readOnly /> </React.Fragment> ), }); } }) .catch(error => { const message = find([get(error, "response.data.message"), get(error, "message"), "Failed saving."], isString); return Promise.reject(new Error(message)); }); showCreateUserDialog = () => { if (policy.isCreateUserEnabled()) { const goToUsersList = () => { if (this.props.controller.params.isNewUserPage) { navigateTo("users"); } }; CreateUserDialog.showModal() .onClose(values => this.createUser(values).then(() => { this.props.controller.update(); goToUsersList(); }) ) .onDismiss(goToUsersList); } }; enableUser = (event, user) => User.enableUser(user).then(() => this.props.controller.update()); disableUser = (event, user) => User.disableUser(user).then(() => this.props.controller.update()); deleteUser = (event, user) => User.deleteUser(user).then(() => this.props.controller.update()); // eslint-disable-next-line class-methods-use-this renderPageHeader() { if (!policy.canCreateUser()) { return null; } return ( <div className="m-b-15"> <Button type="primary" disabled={!policy.isCreateUserEnabled()} onClick={this.showCreateUserDialog}> <i className="fa fa-plus m-r-5" aria-hidden="true" /> New User </Button> <DynamicComponent name="UsersListExtra" /> </div> ); } render() { const { controller } = this.props; return ( <React.Fragment> {this.renderPageHeader()} <Layout> <Layout.Sidebar className="m-b-0"> <Sidebar.SearchInput value={controller.searchTerm} onChange={controller.updateSearch} label="Search users" /> <Sidebar.Menu items={this.sidebarMenu} selected={controller.params.currentPage} /> </Layout.Sidebar> <Layout.Content> {!controller.isLoaded && <LoadingState className="" />} {controller.isLoaded && controller.isEmpty && <EmptyState className="" />} {controller.isLoaded && !controller.isEmpty && ( <div className="table-responsive" data-test="UserList"> <ItemsTable items={controller.pageItems} columns={this.listColumns} context={this.actions} orderByField={controller.orderByField} orderByReverse={controller.orderByReverse} toggleSorting={controller.toggleSorting} /> <Paginator showPageSizeSelect totalCount={controller.totalItemsCount} pageSize={controller.itemsPerPage} onPageSizeChange={itemsPerPage => controller.updatePagination({ itemsPerPage })} page={controller.page} onChange={page => controller.updatePagination({ page })} /> </div> )} </Layout.Content> </Layout> </React.Fragment> ); } } const UsersListPage = wrapSettingsTab( "Users.List", { permission: "list_users", title: "Users", path: "users", isActive: path => path.startsWith("/users") && path !== "/users/me", order: 2, }, itemsList( UsersList, () => new ResourceItemsSource({ getRequest(request, { params: { currentPage } }) { switch (currentPage) { case "active": request.pending = false; break; case "pending": request.pending = true; break; case "disabled": request.disabled = true; break; // no default } return request; }, getResource() { return User.query.bind(User); }, }), () => new UrlStateStorage({ orderByField: "created_at", orderByReverse: true }) ) ); routes.register( "Users.New", routeWithUserSession({ path: "/users/new", title: "Users", render: pageProps => <UsersListPage {...pageProps} currentPage="active" isNewUserPage />, }) ); routes.register( "Users.List", routeWithUserSession({ path: "/users", title: "Users", render: pageProps => <UsersListPage {...pageProps} currentPage="active" />, }) ); routes.register( "Users.Pending", routeWithUserSession({ path: "/users/pending", title: "Pending Invitations", render: pageProps => <UsersListPage {...pageProps} currentPage="pending" />, }) ); routes.register( "Users.Disabled", routeWithUserSession({ path: "/users/disabled", title: "Disabled Users", render: pageProps => <UsersListPage {...pageProps} currentPage="disabled" />, }) );