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

Gene Panel Modal in Patient View #2782

Merged
merged 1 commit into from
Dec 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 98 additions & 5 deletions end-to-end-test/local/specs/core/patientview.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, "");
const patienViewUrl = CBIOPORTAL_URL+'/patient?studyId=teststudy_genepanels&caseId=patientA';

describe('patient view page', function() {

if (useExternalFrontend) {

describe('gene panel information', () => {

before(()=>{
Expand Down Expand Up @@ -216,7 +216,6 @@ describe('patient view page', function() {
});

describe('VAF plot', () => {

before(()=>{
goToUrlAndSetLocalStorage(patienViewUrl);
waitForPatientView();
Expand All @@ -228,9 +227,103 @@ describe('patient view page', function() {
var genePanelIcon = $('svg[data-test=vaf-plot] rect.genepanel-icon');
assert(genePanelIcon.isExisting());
});

});


describe('gene panel modal', () => {
function closeModal() {
$('.modal-footer button').click();
}

function clickOnGenePanelLinks() {
const genePanelLinks = $$('.rc-tooltip table td a');
genePanelLinks[genePanelLinks.length - 1].click();
}

beforeEach(() => {
goToUrlAndSetLocalStorage(patienViewUrl);
waitForPatientView();
})

it('toggles gene panel modal from patient header', () => {
browser
.moveToObject('.patientSamples .clinical-spans svg')
.pause(500);
clickOnGenePanelLinks();
assert($('#patient-view-gene-panel').isExisting());
});

it('toggles gene panel modal from genomic tracks', () => {
// mouse over sample icon
browser
.moveToObject(
'.genomicOverviewTracksContainer svg[data-test=sample-icon]'
)
.pause(500);
clickOnGenePanelLinks();
assert($('#patient-view-gene-panel').isExisting());

closeModal();
assert(!$('#patient-view-gene-panel').isExisting());

// mouse over gene panel icon
browser
.moveToObject(
'.genomicOverviewTracksContainer [data-test=cna-track-genepanel-icon-0]'
)
.pause(500);
$('.qtip-content a').click();
assert($('#patient-view-gene-panel').isExisting());
});

it('toggles gene panel modal from mutations table', () => {
const mutationsTable = '[data-test=patientview-mutation-table]';

// mouse over sample icon in "Samples" column
browser
.moveToObject(
`${mutationsTable} table td [data-test=not-profiled-icon]`
)
.pause(500);
clickOnGenePanelLinks();
assert($('#patient-view-gene-panel').isExisting());

closeModal();
assert(!$('#patient-view-gene-panel').isExisting());

// click on gene panel id in "Gene panel" column
$(`${mutationsTable} button#dropdown-custom-1`).click();
$(`${mutationsTable} ul.dropdown-menu`)
.$$('li')[2]
.click();
$(`${mutationsTable} table a`).click();
assert($('#patient-view-gene-panel').isExisting());
});

it('toggles gene panel modal from copy number table', () => {
const copyNumberTable = '[data-test=patientview-copynumber-table]';

// mouse over sample icon in "Samples" column
browser
.moveToObject(
`${copyNumberTable} table td [data-test=not-profiled-icon]`
)
.pause(500);
clickOnGenePanelLinks();
assert($('#patient-view-gene-panel').isExisting());

closeModal();
assert(!$('#patient-view-gene-panel').isExisting());

// click on gene panel id in "Gene panel" column
$(`${copyNumberTable} button#dropdown-custom-1`).click();
$(`${copyNumberTable} ul.dropdown-menu`)
.$$('li')[2]
.click();
$(`${copyNumberTable} table a`).click();
assert($('#patient-view-gene-panel').isExisting());
});
})
}
});

Expand All @@ -250,4 +343,4 @@ function testSampleIcon(geneSymbol, tableTag, sampleIconTypes, sampleVisibilitie
assert.equal(actualVisibility, desiredVisibility, "Gene "+geneSymbol+": icon visibility at position "+i+" is not `"+desiredVisibility+"`, but is `"+actualVisibility+"`");
});

}
}
51 changes: 51 additions & 0 deletions src/pages/patientView/PatientViewGenePanelModal/GenesList.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import GenesList from './GenesList';
import React from 'react';
import { assert } from 'chai';
import { mount } from 'enzyme';

describe('GenesList', () => {
let wrapper: any;

after(() => {
wrapper.unmount();
});

it('renders the component', () => {
wrapper = mount(
<GenesList
genePanel={{
description: 'description',
genePanelId: 'TESTPANEL1',
genes: [
{ entrezGeneId: 1, hugoGeneSymbol: 'ABLIM1' },
{ entrezGeneId: 2, hugoGeneSymbol: 'ACPP' },
{ entrezGeneId: 3, hugoGeneSymbol: 'ADAMTS20' },
],
}}
columns={3}
/>
);
assert(wrapper.text().includes('TESTPANEL1')); // renders panel name
assert(wrapper.text().includes('Number of genes: 3')); // renders number of genes
assert(wrapper.text().includes('ABLIM1')); // renders gene name
assert(wrapper.find('.input-group-sm').exists()); // renders filter input
assert(wrapper.find('.btn-group').exists()); // renders copy download buttons
});

it('renders 3 columns in the genes table', () => {
assert(wrapper.find('td').length === 3);
});

it('filters genes based on input value', () => {
// with matching keywords
wrapper.find('input').simulate('input', { target: { value: 'AB' } });
assert(wrapper.text().includes('ABLIM1'));
assert(!wrapper.text().includes('ACPP'));
assert(!wrapper.text().includes('ADAMTS20'));

// with non-matching keywords
wrapper.find('input').simulate('input', { target: { value: 'xyz' } });
assert(wrapper.text().includes('No matches'));
assert(wrapper.find('td').length === 1);
});
});
147 changes: 147 additions & 0 deletions src/pages/patientView/PatientViewGenePanelModal/GenesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as React from 'react';
import { GenePanel, GenePanelToGene } from 'shared/api/generated/CBioPortalAPI';
import { observer } from 'mobx-react';
import { SimpleCopyDownloadControls } from 'shared/components/copyDownloadControls/SimpleCopyDownloadControls';
import { serializeData } from 'shared/lib/Serializer';
import styles from './styles.module.scss';
import { chunk, flatten } from 'lodash';
import { observable, action, computed } from 'mobx';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import SimpleTable from 'shared/components/simpleTable/SimpleTable';

interface IGenesListProps {
genePanel: GenePanel;
columns?: number;
id?: string | undefined;
}

@observer
export default class GenesList extends React.Component<
IGenesListProps,
{}
> {
@observable filter:string = '';

@autobind
@action handleChangeInput(value: string) {
this.filter = value;
};

@computed get filteredGenes() {
const { genes } = this.props.genePanel;
if (this.filter) {
const regex = new RegExp(this.filter, 'i');
return genes.filter(
gene => regex.test(gene.entrezGeneId.toString()) || regex.test(gene.hugoGeneSymbol)
);
}
return genes;
};

genesDividedToColumns = (genes: GenePanelToGene[]) => {
let result = [];
let columnCount = this.columnCount;
let remainingGenes = [...genes];
while (columnCount > 0) {
const chunked = chunk(remainingGenes, Math.ceil(remainingGenes.length/columnCount));
if (chunked.length === columnCount) {
result = result.concat(chunked);
break;
} else {
result.push(chunked[0]);
remainingGenes = remainingGenes.slice(flatten(result).length);
columnCount--;
}
}
return result;
};

@computed get renderTableRows() {
const filtered = this.filteredGenes;
if (filtered.length === 0) {
return [];
}
const rows:JSX.Element[] = [];
this.genesDividedToRows(filtered).forEach(row => {
const tdValues = row.map(gene => <td key={gene ? gene : Math.random()}>{gene}</td>);
rows.push(<tr>{tdValues}</tr>);
});
return rows;
};

getDownloadData = () => {
const downloadData = [['Genes'], ...this.genesDividedToRows(this.props.genePanel.genes)];
return serializeData(downloadData);
};

genesDividedToRows = (genes: GenePanelToGene[]) => {
const genesByColumns = this.genesDividedToColumns(genes);
const genesByRows = [];
const geneCountPerColumn = this.geneCountPerColumn(genes.length);
for (let i = 0; i < geneCountPerColumn; i++) {
const genesPerRow = [];
for (let j = 0; j < this.columnCount; j++) {
genesPerRow.push(
genesByColumns[j] && genesByColumns[j][i]
? genesByColumns[j][i].hugoGeneSymbol
: ''
);
}
genesByRows.push(genesPerRow);
}
return genesByRows;
};

@computed get columnCount() {
return this.props.columns || 1;
};

geneCountPerColumn = (totalLength: number) => {
return Math.ceil(totalLength / this.columnCount);
};

@computed get renderTableHeaders() {
const thValues = [<th>Genes</th>];
return thValues.concat(Array(this.columnCount - 1).fill(<th></th>));
}

render() {
return (
<div id={this.props.id} className={styles.genesList}>
<h4 className={styles.panelName}>
{this.props.genePanel.genePanelId}
</h4>
<span>
Number of genes: {this.props.genePanel.genes.length}
</span>
<div className={classnames('pull-right has-feedback input-group-sm', styles.searchInput)}>
<input
type="text"
value={this.filter}
onInput={(e: React.ChangeEvent<HTMLInputElement>) =>
this.handleChangeInput(e.target.value)
}
className="form-control"
/>
<span
className="fa fa-search form-control-feedback"
aria-hidden="true"
/>
</div>
<SimpleCopyDownloadControls
className={classnames("pull-right", styles.copyDownloadControls)}
downloadData={this.getDownloadData}
downloadFilename={`gene_panel_${this.props.genePanel.genePanelId}.tsv`}
controlsStyle="BUTTON"
containerId={this.props.id}
/>
<SimpleTable
headers={this.renderTableHeaders}
rows={this.renderTableRows}
noRowsText="No matches"
/>
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react';
import { GenePanel } from 'shared/api/generated/CBioPortalAPI';
import { observer } from 'mobx-react';
import GenePanelModal from 'shared/components/GenePanelModal/GenePanelModal';
import GenesList from './GenesList';
import styles from './styles.module.scss';

interface IPatientViewGenePanelModalProps {
genePanel: GenePanel;
show: boolean;
onHide: () => void;
columns?: number;
}

@observer
export default class PatientViewGenePanelModal extends React.Component<
IPatientViewGenePanelModalProps,
{}
> {
render() {
return (
<GenePanelModal
panelName="Gene Panel"
adamabeshouse marked this conversation as resolved.
Show resolved Hide resolved
show={this.props.show}
onHide={this.props.onHide}
className={styles.patientViewModal}
>
<GenesList
id="patient-view-gene-panel"
genePanel={this.props.genePanel}
columns={this.props.columns}
/>
</GenePanelModal>
);
}
}
21 changes: 21 additions & 0 deletions src/pages/patientView/PatientViewGenePanelModal/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.genesList {
table {
margin-top: 20px;
}

.searchInput {
margin-left: 5px;
}

h4.panelName {
margin-bottom: 15px;
}

.copyDownloadControls {
z-index: 1080;
}
}

.patientViewModal {
z-index: 1070 !important;
}
Loading