From 131fedd85891d46b6205d09c5c954df97fbf8bd1 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 14 Sep 2023 08:10:33 +0200 Subject: [PATCH] feat(web): reimplement main page --- .../src/components/Common/List.tsx | 34 ++- .../src/components/Common/SearchBox.tsx | 96 +++++++++ .../src/components/FilePicker/FilePicker.tsx | 1 + .../src/components/FilePicker/UploadBox.tsx | 1 + .../src/components/Main/DatasetInfo.tsx | 25 ++- .../src/components/Main/DatasetSelector.tsx | 198 +++++++----------- .../components/Main/DatasetSelectorList.tsx | 123 ++++++----- .../src/components/Main/MainInputForm.tsx | 7 +- .../Main/QuerySequenceFilePicker.tsx | 155 +++++--------- .../src/components/Main/QuerySequenceList.tsx | 145 ++++++------- .../src/components/Main/RunPanel.tsx | 132 ++++++++++++ .../src/components/Main/SuggestionPanel.tsx | 101 +++++++++ .../nextclade-web/src/state/dataset.state.ts | 2 - .../nextclade-web/src/state/inputs.state.ts | 9 +- 14 files changed, 650 insertions(+), 379 deletions(-) create mode 100644 packages_rs/nextclade-web/src/components/Common/SearchBox.tsx create mode 100644 packages_rs/nextclade-web/src/components/Main/RunPanel.tsx create mode 100644 packages_rs/nextclade-web/src/components/Main/SuggestionPanel.tsx diff --git a/packages_rs/nextclade-web/src/components/Common/List.tsx b/packages_rs/nextclade-web/src/components/Common/List.tsx index e8496a60f..0ec0fdc27 100644 --- a/packages_rs/nextclade-web/src/components/Common/List.tsx +++ b/packages_rs/nextclade-web/src/components/Common/List.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components' +import styled, { css } from 'styled-components' export const Ul = styled.ul` padding-left: 1.5rem; @@ -13,3 +13,35 @@ export const UlInvisible = styled.ul` export const LiInvisible = styled.li` list-style: none; ` + +// @formatter:off +// prettier-ignore +export const ScrollShadowVerticalCss = css` + /** Taken from: https://css-tricks.com/books/greatest-css-tricks/scroll-shadows */ + background: + /* Shadow Cover TOP */ linear-gradient(white 30%, rgba(255, 255, 255, 0)) center top, + /* Shadow Cover BOTTOM */ linear-gradient(rgba(255, 255, 255, 0), white 70%) center bottom, + /* Shadow TOP */ radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) center top, + /* Shadow BOTTOM */ radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) center bottom; + background-repeat: no-repeat; + background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; + background-attachment: local, local, scroll, scroll; +` +// @formatter:on + +export const ListGenericCss = css` + ${ScrollShadowVerticalCss}; + list-style: none; + padding: 0; + margin: 0; + -webkit-overflow-scrolling: touch; + overflow-scrolling: touch; + + & li { + border: 0; + } +` + +export const UlGeneric = styled.ul` + ${ListGenericCss} +` diff --git a/packages_rs/nextclade-web/src/components/Common/SearchBox.tsx b/packages_rs/nextclade-web/src/components/Common/SearchBox.tsx new file mode 100644 index 000000000..c5b634d7a --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Common/SearchBox.tsx @@ -0,0 +1,96 @@ +import React, { ChangeEvent, useCallback, useMemo, HTMLProps } from 'react' +import styled from 'styled-components' +import { Form, Input as InputBase } from 'reactstrap' +import { MdSearch as IconSearchBase, MdClear as IconClearBase } from 'react-icons/md' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { ButtonTransparent } from 'src/components/Common/ButtonTransparent' + +const SearchForm = styled(Form)` + display: inline; + position: relative; +` + +const IconSearchWrapper = styled.span` + display: inline; + position: absolute; + padding: 5px 7px; +` + +const IconSearch = styled(IconSearchBase)` + * { + color: ${(props) => props.theme.gray500}; + } +` + +const ButtonClear = styled(ButtonTransparent)` + display: inline; + position: absolute; + right: 0; + padding: 0 7px; +` + +const IconClear = styled(IconClearBase)` + * { + color: ${(props) => props.theme.gray500}; + } +` + +const Input = styled(InputBase)` + display: inline !important; + padding-left: 35px; + padding-right: 30px; + height: 2.2em; +` + +export interface SearchBoxProps extends Omit, 'as'> { + searchTitle?: string + searchTerm: string + onSearchTermChange(term: string): void +} + +export function SearchBox({ searchTitle, searchTerm, onSearchTermChange, ...restProps }: SearchBoxProps) { + const { t } = useTranslationSafe() + + const onChange = useCallback( + (event: ChangeEvent) => { + onSearchTermChange(event.target.value) + }, + [onSearchTermChange], + ) + + const onClear = useCallback(() => { + onSearchTermChange('') + }, [onSearchTermChange]) + + const buttonClear = useMemo(() => { + if (searchTerm.length === 0) { + return null + } + return ( + + + + ) + }, [onClear, searchTerm.length, t]) + + return ( + + + + + + {buttonClear} + + ) +} diff --git a/packages_rs/nextclade-web/src/components/FilePicker/FilePicker.tsx b/packages_rs/nextclade-web/src/components/FilePicker/FilePicker.tsx index 30f2e95b9..cdb00d85c 100644 --- a/packages_rs/nextclade-web/src/components/FilePicker/FilePicker.tsx +++ b/packages_rs/nextclade-web/src/components/FilePicker/FilePicker.tsx @@ -16,6 +16,7 @@ import { UploadedFileInfo } from './UploadedFileInfo' import { UploadedFileInfoCompact } from './UploadedFileInfoCompact' export const FilePickerContainer = styled.div` + flex: 1; display: flex; flex-direction: column; ` diff --git a/packages_rs/nextclade-web/src/components/FilePicker/UploadBox.tsx b/packages_rs/nextclade-web/src/components/FilePicker/UploadBox.tsx index 24d1995e6..d559604e2 100644 --- a/packages_rs/nextclade-web/src/components/FilePicker/UploadBox.tsx +++ b/packages_rs/nextclade-web/src/components/FilePicker/UploadBox.tsx @@ -84,6 +84,7 @@ export function UploadBox({ onUpload, children, multiple = false, ...props }: Pr () => ( {t('Drag & drop files')} + {t('or folders')} {t('Select files')} ), diff --git a/packages_rs/nextclade-web/src/components/Main/DatasetInfo.tsx b/packages_rs/nextclade-web/src/components/Main/DatasetInfo.tsx index b4bef8841..db8e79afc 100644 --- a/packages_rs/nextclade-web/src/components/Main/DatasetInfo.tsx +++ b/packages_rs/nextclade-web/src/components/Main/DatasetInfo.tsx @@ -15,9 +15,21 @@ import { import type { Dataset } from 'src/types' import styled from 'styled-components' -export const DatasetInfoContainer = styled.div` +export const Container = styled.div` display: flex; - flex-direction: row; + //border: 1px #ccc9 solid; + //border-radius: 5px; + + //margin-top: 3px !important; + //margin-bottom: 3px !important; + //margin-left: 5px; + //padding: 15px; + + margin: 0; + padding: 15px; + box-shadow: 0 0 12px 0 #0002; + border: 1px #ccc9 solid; + border-radius: 5px; ` export const FlexLeft = styled.div` @@ -85,7 +97,7 @@ export function DatasetInfo({ dataset }: DatasetInfoProps) { } return ( - + @@ -144,7 +156,7 @@ export function DatasetInfo({ dataset }: DatasetInfoProps) { {t('Updated at: {{updated}}', { updated: updatedAt })} {t('Dataset name: {{name}}', { name: path })} - + ) } @@ -152,14 +164,14 @@ export function DatasetUndetectedInfo() { const { t } = useTranslationSafe() return ( - + {t('Autodetect')} {t('Detect pathogen automatically from sequences')} - + ) } @@ -206,6 +218,7 @@ function DatasetInfoAutodetectProgressCircle({ dataset }: DatasetInfoCircleProps const CountText = styled.span` text-align: center; + font-size: 0.8rem; ` interface CircleBorderProps { diff --git a/packages_rs/nextclade-web/src/components/Main/DatasetSelector.tsx b/packages_rs/nextclade-web/src/components/Main/DatasetSelector.tsx index 2fb2eae9b..ca7f290b4 100644 --- a/packages_rs/nextclade-web/src/components/Main/DatasetSelector.tsx +++ b/packages_rs/nextclade-web/src/components/Main/DatasetSelector.tsx @@ -1,38 +1,89 @@ -import { isNil } from 'lodash' -import React, { HTMLProps, useCallback, useState } from 'react' +import React, { HTMLProps, useState } from 'react' +import { useRecoilState, useRecoilValue } from 'recoil' +import styled from 'styled-components' import { ThreeDots } from 'react-loader-spinner' -import { Button, Col, Container, Form, FormGroup, Input, Row } from 'reactstrap' -import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil' -import { Toggle } from 'src/components/Common/Toggle' +import { SuggestionPanel } from 'src/components/Main/SuggestionPanel' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' -import { useRecoilToggle } from 'src/hooks/useToggle' -import { autodetectResultsAtom, hasAutodetectResultsAtom } from 'src/state/autodetect.state' -import { datasetCurrentAtom, datasetsAtom, minimizerIndexVersionAtom } from 'src/state/dataset.state' -import { shouldSuggestDatasetsAtom } from 'src/state/settings.state' -import styled from 'styled-components' -import { DatasetSelectorList } from './DatasetSelectorList' +import { datasetCurrentAtom, datasetsAtom } from 'src/state/dataset.state' +import { SearchBox } from 'src/components/Common/SearchBox' +import { DatasetSelectorList } from 'src/components/Main/DatasetSelectorList' + +export function DatasetSelector() { + const { t } = useTranslationSafe() + const [searchTerm, setSearchTerm] = useState('') + const { datasets } = useRecoilValue(datasetsAtom) + const [datasetCurrent, setDatasetCurrent] = useRecoilState(datasetCurrentAtom) + + const isBusy = datasets.length === 0 + + return ( + +
+ {t('Select dataset')} + + +
+ +
+ {!isBusy && ( + + )} + + {isBusy && ( + + + + + + )} +
+ +
+ +
+
+ ) +} -const DatasetSelectorContainer = styled(Container)` +const Container = styled.div` display: flex; + flex: 1; flex-direction: column; - width: 100%; height: 100%; overflow: hidden; - padding: 0; + margin-right: 10px; ` -const DatasetSelectorTitle = styled.h4` - flex: 1; - margin: auto 0; +const Header = styled.div` + display: flex; + flex: 0; + padding-left: 10px; + margin-top: 10px; + margin-bottom: 3px; ` -const DatasetSelectorListContainer = styled.section` +const Main = styled.div` display: flex; - width: 100%; - height: 100%; + flex: 1; + flex-direction: column; overflow: hidden; ` +const Footer = styled.div` + display: flex; + flex: 0; +` + +const Title = styled.h4` + flex: 1; + margin: auto 0; +` + const SpinnerWrapper = styled.div>` width: 100%; height: 100%; @@ -48,110 +99,3 @@ const Spinner = styled(ThreeDots)` margin: auto; height: 100%; ` - -export function DatasetSelector() { - const { t } = useTranslationSafe() - const [searchTerm, setSearchTerm] = useState('') - const { datasets } = useRecoilValue(datasetsAtom) - const [datasetCurrent, setDatasetCurrent] = useRecoilState(datasetCurrentAtom) - - const onSearchTermChange = useCallback( - (event: React.ChangeEvent) => { - const { value } = event.target - setSearchTerm(value) - }, - [setSearchTerm], - ) - - const isBusy = datasets.length === 0 - - return ( - - - - {t('Select pathogen dataset')} - - - - - - - - - - - - - - - - - {!isBusy && ( - - )} - - {isBusy && ( - - - - - - )} - - - - - ) -} - -function AutodetectToggle() { - const { t } = useTranslationSafe() - const minimizerIndexVersion = useRecoilValue(minimizerIndexVersionAtom) - const resetAutodetectResults = useResetRecoilState(autodetectResultsAtom) - const hasAutodetectResults = useRecoilValue(hasAutodetectResultsAtom) - const { state: shouldSuggestDatasets, toggle: toggleSuggestDatasets } = useRecoilToggle(shouldSuggestDatasetsAtom) - - if (isNil(minimizerIndexVersion)) { - return null - } - - return ( -
- - - - {t('Suggest best matches')} - - - - - -
- ) -} diff --git a/packages_rs/nextclade-web/src/components/Main/DatasetSelectorList.tsx b/packages_rs/nextclade-web/src/components/Main/DatasetSelectorList.tsx index a5c69c2d9..d02a23bde 100644 --- a/packages_rs/nextclade-web/src/components/Main/DatasetSelectorList.tsx +++ b/packages_rs/nextclade-web/src/components/Main/DatasetSelectorList.tsx @@ -1,7 +1,9 @@ import { get, isNil, sortBy } from 'lodash' +import { lighten } from 'polished' import React, { useCallback, useMemo } from 'react' import { ListGroup, ListGroupItem } from 'reactstrap' import { useRecoilValue } from 'recoil' +import { ListGenericCss } from 'src/components/Common/List' import styled from 'styled-components' import type { Dataset } from 'src/types' import { areDatasetsEqual } from 'src/types' @@ -9,36 +11,6 @@ import { autodetectResultsAtom, groupByDatasets } from 'src/state/autodetect.sta import { search } from 'src/helpers/search' import { DatasetInfo } from 'src/components/Main/DatasetInfo' -export const DatasetSelectorUl = styled(ListGroup)` - flex: 1; - overflow-y: scroll; - height: 100%; -` - -export const DatasetSelectorLi = styled(ListGroupItem)<{ $isDimmed?: boolean }>` - list-style: none; - margin: 0; - padding: 0.5rem; - cursor: pointer; - opacity: ${(props) => props.$isDimmed && 0.33}; - background-color: transparent; -` - -export interface DatasetSelectorListItemProps { - dataset: Dataset - isCurrent?: boolean - isDimmed?: boolean - onClick?: () => void -} - -export function DatasetSelectorListItem({ dataset, isCurrent, isDimmed, onClick }: DatasetSelectorListItemProps) { - return ( - - - - ) -} - export interface DatasetSelectorListProps { datasets: Dataset[] searchTerm: string @@ -93,30 +65,73 @@ export function DatasetSelectorList({ const { itemsStartWith, itemsInclude, itemsNotInclude } = searchResult + const listItems = useMemo(() => { + return ( + <> + {[itemsStartWith, itemsInclude].map((datasets) => + datasets.map((dataset) => ( + + )), + )} + + {[itemsNotInclude].map((datasets) => + datasets.map((dataset) => ( + + )), + )} + + ) + }, [datasetHighlighted, itemsInclude, itemsNotInclude, itemsStartWith, onItemClick]) + + return
    {listItems}
+} + +export const Ul = styled(ListGroup)` + ${ListGenericCss}; + flex: 1; + overflow: auto; + padding: 5px 5px; + border-radius: 0 !important; +` + +export const Li = styled(ListGroupItem)<{ $isDimmed?: boolean }>` + cursor: pointer; + opacity: ${(props) => props.$isDimmed && 0.4}; + background-color: transparent; + + margin: 3px 3px !important; + padding: 0 !important; + border-radius: 5px !important; + + &.active { + background-color: ${(props) => lighten(0.033)(props.theme.primary)}; + box-shadow: -3px 3px 12px 3px #0005; + opacity: ${(props) => props.$isDimmed && 0.66}; + } +` + +interface DatasetSelectorListItemProps { + dataset: Dataset + isCurrent?: boolean + isDimmed?: boolean + onClick?: () => void +} + +function DatasetSelectorListItem({ dataset, isCurrent, isDimmed, onClick }: DatasetSelectorListItemProps) { return ( - - {[itemsStartWith, itemsInclude].map((datasets) => - datasets.map((dataset) => ( - - )), - )} - - {[itemsNotInclude].map((datasets) => - datasets.map((dataset) => ( - - )), - )} - +
  • + +
  • ) } diff --git a/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx b/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx index c3c0a890b..94b4b9e84 100644 --- a/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx +++ b/packages_rs/nextclade-web/src/components/Main/MainInputForm.tsx @@ -2,12 +2,13 @@ import React from 'react' import { QuerySequenceFilePicker } from 'src/components/Main/QuerySequenceFilePicker' import styled from 'styled-components' import { Col as ColBase, Row as RowBase } from 'reactstrap' -import { DatasetSelector } from 'src/components/Main/DatasetSelector' import { useUpdatedDatasetIndex } from 'src/io/fetchDatasets' +import { DatasetSelector } from 'src/components/Main/DatasetSelector' const Container = styled.div` height: 100%; overflow: hidden; + margin-top: 10px; ` const Row = styled(RowBase)` @@ -27,10 +28,10 @@ export function MainInputForm() { return ( - + - + diff --git a/packages_rs/nextclade-web/src/components/Main/QuerySequenceFilePicker.tsx b/packages_rs/nextclade-web/src/components/Main/QuerySequenceFilePicker.tsx index 8eed4018f..c0f3e4bc5 100644 --- a/packages_rs/nextclade-web/src/components/Main/QuerySequenceFilePicker.tsx +++ b/packages_rs/nextclade-web/src/components/Main/QuerySequenceFilePicker.tsx @@ -1,51 +1,28 @@ import React, { useCallback, useMemo } from 'react' -import { Button, Col, Form, FormGroup, Row } from 'reactstrap' import { useRecoilValue } from 'recoil' +import styled from 'styled-components' +import type { AlgorithmInput } from 'src/types' import { QuerySequenceList } from 'src/components/Main/QuerySequenceList' +import { RunPanel } from 'src/components/Main/RunPanel' import { useRunAnalysis } from 'src/hooks/useRunAnalysis' import { useRunSeqAutodetect } from 'src/hooks/useRunSeqAutodetect' import { useRecoilToggle } from 'src/hooks/useToggle' -import { canRunAtom } from 'src/state/results.state' -import styled from 'styled-components' -import { datasetCurrentAtom } from 'src/state/dataset.state' -import { hasInputErrorsAtom, qrySeqErrorAtom } from 'src/state/error.state' +import { qrySeqErrorAtom } from 'src/state/error.state' import { shouldRunAutomaticallyAtom, shouldSuggestDatasetsAtom } from 'src/state/settings.state' -import type { AlgorithmInput } from 'src/types' -import { Toggle } from 'src/components/Common/Toggle' -import { FlexLeft, FlexRight } from 'src/components/FilePicker/FilePickerStyles' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' -import { AlgorithmInputDefault } from 'src/io/AlgorithmInput' import { FilePicker } from 'src/components/FilePicker/FilePicker' import { FileIconFasta } from 'src/components/Common/FileIcons' -import { hasRequiredInputsAtom, useQuerySeqInputs } from 'src/state/inputs.state' - -const SequenceFilePickerContainer = styled.section` - display: flex; - flex-direction: column; - width: 100%; - height: 100%; -` - -const ButtonRunStyled = styled(Button)` - min-width: 160px; - min-height: 50px; - margin-left: 1rem; -` +import { useQuerySeqInputs } from 'src/state/inputs.state' export function QuerySequenceFilePicker() { const { t } = useTranslationSafe() - const datasetCurrent = useRecoilValue(datasetCurrentAtom) const { qryInputs, addQryInputs } = useQuerySeqInputs() const qrySeqError = useRecoilValue(qrySeqErrorAtom) - const canRun = useRecoilValue(canRunAtom) - const { state: shouldRunAutomatically, toggle: toggleRunAutomatically } = useRecoilToggle(shouldRunAutomaticallyAtom) + const { state: shouldRunAutomatically } = useRecoilToggle(shouldRunAutomaticallyAtom) const shouldSuggestDatasets = useRecoilValue(shouldSuggestDatasetsAtom) - const hasRequiredInputs = useRecoilValue(hasRequiredInputsAtom) - const hasInputErrors = useRecoilValue(hasInputErrorsAtom) - const icon = useMemo(() => , []) const runAnalysis = useRunAnalysis() @@ -64,29 +41,6 @@ export function QuerySequenceFilePicker() { [addQryInputs, runAnalysis, runAutodetect, shouldRunAutomatically, shouldSuggestDatasets], ) - const setExampleSequences = useCallback(() => { - if (datasetCurrent) { - addQryInputs([new AlgorithmInputDefault(datasetCurrent)]) - if (shouldSuggestDatasets) { - runAutodetect() - } - if (shouldRunAutomatically) { - runAnalysis() - } - } - }, [addQryInputs, datasetCurrent, runAnalysis, runAutodetect, shouldRunAutomatically, shouldSuggestDatasets]) - - const { isRunButtonDisabled, runButtonColor, runButtonTooltip } = useMemo(() => { - const isRunButtonDisabled = !(canRun && hasRequiredInputs) || hasInputErrors - return { - isRunButtonDisabled, - runButtonColor: isRunButtonDisabled ? 'secondary' : 'success', - runButtonTooltip: isRunButtonDisabled - ? t('Please provide input files for the algorithm') - : t('Launch the algorithm!'), - } - }, [canRun, hasInputErrors, hasRequiredInputs, t]) - const headerText = useMemo(() => { if (qryInputs.length > 0) { return t('Add more sequence data') @@ -95,56 +49,55 @@ export function QuerySequenceFilePicker() { }, [qryInputs.length, t]) return ( - - + +
    + +
    + +
    + +
    + +
    + +
    +
    + ) +} - +const Container = styled.div` + display: flex; + flex: 1; + flex-direction: column; + height: 100%; + overflow: hidden; + margin-left: 10px; +` - - - -
    - - - - {t('Run automatically')} - - - -
    -
    +const Header = styled.div` + display: flex; + flex: 0; + margin-bottom: 15px; +` - - +const Main = styled.div` + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; +` - - {t('Run')} - - - -
    -
    - ) -} +const Footer = styled.div` + display: flex; + flex: 0; +` diff --git a/packages_rs/nextclade-web/src/components/Main/QuerySequenceList.tsx b/packages_rs/nextclade-web/src/components/Main/QuerySequenceList.tsx index d7ec69ab5..cb7802f94 100644 --- a/packages_rs/nextclade-web/src/components/Main/QuerySequenceList.tsx +++ b/packages_rs/nextclade-web/src/components/Main/QuerySequenceList.tsx @@ -1,111 +1,88 @@ import React, { useCallback, useMemo } from 'react' -import { Button, Col, Container, Row } from 'reactstrap' -import styled from 'styled-components' +import { Button } from 'reactstrap' +import styled, { useTheme } from 'styled-components' import { ImCross } from 'react-icons/im' -import { rgba } from 'polished' - import { AlgorithmInput } from 'src/types' import { ButtonTransparent } from 'src/components/Common/ButtonTransparent' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' import { useQuerySeqInputs } from 'src/state/inputs.state' - -const SequencesCurrentWrapper = styled(Container)` - border: 1px #ccc9 solid; - border-radius: 5px; -` - -const InputFileInfoWrapper = styled.section` - box-shadow: ${(props) => `1px 1px 5px ${rgba(props.theme.black, 0.1)}`}; - border: 1px #ccc9 solid; - border-radius: 5px; - margin: 0.5rem 0; - padding: 0.5rem 1rem; -` - -export interface InputFileInfoProps { - input: AlgorithmInput - index: number -} - -export function InputFileInfo({ input, index }: InputFileInfoProps) { - const { t } = useTranslationSafe() - const { removeQryInput } = useQuerySeqInputs() - const onRemoveClicked = useCallback(() => { - removeQryInput(index) - }, [index, removeQryInput]) - - return ( - - - {input.description} - - - - - - ) -} +import { UlGeneric } from '../Common/List' export function QuerySequenceList() { const { t } = useTranslationSafe() const { qryInputs, clearQryInputs } = useQuerySeqInputs() - const inputComponents = useMemo( - () => ( - - - {qryInputs.map((input, index) => ( - // eslint-disable-next-line react/no-array-index-key - - ))} - - - ), - [qryInputs], - ) - - const removeButton = useMemo( - () => - qryInputs.length > 0 ? ( - - - - - - ) : null, + const listItems = useMemo(() => { + return qryInputs.map((input, index) => ( +
  • + +
  • + )) + }, [qryInputs]) - [clearQryInputs, qryInputs.length, t], - ) const headerText = useMemo(() => { if (qryInputs.length === 0) { return null } return ( - - -

    {t("Sequence data you've added")}

    - -
    +
    +

    {t("Sequence data you've added")}

    + +
    ) - }, [qryInputs.length, t]) + }, [clearQryInputs, qryInputs.length, t]) if (qryInputs.length === 0) { return null } return ( -
    + <> {headerText} - - - - {inputComponents} - {removeButton} - - - -
    +
      {listItems}
    + ) } + +export const Ul = styled(UlGeneric)` + flex: 1; + overflow: auto; +` + +export const Li = styled.li` + margin: 5px 0; + border-radius: 5px !important; +` + +export interface InputFileInfoProps { + input: AlgorithmInput + index: number +} + +export function InputFileInfo({ input, index }: InputFileInfoProps) { + const { t } = useTranslationSafe() + const theme = useTheme() + const { removeQryInput } = useQuerySeqInputs() + const onRemoveClicked = useCallback(() => { + removeQryInput(index) + }, [index, removeQryInput]) + + return ( + +
    {input.description}
    + + + +
    + ) +} + +const Container = styled.section` + display: flex; + padding: 0.5rem 1rem; + box-shadow: 0 0 12px 0 #0002; + border: 1px #ccc9 solid; + border-radius: 5px; +` diff --git a/packages_rs/nextclade-web/src/components/Main/RunPanel.tsx b/packages_rs/nextclade-web/src/components/Main/RunPanel.tsx new file mode 100644 index 000000000..9e573dc0d --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Main/RunPanel.tsx @@ -0,0 +1,132 @@ +import React, { useCallback, useMemo } from 'react' +import styled from 'styled-components' +import { Button, Form as FormBase, FormGroup } from 'reactstrap' +import { useRecoilValue } from 'recoil' +import { useRunAnalysis } from 'src/hooks/useRunAnalysis' +import { useRunSeqAutodetect } from 'src/hooks/useRunSeqAutodetect' +import { useRecoilToggle } from 'src/hooks/useToggle' +import { canRunAtom } from 'src/state/results.state' +import { datasetCurrentAtom } from 'src/state/dataset.state' +import { hasInputErrorsAtom } from 'src/state/error.state' +import { shouldRunAutomaticallyAtom, shouldSuggestDatasetsAtom } from 'src/state/settings.state' +import { Toggle } from 'src/components/Common/Toggle' +import { FlexLeft, FlexRight } from 'src/components/FilePicker/FilePickerStyles' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { AlgorithmInputDefault } from 'src/io/AlgorithmInput' +import { hasRequiredInputsAtom, useQuerySeqInputs } from 'src/state/inputs.state' + +export function RunPanel() { + const { t } = useTranslationSafe() + + const datasetCurrent = useRecoilValue(datasetCurrentAtom) + const { addQryInputs } = useQuerySeqInputs() + + const canRun = useRecoilValue(canRunAtom) + const { state: shouldRunAutomatically, toggle: toggleRunAutomatically } = useRecoilToggle(shouldRunAutomaticallyAtom) + const shouldSuggestDatasets = useRecoilValue(shouldSuggestDatasetsAtom) + + const hasRequiredInputs = useRecoilValue(hasRequiredInputsAtom) + const hasInputErrors = useRecoilValue(hasInputErrorsAtom) + + const runAnalysis = useRunAnalysis() + const runAutodetect = useRunSeqAutodetect() + + const setExampleSequences = useCallback(() => { + if (datasetCurrent) { + addQryInputs([new AlgorithmInputDefault(datasetCurrent)]) + if (shouldSuggestDatasets) { + runAutodetect() + } + if (shouldRunAutomatically) { + runAnalysis() + } + } + }, [addQryInputs, datasetCurrent, runAnalysis, runAutodetect, shouldRunAutomatically, shouldSuggestDatasets]) + + const { isRunButtonDisabled, runButtonColor, runButtonTooltip } = useMemo(() => { + const isRunButtonDisabled = !(canRun && hasRequiredInputs) || hasInputErrors + return { + isRunButtonDisabled, + runButtonColor: isRunButtonDisabled ? 'secondary' : 'success', + runButtonTooltip: isRunButtonDisabled + ? t('Please provide sequence data for the algorithm') + : t('Launch the algorithm!'), + } + }, [canRun, hasInputErrors, hasRequiredInputs, t]) + + return ( + +
    + + + + + {t('Run automatically')} + + + + + + + + + + {t('Run')} + + +
    +
    + ) +} + +const Container = styled.div` + flex: 1; + margin-top: auto; + margin-bottom: 7px; + padding: 7px 0; + padding-right: 5px; +` + +const Form = styled(FormBase)` + display: flex; + width: 100%; + height: 100%; + margin-top: auto; + padding: 10px; + border: 1px #ccc9 solid; + border-radius: 5px; +` + +// const Container = styled.div` +// flex: 1; +// margin-top: auto; +// margin-bottom: 7px; +// padding: 10px; +// padding-right: 5px; +// box-shadow: 0 3px 20px 3px #0003; +// ` +// +// const Form = styled(FormBase)` +// display: flex; +// width: 100%; +// height: 100%; +// padding: 10px; +// border: 1px #ccc9 solid; +// border-radius: 5px; +// ` + +const ButtonRunStyled = styled(Button)` + min-width: 150px; + min-height: 45px; +` diff --git a/packages_rs/nextclade-web/src/components/Main/SuggestionPanel.tsx b/packages_rs/nextclade-web/src/components/Main/SuggestionPanel.tsx new file mode 100644 index 000000000..8d9800c17 --- /dev/null +++ b/packages_rs/nextclade-web/src/components/Main/SuggestionPanel.tsx @@ -0,0 +1,101 @@ +import { isNil } from 'lodash' +import React, { useMemo } from 'react' +import { useRunSeqAutodetect } from 'src/hooks/useRunSeqAutodetect' +import { hasRequiredInputsAtom } from 'src/state/inputs.state' +import styled from 'styled-components' +import { Button, Form as FormBase, FormGroup } from 'reactstrap' +import { useRecoilValue, useResetRecoilState } from 'recoil' +import { Toggle } from 'src/components/Common/Toggle' +import { FlexLeft, FlexRight } from 'src/components/FilePicker/FilePickerStyles' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { useRecoilToggle } from 'src/hooks/useToggle' +import { autodetectResultsAtom, hasAutodetectResultsAtom } from 'src/state/autodetect.state' +import { minimizerIndexVersionAtom } from 'src/state/dataset.state' +import { shouldSuggestDatasetsAtom } from 'src/state/settings.state' + +export function SuggestionPanel() { + const { t } = useTranslationSafe() + const minimizerIndexVersion = useRecoilValue(minimizerIndexVersionAtom) + const resetAutodetectResults = useResetRecoilState(autodetectResultsAtom) + const hasAutodetectResults = useRecoilValue(hasAutodetectResultsAtom) + const hasRequiredInputs = useRecoilValue(hasRequiredInputsAtom) + const runSuggest = useRunSeqAutodetect() + + const { canRun, runButtonColor, runButtonTooltip } = useMemo(() => { + const canRun = hasRequiredInputs + return { + canRun, + runButtonColor: !canRun ? 'secondary' : 'success', + runButtonTooltip: !canRun ? t('Please provide sequence data for the algorithm') : t('Launch suggestions engine!'), + } + }, [hasRequiredInputs, t]) + + if (isNil(minimizerIndexVersion)) { + return null + } + + return ( + +
    + + + + + + + + + {t('Suggest')} + + +
    +
    + ) +} + +const Container = styled.div` + flex: 1; + margin-top: auto; + margin-bottom: 7px; + padding: 7px 0; + padding-left: 5px; +` + +const Form = styled(FormBase)` + display: flex; + width: 100%; + height: 100%; + margin-top: auto; + padding: 10px; + border: 1px #ccc9 solid; + border-radius: 5px; +` + +const ButtonRunStyled = styled(Button)` + min-width: 150px; + min-height: 45px; +` + +function AutosuggestionToggle() { + const { t } = useTranslationSafe() + const { state: shouldSuggestDatasets, toggle: toggleSuggestDatasets } = useRecoilToggle(shouldSuggestDatasetsAtom) + return ( + + + + {t('Suggest automatically')} + + + + ) +} diff --git a/packages_rs/nextclade-web/src/state/dataset.state.ts b/packages_rs/nextclade-web/src/state/dataset.state.ts index 913ffee91..880acf271 100644 --- a/packages_rs/nextclade-web/src/state/dataset.state.ts +++ b/packages_rs/nextclade-web/src/state/dataset.state.ts @@ -3,7 +3,6 @@ import { atom, DefaultValue, selector } from 'recoil' import type { Dataset, MinimizerIndexVersion } from 'src/types' // import { GENE_OPTION_NUC_SEQUENCE } from 'src/constants' -import { inputResetAtom } from 'src/state/inputs.state' import { persistAtom } from 'src/state/persist/localStorage' // import { viewedGeneAtom } from 'src/state/seqViewSettings.state' import { isDefaultValue } from 'src/state/utils/isDefaultValue' @@ -41,7 +40,6 @@ export const datasetCurrentAtom = selector({ set(datasetCurrentStorageAtom, dataset) // FIXME // set(viewedGeneAtom, dataset?.defaultGene ?? GENE_OPTION_NUC_SEQUENCE) - reset(inputResetAtom) } }, }) diff --git a/packages_rs/nextclade-web/src/state/inputs.state.ts b/packages_rs/nextclade-web/src/state/inputs.state.ts index be09c858f..2ddff2b13 100644 --- a/packages_rs/nextclade-web/src/state/inputs.state.ts +++ b/packages_rs/nextclade-web/src/state/inputs.state.ts @@ -1,6 +1,7 @@ import { isEmpty } from 'lodash' import { useCallback } from 'react' import { atom, selector, useRecoilState, useResetRecoilState } from 'recoil' +import { autodetectResultsAtom } from 'src/state/autodetect.state' import { AlgorithmInput } from 'src/types' import { notUndefinedOrNull } from 'src/helpers/notUndefined' @@ -11,7 +12,8 @@ export const qrySeqInputsStorageAtom = atom({ export function useQuerySeqInputs() { const [qryInputs, setQryInputs] = useRecoilState(qrySeqInputsStorageAtom) - const clearQryInputs = useResetRecoilState(qrySeqInputsStorageAtom) + const resetSeqInputsStorage = useResetRecoilState(qrySeqInputsStorageAtom) + const resetAutodetectResults = useResetRecoilState(autodetectResultsAtom) const addQryInputs = useCallback( (newInputs: AlgorithmInput[]) => { @@ -27,6 +29,11 @@ export function useQuerySeqInputs() { [setQryInputs], ) + const clearQryInputs = useCallback(() => { + resetAutodetectResults() + resetSeqInputsStorage() + }, [resetAutodetectResults, resetSeqInputsStorage]) + return { qryInputs, addQryInputs, removeQryInput, clearQryInputs } }