import React, { useRef, useState, useMemo, useEffect } from 'react' import { BsArrowDownShort as SortDesc, BsArrowUpShort as SortAsc, BsCheckCircle } from 'react-icons/bs' import { FixedSizeList as List } from 'react-window' import InfiniteLoader from 'react-window-infinite-loader' import classNames from 'classnames' import Measure from 'react-measure' import Spinner from './Spinner' import FadeTransition from './FadeTransition' import useMutationsList from '../hooks/useMutationsList' const TableSort = ({ active, ascending }) => ( <span className='text-primary w-6 h-6 flex-shrink-0'> {active ? ascending ? <SortAsc className='w-full h-full' /> : <SortDesc className='w-full h-full' /> : null} </span> ) const TableHeader = ({ children, className, sorted, align, ...props }) => ( <div {...props} scope="col" className={classNames( className, 'py-1.5 cursor-pointer sticky top-0 z-0', 'text-xs leading-5 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider', align === 'right' ? 'text-right' : 'text-left' )} > <span className={classNames('h-full flex items-center select-none leading-4', { 'text-primary font-bold': sorted, 'justify-end': align === 'right' })}> {children} </span> </div> ) const formatFrequency = f => { const _f = (f * 100).toPrecision(3) if (_f.startsWith('0.000')) return '> 0.001' if (_f.startsWith('-0.000')) return '< -0.001' return _f } const MutationsList = props => { const { api_url, dates, filter, gene, isLarge, lineagesForApi, pangoClade, queryParams, selected, selectMutation } = props const [state, actions] = useMutationsList(api_url, queryParams, pangoClade, lineagesForApi, gene, filter, dates) const [listSize, setListSize] = useState({ width: 0, height: 0 }) const hasNextPage = state.rows.length < state.total const itemCount = hasNextPage ? state.rows.length + 1 : state.rows.length const isItemLoaded = index => !hasNextPage || index < state.rows.length const loaderRef = useRef() const showFrequency = useMemo(() => { return !!state.denominator }, [state.denominator]) useEffect(() => { if (state.loading === 'LIST' && loaderRef.current) { loaderRef.current._listRef.scrollTo(0) } }, [state.loading]) return ( <div className='flex-grow flex flex-col bg-white dark:bg-gray-700'> <div className='flex border-b border-solid dark:border-gray-500'> <div className='flex flex-grow space-x-4 lg:space-x-6 px-4 lg:px-6'> <TableHeader key='not-searching' className='mr-auto' sorted={state.sortColumn === 'name'} onClick={() => actions.sortBy('name')} > <span className={classNames({ 'mr-1': state.sortColumn !== 'name' })}>Mutation</span> {state.sortColumn === 'name' && <TableSort active ascending={state.sortAscending} /> } </TableHeader> <TableHeader sorted={state.sortColumn === 'prop'} className='w-1/4' align='right' onClick={() => actions.sortBy('prop')} > <TableSort active={state.sortColumn === 'prop'} ascending={state.sortAscending} /> <span className='whitespace-nowrap'> Proportion </span> </TableHeader> <TableHeader sorted={state.sortColumn === 'change'} className='w-1/4 lg:w-1/5' align='right' onClick={() => actions.sortBy('change')} > <TableSort active={state.sortColumn === 'change'} ascending={state.sortAscending} /> <span> {/* Growth */} Recent<br/> Change </span> </TableHeader> </div> <div> <div className='overflow-y-scroll heron-styled-scrollbars opacity-0' /> </div> </div> <Measure bounds onResize={rect => { setListSize({ width: rect.bounds.width, height: rect.bounds.height }) }} > {({ measureRef }) => ( <div ref={measureRef} className='flex-grow relative'> <div className='absolute w-full' style={{ height: listSize.height }}> <InfiniteLoader ref={loaderRef} isItemLoaded={isItemLoaded} itemCount={itemCount} loadMoreItems={actions.loadMoreItems} threshold={state.rows.length ? 0 : undefined} > {({ onItemsRendered, ref }) => ( <List className='!overflow-y-scroll heron-styled-scrollbars' height={listSize.height} itemCount={itemCount} itemSize={36} onItemsRendered={onItemsRendered} ref={ref} width={listSize.width} > {({ index, style }) => { if (index < state.rows.length) { const row = state.rows[index] const isSelected = selected.includes(row.mutation) // const previous = state.rows[index - 1] return ( <div style={style} className={classNames( 'px-4 space-x-4 lg:px-6 lg:space-x-6 flex items-baseline cursor-pointer text-sm leading-9 big:text-base big:leading-9 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-sm', { 'font-bold': isSelected } )} onClick={() => selectMutation(row.mutation)} > { row && <> <span className='flex-grow flex items-baseline'> <span>{row.mutation}</span> { isSelected && <BsCheckCircle className='flex-shrink-0 fill-current text-primary w-4 h-4 ml-2 self-center' /> } </span> <span className='w-1/4 text-right whitespace-nowrap' title={!isLarge && `${row.count.toLocaleString()} sample${row.count === 1 ? '' : 's'}`}> { isLarge && <span className='text-subheading'>{row.count.toLocaleString()}<span className='mx-2'>/</span></span> } {showFrequency ? `${formatFrequency(row.count / state.denominator)}%` : ''} </span> <span className='w-1/4 lg:w-1/5 text-right whitespace-nowrap'> {`${formatFrequency(row.growth)}%`} </span> </> } </div> ) } if (state.rows.length > 0 && hasNextPage) { return ( <div style={style} className='grid place-items-center'> <Spinner className='block h-5 w-5 text-gray-600 dark:text-gray-300' /> </div> ) } return null }} </List> )} </InfiniteLoader> </div> <FadeTransition in={state.loading === 'LIST'}> <div className='absolute inset-0 z-10 flex justify-center items-center bg-white bg-opacity-50 dark:bg-gray-700 dark:bg-opacity-50'> <Spinner className='block h-6 w-6 text-gray-600 dark:text-gray-300' /> </div> </FadeTransition> </div> )} </Measure> </div> ) } export default MutationsList