wurmlab/sequenceserver

View on GitHub
public/js/sidebar.js

Summary

Maintainability
D
1 day
Test Coverage
F
53%
import { Component } from 'react';
import _ from 'underscore';

import downloadFASTA from './download_fasta';
import asMailtoHref from './mailto';
import CloudShareModal from './cloud_share_modal';
import DownloadLinks from 'download_links';
/**
 * checks whether code is being run by jest
 */
// eslint-disable-next-line no-undef
const isTestMode = () => process.env.JEST_WORKER_ID !== undefined || process.env.NODE_ENV === 'test';
/**
 * Renders links for downloading hit information in different formats.
 * Renders links for navigating to each query.
 */
export default class extends Component {

    constructor(props) {
        super(props);
        this.downloadFastaOfAll = this.downloadFastaOfAll.bind(this);
        this.downloadFastaOfSelected = this.downloadFastaOfSelected.bind(this);
        this.topPanelJSX = this.topPanelJSX.bind(this);
        this.summaryString = this.summaryString.bind(this);
        this.indexJSX = this.indexJSX.bind(this);
        this.downloadsPanelJSX = this.downloadsPanelJSX.bind(this);
        this.handleQueryIndexChange = this.handleQueryIndexChange.bind(this);
        this.isElementInViewPort = this.isElementInViewPort.bind(this);
        this.setVisibleQueryIndex = this.setVisibleQueryIndex.bind(this);
        this.debounceScrolling = this.debounceScrolling.bind(this);
        this.scrollListener = this.scrollListener.bind(this);
        this.copyURL = this.copyURL.bind(this);
        this.shareCloudInit = this.shareCloudInit.bind(this);
        this.sharingPanelJSX = this.sharingPanelJSX.bind(this);
        this.timeout = null;
        this.queryElems = [];
        this.state = {
            queryIndex: 1
        };
    }

    componentDidMount() {
        /**
         * Fixes tooltips in the sidebar, allows tooltip display on click
         */
        $(function () {
            $('.sidebar [data-toggle="tooltip"]').tooltip({ placement: 'right' });
            $('#copyURL').tooltip({ title: 'Copied!', trigger: 'click', placement: 'right', delay: 0 });
        });

        //keep track of the current queryIndex so it doesn't get lost on page reload
        const urlMatch = window.location.href.match(/#Query_(\d+)/);
        if (urlMatch && urlMatch.length > 1) {
            const queryNumber = +urlMatch[1];
            const index = this.props.data.queries.findIndex(query => query.number === queryNumber);
            this.setState({ queryIndex: index + 1 });
        }
        window.addEventListener('scroll', this.scrollListener);
        $('a[href^="#Query_"]').on('click', this.animateAnchorElements);
    }
    componentWillUnmount() {
        window.removeEventListener('scroll', this.scrollListener);
    }
    componentDidUpdate(prevProps) {
        if (this.props.allQueriesLoaded && !prevProps.allQueriesLoaded) {
            /**
             * storing all query elements in this variable once they all become available so we don't have to fetch them all over again
             */
            this.queryElems = Array.from(document.querySelectorAll('.resultn'));
        }
    }


    /**
     * to avoid unnecessary computations, we debounce the scroll listener so it only fires after user has stopped scrolling for some milliseconds
     */
    scrollListener() {
        this.debounceScrolling(this.setVisibleQueryIndex, 500);
    }

    debounceScrolling(callback, timer) {
        if (this.timeout) {
            clearTimeout(this.timeout);
        }
        this.timeout = setTimeout(callback, timer);
    }

    /**
     * This method makes the page aware of what query is visible so that clicking previous / next button at any point
     * navigates to the proper query
     */
    setVisibleQueryIndex() {
        const queryElems = this.queryElems.length ? this.queryElems : Array.from(document.querySelectorAll('.resultn'));
        const hits = Array.from(document.querySelectorAll('.hit[id^=Query_]'));
        // get the first visible element and marks it as the current query
        const topmostEl = queryElems.find(this.isElementInViewPort) || hits.find(this.isElementInViewPort);
        if (topmostEl) {
            const queryIndex = Number(topmostEl.id.match(/Query_(\d+)/)[1]);
            let hash = `#Query_${queryIndex}`;
            // if we can guarantee that the browser can handle change in url hash without the page jumping,
            // then we update the url hash after scroll. else, hash is only updated on click of next or prev button
            if (window.history.pushState) {
                window.history.pushState(null, null, hash);
            }
            this.setState({ queryIndex });
        }
    }
    animateAnchorElements(e) {
        // allow normal behavior in test mode to prevent warnings or errors from jquery
        if (isTestMode()) return;
        e.preventDefault();
        $('html, body').animate({
            scrollTop: $(this.hash).offset().top
        }, 300);
        if (window.history.pushState) {
            window.history.pushState(null, null, this.hash);
        } else {
            window.location.hash = this.hash;
        }
    }
    isElementInViewPort(elem) {
        const { top, left, right, bottom } = elem.getBoundingClientRect();
        return (
            top >= 0 &&
            left >= 0 &&
            bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            right <= (window.innerWidth || document.documentElement.clientWidth)
        );
    }
    /**
         * Clear sessionStorage - useful to initiate a new search in the same tab.
         * Passing sessionStorage.clear directly as onclick callback didn't work
         * (on macOS Chrome).
        */
    clearSession() {
        sessionStorage.clear();
    }
    /**
     *
     * handle next and previous query button clicks
     */
    handleQueryIndexChange(nextQuery) {
        if (nextQuery < 1 || nextQuery > this.props.data.queries.length) return;
        const anchorEl = document.createElement('a');
        //indexing at [nextQuery - 1] because array is 0-indexed
        anchorEl.setAttribute('href', '#Query_' + this.props.data.queries[nextQuery - 1].number);
        anchorEl.setAttribute('hidden', true);
        document.body.appendChild(anchorEl);
        // add smooth scrolling animation with jquery
        $(anchorEl).on('click', this.animateAnchorElements);
        anchorEl.click();
        document.body.removeChild(anchorEl);
        this.setState({ queryIndex: nextQuery });
    }
    /**
     * Event-handler for downloading fasta of all hits.
     */
    downloadFastaOfAll() {
        var sequence_ids = [];
        this.props.data.queries.forEach(
            (query) => query.hits.forEach(
                (hit) => sequence_ids.push(hit.id)));
        var database_ids = this.props.data.querydb.map((querydb) => querydb.id);
        downloadFASTA(sequence_ids, database_ids);
        return false;
    }

    /**
     * Handles downloading fasta of selected hits.
     */
    downloadFastaOfSelected() {
        var sequence_ids = $('.hit-links :checkbox:checked').map(function () {
            return this.value;
        }).get();
        if (sequence_ids.length === 0) {
            return false;
        }
        var database_ids = _.map(this.props.data.querydb, _.iteratee('id'));
        downloadFASTA(sequence_ids, database_ids);
        return false;
    }




    /**
     * Handles copying the URL into the user's clipboard. Modified from: https://stackoverflow.com/a/49618964/18117380
     * Hides the 'Copied!' tooltip after 3 seconds
     */

    copyURL() {
        var element = document.createElement('input');
        var url = window.location.href;
        document.body.appendChild(element);
        element.value = url;
        element.select();
        document.execCommand('copy');
        document.body.removeChild(element);

        setTimeout(function () {
            $('#copyURL')._tooltip('hide');
        }, 3000);
    }

    shareCloudInit() {
        this.refs.cloudShareModal.show();
    }

    topPanelJSX() {
        var path = location.pathname.split('/');
        // Get job id.
        var job_id = path.pop();
        // Deriving rootURL this way is required for subURI deployments
        // - we cannot just send to '/'.
        var rootURL = path.join('/');
        return (
            <div className="sidebar-top-panel">
                <div className="section-header-sidebar">
                    <h4>
                        {this.summaryString()}
                    </h4>
                </div>
                {this.props.data.queries.length > 12 && this.queryIndexButtons()}
                <div>
                    <a href={`${rootURL}/?job_id=${job_id}`}>
                        <i className="fa fa-pencil"></i> Edit search
                    </a>
                    <span className="line">|</span>
                    <a href={`${rootURL}/`}
                        onClick={this.clearSession}>
                        <i className="fa fa-file-o"></i> New search
                    </a>
                </div>
                {this.props.shouldShowIndex && this.indexJSX()}
            </div>
        );
    }

    summaryString() {
        var program = this.props.data.program;
        var numqueries = this.props.data.queries.length;
        var numquerydb = this.props.data.querydb.length;

        return (
            program.toUpperCase() + ': ' +
            numqueries + ' ' + (numqueries > 1 ? 'queries' : 'query') + ', ' +
            numquerydb + ' ' + (numquerydb > 1 ? 'databases' : 'database')
        );
    }

    queryIndexButtons() {
        const buttonStyle = {
            outline: 'none', border: 'none', background: 'none'
        };
        const buttonClasses = 'btn-link nowrap-ellipsis hover-bold';

        const handlePreviousBtnClick = () => this.handleQueryIndexChange(this.state.queryIndex - 1);
        const handleNextBtnClick = () => this.handleQueryIndexChange(this.state.queryIndex + 1);

        // eslint-disable-next-line no-unused-vars
        const NavButton = ({ text, onClick }) => (
            <button className={buttonClasses} onClick={onClick} style={buttonStyle}>{text}</button>
        );
        return <div style={{ display: 'flex', width: '100%', margin: '7px 0' }}>
            {this.state.queryIndex > 1 && <NavButton text="Previous Query" onClick={handlePreviousBtnClick} />}
            {this.state.queryIndex > 1 && this.state.queryIndex < this.props.data.queries.length && <span className="line">|</span>}
            {this.state.queryIndex < this.props.data.queries.length && <NavButton onClick={handleNextBtnClick} text="Next Query" />}
        </div>;
    }
    indexJSX() {
        return <ul className="nav hover-reset active-bold"> {
            _.map(this.props.data.queries, (query) => {
                return <li key={'Side_bar_' + query.id}>
                    <a className="btn-link nowrap-ellipsis hover-bold"
                        title={'Query= ' + query.id + ' ' + query.title}
                        href={'#Query_' + query.number}>
                        {'Query= ' + query.id}
                    </a>
                </li>;
            })
        }
        </ul>;
    }

    downloadsPanelJSX() {
        return (
            <div className="downloads">
                <div className="section-header-sidebar">
                    <h4>
                        Download FASTA, XML, TSV
                    </h4>
                </div>
                <ul className="nav">
                    {
                        !(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li>
                            <a href="#" className={`btn-link download-fasta-of-all ${!this.props.atLeastOneHit && 'disabled'}`}
                                onClick={this.downloadFastaOfAll}>
                                FASTA of all hits
                            </a>
                        </li>
                    }
                    {
                        !(this.props.data.imported_xml || this.props.data.non_parse_seqids) && <li>
                            <a href="#" className="btn-link download-fasta-of-selected disabled"
                                onClick={this.downloadFastaOfSelected}>
                                FASTA of <span className="text-bold"></span> selected hit(s)
                            </a>
                        </li>
                    }
                    <li>
                        <a href="#" className={`btn-link download-alignment-of-all ${!this.props.atLeastOneHit && 'disabled'}`}>
                            Alignment of all hits
                        </a>
                    </li>
                    <li>
                        <a href="#" className="btn-link download-alignment-of-selected disabled">
                            Alignment of <span className="text-bold"></span> selected hit(s)
                        </a>
                    </li>
                    {
                        !this.props.data.imported_xml && <li>
                            <a className="btn-link download" data-toggle="tooltip"
                                title="15 columns: query and subject ID; scientific
                                name, alignment length, mismatches, gaps, identity,
                                start and end coordinates, e value, bitscore, query
                                coverage per subject and per HSP."
                                href={'download/' + this.props.data.search_id + '.std_tsv'}>
                                Standard tabular report
                            </a>
                        </li>
                    }
                    {
                        !this.props.data.imported_xml && <li>
                            <a className="btn-link download" data-toggle="tooltip"
                                title="44 columns: query and subject ID, GI,
                                accessions, and length; alignment details;
                                taxonomy details of subject sequence(s) and
                                query coverage per subject and per HSP."
                                href={'download/' + this.props.data.search_id + '.full_tsv'}>
                                Full tabular report
                            </a>
                        </li>
                    }
                    {
                        !this.props.data.imported_xml && <li>
                            <a className="btn-link download" data-toggle="tooltip"
                                title="Results in XML format."
                                href={'download/' + this.props.data.search_id + '.xml'}>
                                Full XML report
                            </a>
                        </li>
                    }
                    <DownloadLinks imported_xml={this.props.data.imported_xml} search_id={this.props.data.search_id} />
                </ul>
            </div>
        );
    }

    sharingPanelJSX() {
        return (
            <div className="sharing-panel">
                <div className="section-header-sidebar">
                    <h4>
                        Share results
                    </h4>
                </div>
                <ul className="nav">
                    {!this.props.cloudSharingEnabled &&
                        <li>
                            <a id="copyURL" className="btn-link copy-URL cursor-pointer" data-toggle="tooltip"
                                onClick={this.copyURL}>
                                <i className="fa fa-copy"></i> Copy URL to clipboard
                            </a>
                        </li>
                    }
                    {!this.props.cloudSharingEnabled &&
                        <li>
                            <a id="sendEmail" className="btn-link email-URL cursor-pointer" data-toggle="tooltip"
                                title="Send by email" href={asMailtoHref(this.props.data.querydb, this.props.data.program, this.props.data.queries.length, window.location.href)}
                                target="_blank" rel="noopener noreferrer">
                                <i className="fa fa-envelope"></i> Send by email
                            </a>
                        </li>
                    }
                    {this.props.cloudSharingEnabled &&
                        <li>
                            <button className="btn-link cloud-Post cursor-pointer" data-toggle="tooltip"
                                title="Upload results to SequenceServer Cloud where it will become accessable
                                to everyone who has a link." onClick={this.shareCloudInit}>
                                <i className="fa fa-cloud"></i> Share to cloud
                            </button>
                        </li>
                    }
                </ul>
                {
                    <CloudShareModal
                        ref="cloudShareModal"
                        querydb={this.props.data.querydb}
                        program={this.props.data.program}
                        queryLength={this.props.data.queries.length}
                    />
                }
            </div>
        );
    }

    render() {
        return (
            <div className="sidebar">
                {this.topPanelJSX()}
                {this.downloadsPanelJSX()}
                {this.sharingPanelJSX()}
                <div className="referral-panel">
                    <div className="section-header-sidebar">
                        <h4>Recommend SequenceServer</h4>
                        <p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $100 per signup</a></p>
                    </div>
                </div>
            </div>
        );
    }
}