Skip to content

Commit

Permalink
Pages can define which columns are rendered
Browse files Browse the repository at this point in the history
This commit creates a more flexible react pattern that allows pages
(e.g. /influenza, /sars-cov-2) to decide which columns are rendered,
as well as how they look (e.g. logo, mobile text, links etc).

In turn, this will give us the ability for different pages to use
different dataset structures and display bespoke information in the
table.

Currently the number of columns must be 2 or 3, and the 3rd will
not be rendered for small screen sizes. This could be improved in
future as needs arise.

Based on work by @eharkins in PR nextstrain#289

Co-authored-by: eharkins <eli.harkins@gmail.com>
  • Loading branch information
jameshadfield and eharkins committed Apr 10, 2021
1 parent 97925b6 commit eb82874
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 80 deletions.
6 changes: 3 additions & 3 deletions static-site/src/components/Datasets/dataset-select.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { collectAvailableFilteringOptions, computeFilterValues } from "./filter-
* @prop {string | undefined} urlDefinedFilterPath slash-separated keywords which will be applied as filters
* @prop {string | undefined} intendedUri Intended URI. Browser address will be replaced with this.
* @prop {Array} datasets Available datasets. Array of Objects.
* @prop {boolean} noDates Note: will be replaced in a subsequent commit
* @prop {Array} columns Columns to be rendered by the table. See <ListDatasets> for signature.
* @prop {Array | undefined} interface What elements to render? Elements may be strings or functinos. Order is respected.
* Available strings: "FilterSelect" "FilterDisplay", "ListDatasets"
* Functions will be handed an object with key(s): `datasets` (which may be filtered), and should return a react component for rendering.
Expand Down Expand Up @@ -100,8 +100,8 @@ class DatasetSelect extends React.Component {
return (
<ListDatasets
key="ListDatasets"
columns={this.props.columns}
datasets={filteredDatasets}
showDates={!this.props.noDates}
/>
);
default:
Expand All @@ -126,7 +126,7 @@ DatasetSelect.propTypes = {
urlDefinedFilterPath: PropTypes.string,
intendedUri: PropTypes.string,
interface: PropTypes.array,
noDates: PropTypes.bool,
columns: PropTypes.array.isRequired,
datasets: PropTypes.array.isRequired
};

Expand Down
196 changes: 120 additions & 76 deletions static-site/src/components/Datasets/list-datasets.jsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,157 @@
import React from "react";
import "react-select/dist/react-select.css";
import "react-virtualized-select/styles.css";
import { get } from 'lodash';
import styled from 'styled-components';
import { MdPerson } from "react-icons/md";
import {Grid, Col, Row} from 'react-styled-flexboxgrid';
import { CenteredContainer } from "./styles";

const logoPNG = require("../../../static/logos/favicon.png");

const StyledLinkContainer = styled.div`
a {
color: #444;
font-weight: ${(props) => props.bold ? 700 : "normal"};
}
a:hover,
a:focus {
color: #5097BA;
const StyledLink = styled.a`
color: #444 !important;
font-weight: ${(props) => props.bold ? 500 : 300} !important;
&:hover,
&:focus {
color: #5097BA !important;
text-decoration: underline;
}
`;

const StyledIconLinkContainer = styled.div`
svg {
color: #444;
}
svg:hover,
svg:focus {
color: #5097BA;
}
`;

const DatasetSelectionResultsContainer = styled.div`
height: 600px;
overflow-x: hidden;
overflow-y: visible;
`;

const DatasetContainer = styled.div`
const RowContainer = styled.div`
font-family: ${(props) => props.theme.generalFont};
font-weight: 900;
font-size: 18px;
padding: 10px 1px 10px 1px;
line-height: 24px;
${(props) => props.border && `border-bottom: 1px solid #CCC;`}
`;

const LogoContainer = styled.a`
const LogoContainerLink = styled.a`
padding: 1px 1px;
margin-right: 5px;
width: 24px;
cursor: pointer;
`;

export const ListDatasets = ({datasets, showDates}) => {
const LogoContainer = styled.span`
padding: 1px 1px;
margin-right: 5px;
width: 24px;
`;

const columnStyles = [
[{xs: 8, sm: 6, md: 7}], // column 1 rendered as a single <Col>
[{xs: false, sm: 3, md: 3}, {xs: 4, sm: false, style: {textAlign: 'right'}}], // column 2
[{xs: false, sm: 3, md: 2}]
];

const HeaderRow = ({columns}) => {
const names = columns.map((c) => c.name);
return (
<RowContainer>
<Row>
{/* column 1 (typically the dataset) - same rendering on main & mobile views */}
<Col {...columnStyles[0][0]} key={names[0]}>{names[0]}</Col>

{/* column 2: (typically the contributor) - both main & mobile views */}
<Col {...columnStyles[1][0]} key={names[1]}>{names[1]}</Col>
<Col {...columnStyles[1][1]} key={`${names[1]}-mobile`}>{names[1]}</Col>

{/* column 3: optional. Not rendered on small screens. */}
{names.length===3 && (
<Col {...columnStyles[2][0]} key={names[2]}>{names[2]}</Col>
)}
</Row>
</RowContainer>
);
};

const NormalRow = ({columns, dataset}) => {
const names = columns.map((c) => c.name);
return (
<RowContainer>
<Row>
{/* column 1 (typically the dataset) - same rendering on main & mobile views */}
<Col {...columnStyles[0][0]} key={names[0]}>
<Value dataset={dataset} columnInfo={columns[0]} firstColumn/>
</Col>

{/* column 2: (typically the contributor) - both main & mobile views */}
<Col {...columnStyles[1][0]} key={names[1]}>
<Value dataset={dataset} columnInfo={columns[1]}/>
</Col>
<Col {...columnStyles[1][1]} key={`${names[1]}-mobile`}>
<Value dataset={dataset} columnInfo={columns[1]} mobileView/>
</Col>

{/* column 3: optional. Not rendered on small screens. */}
{names.length===3 && (
<Col {...columnStyles[2][0]} key={names[2]}>
<Value dataset={dataset} columnInfo={columns[2]} mobileView/>
</Col>
)}
</Row>
</RowContainer>
);
};

/**
* Render the value for a particular cell in the table.
* May be a link and/or have a logo, depending on the data in `columnInfo`
*/
const Value = ({dataset, columnInfo, mobileView, firstColumn}) => {
const url = typeof columnInfo.url === "function" && columnInfo.url(dataset);
const value = mobileView && typeof columnInfo.valueMobile === "function" ?
columnInfo.valueMobile(dataset) :
columnInfo.value(dataset);
const logo = mobileView && typeof columnInfo.logoMobile === "function" ?
columnInfo.logoMobile(dataset) :
typeof columnInfo.logo === "function" ?
columnInfo.logo(dataset) :
undefined;
return (
<>
{(logo && url) ?
(<LogoContainerLink href={url}>{logo}</LogoContainerLink>) :
logo ?
(<LogoContainer>{logo}</LogoContainer>) :
null
}
{url ?
<StyledLink bold={firstColumn} href={url}>{value}</StyledLink> :
value
}
</>
);
};


/**
* React component to render a table showing the `datasets` as rows with
* the specified `columns`. Open to future expansion.
* Currently only 2 or 3 columns are supported.
* If 3 columns are supplied, the 3rd will not be shown on small screens.
* @prop {Array} columns Array of columns. Each entry is an object with following properties:
* `name` {string} To be displayed in the header
* `value` {function} return the value, given an individual entry from `datasets`
* `valueMobile` {function | undefined} value to be used on small screens
* `url` {function | undefined} render the value as a link to this URL
* `logo` {function | undefined} if the function returns "nextstrain" then we render the Nextstrain logo.
* @returns React Component
*/
export const ListDatasets = ({datasets, columns}) => {
return (
<CenteredContainer>
<Grid fluid>
<DatasetContainer key="Column labels" style={{borderBottom: "1px solid #CCC"}}>
<Row>
<Col xs={8} sm={6} md={7}>
Dataset
</Col>
<Col xs={false} sm={3} md={3}>
Contributor
</Col>
{showDates && <Col xs={false} sm={3} md={2}>
Uploaded date
</Col>}
<Col xs={4} sm={false} style={{textAlign: "right"}}>
Contributor
</Col>
</Row>
</DatasetContainer>
<HeaderRow columns={columns}/>
<DatasetSelectionResultsContainer>
{ datasets.map((dataset) => (
<DatasetContainer key={dataset.filename}>
<Row>
<Col xs={10} sm={6} md={7}>
<StyledLinkContainer bold>
<a href={dataset.url}>{dataset.filename.replace(/_/g, ' / ').replace('.json', '')}</a>
</StyledLinkContainer>
</Col>
<Col xs={false} sm={3} md={3}>
<span>
{dataset.contributor.includes("Nextstrain") && <LogoContainer href="https://nextstrain.org">
<img alt="nextstrain.org" className="logo" width="24px" src={logoPNG}/>
</LogoContainer>}
{dataset.contributorUrl === undefined ?
dataset.contributor :
<StyledLinkContainer>
<a href={dataset.contributorUrl}>{dataset.contributor}</a>
</StyledLinkContainer>}
</span>
</Col>
{showDates && <Col xs={false} sm={3} md={2}>
{dataset.date_uploaded}
</Col>}
<Col xs={2} sm={false} style={{textAlign: "right"}}>
<LogoContainer href={dataset.contributor.includes("Nextstrain") ? "https://nextstrain.org" : get(dataset, "contributorUrl")}>
{dataset.contributor.includes("Nextstrain") ?
<img alt="nextstrain.org" className="logo" width="24px" src={logoPNG}/> :
<StyledIconLinkContainer><MdPerson/></StyledIconLinkContainer>
}
</LogoContainer>
</Col>
</Row>
</DatasetContainer>
))
}
{datasets.map((dataset) => (
<NormalRow dataset={dataset} columns={columns} key={columns[0].value(dataset)}/>
))}
</DatasetSelectionResultsContainer>
</Grid>
</CenteredContainer>
Expand Down
22 changes: 22 additions & 0 deletions static-site/src/pages/influenza-page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import Footer from "../components/Footer";
import { PathogenPageIntroduction } from "../components/Datasets/pathogen-page-introduction";
import DatasetSelect from "../components/Datasets/dataset-select";

const nextstrainLogoPNG = require("../../static/logos/favicon.png");

const title = "Influenza resources";
const abstract = `The Nextstrain team maintains datasets and other tools for analyzing a variety of influenza viruses.
We track the evolution of seasonal influenza viruses (A/H3N2, A/H1N1pdm, B/Victoria, and B/Yamagata)
Expand Down Expand Up @@ -54,6 +56,25 @@ const contents = [
}
];

const tableColumns = [
{
name: "Dataset",
value: (dataset) => dataset.filename.replace(/_/g, ' / ').replace('.json', ''),
url: (dataset) => dataset.url
},
{
name: "Contributor",
value: () => "Nextstrain",
valueMobile: () => "",
url: () => "https://nextstrain.org",
logo: () => (<img alt="nextstrain.org" className="logo" width="24px" src={nextstrainLogoPNG}/>)
},
{
name: "Uploaded Date",
value: (dataset) => dataset.date_uploaded
}
];


class Index extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -109,6 +130,7 @@ class Index extends React.Component {
{this.state.dataLoaded && (
<DatasetSelect
datasets={this.state.datasets}
columns={tableColumns}
urlDefinedFilterPath={this.props["*"]}
intendedUri={this.props.uri}
/>
Expand Down
27 changes: 26 additions & 1 deletion static-site/src/pages/sars-cov-2-page.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import Helmet from "react-helmet";
import ScrollableAnchor, { configureAnchors } from "react-scrollable-anchor";
import { MdPerson } from "react-icons/md";
import { get } from 'lodash';
import config from "../../data/SiteConfig";
import NavBar from "../components/nav-bar";
Expand All @@ -21,6 +22,9 @@ import { PathogenPageIntroduction } from "../components/Datasets/pathogen-page-i
import {parseNcovSitRepInfo} from "../../../auspice-client/customisations/languageSelector";
import sarscov2Catalogue from "../../content/SARS-CoV-2-Datasets.yaml";

const nextstrainLogoPNG = require("../../static/logos/favicon.png");


const title = "Nextstrain SARS-CoV-2 resources";
const abstract = `Around the world, people are sequencing and sharing SARS-CoV-2
genomic data. The Nextstrain team analyzes these data on a global and continental
Expand Down Expand Up @@ -104,6 +108,27 @@ const contents = [
}
];

const tableColumns = [
{
name: "Dataset",
value: (dataset) => dataset.filename.replace(/_/g, ' / ').replace('.json', ''),
url: (dataset) => dataset.url
},
{
name: "Contributor",
value: (dataset) => dataset.contributor,
valueMobile: () => "",
url: (dataset) => dataset.contributorUrl,
logo: (dataset) => dataset.contributor==="Nextstrain Team" ?
<img alt="nextstrain.org" className="logo" width="24px" src={nextstrainLogoPNG}/> :
undefined,
logoMobile: (dataset) => dataset.contributor==="Nextstrain Team" ?
<img alt="nextstrain.org" className="logo" width="24px" src={nextstrainLogoPNG}/> :
<MdPerson/>
}
];


class Index extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -165,7 +190,7 @@ class Index extends React.Component {
{this.state.filterParsed && (
<DatasetSelect
datasets={this.state.filterList}
noDates
columns={tableColumns}
interface={[
DatasetMap,
"FilterSelect",
Expand Down

0 comments on commit eb82874

Please sign in to comment.