UNPKG

passbolt-styleguide

Version:

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

497 lines (457 loc) 17.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 PropTypes from "prop-types"; import DisplayResourceDetailsInformation from "./DisplayResourceDetailsInformation"; import DisplayResourceDetailsTag from "./DisplayResourceDetailsTag"; import DisplayResourceDetailsComment from "./DisplayResourceDetailsComment"; import DisplayResourceDetailsDescription from "./DisplayResourceDetailsDescription"; import { withResourceWorkspace } from "../../../contexts/ResourceWorkspaceContext"; import DisplayResourceDetailsPermission from "./DisplayResourceDetailsPermission"; import { withAppContext } from "../../../../shared/context/AppContext/AppContext"; import DisplayResourceDetailsActivity from "./DisplayResourceDetailsActivity"; import { withActionFeedback } from "../../../contexts/ActionFeedbackContext"; import { Trans, withTranslation } from "react-i18next"; import { uiActions } from "../../../../shared/services/rbacs/uiActionEnumeration"; import { withRbac } from "../../../../shared/context/Rbac/RbacContext"; import { withResourceTypesLocalStorage } from "../../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext"; import ResourceTypesCollection from "../../../../shared/models/entity/resourceType/resourceTypesCollection"; import DisplayResourceDetailsPassword from "./DisplayResourceDetailsPassword"; import DisplayResourceDetailsTotp from "./DisplayResourceDetailsTotp"; import LinkSVG from "../../../../img/svg/link.svg"; import Tabs from "../../Common/Tab/Tabs"; import Tab from "../../Common/Tab/Tab"; import DisplayResourceDetailsNote from "./DisplayResourceDetailsNote"; import ResourceIcon from "../../../../shared/components/Icons/ResourceIcon"; import ArrowBigUpDashSVG from "../../../../img/svg/arrow_big_up_dash.svg"; import CaretDownSVG from "../../../../img/svg/caret_down.svg"; import CaretRightSVG from "../../../../img/svg/caret_right.svg"; import { V4_TO_V5_RESOURCE_TYPE_MAPPING } from "../../../../shared/models/entity/resourceType/resourceTypeSchemasDefinition"; import { withMetadataTypesSettingsLocalStorage } from "../../../../shared/context/MetadataTypesSettingsLocalStorageContext/MetadataTypesSettingsLocalStorageContext"; import MetadataTypesSettingsEntity from "../../../../shared/models/entity/metadata/metadataTypesSettingsEntity"; import ResourceFormEntity from "../../../../shared/models/entity/resource/resourceFormEntity"; import DisplayResourceDetailsCustomFields from "./DisplayResourceDetailsCustomFields"; import DisplayResourceDetailsURIs from "./DisplayResourceDetailsURIs"; import { withClipboard } from "../../../contexts/Clipboard/ManagedClipboardServiceProvider"; import { withMetadataKeysSettingsLocalStorage } from "../../../../shared/context/MetadataKeysSettingsLocalStorageContext/MetadataKeysSettingsLocalStorageContext"; import MetadataKeysSettingsEntity from "../../../../shared/models/entity/metadata/metadataKeysSettingsEntity"; import { withDialog } from "../../../contexts/DialogContext"; import ActionAbortedMissingMetadataKeys from "../../Metadata/ActionAbortedMissingMetadataKeys/ActionAbortedMissingMetadataKeys"; class DisplayResourceDetails extends React.Component { /** * Constructor * @param {Object} props */ constructor(props) { super(props); this.state = this.defaultState; this.bindCallbacks(); } /** * Get the default state * @returns {object} */ get defaultState() { return { displayUpgrade: true, processing: false, }; } /** * Bind callbacks methods */ bindCallbacks() { this.handlePermalinkClick = this.handlePermalinkClick.bind(this); this.handleCloseClick = this.handleCloseClick.bind(this); this.handleDisplayUpgradeClick = this.handleDisplayUpgradeClick.bind(this); this.handleUpgradeClick = this.handleUpgradeClick.bind(this); } /** * Get the sidebar subtitle */ get subtitle() { const resource = this.resource; // Resources types might not be yet initialized at the moment this component is rendered. if (!this.props.resourceTypes) { return ""; } const resourceType = this.props.resourceTypes.getFirstById(resource.resource_type_id); switch (resourceType?.slug) { case "password-string": case "v5-password-string": return this.translate("Password"); case "password-and-description": case "v5-default": return this.translate("Password and Note"); case "password-description-totp": case "v5-default-with-totp": return this.translate("Password, Note and TOTP"); case "totp": case "v5-totp-standalone": return this.translate("TOTP"); case "v5-custom-fields": return this.translate("Custom fields"); case "v5-note": return this.translate("Note"); default: return this.translate("Resource"); } } /** * Handle when the user copies the permalink. */ async handlePermalinkClick() { const baseUrl = this.props.context.userSettings.getTrustedDomain(); const permalink = `${baseUrl}/app/passwords/view/${this.resource.id}`; await this.props.clipboardContext.copy(permalink, this.translate("The permalink has been copied to clipboard.")); } /** * Handle close sidebar click */ handleCloseClick() { this.props.resourceWorkspaceContext.onLockDetail(); } /** * Handles the click on the display upgrade button. */ handleDisplayUpgradeClick() { this.setState({ displayUpgrade: !this.state.displayUpgrade }); } /** * Handle upgrade resource */ async handleUpgradeClick() { const canUpgradeResource = this.canUpgradeResource(); if (canUpgradeResource) { await this.upgradeResource(); } else { this.displayActionAborted(); } } /** * Can upgrade resource * @return {boolean} */ canUpgradeResource() { const isMetadataSharedKeyEnforced = !this.props.metadataKeysSettings?.allowUsageOfPersonalKeys; const isPersonalResource = this.resource.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; } /** * Display action aborted */ displayActionAborted() { this.props.dialogContext.open(ActionAbortedMissingMetadataKeys); } /** * Upgrade the resource */ async upgradeResource() { this.setState({ processing: true }); try { const resourceFormEntity = await this.createResourceFormEntity(); resourceFormEntity.upgradeToV5(); await this.save(resourceFormEntity); } catch (error) { await this.handleSaveError(error); } finally { this.setState({ processing: false }); } } /** * Create resource form entity * @returns {Promise<ResourceFormEntity>} */ async createResourceFormEntity() { const resourceDto = { ...this.resource }; resourceDto.secret = await this.getDecryptedSecret(); return new ResourceFormEntity(resourceDto, { resourceTypes: this.props.resourceTypes }); } /** * Get the decrypted secret associated to the resource * @returns {Promise<void>} */ async getDecryptedSecret() { return await this.props.context.port.request("passbolt.secret.find-by-resource-id", this.resource.id); } /** * Save the resource * @param {ResourceFormEntity} resource * @returns {Promise<void>} */ async save(resource) { await this.updateResource(resource); await this.handleSaveSuccess(); } /** * Update the resource * @param {ResourceFormEntity} resource * @returns {Promise<void>} */ async updateResource(resource) { const resourceDto = resource.toResourceDto(); const secretDto = resource.toSecretDto(); await this.props.context.port.request("passbolt.resources.update", resourceDto, secretDto); } /** * Handle save operation success. * @returns {Promise<void>} */ async handleSaveSuccess() { await this.props.actionFeedbackContext.displaySuccess(this.translate("The resource has been updated successfully")); } /** * Handle save operation error. * @param {object} error The returned error */ async handleSaveError(error) { // It can happen when the user has closed the passphrase entry dialog by instance. if (error?.name === "UserAbortsOperationError" || error?.name === "UntrustedMetadataKeyError") { console.warn(error); return; } await this.props.actionFeedbackContext.displayError(error.message); } /** * Get the resource selected * @returns {*} */ get resource() { return this.props.resourceWorkspaceContext.details.resource; } /** * Is TOTP resource * @return {boolean} */ get isStandaloneTotpResource() { return this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.isStandaloneTotp(); } /** * Is password resource * @return {boolean} */ get isPasswordResources() { return this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.hasPassword(); } /** * Is totp resource * @return {boolean} */ get isTotpResources() { return this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.hasTotp(); } /** * Has description * @return {boolean} */ get hasDescription() { return this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.hasMetadataDescription(); } /* * Is resource with secure note * @return {boolean} */ get hasSecureNote() { return this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.hasSecretDescription(); } /* * Is resource has custom fields * @return {boolean} */ get hasCustomFields() { return ( this.props.resourceTypes?.getFirstById(this.resource.resource_type_id)?.hasCustomFields() && this.resource.metadata.custom_fields?.length > 0 ); } /** * Checks if the resource has multiple URIs. * @returns {boolean} True if the resource has more than one URI, false otherwise. */ get hasMultipleUris() { return this.resource.metadata.uris?.length > 1; } /** * Should display the upgrade resource section * @returns {boolean} */ get shouldDisplayUpgradeResource() { const resourceType = this.props.resourceTypes?.getFirstById(this.resource.resource_type_id); const v5ResourceTypeSlug = V4_TO_V5_RESOURCE_TYPE_MAPPING[resourceType?.slug]; return ( this.canUpdate && this.props.metadataTypeSettings?.allowV4V5Upgrade && resourceType?.isV4() && this.props.resourceTypes.hasOneWithSlug(v5ResourceTypeSlug) ); } /** * Can update the resource */ get canUpdate() { return this.resource.permission.type >= 7; } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } /** * Renders the "Details" section tab of a resource. * @returns {JSX} */ renderResourceDetail() { const canUseTags = this.props.context.siteSettings.canIUse("tags") && this.props.rbacContext.canIUseAction(uiActions.TAGS_USE); const canViewShare = this.props.rbacContext.canIUseAction(uiActions.SHARE_VIEW_LIST); const canSeeComments = this.props.rbacContext.canIUseAction(uiActions.RESOURCES_SEE_COMMENTS); return ( <> {this.isPasswordResources && <DisplayResourceDetailsPassword />} {this.isTotpResources && <DisplayResourceDetailsTotp isStandaloneTotp={this.isStandaloneTotpResource} />} {this.hasCustomFields && <DisplayResourceDetailsCustomFields />} {this.hasSecureNote && <DisplayResourceDetailsNote />} {this.hasMultipleUris && <DisplayResourceDetailsURIs />} {canViewShare && <DisplayResourceDetailsPermission />} <DisplayResourceDetailsInformation /> {this.hasDescription && <DisplayResourceDetailsDescription />} {canUseTags && <DisplayResourceDetailsTag />} {canSeeComments && <DisplayResourceDetailsComment />} </> ); } /** * Render the component * @returns {JSX} */ render() { const canUseAuditLog = (this.props.context.siteSettings.canIUse("auditLog") || this.props.context.siteSettings.canIUse("audit_log")) && // @deprecated remove with v4 this.props.rbacContext.canIUseAction(uiActions.RESOURCES_SEE_ACTIVITIES); return ( <div className="sidebar resource"> <div className={`sidebar-header ${canUseAuditLog ? "" : "with-separator"}`}> <ResourceIcon resource={this.resource} /> <div className="title-area"> <h3> <div className="title-wrapper"> <span className="name">{this.resource.metadata.name}</span> </div> <span className="subtitle">{this.subtitle}</span> </h3> <button type="button" className="title-link button-transparent inline" title={this.translate("Copy the link to this password")} onClick={this.handlePermalinkClick} > <LinkSVG /> <span className="visuallyhidden"> <Trans>Copy the link to this password</Trans> </span> </button> </div> </div> {this.shouldDisplayUpgradeResource && ( <div className="section-card"> <div className="card"> <button type="button" className="title no-border" onClick={this.handleDisplayUpgradeClick}> <ArrowBigUpDashSVG /> <span className="text ellipsis"> <Trans>Resource upgrade available</Trans> </span> {this.state.displayUpgrade ? ( <CaretDownSVG className="caret-down" /> ) : ( <CaretRightSVG className="caret-right" /> )} </button> {this.state.displayUpgrade && ( <div className="content"> <p> <Trans>Upgrade for security improvements and new features.</Trans> </p> <div className="actions-wrapper"> <a className="link" href="https://www.passbolt.com/blog/the-road-to-passbolt-v5-encrypted-metadata-and-other-core-security-changes-2" target="_blank" rel="noopener noreferrer" > <span className="ellipsis"> <Trans>Learn more</Trans> </span> </a> <button disabled={this.state.processing} type="button" onClick={this.handleUpgradeClick}> <span className="ellipsis"> <Trans>Upgrade</Trans> </span> </button> </div> </div> )} </div> </div> )} <div className="sidebar-content"> {!canUseAuditLog ? ( this.renderResourceDetail() ) : ( <Tabs activeTabName="Details"> <Tab key="Details" name={this.props.t("Details")} type="Details"> {this.renderResourceDetail()} </Tab> <Tab key="Activity" name={this.props.t("Activity")} type="Activity"> <DisplayResourceDetailsActivity /> </Tab> </Tabs> )} </div> </div> ); } } DisplayResourceDetails.propTypes = { context: PropTypes.any, // The application context rbacContext: PropTypes.any, // The role based access control context dialogContext: PropTypes.any, // The dialog context resourceWorkspaceContext: PropTypes.object, resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection actionFeedbackContext: PropTypes.any, // The action feedback context metadataTypeSettings: PropTypes.instanceOf(MetadataTypesSettingsEntity), // The metadata type settings metadataKeysSettings: PropTypes.instanceOf(MetadataKeysSettingsEntity), // The metadata key settings clipboardContext: PropTypes.object, // the clipboard service t: PropTypes.func, // The translation function }; export default withAppContext( withDialog( withRbac( withMetadataTypesSettingsLocalStorage( withMetadataKeysSettingsLocalStorage( withResourceTypesLocalStorage( withActionFeedback(withClipboard(withResourceWorkspace(withTranslation("common")(DisplayResourceDetails)))), ), ), ), ), ), );