/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Any modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { Component, ReactElement } from 'react'; import { EuiButton, EuiCopy, EuiFlexGroup, EuiSpacer, EuiFlexItem, EuiForm, EuiFormRow, EuiIconTip, EuiLoadingSpinner, EuiRadioGroup, EuiSwitch, EuiSwitchEvent, } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; import { HttpStart } from 'opensearch-dashboards/public'; import { i18n } from '@osd/i18n'; import { shortenUrl } from '../lib/url_shortener'; import { UrlParamExtension } from '../types'; interface Props { allowShortUrl: boolean; isEmbedded?: boolean; objectId?: string; objectType: string; shareableUrl?: string; basePath: string; post: HttpStart['post']; urlParamExtensions?: UrlParamExtension[]; } export enum ExportUrlAsType { EXPORT_URL_AS_SAVED_OBJECT = 'savedObject', EXPORT_URL_AS_SNAPSHOT = 'snapshot', } interface UrlParams { [extensionName: string]: { [queryParam: string]: boolean; }; } interface State { exportUrlAs: ExportUrlAsType; useShortUrl: boolean; isCreatingShortUrl: boolean; url?: string; shortUrlErrorMsg?: string; urlParams?: UrlParams; } export class UrlPanelContent extends Component { private mounted?: boolean; private shortUrlCache?: string; constructor(props: Props) { super(props); this.shortUrlCache = undefined; this.state = { exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, useShortUrl: false, isCreatingShortUrl: false, url: '', }; } public componentWillUnmount() { window.removeEventListener('hashchange', this.resetUrl); this.mounted = false; } public componentDidMount() { this.mounted = true; this.setUrl(); window.addEventListener('hashchange', this.resetUrl, false); } public render() { return ( {this.renderExportAsRadioGroup()} {this.renderUrlParamExtensions()} {this.renderShortUrlSwitch()} {(copy: () => void) => ( {this.props.isEmbedded ? ( ) : ( )} )} ); } private isNotSaved = () => { return this.props.objectId === undefined || this.props.objectId === ''; }; private resetUrl = () => { if (this.mounted) { this.shortUrlCache = undefined; this.setState( { useShortUrl: false, }, this.setUrl ); } }; private updateUrlParams = (url: string) => { const embedUrl = this.props.isEmbedded ? this.makeUrlEmbeddable(url) : url; const extendUrl = this.state.urlParams ? this.getUrlParamExtensions(embedUrl) : embedUrl; return extendUrl; }; private getSavedObjectUrl = () => { if (this.isNotSaved()) { return; } const url = this.getSnapshotUrl(); const parsedUrl = new URL(url); if (!parsedUrl || !parsedUrl.hash) { return; } // Get the application route, after the hash, and remove the #. const parsedAppUrl = new URL(parsedUrl.hash.slice(1), window.location.origin); const formattedAppUrl = new URL(parsedAppUrl.pathname, parsedAppUrl.origin); const formattedUrl = new URL(parsedUrl.pathname, parsedUrl.origin); formattedAppUrl.search = new URLSearchParams({ _g: parsedAppUrl.searchParams.get('_g') ?? '', }).toString(); formattedUrl.hash = formattedAppUrl.toString().substring(formattedAppUrl.origin.length); return this.updateUrlParams(formattedUrl.toString()); }; private getSnapshotUrl = () => { const url = this.props.shareableUrl || window.location.href; return this.updateUrlParams(url); }; private makeUrlEmbeddable = (url: string): string => { const embedParam = '?embed=true'; const urlHasQueryString = url.indexOf('?') !== -1; if (urlHasQueryString) { return url.replace('?', `${embedParam}&`); } return `${url}${embedParam}`; }; private getUrlParamExtensions = (url: string): string => { const { urlParams } = this.state; return urlParams ? Object.keys(urlParams).reduce((urlAccumulator, key) => { const urlParam = urlParams[key]; return urlParam ? Object.keys(urlParam).reduce((queryAccumulator, queryParam) => { const isQueryParamEnabled = urlParam[queryParam]; return isQueryParamEnabled ? queryAccumulator + `&${queryParam}=true` : queryAccumulator; }, urlAccumulator) : urlAccumulator; }, url) : url; }; private makeIframeTag = (url?: string) => { if (!url) { return; } return ``; }; private setUrl = () => { let url; if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { url = this.getSavedObjectUrl(); } else if (this.state.useShortUrl) { url = this.shortUrlCache; } else { url = this.getSnapshotUrl(); } if (this.props.isEmbedded) { url = this.makeIframeTag(url); } this.setState({ url }); }; private handleExportUrlAs = (optionId: string) => { this.setState( { exportUrlAs: optionId as ExportUrlAsType, }, this.setUrl ); }; private handleShortUrlChange = async (evt: EuiSwitchEvent) => { const isChecked = evt.target.checked; if (!isChecked || this.shortUrlCache !== undefined) { this.setState({ useShortUrl: isChecked }, this.setUrl); return; } // "Use short URL" is checked but shortUrl has not been generated yet so one needs to be created. this.createShortUrl(); }; private createShortUrl = async () => { this.setState({ isCreatingShortUrl: true, shortUrlErrorMsg: undefined, }); try { const shortUrl = await shortenUrl(this.getSnapshotUrl(), { basePath: this.props.basePath, post: this.props.post, }); if (this.mounted) { this.shortUrlCache = shortUrl; this.setState( { isCreatingShortUrl: false, useShortUrl: true, }, this.setUrl ); } } catch (fetchError) { if (this.mounted) { this.shortUrlCache = undefined; this.setState( { useShortUrl: false, isCreatingShortUrl: false, shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', { defaultMessage: 'Unable to create short URL. Error: {errorMessage}', values: { errorMessage: fetchError.message, }, }), }, this.setUrl ); } } }; private renderExportUrlAsOptions = () => { return [ { id: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT, label: this.renderWithIconTip( , ), ['data-test-subj']: 'exportAsSnapshot', }, { id: ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT, disabled: this.isNotSaved(), label: this.renderWithIconTip( , ), ['data-test-subj']: 'exportAsSavedObject', }, ]; }; private renderWithIconTip = (child: React.ReactNode, tipContent: React.ReactNode) => { return ( {child} ); }; private renderExportAsRadioGroup = () => { const generateLinkAsHelp = this.isNotSaved() ? ( ) : undefined; return ( } helpText={generateLinkAsHelp} > ); }; private renderShortUrlSwitch = () => { if ( this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT || !this.props.allowShortUrl ) { return; } const shortUrlLabel = ( ); const switchLabel = this.state.isCreatingShortUrl ? ( {shortUrlLabel} ) : ( shortUrlLabel ); const switchComponent = ( ); const tipContent = ( ); return ( {this.renderWithIconTip(switchComponent, tipContent)} ); }; private renderUrlParamExtensions = (): ReactElement | void => { if (!this.props.urlParamExtensions) { return; } const setParamValue = (paramName: string) => ( values: { [queryParam: string]: boolean } = {} ): void => { const stateUpdate = { urlParams: { ...this.state.urlParams, [paramName]: { ...values, }, }, }; this.setState(stateUpdate, this.state.useShortUrl ? this.createShortUrl : this.setUrl); }; return ( {this.props.urlParamExtensions.map(({ paramName, component: UrlParamComponent }) => ( ))} ); }; }