UNPKG

passbolt-styleguide

Version:

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

1,019 lines (943 loc) 37.2 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 3.2.0 */ import React from "react"; import Transition from "react-transition-group/Transition"; import PropTypes from "prop-types"; import { Trans, withTranslation } from "react-i18next"; import { withRouter } from "react-router-dom"; import SpinnerSVG from "../../../img/svg/spinner.svg"; import { uiActions } from "../../../shared/services/rbacs/uiActionEnumeration"; import { withRbac } from "../../../shared/context/Rbac/RbacContext"; import HiddenPassword from "../../../shared/components/Password/HiddenPassword"; import { withAppContext } from "../../../shared/context/AppContext/AppContext"; import Totp from "../../../shared/components/Totp/Totp"; import { TotpCodeGeneratorService } from "../../../shared/services/otp/TotpCodeGeneratorService"; import sanitizeUrl, { urlProtocols } from "../../../react-extension/lib/Sanitize/sanitizeUrl"; import { resourceLinkAuthorizedProtocols } from "../../../react-extension/contexts/ResourceWorkspaceContext"; import { withResourceTypesLocalStorage } from "../../../shared/context/ResourceTypesLocalStorageContext/ResourceTypesLocalStorageContext"; import ResourceTypesCollection from "../../../shared/models/entity/resourceType/resourceTypesCollection"; import CaretDownSVG from "../../../img/svg/caret_down.svg"; import CaretRightSVG from "../../../img/svg/caret_right.svg"; import CaretLeftSVG from "../../../img/svg/caret_left.svg"; import GoSVG from "../../../img/svg/go.svg"; import CopySVG from "../../../img/svg/copy.svg"; import HealthCheckSuccessSvg from "../../../img/svg/healthcheck_success.svg"; import EyeCloseSVG from "../../../img/svg/eye_close.svg"; import EyeOpenSVG from "../../../img/svg/eye_open.svg"; import TimerSVG from "../../../img/svg/timer.svg"; import ClipboardServiceWorkerService from "../../../shared/services/serviceWorker/clipboard/clipboardServiceWorkerService"; const CLIPBOARD_TEMPORARY_CONTENT_FLUSH_DELAY_IN_SECOND = 30; /** * Default display time of error message in ms. * @type {number} */ const DEFAULT_ERROR_DISPLAY_TIME_IN_MS = 5000; const TRANSITION_STATES = { COPY_LOGIN_STATE_DEFAULT: "copy_login_state_default", COPY_LOGIN_STATE_PROCESSING: "copy_login_state_processing", COPY_LOGIN_STATE_DONE: "copy_login_state_done", COPY_PASSWORD_STATE_DEFAULT: "copy_password_state_default", COPY_PASSWORD_STATE_PROCESSING: "copy_password_state_processing", COPY_PASSWORD_STATE_DONE: "copy_password_state_done", COPY_TOTP_STATE_DEFAULT: "copy_totp_state_default", COPY_TOTP_STATE_PROCESSING: "copy_totp_state_processing", COPY_TOTP_STATE_DONE: "copy_totp_state_done", PASSWORD_DECRYPTING: "password_decrypting", PASSWORD_NOT_DECRYPTING: "password_not_decrypting", TOTP_DECRYPTING: "totp_decrypting", TOTP_NOT_DECRYPTING: "totp_not_decrypting", }; class ResourceViewPage extends React.Component { constructor(props) { super(props); this.state = this.initState(); this.initEventHandlers(); this.loadResource(); this.currentTimeout = null; this.clipboardServiceWorkerService = new ClipboardServiceWorkerService(props.context.port); this.generateNodeRefs(); } initEventHandlers() { this.handleGoBackClick = this.handleGoBackClick.bind(this); this.handleCopyLoginClick = this.handleCopyLoginClick.bind(this); this.handleCopyPasswordClick = this.handleCopyPasswordClick.bind(this); this.handleGoToUrlClick = this.handleGoToUrlClick.bind(this); this.handleUseOnThisTabClick = this.handleUseOnThisTabClick.bind(this); this.handleViewPasswordButtonClick = this.handleViewPasswordButtonClick.bind(this); this.handleCopyTotpClick = this.handleCopyTotpClick.bind(this); this.handlePreviewTotpButtonClick = this.handlePreviewTotpButtonClick.bind(this); this.handleClickAdditionalUrisSection = this.handleClickAdditionalUrisSection.bind(this); this.getNodeRef = this.getNodeRef.bind(this); } initState() { return { resource: {}, passphrase: "", usingOnThisTab: false, copyPasswordState: "default", copyLoginState: "default", copyTotpState: "default", error: "", errorTimeout: null, previewedSecret: null, // The type of previewed secret plaintextSecretDto: null, // The current resource password decrypted isPasswordDecrypting: false, // if the password is decrypting isTotpDecrypting: false, // if the totp is decrypting isOpenAdditionalUris: false, // section additional uris open copiedProperty: null, //the last property copied }; } componentWillUnmount() { if (this.currentTimeout) { clearTimeout(this.currentTimeout); } } /** * Create refs for each Transition * @returns {void} */ generateNodeRefs() { this.nodeRefs = {}; Object.values(TRANSITION_STATES).forEach((stateKey) => { this.nodeRefs[stateKey] = React.createRef(); }); } /** * Get NodeRef or create if it doesn't exist * @param {string} key - The key for the node ref * @returns {React.RefObject<HTMLElement>} The ref object for the node */ getNodeRef(key) { if (!this.nodeRefs[key]) { this.nodeRefs[key] = React.createRef(); } return this.nodeRefs[key]; } /** * Get the translate function * @returns {function(...[*]=)} */ get translate() { return this.props.t; } handleGoBackClick(ev) { ev.preventDefault(); // Additional variables were passed via the history.push state option. if (this.props.location.state) { /* * A specific number of entries to go back to was given in parameter. * It happens when the user comes from the create resource page by instance. */ if (this.props.location.state.goBackEntriesCount) { this.props.history.go(this.props.location.state.goBackEntriesCount); return; } } this.props.history.goBack(); } async loadResource() { const storageData = await this.props.context.storage.local.get(["resources"]); const resource = storageData.resources.find((item) => item.id === this.props.match.params.id); this.setState({ resource }); } resetError() { this.setState({ error: "" }); } async handleCopyLoginClick(event) { event.preventDefault(); this.resetError(); if (!this.state.resource.metadata?.username) { return; } try { this.setState({ copiedProperty: "username", copyLoginState: "processing", copyPasswordState: "default", copyTotpState: "default", }); if (this.currentTimeout) { clearTimeout(this.currentTimeout); } await this.clipboardServiceWorkerService.copy(this.state.resource.metadata?.username); this.setState({ copyLoginState: "done" }); this.currentTimeout = setTimeout(() => { this.setState({ copyLoginState: "default" }); }, 15000); } catch (error) { console.error("An unexpected error occured", error); } } /** * Handle copy password click. */ async handleCopyPasswordClick() { await this.copyPasswordToClipboard(); } /** * Handle preview password button click. */ async handleViewPasswordButtonClick() { await this.togglePreviewPassword(); } /** * Copy the resource password to clipboard. * @returns {Promise<void>} */ async copyPasswordToClipboard() { const isPasswordPreviewed = this.isPasswordPreviewed(); let plaintextSecretDto; this.resetError(); this.setState({ copyPasswordState: "processing" }); if (isPasswordPreviewed) { plaintextSecretDto = this.state.plaintextSecretDto; } else { try { plaintextSecretDto = await this.decryptResourceSecret(this.state.resource.id); } catch (error) { if (error.name !== "UserAbortsOperationError") { return; } } finally { this.setState({ copyPasswordState: "default" }); } } if (!plaintextSecretDto) { this.setState({ copyPasswordState: "default" }); return; } if (!plaintextSecretDto.password?.length) { this.displayTemporarilyError(this.translate("The password is empty and cannot be copied to clipboard.")); this.setState({ copyPasswordState: "default" }); return; } await this.clipboardServiceWorkerService.copyTemporarily(plaintextSecretDto.password); const newState = { copyLoginState: "default", copyPasswordState: "done", copyTotpState: "default", copiedProperty: null, }; this.setState(newState, () => { //ensure it refreshes the animation after another click on the same property this.setState({ copiedProperty: "password" }); }); if (this.currentTimeout) { clearTimeout(this.currentTimeout); } this.currentTimeout = setTimeout(() => { this.setState({ copyPasswordState: "default", copiedProperty: null }); }, CLIPBOARD_TEMPORARY_CONTENT_FLUSH_DELAY_IN_SECOND * 1000); } /** * Toggle preview password * @returns {Promise<void>} */ async togglePreviewPassword() { const isPasswordPreviewed = this.isPasswordPreviewed(); this.hidePreviewedSecret(); if (!isPasswordPreviewed) { await this.previewPassword(); } } /** * Hide the previewed resource secret. */ hidePreviewedSecret() { this.setState({ plaintextSecretDto: null, previewedSecret: null }); } /** * Preview password * @returns {Promise<void>} */ async previewPassword() { const previewedSecret = "password"; let plaintextSecretDto; this.setState({ error: "", isPasswordDecrypting: true }); try { plaintextSecretDto = await this.decryptResourceSecret(this.state.resource.id); } catch (error) { if (error.name !== "UserAbortsOperationError") { return; } } finally { this.setState({ isPasswordDecrypting: false }); } if (!plaintextSecretDto) { return; } if (!plaintextSecretDto.password?.length) { this.displayTemporarilyError(this.translate("The password is empty and cannot be previewed.")); return; } this.setState({ plaintextSecretDto, previewedSecret }); } /** * Display error temporarily. * @param {string} error The error message * @param {number} time The time to persist the error. */ displayTemporarilyError(error, time = DEFAULT_ERROR_DISPLAY_TIME_IN_MS) { clearTimeout(this.state.errorTimeout); const errorTimeout = setTimeout(() => this.setState({ error: "" }), time); this.setState({ errorTimeout, error }); } /** * Decrypt the resource secret * @param {string} resourceId The target resource id * @returns {Promise<object>} The secret in plaintext format * @throw UserAbortsOperationError If the user cancel the operation */ decryptResourceSecret(resourceId) { return this.props.context.port.request("passbolt.secret.find-by-resource-id", resourceId); } /** * Handle copy totp * @return {Promise<void>} */ async handleCopyTotpClick() { let plaintextSecretDto; const isTotpPreviewed = this.isTotpPreviewed(); this.resetError(); this.setState({ copyTotpState: "processing" }); if (isTotpPreviewed) { plaintextSecretDto = this.state.plaintextSecretDto; } else { try { plaintextSecretDto = await this.decryptResourceSecret(this.state.resource.id); } catch (error) { if (error.name !== "UserAbortsOperationError") { return; } } finally { this.setState({ copyTotpState: "default" }); } } if (!plaintextSecretDto) { this.setState({ copyTotpState: "default" }); return; } if (!plaintextSecretDto.totp) { this.displayTemporarilyError(this.translate("The TOTP is empty and cannot be copied to clipboard.")); this.setState({ copyTotpState: "default" }); return; } const code = TotpCodeGeneratorService.generate(plaintextSecretDto.totp); await this.clipboardServiceWorkerService.copyTemporarily(code); const newState = { copyLoginState: "default", copyTotpState: "done", copyPasswordState: "default", copiedProperty: null, }; this.setState(newState, () => { //ensure it refreshes the animation after another click on the same property this.setState({ copiedProperty: "totp" }); }); if (this.currentTimeout) { clearTimeout(this.currentTimeout); } this.currentTimeout = setTimeout(() => { this.setState({ copyTotpState: "default", copiedProperty: null }); }, CLIPBOARD_TEMPORARY_CONTENT_FLUSH_DELAY_IN_SECOND * 1000); } /** * Handle preview totp button click. */ async handlePreviewTotpButtonClick() { const isTotpPreviewed = this.isTotpPreviewed(); this.hidePreviewedSecret(); if (!isTotpPreviewed) { await this.previewTotp(); } } /** * Preview totp * @returns {Promise<void>} */ async previewTotp() { const previewedSecret = "totp"; let plaintextSecretDto; this.setState({ error: "", isTotpDecrypting: true }); try { plaintextSecretDto = await this.decryptResourceSecret(this.state.resource.id); } catch (error) { if (error.name !== "UserAbortsOperationError") { return; } } finally { this.setState({ isTotpDecrypting: false }); } if (!plaintextSecretDto) { return; } if (!plaintextSecretDto.totp) { this.displayTemporarilyError(this.translate("The TOTP is empty and cannot be previewed.")); return; } this.setState({ plaintextSecretDto, previewedSecret }); } handleGoToUrlClick(event) { const primaryUri = this.state.resource.metadata?.uris?.[0]; this.resetError(); if (!this.sanitizeResourceUrl(primaryUri)) { event.preventDefault(); } } async handleUseOnThisTabClick(event) { event.preventDefault(); this.setState({ usingOnThisTab: true }); try { await this.props.context.port.request( "passbolt.quickaccess.use-resource-on-current-tab", this.state.resource.id, this.props.context.getOpenerTabId(), ); if (this.props.context.getDetached()) { await this.props.context.port.request("passbolt.active-tab.close"); } else { await this.props.context.closeWindow(); } } catch (error) { if (error && error.name === "UserAbortsOperationError") { this.setState({ usingOnThisTab: false }); } else { console.error("An error occured", error); this.setState({ usingOnThisTab: false, error: this.props.t("Unable to use the password on this page. Copy and paste the information instead."), }); } } } /** * Handle click on additional uris */ handleClickAdditionalUrisSection() { this.setState({ isOpenAdditionalUris: !this.state.isOpenAdditionalUris }); } /** * Sanitize resource url * @param url * @returns {string|boolean|*} */ sanitizeResourceUrl(url) { return sanitizeUrl(url, { whiteListedProtocols: resourceLinkAuthorizedProtocols, defaultProtocol: urlProtocols.HTTPS, }); } /** * Check if the password is previewed * @returns {boolean} */ isPasswordPreviewed() { return this.state.previewedSecret === "password"; } /** * Check if the totp is previewed * @returns {boolean} */ isTotpPreviewed() { return this.state.previewedSecret === "totp"; } /** * Returns true if the logged in user can use the preview password capability. * @returns {boolean} */ get canPreviewSecret() { return ( this.props.context.siteSettings.canIUse("previewPassword") && this.props.rbacContext.canIUseAction(uiActions.SECRETS_PREVIEW) ); } /** * Is TOTP resource * @return {boolean} */ get isTotpResources() { return ( Boolean(this.state.resource.resource_type_id) && this.props.resourceTypes?.getFirstById(this.state.resource.resource_type_id)?.hasTotp() ); } /** * Is standalone TOTP resource * @return {boolean} */ get isStandaloneTotpResource() { return ( Boolean(this.state.resource.resource_type_id) && this.props.resourceTypes?.getFirstById(this.state.resource.resource_type_id)?.isStandaloneTotp() ); } render() { const primaryUri = this.state.resource.metadata?.uris?.[0]; const additionalUris = this.state.resource.metadata?.uris?.slice(1); const isPasswordPreviewed = this.isPasswordPreviewed(); const isTotpPreviewed = this.isTotpPreviewed(); const canCopySecret = this.props.rbacContext.canIUseAction(uiActions.SECRETS_COPY); return ( <div className="resource item-browse"> <div className="back-link"> <a href="#" className="primary-action" onClick={this.handleGoBackClick}> <CaretLeftSVG /> <span className="primary-action-title">{this.state.resource.metadata?.name}</span> </a> <a href={`${this.props.context.userSettings.getTrustedDomain()}/app/passwords/view/${this.props.match.params.id}`} className="secondary-action button-transparent button" target="_blank" rel="noopener noreferrer" title={this.translate("View it in passbolt")} > <GoSVG /> <span className="visually-hidden"> <Trans>Edit in passbolt</Trans> </span> </a> </div> <ul className="properties"> {!this.isStandaloneTotpResource && ( <> <li className="property"> <div className="information"> <span className="property-name"> <Trans>Username</Trans> </span> {this.state.resource.metadata?.username && ( <a href="#" role="button" className="property-value" onClick={this.handleCopyLoginClick}> {this.state.resource.metadata?.username} </a> )} {!this.state.resource.metadata?.username && ( <span className="property-value empty"> <Trans>no username provided</Trans> </span> )} </div> <a role="button" className={`button button-transparent property-action ${!this.state.resource.metadata?.username ? "disabled" : ""}`} onClick={this.handleCopyLoginClick} title={this.translate("Copy to clipboard")} > <Transition in={this.state.copyLoginState === "default"} appear={false} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.COPY_LOGIN_STATE_DEFAULT)} > {(status) => ( <span className={`transition fade-${status} ${this.state.copyLoginState !== "default" ? "visually-hidden" : ""}`} > <CopySVG /> </span> )} </Transition> <Transition in={this.state.copyLoginState === "processing"} appear={true} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.COPY_LOGIN_STATE_PROCESSING)} > {(status) => ( <span className={`transition fade-${status} ${this.state.copyLoginState !== "processing" ? "visually-hidden" : ""}`} > <SpinnerSVG /> </span> )} </Transition> <Transition in={this.state.copyLoginState === "done"} appear={true} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.COPY_LOGIN_STATE_DONE)} > {(status) => ( <span className={`transition fade-${status} ${this.state.copyLoginState !== "done" ? "visually-hidden" : ""}`} > <HealthCheckSuccessSvg /> </span> )} </Transition> <span className="visually-hidden"> <Trans>Copy to clipboard</Trans> </span> </a> </li> <li className="property"> <div className="information"> <span className="property-name"> <Trans>Password</Trans> </span> <div className="password-wrapper"> <div className={`property-value secret secret-password ${isPasswordPreviewed ? "" : "secret-copy"}`} title={ isPasswordPreviewed ? this.state.plaintextSecretDto?.password : this.translate("Click to copy") } > <HiddenPassword canClick={canCopySecret} preview={this.state.plaintextSecretDto?.password} onClick={this.handleCopyPasswordClick} /> </div> {this.canPreviewSecret && ( <button onClick={this.handleViewPasswordButtonClick} className="password-view inline button-transparent" disabled={this.state.isPasswordDecrypting} > <Transition in={!this.state.isPasswordDecrypting} appear={false} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.PASSWORD_NOT_DECRYPTING)} > {(status) => ( <span className={`transition fade-${status} ${this.state.isPasswordDecrypting ? "visually-hidden" : ""}`} > {isPasswordPreviewed ? <EyeCloseSVG /> : <EyeOpenSVG />} </span> )} </Transition> <Transition in={this.state.isPasswordDecrypting} appear={true} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.PASSWORD_DECRYPTING)} > {(status) => ( <span className={`transition fade-${status} ${!this.state.isPasswordDecrypting ? "visually-hidden" : ""}`} > <SpinnerSVG /> </span> )} </Transition> <span className="visually-hidden"> <Trans>View</Trans> </span> </button> )} </div> </div> {canCopySecret && ( <> <a role="button" className="button button-transparent property-action copy-password" onClick={this.handleCopyPasswordClick} title={this.translate("Copy to clipboard")} > <Transition in={this.state.copyPasswordState === "default"} appear={false} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.COPY_PASSWORD_STATE_DEFAULT)} > {(status) => ( <span className={`transition fade-${status} ${this.state.copyPasswordState !== "default" ? "visually-hidden" : ""}`} > <CopySVG /> </span> )} </Transition> <Transition in={this.state.copyPasswordState === "processing"} appear={true} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.COPY_PASSWORD_STATE_PROCESSING)} > {(status) => ( <span className={`transition fade-${status} ${this.state.copyPasswordState !== "processing" ? "visually-hidden" : ""}`} > <SpinnerSVG /> </span> )} </Transition> <Transition in={this.state.copyPasswordState === "done"} appear={true} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.COPY_PASSWORD_STATE_DONE)} > {(status) => ( <span className={`transition fade-${status} ${this.state.copyPasswordState !== "done" ? "visually-hidden" : ""}`} > <HealthCheckSuccessSvg /> </span> )} </Transition> <span className="visually-hidden"> <Trans>Copy to clipboard</Trans> </span> </a> {this.state.copiedProperty === "password" && ( <TimerSVG style={{ "--timer-duration": `${CLIPBOARD_TEMPORARY_CONTENT_FLUSH_DELAY_IN_SECOND}s`, }} /> )} </> )} </li> </> )} {this.isTotpResources && ( <li className="property"> <div className="information"> <span className="property-name"> <Trans>TOTP</Trans> </span> <div className="totp-wrapper"> <div className={`property-value secret secret-totp ${isTotpPreviewed ? "" : "secret-copy"}`} title={isTotpPreviewed ? this.state.plaintextSecretDto?.totp : this.translate("Click to copy")} > {isTotpPreviewed && ( <Totp totp={this.state.plaintextSecretDto?.totp} canClick={canCopySecret} onClick={this.handleCopyTotpClick} /> )} {!isTotpPreviewed && ( <button type="button" className="no-border" onClick={this.handleCopyTotpClick} disabled={!canCopySecret} > <span>Copy TOTP to clipboard</span> </button> )} </div> {this.canPreviewSecret && ( <button onClick={this.handlePreviewTotpButtonClick} className="totp-view inline button-transparent" disabled={this.state.isTotpDecrypting} > <Transition in={!this.state.isTotpDecrypting} appear={false} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.TOTP_NOT_DECRYPTING)} > {(status) => ( <span className={`transition fade-${status} ${this.state.isTotpDecrypting ? "visually-hidden" : ""}`} > {isTotpPreviewed ? <EyeCloseSVG /> : <EyeOpenSVG />} </span> )} </Transition> <Transition in={this.state.isTotpDecrypting} appear={true} timeout={500} nodeRef={this.getNodeRef(TRANSITION_STATES.TOTP_DECRYPTING)} > {(status) => ( <span className={`transition fade-${status} ${!this.state.isTotpDecrypting ? "visually-hidden" : ""}`} > <SpinnerSVG /> </span> )} </Transition> <span className="visually-hidden"> <Trans>View</Trans> </span> </button> )} </div> </div> {canCopySecret && ( <> <a role="button" className="button button-transparent property-action copy-totp" onClick={this.handleCopyTotpClick} title={this.translate("Copy to clipboard")} > <Transition in={this.state.copyTotpState === "default"} appear={false} timeout={500}> {(status) => ( <span className={`transition fade-${status} ${this.state.copyTotpState !== "default" ? "visually-hidden" : ""}`} > <CopySVG /> </span> )} </Transition> <Transition in={this.state.copyTotpState === "processing"} appear={true} timeout={500}> {(status) => ( <span className={`transition fade-${status} ${this.state.copyTotpState !== "processing" ? "visually-hidden" : ""}`} > <SpinnerSVG /> </span> )} </Transition> <Transition in={this.state.copyTotpState === "done"} appear={true} timeout={500}> {(status) => ( <span className={`transition fade-${status} ${this.state.copyTotpState !== "done" ? "visually-hidden" : ""}`} > <HealthCheckSuccessSvg /> </span> )} </Transition> <span className="visually-hidden"> <Trans>Copy to clipboard</Trans> </span> </a> {this.state.copiedProperty === "totp" && ( <TimerSVG style={{ "--timer-duration": `${CLIPBOARD_TEMPORARY_CONTENT_FLUSH_DELAY_IN_SECOND}s`, }} /> )} </> )} </li> )} <li className="property"> <div className="information"> <span className="property-name">URI</span> {primaryUri && this.sanitizeResourceUrl(primaryUri) && ( <a href={this.sanitizeResourceUrl(primaryUri)} role="button" className="property-value" target="_blank" rel="noopener noreferrer" > {primaryUri} </a> )} {primaryUri && !this.sanitizeResourceUrl(primaryUri) && ( <span className="property-value">{primaryUri}</span> )} {!primaryUri && ( <span className="property-value empty"> <Trans>no url provided</Trans> </span> )} </div> <a href={`${this.sanitizeResourceUrl(primaryUri) ? this.sanitizeResourceUrl(primaryUri) : "#"}`} role="button" className={`button button-transparent property-action ${!this.sanitizeResourceUrl(primaryUri) ? "disabled" : ""}`} onClick={this.handleGoToUrlClick} target="_blank" rel="noopener noreferrer" title={this.translate("open in a new tab")} > <GoSVG /> <span className="visually-hidden"> <Trans>Open in new window</Trans> </span> </a> </li> {additionalUris?.length > 0 && ( <li className="property"> <div className="information"> <div className="accordion"> <div className="accordion-header additional-uris" onClick={this.handleClickAdditionalUrisSection}> <button type="button" className="link no-border property-name"> {this.state.isOpenAdditionalUris ? ( <CaretDownSVG className="caret-down" /> ) : ( <CaretRightSVG className="caret-right" /> )} <span> <Trans>Additional URIs</Trans> </span> </button> </div> {this.state.isOpenAdditionalUris && ( <div className="accordion-content"> <div className="list-uris"> {additionalUris.map((uri, index) => { const safeUri = this.sanitizeResourceUrl(uri); if (safeUri) { return ( <a href={safeUri} className="property-value" key={index} target="_blank" rel="noopener noreferrer" > <span className="ellipsis">{uri}</span> </a> ); } return ( <span className="property-value" key={index}> {uri} </span> ); })} </div> </div> )} </div> </div> </li> )} </ul> <div className="submit-wrapper input"> <a href="#" id="popupAction" className={`button primary big full-width ${this.state.usingOnThisTab ? "disabled" : ""}`} role="button" onClick={this.handleUseOnThisTabClick} > {this.state.usingOnThisTab && <SpinnerSVG />} {!this.state.usingOnThisTab && <Trans>Use on this page</Trans>} </a> {this.state.error && <div className="error-message">{this.state.error}</div>} </div> </div> ); } } ResourceViewPage.propTypes = { context: PropTypes.any, // The application context rbacContext: PropTypes.any, // The role based access control context resourceTypes: PropTypes.instanceOf(ResourceTypesCollection), // The resource types collection // Match, location and history props are injected by the withRouter decoration call. match: PropTypes.object, location: PropTypes.object, history: PropTypes.object, t: PropTypes.func, // The translation function }; export default withAppContext( withRbac(withRouter(withResourceTypesLocalStorage(withTranslation("common")(ResourceViewPage)))), );