diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js
index a1fc7a42b..384c3b110 100644
--- a/src/actions/recomputeReduxState.js
+++ b/src/actions/recomputeReduxState.js
@@ -648,6 +648,9 @@ const createMetadataStateFromJSON = (json) => {
if (json.meta.genome_annotations) {
metadata.genomeAnnotations = json.meta.genome_annotations;
}
+ if (json.meta.data_provenance) {
+ metadata.dataProvenance = json.meta.data_provenance;
+ }
if (json.meta.filters) {
metadata.filters = json.meta.filters;
}
diff --git a/src/components/info/byline.js b/src/components/info/byline.js
index f1f9c6ad2..6dd008260 100644
--- a/src/components/info/byline.js
+++ b/src/components/info/byline.js
@@ -4,49 +4,25 @@ import { withTranslation } from 'react-i18next';
import styled from 'styled-components';
import { headerFont } from "../../globalStyles";
+/**
+ * React component for the byline of the current dataset.
+ * This details (non-dynamic) information about the dataset, such as the
+ * maintainers, source, data provenance etc.
+ */
@connect((state) => {
return {
metadata: state.metadata
};
})
class Byline extends React.Component {
- constructor(props) {
- super(props);
- }
-
render() {
const { t } = this.props;
-
- /** Render a special byline for nexstrain's nCoV (SARS-CoV-2) builds.
- * This is somewhat temporary and may be switched to a nextstrain.org
- * auspice customisation in the future.
- */
- if (
- // comment out the next line for testing on localhost
- (window.location.hostname === "nextstrain.org" || window.location.hostname === "dev.nextstrain.org") &&
- window.location.pathname.startsWith("/ncov")
- ) {
- return (
- <>
- {renderAvatar(t, this.props.metadata)}
- {renderMaintainers(t, this.props.metadata)}
- {
- this.props.metadata.buildUrl &&
-
- {" Enabled by data from "}
-
-
- }
- >
- );
- }
- /* End nextstrain-specific ncov / SARS-CoV-2 code */
-
return (
<>
{renderAvatar(t, this.props.metadata)}
{renderBuildInfo(t, this.props.metadata)}
{renderMaintainers(t, this.props.metadata)}
+ {renderDataProvenance(t, this.props.metadata)}
>
);
}
@@ -57,6 +33,10 @@ const AvatarImg = styled.img`
margin-bottom: 2px;
`;
+/**
+ * Renders the GitHub avatar of the current dataset for datasets with a `buildUrl`
+ * which is a GitHub repo. The avatar image is fetched from GitHub (by the client).
+ */
function renderAvatar(t, metadata) {
const repo = metadata.buildUrl;
if (typeof repo === 'string') {
@@ -71,7 +51,8 @@ function renderAvatar(t, metadata) {
}
/**
- * Render the byline of the page to indicate the source of the build (often a GitHub repo)
+ * Returns a React component detailing the source of the build (pipeline).
+ * Renders a containing "Built with X", where X derives from `metadata.buildUrl`
*/
function renderBuildInfo(t, metadata) {
if (Object.prototype.hasOwnProperty.call(metadata, "buildUrl")) {
@@ -94,7 +75,10 @@ function renderBuildInfo(t, metadata) {
return null;
}
-
+/**
+ * Returns a React component detailing the maintainers of the build (pipeline).
+ * Renders a containing "Maintained by X", where X derives from `metadata.maintainers`
+ */
function renderMaintainers(t, metadata) {
let maintainersArray;
if (Object.prototype.hasOwnProperty.call(metadata, "maintainers")) {
@@ -109,7 +93,7 @@ function renderMaintainers(t, metadata) {
{i === maintainersArray.length-1 ? "" : i === maintainersArray.length-2 ? " and " : ", "}
))}
- {"."}
+ {". "}
);
}
@@ -117,6 +101,58 @@ function renderMaintainers(t, metadata) {
return null;
}
+
+/**
+ * Returns a React component detailing the data provenance of the build (pipeline).
+ * Renders a containing "Enabled by data from X", where X derives from `metadata.dataProvenance`
+ * Note that this function includes logic to special-case certain values which may appear there.
+ */
+function renderDataProvenance(t, metadata) {
+ if (!Array.isArray(metadata.dataProvenance)) return null;
+ const sources = metadata.dataProvenance
+ .filter((source) => typeof source === "object")
+ .filter((source) => Object.prototype.hasOwnProperty.call(source, "name"))
+ .map((source) => {
+ if (source.name.toUpperCase() === "GISAID") { // SPECIAL CASE
+ return (
+
+ );
+ }
+ const url = parseUrl(source.url);
+ if (url) {
+ return {source.name};
+ }
+ return source.name;
+ });
+ if (!sources.length) return null;
+ return (
+
+ {t("Enabled by data from") + " "}
+ {makePrettyList(sources)}
+ {"."}
+
+ );
+}
+
+function makePrettyList(els) {
+ if (els.length<2) return els;
+ return Array.from({length: els.length*2-1})
+ .map((_, idx) => idx%2===0 ? els[idx/2] : idx===els.length*2-3 ? " and " : ", ");
+}
+
+
+/**
+ * Attempts to parse a url. Returns a valid-looking URL string or `false`.
+ */
+function parseUrl(potentialUrl) {
+ try {
+ const urlObj = new URL(potentialUrl);
+ return urlObj.href;
+ } catch (err) {
+ return false;
+ }
+}
+
const BylineLink = styled.a`
font-family: ${headerFont};
font-size: 15;