diff --git a/src/components/framework/fine-print.js b/src/components/framework/fine-print.js index 696048898..3fd327560 100644 --- a/src/components/framework/fine-print.js +++ b/src/components/framework/fine-print.js @@ -2,13 +2,14 @@ import React, { Suspense, lazy } from "react"; import { connect } from "react-redux"; import styled from 'styled-components'; import { withTranslation } from "react-i18next"; -import { FaDownload } from "react-icons/fa"; +import { FaDownload, FaExternalLinkAlt } from "react-icons/fa"; import { dataFont, medGrey, materialButton } from "../../globalStyles"; import { SET_MODAL } from "../../actions/types"; import Flex from "./flex"; import { version } from "../../version"; import { publications } from "../download/downloadModal"; import { hasExtension, getExtension } from "../../util/extensions"; +import { canShowLinkOuts } from "../modal/LinkOutModalContents.jsx"; const logoPNG = require("../../images/favicon.png"); @@ -85,7 +86,22 @@ class FinePrint extends React.Component { onClick={() => { this.props.dispatch({ type: SET_MODAL, modal: "download" }); }} > - {" "+t("Download data")} + {" "+t("Download data")} + + ); + } + linkOutButton() { + // TODO XXX conditional on extensions? i.e. extensions should set which platforms they can link to... + // Ideally this entire stuff would in injected as it's nextstrain.org specific but Auspice is the nextstrain viewer + // and we have lots of nextstrain.org-specific code + const { t } = this.props; + return ( + ); } @@ -96,10 +112,16 @@ class FinePrint extends React.Component { return (
- + {this.getUpdated()} {dot} {this.downloadDataButton()} + {canShowLinkOuts() && ( + <> + {dot} + {this.linkOutButton()} + + )} {dot} {"Auspice v" + version} diff --git a/src/components/modal/LinkOutModalContents.jsx b/src/components/modal/LinkOutModalContents.jsx new file mode 100644 index 000000000..046cf91c9 --- /dev/null +++ b/src/components/modal/LinkOutModalContents.jsx @@ -0,0 +1,141 @@ +import React from "react"; +import styled from 'styled-components'; +import { useSelector } from "react-redux"; +import { infoPanelStyles } from "../../globalStyles"; +import { dataFont, lighterGrey} from "../../globalStyles"; +import { isColorByGenotype, decodeColorByGenotype} from "../../util/getGenotype"; + +/** + * The following value is useful for development purposes as we'll not show any + * link-outs on localhost (because external sites can't access it!) so we can + * replace it with something else such as "https://nextstrain.org" for testing + */ +const forceNextstrainHost = false; + +const ButtonText = styled.a` + border: 1px solid ${lighterGrey}; + border-radius: 4px; + cursor: pointer; + padding: 4px 7px; + margin-right: 10px; + font-family: ${dataFont}; + background-color: rgba(0,0,0,0); + color: white !important; + font-weight: 400; + text-decoration: none !important; + font-size: 16px; + & :hover { + background-color: ${(props) => props.theme.selectedColor}; + } +` + +const ButtonDescription = styled.div` + display: inline-block; + height: 30px; + font-style: italic; + font-size: 14px; + color: white; +` + +const ButtonContainer = styled.div` + margin-top: 10px; + margin-bottom: 10px; +` + +const data = ({distanceMeasure, colorBy}) => { + const pathname = window.location.pathname; + const origin = forceNextstrainHost ? 'https://nextstrain.org' : window.location.origin; + return ([ + { + name: 'taxonium.org', + description() { + return <>{`Visualise this dataset in Taxonium. We'll try to preserve your current view settings where possible.`} + }, + taxoniumColoring() { + if (isColorByGenotype(colorBy)) { + /* Taxonium syntax looks like 'color={"field":"genotype","pos":485,"gene":"M"}' + Note that taxonium (I think) does't backfill bases/residues which match the root like Auspice does + */ + const subfields = ['"genotype"']; // include quoting as taxonium uses + const colorInfo = decodeColorByGenotype(colorBy); + // Multiple mutations (positions) aren't allowed + if (!colorInfo || colorInfo.positions.length>1) return null; + // The (integer) position is not enclosed in double quotes + subfields.push(`"pos":${colorInfo.positions[0]}`); + // The gene value is optional, without it we use nucleotide ("nt" in taxonium syntax) + if (colorInfo.aa) subfields.push(`"gene":"${colorInfo.gene}"`); + // Note that this string will be encoded when converted to a URL + return `{"field":${subfields.join(',')}}`; + } + return `{"field":"meta_${colorBy}"}`; + }, + url() { + /** + * Tested on genotype colors + normal colors + * TODO: tanglegrams (they'll be part of the pathname) + * TODO: /fetch URLs (won't work) + */ + const baseUrl = 'https://taxonium.org'; + const queries = { + treeUrl: `${origin}${pathname}`, // no nextstrain queries + // treeUrl: `https://nextstrain.org${pathname}`, // uncomment for development on localhost + treeType: 'nextstrain', + ladderizeTree: 'false', // keep same orientation as Auspice + xType: distanceMeasure==='num_date' ? 'x_time' : 'x_dist', + } + const color = this.taxoniumColoring(); + if (color) queries.color = color; + + return `${baseUrl}?${Object.entries(queries).map(([k,v]) => `${k}=${encodeURIComponent(v)}`).join("&")}`; + } + } + ]) +} + +export const LinkOutModalContents = () => { + const {distanceMeasure, colorBy} = useSelector((state) => state.controls) + + return ( + <> +
+ View this dataset in other platforms: +
+ +
+ +
+ Clicking on the following links will take you to an external site which will then make requests to nextstrain.org for the data you are currently viewing +
+ + {window.location.hostname==='localhost' && ( +
+ NOTE: The site is currently running on localhost and thus the following links will not work +
+ )} + +
+ + {data({distanceMeasure, colorBy}).map((d) => ( + + {d.name} + {d.description()} + + ))} + +
+ These are only gonna work with nextstrain.org (and dev, review apps etc) because it uses the RESTful API +
+ + + ); +} + + +export const canShowLinkOuts = () => { + // TODO XXX - additionally query an extension flag + if (window.location.hostname==='localhost' && !forceNextstrainHost) { + console.log("Link-out modal disabled while running on localhost") + return false; + } + return true; +} \ No newline at end of file diff --git a/src/components/modal/Modal.jsx b/src/components/modal/Modal.jsx index 312193891..025c5500e 100644 --- a/src/components/modal/Modal.jsx +++ b/src/components/modal/Modal.jsx @@ -6,6 +6,7 @@ import { SET_MODAL } from "../../actions/types"; import { infoPanelStyles } from "../../globalStyles"; import { stopProp } from "../tree/infoPanels/click"; import DownloadModalContents from "../download/downloadModal"; +import { LinkOutModalContents } from "./LinkOutModalContents.jsx"; @connect((state) => ({ browserDimensions: state.browserDimensions.browserDimensions, @@ -85,6 +86,9 @@ class Modal extends React.Component { case 'download': Contents = DownloadModalContents; break; + case 'linkOut': + Contents = LinkOutModalContents; + break; default: return null; }