UNPKG

passbolt-styleguide

Version:

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

571 lines (527 loc) 19.6 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 { withAppContext } from "../../../../shared/context/AppContext/AppContext"; 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 { withAccountRecovery } from "../../../contexts/AccountRecoveryUserContext"; import { isUserSuspended } from "../../../../shared/utils/userUtils"; import ColumnCheckboxModel from "../../../../shared/models/column/ColumnCheckboxModel"; import CellCheckbox from "../../../../shared/components/Table/CellChecbox"; import CellHeaderCheckbox from "../../../../shared/components/Table/CellHeaderCheckbox"; import CellHeaderDefault from "../../../../shared/components/Table/CellHeaderDefault"; import ColumnModifiedModel from "../../../../shared/models/column/ColumnModifiedModel"; import CellDate from "../../../../shared/components/Table/CellDate"; import GridTable from "../../../../shared/components/Table/GridTable"; import CellUserProfile from "../../../../shared/components/Table/CellUserProfile"; import ColumnUserUsernameModel from "../../../../shared/models/column/ColumnUserUsernameModel"; import CellUserRole from "../../../../shared/components/Table/CellUserRole"; import ColumnUserRoleModel from "../../../../shared/models/column/ColumnUserRoleModel"; import ColumnUserProfileModel from "../../../../shared/models/column/ColumnUserProfileModel"; import ColumnUserSuspendedModel from "../../../../shared/models/column/ColumnUserSuspendedModel"; import ColumnUserLastLoggedInModel from "../../../../shared/models/column/ColumnUserLastLoggedInModel"; import ColumnUserMfaModel from "../../../../shared/models/column/ColumnUserMfaModel"; import CellUserSuspended from "../../../../shared/components/Table/CellUserSuspended"; import CellUserMfa from "../../../../shared/components/Table/CellUserMfa"; import ColumnUserAccountRecoveryModel from "../../../../shared/models/column/ColumnUserAccountRecoveryModel"; import CellUserAccountRecovery from "../../../../shared/components/Table/CellUserAccountRecovery"; import ColumnsUserSettingCollection from "../../../../shared/models/entity/user/columnsUserSettingCollection"; import ColumnModel from "../../../../shared/models/column/ColumnModel"; import CircleOffSVG from "../../../../img/svg/circle_off.svg"; import { withRbac } from "../../../../shared/context/Rbac/RbacContext"; import { actions } from "../../../../shared/services/rbacs/actionEnumeration"; import { withRoles } from "../../../contexts/RoleContext"; import DisplayDragUser from "./DisplayDragUser"; import { withDrag } from "../../../contexts/DragContext"; /** * 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); // The grid columns this.defaultColumns = []; this.state = this.getDefaultState(); this.initEventHandlers(); this.createRefs(); } /** * Returns the component default state */ getDefaultState() { return { columns: [], // The current list of columns to display. }; } /** * 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); this.handleUserDragStartEvent = this.handleUserDragStartEvent.bind(this); this.handleDragEndEvent = this.handleDragEndEvent.bind(this); this.isRowInactive = this.isRowInactive.bind(this); this.getDisabledGroupIds = this.getDisabledGroupIds.bind(this); } /** * Create DOM nodes or React elements references in order to be able to access them programmatically. */ createRefs() { this.listRef = React.createRef(); } /** * ComponentDidMount * Invoked immediately after component is inserted into the tree * @return {void} */ async componentDidMount() { await this.props.accountRecoveryContext.loadAccountRecoveryPolicy(); this.initColumns(); this.mergeAndSortColumns(); } /** * Whenever the component has been updated */ componentDidUpdate() { this.handleUserScroll(); } /** * Init the grid columns. */ initColumns() { this.defaultColumns.push( new ColumnCheckboxModel({ cellRenderer: { component: CellCheckbox, props: { onClick: this.handleCheckboxWrapperClick } }, headerCellRenderer: { component: CellHeaderCheckbox, props: { disabled: true } }, }), ); this.defaultColumns.push( new ColumnUserProfileModel({ cellRenderer: { component: CellUserProfile, props: { hasAttentionRequiredFeature: this.hasAttentionRequiredColumn }, }, headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Name") } }, }), ); this.defaultColumns.push( new ColumnUserUsernameModel({ headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Username") } }, }), ); this.defaultColumns.push( new ColumnUserRoleModel({ getValue: (user) => this.props.userWorkspaceContext.getTranslatedRoleName(user.role_id), cellRenderer: { component: CellUserRole }, headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Role") } }, }), ); if (this.hasSuspendedColumn) { this.defaultColumns.push( new ColumnUserSuspendedModel({ cellRenderer: { component: CellUserSuspended }, headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Suspended") } }, }), ); } this.defaultColumns.push( new ColumnModifiedModel({ cellRenderer: { component: CellDate, props: { locale: this.props.context.locale, t: this.props.t } }, headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Modified") } }, }), ); this.defaultColumns.push( new ColumnUserLastLoggedInModel({ cellRenderer: { component: CellDate, props: { locale: this.props.context.locale, t: this.props.t } }, headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Last logged in") } }, }), ); if (this.hasMfaColumn) { this.defaultColumns.push( new ColumnUserMfaModel({ cellRenderer: { component: CellUserMfa }, headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("MFA") } }, }), ); } if (this.hasAccountRecoveryColumn) { this.defaultColumns.push( new ColumnUserAccountRecoveryModel({ cellRenderer: { component: CellUserAccountRecovery }, headerCellRenderer: { component: CellHeaderDefault, props: { label: this.translate("Account recovery") } }, }), ); } } /** * Merge and sort columns */ mergeAndSortColumns() { // Get the column with id as a key from the column to merge const columnsUserSetting = this.columnsUserSetting.toHashTable(); // Merge the column values const columns = this.defaultColumns.map((column) => Object.assign(new ColumnModel(column), columnsUserSetting[column.id]), ); // Sort the position of the column, the column with no position will be at the beginning columns.sort((columnA, columnB) => ((columnA.position || 0) < (columnB.position || 0) ? -1 : 1)); this.setState({ columns }); } /** * Handles the user scroll ( with a specific manual user url /users/view/:id ) */ handleUserScroll() { const userToScroll = this.props.userWorkspaceContext.scrollTo.user; const hasNotEmptyRange = this.listRef.current?.getVisibleRange().some((value) => value); if (userToScroll && hasNotEmptyRange) { 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 sortProperty The user property to sort on */ async handleSortByColumnClick(sortProperty) { this.props.userWorkspaceContext.onSorterChanged(sortProperty); } /** * Handle the drag start on the selected user * @param event The DOM event * @param user The selected user * @param isSelected is user selected * @returns {Promise<void>} */ async handleUserDragStartEvent(event, user, isSelected) { if (!isSelected) { await this.props.userWorkspaceContext.onUserSelected.single(user); } const draggedItems = { users: this.props.userWorkspaceContext.selectedUsers, groups: [], disabledGroupIds: this.getDisabledGroupIds(this.props.userWorkspaceContext.selectedUsers), }; this.props.dragContext.onDragStart(event, DisplayDragUser, draggedItems); } /** * Handle when the user stop dragging content. */ handleDragEndEvent() { this.props.dragContext.onDragEnd(); } /** * 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; } /** * get columns user setting * @return {ColumnsSettingCollection} */ get columnsUserSetting() { return ColumnsUserSettingCollection.DEFAULT; } /** * Get the list of group IDs where the logged-in user is not an admin or any of the selected users are already members * @param {Array} selectedUsers - Array of selected user objects * @returns {string[]} Array of group IDs to disable */ getDisabledGroupIds(selectedUsers) { const groups = this.props.context.groups; const currentUserId = this.props.context.loggedInUser?.id; if (!groups || !currentUserId) { return []; } return groups .filter((group) => { const isAdmin = group.groups_users.some((gu) => gu.user_id === currentUserId && gu.is_admin); if (!isAdmin) { return true; } return selectedUsers.some((selectedUser) => group.groups_users.some((gu) => gu.user_id === selectedUser.id)); }) .map((group) => group.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); } } /** * 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} */ get isLoggedInUserAdmin() { return this.props.context.loggedInUser && this.props.context.loggedInUser.role.name === "admin"; } /** * Returns true if * - accountRecovery feature is enabled and the user has account recovery request view allowed * or * - metadata feature is enabled and the logged-in user is an admin. * @returns {boolean} */ get hasAttentionRequiredColumn() { return ( (this.props.context.siteSettings.canIUse("accountRecovery") && this.props.rbacContext.canIUseAction(actions.ACCOUNT_RECOVERY_REQUEST_VIEW)) || (this.props.context.siteSettings.canIUse("metadata") && this.isLoggedInUserAdmin) ); } /** * Returns true if the mfa feature is enabled and if the logged in user is an admin. * @returns {boolean} */ get hasMfaColumn() { return this.props.context.siteSettings.canIUse("multiFactorAuthentication") && this.isLoggedInUserAdmin; } /** * Returns true if the suspended user feature is enabled. * @returns {boolean} */ get hasSuspendedColumn() { return this.props.context.siteSettings.canIUse("disableUser") && this.isLoggedInUserAdmin; } /** * Returns true if the accountRecovery feature is enabled and if the logged in user is an admin. * @returns {boolean} */ get hasAccountRecoveryColumn() { return ( this.props.context.siteSettings.canIUse("accountRecovery") && this.props.rbacContext.canIUseAction(actions.ACCOUNT_RECOVERY_REQUEST_VIEW) && this.props.accountRecoveryContext.isPolicyEnabled() ); } /** * Get selected users ids * @return {*} */ get selectedUsersIds() { const getIds = (user) => user.id; return this.selectedUsers.map(getIds); } /** * Is row inactive * @param user * @returns {boolean} */ isRowInactive(user) { return !user.active || (this.hasSuspendedColumn && isUserSuspended(user)); } /** * Get the columns to display * @return {[]} */ get columnsFiltered() { const filteredByColumnToDisplay = (column) => column.id === "checkbox" || column.show; return this.state.columns.filter(filteredByColumnToDisplay); } /** * 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; const isGridReady = isReady && this.users.length !== 0 && this.columnsFiltered.length !== 0; const isSearchFilterApplied = filterType === UserWorkspaceFilterTypes.TEXT; return ( <> {!isReady && ( <div className="tableview empty"> <div className="empty-content"></div> </div> )} {isEmpty && ( <div className="tableview empty"> {isSearchFilterApplied && ( <div className="empty-content"> <CircleOffSVG /> <div className="message"> <h1> <Trans>None of the users matched this search.</Trans> </h1> <p className="try-another-search"> <Trans>Try another search or use the left panel to navigate into your organization.</Trans> </p> </div> </div> )} {!isSearchFilterApplied && ( <div className="empty-content"> <CircleOffSVG /> <div className="message"> <h1> <Trans>There are no users.</Trans> </h1> <p className="try-another-filter"> <Trans>You could remove some filters.</Trans> </p> </div> </div> )} </div> )} {isGridReady && ( <GridTable columns={this.columnsFiltered} rows={this.users} sorter={this.props.userWorkspaceContext.sorter} onSortChange={this.handleSortByColumnClick} onRowClick={this.handleUserSelected} onRowContextMenu={this.handleUserRightClick} onRowDragStart={this.handleUserDragStartEvent} onRowDragEnd={this.handleDragEndEvent} selectedRowsIds={this.selectedUsersIds} isRowInactive={this.isRowInactive} rowsRef={this.listRef} ></GridTable> )} </> ); } } DisplayUsers.propTypes = { context: PropTypes.any, // The application context rbacContext: PropTypes.any, // The rbac context roleContext: PropTypes.object, // The role 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 dragContext: PropTypes.any, t: PropTypes.func, // The translation function }; export default withAppContext( withRbac( withRouter( withActionFeedback( withContextualMenu( withUserWorkspace(withRoles(withAccountRecovery(withDrag(withTranslation("common")(DisplayUsers))))), ), ), ), ), );