diff --git a/src/components/info/byline.js b/src/components/info/byline.tsx similarity index 85% rename from src/components/info/byline.js rename to src/components/info/byline.tsx index 83f136611..450bb24a6 100644 --- a/src/components/info/byline.js +++ b/src/components/info/byline.tsx @@ -1,25 +1,25 @@ import React from "react"; -import { connect } from "react-redux"; import { withTranslation } from 'react-i18next'; import styled from 'styled-components'; import { headerFont } from "../../globalStyles"; +import { MetadataState} from "../../reducers/metadata"; + +interface Props { + t: any; // TODO XXX - look up how to type WithTranslation + metadata: MetadataState +} /** * 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 { - render() { +class Byline extends React.Component { + override render() { const { t } = this.props; return ( <> - {renderAvatar(t, this.props.metadata)} + {renderAvatar(this.props.metadata)} {renderBuildInfo(t, this.props.metadata)} {renderMaintainers(t, this.props.metadata)} {renderDataUpdated(t, this.props.metadata)} @@ -38,9 +38,9 @@ const AvatarImg = styled.img` * 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) { +function renderAvatar(metadata: MetadataState) { const repo = metadata.buildUrl; - if (typeof repo === 'string') { + if (repo) { const match = repo.match(/(https?:\/\/)?(www\.)?github.com\/([^/]+)/); if (match) { return ( @@ -55,10 +55,10 @@ function renderAvatar(t, metadata) { * 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) { +function renderBuildInfo(t, metadata: MetadataState) { if (Object.prototype.hasOwnProperty.call(metadata, "buildUrl")) { const repo = metadata.buildUrl; - if (typeof repo === 'string') { + if (typeof repo === 'string') { // TODO - we can relax this now that we have proper types if (repo.startsWith("https://") || repo.startsWith("http://") || repo.startsWith("www.")) { return ( @@ -80,11 +80,11 @@ function renderBuildInfo(t, metadata) { * 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) { +function renderMaintainers(t, metadata: MetadataState): JSX.Element | null { let maintainersArray; if (Object.prototype.hasOwnProperty.call(metadata, "maintainers")) { maintainersArray = metadata.maintainers; - if (Array.isArray(maintainersArray) && maintainersArray.length) { + if (Array.isArray(maintainersArray) && maintainersArray.length) { // TODO - we can relax this now that we have proper types return ( {t("Maintained by") + " "} @@ -106,7 +106,7 @@ function renderMaintainers(t, metadata) { * Returns a React component detailing the date the data was last updated. * Renders a containing "Data updated X", where X derives from `metadata.updated` */ -function renderDataUpdated(t, metadata) { +function renderDataUpdated(t, metadata: MetadataState): JSX.Element | null { if (metadata.updated) { return ( @@ -122,7 +122,7 @@ function renderDataUpdated(t, metadata) { * 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) { +function renderDataProvenance(t, metadata: MetadataState): JSX.Element | null { if (!Array.isArray(metadata.dataProvenance)) return null; const sources = metadata.dataProvenance .filter((source) => typeof source === "object") @@ -174,7 +174,7 @@ const BylineLink = styled.a` font-weight: 500; `; -function Link({url, children}) { +function Link({url, children}): JSX.Element { return ( {children} diff --git a/src/components/info/info.js b/src/components/info/info.tsx similarity index 75% rename from src/components/info/info.js rename to src/components/info/info.tsx index 7734df881..35f08c9d7 100644 --- a/src/components/info/info.js +++ b/src/components/info/info.tsx @@ -1,11 +1,34 @@ import React from "react"; -import { connect } from "react-redux"; +import { connect, ConnectedProps } from "react-redux"; import { withTranslation } from 'react-i18next'; import Card from "../framework/card"; import { titleFont, headerFont, medGrey, darkGrey } from "../../globalStyles"; import Byline from "./byline"; import {datasetSummary} from "./datasetSummary"; import FiltersSummary from "./filtersSummary"; +import { RootState } from "../../store"; + +const mapState = (state: RootState) => { + // can we generalise the mapState function so the following is for free? + if (!state.metadata.loaded) { // loaded is the discriminant property to narrow types + throw new Error("Something's gone seriously wrong") + } + return { + browserWidth: state.browserDimensions.browserDimensions.width, + animationPlayPauseButton: state.controls.animationPlayPauseButton, + metadata: state.metadata, + nodes: state.tree.nodes, + branchLengthsToDisplay: state.controls.branchLengthsToDisplay, + visibility: state.tree.visibility + } +} +const connector = connect(mapState) +type PropsFromRedux = ConnectedProps + +interface Props extends PropsFromRedux { + t: any; // TODO XXX - look up how to type WithTranslation + width: number; +} /** * The panel is shown above data viz panels and conveys static and dynamic @@ -15,22 +38,12 @@ import FiltersSummary from "./filtersSummary"; * Dataset summary (dynamic) * Current Filters (dynamic) */ -@connect((state) => { - return { - browserWidth: state.browserDimensions.browserDimensions.width, - animationPlayPauseButton: state.controls.animationPlayPauseButton, - metadata: state.metadata, - nodes: state.tree.nodes, - branchLengthsToDisplay: state.controls.branchLengthsToDisplay, - visibility: state.tree.visibility - }; -}) -class Info extends React.Component { +class Info extends React.Component { constructor(props) { super(props); } - render() { + override render() { const { t } = this.props; if (!this.props.metadata || !this.props.nodes || !this.props.visibility) return null; const styles = computeStyles(this.props.width, this.props.browserWidth); @@ -40,15 +53,15 @@ class Info extends React.Component {
-
- {this.props.metadata.title || ""} +
+ {this.props.metadata.title}
-
- +
+
-
+
{animating ? t("Animation in progress") + ". " : null} {showExtended && <> @@ -111,5 +124,5 @@ function computeStyles(width, browserWidth) { }; } -const WithTranslation = withTranslation()(Info); +const WithTranslation = withTranslation()(connector(Info)); export default WithTranslation; diff --git a/src/reducers/metadata.ts b/src/reducers/metadata.ts index 1a47b3320..493db153a 100644 --- a/src/reducers/metadata.ts +++ b/src/reducers/metadata.ts @@ -1,7 +1,7 @@ import * as types from "../actions/types"; import { DatasetJsonRootSequence, DatasetJson, DatasetJsonMeta } from "../types/datasetJson"; -export interface MetadataState { +export interface MetadataState { // TODO -- work out exactly what properties you want to have as required here loaded: true; rootSequence: DatasetJsonRootSequence; identicalGenomeMapAcrossBothTrees: boolean; @@ -13,7 +13,7 @@ export interface MetadataState { displayDefaults: Record; // TODO XXX panels: Required['panels']; mainTreeNumTips: number; - title: Required['title']; + title: DatasetJsonMeta['title']; version: DatasetJson['version']; filters: Required['filters']; dataProvenance: Required['data_provenance']; @@ -30,7 +30,7 @@ export function convertIncompleteMetadataStateToMetadataState(meta: IncompleteMe // and we don't want to be doing that. What's the best path here? const expectedProperties: [string, string, any][] = [ // THIS IS INCOMPLETE - TODO XXX - ["title", "string", null], + // ["title", "string", null], // title is optional! ["version", "string", null], ["filters", "string", []], ["updated", "string", new Error("JSON.meta missing property 'updated' which is essential")], // TKTK - it's not essential, just for testing