From c73f590d8ebdba1f00bf473c65bb510f6a14eb15 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Fri, 2 Nov 2018 15:19:39 -0700 Subject: [PATCH] Allow extensions to replace splash page The splash page can now be replaced by providing a compatable react component. --- server.js | 4 +- src/components/splash/index.js | 136 ++++++-------------------------- src/components/splash/splash.js | 123 +++++++++++++++++++++++++++++ src/index.js | 2 - src/util/errorBoundry.js | 30 +++++++ src/util/extensions.js | 33 +++++--- webpack.config.dev.js | 22 +++++- 7 files changed, 219 insertions(+), 131 deletions(-) create mode 100644 src/components/splash/splash.js create mode 100644 src/util/errorBoundry.js diff --git a/server.js b/server.js index 290e0891d..94d050933 100755 --- a/server.js +++ b/server.js @@ -41,7 +41,9 @@ if (args.dev) { /* if we are in dev-mode, we need to import specific libraries & set up hot reloading */ const webpack = require("webpack"); // eslint-disable-line if (args.extend) { - process.env.EXTEND_AUSPICE_JSON = JSON.stringify(JSON.parse(fs.readFileSync(args.extend, {encoding: 'utf8'}))); + const extensionData = JSON.parse(fs.readFileSync(args.extend, {encoding: 'utf8'})); + process.env.EXTENSION_PATH = path.dirname(args.extend); + process.env.EXTEND_AUSPICE_DATA = JSON.stringify(extensionData); } const webpackConfig = require(process.env.WEBPACK_CONFIG ? process.env.WEBPACK_CONFIG : './webpack.config.dev'); // eslint-disable-line const compiler = webpack(webpackConfig); diff --git a/src/components/splash/index.js b/src/components/splash/index.js index aa22442ba..c5ca9ea64 100644 --- a/src/components/splash/index.js +++ b/src/components/splash/index.js @@ -1,13 +1,17 @@ import React from "react"; import { connect } from "react-redux"; -import Title from "../framework/title"; -import NavBar from "../navBar"; -import Flex from "../../components/framework/flex"; -import { logos } from "./logos"; -import { CenterContent } from "./centerContent"; -import { changePage } from "../../actions/navigation"; +import DefaultSplashContent from "./splash"; +import { hasExtension, getExtension } from "../../util/extensions"; +import ErrorBoundary from "../../util/errorBoundry"; import { fetchJSON } from "../../util/serverInteraction"; import { charonAPIAddress, controlsHiddenWidth } from "../../util/globals"; +import { changePage } from "../../actions/navigation"; + +const SplashContent = hasExtension("splashComponent") ? + getExtension("splashComponent") : + DefaultSplashContent; +/* TODO: check that when compiling DefaultSplashContent isn't included if extension is defined */ + @connect((state) => ({ errorMessage: state.general.errorMessage, @@ -28,116 +32,20 @@ class Splash extends React.Component { console.warn(err.message); }); } - formatDataset(fields) { - let path = fields.join("/"); - if (this.state.source !== "live") { - path = this.state.source + "/" + path; - } - return ( -
  • -
    this.props.dispatch(changePage({path, push: true}))} - > - {path} -
    -
  • - ); - } - listAvailable() { - if (!this.state.source) return null; - if (!this.state.available) { - if (this.state.source === "live" || this.state.source === "staging") { - return ( - -
    - {`No available ${this.state.source} datasets. Try "/local/" for local datasets.`} -
    -
    - ); - } - return null; - } - - let listJSX; - /* make two columns for wide screens */ - if (this.props.browserDimensions.width > 1000) { - const secondColumnStart = Math.ceil(this.state.available.length / 2); - listJSX = ( -
    -
    -
      - {this.state.available.slice(0, secondColumnStart).map((data) => this.formatDataset(data))} -
    -
    -
    -
      - {this.state.available.slice(secondColumnStart).map((data) => this.formatDataset(data))} -
    -
    -
    - ); - } else { - listJSX = ( - - ); - } - return ( - -
    -
    - {`Available ${this.state.narratives ? "Narratives" : "Datasets"} for source ${this.state.source}`} -
    - {listJSX} -
    -
    - ); - } render() { - const isMobile = this.props.browserDimensions.width < controlsHiddenWidth; return ( -
    - - -
    - - - </Flex> - <div className="row"> - <h1 style={{textAlign: "center", marginTop: "-10px", fontSize: "29px"}}> Real-time tracking of virus evolution </h1> - </div> - {/* First: either display the error message or the intro-paragraph */} - {this.props.errorMessage || this.state.errorMessage ? ( - <CenterContent> - <div> - <p style={{color: "rgb(222, 60, 38)", fontWeight: 600, fontSize: "24px"}}> - {"😱 404, or an error has occured 😱"} - </p> - <p style={{color: "rgb(222, 60, 38)", fontWeight: 400, fontSize: "18px"}}> - {`Details: ${this.props.errorMessage || this.state.errorMessage}`} - </p> - <p style={{fontSize: "16px"}}> - {"If this keeps happening, or you believe this is a bug, please "} - <a href={"mailto:hello@nextstrain.org"}>{"get in contact with us."}</a> - </p> - </div> - </CenterContent> - ) : ( - <p style={{maxWidth: 600, marginTop: 0, marginRight: "auto", marginBottom: 20, marginLeft: "auto", textAlign: "center", fontSize: 16, fontWeight: 300, lineHeight: 1.42857143}}> - Nextstrain is an open-source project to harness the scientific and public health potential of pathogen genome data. We provide a continually-updated view of publicly available data with powerful analytics and visualizations showing pathogen evolution and epidemic spread. Our goal is to aid epidemiological understanding and improve outbreak response. - </p> - )} - {/* Secondly, list the available datasets / narratives */} - {this.listAvailable()} - {/* Finally, the footer (logos) */} - <CenterContent> - {logos} - </CenterContent> - - </div> - </div> + <ErrorBoundary> + <SplashContent + isMobile={this.props.browserDimensions.width < controlsHiddenWidth} + source={this.state.source} + available={this.state.available} + narratives={this.state.narratives} + browserDimensions={this.props.browserDimensions} + dispatch={this.props.dispatch} + errorMessage={this.props.errorMessage || this.state.errorMessage} + changePage={changePage} + /> + </ErrorBoundary> ); } } diff --git a/src/components/splash/splash.js b/src/components/splash/splash.js new file mode 100644 index 000000000..e5df86691 --- /dev/null +++ b/src/components/splash/splash.js @@ -0,0 +1,123 @@ +import React from "react"; +import Title from "../framework/title"; +import NavBar from "../navBar"; +import Flex from "../../components/framework/flex"; +import { logos } from "./logos"; +import { CenterContent } from "./centerContent"; + + +const formatDataset = (source, fields, dispatch, changePage) => { + let path = fields.join("/"); + if (source !== "live") { + path = source + "/" + path; + } + return ( + <li key={path}> + <div + style={{color: "#5097BA", textDecoration: "none", cursor: "pointer", fontWeight: "400", fontSize: "94%"}} + onClick={() => dispatch(changePage({path, push: true}))} + > + {path} + </div> + </li> + ); +}; + +const listAvailable = (source, available, narratives, browserDimensions, dispatch, changePage) => { + if (!source) return null; + if (!available) { + if (source === "live" || source === "staging") { + return ( + <CenterContent> + <div style={{fontSize: "18px"}}> + {`No available ${source} datasets. Try "/local/" for local datasets.`} + </div> + </CenterContent> + ); + } + return null; + } + + + let listJSX; + /* make two columns for wide screens */ + if (browserDimensions.width > 1000) { + const secondColumnStart = Math.ceil(available.length / 2); + listJSX = ( + <div style={{display: "flex", flexWrap: "wrap"}}> + <div style={{flex: "1 50%", minWidth: "0"}}> + <ul> + {available.slice(0, secondColumnStart).map((data) => formatDataset(source, data, dispatch, changePage))} + </ul> + </div> + <div style={{flex: "1 50%", minWidth: "0"}}> + <ul> + {available.slice(secondColumnStart).map((data) => formatDataset(source, data, dispatch, changePage))} + </ul> + </div> + </div> + ); + } else { + listJSX = ( + <ul style={{marginLeft: "-22px"}}> + {available.map((data) => formatDataset(source, data, dispatch, changePage))} + </ul> + ); + } + return ( + <CenterContent> + <div> + <div style={{fontSize: "26px"}}> + {`Available ${narratives ? "Narratives" : "Datasets"} for source ${source}`} + </div> + {listJSX} + </div> + </CenterContent> + ); +}; + + +const SplashContent = ({isMobile, source, available, narratives, browserDimensions, dispatch, errorMessage, changePage}) => ( + <div> + <NavBar minified={isMobile}/> + + <div className="static container"> + <Flex justifyContent="center"> + <Title/> + </Flex> + <div className="row"> + <h1 style={{textAlign: "center", marginTop: "-10px", fontSize: "29px"}}> Real-time tracking of virus evolution </h1> + </div> + {/* First: either display the error message or the intro-paragraph */} + {errorMessage ? ( + <CenterContent> + <div> + <p style={{color: "rgb(222, 60, 38)", fontWeight: 600, fontSize: "24px"}}> + {"😱 404, or an error has occured 😱"} + </p> + <p style={{color: "rgb(222, 60, 38)", fontWeight: 400, fontSize: "18px"}}> + {`Details: ${errorMessage}`} + </p> + <p style={{fontSize: "16px"}}> + {"If this keeps happening, or you believe this is a bug, please "} + <a href={"mailto:hello@nextstrain.org"}>{"get in contact with us."}</a> + </p> + </div> + </CenterContent> + ) : ( + <p style={{maxWidth: 600, marginTop: 0, marginRight: "auto", marginBottom: 20, marginLeft: "auto", textAlign: "center", fontSize: 16, fontWeight: 300, lineHeight: 1.42857143}}> + Nextstrain is an open-source project to harness the scientific and public health potential of pathogen genome data. We provide a continually-updated view of publicly available data with powerful analytics and visualizations showing pathogen evolution and epidemic spread. Our goal is to aid epidemiological understanding and improve outbreak response. + </p> + )} + {/* Secondly, list the available datasets / narratives */} + {listAvailable(source, available, narratives, browserDimensions, dispatch, changePage)} + {/* Finally, the footer (logos) */} + <CenterContent> + {logos} + </CenterContent> + + </div> + </div> +); + +export default SplashContent; diff --git a/src/index.js b/src/index.js index 1be89949c..f1f891584 100644 --- a/src/index.js +++ b/src/index.js @@ -18,8 +18,6 @@ import "./css/boxed.css"; import "./css/select.css"; import "./css/narrative.css"; -import "./util/extensions" - const store = configureStore(); /* set up non-redux state storage for the animation - use this conservitavely! */ diff --git a/src/util/errorBoundry.js b/src/util/errorBoundry.js new file mode 100644 index 000000000..a8555a97b --- /dev/null +++ b/src/util/errorBoundry.js @@ -0,0 +1,30 @@ +import React from "react"; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error, info) { + // You can also log the error to an error reporting service + console.error(error); + console.error(info); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return (<h1>Something went wrong.</h1>); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/util/extensions.js b/src/util/extensions.js index d695aa39f..15c94f51c 100644 --- a/src/util/extensions.js +++ b/src/util/extensions.js @@ -1,20 +1,31 @@ - -/* set up time -- only runs once no matter how many components import this */ - const registry = (() => { - console.log("EXTENSTIONS.jS"); if (!process.env.EXTENSION_DATA) { - console.log("no EXTENSION_DATA found") + console.log("no EXTENSION_DATA found"); return {}; } - const extensionJson = JSON.parse(process.env.EXTENSION_DATA); - console.log("extensionJson", extensionJson); - return extensionJson; + const extensions = JSON.parse(process.env.EXTENSION_DATA); + + Object.keys(extensions).forEach((key) => { + if (key.endsWith("Component")) { + console.log("loading component", key); + /* "@extensions" is a webpack alias */ + extensions[key] = require(`@extensions/${extensions[key]}`).default; // eslint-disable-line + } + }); + console.log("extensions", extensions); + return extensions; })(); export const getExtension = (what) => { - console.log("trying to get:", what) - return "something"; -} + if (registry[what]) { + return registry[what]; + } + console.error("Requested non-existing extension", what); + return false; +}; + +export const hasExtension = (what) => { + return Object.keys(registry).includes(what); +}; diff --git a/webpack.config.dev.js b/webpack.config.dev.js index 7d6effed6..d43eeb7b5 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -1,6 +1,19 @@ const path = require('path'); const webpack = require('webpack'); +const directoriesToTransform = [path.join(__dirname, 'src')]; +const aliasesToResolve = { + "@extensions": '.', /* must provide an (unused) default, else it won't compile */ + "@auspice": path.join(__dirname, 'src') +}; + +if (process.env.EXTENSION_PATH) { + const dir = path.resolve(__dirname, process.env.EXTENSION_PATH); + directoriesToTransform.push(dir); + aliasesToResolve["@extensions"] = dir; +} + + module.exports = { mode: 'development', context: __dirname, @@ -16,12 +29,15 @@ module.exports = { filename: 'bundle.js', publicPath: '/dist/' }, + resolve: { + alias: aliasesToResolve + }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.DefinePlugin({ "process.env": { NODE_ENV: JSON.stringify("dev"), - EXTENSION_DATA: JSON.stringify(process.env.EXTEND_AUSPICE_JSON) + EXTENSION_DATA: JSON.stringify(process.env.EXTEND_AUSPICE_DATA) } }), new webpack.NoEmitOnErrorsPlugin() @@ -34,7 +50,7 @@ module.exports = { { test: /\.js$/, use: ['babel-loader'], - include: path.join(__dirname, 'src') + include: directoriesToTransform }, { test: /\.css$/, @@ -43,7 +59,7 @@ module.exports = { { test: /\.(gif|png|jpe?g|svg)$/i, use: "file-loader", - include: path.join(__dirname, "src") + include: directoriesToTransform } ] },