Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add types for JSON.metadata #1887

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { entropyCreateState, genomeMap as createGenomeMap } from "../util/entrop
import { calcNodeColor } from "../util/colorHelpers";
import { calcColorScale, createVisibleLegendValues } from "../util/colorScale";
import { computeMatrixFromRawData, checkIfNormalizableFromRawData } from "../util/processFrequencies";
import { applyInViewNodesToTree } from "../actions/tree";
import { applyInViewNodesToTree } from "./tree";
import { validateScatterVariables } from "../util/scatterplotHelpers";
import { isColorByGenotype, decodeColorByGenotype, encodeColorByGenotype, decodeGenotypeFilters, encodeGenotypeFilters, getCdsFromGenotype } from "../util/getGenotype";
import { getTraitFromNode, getDivFromNode, collectGenotypeStates } from "../util/treeMiscHelpers";
Expand All @@ -24,6 +24,10 @@ import { hasMultipleGridPanels } from "./panelDisplay";
import { strainSymbolUrlString } from "../middleware/changeURL";
import { combineMeasurementsControlsAndQuery, loadMeasurements } from "./measurements";

import { DatasetJson } from "../types/datasetJson";
import { IncompleteMetadataState, convertIncompleteMetadataStateToMetadataState } from "../reducers/metadata";
import { RootState } from "../store";

export const doesColorByHaveConfidence = (controlsState, colorBy) =>
controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy);

Expand Down Expand Up @@ -153,8 +157,10 @@ const modifyStateViaURLQuery = (state, query) => {
console.error("Invalid 'animate' URL query (invalid date range)")
delete query.animate
} else {
window.NEXTSTRAIN.animationStartPoint = _dminNum;
window.NEXTSTRAIN.animationEndPoint = _dmaxNum;
if ('NEXTSTRAIN' in window) {
(window.NEXTSTRAIN as any).animationStartPoint = calendarToNumeric(params[0]);
(window.NEXTSTRAIN as any).animationEndPoint = calendarToNumeric(params[1]);
}
state.dateMin = _dmin;
state.dateMax = _dmax;
state.dateMinNumeric = _dminNum;
Expand Down Expand Up @@ -268,22 +274,23 @@ const modifyStateViaMetadata = (state, metadata, genomeMap) => {
const expectedTypes = ["string", "string", "string", "string", "boolean", "string", 'string', 'string', "boolean" , "boolean"];

for (let i = 0; i < keysToCheckFor.length; i += 1) {
if (Object.hasOwnProperty.call(metadata.displayDefaults, keysToCheckFor[i])) {
if (typeof metadata.displayDefaults[keysToCheckFor[i]] === expectedTypes[i]) {
const key = keysToCheckFor[i] as string; // cast as TS doesn't know i is a valid index of keysToCheckFor
if (Object.hasOwnProperty.call(metadata.displayDefaults, key)) {
if (typeof metadata.displayDefaults[key] === expectedTypes[i]) {
if (keysToCheckFor[i] === "sidebar") {
if (metadata.displayDefaults[keysToCheckFor[i]] === "open") {
if (metadata.displayDefaults[key] === "open") {
state.defaults.sidebarOpen = true;
state.sidebarOpen = true;
} else if (metadata.displayDefaults[keysToCheckFor[i]]=== "closed") {
} else if (metadata.displayDefaults[key]=== "closed") {
state.defaults.sidebarOpen = false;
state.sidebarOpen = false;
} else {
console.error("Skipping 'display_default' for sidebar as it's not 'open' or 'closed'");
}
} else {
/* most of the time if key=geoResolution, set both state.geoResolution and state.defaults.geoResolution */
state[keysToCheckFor[i]] = metadata.displayDefaults[keysToCheckFor[i]];
state.defaults[keysToCheckFor[i]] = metadata.displayDefaults[keysToCheckFor[i]];
state[key] = metadata.displayDefaults[key];
state.defaults[key] = metadata.displayDefaults[key];
}
} else {
console.error("Skipping 'display_default' for ", keysToCheckFor[i], "as it is not of type ", expectedTypes[i]);
Expand Down Expand Up @@ -403,7 +410,7 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => {
state.coloringsPresentOnTree = new Set();
state.coloringsPresentOnTreeWithConfidence = new Set(); // subset of above

let coloringsToCheck = [];
let coloringsToCheck: string[] = [];
if (colorings) {
coloringsToCheck = Object.keys(colorings);
}
Expand Down Expand Up @@ -467,7 +474,7 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => {
return state;
};

const checkAndCorrectErrorsInState = (state, metadata, genomeMap, query, tree, viewingNarrative) => {
const checkAndCorrectErrorsInState = (state, metadata: IncompleteMetadataState, genomeMap, query, tree, viewingNarrative) => {
/* want to check that the (currently set) colorBy (state.colorBy) is valid,
* and fall-back to an available colorBy if not
*/
Expand Down Expand Up @@ -618,7 +625,8 @@ const checkAndCorrectErrorsInState = (state, metadata, genomeMap, query, tree, v
continue
}
/* delete filter names (e.g. country, region) which aren't observed on the tree */
if (!Object.keys(tree.totalStateCounts).includes(traitName) && traitName!==strainSymbol && traitName!==genotypeSymbol) {
// TS TODO - remove the following cast (and check code _was_ working as I thought it was...)
if (!Object.keys(tree.totalStateCounts).includes(traitName as string) && traitName!==strainSymbol && traitName!==genotypeSymbol) {
delete state.filters[traitName];
delete query[_queryKey(traitName)];
continue
Expand Down Expand Up @@ -732,11 +740,10 @@ const convertColoringsListToDict = (coloringsList) => {
};

/**
*
* A lot of this is simply changing augur's snake_case to auspice's camelCase
*/
const createMetadataStateFromJSON = (json) => {
const metadata = {};
const createMetadataStateFromJSON = (json: DatasetJson): IncompleteMetadataState => {
const metadata: IncompleteMetadataState = {loaded: false};
if (json.meta.colorings) {
metadata.colorings = convertColoringsListToDict(json.meta.colorings);
}
Expand Down Expand Up @@ -794,10 +801,10 @@ const createMetadataStateFromJSON = (json) => {
}


if (Object.prototype.hasOwnProperty.call(metadata, "loaded")) {
if (Object.prototype.hasOwnProperty.call(json.meta, "loaded")) {
console.error("Metadata JSON must not contain the key \"loaded\". Ignoring.");
}
metadata.loaded = true;
// metadata.loaded = true; // TODO XXX
return metadata;
};

Expand All @@ -819,7 +826,7 @@ function updateMetadataStateViaSecondTree(metadata, json, genomeMap) {
metadata.identicalGenomeMapAcrossBothTrees = isEqualWith(
genomeMap,
createGenomeMap(json.meta.genome_annotations),
(objValue, othValue, indexOrKey) => {
(_objValue, _othValue, indexOrKey) => {
if (indexOrKey==='color') return true; // don't compare CDS colors!
// don't compare metadata section as there may be Infinities here
// (and if everything else is equal then the metadata will be the same too)
Expand Down Expand Up @@ -852,6 +859,19 @@ export const getNarrativePageFromQuery = (query, narrative) => {
return n;
};

/* TS TODO */
interface CreateStateArgs {
json: DatasetJson | false;
secondTreeDataset: any;
measurementsData: any;
oldState: RootState | false;
narrativeBlocks: any; /* if in a narrative this argument is set */
mainTreeName: string | false;
secondTreeName: string | false;
query?: any;
dispatch: any; /* redux should define this for us... */
}

export const createStateFromQueryOrJSONs = ({
json = false, /* raw json data - completely nuke existing redux state */
measurementsData = false, /* raw measurements json data or error, only used when main json is provided */
Expand All @@ -862,8 +882,9 @@ export const createStateFromQueryOrJSONs = ({
secondTreeName = false,
query,
dispatch
}) => {
let tree, treeToo, entropy, controls, metadata, narrative, frequencies, measurements;
}: CreateStateArgs) => {
let tree, treeToo, entropy, controls, narrative, frequencies, measurements;
let metadata: IncompleteMetadataState;
/* first task is to create metadata, entropy, controls & tree partial state */
if (json) {
/* create metadata state */
Expand Down Expand Up @@ -895,11 +916,14 @@ export const createStateFromQueryOrJSONs = ({
entropy = {...oldState.entropy};
tree = {...oldState.tree};
treeToo = {...oldState.treeToo};
metadata = {...oldState.metadata};
metadata = {...oldState.metadata, loaded: false};
frequencies = {...oldState.frequencies};
measurements = {...oldState.measurements};
controls = restoreQueryableStateToDefaults(controls);
controls = modifyStateViaMetadata(controls, metadata, entropy.genomeMap);
} else {
// can this _ever_ occur? If it does then `metadata` is undefined...
throw new Error("TKTK")
}

/* For the creation of state, we want to parse out URL query parameters
Expand Down Expand Up @@ -949,7 +973,7 @@ export const createStateFromQueryOrJSONs = ({


/* calculate colours if loading from JSONs or if the query demands change */
if (json || controls.colorBy !== oldState.controls.colorBy) {
if (json || (oldState && controls.colorBy !== oldState.controls.colorBy)) {
const colorScale = calcColorScale(controls.colorBy, controls, tree, treeToo, metadata);
const nodeColors = calcNodeColor(tree, colorScale);
controls.colorScale = colorScale;
Expand Down Expand Up @@ -1043,7 +1067,9 @@ export const createStateFromQueryOrJSONs = ({
/* if narratives then switch the query back to ?n=<SLIDE> for display */
if (narrativeBlocks) query = {n: narrativeSlideIdx};

return {tree, treeToo, metadata, entropy, controls, narrative, frequencies, measurements, query};
const meta = convertIncompleteMetadataStateToMetadataState(metadata);

return {tree, treeToo, metadata: meta, entropy, controls, narrative, frequencies, measurements, query};
};

export const createTreeTooState = ({
Expand Down
36 changes: 18 additions & 18 deletions src/components/info/byline.js → src/components/info/byline.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> {
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)}
Expand All @@ -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 (
Expand All @@ -55,10 +55,10 @@ function renderAvatar(t, metadata) {
* Returns a React component detailing the source of the build (pipeline).
* Renders a <span> 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 (
<span>
Expand All @@ -80,11 +80,11 @@ function renderBuildInfo(t, metadata) {
* Returns a React component detailing the maintainers of the build (pipeline).
* Renders a <span> 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 (
<span>
{t("Maintained by") + " "}
Expand All @@ -106,7 +106,7 @@ function renderMaintainers(t, metadata) {
* Returns a React component detailing the date the data was last updated.
* Renders a <span> 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 (
<span>
Expand All @@ -122,7 +122,7 @@ function renderDataUpdated(t, metadata) {
* Renders a <span> 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")
Expand Down Expand Up @@ -174,7 +174,7 @@ const BylineLink = styled.a`
font-weight: 500;
`;

function Link({url, children}) {
function Link({url, children}): JSX.Element {
return (
<BylineLink rel="noopener noreferrer" href={url} target="_blank">
{children}
Expand Down
51 changes: 32 additions & 19 deletions src/components/info/info.js → src/components/info/info.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof connector>

interface Props extends PropsFromRedux {
t: any; // TODO XXX - look up how to type WithTranslation
width: number;
}

/**
* The <Info> panel is shown above data viz panels and conveys static and dynamic
Expand All @@ -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<Props> {
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);
Expand All @@ -40,15 +53,15 @@ class Info extends React.Component {
<Card center infocard>
<div style={styles.base}>

<div width={this.props.width} style={styles.title}>
{this.props.metadata.title || ""}
<div style={styles.title}>
{this.props.metadata.title}
</div>

<div width={this.props.width} style={styles.byline}>
<Byline/>
<div style={styles.byline}>
<Byline metadata={this.props.metadata}/>
</div>

<div width={this.props.width} style={styles.n}>
<div style={styles.n}>
{animating ? t("Animation in progress") + ". " : null}
{showExtended &&
<>
Expand Down Expand Up @@ -111,5 +124,5 @@ function computeStyles(width, browserWidth) {
};
}

const WithTranslation = withTranslation()(Info);
const WithTranslation = withTranslation()(connector(Info));
export default WithTranslation;
Loading
Loading