From 8f1334edc5d6114e14a9220e377c5da887e3e8e1 Mon Sep 17 00:00:00 2001 From: leanneeliatra <131779422+leanneeliatra@users.noreply.github.com> Date: Tue, 28 Nov 2023 19:14:32 +0000 Subject: [PATCH] Fix copy link issue in Safari (#1633) Signed-off-by: leanne.laceybyrne@eliatra.com Signed-off-by: leanneeliatra Signed-off-by: leanneeliatra <131779422+leanneeliatra@users.noreply.github.com> --- public/services/shared-link.ts | 101 ++++++++++++---------- public/services/test/shared-link.test.ts | 102 +++++++++++++++++++++++ 2 files changed, 161 insertions(+), 42 deletions(-) create mode 100644 public/services/test/shared-link.test.ts diff --git a/public/services/shared-link.ts b/public/services/shared-link.ts index 738c662a7..4a078735e 100644 --- a/public/services/shared-link.ts +++ b/public/services/shared-link.ts @@ -12,15 +12,16 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ - import { parse } from 'url'; import { CoreStart } from 'opensearch-dashboards/public'; import { API_ENDPOINT_MULTITENANCY } from '../apps/configuration/constants'; export async function addTenantToShareURL(core: CoreStart) { let tenant = ''; + try { tenant = await core.http.get(API_ENDPOINT_MULTITENANCY); + if (!tenant) { tenant = 'global'; } else if (tenant === '__user__') { @@ -30,70 +31,86 @@ export async function addTenantToShareURL(core: CoreStart) { console.log(`failed to get user tenant: ${error}`); return; } - // Add the tenant to URLs copied from the share panel + document.addEventListener('copy', (event) => { - const shareButton = document.querySelector('[data-share-url]'); - const target = document.querySelector('body > span'); - // The copy event listens to Cmd + C too, so we need to make sure - // that we're actually copied something via the share panel - if ( - shareButton && - target && - shareButton.getAttribute('data-share-url') === target.textContent - ) { - const originalValue = target.textContent; - let urlPart = originalValue; - - // We need to figure out where in the value to add the tenant. - // Since OpenSearchDashboards sometimes adds values that aren't in the current location/url, - // we need to use the actual input values to do a sanity check. - try { - // For the iFrame urls we need to parse out the src - if (originalValue && originalValue.toLowerCase().indexOf(']*src="([^"]*)"/i; - const match = regex.exec(originalValue); - if (match) { - urlPart = match[1]; // Contains the matched src, [0] contains the string where the match was found - } - } + processCopyEvent(tenant); + }); +} - const newValue = addTenantToURL(urlPart!, originalValue!, tenant); +export function processCopyEvent(userRequestedTenant: string) { + const shareButton = document.querySelector('[data-share-url]') as any; + const target = document.querySelector('body > span'); - if (newValue !== originalValue) { - target.textContent = newValue; + // The copy event listens to Cmd + C too, so we need to make sure + // that we're actually copied something via the share panel + if (shareButton && target && shareButton.getAttribute('data-share-url') === target.textContent) { + const originalValue = target.textContent; + let urlPart = originalValue; + + // We need to figure out where in the value to add the tenant. + // Since OpenSearchDashboards sometimes adds values that aren't in the current location/url, + // we need to use the actual input values to do a sanity check. + try { + // For the iFrame urls we need to parse out the src + if (originalValue && originalValue.toLowerCase().indexOf(']*src="([^"]*)"/i; + const match = regex.exec(originalValue); + if (match) { + urlPart = match[1]; // Contains the matched src, [0] contains the string where the match was found } - } catch (error) { - // Probably wasn't an url, so we just ignore this } + + updateClipboard(urlPart, originalValue, userRequestedTenant); + } catch (error) { + // Probably wasn't an url, so we just ignore this } - }); + } } -function addTenantToURL( - url: string, +export function updateClipboard( + urlPart: string, originalValue: string | undefined, - userRequestedTenant: string + tenant: string ) { - const tenantKey = 'security_tenant'; - const tenantKeyAndValue = tenantKey + '=' + encodeURIComponent(userRequestedTenant); + const shareButton = document.querySelector('[data-share-url]') as any; + const target = document.querySelector('body > span'); if (!originalValue) { - originalValue = url; + originalValue = urlPart; } - const { host, pathname, search } = parse(url); + const { host, pathname, search } = parse(urlPart); const queryDelimiter = !search ? '?' : '&'; // The url parser returns null if the search is empty. Change that to an empty // string so that we can use it to build the values later - if (search && search.toLowerCase().indexOf(tenantKey) > -1) { + if (search && search.toLowerCase().indexOf('security_tenant') > -1) { // If we for some reason already have a tenant in the URL we skip any updates return originalValue; } // A helper for finding the part in the string that we want to extend/replace const valueToReplace = host! + pathname! + (search || ''); - const replaceWith = valueToReplace + queryDelimiter + tenantKeyAndValue; + const replaceWith = + valueToReplace + queryDelimiter + 'security_tenant=' + encodeURIComponent(tenant); + + setClipboardAndTarget(shareButton, target, replaceWith, originalValue); +} - return originalValue.replace(valueToReplace, replaceWith); +export function setClipboardAndTarget( + shareButton: any, + target: any, + newValue: string, + originalValue: string +) { + const range = document.createRange() as any; + const referenceNode = document.getElementsByTagName('span').item(0); + + range.selectNode(referenceNode); + shareButton.removeAllRanges(); + shareButton.addRange(range); + + if (newValue !== originalValue) { + target.textContent = newValue; + } } diff --git a/public/services/test/shared-link.test.ts b/public/services/test/shared-link.test.ts new file mode 100644 index 000000000..8849b2f76 --- /dev/null +++ b/public/services/test/shared-link.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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 necessary modules and dependencies +import { API_ENDPOINT_MULTITENANCY } from '../../apps/configuration/constants.tsx'; +import { + addTenantToShareURL, + processCopyEvent, + setClipboardAndTarget, + updateClipboard, +} from '../shared-link.ts'; + +describe('addTenantToShareURL function', () => { + it('should add a listener for copy events', () => { + const coreMock: any = { + http: { + get: jest.fn().mockResolvedValue('mocked-tenant'), + }, + }; + + jest.spyOn(document, 'addEventListener').mockImplementation((event, callback) => { + if (event === 'copy') { + callback(new Event('copy')); + expect(coreMock.http.get).toHaveBeenCalledWith(API_ENDPOINT_MULTITENANCY); + } + }); + addTenantToShareURL(coreMock); + }); +}); + +describe('processCopyEvent function', () => { + it('should update the clipboard and target text content', () => { + const shareButtonMock: any = { + getAttribute: jest.fn().mockReturnValue('mocked-share-url'), + }; + + const targetMock: any = { + textContent: 'mocked-text-content', + }; + + jest.spyOn(document, 'querySelector').mockImplementation((selector) => { + if (selector === '[data-share-url]') { + return shareButtonMock; + } else if (selector === 'body > span') { + return targetMock; + } + }); + + jest.spyOn(document, 'createRange').mockReturnValue({ + selectNode: jest.fn(), + } as any); + + processCopyEvent('mocked-tenant'); + }); +}); + +describe('updateClipboard function', () => { + it('should update the clipboard and target text content', () => { + const shareButtonMock: any = { + getAttribute: jest.fn().mockReturnValue('mocked-share-url'), + removeAllRanges: jest.fn(), + addRange: jest.fn(), + }; + + const targetMock: any = { + textContent: 'mocked-text-content', + }; + + jest.spyOn(document, 'querySelector').mockImplementation((selector) => { + if (selector === '[data-share-url]') { + return shareButtonMock; + } else if (selector === 'body > span') { + return targetMock; + } + }); + updateClipboard('mocked-url-part', 'mocked-original-value', 'mocked-tenant'); + }); +}); +describe('setClipboardAndTarget function', () => { + it('should set clipboard and target correctly', () => { + const shareButtonMock: any = { + removeAllRanges: jest.fn(), + addRange: jest.fn(), + }; + + const targetMock: any = { + textContent: 'mocked-text-content', + }; + setClipboardAndTarget(shareButtonMock, targetMock, 'mocked-new-value', 'mocked-original-value'); + }); +});