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}
+}