UNPKG

passbolt-styleguide

Version:

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

782 lines (721 loc) 27.5 kB
/** * Passbolt ~ Open source password manager for teams * Copyright (c) Passbolt SA (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 SA (https://www.passbolt.com) * @license https://opensource.org/licenses/AGPL-3.0 AGPL License * @link https://www.passbolt.com Passbolt(tm) * @since 2.13.0 */ import React from "react"; import { withActionFeedback } from "../../../contexts/ActionFeedbackContext"; import PropTypes from "prop-types"; import { withAppContext } from "../../../../shared/context/AppContext/AppContext"; import { withResourceWorkspace } from "../../../contexts/ResourceWorkspaceContext"; import { withDialog } from "../../../contexts/DialogContext"; import DeleteResource from "../DeleteResource/DeleteResource"; import EditResource from "../EditResource/EditResource"; import ShareDialog from "../../Share/ShareDialog"; import ExportResources from "../ExportResources/ExportResources"; import { Trans, withTranslation } from "react-i18next"; import { withRbac } from "../../../../shared/context/Rbac/RbacContext"; import { uiActions } from "../../../../shared/services/rbacs/uiActionEnumeration"; import { withProgress } from "../../../contexts/ProgressContext"; import { TotpCodeGeneratorService } from "../../../../shared/services/otp/TotpCodeGeneratorService"; import PasswordExpiryDialog from "../PasswordExpiryDialog/PasswordExpiryDialog"; import { withPasswordExpiry } from "../../../contexts/PasswordExpirySettingsContext"; import { formatDateForApi } from "../../../../shared/utils/dateUtils"; import { DateTime } from "luxon"; import { withResourceTypesLocalStorage } from "../../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext"; import ResourceTypesCollection from "../../../../shared/models/entity/resourceType/resourceTypesCollection"; import DropdownButton from "../../Common/Dropdown/DropdownButton"; import CaretDownSVG from "../../../../img/svg/caret_down.svg"; import Dropdown from "../../Common/Dropdown/Dropdown"; import DropdownMenu from "../../Common/Dropdown/DropdownMenu"; import MoreHorizontalSVG from "../../../../img/svg/more_horizontal.svg"; import DropdownMenuItem from "../../Common/Dropdown/DropdownMenuItem"; import DownloadFileSVG from "../../../../img/svg/download_file.svg"; import CalendarCogSVG from "../../../../img/svg/calendar_cog.svg"; import AlarmClockSVG from "../../../../img/svg/alarm_clock.svg"; import CopySVG from "../../../../img/svg/copy.svg"; import OwnedByMeSVG from "../../../../img/svg/owned_by_me.svg"; import KeySVG from "../../../../img/svg/key.svg"; import TotpSVG from "../../../../img/svg/totp.svg"; import GlobeSVG from "../../../../img/svg/globe.svg"; import LinkSVG from "../../../../img/svg/link.svg"; import DeleteSVG from "../../../../img/svg/delete.svg"; import EditSVG from "../../../../img/svg/edit.svg"; import ShareSVG from "../../../../img/svg/share.svg"; import CloseSVG from "../../../../img/svg/close.svg"; import SecretHistorySVG from "../../../../img/svg/history.svg"; import { withClipboard } from "../../../contexts/Clipboard/ManagedClipboardServiceProvider"; import { withMetadataKeysSettingsLocalStorage } from "../../../../shared/context/MetadataKeysSettingsLocalStorageContext/MetadataKeysSettingsLocalStorageContext"; import MetadataKeysSettingsEntity from "../../../../shared/models/entity/metadata/metadataKeysSettingsEntity"; import ActionAbortedMissingMetadataKeys from "../../Metadata/ActionAbortedMissingMetadataKeys/ActionAbortedMissingMetadataKeys"; import Logger from "../../../../shared/utils/logger"; import { withSecretRevisionsSettings } from "../../../../shared/context/SecretRevisionSettingsContext/SecretRevisionsSettingsContext"; import SecretRevisionsSettingsEntity from "../../../../shared/models/entity/secretRevision/secretRevisionsSettingsEntity"; import DisplayResourceSecretHistory from "../../SecretHistory/DisplayResourceSecretHistory"; /** * This component allows the current user to add a new comment on a resource */ class DisplayResourcesWorkspaceMenu extends React.Component { /** * Constructor * @param {Object} props */ constructor(props) { super(props); this.bindCallbacks(); } /** * Bind callbacks methods */ bindCallbacks() { this.handleDeleteClickEvent = this.handleDeleteClickEvent.bind(this); this.handleEditClickEvent = this.handleEditClickEvent.bind(this); this.handleCopyPermalinkClickEvent = this.handleCopyPermalinkClickEvent.bind(this); this.handleCopyUsernameClickEvent = this.handleCopyUsernameClickEvent.bind(this); this.handleCopyUriClickEvent = this.handleCopyUriClickEvent.bind(this); this.handleShareClickEvent = this.handleShareClickEvent.bind(this); this.handleCopySecretClickEvent = this.handleCopySecretClickEvent.bind(this); this.handleCopyTotpClickEvent = this.handleCopyTotpClickEvent.bind(this); this.handleExportClickEvent = this.handleExportClickEvent.bind(this); this.handleMarkAsExpiredClick = this.handleMarkAsExpiredClick.bind(this); this.handleSetExpiryDateClickEvent = this.handleSetExpiryDateClickEvent.bind(this); this.handleClearSelectionClick = this.handleClearSelectionClick.bind(this); this.handleSecretHistoryClick = this.handleSecretHistoryClick.bind(this); } /** * handle delete one or more resources */ handleDeleteClickEvent() { this.props.dialogContext.open(DeleteResource, { resources: this.selectedResources }); } /** * Handle mark as expired * @returns {Promise<void>} */ async handleMarkAsExpiredClick() { const resourcesExpiryDateToUpdate = this.selectedResources.map((resource) => ({ id: resource.id, expired: formatDateForApi(DateTime.utc()), })); try { await this.props.context.port.request("passbolt.resources.set-expiration-date", resourcesExpiryDateToUpdate); await this.props.actionFeedbackContext.displaySuccess( this.translate("The resource has been marked as expired.", { count: resourcesExpiryDateToUpdate.length }), ); } catch (error) { Logger.error(error); await this.props.actionFeedbackContext.displayError( this.translate("Unable to mark the resource as expired.", { count: resourcesExpiryDateToUpdate.length }), ); } } /** * Handle secret history click */ handleSecretHistoryClick() { this.props.dialogContext.open(DisplayResourceSecretHistory, { resource: this.selectedResources[0] }); } /** * handle edit one resource */ handleEditClickEvent() { const canEditResource = this.canEditResource(); if (canEditResource) { this.props.dialogContext.open(EditResource, { resource: this.selectedResources[0] }); } else { this.displayActionAborted(); } } /** * Can edit the resource * @return {boolean} */ canEditResource() { const resourceType = this.props.resourceTypes.getFirstById(this.selectedResources[0].resource_type_id); if (resourceType.isV5()) { const isMetadataSharedKeyEnforced = !this.props.metadataKeysSettings?.allowUsageOfPersonalKeys; const isPersonalResource = this.selectedResources[0].personal; const userHasMissingKeys = this.props.context.loggedInUser.missing_metadata_key_ids?.length > 0; if (isPersonalResource && isMetadataSharedKeyEnforced && userHasMissingKeys) { return false; } else if (!isPersonalResource && userHasMissingKeys) { return false; } } return true; } /** * handle share resources */ async handleShareClickEvent() { const canShareResource = this.canShareResource(); if (canShareResource) { const resourcesIds = this.selectedResources.map((resource) => resource.id); await this.props.context.setContext({ shareDialogProps: { resourcesIds } }); this.props.dialogContext.open(ShareDialog); } else { this.displayActionAborted(); } } /** * Can share the resource * @return {boolean} */ canShareResource() { const resourceType = this.props.resourceTypes.getFirstById(this.selectedResources[0].resource_type_id); if (resourceType.isV5()) { const userHasMissingKeys = this.props.context.loggedInUser.missing_metadata_key_ids?.length > 0; return !userHasMissingKeys; } return true; } /** * handle copy permalink of one resource */ async handleCopyPermalinkClickEvent() { const baseUrl = this.props.context.userSettings.getTrustedDomain(); const permalink = `${baseUrl}/app/passwords/view/${this.selectedResources[0].id}`; await this.props.clipboardContext.copy(permalink, this.translate("The permalink has been copied to clipboard.")); } /** * handle copy username of one resource */ async handleCopyUsernameClickEvent() { await this.props.clipboardContext.copy( this.selectedResources[0].metadata.username, this.translate("The username has been copied to clipboard."), ); } /** * handle copy uri of one resource */ async handleCopyUriClickEvent() { await this.props.clipboardContext.copy( this.selectedResources[0].metadata.uris[0], this.translate("The uri has been copied to clipboard."), ); } /** * Handle the event on the 'close' icon to clear the current selection. * @returns {Promise<void>} */ async handleClearSelectionClick() { await this.props.resourceWorkspaceContext.onResourceSelected.none(); } /** * Decrypt the resource secret * @returns {Promise<object>} The secret in plaintext format * @throw UserAbortsOperationError If the user cancel the operation */ decryptResourceSecret() { return this.props.context.port.request("passbolt.secret.find-by-resource-id", this.selectedResources[0].id); } /** * Copy password from dto to clipboard * Support original password (a simple string) and composed objects) * * @param {object} plaintextSecretDto The plaintext secret DTO. * @returns {Promise<void>} */ async copyPasswordToClipboard(plaintextSecretDto) { const password = plaintextSecretDto.password; if (!password) { throw new TypeError(this.translate("The password is empty.")); } await this.props.clipboardContext.copyTemporarily( password, this.translate("The secret has been copied to clipboard."), ); } /** * handle copy to clipboard the secret of the selected resource */ async handleCopySecretClickEvent() { let plaintextSecretDto; this.props.progressContext.open(this.props.t("Decrypting secret")); try { plaintextSecretDto = await this.decryptResourceSecret(); } catch (error) { if (error.name !== "UserAbortsOperationError") { this.props.actionFeedbackContext.displayError(error.message); } } this.props.progressContext.close(); if (!plaintextSecretDto?.password?.length) { await this.props.actionFeedbackContext.displayWarning( this.translate("The password is empty and cannot be copied to clipboard."), ); return; } await this.copyPasswordToClipboard(plaintextSecretDto); this.props.resourceWorkspaceContext.onResourceCopied(); } /** * handle copy to clipboard the totp of the selected resource */ async handleCopyTotpClickEvent() { let plaintextSecretDto, code; this.props.progressContext.open(this.props.t("Decrypting secret")); try { plaintextSecretDto = await this.decryptResourceSecret(); } catch (error) { if (error.name !== "UserAbortsOperationError") { this.props.actionFeedbackContext.displayError(error.message); } } this.props.progressContext.close(); if (!plaintextSecretDto) { return; } if (!plaintextSecretDto.totp) { await this.props.actionFeedbackContext.displayError( this.translate("The TOTP is empty and cannot be copied to clipboard."), ); return; } try { code = TotpCodeGeneratorService.generate(plaintextSecretDto.totp); } catch (error) { Logger.error(error); await this.props.actionFeedbackContext.displayError(this.translate("Unable to copy the TOTP")); return; } await this.props.clipboardContext.copyTemporarily(code, this.translate("The TOTP has been copied to clipboard.")); await this.props.resourceWorkspaceContext.onResourceCopied(); } /** * Whenever the user intends to set the expiration date on the selected resources */ handleSetExpiryDateClickEvent() { this.props.dialogContext.open(PasswordExpiryDialog, { resources: this.selectedResources, }); } /** * Whenever the user intends to export the selected resources */ handleExportClickEvent() { this.export(); } /** * Display action aborted */ displayActionAborted() { this.props.dialogContext.open(ActionAbortedMissingMetadataKeys); } /** * Selected resources * @returns {Array|null} */ get selectedResources() { return this.props.resourceWorkspaceContext.selectedResources; } /** * has at least one resource selected * @returns {boolean} */ hasOneResourceSelected() { return this.selectedResources.length === 1; } /** * Can update the selected resources * @return {boolean} */ canUpdate() { return this.selectedResources.every((resource) => resource.permission.type >= 7); } /** * Can share the selected resources * @return {boolean} */ canShare() { return ( this.props.rbacContext.canIUseAction(uiActions.SHARE_VIEW_LIST) && this.selectedResources.every((resource) => resource.permission.type === 15) ); } /** * Check if the user can export. * @return {boolean} */ canExport() { return ( this.props.context.siteSettings.canIUse("export") && this.props.rbacContext.canIUseAction(uiActions.RESOURCES_EXPORT) ); } /** * Can copy username * @returns {boolean} */ canCopyUsername() { return this.selectedResources[0].metadata?.username; } /** * Can copy uri * @returns {boolean} */ canCopyUri() { return Boolean(this.selectedResources[0].metadata?.uris?.[0]); } /** * Can copy password * @returns {boolean} */ canCopyPassword() { return this.isPasswordResources; } /** * Can view secret history * @return {boolean} */ canViewSecretHistory() { return ( this.props.context.siteSettings.canIUse("secretRevisions") && this.props.secretRevisionsSettings?.isFeatureEnabled ); } /** * Is password resource * @return {boolean} */ get isPasswordResources() { return this.props.resourceTypes?.getFirstById(this.selectedResources[0].resource_type_id)?.hasPassword(); } /** * Can copy totp * @returns {boolean} */ canCopyTotp() { return this.props.resourceTypes?.getFirstById(this.selectedResources[0].resource_type_id)?.hasTotp(); } /** * Is TOTP resource * @return {boolean} */ get isStandaloneTotpResource() { return this.props.resourceTypes?.getFirstById(this.selectedResources[0].resource_type_id).isStandaloneTotp(); } /** * Returns true if the resource type has a username associated. * @returns {boolean} */ get hasResourceUsername() { return !this.isStandaloneTotpResource; } /** * Has at least one action of the more menu allowed. * @return {boolean} */ hasMoreActionAllowed() { return this.canExport() || (this.canOverridePasswordExpiry() && this.canUpdate()) || this.canViewSecretHistory(); } /** * Exports the selected resources */ async export() { const resourcesIds = this.selectedResources.map((resource) => resource.id); await this.props.resourceWorkspaceContext.onResourcesToExport({ resourcesIds }); await this.props.dialogContext.open(ExportResources); } /** * Can use Totp * @return {boolean} */ canUseTotp() { return this.props.context.siteSettings.canIUse("totpResourceTypes"); } /** * Can override password expiry * @return {boolean} */ canOverridePasswordExpiry() { const passwordExpirySettings = this.props.passwordExpiryContext.getSettings(); return this.props.passwordExpiryContext.isFeatureEnabled() && passwordExpirySettings?.policy_override; } /** * Can copy secrets * @return {boolean} */ canCopySecrets() { return this.props.rbacContext.canIUseAction(uiActions.SECRETS_COPY); } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } /** * Render the component * @returns {JSX} */ render() { const count = this.selectedResources?.length; const hasOneResourceSelected = this.hasOneResourceSelected(); // Main actions const canViewShare = this.canShare(); const canViewCopy = hasOneResourceSelected; const canUpdate = this.canUpdate(); const canViewEdit = hasOneResourceSelected && canUpdate; const canViewDelete = canUpdate; const hasMoreActionAllowed = this.hasMoreActionAllowed(); // Three dot menu const canExport = this.canExport(); const canViewSecretHistory = hasOneResourceSelected && this.canViewSecretHistory(); const canSetExpiryDate = this.canOverridePasswordExpiry() && canUpdate; // Copy menu const canCopySecret = this.canCopySecrets() && this.canCopyPassword(); const canCopyTotp = this.canUseTotp() && this.canCopyTotp(); return ( <div className="actions" ref={this.props.actionsButtonRef}> <div className="actions-wrapper"> <ul> {canViewShare && ( <li id="share_action"> <button type="button" className="button-action-contextual" onClick={this.handleShareClickEvent}> <ShareSVG /> <span> <Trans>Share</Trans> </span> </button> </li> )} {canViewCopy && ( <li id="copy_action"> <Dropdown> <DropdownButton className="button-action-contextual"> <CopySVG /> <span> <Trans>Copy</Trans> </span> <CaretDownSVG /> </DropdownButton> <DropdownMenu className="menu-action-contextual"> {this.hasResourceUsername && ( <DropdownMenuItem> <button id="username_action" type="button" className="no-border" disabled={!this.canCopyUsername()} onClick={this.handleCopyUsernameClickEvent} > <OwnedByMeSVG /> <span> <Trans>Copy username</Trans> </span> </button> </DropdownMenuItem> )} {canCopySecret && ( <DropdownMenuItem> <button id="secret_action" type="button" className="no-border" onClick={this.handleCopySecretClickEvent} > <KeySVG /> <span> <Trans>Copy password</Trans> </span> </button> </DropdownMenuItem> )} {canCopyTotp && ( <DropdownMenuItem> <button id="totp_action" type="button" className="no-border" onClick={this.handleCopyTotpClickEvent} > <TotpSVG /> <span> <Trans>Copy TOTP</Trans> </span> </button> </DropdownMenuItem> )} <DropdownMenuItem> <button id="uri_action" type="button" className="no-border" disabled={!this.canCopyUri()} onClick={this.handleCopyUriClickEvent} > <GlobeSVG /> <span> <Trans>Copy URI</Trans> </span> </button> </DropdownMenuItem> <DropdownMenuItem> <button id="permalink_action" type="button" className="no-border" onClick={this.handleCopyPermalinkClickEvent} > <LinkSVG /> <span> <Trans>Copy permalink</Trans> </span> </button> </DropdownMenuItem> </DropdownMenu> </Dropdown> </li> )} {canViewEdit && ( <li id="edit_action"> <button type="button" className="button-action-contextual" onClick={this.handleEditClickEvent}> <EditSVG /> <span> <Trans>Edit</Trans> </span> </button> </li> )} {canViewDelete && ( <li id="delete_action"> <button type="button" className="button-action-contextual" onClick={this.handleDeleteClickEvent}> <DeleteSVG /> <span> <Trans>Delete</Trans> </span> </button> </li> )} {hasMoreActionAllowed && ( <li> <Dropdown> <DropdownButton className="more button-action-contextual button-action-icon"> <MoreHorizontalSVG /> </DropdownButton> <DropdownMenu className="menu-action-contextual"> {canExport && ( <DropdownMenuItem separator={!canSetExpiryDate && canViewSecretHistory}> <button id="export_action" type="button" className="no-border" onClick={this.handleExportClickEvent} > <DownloadFileSVG /> <span> <Trans>Export</Trans> </span> </button> </DropdownMenuItem> )} {canSetExpiryDate && ( <> <DropdownMenuItem> <button id="set_expiry_date_action" type="button" className="no-border" onClick={this.handleSetExpiryDateClickEvent} > <CalendarCogSVG /> <span> <Trans>Set expiry date</Trans> </span> </button> </DropdownMenuItem> <DropdownMenuItem separator={canViewSecretHistory}> <button id="mark_as_expired_action" type="button" className="no-border" onClick={this.handleMarkAsExpiredClick} > <AlarmClockSVG /> <span> <Trans>Mark as expired</Trans> </span> </button> </DropdownMenuItem> </> )} {canViewSecretHistory && ( <DropdownMenuItem> <button id="secret_history_action" type="button" className="no-border" onClick={this.handleSecretHistoryClick} > <SecretHistorySVG /> <span> <Trans>Secret history</Trans> </span> </button> </DropdownMenuItem> )} </DropdownMenu> </Dropdown> </li> )} </ul> <span className="counter"> <Trans count={count}>{{ count }} selected</Trans> </span> <button type="button" className="button-transparent inline" onClick={this.handleClearSelectionClick}> <CloseSVG /> <span className="visuallyhidden"> <Trans>Clear selection</Trans> </span> </button> </div> </div> ); } } DisplayResourcesWorkspaceMenu.propTypes = { actionsButtonRef: PropTypes.object, // The forwarded ref of the buttons container context: PropTypes.any, // The application context rbacContext: PropTypes.any, // The role based access control context actionFeedbackContext: PropTypes.any, // The action feedback context resourceWorkspaceContext: PropTypes.any, // the resource workspace context resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection passwordExpiryContext: PropTypes.object, // the password expiry context dialogContext: PropTypes.any, // the dialog context progressContext: PropTypes.any, // The progress context clipboardContext: PropTypes.object, // the clipboard service provider metadataKeysSettings: PropTypes.instanceOf(MetadataKeysSettingsEntity), // The metadata key settings secretRevisionsSettings: PropTypes.instanceOf(SecretRevisionsSettingsEntity), // The secret revision settings t: PropTypes.func, // The translation function }; export default withAppContext( withMetadataKeysSettingsLocalStorage( withClipboard( withRbac( withDialog( withProgress( withPasswordExpiry( withSecretRevisionsSettings( withResourceWorkspace( withResourceTypesLocalStorage( withActionFeedback(withTranslation("common")(DisplayResourcesWorkspaceMenu)), ), ), ), ), ), ), ), ), ), );