Skip to content

Commit

Permalink
Fix copy link issue in Safari (#1633)
Browse files Browse the repository at this point in the history
Signed-off-by: [email protected] <[email protected]>
Signed-off-by: leanneeliatra <[email protected]>
Signed-off-by: leanneeliatra <[email protected]>
(cherry picked from commit 8f1334e)
  • Loading branch information
leanneeliatra authored and github-actions[bot] committed Nov 28, 2023
1 parent a77d969 commit 7927cbd
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 42 deletions.
101 changes: 59 additions & 42 deletions public/services/shared-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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__') {
Expand All @@ -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('<iframe') === 0) {
const regex = /<iframe[^>]*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;

Check warning on line 48 in public/services/shared-link.ts

View check run for this annotation

Codecov / codecov/patch

public/services/shared-link.ts#L47-L48

Added lines #L47 - L48 were not covered by tests

// 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 {

Check warning on line 53 in public/services/shared-link.ts

View check run for this annotation

Codecov / codecov/patch

public/services/shared-link.ts#L53

Added line #L53 was not covered by tests
// For the iFrame urls we need to parse out the src
if (originalValue && originalValue.toLowerCase().indexOf('<iframe') === 0) {
const regex = /<iframe[^>]*src="([^"]*)"/i;
const match = regex.exec(originalValue);

Check warning on line 57 in public/services/shared-link.ts

View check run for this annotation

Codecov / codecov/patch

public/services/shared-link.ts#L56-L57

Added lines #L56 - L57 were not covered by tests
if (match) {
urlPart = match[1]; // Contains the matched src, [0] contains the string where the match was found

Check warning on line 59 in public/services/shared-link.ts

View check run for this annotation

Codecov / codecov/patch

public/services/shared-link.ts#L59

Added line #L59 was not covered by tests
}
} catch (error) {
// Probably wasn't an url, so we just ignore this
}

updateClipboard(urlPart, originalValue, userRequestedTenant);

Check warning on line 63 in public/services/shared-link.ts

View check run for this annotation

Codecov / codecov/patch

public/services/shared-link.ts#L63

Added line #L63 was not covered by tests
} 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;

Check warning on line 79 in public/services/shared-link.ts

View check run for this annotation

Codecov / codecov/patch

public/services/shared-link.ts#L79

Added line #L79 was not covered by tests
}

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;
}
}
102 changes: 102 additions & 0 deletions public/services/test/shared-link.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});

0 comments on commit 7927cbd

Please sign in to comment.