UNPKG

passbolt-styleguide

Version:

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

723 lines (676 loc) 29.6 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 4.12.0 */ import PropTypes from "prop-types"; import React, { Component } from "react"; import { Trans, withTranslation } from "react-i18next"; import memoize from "memoize-one"; import { withAppContext } from "../../../../shared/context/AppContext/AppContext"; import MetadataSettingsServiceWorkerService from "../../../../shared/services/serviceWorker/metadata/metadataSettingsServiceWorkerService"; import NotifyError from "../../Common/Error/NotifyError/NotifyError"; import { withDialog } from "../../../contexts/DialogContext"; import { withActionFeedback } from "../../../contexts/ActionFeedbackContext"; import { withResourceTypesLocalStorage } from "../../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext"; import ResourceTypesCollection from "../../../../shared/models/entity/resourceType/resourceTypesCollection"; import AnimatedFeedback from "../../../../shared/components/Icons/AnimatedFeedback"; import MetadataKeysServiceWorkerService from "../../../../shared/services/serviceWorker/metadata/metadataKeysServiceWorkerService"; import MigrateMetadataFormEntity from "../../../../shared/models/entity/metadata/migrateMetadataFormEntity"; import MetadataMigrateContentServiceWorkerService from "../../../../shared/services/serviceWorker/metadata/metadataMigrateContentServiceWorkerService"; import ConfirmMigrateMetadataDialog from "./ConfirmMigrateMetadataDialog"; import { createSafePortal } from "../../../../shared/utils/portals"; import InfoSVG from "../../../../img/svg/info.svg"; class DisplayMigrateMetadataAdministration extends Component { /** @type {MigrateMetadataFormEntity} */ formSettings = undefined; /** @type {MetadataTypesSettingsEntity} */ metadataTypesSettings = undefined; /** @type {MetadataKeysCollection} */ metadataKeys = undefined; /** @type {PassboltResponsePaginationHeaderEntity} */ migrationCountDetails = undefined; /** @type {PassboltResponsePaginationHeaderEntity} */ migrationCountDetailsShared = undefined; constructor(props) { super(props); this.formSettings = new MigrateMetadataFormEntity({}); this.metadataSettingsServiceWorkerService = props.metadataSettingsServiceWorkerService ?? new MetadataSettingsServiceWorkerService(props.context.port); this.metadataKeysServiceWorkerService = props.metadataKeysServiceWorkerService ?? new MetadataKeysServiceWorkerService(props.context.port); this.metadataMigrateContentServiceWorkerService = props.metadataMigrateContentServiceWorkerService ?? new MetadataMigrateContentServiceWorkerService(props.context.port); this.state = this.defaultState; this.bindCallbacks(); } /** * Get default state * @returns {Object} */ get defaultState() { return { isReady: false, isProcessing: false, // Is the form processing (loading, submitting). hasAlreadyBeenValidated: false, // True if the form has already been submitted once. hasMigrationRunOnce: false, // True if the migration has been started at least once. settings: this.formSettings.toDto(), }; } /** * Bind callbacks methods */ bindCallbacks() { this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleInputChange = this.handleInputChange.bind(this); this.handleMigrateScopeInputChange = this.handleMigrateScopeInputChange.bind(this); this.runMigration = this.runMigration.bind(this); this.askForMigrationConfirmation = this.askForMigrationConfirmation.bind(this); } /** * ComponentDidMount * Invoked immediately after component is inserted into the tree * @return {void} */ async componentDidMount() { await this.initData(); } async initData() { this.metadataTypesSettings = await this.metadataSettingsServiceWorkerService.findTypesSettings(); this.metadataKeys = await this.metadataKeysServiceWorkerService.findAll(); this.migrationCountDetailsShared = await this.metadataMigrateContentServiceWorkerService.findCountMetadataMigrateResources(true); this.migrationCountDetails = await this.metadataMigrateContentServiceWorkerService.findCountMetadataMigrateResources(); this.setState({ settings: this.formSettings.toDto(), isProcessing: false, isReady: true, }); } /** * Validate form. * @param {object} formSetingsDto The form settings dto store in state, not used but required to ensure the memoized * function is only triggered when the form is updated. * @param {ResourceTypesCollection} resourceTypes The resource types. * @return {EntityValidationError} */ // eslint-disable-next-line no-unused-vars validateForm = memoize((formSettingsDto) => this.formSettings?.validate()); /** * Verify the data health. This intends for administrators, helping them adjust settings to prevent unusual or * problematic situations. By instance enabling a metadata types without active related content types. * @param {object} formSetingsDto The form settings dto store in state, not used but required to ensure the memoized * function is only triggered when the form is updated. * @param {ResourceTypesCollection} resourceTypes The resource types. * @return {EntityValidationError} */ verifyDataHealth = memoize((_, resourceTypes, metadataTypesSettings, metadataKeys) => this.formSettings?.verifyHealth(resourceTypes, metadataTypesSettings, metadataKeys), ); /** * Handle form input changes. * @params {ReactEvent} The react event * @returns {void} */ handleInputChange(event) { if (this.hasAllInputDisabled()) { return; } const { type, checked, value, name } = event.target; const parsedValue = type === "checkbox" ? checked : value; this.setFormPropertyValue(name, parsedValue); } /** * Handle migrate scope form input changes. * @params {ReactEvent} The react event * @returns {void} */ handleMigrateScopeInputChange(event) { const { value, name } = event.target; this.setFormPropertyValue(name, value === "all-content"); } /** * Set a form property value. Trigger the validation if the form has already been submitted once. * @param name * @param parsedValue */ setFormPropertyValue(name, parsedValue) { this.formSettings.set(name, parsedValue, { validate: false }); this.setState({ settings: this.formSettings.toDto() }); } /** * Has missing metadata keys * @return {boolean} */ get hasMissingMetadataKeys() { return this.props.context.loggedInUser.missing_metadata_key_ids?.length > 0; } /** * Should input be disabled? True if state is loading or processing * @returns {boolean} */ hasAllInputDisabled() { return this.state.isProcessing; } /** * Handle form submission that can be trigger when hitting `enter` * @param {Event} event The html event triggering the form submit. */ handleFormSubmit(event) { // Avoid the form to be submitted natively by the browser and avoid a redirect to a broken page. event.preventDefault(); this.askForMigrationConfirmation(); } /** * Returns true if any content migration is not fully done yet. * @returns {boolean} */ get hasPendingMigration() { return ( this.hasPendingResourcesMigration || this.hasPendingFoldersMigration || this.hasPendingCommentsMigration || this.hasPendingTagsMigration ); } /** * Returns the total of resources to be migrated (shared + personal) * @returns {integer} */ get totalResources() { return this.totalSharedResources + this.totalPersonalResources; } /** * Returns the total of shared resources to be migrated * @returns {integer} */ get totalSharedResources() { return this.migrationCountDetailsShared?.count; } /** * Returns the total of personal resources to be migrated * @returns {integer} */ get totalPersonalResources() { return this.migrationCountDetailsPersonal?.count; } /** * Returns the migration details computed based on the admin choice * @returns {object} */ get migrationCountDetailsPersonal() { return { ...this.migrationCountDetails, count: this.migrationCountDetails?.count - this.migrationCountDetailsShared?.count, }; } /** * Returns true if the resources migration is not fully done yet. * @returns {boolean} */ get hasPendingResourcesMigration() { return this.totalResources > 0; } /** * Returns true if the folders migration is not fully done yet. * @returns {boolean} */ get hasPendingFoldersMigration() { return this.totalFolders > 0; } /** * Returns true if the comments migration is not fully done yet. * @returns {boolean} */ get hasPendingTagsMigration() { return this.totalTags > 0; } /** * Returns true if the tags migration is not fully done yet. * @returns {boolean} */ get hasPendingCommentsMigration() { return this.totalComments > 0; } /** * Returns migration status text to display to the user. * @returns {string} */ get migrationStatus() { if (!this.hasPendingMigration) { return this.props.t("Done"); } return this.state.hasMigrationRunOnce ? this.props.t("Pending") : this.props.t("Required"); } /** * Returns true if there are still elements to migrate. * @todo: when other content to migrate, the total count should be adapted. * @returns {boolean} */ hasElementsToMigrate() { return this.totalResources > 0; } /** * Returns true if the form has global error from the health check. * @param {Error} healthIssue * @returns {boolean} */ hasGlobalError(healthIssue) { return healthIssue?.hasError("global_form") || false; } /** * Asks the current user for confirmation before running the metadata migration process. */ askForMigrationConfirmation() { this.props.dialogContext.open(ConfirmMigrateMetadataDialog, { confirm: this.runMigration, cancel: () => {}, }); } /** * Triggers the migration of the content * @returns {Promise<void>} */ async runMigration() { if (this.state.isProcessing) { return; } const validationError = this.validateForm(this.state.settings); const healthWarnings = this.verifyDataHealth( this.state.settings, this.props.resourceTypes, this.metadataTypesSettings, this.metadataKeys, ); if (validationError?.hasErrors() || this.hasGlobalError(healthWarnings)) { this.setState({ hasAlreadyBeenValidated: true }); return; } this.setState({ isProcessing: true }); try { const migrationDetails = this.formSettings.sharedContentOnly ? this.migrationCountDetailsShared : this.migrationCountDetails; await this.metadataMigrateContentServiceWorkerService.migrate(this.formSettings.toDto(), migrationDetails); await this.initData(); if (this.hasElementsToMigrate()) { this.props.actionFeedbackContext.displayWarning(this.props.t("Encrypted metadata were partially migrated.")); } else { this.props.actionFeedbackContext.displaySuccess(this.props.t("The encrypted metadata were migrated.")); } } catch (error) { this.props.dialogContext.open(NotifyError, { error }); } this.setState({ hasAlreadyBeenValidated: true, hasMigrationRunOnce: true, }); } /** * Render the component * @returns {JSX} */ render() { const warnings = this.verifyDataHealth( this.state.settings, this.props.resourceTypes, this.metadataTypesSettings, this.metadataKeys, ); const hasGlobalError = this.hasGlobalError(warnings); const isFeatureBeta = this.props.context.siteSettings.isFeatureBeta("metadata"); const shouldDisplayAWarningBlock = !hasGlobalError && (isFeatureBeta || (!this.hasMissingMetadataKeys && this.hasPendingMigration)); return ( <div className="row"> <div id="migrate-metadata-settings" className="main-column"> <div className="main-content"> <form onSubmit={this.handleFormSubmit} data-testid="submit-form"> <h3 className="title"> <label> <Trans>Migrate metadata</Trans> </label> </h3> <p className="description"> <Trans>Initiate a migration to convert cleartext metadata to encrypted metadata.</Trans> </p> <h4> <Trans>Summary</Trans> </h4> <div className="feedback-card"> {this.state.isReady && this.hasPendingMigration && <AnimatedFeedback name="warning" />} {this.state.isReady && !this.hasPendingMigration && <AnimatedFeedback name="success" />} <div className="migration-status-information"> <ul> <li className="migration-status"> <span className="label"> <Trans>Migration status</Trans> </span> <span className="value">{this.migrationStatus}</span> </li> <li className="migration-resources-count"> <span className="label"> <Trans>Resources</Trans> </span> <span className="value"> {this.hasPendingResourcesMigration ? ( <> {this.props.t("{{count}} to be migrated", { count: this.totalResources })} ( {this.props.t("{{count}} shared resources", { count: this.totalSharedResources })},{" "} {this.props.t("{{count}} personal resources", { count: this.totalPersonalResources })}) </> ) : ( <Trans>All migrated</Trans> )} </span> </li> {/* <li className="migration-folders-count"> <span className="label"><Trans>Folders</Trans></span> <span className="value"> {this.hasPendingFoldersMigration ? <>{this.props.t("{{count}} to be migrated", {count: this.totalFolders})}</> : <Trans>All migrated</Trans> } </span> </li> <li className="migration-tags-count"> <span className="label"><Trans>Tags</Trans></span> <span className="value"> {this.hasPendingCommentsMigration ? <>{this.props.t("{{count}} to be migrated", {count: this.totalTags})}</> : <Trans>All migrated</Trans> } </span> </li> <li className="migration-comments-count"> <span className="label"><Trans>Comments</Trans></span> <span className="value"> {this.hasPendingTagsMigration ? <>{this.props.t("{{count}} to be migrated", {count: this.totalComments})}</> : <Trans>All migrated</Trans> } </span> </li> */} </ul> </div> </div> <h4> <Trans>Items to migrate</Trans> </h4> <div className="togglelist"> <span className={`input toggle-switch form-element ${!hasGlobalError && warnings?.hasError("migrate_resources_to_v5") && "warning"}`} > <input id="migrateResourcesInput" type="checkbox" name="migrate_resources_to_v5" className="toggle-switch-checkbox checkbox" onChange={this.handleInputChange} checked={this.state.settings.migrate_resources_to_v5} disabled={this.hasAllInputDisabled()} /> <label htmlFor="migrateResourcesInput"> <span className="name"> <Trans>Resources:</Trans> </span> <span className="info"> <Trans>Name, Username, URI, Cleartext description.</Trans> </span> {!hasGlobalError && warnings?.hasError("migrate_resources_to_v5") && ( <div className="warning"> {warnings?.hasError("migrate_resources_to_v5", "allow_creation_of_v5_resources") && ( <div className="warning-message"> <Trans>Resource types v5 creation is not allowed.</Trans> </div> )} {warnings?.hasError("migrate_resources_to_v5", "resource_types_v5_deleted") && ( <div className="warning-message"> <Trans> Resources will not be migrated as no content types with encrypted metadata is allowed. </Trans> </div> )} {warnings?.hasError("migrate_resources_to_v5", "resource_types_v5_partially_deleted") && ( <div className="warning-message"> <Trans> Not all resources will be migrated, some corresponding content types are not active. </Trans> </div> )} </div> )} </label> </span> {/* <span className={`input toggle-switch form-element ${!hasGlobalError && warnings?.hasError("migrate_folders_to_v5") && "warning"}`}> <input id="migrateFoldersInput" type="checkbox" name="migrate_folders_to_v5" className={`toggle-switch-checkbox checkbox ${!hasGlobalError && warnings?.hasError("migrate_folders_to_v5") && "warning"}`} onChange={this.handleInputChange} checked={this.state.settings.migrate_folders_to_v5} disabled={this.hasAllInputDisabled()}/> <label htmlFor="migrateFoldersInput"> <span className="name"><Trans>Folders:</Trans></span> <span className="info"><Trans>Name.</Trans></span> {!hasGlobalError && warnings?.hasError("migrate_folders_to_v5") && <div className="warning"> {warnings?.hasError("migrate_folders_to_v5", "allow_v4_v5_upgrade") && <div className="warning-message"><Trans>Folder upgrade from v4 to v5 is not allowed</Trans></div> } {warnings?.hasError("migrate_folders_to_v5", "allow_creation_of_v5_folders") && <div className="warning-message"><Trans>Folder v5 creation is not allowed.</Trans></div> } </div> } </label> </span> <span className={`input toggle-switch form-element ${!hasGlobalError && warnings?.hasError("migrate_tags_to_v5") && "warning"}`}> <input id="migrateTagsInput" type="checkbox" name="migrate_tags_to_v5" className={`toggle-switch-checkbox checkbox ${!hasGlobalError && warnings?.hasError("migrate_tags_to_v5") && "warning"}`} onChange={this.handleInputChange} checked={this.state.settings.migrate_tags_to_v5} disabled={this.hasAllInputDisabled()}/> <label htmlFor="migrateTagsInput"> <span className="name"><Trans>Tags:</Trans></span> <span className="info"><Trans>Slug.</Trans></span> {!hasGlobalError && warnings?.hasError("migrate_tags_to_v5") && <div className="warning"> {warnings?.hasError("migrate_tags_to_v5", "allow_v4_v5_upgrade") && <div className="warning-message"><Trans>Tag upgrade from v4 to v5 is not allowed</Trans></div> } {warnings?.hasError("migrate_tags_to_v5", "allow_creation_of_v5_tags") && <div className="warning-message"><Trans>Tag v5 creation is not allowed.</Trans></div> } </div> } </label> </span> <span className={`input toggle-switch form-element ${!hasGlobalError && warnings?.hasError("migrate_comments_to_v5") && "warning"}`}> <input id="migrateCommentsInput" type="checkbox" name="migrate_comments_to_v5" className={`toggle-switch-checkbox checkbox ${!hasGlobalError && warnings?.hasError("migrate_comments_to_v5") && "warning"}`} onChange={this.handleInputChange} checked={this.state.settings.migrate_comments_to_v5} disabled={this.hasAllInputDisabled()}/> <label htmlFor="migrateCommentsInput"> <span className="name"><Trans>Comments</Trans></span> {!hasGlobalError && warnings?.hasError("migrate_comments_to_v5") && <div className="warning"> {warnings?.hasError("migrate_comments_to_v5", "allow_v4_v5_upgrade") && <div className="warning-message"><Trans>Comment upgrade from v4 to v5 is not allowed</Trans></div> } {warnings?.hasError("migrate_comments_to_v5", "allow_creation_of_v5_comments") && <div className="warning-message"><Trans>Comment v5 creation is not allowed.</Trans></div> } </div> } </label> </span> */} </div> <h4> <Trans>Migration scope</Trans> </h4> <div className="radiolist-alt"> <div className={`input radio ${this.state.settings.migrate_personal_content && "checked"} ${this.hasAllInputDisabled() && "disabled"}`} > <input type="radio" value="all-content" onChange={this.handleMigrateScopeInputChange} name="migrate_personal_content" checked={this.state.settings.migrate_personal_content} id="migrateScopeAllContentInput" disabled={this.hasAllInputDisabled()} /> <label htmlFor="migrateScopeAllContentInput"> <span className="name"> <Trans>All content</Trans> </span> <span className="info"> <Trans>All resources including the private ones.</Trans> </span> </label> </div> <div className={`input radio ${!this.state.settings.migrate_personal_content && "checked"} ${this.hasAllInputDisabled() && "disabled"}`} > <input type="radio" value="shared-only" onChange={this.handleMigrateScopeInputChange} name="migrate_personal_content" checked={!this.state.settings.migrate_personal_content} id="migrateScopeSharedContentInput" disabled={this.hasAllInputDisabled()} /> <label htmlFor="migrateScopeSharedContentInput"> <span className="name"> <Trans>Shared content only</Trans> </span> <span className="info"> <Trans>Only shared resources are migrated.</Trans> </span> </label> </div> </div> </form> </div> {hasGlobalError && this.hasPendingMigration && ( <div className="error message"> <div> <Trans>No active metadata keys available.</Trans> </div> </div> )} {!hasGlobalError && this.hasMissingMetadataKeys && ( <div className="error message"> <div> <Trans>You lack access to the shared metadata key.</Trans>&nbsp; <Trans>Please ask another administrator to share it with you.</Trans> </div> </div> )} {shouldDisplayAWarningBlock && ( <div className="warning message"> {isFeatureBeta && ( <div className="form-banner"> <b> <Trans>Warning:</Trans> </b>{" "} <Trans> Your current API version includes beta support for encrypted metadata and new resource types. </Trans>{" "} <Trans> To ensure stability and avoid potential issues, upgrade to the latest version before enabling these features. </Trans> </div> )} {!this.hasMissingMetadataKeys && this.hasPendingMigration && ( <div> <p> <b> <Trans>Warning:</Trans> </b>{" "} <Trans> If you have integrations, you will have to make sure they are updated before triggering the migration. </Trans> </p> </div> )} </div> )} </div> <div className="actions-wrapper"> <button type="button" className="button primary" disabled={this.state.isProcessing || hasGlobalError || this.hasMissingMetadataKeys} onClick={this.handleFormSubmit} > <span> <Trans>Migrate</Trans> </span> </button> </div> {createSafePortal( <div className="sidebar-help-section"> <h3> <Trans>Need help?</Trans> </h3> <p> <Trans> For more information about the content type support and migration, checkout the dedicated page on the official website. </Trans> </p> <a className="button" target="_blank" rel="noopener noreferrer" href="https://passbolt.com/docs/admin/metadata-encryption/migrate-metadata/" > <InfoSVG /> <span> <Trans>Read the documentation</Trans> </span> </a> </div>, document.getElementById("administration-help-panel"), )} </div> ); } } DisplayMigrateMetadataAdministration.propTypes = { context: PropTypes.object, // Defined the expected type for context actionFeedbackContext: PropTypes.object, // The action feedback context dialogContext: PropTypes.object, // The dialog context createPortal: PropTypes.func, // The mocked create portal react dom primitive if test needed. metadataSettingsServiceWorkerService: PropTypes.object, // The bext service that handle metadata settings. metadataKeysServiceWorkerService: PropTypes.object, metadataMigrateContentServiceWorkerService: PropTypes.object, resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection t: PropTypes.func, // translation function }; export default withAppContext( withActionFeedback( withDialog( withResourceTypesLocalStorage(withDialog(withTranslation("common")(DisplayMigrateMetadataAdministration))), ), ), );