Skip to content

Commit

Permalink
RFC87: add button to load GroupComparison table data in external tool (
Browse files Browse the repository at this point in the history
…cBioPortal#4938)

* Visualize Your Data: add 3rd party tools section, link, and image.
* Allow for configuration of custom buttons in data tables that send data to third-party tools on client system
  • Loading branch information
pappde authored and Nelliney committed Aug 14, 2024
1 parent 560f914 commit 73237fb
Show file tree
Hide file tree
Showing 18 changed files with 700 additions and 1 deletion.
17 changes: 17 additions & 0 deletions OPEN-SOURCE-DOCUMENTATION
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,20 @@ Available under license:
5. Products derived from this software may not be called
"ColorBrewer", nor may "ColorBrewer" appear in their name, without
prior written permission of Cynthia Brewer.

* JavaScript/CSS Font Detector

JavaScript/CSS Font Detector
----------------------------
Available under license:

JavaScript code to detect available availability of a
particular font in a browser using JavaScript and CSS.

Author : Lalit Patel
Website: http://www.lalit.org/lab/javascript-css-font-detect/
License: Apache Software License 2.0
http://www.apache.org/licenses/LICENSE-2.0



1 change: 1 addition & 0 deletions src/config/IAppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,5 @@ export interface IServerConfig {
vaf_log_scale_default: boolean; // this has a default
skin_study_view_show_sv_table: boolean; // this has a default
enable_study_tags: boolean;
download_custom_buttons_json: string;
}
5 changes: 4 additions & 1 deletion src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,10 @@ export function initializeServerConfiguration(rawConfiguration: any) {
);
} catch (err) {
// ignore
console.log('Error parsing localStorage.frontendConfig');
console.log(
'Error parsing localStorage.frontendConfig:' +
localStorage.frontendConfig
);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/config/serverConfigDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ export const ServerConfigDefaults: Partial<IServerConfig> = {
vaf_log_scale_default: false,

skin_study_view_show_sv_table: false,

download_custom_buttons_json: '',
};

export default ServerConfigDefaults;
54 changes: 54 additions & 0 deletions src/pages/staticPages/visualize/Visualize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,61 @@ import { PageLayout } from 'shared/components/PageLayout/PageLayout';
import './styles.scss';
import styles from './visualize.module.scss';
import { getNCBIlink } from 'cbioportal-frontend-commons';
import { getCustomButtonConfigs } from 'shared/components/CustomButton/CustomButtonServerConfig';

@observer
export default class Visualize extends React.Component<{}, {}> {
/**
* Display the 'visualize_html' data associated with serverConfig.download_custom_buttons_json
* @returns JSX.element
*/
customButtonsSection() {
const displayButtons = getCustomButtonConfigs().filter(
button => button.visualize_href
);
if (!displayButtons || displayButtons.length === 0) {
return;
}

return (
<>
<hr />

<h2>3rd party tools not maintained by cBioPortal community</h2>

<div
style={{ display: 'flex' }}
className={styles.customToolArray}
>
{displayButtons.map((button, index) => (
<div key={index} style={{ marginTop: 20 }}>
<h2>
<a href={button.visualize_href} target="_blank">
{button.visualize_title}
</a>
</h2>
<p>
{button.visualize_description}
<a href={button.visualize_href} target="_blank">
Try it!
</a>
</p>
{button.visualize_image_src && (
<a href={button.visualize_href} target="_blank">
<img
className="tile-image top-image"
alt={button.visualize_title}
src={button.visualize_image_src}
/>
</a>
)}
</div>
))}
</div>
</>
);
}

public render() {
return (
<PageLayout className={'whiteBackground staticPage'}>
Expand Down Expand Up @@ -128,6 +180,8 @@ export default class Visualize extends React.Component<{}, {}> {
</a>
</div>
</div>

{this.customButtonsSection()}
</PageLayout>
);
}
Expand Down
7 changes: 7 additions & 0 deletions src/pages/staticPages/visualize/visualize.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@
padding-right: 40px;
}
}

.customToolArray {
> div {
width: 550px;
padding-right: 40px;
}
}
1 change: 1 addition & 0 deletions src/pages/staticPages/visualize/visualize.module.scss.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
declare const styles: {
readonly "customToolArray": string;
readonly "toolArray": string;
};
export = styles;
Expand Down
153 changes: 153 additions & 0 deletions src/shared/components/CustomButton/CustomButton.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { CustomButton } from './CustomButton';
import { CustomButtonConfig } from './CustomButtonConfig';
import { ICustomButtonProps, CustomButtonUrlParameters } from './ICustomButton';

jest.mock('cbioportal-frontend-commons', () => ({
DefaultTooltip: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));

describe('CustomButton Component', () => {
const testData = 'test data';
const testDataLengthString = testData.length.toString();
const testUrlFormat =
'http://example.com?study={studyName}&-DataLength={dataLength}';
const testStudyName = 'Test Study';
const navigatorClipboardOriginal = navigator.clipboard;

// we used to use window.location to navigate, then changed to window.open
const windowLocationOriginal = window.location;
const windowOpenOriginal = window.open;
const windowOpenMock = jest.fn();

const mockJson: string = `
[
{
"id": "test",
"name": "Test Tool",
"tooltip": "This button shows that the Test Tool is working",
"image_src": "https://frontend.cbioportal.org/reactapp/images/369b022222badf37b2b0c284f4ae2284.png",
"url_format": "https://eu.httpbin.org/anything?-StudyName={studyName}&-ImportDataLength={dataLength}"
}
]
`;

const mockProps: ICustomButtonProps = {
toolConfig: {
name: 'Test',
id: 'test-tool',
url_format: testUrlFormat,
tooltip: 'Test Tooltip',
image_src: 'test-icon.png',
},
baseTooltipProps: {},
overlayClassName: '',
downloadDataAsync: () => Promise.resolve(testData),
urlFormatOverrides: {},
};

beforeEach(() => {
(window as any).groupComparisonPage = {
store: {
displayedStudies: {
result: [{ name: testStudyName }],
},
},
};

// mock clipboard
Object.assign(navigator, {
clipboard: {
writeText: jest.fn().mockResolvedValueOnce(''),
},
});

// Mock window.location.href
delete (window as any).location;
(window as any).location = {
href: '',
assign: jest.fn().mockImplementation(url => {
(window as any).location.href = url;
}),
};

// Mock window.open
(window as any).open = windowOpenMock;
});

afterEach(() => {
delete (window as any).groupComparisonPage;
Object.assign(navigator, navigatorClipboardOriginal);
window.location = windowLocationOriginal;
window.open = windowOpenOriginal;
});

it('parses json correctly and creates Config objects', () => {
const config = CustomButtonConfig.parseCustomButtonConfigs(mockJson);
expect(config.length).toBe(1);
expect(config[0].id).toBe('test');
// TECH: compiler doesn't know that config[0] is valid, so we add a spurious optional chaining operator
expect(config[0]?.isAvailable?.()).toBe(true);
});

it('renders correctly', () => {
render(<CustomButton {...mockProps} />);
expect(screen.getByRole('button')).toBeTruthy();
});

it('returns the correct study name from getSingleStudyName', () => {
const component = new CustomButton(mockProps);
expect(component.getSingleStudyName()).toBe('Test Study');
});

it('calls handleClick on button click', () => {
const handleClickSpy = jest.spyOn(
CustomButton.prototype,
'handleClick'
);
const { getByRole } = render(<CustomButton {...mockProps} />);
const button = getByRole('button');
fireEvent.click(button);
expect(handleClickSpy).toHaveBeenCalled();
});

it('copies data to clipboard and calls openCustomUrl', async () => {
const openCustomUrlSpy = jest.spyOn(
CustomButton.prototype,
'openCustomUrl'
);
const { getByRole } = render(<CustomButton {...mockProps} />);
const button = getByRole('button');

fireEvent.click(button);

await waitFor(() =>
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(testData)
);

await waitFor(() => expect(openCustomUrlSpy).toHaveBeenCalled());

expect(openCustomUrlSpy).toHaveBeenCalledWith({
dataLength: testDataLengthString,
});
});

it('formats URL correctly and redirects', () => {
const component = new CustomButton(mockProps);
const urlParametersLaunch: CustomButtonUrlParameters = {
studyName: testStudyName,
dataLength: testDataLengthString,
};

// LOW: should manually assemble using actual test property values
const expectedUrl =
'http://example.com?study=Test%20Study&-DataLength=9';

component.openCustomUrl(urlParametersLaunch);

expect(windowOpenMock).toHaveBeenCalledWith(expectedUrl, '_blank');
});
});
Loading

0 comments on commit 73237fb

Please sign in to comment.