Skip to content

Commit

Permalink
Merge pull request #15 from c3g/feat/client-node-switching
Browse files Browse the repository at this point in the history
feat(client): support node switching
  • Loading branch information
davidlougheed authored Jan 18, 2024
2 parents 8a62804 + e9ac9d5 commit d5d8f54
Show file tree
Hide file tree
Showing 23 changed files with 407 additions and 256 deletions.
129 changes: 87 additions & 42 deletions client/src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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);

Expand All @@ -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,
Expand Down Expand Up @@ -110,31 +115,71 @@ const RoutedApp = () => {
};


const App = () => (
<div className='App'>
<Routes>
<Route path="/" element={<RoutedApp />}>
<Route index={true} element={<Navigate to="/about" replace={true} />} />
<Route path="about" element={<AboutPage />} />
<Route path="datasets" element={<DatasetsPage />} />
<Route path="dataset/about" element={<DatasetAboutPage />} />
<Route path="dataset/overview" element={<ProtectedPageContainer>
<OverviewPage />
</ProtectedPageContainer>} />
<Route path="dataset/explore" element={<ProtectedPageContainer>
<ExplorePage />
</ProtectedPageContainer>}>
<Route index={true} element={<PeakResults />} />
<Route path="locus/:chrom/:position/:assay" element={<PeakResults />} />
<Route path="locus/:chrom/:position" element={<PeakResults />} />
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 (
<div className='App'>
<Routes>
<Route path="/" element={<RoutedApp />}>
<Route index={true} element={<Navigate to="/about" replace={true} />} />
<Route path="about" element={<AboutPage />} />
{/*<Route path="datasets" element={<DatasetsPage />} />*/}
<Route path="datasets/:node" element={<DatasetPage />}>
<Route path="about" element={<DatasetAboutPage />} />
<Route path="overview" element={<ProtectedPageContainer><OverviewPage /></ProtectedPageContainer>} />
<Route path="explore" element={<ProtectedPageContainer><ExplorePage /></ProtectedPageContainer>}>
<Route index={true} element={<PeakResults />} />
<Route path="locus/:chrom/:position/:assay" element={<PeakResults />} />
<Route path="locus/:chrom/:position" element={<PeakResults />} />
<Route path="*" element={<NotFound context="no explore route" />} />
</Route>
</Route>
<Route path="faq" element={<FAQPage />} />
<Route path="auth-failure" element={<div />} />
</Route>
<Route path="faq" element={<FAQPage />} />
<Route path="auth-failure" element={<div />} />
</Route>
<Route path="*" element={<Navigate to="/" />}/>
</Routes>
</div>
);
<Route path="*" element={<Navigate to="/" />}/>
</Routes>
</div>
);
}


export default App;
10 changes: 6 additions & 4 deletions client/src/components/Controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
fetchPositions,
} from '../actions.js'
import {useNavigate, useParams} from "react-router-dom";
import {useUrlEncodedNode} from "../hooks";

const defaultChrom = "rsID";

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 <div className={cx('Controls', { didFirstSearch })}>
<div className='Controls__content'>
Expand Down
58 changes: 31 additions & 27 deletions client/src/components/Footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}) => (
<Container>
<div className="Footer">
<div className="Footer__text">
<div className="Footer__logo">
<a href="https://computationalgenomics.ca" target="_blank" rel="nofollow">
<img src="/c3g_logo_small.png" alt="Canadian Centre for Computational Genomics" />
</a>
<div>
const Footer = ({/*onContact, */onTerms}) => {
const urlEncodedNode = useUrlEncodedNode();
return (
<Container>
<div className="Footer">
<div className="Footer__text">
<div className="Footer__logo">
<a href="https://computationalgenomics.ca" target="_blank" rel="nofollow">
<img src="/c3g_logo_small.png" alt="Canadian Centre for Computational Genomics" />
</a>
<div>
<span>Developed by <a href="https://computationalgenomics.ca" target="_blank" rel="nofollow">
C3G</a> at McGill University &copy; 2017-2023</span><br />
<em>
Version {packageJson.version} &bull;{" "}
<a href="https://github.com/c3g/epivar-browser" target="_blank" rel="nofollow">source code</a>
</em><br />
C3G</a> at McGill University &copy; 2017-2024</span><br />
<em>
Version {packageJson.version} &bull;{" "}
<a href="https://github.com/c3g/epivar-browser" target="_blank" rel="nofollow">source code</a>
</em><br />
</div>
</div>
</div>
<nav className="Footer__nav">
<ul>
{/*<li><Link to="/datasets">Datasets</Link></li>*/}
<li><Link to={`/datasets/${urlEncodedNode}/about`}>About Dataset</Link></li>
<li><Link to={`/datasets/${urlEncodedNode}/overview`}>Overview</Link></li>
<li><Link to={`/datasets/${urlEncodedNode}/explore`}>Explore</Link></li>
<li><Link to="/about">About EpiVar</Link></li>
<li><a href="#" onClick={onTerms}>Terms of Use</a></li>
</ul>
</nav>
</div>
<nav className="Footer__nav">
<ul>
{/*<li><Link to="/datasets">Datasets</Link></li>*/}
<li><Link to="/dataset/about">About Dataset</Link></li>
<li><Link to="/dataset/overview">Overview</Link></li>
<li><Link to="/dataset/explore">Explore</Link></li>
<li><Link to="/about">About EpiVar</Link></li>
<li><a href="#" onClick={onTerms}>Terms of Use</a></li>
</ul>
</nav>
</div>
</Container>
);
</Container>
);
}

export default Footer;
38 changes: 28 additions & 10 deletions client/src/components/Header.js
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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();

Expand All @@ -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 <div>
<div className='Header'>
Expand All @@ -43,14 +62,13 @@ export default function Header({children, onAbout, /*onDatasets, */onDatasetAbou
<div className="Header__dataset">
<div>
<label htmlFor="dataset-selector">Dataset:</label>
<Input type="select" id="dataset-selector" value={node ?? undefined}>
<Input type="select" id="dataset-selector" value={node ?? undefined} onChange={onDatasetChange}>
{EPIVAR_NODES.map((n) => {
if (n in datasetsByNode) {
const d = datasetsByNode[n];
console.debug("Adding option for dataset", n, d);
return <option key={n} >{d?.title ?? ""} ({d?.assembly ?? ""})</option>;
return <option key={n} value={n}>{d?.title ?? ""} ({d?.assembly ?? ""})</option>;
} else {
return <option key={n} disabled={true}>{n} (unreachable)</option>;
return <option key={n} value={n} disabled={true}>{n} (unreachable)</option>;
}
})}
</Input>
Expand All @@ -66,16 +84,16 @@ export default function Header({children, onAbout, /*onDatasets, */onDatasetAbou
<div className="Header__highlight_group">
<Button color="link"
disabled={!dataset}
className={location.pathname.startsWith("/dataset/about") ? "active" : ""}
className={location.pathname.match(/^\/datasets\/.*\/about/) ? "active" : ""}
onClick={onDatasetAbout}>
<Icon name="info-circle" bootstrap={true}/>About Dataset</Button>
<Button color="link"
disabled={!dataset}
className={location.pathname.startsWith("/dataset/overview") ? "active" : ""}
className={location.pathname.match(/^\/datasets\/.*\/overview/) ? "active" : ""}
onClick={onOverview}><Icon name="graph-up" bootstrap={true} />Overview</Button>
<Button color="link"
disabled={!dataset}
className={"highlight" + (location.pathname.startsWith("/dataset/explore") ? " active" : "")}
className={"highlight" + (location.pathname.match(/^\/datasets\/.*\/explore/) ? " active" : "")}
onClick={onExplore}><Icon name="search" bootstrap={true} />Explore</Button>
</div>
<Button color="link"
Expand Down
18 changes: 18 additions & 0 deletions client/src/components/NotFound.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, {useEffect} from "react";
import {useLocation} from "react-router-dom";

const NotFound = ({context}) => {
const location = useLocation();

useEffect(() => {
console.debug("not found location", location);
}, [location]);

return (
<div>
<h2>Not Found{context ? `: ${context}` : null}</h2>
</div>
);
};

export default NotFound;
2 changes: 1 addition & 1 deletion client/src/components/PeakBoxplot.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {useNode, useCurrentDataset} from "../hooks";
function PeakBoxplot({ title, peak, /*values = defaultValues*/ }) {
const dataset = useCurrentDataset();

const ethnicities = useMemo(() => dataset.data?.ethnicities ?? [], [dataset]);
const ethnicities = useMemo(() => dataset?.ethnicities ?? [], [dataset]);
const node = useNode();
const usePrecomputed = useSelector(state => state.ui.usePrecomputed);

Expand Down
Loading

0 comments on commit d5d8f54

Please sign in to comment.