diff --git a/client/src/components/App.js b/client/src/components/App.js index b71afa3e..c6bb1521 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -14,11 +14,15 @@ import ProtectedPageContainer from "./pages/ProtectedPageContainer"; import DatasetAboutPage from "./pages/DatasetAboutPage"; import OverviewPage from "./pages/OverviewPage"; import ExplorePage from "./pages/ExplorePage"; -import DatasetsPage from "./pages/DatasetsPage"; +// import DatasetsPage from "./pages/DatasetsPage"; import FAQPage from "./pages/FAQPage"; -import {setDevMode, saveUser} from "../actions"; +import {setDevMode, saveUser, fetchDatasets, setNode, fetchUser, fetchMessages, fetchAssays} from "../actions"; import {SITE_SUBTITLE, SITE_TITLE} from "../constants/app"; +import {EPIVAR_NODES} from "../config"; +import {useNode, useUrlEncodedNode} from "../hooks"; +import DatasetPage from "./pages/DatasetPage"; +import NotFound from "./NotFound"; const RoutedApp = () => { @@ -32,6 +36,8 @@ const RoutedApp = () => { const [contactModal, setContactModal] = useState(false); const [termsModal, setTermsModal] = useState(false); + const urlEncodedNode = useUrlEncodedNode(); + const chrom = useSelector(state => state.ui.chrom); const position = useSelector(state => state.ui.position); @@ -42,28 +48,27 @@ const RoutedApp = () => { const navigateAbout = useCallback(() => navigate("/about"), [navigate]); const navigateDatasets = useCallback(() => navigate("/datasets"), [navigate]); // TODO: remember chrom and assay: - const navigateDatasetAbout = useCallback(() => navigate("/dataset/about"), [navigate]); - const navigateOverview = useCallback(() => navigate("/dataset/overview"), [navigate]); + const navigateDatasetAbout = useCallback(() => navigate(`/datasets/${urlEncodedNode}/about`), + [navigate, urlEncodedNode]); + const navigateOverview = useCallback(() => navigate(`/datasets/${urlEncodedNode}/overview`), + [navigate, urlEncodedNode]); const navigateExplore = useCallback(() => { - if (location.pathname.startsWith("/dataset/explore")) return; + if (location.pathname.startsWith(`/datasets/${urlEncodedNode}/explore`)) { + console.debug("navigate explore - already on explore URL:", location.pathname); + return; + } if (chrom && position) { - navigate(`/dataset/explore/locus/${chrom}/${position}`); + const url = `/datasets/${urlEncodedNode}/explore/locus/${chrom}/${position}`; + console.debug("navigate explore - have URL-encoded node, chrom, and position", url); + navigate(url); } else { - navigate("/dataset/explore"); + const url = `/datasets/${urlEncodedNode}/explore`; + console.debug("navigate explore - have URL-encoded node only", url); + navigate(url); } - }, [location.pathname, chrom, position, navigate]); + }, [location.pathname, urlEncodedNode, chrom, position, navigate]); const navigateFAQ = () => navigate("/faq"); - useEffect(() => { - document.title = `${SITE_TITLE} | ${SITE_SUBTITLE}`; - - document.addEventListener("keydown", (e) => { - if (e.key === "~") { - dispatch(setDevMode(true)); - } - }); - }, []); - useEffect(() => { if (userData.isLoaded && userData.data && !userData.data.consentedToTerms) { // If the user has signed in and has not yet consented to the current terms version, @@ -110,31 +115,71 @@ const RoutedApp = () => { }; -const App = () => ( -
- - }> - } /> - } /> - } /> - } /> - - - } /> - - - }> - } /> - } /> - } /> +const App = () => { + const dispatch = useDispatch(); + const node = useNode(); + + const datasetsByNode = useSelector((state) => state.datasets.datasetsByNode); + + useEffect(() => { + document.title = `${SITE_TITLE} | ${SITE_SUBTITLE}`; + + document.addEventListener("keydown", (e) => { + if (e.key === "~") { + dispatch(setDevMode(true)); + } + }); + + // On first initialization, load datasets: + dispatch(fetchDatasets()); + }, [dispatch]); + + useEffect(() => { + const firstNode = EPIVAR_NODES[0]; + if (!window.location.pathname.match(/^\/datasets\/.+/) && !node && firstNode && datasetsByNode[firstNode]) { + // Select first node if we haven't already done so, and we're not on a URL which will set a node for us via the + // DatasetPage component effect. + console.info( + `setting node to the first one in the list (pathname=${window.location.pathname}; firstNode=${firstNode})`); + dispatch(setNode(firstNode)); + } + }, [node, datasetsByNode]); + + useEffect(() => { + if (node) { + // When the node is set / changed, load relevant data: + console.info("node changed to: ", node, "re-fetching user/messages/assays"); + dispatch(fetchUser()); + dispatch(fetchMessages()); // Server-side messages, e.g. auth errors + dispatch(fetchAssays()); + } + }, [dispatch, node]); + + return ( +
+ + }> + } /> + } /> + {/*} />*/} + }> + } /> + } /> + }> + } /> + } /> + } /> + } /> + + + } /> + } /> - } /> - } /> - - }/> - -
-); + }/> +
+
+ ); +} export default App; diff --git a/client/src/components/Controls.js b/client/src/components/Controls.js index 96cd7315..0374aed6 100644 --- a/client/src/components/Controls.js +++ b/client/src/components/Controls.js @@ -21,6 +21,7 @@ import { fetchPositions, } from '../actions.js' import {useNavigate, useParams} from "react-router-dom"; +import {useUrlEncodedNode} from "../hooks"; const defaultChrom = "rsID"; @@ -47,6 +48,7 @@ const Controls = ({toggleHelp}) => { const params = useParams(); const navigate = useNavigate(); + const urlEncodedNode = useUrlEncodedNode(); const chroms = useSelector(state => state.chroms); const {chrom, position} = useSelector(state => state.ui); const positions = useSelector(state => state.positions); @@ -141,11 +143,11 @@ const Controls = ({toggleHelp}) => { // The item assay is the tab with the most significant result - which will be // selected first by nature of ordering, thus leading the user to the most interesting // detail from the autocomplete. - navigate(`/dataset/explore/locus/${chrom}/${position}/${item.assay}`, {replace: true}); + navigate(`/datasets/${urlEncodedNode}/explore/locus/${chrom}/${position}/${item.assay}`, {replace: true}); changePosition(position); dispatch(doSearch()); setDidFirstSearch(true); - }, [list, dispatch, navigate, changePosition]); + }, [urlEncodedNode, list, dispatch, navigate, changePosition]); const moveSelection = useCallback(n => { const {length} = list; @@ -192,10 +194,10 @@ const Controls = ({toggleHelp}) => { const onClickSearch = useCallback(() => { if (!chrom || !position) return; - navigate(`/dataset/explore/locus/${chrom}/${position}`, {replace: true}); + navigate(`/datasets/${urlEncodedNode}/explore/locus/${chrom}/${position}`, {replace: true}); dispatch(doSearch()); setDidFirstSearch(true); - }, [navigate, chrom, position]); + }, [navigate, urlEncodedNode, chrom, position]); return
diff --git a/client/src/components/Footer.js b/client/src/components/Footer.js index d5400d41..900be7bd 100644 --- a/client/src/components/Footer.js +++ b/client/src/components/Footer.js @@ -3,37 +3,41 @@ import {Container} from "reactstrap"; import {Link} from "react-router-dom"; import packageJson from "../../package.json"; +import {useUrlEncodedNode} from "../hooks"; -const Footer = ({/*onContact, */onTerms}) => ( - -
-
-
- - Canadian Centre for Computational Genomics - -
+const Footer = ({/*onContact, */onTerms}) => { + const urlEncodedNode = useUrlEncodedNode(); + return ( + +
+
+
+ + Canadian Centre for Computational Genomics + +
Developed by - C3G at McGill University © 2017-2023
- - Version {packageJson.version} •{" "} - source code -
+ C3G at McGill University © 2017-2024
+ + Version {packageJson.version} •{" "} + source code +
+
+
- -
- -); + + ); +} export default Footer; diff --git a/client/src/components/Header.js b/client/src/components/Header.js index d4d313f3..773eef95 100644 --- a/client/src/components/Header.js +++ b/client/src/components/Header.js @@ -1,5 +1,5 @@ -import React from 'react' -import {useSelector} from "react-redux"; +import React, {useCallback} from 'react' +import {useDispatch, useSelector} from "react-redux"; import {Alert, Button, Container, Input} from 'reactstrap' import {Link, useLocation, useNavigate} from "react-router-dom"; @@ -8,9 +8,11 @@ import Icon from "./Icon"; import {EPIVAR_NODES} from "../config"; import {SITE_SUBTITLE, SITE_TITLE} from "../constants/app"; import {useCurrentDataset, useDatasetsByNode, useDevMode, useNode} from "../hooks"; +import {setNode} from "../actions"; export default function Header({children, onAbout, /*onDatasets, */onDatasetAbout, onOverview, onExplore, onFAQ, /*, onContact*/}) { + const dispatch = useDispatch(); const location = useLocation(); const navigate = useNavigate(); @@ -22,7 +24,24 @@ export default function Header({children, onAbout, /*onDatasets, */onDatasetAbou const datasetsByNode = useDatasetsByNode(); - console.debug("Datasets by node:", datasetsByNode); + const isLoadingData = useSelector((state) => + state.assays.isLoading || + state.samples.isLoading || + state.peaks.isLoading || + state.positions.isLoading || + state.overview.isLoading || + state.user.isLoading); + + const onDatasetChange = useCallback((e) => { + if (isLoadingData) return; + + const newNode = e.target.value; + if (newNode !== node) { + console.info("selecting node", newNode); + dispatch(setNode(newNode)); + navigate(`/datasets/${encodeURIComponent(newNode)}/about`); + } + }, [dispatch, isLoadingData, navigate]); return
@@ -43,14 +62,13 @@ export default function Header({children, onAbout, /*onDatasets, */onDatasetAbou
- + {EPIVAR_NODES.map((n) => { if (n in datasetsByNode) { const d = datasetsByNode[n]; - console.debug("Adding option for dataset", n, d); - return ; + return ; } else { - return ; + return ; } })} @@ -66,16 +84,16 @@ export default function Header({children, onAbout, /*onDatasets, */onDatasetAbou