UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

590 lines (551 loc) 21.8 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) Passbolt SARL (https://www.passbolt.com) * * Licensed under GNU Affero General Public License version 3 of the or any later version. * For full copyright and license information, please see the LICENSE.txt * Redistributions of files must retain the above copyright notice. * * @copyright Copyright (c) Passbolt SARL (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 2.11.0 */ import PropTypes from "prop-types"; import React from "react"; import ReactList from "react-list"; import {withAppContext} from "../../../contexts/AppContext"; import Icon from "../../../../shared/components/Icons/Icon"; import {withActionFeedback} from "../../../contexts/ActionFeedbackContext"; import {withRouter} from "react-router-dom"; import {withContextualMenu} from "../../../contexts/ContextualMenuContext"; import {UserWorkspaceFilterTypes, withUserWorkspace} from "../../../contexts/UserWorkspaceContext"; import DisplayUsersContextualMenu from "../DisplayUsersContextualMenu/DisplayUsersContextualMenu"; import {Trans, withTranslation} from "react-i18next"; import {DateTime} from "luxon"; import {withAccountRecovery} from "../../../contexts/AccountRecoveryUserContext"; /** * This component allows to display the filtered users into a grid */ class DisplayUsers extends React.Component { /** * Default constructor * @param props Component props */ constructor(props) { super(props); this.state = this.getDefaultState(); this.initEventHandlers(); this.createRefs(); } /** * Returns the component default state */ getDefaultState() { return {}; } /** * Initialize the component event handlers */ initEventHandlers() { this.handleUserSelected = this.handleUserSelected.bind(this); this.handleUserRightClick = this.handleUserRightClick.bind(this); this.handleCheckboxWrapperClick = this.handleCheckboxWrapperClick.bind(this); this.handleSortByColumnClick = this.handleSortByColumnClick.bind(this); } /** * Create DOM nodes or React elements references in order to be able to access them programmatically. */ createRefs() { this.listRef = React.createRef(); this.dragFeedbackElement = React.createRef(); } /** * ComponentDidMount * Invoked immediately after component is inserted into the tree * @return {void} */ componentDidMount() { this.props.accountRecoveryContext.loadAccountRecoveryPolicy(); } /** * Whenever the component has been updated */ componentDidUpdate() { this.handleUserScroll(); } /** * Handles the user scroll ( with a specific manual user url /users/view/:id ) */ handleUserScroll() { const userToScroll = this.props.userWorkspaceContext.scrollTo.user; if (userToScroll) { this.scrollTo(userToScroll.id); this.props.userWorkspaceContext.onUserScrolled(); } } /** * Handle the user selection * @param event The DOM event * @param user The selected user */ async handleUserSelected(event, user) { event.preventDefault(); event.stopPropagation(); await this.selectUser(user); } /** * Handle the right click on a user * @param event A DOM event * @param user A user */ async handleUserRightClick(event, user) { // Prevent the default contextual menu to popup. event.preventDefault(); this.displayContextualMenu(event, user); await this.selectUserIfNotAlreadySelected(user); } /** * Handle the checkbox click wrapping * @param event An event * @param user An user */ handleCheckboxWrapperClick(event, user) { /* * We want the td to extend the clickable area of the checkbox. * If we propagate the event, the tr will listen to the click and select only the clicked row. */ event.stopPropagation(); this.props.userWorkspaceContext.onUserSelected.single(user); } /** * Handle the user sorter change * @param event A DOM event * @param sortProperty The user property to sort on */ async handleSortByColumnClick(event, sortProperty) { this.props.userWorkspaceContext.onSorterChanged(sortProperty); } /** * Returns the current list of filtered users to display */ get users() { return this.props.userWorkspaceContext.filteredUsers; } /** * Returns the current list of selected users */ get selectedUsers() { return this.props.userWorkspaceContext.selectedUsers; } /** * Returns true if the given user is selected * @param user A user */ isUserSelected(user) { return this.props.userWorkspaceContext.selectedUsers.some(selectedUser => user.id === selectedUser.id); } /** * Displays the contextual menu for the given user and following the given event * @param event A dom event * @param user A user */ displayContextualMenu(event, user) { const left = event.pageX; const top = event.pageY; const contextualMenuProps = {left, top, user}; this.props.contextualMenuContext.show(DisplayUsersContextualMenu, contextualMenuProps); } /** * Select the user. * @param user An user */ async selectUser(user) { await this.props.userWorkspaceContext.onUserSelected.single(user); } /** * Scroll to the given user * @param userId An user identifier */ scrollTo(userId) { const userIndex = this.users.findIndex(user => user.id === userId); const [visibleStartIndex, visibleEndIndex] = this.listRef.current.getVisibleRange(); const isInvisible = userIndex < visibleStartIndex || userIndex > visibleEndIndex; if (isInvisible) { this.listRef.current.scrollTo(userIndex); } } /** * Select the user if not already selected. * @param user An user */ async selectUserIfNotAlreadySelected(user) { const [selectedUser] = this.props.userWorkspaceContext.selectedUsers; const isUserNotAlreadySelected = !selectedUser || selectedUser.id !== user.id; if (isUserNotAlreadySelected) { await this.selectUser(user); } } /** * Render the users table * @param items Items to display * @param ref The table element reference * @returns {JSX.Element} */ renderTable(items, ref) { const tableStyle = { MozUserSelect: "none", WebkitUserSelect: "none", msUserSelect: "none" }; return ( <table style={tableStyle}> <tbody ref={ref}> {items} </tbody> </table> ); } /** * Check if the grid is sorted for a given column * @param column The column name */ isSortedColumn(column) { return this.props.userWorkspaceContext.sorter.propertyName === column; } /** * Check if the sort is ascendant. * @returns {boolean} */ isSortedAsc() { return this.props.userWorkspaceContext.sorter.asc; } /** * Check if the logged in user is admin * @return {boolean} */ isLoggedInUserAdmin() { return this.props.context.loggedInUser && this.props.context.loggedInUser.role.name === 'admin'; } /** * Returns true if the accountRecovery feature is enabled and if the logged in user is an admin. * @returns {boolean} */ hasAttentionRequiredColumn() { return this.props.context.siteSettings.canIUse("accountRecovery") && this.isLoggedInUserAdmin(); } /** * Returns true if the mfa feature is enabled and if the logged in user is an admin. * @returns {boolean} */ hasMfaColumn() { return this.props.context.siteSettings.canIUse("multiFactorAuthentication") && this.isLoggedInUserAdmin(); } /** * Returns true if the accountRecovery feature is enabled and if the logged in user is an admin. * @returns {boolean} */ hasAccountRecoveryColumn() { return this.props.context.siteSettings.canIUse("accountRecovery") && this.isLoggedInUserAdmin() && this.props.accountRecoveryContext.isPolicyEnabled(); } /** * Format date in time ago * @param {string} date The date to format * @return {string} The formatted date */ formatDateTimeAgo(date) { const dateTime = DateTime.fromISO(date); const duration = dateTime.diffNow().toMillis(); return duration > -1000 && duration < 0 ? this.translate('Just now') : dateTime.toRelative({locale: this.props.context.locale}); } renderItem(index, key) { const user = this.users[index]; const isSelected = this.isUserSelected(user); const modifiedFormatted = this.formatDateTimeAgo(user.modified); const lastLoggedInFormatted = user.last_logged_in ? this.formatDateTimeAgo(user.last_logged_in) : ""; const roleName = this.props.userWorkspaceContext.getTranslatedRoleName(user.role_id); const mfa = user.is_mfa_enabled ? this.translate("Enabled") : this.translate("Disabled"); const rowClassName = `${isSelected ? "selected" : ""} ${user.active ? "" : "inactive"}`; const hasUserAttentionRequired = Boolean(user.pending_account_recovery_request); return ( <tr id={`user_${user.id}`} key={key} className={rowClassName} onClick={event => this.handleUserSelected(event, user)} onContextMenu={event => this.handleUserRightClick(event, user)}> <td className="cell-multiple-select selections s-cell"> <div className="input checkbox"> <input type="checkbox" id={`checkbox_multiple_select_checkbox_${user.id}`} checked={isSelected} readOnly={true} onClick={ev => this.handleCheckboxWrapperClick(ev, user)}/> <span className="visually-hidden"><Trans>Select user</Trans></span> </div> </td> {this.hasAttentionRequiredColumn() && <td className="s-cell attention-required"> {hasUserAttentionRequired && <Icon name="exclamation" baseline={true}/> } </td> } <td className="cell-name l-cell"> <div title={`${user.profile.first_name} ${user.profile.last_name}`}> {`${user.profile.first_name} ${user.profile.last_name}`} </div> </td> <td className="cell-username l-cell username"> <div title={user.username}> {user.username} </div> </td> <td className="cell-role m-cell role"> <div title={roleName}> {roleName} </div> </td> <td className="cell-modified m-cell"> <div title={user.modified}> {modifiedFormatted} </div> </td> <td className="cell-last_logged_in m-cell"> <div title={user.last_logged_in}> {lastLoggedInFormatted} </div> </td> {this.hasMfaColumn() && <td className="cell-is_mfa_enabled m-cell"> <div> {mfa} </div> </td> } {this.hasAccountRecoveryColumn() && <td className="cell-is_account_recovery_enabled m-cell"> <div> {{ "approved": <Trans>Approved</Trans>, "rejected": <Trans>Rejected</Trans>, [undefined]: <Trans>Pending</Trans>, }[user?.account_recovery_user_setting?.status]} </div> </td> } </tr> ); } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } /** * Renders the component */ render() { const isReady = this.users !== null; const isEmpty = isReady && this.users.length === 0; const filterType = this.props.userWorkspaceContext.filter.type; return ( <div className={`tableview ready ${isEmpty ? "empty" : ""}`}> {!isReady && <div className="empty-content"> </div> } {isReady && <React.Fragment> {isEmpty && filterType === UserWorkspaceFilterTypes.TEXT && <div className="empty-content"> <h2><Trans>None of the users matched this search.</Trans></h2> <p className="try-another-search"><Trans>Try another search or use the left panel to navigate into your organization.</Trans></p> </div> } {!isEmpty && <React.Fragment> <div className="tableview-header"> <table> <thead> <tr> <th className="cell-multiple-select selections s-cell"> <div className="input checkbox"> <input type="checkbox" name="select all" checked={false} readOnly={true}/> <span className="visually-hidden"><Trans>Select all</Trans></span> </div> </th> {this.hasAttentionRequiredColumn() && <th className="s-cell attention-required"> <a onClick={ev => this.handleSortByColumnClick(ev, "attentionRequired")}> <div className="cell-header"> <span className="cell-header-text"> <Icon name="exclamation" baseline={true}/> </span> <span className="cell-header-icon-sort"> {this.isSortedColumn("attentionRequired") && this.isSortedAsc() && <Icon baseline={true} name="caret-up"/> } {this.isSortedColumn("attentionRequired") && !this.isSortedAsc() && <Icon baseline={true} name="caret-down"/> } </span> </div> </a> </th> } <th className="cell-name l-cell sortable"> <a onClick={ev => this.handleSortByColumnClick(ev, "name")}> <div className="cell-header"> <span className="cell-header-text"> <Trans>Name</Trans> </span> <span className="cell-header-icon-sort"> {this.isSortedColumn("name") && this.isSortedAsc() && <Icon baseline={true} name="caret-up"/> } {this.isSortedColumn("name") && !this.isSortedAsc() && <Icon baseline={true} name="caret-down"/> } </span> </div> </a> </th> <th className="cell-username l-cell username sortable"> <a onClick={ev => this.handleSortByColumnClick(ev, "username")}> <div className="cell-header"> <span className="cell-header-text"> <Trans>Username</Trans> </span> <span className="cell-header-icon-sort"> {this.isSortedColumn("username") && this.isSortedAsc() && <Icon baseline={true} name="caret-up"/> } {this.isSortedColumn("username") && !this.isSortedAsc() && <Icon baseline={true} name="caret-down"/> } </span> </div> </a> </th> <th className="cell-role m-cell role sortable"> <a onClick={ev => this.handleSortByColumnClick(ev, "role.name")}> <div className="cell-header"> <span className="cell-header-text"> <Trans>Role</Trans> </span> <span className="cell-header-icon-sort"> {this.isSortedColumn("role.name") && this.isSortedAsc() && <Icon baseline={true} name="caret-up"/> } {this.isSortedColumn("role.name") && !this.isSortedAsc() && <Icon baseline={true} name="caret-down"/> } </span> </div> </a> </th> <th className="cell-modified m-cell sortable"> <a onClick={ev => this.handleSortByColumnClick(ev, "modified")}> <div className="cell-header"> <span className="cell-header-text"> <Trans>Modified</Trans> </span> <span className="cell-header-icon-sort"> {this.isSortedColumn("modified") && this.isSortedAsc() && <Icon baseline={true} name="caret-up"/> } {this.isSortedColumn("modified") && !this.isSortedAsc() && <Icon baseline={true} name="caret-down"/> } </span> </div> </a> </th> <th className="cell-last_logged_in m-cell sortable"> <a onClick={ev => this.handleSortByColumnClick(ev, "last_logged_in")}> <div className="cell-header"> <span className="cell-header-text"> <Trans>Last logged in</Trans> </span> <span className="cell-header-icon-sort"> {this.isSortedColumn("last_logged_in") && this.isSortedAsc() && <Icon baseline={true} name="caret-up"/> } {this.isSortedColumn("last_logged_in") && !this.isSortedAsc() && <Icon baseline={true} name="caret-down"/> } </span> </div> </a> </th> {this.hasMfaColumn() && <th className="cell-is_mfa_enabled m-cell sortable"> <a onClick={ev => this.handleSortByColumnClick(ev, "is_mfa_enabled")}> <div className="cell-header"> <span className="cell-header-text"> <Trans>MFA</Trans> </span> <span className="cell-header-icon-sort"> {this.isSortedColumn("is_mfa_enabled") && this.isSortedAsc() && <Icon baseline={true} name="caret-up"/> } {this.isSortedColumn("is_mfa_enabled") && !this.isSortedAsc() && <Icon baseline={true} name="caret-down"/> } </span> </div> </a> </th> } {this.hasAccountRecoveryColumn() && <th className="cell-account_recovery_user_setting_status m-cell sortable"> <a onClick={ev => this.handleSortByColumnClick(ev, "account_recovery_user_setting.status")}> <div className="cell-header"> <span className="cell-header-text"> <Trans>Account recovery</Trans> </span> <span className="cell-header-icon-sort"> {this.isSortedColumn("account_recovery_user_setting.status") && this.isSortedAsc() && <Icon baseline={true} name="caret-up"/> } {this.isSortedColumn("account_recovery_user_setting.status") && !this.isSortedAsc() && <Icon baseline={true} name="caret-down"/> } </span> </div> </a> </th> } </tr> </thead> </table> </div> <div className="tableview-content scroll"> <ReactList itemRenderer={(index, key) => this.renderItem(index, key)} itemsRenderer={(items, ref) => this.renderTable(items, ref)} length={this.users.length} pageSize={20} minSize={20} type="uniform" ref={this.listRef}> </ReactList> </div> </React.Fragment> } </React.Fragment> } </div> ); } } DisplayUsers.propTypes = { context: PropTypes.any, // The application context userWorkspaceContext: PropTypes.any, // The user workspace context actionFeedbackContext: PropTypes.any, // The action feedback context contextualMenuContext: PropTypes.any, // The contextual menu context accountRecoveryContext: PropTypes.object, // The account recovery context t: PropTypes.func, // The translation function }; export default withAppContext(withRouter(withActionFeedback(withContextualMenu(withUserWorkspace(withAccountRecovery(withTranslation('common')(DisplayUsers)))))));