diff --git a/src/actions/getAvailableDatasets.js b/src/actions/getAvailableDatasets.js deleted file mode 100644 index 59c78abae..000000000 --- a/src/actions/getAvailableDatasets.js +++ /dev/null @@ -1,30 +0,0 @@ -import * as types from "./types"; -import { charonAPIAddress } from "../util/globals"; -import { goTo404 } from "./navigation"; -import { fetchJSON } from "../util/serverInteraction"; - -export const getSource = () => { - let parts = window.location.pathname.toLowerCase().replace(/^\//, "").split("/"); - if (parts[0] === "status") parts = parts.slice(1); - switch (parts[0]) { - case "local": return "local"; - case "staging": return "staging"; - case "community": { - return undefined; - } default: { - return "/"; - } - } -}; - -export const getAvailableDatasets = (source) => (dispatch) => { - fetchJSON(`${charonAPIAddress}request=available&source=${source}`) - .then((res) => { - // console.log(res) - dispatch({type: types.AVAILABLE_DATASETS, source, available: res}); - }) - .catch((err) => { - console.error("Failed to get available datasets for", source, "Error type:", err.type); - dispatch(goTo404(`Couldn't get available datasets from the server for source "${source}"`)); - }); -}; diff --git a/src/actions/loadData.js b/src/actions/loadData.js index c1180e077..f1ea915dc 100644 --- a/src/actions/loadData.js +++ b/src/actions/loadData.js @@ -1,92 +1,25 @@ import queryString from "query-string"; import * as types from "./types"; import { charonAPIAddress } from "../util/globals"; -import { getDatapath, goTo404, chooseDisplayComponentFromPathname, makeDataPathFromPathname } from "./navigation"; +import { goTo404 } from "./navigation"; import { createStateFromQueryOrJSONs, createTreeTooState } from "./recomputeReduxState"; -import parseParams, { createDatapathForSecondSegment } from "../util/parseParams"; import { loadFrequencies } from "./frequencies"; +import { fetchJSON } from "../util/serverInteraction"; -export const getManifest = (dispatch, s3bucket = "live") => { - const charonErrorHandler = () => { - console.warn("Failed to get manifest JSON from server"); - - const datapath = makeDataPathFromPathname(window.location.pathname); - - dispatch({type: types.PROCEED_SANS_MANIFEST, datapath}); - }; - const processData = (data) => { - const datasets = JSON.parse(data); - // console.log("SERVER API REQUEST RETURNED:", datasets); - const availableDatasets = {pathogen: datasets.pathogen}; - const datapath = chooseDisplayComponentFromPathname(window.location.pathname) === "app" ? - getDatapath(window.location.pathname, availableDatasets) : - undefined; - dispatch({ - type: types.MANIFEST_RECEIVED, - s3bucket, - splash: datasets.splash, - availableDatasets, - user: "guest", - datapath - }); - }; - - /* who am i? */ - const query = queryString.parse(window.location.search); - const user = Object.keys(query).indexOf("user") === -1 ? "guest" : query.user; - - const xmlHttp = new XMLHttpRequest(); - xmlHttp.onload = () => { - if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { - processData(xmlHttp.responseText); - } else { - charonErrorHandler(); - } - }; - xmlHttp.onerror = charonErrorHandler; - xmlHttp.open("get", `${charonAPIAddress}request=manifest&user=${user}&s3=${s3bucket}`, true); // true for asynchronous - xmlHttp.send(null); -}; - -const getSegmentName = (datapath, availableDatasets) => { - /* this code is duplicated too many times. TODO */ - if (!availableDatasets || !datapath) { - return undefined; - } - - const paramFields = parseParams(datapath, availableDatasets).dataset; - const fields = Object.keys(paramFields).sort((a, b) => paramFields[a][0] > paramFields[b][0]); - const choices = fields.map((d) => paramFields[d][1]); - let level = availableDatasets; - for (let vi = 0; vi < fields.length; vi++) { - if (choices[vi]) { - const options = Object.keys(level[fields[vi]]).filter((d) => d !== "default"); - if (Object.keys(level).indexOf("segment") !== -1 && options.length > 1) { - return choices[vi]; - } - // move to the next level in the data set hierarchy - level = level[fields[vi]][choices[vi]]; - } - } - return undefined; -}; - - -const fetchDataAndDispatch = (dispatch, datasets, query, s3bucket, narrativeJSON) => { - const requestJSONPath = window.location.pathname; // .slice(1).replace(/_/g, "/"); - const apiPath = (jsonType) => `${charonAPIAddress}request=json&want=${requestJSONPath}&type=${jsonType}`; +const fetchDataAndDispatch = (dispatch, url, query) => { + const apiPath = (jsonType) => `${charonAPIAddress}request=json&url=${url}&type=${jsonType}`; // const treeName = getSegmentName(datasets.datapath, datasets.availableDatasets); if (query.tt) { /* SECOND TREE */ console.warn("SECOND TREE TODO -- SERVER SHOULD ADD IT TO THE TREE/UNIFIED JSON"); } - Promise.all([fetch(apiPath("meta")).then((res) => res.json()), fetch(apiPath("tree")).then((res) => res.json())]) + Promise.all([fetchJSON(apiPath("meta")), fetchJSON(apiPath("tree"))]) .then((values) => { const data = {JSONs: {meta: values[0], tree: values[1]}, query}; - if (narrativeJSON) { - data.JSONs.narrative = narrativeJSON; - } + // if (narrativeJSON) { + // data.JSONs.narrative = narrativeJSON; + // } dispatch({ type: types.CLEAN_START, ...createStateFromQueryOrJSONs(data) @@ -104,65 +37,56 @@ const fetchDataAndDispatch = (dispatch, datasets, query, s3bucket, narrativeJSON }) .catch((err) => { console.error(err.message); - dispatch(goTo404(`Couldn't load JSONs for ${requestJSONPath}`)); + dispatch(goTo404(`Couldn't load JSONs for ${url}`)); }); }; -const fetchNarrativesAndDispatch = (dispatch, datasets, query, s3bucket) => { - fetch(`${charonAPIAddress}request=narrative&name=${datasets.datapath.replace(/^\//, '').replace(/\//, '_').replace(/narratives_/, '')}`) - .then((res) => res.json()) - .then((blocks) => { - const newDatasets = {...datasets}; - newDatasets.datapath = getDatapath(blocks[0].dataset, datasets.availableDatasets); - fetchDataAndDispatch(dispatch, newDatasets, query, s3bucket, blocks); - }) - .catch((err) => { - // some coding error in handling happened. This is not the rejection of the promise you think it is! - // syntax error is akin to a 404 - console.error("Error in fetchNarrativesAndDispatch", err); - }); - -}; - -export const loadJSONs = (s3override = undefined) => { +// const fetchNarrativesAndDispatch = (dispatch, datasets, query) => { +// fetch(`${charonAPIAddress}request=narrative&name=${datasets.datapath.replace(/^\//, '').replace(/\//, '_').replace(/narratives_/, '')}`) +// .then((res) => res.json()) +// .then((blocks) => { +// const newDatasets = {...datasets}; +// newDatasets.datapath = getDatapath(blocks[0].dataset, datasets.availableDatasets); +// fetchDataAndDispatch(dispatch, newDatasets, query, blocks); +// }) +// .catch((err) => { +// // some coding error in handling happened. This is not the rejection of the promise you think it is! +// // syntax error is akin to a 404 +// console.error("Error in fetchNarrativesAndDispatch", err); +// }); +// +// }; + +export const loadJSONs = ({url = window.location.pathname, search = window.location.search} = {}) => { return (dispatch, getState) => { - const { datasets, tree } = getState(); + const { tree } = getState(); if (tree.loaded) { dispatch({type: types.DATA_INVALID}); } - const query = queryString.parse(window.location.search); - const s3bucket = s3override ? s3override : datasets.s3bucket; - if (datasets.datapath.startsWith("narrative")) { - fetchNarrativesAndDispatch(dispatch, datasets, query, s3bucket); - } else { - fetchDataAndDispatch(dispatch, datasets, query, s3bucket, false); - } - }; -}; - -export const changeS3Bucket = () => { - return (dispatch, getState) => { - const {datasets} = getState(); - const newBucket = datasets.s3bucket === "live" ? "staging" : "live"; - // 1. re-fetch the manifest - getManifest(dispatch, newBucket); - // 2. this can *only* be toggled through the app, so we must reload data - dispatch(loadJSONs(newBucket)); + const query = queryString.parse(search); + fetchDataAndDispatch(dispatch, url, query); + + // if (datasets.datapath.startsWith("narrative")) { + // fetchNarrativesAndDispatch(dispatch, datasets, query); + // } else { + // fetchDataAndDispatch(dispatch, datasets, query, false); + // } }; }; export const loadTreeToo = (name, path) => (dispatch, getState) => { - const { datasets } = getState(); - const apiCall = `${charonAPIAddress}request=json&path=${path}_tree.json&s3=${datasets.s3bucket}`; - fetch(apiCall) - .then((res) => res.json()) - .then((res) => { - const newState = createTreeTooState( - {treeTooJSON: res, oldState: getState(), segment: name} - ); - dispatch({ type: types.TREE_TOO_DATA, treeToo: newState.treeToo, controls: newState.controls, segment: name}); - }) - .catch((err) => { - console.error("Error while loading second tree", err); - }); + console.log("loadTreeToo not yet implemented"); + // const { datasets } = getState(); + // const apiCall = `${charonAPIAddress}request=json&path=${path}_tree.json`; + // fetch(apiCall) + // .then((res) => res.json()) + // .then((res) => { + // const newState = createTreeTooState( + // {treeTooJSON: res, oldState: getState(), segment: name} + // ); + // dispatch({ type: types.TREE_TOO_DATA, treeToo: newState.treeToo, controls: newState.controls, segment: name}); + // }) + // .catch((err) => { + // console.error("Error while loading second tree", err); + // }); }; diff --git a/src/actions/navigation.js b/src/actions/navigation.js index 7ade5e0d7..533ae4f0f 100644 --- a/src/actions/navigation.js +++ b/src/actions/navigation.js @@ -1,33 +1,10 @@ import queryString from "query-string"; -import parseParams from "../util/parseParams"; import { createStateFromQueryOrJSONs } from "./recomputeReduxState"; import { PAGE_CHANGE, URL_QUERY_CHANGE_WITH_COMPUTED_STATE } from "./types"; +import { loadJSONs } from "./loadData"; -// make prefix for data files with fields joined by _ instead of / as in URL -const makeDataPathFromParsedParams = (parsedParams) => { - const tmp_levels = Object.keys(parsedParams.dataset).map((d) => parsedParams.dataset[d]); - tmp_levels.sort((x, y) => x[0] > y[0]); - return tmp_levels.map((d) => d[1]).join("_"); -}; - -export const makeDataPathFromPathname = (pathname) => { - return pathname - .replace(/^\/+/, '') // strip leading - .replace(/\/+$/, '') // and trailing slashes - .replace(/\/+/g, '_'); // replacing all internal ones with underscores -}; - -/* match URL pathname to datasets (from manifest) */ -export const getDatapath = (pathname, availableDatasets) => { - if (!availableDatasets) {return undefined;} - const parsedParams = parseParams(pathname, availableDatasets); - return parsedParams.valid - ? makeDataPathFromParsedParams(parsedParams) - : makeDataPathFromPathname(pathname); -}; - -export const chooseDisplayComponentFromPathname = (pathname) => { - const parts = pathname.toLowerCase().replace(/^\/+/, "").replace(/\/+$/, "").split("/"); +export const chooseDisplayComponentFromURL = (url) => { + const parts = url.toLowerCase().replace(/^\/+/, "").replace(/\/+$/, "").split("/"); if ( !parts.length || (parts.length === 1 && parts[0] === "") || (parts.length === 1 && parts[0] === "local") || @@ -59,18 +36,19 @@ In , this causes a call to loadJSONs, which will, as part of it's dispatch, In this way, the URL query is "used". */ export const changePage = ({path, query = undefined, push = true}) => (dispatch, getState) => { - if (!path) {console.error("changePage called without a path"); return;} - const { datasets } = getState(); - const d = { - type: PAGE_CHANGE, - displayComponent: chooseDisplayComponentFromPathname(path), - errorMessage: undefined - }; - d.datapath = d.displayComponent === "app" ? getDatapath(path, datasets.availableDatasets) : undefined; - if (query !== undefined) { d.query = query; } - if (push) { d.pushState = true; } - /* check if this is "valid" - we can change it here before it is dispatched */ - dispatch(d); + if (!path) { + console.error("changePage called without a path"); + return; + } + const displayComponent = chooseDisplayComponentFromURL(path); + const { general } = getState(); + if (general.displayComponent === displayComponent && displayComponent === "app") { + dispatch(loadJSONs({url: path})); + return; + } + const action = {type: PAGE_CHANGE, displayComponent, pushState: push}; + if (query !== undefined) { action.query = query; } + dispatch(action); }; /* a 404 uses the same machinery as changePage, but it's not a thunk */ @@ -98,10 +76,11 @@ export const changePageQuery = ({queryToUse, queryToDisplay = false, push = true }; export const browserBackForward = () => (dispatch, getState) => { - const { datasets } = getState(); - /* if the pathname has changed, trigger the changePage action (will trigger new post to load, new dataset to load, etc) */ - // console.log("broswer back/forward detected. From: ", datasets.urlPath, datasets.urlSearch, "to:", window.location.pathname, window.location.search) - if (datasets.urlPath !== window.location.pathname) { + const { general } = getState(); + const potentiallyOutOfDatePathname = general.pathname; + /* differentiate between ∆pathname and ∆query (only) */ + console.log("broswer back/forward detected. From: ", potentiallyOutOfDatePathname, "to:", window.location.pathname, window.location.search) + if (potentiallyOutOfDatePathname !== window.location.pathname) { dispatch(changePage({path: window.location.pathname})); } else { dispatch(changePageQuery({queryToUse: queryString.parse(window.location.search)})); diff --git a/src/actions/types.js b/src/actions/types.js index c4b86eceb..8c495fd24 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -34,8 +34,6 @@ export const TOGGLE_PANEL_DISPLAY = "TOGGLE_PANEL_DISPLAY"; export const TRIGGER_DOWNLOAD_MODAL = "TRIGGER_DOWNLOAD_MODAL"; export const DISMISS_DOWNLOAD_MODAL = "DISMISS_DOWNLOAD_MODAL"; export const ADD_COLOR_BYS = "ADD_COLOR_BYS"; -export const MANIFEST_RECEIVED = "MANIFEST_RECEIVED"; -export const PROCEED_SANS_MANIFEST = "PROCEED_SANS_MANIFEST"; export const CHANGE_TREE_ROOT_IDX = "CHANGE_TREE_ROOT_IDX"; export const TOGGLE_NARRATIVE = "TOGGLE_NARRATIVE"; export const ENTROPY_DATA = "ENTROPY_DATA"; @@ -48,4 +46,4 @@ export const URL_QUERY_CHANGE_WITH_COMPUTED_STATE = "URL_QUERY_CHANGE_WITH_COMPU export const TREE_TOO_DATA = "TREE_TOO_DATA"; export const REMOVE_TREE_TOO = "REMOVE_TREE_TOO"; export const TOGGLE_TANGLE = "TOGGLE_TANGLE"; -export const AVAILABLE_DATASETS = "AVAILABLE_DATASETS"; +export const UPDATE_PATHNAME = "UPDATE_PATHNAME"; diff --git a/src/components/app.js b/src/components/app.js index 1c694477d..b8072fecb 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -100,8 +100,6 @@ const Overlay = ({styles, mobileDisplay, handler}) => { }; @connect((state) => ({ - readyToLoad: state.datasets.ready, - datapath: state.datasets.datapath, metadataLoaded: state.metadata.loaded, treeLoaded: state.tree.loaded, panelsToDisplay: state.controls.panelsToDisplay, @@ -135,9 +133,7 @@ class App extends React.Component { } } componentWillMount() { - if (this.props.datapath) { /* datapath (pathname) only appears after manifest JSON has arrived */ - this.props.dispatch(loadJSONs()); - } + this.props.dispatch(loadJSONs()); // choose via URL } componentDidMount() { document.addEventListener("dragover", (e) => {e.preventDefault();}, false); @@ -146,11 +142,6 @@ class App extends React.Component { return this.props.dispatch(filesDropped(e.dataTransfer.files)); }, false); } - componentDidUpdate(prevProps) { - if (prevProps.datapath !== this.props.datapath) { - this.props.dispatch(loadJSONs()); - } - } render() { /* D I M E N S I O N S */ let availableWidth = this.props.browserDimensions.width; diff --git a/src/components/controls/choose-dataset-select.js b/src/components/controls/choose-dataset-select.js index eb73054d0..73cddf184 100644 --- a/src/components/controls/choose-dataset-select.js +++ b/src/components/controls/choose-dataset-select.js @@ -1,10 +1,9 @@ import React from "react"; -import { connect } from "react-redux"; import Select from "react-select"; import { controlsWidth } from "../../util/globals"; -import { changePage } from "../../actions/navigation"; import { analyticsControlsEvent } from "../../util/googleAnalytics"; import { MAP_ANIMATION_PLAY_PAUSE_BUTTON } from "../../actions/types"; +import { changePage } from "../../actions/navigation"; class ChooseDatasetSelect extends React.Component { getStyles() { @@ -16,12 +15,11 @@ class ChooseDatasetSelect extends React.Component { }; } - // assembles a new path from the upstream choices and the new selection - // downstream choices will be set to defaults in parseParams createDataPath(dataset) { let p = (this.props.choice_tree.length > 0) ? "/" : ""; - p += this.props.source + "/" + p += this.props.source + "/"; p += this.props.choice_tree.join("/") + "/" + dataset; + p = p.replace(/\/+/, "/"); return p; } @@ -37,7 +35,7 @@ class ChooseDatasetSelect extends React.Component { data: "Play" }); } - this.props.dispatch(changePage({path: newPath, query: {}})); + this.props.dispatch(changePage({path: newPath})); } getDatasetOptions() { diff --git a/src/components/controls/choose-dataset.js b/src/components/controls/choose-dataset.js index 65347b930..7cb89a2dd 100644 --- a/src/components/controls/choose-dataset.js +++ b/src/components/controls/choose-dataset.js @@ -1,7 +1,6 @@ import React from "react"; import { connect } from "react-redux"; import ChooseDatasetSelect from "./choose-dataset-select"; -import parseParams from "../../util/parseParams"; const renderBareDataPath = (source, fields) => (
diff --git a/src/components/controls/choose-second-tree.js b/src/components/controls/choose-second-tree.js index 08816b100..71c13ca7d 100644 --- a/src/components/controls/choose-second-tree.js +++ b/src/components/controls/choose-second-tree.js @@ -4,7 +4,6 @@ import { connect } from "react-redux"; import { SelectLabel } from "../framework/select-label"; import { loadTreeToo } from "../../actions/loadData"; import { REMOVE_TREE_TOO } from "../../actions/types"; -import parseParams from "../../util/parseParams"; import { controlsWidth } from "../../util/globals"; @connect((state) => { @@ -36,11 +35,10 @@ class ChooseSecondTree extends React.Component { } } return true; - }) + }); const options = matches.map((m) => m[idxOfTree]); if (this.props.showTreeToo) options.unshift("REMOVE"); - console.log(idxOfTree, matches, options); return (
diff --git a/src/components/controls/controls.js b/src/components/controls/controls.js index f56a4e743..639011e1f 100644 --- a/src/components/controls/controls.js +++ b/src/components/controls/controls.js @@ -10,7 +10,6 @@ import ChooseMetric from "./choose-metric"; import PanelLayout from "./panel-layout"; import GeoResolution from "./geo-resolution"; import MapAnimationControls from "./map-animation"; -import DataSource from "./data-source"; import PanelToggles from "./panel-toggles"; import SearchStrains from "./search"; import ToggleTangle from "./toggle-tangle"; @@ -64,9 +63,6 @@ const Controls = ({mapOn}) => ( -
- -
); diff --git a/src/components/controls/data-source.js b/src/components/controls/data-source.js deleted file mode 100644 index f9000b89c..000000000 --- a/src/components/controls/data-source.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import PropTypes from 'prop-types'; -import { connect } from "react-redux"; -import { changeS3Bucket } from "../../actions/loadData"; -import { analyticsControlsEvent } from "../../util/googleAnalytics"; -import Toggle from "./toggle"; - -@connect((state) => ({ - analysisSlider: state.controls.analysisSlider, - panels: state.metadata.panels, - s3bucket: state.datasets.s3bucket -})) -class DataSource extends React.Component { - static propTypes = { - dispatch: PropTypes.func.isRequired, - s3bucket: PropTypes.string.isRequired - } - render() { - return ( - { - analyticsControlsEvent("change-s3-bucket"); - this.props.dispatch(changeS3Bucket()); - }} - label="Staging server (nightly builds)" - /> - ); - } -} - -export default DataSource; diff --git a/src/components/download/downloadModal.js b/src/components/download/downloadModal.js index 13e6e7689..8740a6fdb 100644 --- a/src/components/download/downloadModal.js +++ b/src/components/download/downloadModal.js @@ -45,7 +45,6 @@ export const publications = { browserDimensions: state.browserDimensions.browserDimensions, show: state.controls.showDownload, colorBy: state.controls.colorBy, - datapath: state.datasets.datapath, metadata: state.metadata, tree: state.tree, dateMin: state.controls.dateMin, @@ -154,7 +153,7 @@ class DownloadModal extends React.Component { ); } getFilePrefix() { - return "nextstrain_" + this.props.datapath.replace(/^\//, '').replace(/\//g, '_'); + return "nextstrain_" + window.location.pathname.replace(/^\//, '').replace(/\//g, '_'); } makeTextStringsForSVGExport() { const x = []; diff --git a/src/components/framework/monitor.js b/src/components/framework/monitor.js index 7b7793549..4309c016f 100644 --- a/src/components/framework/monitor.js +++ b/src/components/framework/monitor.js @@ -4,7 +4,6 @@ import { connect } from "react-redux"; import _throttle from "lodash/throttle"; import { BROWSER_DIMENSIONS, CHANGE_PANEL_LAYOUT } from "../../actions/types"; import { browserBackForward } from "../../actions/navigation"; -import { getManifest } from "../../actions/loadData"; import { twoColumnBreakpoint } from "../../util/globals"; @connect((state) => ({ @@ -25,8 +24,6 @@ class Monitor extends React.Component { document.body.appendChild(script); } componentDidMount() { - /* API call to charon to get initial datasets etc (needed to load the splash page) */ - getManifest(this.props.dispatch); /* don't need initial dimensions - they're in the redux store on load */ window.addEventListener( // future resizes "resize", diff --git a/src/components/info/info.js b/src/components/info/info.js index 7cea2016f..9703fef22 100644 --- a/src/components/info/info.js +++ b/src/components/info/info.js @@ -47,7 +47,6 @@ export const createSummary = (virus_count, nodes, filters, visibility, visibleSt @connect((state) => { return { - s3bucket: state.datasets.s3bucket, browserDimensions: state.browserDimensions.browserDimensions, filters: state.controls.filters, animationPlayPauseButton: state.controls.animationPlayPauseButton, @@ -270,10 +269,6 @@ class Info extends React.Component { {title}
- {/* if staging, let the user know */} - {this.props.s3bucket === "staging" ? ( - {"Currently viewing data from the staging server. "} - ) : null} {animating ? `Animation in progress. ` : null} {this.props.selectedStrain ? this.selectedStrainButton(this.props.selectedStrain) : null} {/* part 1 - the summary */} diff --git a/src/components/splash/index.js b/src/components/splash/index.js index 5189dc248..ca66be925 100644 --- a/src/components/splash/index.js +++ b/src/components/splash/index.js @@ -25,7 +25,7 @@ const formatDataset = (fields, dispatch) => { @connect((state) => ({ - errorMessage: state.datasets.errorMessage + errorMessage: state.general.message })) class Splash extends React.Component { constructor(props) { diff --git a/src/index.js b/src/index.js index 7d2da0804..aa532b1dd 100644 --- a/src/index.js +++ b/src/index.js @@ -32,7 +32,7 @@ if (!window.NEXTSTRAIN) {window.NEXTSTRAIN = {};} /* google analytics */ ReactGA.initialize(process.env.NODE_ENV === "production" ? "UA-92687617-1" : "UA-92687617-2"); -@connect((state) => ({displayComponent: state.datasets.displayComponent})) +@connect((state) => ({displayComponent: state.general.displayComponent})) class MainComponentSwitch extends React.Component { render() { // console.log("MainComponentSwitch running (should be infrequent!)", this.props.displayComponent) @@ -41,7 +41,7 @@ class MainComponentSwitch extends React.Component { case "app" : return (); case "status" : return (); default: - console.error(`reduxStore.datasets.displayComponent is invalid (${this.props.displayComponent})`); + console.error(`reduxStore.general.displayComponent is invalid (${this.props.displayComponent})`); return (); } } diff --git a/src/middleware/changeURL.js b/src/middleware/changeURL.js index edadbf66b..cf5ecf4bc 100644 --- a/src/middleware/changeURL.js +++ b/src/middleware/changeURL.js @@ -104,7 +104,7 @@ export const changeURLMiddleware = (store) => (next) => (action) => { case types.PAGE_CHANGE: if (action.query) { query = action.query; - } else if (action.displayComponent !== state.datasets.displayComponent) { + } else if (action.displayComponent !== state.general.displayComponent) { query = {}; } break; @@ -117,13 +117,10 @@ export const changeURLMiddleware = (store) => (next) => (action) => { case types.CLEAN_START: if (action.url) pathname = action.url; break; - case types.MANIFEST_RECEIVED: - pathname = action.datapath.replace(/_/g, "/"); - break; case types.PAGE_CHANGE: /* desired behaviour depends on the displayComponent selected... */ if (action.displayComponent === "app") { - pathname = action.datapath.replace(/_/g, "/"); + // pathname = action.datapath.replace(/_/g, "/"); } else if (action.displayComponent === "splash") { pathname = "/"; } else if (pathname.startsWith(`/${action.displayComponent}`)) { @@ -152,9 +149,9 @@ export const changeURLMiddleware = (store) => (next) => (action) => { } else { window.history.replaceState({}, "", newURLString); } - next({type: types.URL, path: pathname, query: search}); - } else if (pathname !== state.datasets.urlPath && action.type === types.PAGE_CHANGE) { - next({type: types.URL, path: pathname, query: search}); + next({type: types.UPDATE_PATHNAME, pathname: pathname}); + } else if (pathname !== state.general.pathname && action.type === types.PAGE_CHANGE) { + next({type: types.UPDATE_PATHNAME, pathname: pathname}); } return result; diff --git a/src/reducers/controls.js b/src/reducers/controls.js index 490c202ca..43fac7f67 100644 --- a/src/reducers/controls.js +++ b/src/reducers/controls.js @@ -75,7 +75,6 @@ const Controls = (state = getDefaultControlsState(), action) => { switch (action.type) { case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: /* fallthrough */ case types.CLEAN_START: - console.log("CLEAN_START...", action.controls) return action.controls; case types.BRANCH_MOUSEENTER: return Object.assign({}, state, { diff --git a/src/reducers/datasets.js b/src/reducers/datasets.js deleted file mode 100644 index 5a0a3cf11..000000000 --- a/src/reducers/datasets.js +++ /dev/null @@ -1,50 +0,0 @@ -import * as types from "../actions/types"; -import { chooseDisplayComponentFromPathname } from "../actions/navigation"; - -const datasets = (state = { - s3bucket: "live", - available: undefined, - availableDatasets: undefined, - splash: undefined, - source: undefined, - datapath: undefined, // e.g. "zika" or "flu_h3n2_12y" - displayComponent: chooseDisplayComponentFromPathname(window.location.pathname), - urlPath: window.location.pathname, - urlQuery: window.location.search, - errorMessage: undefined -}, action) => { - switch (action.type) { - case types.PAGE_CHANGE: { - return Object.assign({}, state, { - displayComponent: action.displayComponent, - datapath: action.datapath, - errorMessage: action.errorMessage - }); - } case types.MANIFEST_RECEIVED: { - const newState = { - s3bucket: action.s3bucket, - splash: action.splash, - availableDatasets: action.availableDatasets, - user: action.user, - datapath: action.datapath - }; - return Object.assign({}, state, newState); - } case types.PROCEED_SANS_MANIFEST: { - return Object.assign({}, state, {datapath: action.datapath}); - } case types.URL: { - return Object.assign({}, state, { - urlPath: action.path, - urlSearch: action.query - }); - } case types.AVAILABLE_DATASETS: { - return Object.assign({}, state, { - source: action.source, - available: action.available - }); - } default: { - return state; - } - } -}; - -export default datasets; diff --git a/src/reducers/general.js b/src/reducers/general.js new file mode 100644 index 000000000..b33531146 --- /dev/null +++ b/src/reducers/general.js @@ -0,0 +1,28 @@ +import * as types from "../actions/types"; +import { chooseDisplayComponentFromURL } from "../actions/navigation"; + +/* the store for cross-cutting state -- that is, state +not limited to +*/ + +const general = (state = { + displayComponent: chooseDisplayComponentFromURL(window.location.pathname), + errorMessage: undefined, + pathname: window.location.pathname // keep a copy of what the app "thinks" the pathname is +}, action) => { + switch (action.type) { + case types.PAGE_CHANGE: + return Object.assign({}, state, { + displayComponent: action.displayComponent, + errorMessage: action.errorMessage + }); + case types.UPDATE_PATHNAME: + return Object.assign({}, state, { + pathname: action.pathname + }); + default: + return state; + } +}; + +export default general; diff --git a/src/reducers/index.js b/src/reducers/index.js index 045044439..6f9656e1e 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -6,9 +6,9 @@ import entropy from "./entropy"; import controls from "./controls"; import browserDimensions from "./browserDimensions"; import notifications from "./notifications"; -import datasets from "./datasets"; import narrative from "./narrative"; import treeToo from "./treeToo"; +import general from "./general"; const rootReducer = combineReducers({ metadata, @@ -18,9 +18,9 @@ const rootReducer = combineReducers({ entropy, browserDimensions, notifications, - datasets, narrative, - treeToo + treeToo, + general }); export default rootReducer; diff --git a/src/server/charon.js b/src/server/charon.js index a065fcb98..98799ae13 100644 --- a/src/server/charon.js +++ b/src/server/charon.js @@ -1,6 +1,5 @@ /* eslint no-console: off */ const queryString = require("query-string"); -const getFiles = require('./getFiles'); const globals = require("./globals"); const serverNarratives = require('./narratives'); const fs = require('fs'); @@ -25,10 +24,7 @@ const applyCharonToApp = (app) => { return; // 404 } switch (query.request) { - case "manifest": { - getFiles.getManifest(query, res); - break; - } case "narrative": { + case "narrative": { serverNarratives.serveNarrative(query, res); break; } case "rebuildManifest": { @@ -36,9 +32,9 @@ const applyCharonToApp = (app) => { break; } case "json": { let pathname, idealUrl, datasetFields; - const source = sourceSelect.getSource(query.want); + const source = sourceSelect.getSource(query.url); try { - [idealUrl, datasetFields, pathname] = sourceSelect.constructPathToGet(source, query.want, query.type); + [idealUrl, datasetFields, pathname] = sourceSelect.constructPathToGet(source, query.url, query.type); } catch (e) { console.error("Problem parsing the query (didn't attempt to fetch)\n", e.message); res.status(500).send('FETCHING ERROR'); // Perhaps handle more globally... @@ -80,7 +76,7 @@ const applyCharonToApp = (app) => { } break; } default: { - console.warn("Query rejected (unknown want) -- " + req.originalUrl); + console.warn("Query rejected (unknown) -- " + req.originalUrl); res.status(500).send('FETCHING ERROR'); // Perhaps handle more globally... } } diff --git a/src/server/getFiles.js b/src/server/getFiles.js deleted file mode 100644 index e4ef2ebdd..000000000 --- a/src/server/getFiles.js +++ /dev/null @@ -1,74 +0,0 @@ -/* eslint no-console: off */ -// const fs = require('fs'); -const path = require("path"); -// const fetch = require('node-fetch'); // not needed for local data -const request = require('request'); -// const prettyjson = require('prettyjson'); - -// const validUsers = ['guest', 'mumps', 'lassa']; - -const getDataFile = (res, filePath, s3) => { - if (global.LOCAL_DATA) { - res.sendFile(path.join(global.LOCAL_DATA_PATH, filePath)); - } else if (s3 === "staging") { - request(global.REMOTE_DATA_STAGING_BASEURL + filePath).pipe(res); - /* TODO explore https://www.npmjs.com/package/cached-request */ - } else { - // we deliberately don't ensure that s3===live, as this should be the default - request(global.REMOTE_DATA_LIVE_BASEURL + filePath).pipe(res); - /* TODO explore https://www.npmjs.com/package/cached-request */ - } -}; - -// const getStaticFile = (res, filePath) => { -// if (global.LOCAL_STATIC) { -// res.sendFile(path.join(global.LOCAL_STATIC_PATH, filePath)); -// } else { -// request(global.REMOTE_STATIC_BASEURL + filePath).pipe(res); -// /* TODO explore https://www.npmjs.com/package/cached-request */ -// } -// }; - -// const fetchS3 = (res, filePath) => { -// fetch(s3 + filePath) -// .then((fetchRes) => fetchRes.json()) -// .then((json) => { -// res.json(json); -// // if (successMsg) {console.log(successMsg);} -// }) -// .catch((err) => { -// // if (errMsg) {console.error(errMsg);} -// console.error(err); -// }); -// }; - -const getManifest = (query, res) => { - if (Object.keys(query).indexOf("user") === -1) { - res.status(404).send('No user defined'); - return; - } - // if (validUsers.indexOf(query.user) === -1) { - // res.status(404).send('Invalid user'); - // return; - // } - getDataFile(res, 'manifest_' + query.user + '.json', query.s3); -}; - -const getSplashImage = (query, res) => { - getDataFile(res, query.src, query.s3); -}; - -// const getImage = (query, res) => { -// getStaticFile(res, query.src); -// }; - -const getDatasetJson = (query, res) => { - getDataFile(res, query.path, query.s3); -}; - -module.exports = { - getManifest, - getSplashImage, - // getImage, - getDatasetJson -}; diff --git a/src/store/index.js b/src/store/index.js index 327b36f44..631ee0d75 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -7,7 +7,7 @@ import { loggingMiddleware } from "../middleware/logActions"; // eslint-disable- const middleware = [ thunk, changeURLMiddleware, // eslint-disable-line comma-dangle - // loggingMiddleware + loggingMiddleware ]; let CreateStoreWithMiddleware; diff --git a/src/util/parseParams.js b/src/util/parseParams.js deleted file mode 100644 index 8a31709d9..000000000 --- a/src/util/parseParams.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * utility function that takes a string (splat as in flu/h3n2/3y) - * parses it, and compares it against the (hardcoded) available datasets - * identifies whether that string specifies a valid and complete dataset - * incomplete path are augmented with defaults - * - * The dataset structure = {Key1: [val1a, val2a], Key2: [val2a, val2b]...} - * the keys are applicable categories, e.g "pathogen" and "lineage" - * first value: idx of the category (pathogen is 0, lineage is 1 and so on) - * second value: the selected option, e.g. "ebola" or "H7N9", or "NA" - */ - -const parseParams = (path, datasets) => { - // console.log("parseParams. path in:", path, datasets) - let params; // split path at '/', if path === "", set params to [] - if (path.length) { - params = path.replace(/_/g, "/").split("/").filter((d) => d !== ""); - } else { - params = []; - } - const config = { - valid: true, // the URL is incorrect and we don't know what to do! - incomplete: false, // the URL, as passed in, is incomplete - dataset: {}, // see above - fullsplat: "", // just the URL - search: "" // the URL search query - }; - - let elemType; // object whose keys are the available choices (e.g. "zika" and "ebola") - let idx; // the index of the current param (of params) - let elem; // the choice (e.g. "flu", "h7n9", "ebola") - let datasetSlice = datasets; // This is usually an object, sometimes a string - for (idx = 0; idx < params.length; idx++) { - elem = params[idx]; - if (typeof datasetSlice !== "string" && Object.keys(datasetSlice).length) { - elemType = Object.keys(datasetSlice)[0]; - // elemType will be "pathogen", "lineage" or "segment" - if (typeof datasetSlice[elemType][elem] === "undefined") { - // the elem (the param requested) is NOT available in the dataset. BAIL. - console.warn("in manifest, ", elem, " not found at level of ", elemType); - config.valid = false; - return config; - } - // console.log("(from URL)", elemType, "=", elem); - // yes, the param (at the current level) is valid... - // assign valid path element and move datasetSlice down in the hierarchy - config.dataset[elemType] = [idx, elem]; - config.fullsplat += "/" + elem; - datasetSlice = datasetSlice[elemType][elem]; - // now dataset may be {}, {...} or "..." - if (typeof datasetSlice === "string" && datasetSlice !== "") { - config.search = datasetSlice; - config.incomplete = true; // so the search gets put in! - } - } else { - // so we've got a param, but we've run out of levels! - // mark as "incomplete" so the URL will change - config.incomplete = true; - } - - } - // parse any remaining levels of globals.datasets - // this stops when we encounter 'xx: {}' as Object.keys({}).length==0 - // this both populates the dataset property, and sets defaults, - // else the URL wouldn't be valid so the data request would 404 - while (typeof datasetSlice !== "string" && Object.keys(datasetSlice).length) { - elemType = Object.keys(datasetSlice)[0]; - elem = datasetSlice[elemType]["default"]; - // console.log("filling default ", elemType," as ", elem); - config.dataset[elemType] = [idx, elem]; - // double check specified default is actually valid - if (typeof datasetSlice[elemType][elem] === "undefined") { - config.valid = false; - console.warn("incorrect / no default set"); - return config; - } - config.incomplete = true; // i.e. the url, as specified, needs to be updated - config.fullsplat += "/" + elem; - config.dataset[elemType] = [idx, elem]; - // move to next level - datasetSlice = datasetSlice[elemType][elem]; - // now dataset may be {}, {...} or "..." - if (typeof datasetSlice === "string" && datasetSlice !== "") { - config.search = datasetSlice; - // already config.incomplete is true - } - idx++; - } - - return config; -}; - -export const createDatapathForSecondSegment = (newSegment, datapath, availableDatasets) => { - const parts = datapath.split('_'); - let level = availableDatasets['pathogen']; - let i = 0; - let key; - for (;;) { - key = parts[i++]; - if (Object.keys(level).indexOf(key) === -1) { - console.error(`Second segment ${newSegment} not found in available datasets!`); - return false; /* ERROR */ - } - /* jump to the level and through the next one (singleton) */ - level = level[key]; - level = level[Object.keys(level)[0]]; - if (Object.keys(level).indexOf(newSegment) !== -1) { - break; - } - } - parts[i] = newSegment; - // console.log("NEW PARTS:", parts); - return parts.join('_'); -}; - -export default parseParams; diff --git a/test/request_urls.js b/test/request_urls.js index c64d79160..1ae44e717 100644 --- a/test/request_urls.js +++ b/test/request_urls.js @@ -39,5 +39,3 @@ const isValidURLCallback = (url) => { it('loads the spash page as expected', isValidURLCallback('http://localhost:4000')); it('loads /zika', isValidURLCallback('http://localhost:4000/zika')); -it('returns a valid guest manifest (JSON) via API', isValidJSONCallback('http://localhost:4000/charon?request=manifest&user=guest')); -it('returns a valid mumps manifest (JSON) via API', isValidJSONCallback('http://localhost:4000/charon?request=manifest&user=mumps'));