Skip to content

Commit

Permalink
Allow variants to be selected as tracks
Browse files Browse the repository at this point in the history
Better unmounting of IGV
Types for S3 data
Minor other fixes
  • Loading branch information
andrewpatto committed Jan 20, 2022
1 parent cfb353b commit 41d3b7f
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 36 deletions.
138 changes: 102 additions & 36 deletions src/containers/IGV.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,21 @@ const styles = (theme: Theme) => ({
},
});

function TransitionSlideUp(props: SlideProps) {
return <Slide direction='up' {...props} />;
}
// eslint-disable-next-line react/display-name
const TransitionSlideUp = React.forwardRef((props: SlideProps, ref) => (
<Slide direction='up' {...props} ref={ref} />
));

// a typescript class for our Django DB/API S3 record model - this should possibly be defined centrally somewhere
type S3Row = {
id: number;
bucket: string;
key: string;
size: number;
last_modified_date: string;
e_tag: string;
unique_hash: string;
};

// we can optionally route to this page with a preset subjectId in the url
type MatchParams = {
Expand All @@ -80,8 +92,11 @@ type State = {
loadTrackDialogOpened: boolean;
addExtTrackDialogOpened: boolean;
helpDialogOpened: boolean;
subject: any;

// if set, this is the set of S3 files associated with the given subject
subjectId: string | null;
subjectS3Rows: S3Row[];

loadedTrackNames: string[];
extTrackPath: string;
errorMessage: string | null;
Expand All @@ -94,28 +109,54 @@ class IGV extends Component<Props, State> {
loadTrackDialogOpened: false,
addExtTrackDialogOpened: false,
helpDialogOpened: false,
subject: null,
subjectId: null,
subjectS3Rows: [],
loadedTrackNames: [],
extTrackPath: '',
errorMessage: null,
};

async componentDidMount() {
this.initIgv();

// if this page has a /:subjectId URL - then we preload all the IGV loadable files that might be of interest
// related to that subject id - and save into the react state
const { subjectId } = this.props.match.params;
const searchQuery = encodeURIComponent('final .bam$');

if (subjectId) {
const MAX_EXPECTED_FILES_FOR_SUBJECT = 100;

// the search is of a space separated regex/plain strings - with AND logic between them
// in this case, all interesting files have 'final' in the path... and are bams or vcfs
const searchQuery = encodeURIComponent('final (.vcf.gz|.bam)$');

// in the absence of client side paging support here - we set a larger rowsPerPage than the default
// and (sensibly) assume that no one individual is going to have a huge number of BAMS/VCFS
const extraParams = {
queryStringParameters: {
subject: `${subjectId}`,
rowsPerPage: MAX_EXPECTED_FILES_FOR_SUBJECT,
},
};
const subject = await API.get('files', `/s3?search=${searchQuery}`, extraParams);
this.setState({ subject: subject, subjectId: subjectId });

const subjectSearch = await API.get('files', `/s3?search=${searchQuery}`, extraParams);

// if the page has a valid 'next' link then our assumption on page sizing was wrong - we abort by
// just not proceeding with the subject load
if (subjectSearch?.links?.next)
this.setState({
errorMessage: `More than ${MAX_EXPECTED_FILES_FOR_SUBJECT} files were associated with this subject - but we do not have client side paging here - so not proceeding with subject load`,
});
else {
this.setState({ subjectS3Rows: subjectSearch.results || [], subjectId: subjectId });
}
}
}

async componentWillUnmount() {
if (this.state.browser) igv.removeBrowser(this.state.browser);
}

initIgv = () => {
this.setState({ browser: null });
const igvDiv = document.getElementById('igvDiv');
Expand Down Expand Up @@ -187,20 +228,21 @@ class IGV extends Component<Props, State> {
}

browser?.loadTrack(trackConfig).then(() => {
loadedTrackNames.push(name);
this.setState({ loadedTrackNames: loadedTrackNames });
this.setState((prevState) => ({
loadedTrackNames: [...prevState.loadedTrackNames, name],
}));
});
};

/**
* Load an s3:// URL into igv as a track, by converting to a htsget url through known formatting rules
* consistent with our htsget endpoints.
*
* @param data
* @param row
*/
loadS3HtsgetTrackInIgvJs = (data: { bucket: string; key: string }): void => {
loadS3HtsgetTrackInIgvJs = (row: { bucket: string; key: string }): void => {
const { loadedTrackNames, browser } = this.state;
const { bucket, key } = data;
const { bucket, key } = row;
const baseName = this.getBaseName(key);

if (loadedTrackNames.includes(baseName)) {
Expand All @@ -223,8 +265,9 @@ class IGV extends Component<Props, State> {
removable: false,
})
.then(() => {
loadedTrackNames.push(baseName);
this.setState({ loadedTrackNames: loadedTrackNames });
this.setState((prevState) => ({
loadedTrackNames: [...prevState.loadedTrackNames, baseName],
}));
});
} else if (key.endsWith('vcf') || key.endsWith('vcf.gz')) {
browser
Expand All @@ -238,17 +281,17 @@ class IGV extends Component<Props, State> {
name: baseName,
})
.then(() => {
loadedTrackNames.push(baseName);
this.setState({ loadedTrackNames: loadedTrackNames });
this.setState((prevState) => ({
loadedTrackNames: [...prevState.loadedTrackNames, baseName],
}));
});
} else {
this.setState({ errorMessage: 'Unsupported file type!' });
}
};

handleLoadAllTracks = () => {
const { results } = this.state.subject;
results.map((row: any) => this.loadS3HtsgetTrackInIgvJs(row));
this.state.subjectS3Rows.map((row) => this.loadS3HtsgetTrackInIgvJs(row));
};

handleClearAllTracks = () => {
Expand Down Expand Up @@ -328,7 +371,7 @@ class IGV extends Component<Props, State> {
this.setState({ helpDialogOpened: false });
};

renderRowItem = (row: any) => {
renderRowItem = (row: S3Row) => {
const { loadedTrackNames } = this.state;
return (
<ListItem key={row.id} button onClick={() => this.loadS3HtsgetTrackInIgvJs(row)}>
Expand Down Expand Up @@ -362,11 +405,16 @@ class IGV extends Component<Props, State> {

renderLoadTrackDialog = () => {
const classes = this.props.classes;
const { loadTrackDialogOpened, subject, subjectId } = this.state;
const { results } = subject;
const { loadTrackDialogOpened, subjectS3Rows, subjectId } = this.state;

const wgs = subjectS3Rows.filter((r: any) => r.key.includes('WGS/'));
const wts = subjectS3Rows.filter((r: any) => r.key.includes('WTS/'));

// TODO: what is the unique key component for identifying TSO?
// TODO: find a GDS mechanism to allow htsget to browse these, then enable this
// const tso500 = subjectS3Rows.filter((r: any) => r.key.includes('TSO/'));

const wgs = results.filter((r: any) => r.key.includes('WGS/'));
const wts = results.filter((r: any) => r.key.includes('WTS/'));
const hasContent = wgs.length > 0 || wts.length > 0;

return (
<Dialog
Expand All @@ -379,7 +427,7 @@ class IGV extends Component<Props, State> {
<AppBar className={classes.appBar}>
<Toolbar>
<Typography variant='h6' className={classes.title}>
{subjectId} - Select BAM
{subjectId} - Select BAM or VCF
</Typography>
<Button
className={this.props.classes.menuButton}
Expand Down Expand Up @@ -410,12 +458,30 @@ class IGV extends Component<Props, State> {
</Toolbar>
</AppBar>

<List>
<ListSubheader>WGS</ListSubheader>
{wgs.map((row: any) => this.renderRowItem(row))}
<ListSubheader>WTS</ListSubheader>
{wts.map((row: any) => this.renderRowItem(row))}
</List>
{!hasContent && <p>No IGV loadable files were found associated with this subject</p>}

{hasContent && (
<List>
{wgs && wgs.length > 0 && (
<>
<ListSubheader>WGS</ListSubheader>
{wgs.map((row) => this.renderRowItem(row))}
</>
)}
{wts && wts.length > 0 && (
<>
<ListSubheader>WTS</ListSubheader>
{wts.map((row) => this.renderRowItem(row))}
</>
)}
{/*
tso500 && tso500.length > 0 && <>
<ListSubheader>TSO500</ListSubheader>
{tso500.map((row) => this.renderRowItem(row))}
</>
*/}
</List>
)}
</Dialog>
);
};
Expand Down Expand Up @@ -525,7 +591,7 @@ class IGV extends Component<Props, State> {
};

render() {
const { subject, subjectId, refGenome, browser } = this.state;
const { subjectS3Rows, subjectId, refGenome, browser } = this.state;
return (
<Fragment>
<div>
Expand All @@ -539,21 +605,21 @@ class IGV extends Component<Props, State> {
</FormControl>
<Button
component={RouterLink}
to={subject ? '/subjects/' + subjectId : '/'}
to={subjectS3Rows ? '/subjects/' + subjectId : '/'}
className={this.props.classes.menuButton}
variant={'outlined'}
size={'medium'}
color={'primary'}
startIcon={<ExitToAppIcon />}>
{subject ? subjectId : 'Select Subject'}
{subjectS3Rows ? subjectId : 'Select Subject'}
</Button>
<Button
className={this.props.classes.menuButton}
variant={'outlined'}
size={'medium'}
color={'primary'}
startIcon={<AddIcon />}
disabled={subject === null}
disabled={subjectS3Rows === null}
onClick={this.handleLoadTrackDialogOpen}>
Load...
</Button>
Expand Down Expand Up @@ -588,7 +654,7 @@ class IGV extends Component<Props, State> {
</div>
{!browser && <LinearProgress color='secondary' />}
<div id='igvDiv' />
{subject && this.renderLoadTrackDialog()}
{subjectS3Rows && this.renderLoadTrackDialog()}
{this.renderAddExtTrackDialog()}
{this.renderHelpDialog()}
{this.renderErrorMessage()}
Expand Down
1 change: 1 addition & 0 deletions src/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare module 'igv' {

export function setOauthToken(any, any): void;
export function createBrowser(any, any): Promise<IGVBrowser>;
export function removeBrowser(any): void;

export interface IGVBrowser {
loadTrack(config: ITrack): Promise<any>;
Expand Down

0 comments on commit 41d3b7f

Please sign in to comment.