Skip to content

Commit

Permalink
feat(web): add dataset selector to switch between analysis results
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-aksamentov committed Jan 29, 2025
1 parent 0b0e92a commit 1d4b6fb
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 4 deletions.
6 changes: 6 additions & 0 deletions packages/nextclade-web/src/components/Export/ExportPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import { ViewedDatasetSelector } from 'src/components/Main/ViewedDatasetSelector'
import styled from 'styled-components'
import { TabContent, TabLabel, TabNav, TabPane } from 'src/components/Common/TabsFull'
import { ExportTabColumnConfig } from 'src/components/Export/ExportTabColumnConfig'
Expand All @@ -15,6 +16,11 @@ export function ExportPage() {
return (
<Layout>
<Container>
<div>
<label> {t('Select dataset')}</label>
<ViewedDatasetSelector />
</div>

<Header>
<h4 className="mx-auto">{t('Download output files')}</h4>
</Header>
Expand Down
134 changes: 134 additions & 0 deletions packages/nextclade-web/src/components/Main/ViewedDatasetSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { isNil } from 'lodash'
import React, { useCallback, useMemo } from 'react'
import { ErrorInternal } from 'src/helpers/ErrorInternal'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { rgba } from 'polished'
import styled from 'styled-components'
import { useRecoilState, useRecoilValue } from 'recoil'
import Select, { OptionProps, StylesConfig } from 'react-select'
import type { SelectComponents } from 'react-select/dist/declarations/src/components'
import type { ActionMeta, GroupBase, OnChangeValue, Theme } from 'react-select/dist/declarations/src/types'
import { attrStrMaybe, Dataset } from 'src/types'
import type { IsMultiValue } from 'src/components/Common/Dropdown'
import { datasetsCurrentAtom, viewedDatasetNameAtom } from 'src/state/dataset.state'

interface Option {
value: string
dataset: Dataset
}

export function ViewedDatasetSelector() {
const [viewedDatasetName, setViewedDatasetName] = useRecoilState(viewedDatasetNameAtom)
const datasetsSelected = useRecoilValue(datasetsCurrentAtom)

const { options, currentOption } = useMemo(() => {
const options = (datasetsSelected ?? []).map((dataset) => ({ value: dataset.path, dataset }))
const currentOption = options.find((o) => o.value === viewedDatasetName) ?? options[0]
return { options, currentOption }
}, [datasetsSelected, viewedDatasetName])

const handleChange = useCallback(
(option: OnChangeValue<Option, IsMultiValue>, _: ActionMeta<Option>) => {
if (option) {
const datasetName = options.find((o) => o.value === option.value)?.dataset.path
if (isNil(datasetName)) {
throw new ErrorInternal(
`Attempted to select a non-existent dataset in the viewed dataset dropdown menu: '${option.value}'`,
)
}
setViewedDatasetName(datasetName)
}
},
[options, setViewedDatasetName],
)

return (
<div>
<Select
components={COMPONENTS}
options={options}
value={currentOption}
isMulti={false}
onChange={handleChange}
menuPortalTarget={document.body}
styles={STYLES}
theme={getTheme}
maxMenuHeight={400}
/>
</div>
)
}

function OptionComponent({
data: { dataset },
isDisabled,
isFocused,
isSelected,
innerRef,
innerProps,
}: OptionProps<Option, false>) {
const { t } = useTranslationSafe()

const { path, name, reference } = useMemo(() => {
const { path, attributes } = dataset
const name = attrStrMaybe(attributes, 'name') ?? t('Unknown')
const referenceName = attrStrMaybe(attributes, 'reference name') ?? t('Unknown')
const referenceAccession = attrStrMaybe(attributes, 'reference accession') ?? t('Unknown')
const reference = `${referenceName} (${referenceAccession})`
return { path, name, reference }
}, [dataset, t])

return (
<OptionBody
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
ref={innerRef}
aria-disabled={isDisabled}
isSelected={isSelected}
isDisabled={isDisabled}
isFocused={isFocused}
{...innerProps}
>
<div>{name}</div>
<div className="small">{reference}</div>
<div className="small">{path}</div>
</OptionBody>
)
}

const OptionBody = styled.div<{ isSelected?: boolean; isFocused?: boolean; isDisabled?: boolean }>`
padding: 0.4rem 0.2rem;
cursor: pointer;
background-color: ${(props) =>
props.isSelected ? props.theme.primary : props.isFocused ? rgba(props.theme.primary, 0.33) : undefined};
color: ${(props) => props.isSelected && 'white'};
`

const COMPONENTS: Partial<SelectComponents<Option, false, GroupBase<Option>>> = {
Option: OptionComponent,
}

function getTheme(theme: Theme): Theme {
return {
...theme,
borderRadius: 2,
spacing: {
...theme.spacing,
menuGutter: 0,
},
colors: {
...theme.colors,
},
}
}

const STYLES: StylesConfig<Option, false> = {
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
menuList: (base) => ({ ...base, fontSize: '1rem' }),
option: (base) => ({ ...base, fontSize: '1.0rem', padding: '2px 8px' }),
singleValue: (base, state) => ({
...base,
fontSize: '1.0rem',
display: state.selectProps.menuIsOpen ? 'none' : 'block',
}),
}
3 changes: 3 additions & 0 deletions packages/nextclade-web/src/components/Results/ResultsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Suspense } from 'react'
import { useRecoilValue } from 'recoil'
import { ViewedDatasetSelector } from 'src/components/Main/ViewedDatasetSelector'
import { viewedDatasetNameAtom } from 'src/state/dataset.state'
import styled from 'styled-components'
import { resultsTableTotalWidthAtom } from 'src/state/settings.state'
Expand Down Expand Up @@ -51,6 +52,8 @@ export function ResultsPage() {
<Container>
<WrapperOuter>
<WrapperInner $minWidth={totalWidth}>
<ViewedDatasetSelector />

<ResultsFilter />

<MainContent>
Expand Down
13 changes: 9 additions & 4 deletions packages/nextclade-web/src/components/Sort/SortPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { colorHash } from 'src/helpers/colorHash'
import { ErrorInternal } from 'src/helpers/ErrorInternal'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { useDatasetSuggestionResults } from 'src/hooks/useRunSeqAutodetect'
import { datasetsAtom, datasetsCurrentAtom } from 'src/state/dataset.state'
import { datasetsAtom, datasetsCurrentAtom, viewedDatasetNameAtom } from 'src/state/dataset.state'
import styled from 'styled-components'
import { Layout } from 'src/components/Layout/Layout'

Expand Down Expand Up @@ -194,10 +194,15 @@ export function SortPage() {
export function useRunAnalysisMany(selectedDatasets: Dataset[]) {
const run = useRunAnalysis()
const setDatasetsCurrent = useSetRecoilState(datasetsCurrentAtom)
const setViewedDatasetName = useSetRecoilState(viewedDatasetNameAtom)
return useCallback(() => {
setDatasetsCurrent(selectedDatasets)
run()
}, [run, selectedDatasets, setDatasetsCurrent])
if (selectedDatasets.length > 0) {
setDatasetsCurrent(selectedDatasets)
setViewedDatasetName(selectedDatasets[0].path)
run()
}
throw new ErrorInternal('Attempted to run analysis without any of the datasets selected')
}, [run, selectedDatasets, setDatasetsCurrent, setViewedDatasetName])
}

export interface SortingTableRowDatum extends MinimizerSearchRecord {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useMemo } from 'react'
import { ViewedDatasetSelector } from 'src/components/Main/ViewedDatasetSelector'
import styled, { ThemeProvider } from 'styled-components'
import { I18nextProvider } from 'react-i18next'
import { connect } from 'react-redux'
Expand Down Expand Up @@ -98,6 +99,7 @@ function TreePageContentDisconnected({ treeMeta }: TreePageProps) {
return (
<Container>
<MainContent>
<ViewedDatasetSelector />
<AuspiceContainer>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
Expand Down

0 comments on commit 1d4b6fb

Please sign in to comment.