diff --git a/packages_rs/nextclade-web/src/components/Layout/NavigationBar.tsx b/packages_rs/nextclade-web/src/components/Layout/NavigationBar.tsx index ec09f0e25..dc325bc79 100644 --- a/packages_rs/nextclade-web/src/components/Layout/NavigationBar.tsx +++ b/packages_rs/nextclade-web/src/components/Layout/NavigationBar.tsx @@ -11,6 +11,7 @@ import { BsCaretRightFill as ArrowRight } from 'react-icons/bs' import { Link } from 'src/components/Link/Link' import { FaDocker, FaGithub, FaXTwitter, FaDiscourse } from 'react-icons/fa6' import { LinkSmart } from 'src/components/Link/LinkSmart' +import { ResultsStatus } from 'src/components/Results/ResultsStatus' import { canDownloadAtom, hasRanAtom, hasTreeAtom } from 'src/state/results.state' import styled, { useTheme } from 'styled-components' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' @@ -222,6 +223,9 @@ export function NavigationBar() { {linksLeft} + + + ) diff --git a/packages_rs/nextclade-web/src/components/Loading/Loading.tsx b/packages_rs/nextclade-web/src/components/Loading/Loading.tsx index 3daa9ee00..5ed68dc6e 100644 --- a/packages_rs/nextclade-web/src/components/Loading/Loading.tsx +++ b/packages_rs/nextclade-web/src/components/Loading/Loading.tsx @@ -1,12 +1,8 @@ -import React from 'react' - +import React, { HTMLProps } from 'react' import { useTranslationSafe as useTranslation } from 'src/helpers/useTranslationSafe' - import BrandLogoBase from 'src/assets/img/nextclade_logo.svg' import styled from 'styled-components' - -const LOADING_LOGO_SIZE = 150 -const LOADING_SPINNER_THICKNESS = 17 +import { StrictOmit } from 'ts-essentials' const Container = styled.div` display: flex; @@ -29,29 +25,29 @@ const Container = styled.div` } ` -const BrandLogo = styled(BrandLogoBase)` +const BrandLogo = styled(BrandLogoBase)<{ $size: number }>` margin: auto; - width: ${LOADING_LOGO_SIZE}px; - height: ${LOADING_LOGO_SIZE}px; + width: ${(props) => props.$size}px; + height: ${(props) => props.$size}px; box-shadow: 0 0 0 0 rgba(0, 0, 0, 1); ` -const SpinnerAnimation = styled.div` +const SpinnerAnimation = styled.div<{ $size: number }>` margin: auto; display: flex; - width: ${LOADING_LOGO_SIZE + LOADING_SPINNER_THICKNESS}px; - height: ${LOADING_LOGO_SIZE + LOADING_SPINNER_THICKNESS}px; + width: ${(props) => props.$size * 1.2}px; + height: ${(props) => props.$size * 1.2}px; overflow: hidden; - border-radius: 10px; + border-radius: ${(props) => props.$size * 0.15}px; --c1: linear-gradient(90deg, #0000 calc(100% / 3), var(--c0) 0 calc(2 * 100% / 3), #0000 0); --c2: linear-gradient(0deg, #0000 calc(100% / 3), var(--c0) 0 calc(2 * 100% / 3), #0000 0); background: var(--c1), var(--c2), var(--c1), var(--c2); - background-size: 300% 20px, 20px 300%; + background-size: 300% ${(props) => props.$size * 0.2}px, ${(props) => props.$size * 0.2}px 300%; background-repeat: no-repeat; - animation: snake 1.25s infinite linear; + animation: snake 1s infinite linear; @keyframes snake { 0% { background-position: 50% 0, 100% 100%, 0 100%, 0 0; @@ -83,17 +79,25 @@ const SpinnerAnimation = styled.div` } ` -function Loading() { +export interface LoadingProps extends StrictOmit, 'children' | 'ref' | 'as'> { + size: number +} + +export function LoadingSpinner({ size, ...rest }: LoadingProps) { + return ( + + + + ) +} + +function LoadingComponent({ size, ...rest }: LoadingProps) { const { t } = useTranslation() return ( - - - - + + ) } -export default Loading - -export const LOADING = +export const LOADING = diff --git a/packages_rs/nextclade-web/src/components/Results/ButtonBack.tsx b/packages_rs/nextclade-web/src/components/Results/ButtonBack.tsx deleted file mode 100644 index fc472ce52..000000000 --- a/packages_rs/nextclade-web/src/components/Results/ButtonBack.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useMemo } from 'react' -import { useRouter } from 'next/router' -import { Button } from 'reactstrap' -import styled from 'styled-components' -import { FaCaretLeft } from 'react-icons/fa' - -import { useTranslationSafe } from 'src/helpers/useTranslationSafe' - -export const ButtonStyled = styled(Button)` - margin: 2px 2px; - height: 38px; - width: 50px; - color: ${(props) => props.theme.gray700}; - - @media (min-width: 1200px) { - width: 140px; - } -` - -export function ButtonBack() { - const { t } = useTranslationSafe() - const text = useMemo(() => t('Back'), [t]) - const { back } = useRouter() - - return ( - - - {text} - - ) -} diff --git a/packages_rs/nextclade-web/src/components/Results/ButtonFilter.tsx b/packages_rs/nextclade-web/src/components/Results/ButtonFilter.tsx index 0433757f1..f34104c37 100644 --- a/packages_rs/nextclade-web/src/components/Results/ButtonFilter.tsx +++ b/packages_rs/nextclade-web/src/components/Results/ButtonFilter.tsx @@ -1,12 +1,13 @@ import React, { useCallback } from 'react' import { FaFilter } from 'react-icons/fa' import { useSetRecoilState } from 'recoil' - -import { PanelButton } from 'src/components/Results/PanelButton' +import type { ButtonProps } from 'reactstrap' +import { Button } from 'reactstrap' +import styled from 'styled-components' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' import { isResultsFilterPanelCollapsedAtom } from 'src/state/settings.state' -export function ButtonFilter() { +export function ButtonFilter({ ...rest }: ButtonProps) { const { t } = useTranslationSafe() const setIsResultsFilterPanelCollapsed = useSetRecoilState(isResultsFilterPanelCollapsedAtom) @@ -17,8 +18,23 @@ export function ButtonFilter() { ) return ( - - + + ) } + +export const PanelButton = styled(Button)` + margin: auto 0; + left: 20px; + top: -4px; + height: 36px; + width: 36px; + padding: 0; + color: ${(props) => props.theme.gray600}; +` diff --git a/packages_rs/nextclade-web/src/components/Results/ButtonNewRun.tsx b/packages_rs/nextclade-web/src/components/Results/ButtonNewRun.tsx deleted file mode 100644 index 41d5ffe93..000000000 --- a/packages_rs/nextclade-web/src/components/Results/ButtonNewRun.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useCallback } from 'react' -import { useRecoilState, useRecoilValue } from 'recoil' -import { canRunAtom } from 'src/state/results.state' -import styled from 'styled-components' -import { - Col, - Modal as ReactstrapModal, - ModalBody as ReactstrapModalBody, - ModalFooter as ReactstrapModalFooter, - ModalHeader as ReactstrapModalHeader, - Row, -} from 'reactstrap' -import { FaFile } from 'react-icons/fa' - -import { FilePickerAdvanced } from 'src/components/FilePicker/FilePickerAdvanced' -import { isNewRunPopupShownAtom } from 'src/state/settings.state' -import { PanelButton } from 'src/components/Results/PanelButton' -import { useTranslationSafe } from 'src/helpers/useTranslationSafe' - -export const ModalHeader = styled(ReactstrapModalHeader)` - .modal-title { - width: 100%; - } -` - -export const Modal = styled(ReactstrapModal)` - @media (max-width: 1200px) { - min-width: 80vw; - } - @media (min-width: 1201px) { - min-width: 957px; - } -` - -export const ModalBody = styled(ReactstrapModalBody)` - display: flex; - height: 66vh; - flex-direction: column; -` - -export const Scrollable = styled.div` - flex: 1; - overflow-y: auto; - - // prettier-ignore - background: - linear-gradient(#ffffff 33%, rgba(255,255,255, 0)), - linear-gradient(rgba(255,255,255, 0), #ffffff 66%) 0 100%, - radial-gradient(farthest-side at 50% 0, rgba(119,119,119, 0.5), rgba(0,0,0,0)), - radial-gradient(farthest-side at 50% 100%, rgba(119,119,119, 0.5), rgba(0,0,0,0)) 0 100%; - background-color: #ffffff; - background-repeat: no-repeat; - background-attachment: local, local, scroll, scroll; - background-size: 100% 24px, 100% 24px, 100% 8px, 100% 8px; -` - -export const ModalFooter = styled(ReactstrapModalFooter)`` - -export function ButtonNewRun() { - const { t } = useTranslationSafe() - - // const algorithmRun = useCallback(() => { - // // TODO: trigger a run - // }, []) - - const canRun = useRecoilValue(canRunAtom) - const [isNewRunPopupShown, setIsNewRunPopupShown] = useRecoilState(isNewRunPopupShownAtom) - - const open = useCallback(() => setIsNewRunPopupShown(true), [setIsNewRunPopupShown]) - const close = useCallback(() => setIsNewRunPopupShown(false), [setIsNewRunPopupShown]) - const toggle = useCallback( - () => setIsNewRunPopupShown((isNewRunPopupShown) => !isNewRunPopupShown), - [setIsNewRunPopupShown], - ) - - // const run = useCallback(() => { - // close() - // algorithmRun() - // }, [algorithmRun, close]) - - return ( - <> - - - - - - -

{t('New run')}

-
- - - - - - - - - - {/* */} - - -
- - ) -} diff --git a/packages_rs/nextclade-web/src/components/Results/ButtonRerun.tsx b/packages_rs/nextclade-web/src/components/Results/ButtonRerun.tsx deleted file mode 100644 index 6be377566..000000000 --- a/packages_rs/nextclade-web/src/components/Results/ButtonRerun.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useCallback, useMemo } from 'react' -import { useRecoilValue } from 'recoil' -import { canRunAtom } from 'src/state/results.state' -import styled from 'styled-components' -import { MdRefresh } from 'react-icons/md' - -import { useTranslationSafe } from 'src/helpers/useTranslationSafe' -import { PanelButton } from 'src/components/Results/PanelButton' - -export const RefreshIcon = styled(MdRefresh)` - width: 22px; - height: 22px; - margin-bottom: 3px; -` - -export function ButtonRerun() { - const { t } = useTranslationSafe() - const tooltip = useMemo(() => t('Run the algorithm again'), [t]) - const rerun = useCallback(() => { - // TODO - }, []) - const canRun = useRecoilValue(canRunAtom) - - return ( - - - - ) -} diff --git a/packages_rs/nextclade-web/src/components/Results/ButtonTree.tsx b/packages_rs/nextclade-web/src/components/Results/ButtonTree.tsx deleted file mode 100644 index ca7fcd36f..000000000 --- a/packages_rs/nextclade-web/src/components/Results/ButtonTree.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { useCallback, useMemo } from 'react' -import { useRouter } from 'next/router' -import { useRecoilValue } from 'recoil' -import styled from 'styled-components' - -import { hasTreeAtom } from 'src/state/results.state' -import { useTranslationSafe } from 'src/helpers/useTranslationSafe' -import { PanelButton } from 'src/components/Results/PanelButton' -import { TreeIcon } from 'src/components/Tree/TreeIcon' - -const IconContainer = styled.span` - margin-right: 0.5rem; -` - -export function ButtonTree() { - const { t } = useTranslationSafe() - const router = useRouter() - - const text = useMemo(() => t('Show phylogenetic tree'), [t]) - const hasTree = useRecoilValue(hasTreeAtom) - const showTree = useCallback(() => router.push('/tree'), [router]) - - return ( - - - - - - ) -} diff --git a/packages_rs/nextclade-web/src/components/Results/PanelButton.tsx b/packages_rs/nextclade-web/src/components/Results/PanelButton.tsx deleted file mode 100644 index 2fab72903..000000000 --- a/packages_rs/nextclade-web/src/components/Results/PanelButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { ButtonProps } from 'reactstrap' -import { Button } from 'reactstrap' - -import styled from 'styled-components' - -export type PanelButtonProps = ButtonProps - -export const PanelButton = styled(Button)` - margin: 2px 2px; - height: 38px; - width: 45px; - color: ${(props) => props.theme.gray700}; -` diff --git a/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx b/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx index 7da595c13..16c450c01 100644 --- a/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx +++ b/packages_rs/nextclade-web/src/components/Results/ResultsPage.tsx @@ -1,18 +1,11 @@ import React, { Suspense } from 'react' import { useRecoilValue } from 'recoil' import styled from 'styled-components' - import { resultsTableTotalWidthAtom } from 'src/state/settings.state' import { Layout } from 'src/components/Layout/Layout' import { GeneMapTable } from 'src/components/GeneMap/GeneMapTable' -import { ButtonNewRun } from 'src/components/Results/ButtonNewRun' -import { ButtonBack } from './ButtonBack' -import { ButtonFilter } from './ButtonFilter' -import { ButtonTree } from './ButtonTree' -import { ResultsStatus } from './ResultsStatus' -import { ResultsFilter } from './ResultsFilter' -import { ResultsTable } from './ResultsTable' -import { ButtonRerun } from './ButtonRerun' +import { ResultsFilter } from 'src/components/Results/ResultsFilter' +import { ResultsTable } from 'src/components/Results/ResultsTable' export const Container = styled.div` width: 100%; @@ -22,26 +15,6 @@ export const Container = styled.div` flex-wrap: nowrap; ` -const Header = styled.header` - flex-shrink: 1; - display: flex; -` - -const HeaderLeft = styled.header` - flex: 0; -` - -const HeaderCenter = styled.header` - flex: 1; - padding: 5px 10px; - border-radius: 5px; -` - -const HeaderRight = styled.header` - flex: 0; - display: flex; -` - const WrapperOuter = styled.div` flex: 1; width: 100%; @@ -58,10 +31,6 @@ const WrapperInner = styled.div<{ $minWidth: number }>` min-width: ${(props) => props.$minWidth}px; ` -const HeaderRightContainer = styled.div` - flex: 0; -` - const MainContent = styled.main` flex: 1; flex-basis: 100%; @@ -78,29 +47,6 @@ export function ResultsPage() { return ( -
- - - - - - - - - - - - - - - - - - - - -
- diff --git a/packages_rs/nextclade-web/src/components/Results/ResultsStatus.tsx b/packages_rs/nextclade-web/src/components/Results/ResultsStatus.tsx index 3b8b357fc..ce15808b4 100644 --- a/packages_rs/nextclade-web/src/components/Results/ResultsStatus.tsx +++ b/packages_rs/nextclade-web/src/components/Results/ResultsStatus.tsx @@ -1,30 +1,65 @@ -import React, { ReactNode, useMemo } from 'react' -import { Oval } from 'react-loader-spinner' - +import React, { ReactNode, useCallback, useMemo, useState } from 'react' import { useRecoilValue } from 'recoil' -import { AlgorithmGlobalStatus, AlgorithmSequenceStatus } from 'src/types' +import { FaCheckSquare as CheckIcon } from 'react-icons/fa' +import { IoWarning as WarnIcon } from 'react-icons/io5' import i18n from 'src/i18n/i18n' +import styled, { useTheme } from 'styled-components' +import { LoadingSpinner } from 'src/components/Loading/Loading' +import { Tooltip } from 'src/components/Results/Tooltip' import { analysisResultStatusesAtom, analysisStatusGlobalAtom } from 'src/state/results.state' import { numThreadsAtom } from 'src/state/settings.state' -import styled from 'styled-components' +import { AlgorithmGlobalStatus, AlgorithmSequenceStatus } from 'src/types' const ResultsStatusWrapper = styled.div` display: flex; - height: 32px; - margin: 0; + flex: 1; + + height: 37px; + margin-left: 0.75rem; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; + + box-shadow: inset 0 0 10px 0 #0003; + border: #0003 solid 1px; + border-radius: 3px; + padding: 0 0.5rem; + + vertical-align: middle; > span { line-height: 32px; } ` +const ResultsStatusText = styled.span` + margin: auto 0; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +` + +const ResultsStatusSpinnerWrapper = styled.span` + margin: auto 0; +` + export function ResultsStatus() { + const theme = useTheme() + const numThreads = useRecoilValue(numThreadsAtom) const statusGlobal = useRecoilValue(analysisStatusGlobalAtom) const analysisResultStatuses = useRecoilValue(analysisResultStatusesAtom) + const [showTooltip, setShowTooltip] = useState(false) + const onMouseEnter = useCallback(() => setShowTooltip(true), []) + const onMouseLeave = useCallback(() => setShowTooltip(false), []) + const { text, spinner } = useMemo(() => { - const { statusText, failureText, percent } = selectStatus(statusGlobal, analysisResultStatuses, numThreads) + const { statusText, failureText, hasFailures } = selectStatus(statusGlobal, analysisResultStatuses, numThreads) let text = {statusText} if (failureText) { @@ -37,19 +72,31 @@ export function ResultsStatus() { ) } - let spinner: ReactNode = - if (percent === 100) { - spinner = null + let spinner: ReactNode = + if (statusGlobal === AlgorithmGlobalStatus.done) { + spinner = hasFailures ? ( + + ) : ( + + ) } - return { text, spinner } - }, [analysisResultStatuses, numThreads, statusGlobal]) + }, [analysisResultStatuses, numThreads, statusGlobal, theme.success, theme.warning]) + + if (statusGlobal === AlgorithmGlobalStatus.idle) { + return null + } return ( - - {spinner} - {text} - + <> + + {spinner} + {text} + + + {text} + + ) } diff --git a/packages_rs/nextclade-web/src/components/SequenceView/SequenceSelector.tsx b/packages_rs/nextclade-web/src/components/SequenceView/SequenceSelector.tsx index 178eea04f..bb1cdd123 100644 --- a/packages_rs/nextclade-web/src/components/SequenceView/SequenceSelector.tsx +++ b/packages_rs/nextclade-web/src/components/SequenceView/SequenceSelector.tsx @@ -7,6 +7,7 @@ import type { FilterOptionOption } from 'react-select/dist/declarations/src/filt import type { FormatOptionLabelMeta } from 'react-select/dist/declarations/src/Select' import type { Theme } from 'react-select/dist/declarations/src/types' import { Badge as BadgeBase } from 'reactstrap' +import { ButtonFilter } from 'src/components/Results/ButtonFilter' import { notUndefinedOrNull } from 'src/helpers/notUndefined' import styled from 'styled-components' import { viewedGeneAtom } from 'src/state/seqViewSettings.state' @@ -90,6 +91,7 @@ export function SequenceSelector() { return (
+ -
- - - - - -
- {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} diff --git a/packages_rs/nextclade-web/src/pages/_app.tsx b/packages_rs/nextclade-web/src/pages/_app.tsx index d92728277..3116f5a8e 100644 --- a/packages_rs/nextclade-web/src/pages/_app.tsx +++ b/packages_rs/nextclade-web/src/pages/_app.tsx @@ -13,6 +13,7 @@ import { sanitizeError } from 'src/helpers/sanitizeError' import { useRunAnalysis } from 'src/hooks/useRunAnalysis' import i18nAuspice, { changeAuspiceLocale } from 'src/i18n/i18n.auspice' import { createInputFastasFromUrlParam, createInputFromUrlParamMaybe } from 'src/io/createInputFromUrlParamMaybe' +import LoadingPage from 'src/pages/loading' import { globalErrorAtom } from 'src/state/error.state' import { geneMapInputAtom, @@ -42,7 +43,6 @@ import { parseUrl } from 'src/helpers/parseUrl' import { getDatasetServerUrl, initializeDatasets } from 'src/io/fetchDatasets' import { fetchSingleDataset } from 'src/io/fetchSingleDataset' import { ErrorPopup } from 'src/components/Error/ErrorPopup' -import Loading from 'src/components/Loading/Loading' import { LinkExternal } from 'src/components/Link/LinkExternal' import { SEO } from 'src/components/Common/SEO' import { Plausible } from 'src/components/Common/Plausible' @@ -183,7 +183,7 @@ const REACT_QUERY_OPTIONS: QueryClientConfig = { export function MyApp({ Component, pageProps, router }: AppProps) { const queryClient = useMemo(() => new QueryClient(REACT_QUERY_OPTIONS), []) const { store } = useMemo(() => configureStore(), []) - const fallback = useMemo(() => , []) + const fallback = useMemo(() => , []) useEffect(() => { if (process.env.NODE_ENV !== 'development' && !['/', '/loading'].includes(router.pathname)) { diff --git a/packages_rs/nextclade-web/src/pages/loading.tsx b/packages_rs/nextclade-web/src/pages/loading.tsx index 67f87dc50..3280e515a 100644 --- a/packages_rs/nextclade-web/src/pages/loading.tsx +++ b/packages_rs/nextclade-web/src/pages/loading.tsx @@ -1 +1,7 @@ -export { default } from 'src/components/Loading/Loading' +import React from 'react' +import { Layout } from 'src/components/Layout/Layout' +import { LOADING } from 'src/components/Loading/Loading' + +export default function LoadingPage() { + return {LOADING} +}