Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix copy link issue in Safari #1633

Merged
merged 24 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
aa1c1d7
Adds 2.11.0.0 release notes (#1600)
DarshitChanpura Oct 12, 2023
8492ad2
added fix for safari link not copying
leanneeliatra Oct 16, 2023
790e27a
removing unneeded code and comments to create final PR
leanneeliatra Oct 24, 2023
763d2b1
Add filter when checking out OpenSearch-Dashboards to speed up CI che…
cwperks Oct 20, 2023
e68a810
Switch the home link to logout on security error pages (#1564)
leanneeliatra Oct 23, 2023
fac056a
Add permissions for async query and patch datasource API (#1626)
vamsimanohar Oct 24, 2023
fa79471
wip unit tests for safari link in progress
leanneeliatra Nov 2, 2023
f59a29d
Fix bug where custom permission groups are missing (#1636)
derek-ho Nov 2, 2023
a901624
[Refactor-1160] Different Values Pointing to Basic Auth, Need to Unif…
prabhask5 Nov 13, 2023
e3d343f
Update babel imports (#1652)
cwperks Nov 14, 2023
5715026
Stabilize SAML integration test cases for security dashboard CIs (#1641)
RyanL1997 Nov 14, 2023
a56a73d
merge from main
leanneeliatra Nov 16, 2023
eb146db
funtions extracted for unit testing
leanneeliatra Nov 16, 2023
ad38c05
funtions extracted for unit testing
leanneeliatra Nov 16, 2023
07c5222
specific clipboard range code extracted for unit testing
leanneeliatra Nov 16, 2023
75c6242
clipboard range code tested
leanneeliatra Nov 16, 2023
769cec7
Stabilize SAML integration test cases for security dashboard CIs (#1641)
RyanL1997 Nov 14, 2023
8057f24
clipboard range code tested
leanneeliatra Nov 16, 2023
842afb8
covering uncovered test lines
leanneeliatra Nov 16, 2023
0a5fad4
run ci
leanneeliatra Nov 16, 2023
952c00c
lint fixed
leanneeliatra Nov 16, 2023
4a89daf
Revert "covering uncovered test lines"
leanneeliatra Nov 17, 2023
9672422
Merge branch 'main' into safari-copy-link-fix
leanneeliatra Nov 22, 2023
d2aa727
Merge branch 'main' into safari-copy-link-fix
peternied Nov 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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,
cwperks marked this conversation as resolved.
Show resolved Hide resolved
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');
});
});
Loading