From f2e5faa1312699fbdb319be87cba97caf61f2699 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:22:00 -0800 Subject: [PATCH 01/20] Add Zustand state store --- src/state-store.js | 174 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/state-store.js diff --git a/src/state-store.js b/src/state-store.js new file mode 100644 index 0000000..3096173 --- /dev/null +++ b/src/state-store.js @@ -0,0 +1,174 @@ +// This is the Zustand state store for the app. + +// Note: We use package `moment` for date arithmetic. It is excellent but it +// *mutates* its objects. We are using functional components, +// which require values whose identity changes when their value is changed, +// i.e., a new object. Therefore, every change to a `moment` date object should +// be preceded by `.clone()`; for example, `y = x.clone().subtract(1, 'month')` +// yields a new moment object with a value 1 month before the `x` object. `x` is +// unchanged by this operation; `y` is a different object than `x`. + + +import { create } from 'zustand'; +import moment from 'moment/moment'; +import { + getBaselineData, + getLastDateWithDataBefore, + getMonthlyData, +} from './data-services/weather-anomaly-data-service'; +import axios from 'axios'; +import yaml from 'js-yaml'; +import filter from 'lodash/fp/filter'; +import isUndefined from 'lodash/fp/isUndefined'; + + +// Comments: +// - The store knows a lot here, such as the structure of config and the names +// and usages of the data services. OK? Too much dependency? +// + +// Likely latest possible date of available data = current date - 15 d. +// This allows for cron jobs that run in first half of month. +// Subtract fewer/more days if cron jobs run earlier/later in month. +// But it is not guaranteed that there is data for this date; that can only be +// determined by consulting the backend. +export const latestPossibleDataDate = moment().subtract(15, 'days'); + + +export const useStore = create((set, get) => ({ + // States + config: null, + configError: null, + + variable: null, // config.ui.variableSelector.initial + dataset: null, // config.ui.datasetSelector.initial + baseline: null, + monthly: null, + date: latestPossibleDataDate, // TODO: Put subtraction value in config + + // Actions + + // Important: Wrap in useEffect + // Load configuration + // This should not be called except by initialize. + _loadConfig: ({ defaultConfig= {}, requiredConfigKeys = []}) => { + return axios.get(`${process.env.PUBLIC_URL}/config.yaml`) + .then(response => { + // Extend default config with values loaded from config.yaml + let config; + try { + const customConfig = yaml.load(response.data); + config = { ...defaultConfig, ...customConfig }; + } catch (error) { + set({ + configError: ( +
Error parsing config.yaml:
{error.toString()}
+ ) + }); + throw error; + } + + // Check for required config keys (we don't check value types, yet) + const missingRequiredKeys = filter( + key => isUndefined(config[key]), requiredConfigKeys + ); + if (missingRequiredKeys.length > 0) { + const configErrorMsg = ( + `Error in config.yaml: The following keys must have values, + but do not: ${missingRequiredKeys}` + ); + set({ configError: (
{configErrorMsg}
) }); + throw new Error(configErrorMsg); + } + + // TODO: Is there really any value in this? + // Alternatively, just transfer everything in process.env here. + // None of that makes much sense ... + // Extend config with some env var values + config.appVersion = process.env.REACT_APP_APP_VERSION ?? "unknown"; + + // Update the config state. + set({ config }); + }) + .catch(error => { + set({ + configError: ( +
Error fetching configuration:
{error.toString()}
+ ) + }); + throw error; + }); + }, + + // Important: Wrap in useEffect + // Initialize state from config and other async data sources. + initialize: () => { + // TODO: This can probably be done more nicely with async/await. + get()._loadConfig().then(() => { + // TODO: return config from _loadConfig as well as setting + // state.config. Would be neater. + const config = get().config; + const wadsUrl = config.backends.weatherAnomalyDataService; + const variable = config.ui.variableSelector.initial; + const dataset = config.ui.datasetSelector.initial; + set({ variable, dataset }); + getLastDateWithDataBefore(variable, latestPossibleDataDate, wadsUrl) + .then(date => { + // Note: This will also fetch the data for this date (and variable). + // We only want to do this once, so we don't call setVariable also. + get().setDate(date); + }); + }); + }, + + // Important: Wrap in useEffect + // Set the variable and load the data associated with it. + setVariable: (variable) => { + set({ variable }); + get().getData(); + }, + + // Important: Wrap in useEffect + // Set the date and load the data associated with it. + setDate: (date) => { + set({ date }); + get().getData(); + }, + + // Important: Wrap in useEffect + // Fetch baseline and monthly data for current setting of variable and date. + getData: () => { + const wadsUrl = get().config.backends.weatherAnomalyDataService; + set({ baseline: null, monthly: null }); + getBaselineData(get().variable, get().date, wadsUrl) + .then(r => { + set({ baseline: r.data }); + }); + getMonthlyData(get().variable, get().date, wadsUrl) + .then(r => { + set({ monthly: r.data }); + }); + }, + + isDataLoading: () => get().baseline === null || get().monthly === null, + + isBaselineDataset: () => dataset === 'baseline', + + setYear: year => { + get().setDate(get().date.clone().year(year)); + }, + + incrementYear: by => { + get().setDate(get().date.clone().add(by, 'year')); + }, + + setMonth: month => { + get().setDate(get().date.clone().month(month)); + }, + + incrementMonth: by => { + get().setDate(get().date.clone().add(by, 'month')); + }, + + setBaseline: baseline => set({ baseline }), +})) \ No newline at end of file From 575edbd8a69d6fbf334a2aa3a3bc89f7d17b44f6 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:22:38 -0800 Subject: [PATCH 02/20] Convert DatasetSelector to use store --- .../DatasetSelector/DatasetSelector.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/controls/DatasetSelector/DatasetSelector.js b/src/components/controls/DatasetSelector/DatasetSelector.js index 3a2be40..461a1aa 100644 --- a/src/components/controls/DatasetSelector/DatasetSelector.js +++ b/src/components/controls/DatasetSelector/DatasetSelector.js @@ -6,19 +6,32 @@ import map from 'lodash/fp/map'; import DatasetLabel from '../../datasets/DatasetLabel'; import RadioButtonSelector from '../RadioButtonSelector'; -import { useConfigContext } from '../../main/ConfigContext'; +import { useStore } from '../../../state-store'; function DatasetSelector(props) { - const config = useConfigContext(); + const config = useStore(state => state.config); + + // TODO: Memoize const options = flow( keys, map(dataset => ({ value: dataset, label: }) ), )(config.datasets); + + const dataset = useStore(state => state.dataset); + const setDataset = useStore(state => state.setDataset); + return ( - + ); } From cd6f3ae620996e5b5e875cdfbf0b88eafe1552e9 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:22:52 -0800 Subject: [PATCH 03/20] Convert MonthSelector to use store --- src/components/controls/MonthSelector/MonthSelector.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/controls/MonthSelector/MonthSelector.js b/src/components/controls/MonthSelector/MonthSelector.js index 13a40a2..ba4313d 100644 --- a/src/components/controls/MonthSelector/MonthSelector.js +++ b/src/components/controls/MonthSelector/MonthSelector.js @@ -1,15 +1,23 @@ import React from 'react'; import ThrottledInputRange from '../ThrottledInputRange'; +import { useStore } from '../../../state-store'; const monthNames = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' '); const formatLabel = value => monthNames[value]; function MonthSelector(props) { + const date = useStore(state => state.date); + const setMonth = useStore(state => state.setMonth); + const isDataLoading = useStore(state => state.isDataLoading); + return ( ); From 227e100fc21d048689f5a44a716c295d58c34b66 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:23:01 -0800 Subject: [PATCH 04/20] Convert VariableSelector to use store --- .../VariableSelector/VariableSelector.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/controls/VariableSelector/VariableSelector.js b/src/components/controls/VariableSelector/VariableSelector.js index 3b30895..cbea199 100644 --- a/src/components/controls/VariableSelector/VariableSelector.js +++ b/src/components/controls/VariableSelector/VariableSelector.js @@ -3,19 +3,34 @@ import PropTypes from 'prop-types'; import { Tooltip } from 'react-bootstrap'; import keys from 'lodash/fp/keys'; -import { useConfigContext } from '../../main/ConfigContext'; import RadioButtonSelector from '../RadioButtonSelector'; import VariableLabel from '../../variables/VariableLabel'; +import { useStore } from '../../../state-store'; function VariableSelector(props) { - const config = useConfigContext(); + const config = useStore(state => state.config); + + // TODO: Memoize variableOptions const variableKeys = keys(config?.variables); const variableOptions = variableKeys.map( value => ({ value, label: }) ); + + const variable = useStore(state => state.variable); + const setVariable = useStore(state => state.setVariable); + const isDataLoading = useStore(state => state.isDataLoading); + return ( - + ); } From ded47f75059110bc45787187c243b4acb7f45f73 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:23:11 -0800 Subject: [PATCH 05/20] Convert YearSelector to use store --- .../controls/YearSelector/YearSelector.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/controls/YearSelector/YearSelector.js b/src/components/controls/YearSelector/YearSelector.js index 9287dc7..abd1807 100644 --- a/src/components/controls/YearSelector/YearSelector.js +++ b/src/components/controls/YearSelector/YearSelector.js @@ -1,9 +1,21 @@ import React from 'react'; import ThrottledInputRange from '../ThrottledInputRange'; +import { latestPossibleDataDate, useStore } from '../../../state-store'; function YearSelector(props) { + const date = useStore(state => state.date); + const setYear = useStore(state => state.setYear); + const isDataLoading = useStore(state => state.isDataLoading); + return ( - + ); } From e9428d74cd0efdbe95e72146d5f50daeda79c10d Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:23:54 -0800 Subject: [PATCH 06/20] Add store-based month and year inc/dec selectors --- .../MonthIncrementDecrement.js | 21 +++++++++++++++++++ .../MonthIncrementDecrement/package.json | 6 ++++++ .../YearIncrementDecrement.js | 21 +++++++++++++++++++ .../YearIncrementDecrement/package.json | 6 ++++++ 4 files changed, 54 insertions(+) create mode 100644 src/components/controls/MonthIncrementDecrement/MonthIncrementDecrement.js create mode 100644 src/components/controls/MonthIncrementDecrement/package.json create mode 100644 src/components/controls/YearIncrementDecrement/YearIncrementDecrement.js create mode 100644 src/components/controls/YearIncrementDecrement/package.json diff --git a/src/components/controls/MonthIncrementDecrement/MonthIncrementDecrement.js b/src/components/controls/MonthIncrementDecrement/MonthIncrementDecrement.js new file mode 100644 index 0000000..9261db0 --- /dev/null +++ b/src/components/controls/MonthIncrementDecrement/MonthIncrementDecrement.js @@ -0,0 +1,21 @@ +import React from 'react'; +import IncrementDecrement from '../../controls/IncrementDecrement'; +import { useStore } from '../../../state-store'; + +export default function MonthIncrementDecrement(props) { + const config = useStore(state => state.config); + const incrementMonth = useStore(state => state.incrementMonth); + const isDataLoading = useStore(state => state.isDataLoading); + + return ( + + + ) +} \ No newline at end of file diff --git a/src/components/controls/MonthIncrementDecrement/package.json b/src/components/controls/MonthIncrementDecrement/package.json new file mode 100644 index 0000000..8e141f8 --- /dev/null +++ b/src/components/controls/MonthIncrementDecrement/package.json @@ -0,0 +1,6 @@ +{ + "name": "MonthIncrementDecrement", + "version": "0.0.0", + "private": true, + "main": "./MonthIncrementDecrement.js" +} \ No newline at end of file diff --git a/src/components/controls/YearIncrementDecrement/YearIncrementDecrement.js b/src/components/controls/YearIncrementDecrement/YearIncrementDecrement.js new file mode 100644 index 0000000..2843a70 --- /dev/null +++ b/src/components/controls/YearIncrementDecrement/YearIncrementDecrement.js @@ -0,0 +1,21 @@ +import React from 'react'; +import IncrementDecrement from '../../controls/IncrementDecrement'; +import { useStore } from '../../../state-store'; + +export default function YearIncrementDecrement(props) { + const config = useStore(state => state.config); + const incrementYear = useStore(state => state.incrementYear); + const isDataLoading = useStore(state => state.isDataLoading); + + return ( + + + ) +} \ No newline at end of file diff --git a/src/components/controls/YearIncrementDecrement/package.json b/src/components/controls/YearIncrementDecrement/package.json new file mode 100644 index 0000000..cae82b3 --- /dev/null +++ b/src/components/controls/YearIncrementDecrement/package.json @@ -0,0 +1,6 @@ +{ + "name": "YearIncrementDecrement", + "version": "0.0.0", + "private": true, + "main": "./YearIncrementDecrement.js" +} \ No newline at end of file From 68d0959ed2d946106a7619f5002a474057801bab Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:24:24 -0800 Subject: [PATCH 07/20] Add store-aware DataDisplay component --- src/components/map/DataDisplay/DataDisplay.js | 37 +++++++++++++++++++ src/components/map/DataDisplay/package.json | 6 +++ 2 files changed, 43 insertions(+) create mode 100644 src/components/map/DataDisplay/DataDisplay.js create mode 100644 src/components/map/DataDisplay/package.json diff --git a/src/components/map/DataDisplay/DataDisplay.js b/src/components/map/DataDisplay/DataDisplay.js new file mode 100644 index 0000000..2f74e49 --- /dev/null +++ b/src/components/map/DataDisplay/DataDisplay.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { Col, Row } from 'react-bootstrap'; + +import ColourScale from '../../map/ColourScale'; +import DataMap from '../../map/DataMap'; +import VariableTitle from '../../variables/VariableTitle'; +import { useStore } from '../../../state-store'; + +export default function DataDisplay() { + const variable = useStore(state => state.variable); + const dataset = useStore(state => state.dataset); + const baseline = useStore(state => state.baseline); + const monthly = useStore(state => state.monthly); + + return ( + <> + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/map/DataDisplay/package.json b/src/components/map/DataDisplay/package.json new file mode 100644 index 0000000..d6ab28f --- /dev/null +++ b/src/components/map/DataDisplay/package.json @@ -0,0 +1,6 @@ +{ + "name": "DataDisplay", + "version": "0.0.0", + "private": true, + "main": "./DataDisplay.js" +} \ No newline at end of file From f0a618c374cc328cc5311af168936c860e66098f Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:25:24 -0800 Subject: [PATCH 08/20] Add some doc --- src/components/variables/VariableTitle/VariableTitle.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/variables/VariableTitle/VariableTitle.js b/src/components/variables/VariableTitle/VariableTitle.js index 6743e73..167054d 100644 --- a/src/components/variables/VariableTitle/VariableTitle.js +++ b/src/components/variables/VariableTitle/VariableTitle.js @@ -1,3 +1,7 @@ +// Variable title: Long-form description of variable for titling displays of +// data. It is generic, meaning it does not subscribe to the state store, but +// accepts arbitrary inputs for these values. + import React from 'react'; import compact from 'lodash/fp/compact'; import flow from 'lodash/fp/flow'; @@ -6,7 +10,6 @@ import { alternate } from '../../utils'; import VariableLabel from '../VariableLabel'; import VariableUnits from '../VariableUnits'; import DatasetLabel from '../../datasets/DatasetLabel'; -import { useConfigContext } from '../../main/ConfigContext'; export default function VariableTitle({ From 30ba238c4bf75cee8b7301b09d9821fa3385dfdb Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:26:09 -0800 Subject: [PATCH 09/20] Modify main app for store-aware components --- src/components/main/Tool/Tool.js | 209 +++++++++++-------------------- 1 file changed, 73 insertions(+), 136 deletions(-) diff --git a/src/components/main/Tool/Tool.js b/src/components/main/Tool/Tool.js index f14c87e..6734467 100644 --- a/src/components/main/Tool/Tool.js +++ b/src/components/main/Tool/Tool.js @@ -1,96 +1,85 @@ import React, { useEffect, useState } from 'react'; import { Col, Row } from 'react-bootstrap'; -import moment from 'moment'; import DatasetSelector from '../../controls/DatasetSelector' import VariableSelector from '../../controls/VariableSelector' import YearSelector from '../../controls/YearSelector'; import MonthSelector from '../../controls/MonthSelector'; -import IncrementDecrement from '../../controls/IncrementDecrement'; -import ColourScale from '../../map/ColourScale'; -import DataMap from '../../map/DataMap'; -import VariableTitle from '../../variables/VariableTitle'; +import MonthIncrementDecrement from '../../controls/MonthIncrementDecrement'; +import YearIncrementDecrement from '../../controls/YearIncrementDecrement'; +import DataDisplay from '../../map/DataDisplay'; -import { getBaselineData, getLastDateWithDataBefore, getMonthlyData } - from '../../../data-services/weather-anomaly-data-service'; -import { useConfigContext } from '../ConfigContext'; +import { useStore } from '../../../state-store'; import 'react-input-range/lib/css/index.css'; import './Tool.css'; -// Note: We use package `moment` for date arithmetic. It is excellent but it -// *mutates* its objects. We are using functional components, -// which require values whose identity changes when their value is changed, -// i.e., a new object. Therefore every change to a `moment` date object should -// be preceded by `.clone()`; for example, `y = x.clone().subtract(1, 'month')` -// yields a new moment object with a value 1 month before the original `x` -// object. `x` is unchanged by this operation, and `y` is a different object -// than `x`. - -// Compute likely latest possible date of available data = current date - 15 d. -// This allows for cron jobs that run in first half of month. -// Subtract fewer/more days if cron jobs run earlier/later in month. -// But it is not guaranteed that there is data for this date; that can only be -// determined by consulting the backend. -const latestPossibleDataDate = moment().subtract(15, 'days'); - export default function Tool() { - const config = useConfigContext(); - const wadsUrl = config.backends.weatherAnomalyDataService; + const initialize = useStore(state => state.initialize); + const isBaselineDataset = useStore(state => state.isBaselineDataset); - const [variable, setVariable] = useState(config.ui.variableSelector.initial); - const [dataset, setDataset] = useState(config.ui.datasetSelector.initial); - const [date, setDate] = useState(latestPossibleDataDate); - const [baseline, setBaseline] = useState(null); - const [monthly, setMonthly] = useState(null); - - // Determine latest date with data, and set date to it. This happens once, - // on first render. - useEffect(() => { - setBaseline(null); - setMonthly(null); - getLastDateWithDataBefore(variable, date, wadsUrl) - .then(date => { - setDate(date); - }); - }, []); + // Subsumed in state store, not used in this component + // const [variable, setVariable] = useState(config.ui.variableSelector.initial); + // const [dataset, setDataset] = useState(config.ui.datasetSelector.initial); + // const [date, setDate] = useState(latestPossibleDataDate); + // const [baseline, setBaseline] = useState(null); + // const [monthly, setMonthly] = useState(null); - // When variable or date changes, get data. - // (Both datasets are is retrieved for all values of `dataset`. This could - // be refined to get only the dataset(s) required by the value of `dataset`.) - // Consider splitting this into two separate effects, with an if on `dataset`. useEffect(() => { - setBaseline(null); - setMonthly(null); - getBaselineData(variable, date, wadsUrl) - .then(r => { - setBaseline(r.data); - }); - getMonthlyData(variable, date, wadsUrl) - .then(r => { - setMonthly(r.data); - }); - }, [variable, date]); - - const handleChangeMonth = (month) => { - setDate((date) => date.clone().month(month)); - }; - - const handleChangeYear = (year) => { - setDate((date) => date.clone().year(year)); - }; - - const handleIncrementYear = (by) => { - setDate((date) => date.clone().add(by, 'year')); - }; - - const handleIncrementMonth = (by) => { - setDate((date) => date.clone().add(by, 'month')); - }; - - const isDataLoading = baseline === null || monthly === null; - const isBaselineDataset = dataset === 'baseline'; + initialize(); + }); + + // Part of initialize + // // Determine latest date with data, and set date to it. This happens once, + // // on first render. + // useEffect(() => { + // setBaseline(null); + // setMonthly(null); + // getLastDateWithDataBefore(variable, date, wadsUrl) + // .then(date => { + // setDate(date); + // }); + // }, []); + + // Folded into store actions + // // When variable or date changes, get data. + // // (Both datasets are is retrieved for all values of `dataset`. This could + // // be refined to get only the dataset(s) required by the value of `dataset`.) + // // Consider splitting this into two separate effects, with an if on `dataset`. + // useEffect(() => { + // setBaseline(null); + // setMonthly(null); + // getBaselineData(variable, date, wadsUrl) + // .then(r => { + // setBaseline(r.data); + // }); + // getMonthlyData(variable, date, wadsUrl) + // .then(r => { + // setMonthly(r.data); + // }); + // }, [variable, date]); + + // Handlers moved to state store actions + // const handleChangeMonth = (month) => { + // setDate((date) => date.clone().month(month)); + // }; + // + // const handleChangeYear = (year) => { + // setDate((date) => date.clone().year(year)); + // }; + // + // const handleIncrementYear = (by) => { + // setDate((date) => date.clone().add(by, 'year')); + // }; + // + // const handleIncrementMonth = (by) => { + // setDate((date) => date.clone().add(by, 'month')); + // }; + + // Computed values moved to state store actions + // const isDataLoading = baseline === null || monthly === null; + // const isBaselineDataset = dataset === 'baseline'; const displayColWidths = { xs: 12, md: "auto" }; const rowSpacing = "mt-3" @@ -102,92 +91,40 @@ export default function Tool() { Display - + - + for - + - + - {!isBaselineDataset && + {!isBaselineDataset() && - + - + } - - - - - - - - - - - - - + From 3d603a3e46ae7ad0fd53f93243528504c59f4791 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:34:34 -0800 Subject: [PATCH 10/20] Use config from store in remaining components --- src/components/datasets/DatasetLabel/DatasetLabel.js | 4 ++-- src/components/main/Header/Header.js | 4 ++-- src/components/map/ColourScale/ColourScale.js | 4 ++-- src/components/map/DataMap/DataMap.js | 4 ++-- src/components/map/StationPopup/StationPopup.js | 4 ++-- src/components/variables/VariableLabel/VariableLabel.js | 4 ++-- src/components/variables/VariableUnits/VariableUnits.js | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/datasets/DatasetLabel/DatasetLabel.js b/src/components/datasets/DatasetLabel/DatasetLabel.js index dffcf5c..c4ddd74 100644 --- a/src/components/datasets/DatasetLabel/DatasetLabel.js +++ b/src/components/datasets/DatasetLabel/DatasetLabel.js @@ -1,8 +1,8 @@ import React from 'react'; -import { useConfigContext } from '../../main/ConfigContext'; +import { useStore } from '../../../state-store'; export default function DatasetLabel({ dataset }) { - const config = useConfigContext(); + const config = useStore(state => state.config); return ( state.config); return ( diff --git a/src/components/map/ColourScale/ColourScale.js b/src/components/map/ColourScale/ColourScale.js index d56339a..9c5ee95 100644 --- a/src/components/map/ColourScale/ColourScale.js +++ b/src/components/map/ColourScale/ColourScale.js @@ -1,6 +1,6 @@ import React from 'react'; -import { useConfigContext } from '../../main/ConfigContext'; +import { useStore } from '../../../state-store'; export default function ColourScale({ @@ -23,7 +23,7 @@ export default function ColourScale({ return csItem.annotation; }, }) { - const config = useConfigContext(); + const config = useStore(state => state.config); const colourScale = config.colourScales[variable][dataset]; const numItems = colourScale.length; diff --git a/src/components/map/DataMap/DataMap.js b/src/components/map/DataMap/DataMap.js index b361f66..9978463 100644 --- a/src/components/map/DataMap/DataMap.js +++ b/src/components/map/DataMap/DataMap.js @@ -10,14 +10,14 @@ import compact from 'lodash/fp/compact'; import { BCBaseMap } from 'pcic-react-leaflet-components'; import './DataMap.css'; -import { useConfigContext } from '../../main/ConfigContext'; import MapSpinner from '../MapSpinner'; import StationDataMarkers from '../StationDataMarkers'; import StationLocationMarkers from '../StationLocationMarkers'; +import { useStore } from '../../../state-store'; export default function DataMap({ dataset, variable, monthly, baseline }) { - const config = useConfigContext(); + const config = useStore(state => state.config); const stationsForDataset = useMemo( () => { diff --git a/src/components/map/StationPopup/StationPopup.js b/src/components/map/StationPopup/StationPopup.js index 4d3148a..67a61a0 100644 --- a/src/components/map/StationPopup/StationPopup.js +++ b/src/components/map/StationPopup/StationPopup.js @@ -4,7 +4,7 @@ import { Popup } from 'react-leaflet' import VariableLabel from '../../variables/VariableLabel'; import VariableUnits from '../../variables/VariableUnits'; -import { useConfigContext } from '../../main/ConfigContext'; +import { useStore } from '../../../state-store'; export default function StationPopup({ @@ -24,7 +24,7 @@ export default function StationPopup({ departure, }, }) { - const config = useConfigContext(); + const config = useStore(state => state.config); const units = ; const decimalPlaces = config?.variables?.[variable]?.decimalPlaces; diff --git a/src/components/variables/VariableLabel/VariableLabel.js b/src/components/variables/VariableLabel/VariableLabel.js index 667b84e..5fd210a 100644 --- a/src/components/variables/VariableLabel/VariableLabel.js +++ b/src/components/variables/VariableLabel/VariableLabel.js @@ -1,8 +1,8 @@ import React from 'react'; -import { useConfigContext } from '../../main/ConfigContext'; +import { useStore } from '../../../state-store'; export default function VariableLabel({ variable }) { - const config = useConfigContext(); + const config = useStore(state => state.config); return ; diff --git a/src/components/variables/VariableUnits/VariableUnits.js b/src/components/variables/VariableUnits/VariableUnits.js index 80b6cad..c7c8cbc 100644 --- a/src/components/variables/VariableUnits/VariableUnits.js +++ b/src/components/variables/VariableUnits/VariableUnits.js @@ -1,8 +1,8 @@ import React from 'react'; -import { useConfigContext } from '../../main/ConfigContext'; +import { useStore } from '../../../state-store'; export default function VariableUnits({ variable }) { - const config = useConfigContext(); + const config = useStore(state => state.config); return ( Date: Mon, 20 Nov 2023 16:39:33 -0800 Subject: [PATCH 11/20] Add todo --- src/state-store.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/state-store.js b/src/state-store.js index 3096173..e09065e 100644 --- a/src/state-store.js +++ b/src/state-store.js @@ -102,6 +102,7 @@ export const useStore = create((set, get) => ({ // Important: Wrap in useEffect // Initialize state from config and other async data sources. + // TODO: Add params and pass thru to _loadConfig initialize: () => { // TODO: This can probably be done more nicely with async/await. get()._loadConfig().then(() => { From 222464fad3897fb47ea06142bb8d78f52e61f89e Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:40:12 -0800 Subject: [PATCH 12/20] Move state-store init out to App --- src/components/main/App/App.js | 47 +++++++++++++++++++++----------- src/components/main/Tool/Tool.js | 5 ---- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/components/main/App/App.js b/src/components/main/App/App.js index 5647490..ce62ca2 100644 --- a/src/components/main/App/App.js +++ b/src/components/main/App/App.js @@ -1,26 +1,43 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Container } from 'react-bootstrap'; import logger from '../../../logger'; import Header from '../Header'; import Tool from '../Tool' -import ConfigContext, { useFetchConfigContext } from '../ConfigContext'; import './App.css'; +import { useStore } from '../../../state-store'; logger.configure({ active: true }); function App() { - // must be invoked before any other items dependent on context. - const [config, configErrorMessage] = useFetchConfigContext({ - defaultConfig: { - // TODO - }, - requiredConfigKeys: [ - // TODO - ], + const initialize = useStore(state => state.initialize); + const config = useStore(state => state.config); + const configErrorMessage = useStore(state => state.configError); + + // Initialize must be invoked before any items dependent on context. + useEffect(() => { + initialize({ + defaultConfig: { + // TODO + }, + requiredConfigKeys: [ + // TODO + ], + }); }); + // Now in state store + // must be invoked before any other items dependent on context. + // const [config, configErrorMessage] = useFetchConfigContext({ + // defaultConfig: { + // // TODO + // }, + // requiredConfigKeys: [ + // // TODO + // ], + // }); + if (configErrorMessage !== null) { // TODO: Improve presentation return ( @@ -36,12 +53,10 @@ function App() { } return ( - - -
- - - + +
+ + ); } diff --git a/src/components/main/Tool/Tool.js b/src/components/main/Tool/Tool.js index 6734467..2947162 100644 --- a/src/components/main/Tool/Tool.js +++ b/src/components/main/Tool/Tool.js @@ -16,7 +16,6 @@ import './Tool.css'; export default function Tool() { - const initialize = useStore(state => state.initialize); const isBaselineDataset = useStore(state => state.isBaselineDataset); // Subsumed in state store, not used in this component @@ -26,10 +25,6 @@ export default function Tool() { // const [baseline, setBaseline] = useState(null); // const [monthly, setMonthly] = useState(null); - useEffect(() => { - initialize(); - }); - // Part of initialize // // Determine latest date with data, and set date to it. This happens once, // // on first render. From 9bcc584608b9f73d8cd38296191c31acaa87ae35 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 16:40:33 -0800 Subject: [PATCH 13/20] Install Zustand This should have been the first commit! --- package-lock.json | 88 ++++++++++++++++++++++++++++++++++++++++++----- package.json | 3 +- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84cfa8a..33c02b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "weather-anomaly-tool", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "weather-anomaly-tool", - "version": "1.0.0", + "version": "2.0.0", "license": "GPL-3.0", "dependencies": { "@react-leaflet/core": "1.0.2", @@ -28,7 +28,8 @@ "react-scripts": "^4.0.3", "react-test-renderer": "^16.0.0", "svg-loaders-react": "^2.2.1", - "url-join": "^2.0.2" + "url-join": "^2.0.2", + "zustand": "^4.4.6" } }, "node_modules/@babel/code-frame": { @@ -10601,9 +10602,11 @@ } }, "node_modules/immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "optional": true, + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -16528,6 +16531,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", @@ -22053,6 +22065,41 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz", + "integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zustand/node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } } }, "dependencies": { @@ -30036,9 +30083,11 @@ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" }, "immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "optional": true, + "peer": true }, "import-cwd": { "version": "2.1.0", @@ -34590,6 +34639,11 @@ "slash": "^3.0.0" } }, + "immer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" + }, "loader-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", @@ -38976,6 +39030,22 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zustand": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.6.tgz", + "integrity": "sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==", + "requires": { + "use-sync-external-store": "1.2.0" + }, + "dependencies": { + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + } + } } } } diff --git a/package.json b/package.json index 9064d51..78db0e9 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "react-scripts": "^4.0.3", "react-test-renderer": "^16.0.0", "svg-loaders-react": "^2.2.1", - "url-join": "^2.0.2" + "url-join": "^2.0.2", + "zustand": "^4.4.6" }, "scripts": { "start": "react-scripts start", From fcc08a2653498bdcf76dcc488a28a4ef3513f81c Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 17:36:09 -0800 Subject: [PATCH 14/20] Clean up prop types --- .../controls/DatasetSelector/DatasetSelector.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/controls/DatasetSelector/DatasetSelector.js b/src/components/controls/DatasetSelector/DatasetSelector.js index 461a1aa..6a6f287 100644 --- a/src/components/controls/DatasetSelector/DatasetSelector.js +++ b/src/components/controls/DatasetSelector/DatasetSelector.js @@ -9,7 +9,7 @@ import RadioButtonSelector from '../RadioButtonSelector'; import { useStore } from '../../../state-store'; -function DatasetSelector(props) { +export default function DatasetSelector(props) { const config = useStore(state => state.config); // TODO: Memoize @@ -34,10 +34,3 @@ function DatasetSelector(props) { /> ); } - -DatasetSelector.propTypes = { - value: PropTypes.string, - onChange: PropTypes.func.isRequired, -}; - -export default DatasetSelector; From 94221596f312aec1a7871389ade072fb767cfd71 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 17:36:25 -0800 Subject: [PATCH 15/20] Fix usage of isDataLoading --- .../MonthIncrementDecrement.js | 4 ++-- .../controls/MonthSelector/MonthSelector.js | 8 +++----- .../controls/VariableSelector/VariableSelector.js | 13 ++----------- .../YearIncrementDecrement.js | 4 ++-- .../controls/YearSelector/YearSelector.js | 2 +- 5 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/components/controls/MonthIncrementDecrement/MonthIncrementDecrement.js b/src/components/controls/MonthIncrementDecrement/MonthIncrementDecrement.js index 9261db0..e96f57d 100644 --- a/src/components/controls/MonthIncrementDecrement/MonthIncrementDecrement.js +++ b/src/components/controls/MonthIncrementDecrement/MonthIncrementDecrement.js @@ -5,12 +5,12 @@ import { useStore } from '../../../state-store'; export default function MonthIncrementDecrement(props) { const config = useStore(state => state.config); const incrementMonth = useStore(state => state.incrementMonth); - const isDataLoading = useStore(state => state.isDataLoading); + const isDataLoading = useStore(state => state.isDataLoading()); return ( monthNames[value]; -function MonthSelector(props) { +export default function MonthSelector(props) { const date = useStore(state => state.date); const setMonth = useStore(state => state.setMonth); - const isDataLoading = useStore(state => state.isDataLoading); + const isDataLoading = useStore(state => state.isDataLoading()); return ( ); } - -export default MonthSelector; diff --git a/src/components/controls/VariableSelector/VariableSelector.js b/src/components/controls/VariableSelector/VariableSelector.js index cbea199..20a6e31 100644 --- a/src/components/controls/VariableSelector/VariableSelector.js +++ b/src/components/controls/VariableSelector/VariableSelector.js @@ -19,13 +19,13 @@ function VariableSelector(props) { const variable = useStore(state => state.variable); const setVariable = useStore(state => state.setVariable); - const isDataLoading = useStore(state => state.isDataLoading); + const isDataLoading = useStore(state => state.isDataLoading()); return ( Monthly total precipitation, tmin: Monthly average of daily minimum diff --git a/src/components/controls/YearIncrementDecrement/YearIncrementDecrement.js b/src/components/controls/YearIncrementDecrement/YearIncrementDecrement.js index 2843a70..c8ddfd4 100644 --- a/src/components/controls/YearIncrementDecrement/YearIncrementDecrement.js +++ b/src/components/controls/YearIncrementDecrement/YearIncrementDecrement.js @@ -5,12 +5,12 @@ import { useStore } from '../../../state-store'; export default function YearIncrementDecrement(props) { const config = useStore(state => state.config); const incrementYear = useStore(state => state.incrementYear); - const isDataLoading = useStore(state => state.isDataLoading); + const isDataLoading = useStore(state => state.isDataLoading()); return ( state.date); const setYear = useStore(state => state.setYear); - const isDataLoading = useStore(state => state.isDataLoading); + const isDataLoading = useStore(state => state.isDataLoading()); return ( Date: Mon, 20 Nov 2023 21:40:50 -0800 Subject: [PATCH 16/20] Fix minor omissions and errors in state store --- src/state-store.js | 47 ++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/state-store.js b/src/state-store.js index e09065e..28c5768 100644 --- a/src/state-store.js +++ b/src/state-store.js @@ -1,12 +1,17 @@ // This is the Zustand state store for the app. +// +// Comments: +// - The store knows a lot here, such as the structure of config and the names +// and usages of the data services. OK? Too much dependency? + // Note: We use package `moment` for date arithmetic. It is excellent but it -// *mutates* its objects. We are using functional components, -// which require values whose identity changes when their value is changed, -// i.e., a new object. Therefore, every change to a `moment` date object should -// be preceded by `.clone()`; for example, `y = x.clone().subtract(1, 'month')` -// yields a new moment object with a value 1 month before the `x` object. `x` is -// unchanged by this operation; `y` is a different object than `x`. +// *mutates* its objects. We use functional components, which require values +// whose identity changes when their value is changed, i.e., a new object. +// Therefore, every change to a `moment` date object should be preceded by +// `.clone()`; for example, `y = x.clone().subtract(1, 'month')` yields a new +// moment object with a value 1 month before the `x` object. `x` is unchanged by +// this operation; `y` is a different object than `x`. import { create } from 'zustand'; @@ -22,10 +27,6 @@ import filter from 'lodash/fp/filter'; import isUndefined from 'lodash/fp/isUndefined'; -// Comments: -// - The store knows a lot here, such as the structure of config and the names -// and usages of the data services. OK? Too much dependency? -// // Likely latest possible date of available data = current date - 15 d. // This allows for cron jobs that run in first half of month. @@ -51,14 +52,14 @@ export const useStore = create((set, get) => ({ // Important: Wrap in useEffect // Load configuration // This should not be called except by initialize. - _loadConfig: ({ defaultConfig= {}, requiredConfigKeys = []}) => { + _loadConfig: ({ dfault = {}, requiredKeys = [] }) => { return axios.get(`${process.env.PUBLIC_URL}/config.yaml`) .then(response => { // Extend default config with values loaded from config.yaml let config; try { const customConfig = yaml.load(response.data); - config = { ...defaultConfig, ...customConfig }; + config = { ...dfault, ...customConfig }; } catch (error) { set({ configError: ( @@ -70,7 +71,7 @@ export const useStore = create((set, get) => ({ // Check for required config keys (we don't check value types, yet) const missingRequiredKeys = filter( - key => isUndefined(config[key]), requiredConfigKeys + key => isUndefined(config[key]), requiredKeys ); if (missingRequiredKeys.length > 0) { const configErrorMsg = ( @@ -100,12 +101,13 @@ export const useStore = create((set, get) => ({ }); }, + isConfigLoaded: () => get().config !== null, + // Important: Wrap in useEffect - // Initialize state from config and other async data sources. - // TODO: Add params and pass thru to _loadConfig - initialize: () => { + // Load config and initialize state. + initialize: ({ configOpts = { dfault: {}, requiredKeys: [] } }) => { // TODO: This can probably be done more nicely with async/await. - get()._loadConfig().then(() => { + get()._loadConfig(configOpts).then(() => { // TODO: return config from _loadConfig as well as setting // state.config. Would be neater. const config = get().config; @@ -122,6 +124,11 @@ export const useStore = create((set, get) => ({ }); }, + hasValidState: () => + get().variable !== null + && get().dataset !== null + && get().date !== null, + // Important: Wrap in useEffect // Set the variable and load the data associated with it. setVariable: (variable) => { @@ -129,6 +136,10 @@ export const useStore = create((set, get) => ({ get().getData(); }, + setDataset: dataset => { + set({ dataset }); + }, + // Important: Wrap in useEffect // Set the date and load the data associated with it. setDate: (date) => { @@ -153,7 +164,7 @@ export const useStore = create((set, get) => ({ isDataLoading: () => get().baseline === null || get().monthly === null, - isBaselineDataset: () => dataset === 'baseline', + isBaselineDataset: () => get().dataset === 'baseline', setYear: year => { get().setDate(get().date.clone().year(year)); From 427a878853e47d388e4011d2fb257bfad6269133 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 21:41:14 -0800 Subject: [PATCH 17/20] Clean up DataDisplay --- src/components/map/DataDisplay/DataDisplay.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/map/DataDisplay/DataDisplay.js b/src/components/map/DataDisplay/DataDisplay.js index 2f74e49..26019aa 100644 --- a/src/components/map/DataDisplay/DataDisplay.js +++ b/src/components/map/DataDisplay/DataDisplay.js @@ -11,8 +11,9 @@ export default function DataDisplay() { const dataset = useStore(state => state.dataset); const baseline = useStore(state => state.baseline); const monthly = useStore(state => state.monthly); + const hasValidState = useStore(state => state.hasValidState()); - return ( + return hasValidState && ( <> From a7f22c72d4a2d297c784023dc068e1feb97f2a01 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 21:41:47 -0800 Subject: [PATCH 18/20] Convert App and Tool to state store --- src/components/main/App/App.js | 44 +++++++++++++------------------- src/components/main/Tool/Tool.js | 4 +-- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/components/main/App/App.js b/src/components/main/App/App.js index ce62ca2..3983f91 100644 --- a/src/components/main/App/App.js +++ b/src/components/main/App/App.js @@ -12,42 +12,34 @@ logger.configure({ active: true }); function App() { const initialize = useStore(state => state.initialize); - const config = useStore(state => state.config); - const configErrorMessage = useStore(state => state.configError); + const isConfigLoaded = useStore(state => state.isConfigLoaded()); + const configError = useStore(state => state.configError); - // Initialize must be invoked before any items dependent on context. + // Initialize must be invoked before any items dependent on config. + // Done once, at startup. useEffect(() => { initialize({ - defaultConfig: { - // TODO - }, - requiredConfigKeys: [ - // TODO - ], - }); - }); - - // Now in state store - // must be invoked before any other items dependent on context. - // const [config, configErrorMessage] = useFetchConfigContext({ - // defaultConfig: { - // // TODO - // }, - // requiredConfigKeys: [ - // // TODO - // ], - // }); - - if (configErrorMessage !== null) { + configOpts: { + dfault: { + // TODO + }, + requiredKeys: [ + // TODO + ], + } + }); + }, []); + + if (configError !== null) { // TODO: Improve presentation return (
-
{configErrorMessage}
+
{configError}
) } - if (config === null) { + if (!isConfigLoaded) { // TODO: Replace with spinner return
Loading configuration...
} diff --git a/src/components/main/Tool/Tool.js b/src/components/main/Tool/Tool.js index 2947162..13b179c 100644 --- a/src/components/main/Tool/Tool.js +++ b/src/components/main/Tool/Tool.js @@ -16,7 +16,7 @@ import './Tool.css'; export default function Tool() { - const isBaselineDataset = useStore(state => state.isBaselineDataset); + const isBaselineDataset = useStore(state => state.isBaselineDataset()); // Subsumed in state store, not used in this component // const [variable, setVariable] = useState(config.ui.variableSelector.initial); @@ -103,7 +103,7 @@ export default function Tool() {
- {!isBaselineDataset() && + {!isBaselineDataset && From 731e7bbe99452e5a4e9ba15d7b59a4a719c7680b Mon Sep 17 00:00:00 2001 From: rod-glover Date: Mon, 20 Nov 2023 21:53:01 -0800 Subject: [PATCH 19/20] Remove cruft --- .../DatasetSelector/DatasetSelector.js | 1 - .../ThrottledInputRange.js | 2 + .../VariableSelector/VariableSelector.js | 1 - src/components/main/Tool/Tool.js | 61 +------------------ 4 files changed, 4 insertions(+), 61 deletions(-) diff --git a/src/components/controls/DatasetSelector/DatasetSelector.js b/src/components/controls/DatasetSelector/DatasetSelector.js index 6a6f287..61b6a82 100644 --- a/src/components/controls/DatasetSelector/DatasetSelector.js +++ b/src/components/controls/DatasetSelector/DatasetSelector.js @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import flow from 'lodash/fp/flow'; import keys from 'lodash/fp/keys'; import map from 'lodash/fp/map'; diff --git a/src/components/controls/ThrottledInputRange/ThrottledInputRange.js b/src/components/controls/ThrottledInputRange/ThrottledInputRange.js index a679840..19be72d 100644 --- a/src/components/controls/ThrottledInputRange/ThrottledInputRange.js +++ b/src/components/controls/ThrottledInputRange/ThrottledInputRange.js @@ -7,6 +7,8 @@ import InputRange from 'react-input-range'; function ThrottledInputRange({ value, onChange, ...rest }) { const [intermediateValue, setIntermediateValue] = useState(value); + // TODO: This is probably better as useMemo. + // Update intermediate value whenever external value changes. useEffect(() => { setIntermediateValue(value); }, [value]); diff --git a/src/components/controls/VariableSelector/VariableSelector.js b/src/components/controls/VariableSelector/VariableSelector.js index 20a6e31..3105446 100644 --- a/src/components/controls/VariableSelector/VariableSelector.js +++ b/src/components/controls/VariableSelector/VariableSelector.js @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Tooltip } from 'react-bootstrap'; import keys from 'lodash/fp/keys'; diff --git a/src/components/main/Tool/Tool.js b/src/components/main/Tool/Tool.js index 13b179c..ea51eba 100644 --- a/src/components/main/Tool/Tool.js +++ b/src/components/main/Tool/Tool.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Col, Row } from 'react-bootstrap'; import DatasetSelector from '../../controls/DatasetSelector' @@ -18,64 +18,7 @@ import './Tool.css'; export default function Tool() { const isBaselineDataset = useStore(state => state.isBaselineDataset()); - // Subsumed in state store, not used in this component - // const [variable, setVariable] = useState(config.ui.variableSelector.initial); - // const [dataset, setDataset] = useState(config.ui.datasetSelector.initial); - // const [date, setDate] = useState(latestPossibleDataDate); - // const [baseline, setBaseline] = useState(null); - // const [monthly, setMonthly] = useState(null); - - // Part of initialize - // // Determine latest date with data, and set date to it. This happens once, - // // on first render. - // useEffect(() => { - // setBaseline(null); - // setMonthly(null); - // getLastDateWithDataBefore(variable, date, wadsUrl) - // .then(date => { - // setDate(date); - // }); - // }, []); - - // Folded into store actions - // // When variable or date changes, get data. - // // (Both datasets are is retrieved for all values of `dataset`. This could - // // be refined to get only the dataset(s) required by the value of `dataset`.) - // // Consider splitting this into two separate effects, with an if on `dataset`. - // useEffect(() => { - // setBaseline(null); - // setMonthly(null); - // getBaselineData(variable, date, wadsUrl) - // .then(r => { - // setBaseline(r.data); - // }); - // getMonthlyData(variable, date, wadsUrl) - // .then(r => { - // setMonthly(r.data); - // }); - // }, [variable, date]); - - // Handlers moved to state store actions - // const handleChangeMonth = (month) => { - // setDate((date) => date.clone().month(month)); - // }; - // - // const handleChangeYear = (year) => { - // setDate((date) => date.clone().year(year)); - // }; - // - // const handleIncrementYear = (by) => { - // setDate((date) => date.clone().add(by, 'year')); - // }; - // - // const handleIncrementMonth = (by) => { - // setDate((date) => date.clone().add(by, 'month')); - // }; - - // Computed values moved to state store actions - // const isDataLoading = baseline === null || monthly === null; - // const isBaselineDataset = dataset === 'baseline'; - + // TODO: Move into config? const displayColWidths = { xs: 12, md: "auto" }; const rowSpacing = "mt-3" From 7ec11d7d475016a73eb7f7f93dd4ad161d4e27e5 Mon Sep 17 00:00:00 2001 From: rod-glover Date: Tue, 21 Nov 2023 09:07:30 -0800 Subject: [PATCH 20/20] Remove config context provider --- src/components/main/ConfigContext.js | 88 ---------------------------- 1 file changed, 88 deletions(-) delete mode 100644 src/components/main/ConfigContext.js diff --git a/src/components/main/ConfigContext.js b/src/components/main/ConfigContext.js deleted file mode 100644 index 0fdee80..0000000 --- a/src/components/main/ConfigContext.js +++ /dev/null @@ -1,88 +0,0 @@ -// This module provides a React context for app configuration values, -// a hook for fetching its value from a YAML file in the public folder , -// and a hook for using the context value in a functional component. -import React, { useContext, useEffect, useState } from 'react'; -import axios from 'axios'; -import yaml from "js-yaml"; -import filter from "lodash/fp/filter"; -import isUndefined from "lodash/fp/isUndefined"; - - -// The context -const ConfigContext = React.createContext({}); -export default ConfigContext; - - -// Custom hook for fetching configuration from public/config.yaml. -// This hook returns two state variables, `[config, errorMessage]`. -// -// If the configuration file is successfully fetched, parsed as YAML, and -// the resulting configuration object contains all required keys, the `config` -// state variable is set to that value and `errorMessage` remains null. -// -// If not (i.e., there are errors), `config` remains null and `errorMessage` -// is set to an appropriate message (which may be a string or a React object). -// -// This hook should be used only by the top-level component, and the returned -// `config` used to set the value provided by `ConfigContext.Provider`. (For -// specific usage, see component `App`.) All other components should access -// app configuration using the hook `useConfigContext`. -export function useFetchConfigContext({ - defaultConfig = {}, - requiredConfigKeys = [], -}) { - const [config, setConfig] = useState(null); - const [errorMsg, setErrorMsg] = useState(null); - - useEffect(() => { - axios.get(`${process.env.PUBLIC_URL}/config.yaml`) - .then(response => { - // Extend default config with values loaded from config.yaml - let cfg; - try { - const customConfig = yaml.load(response.data); - cfg = { ...defaultConfig, ...customConfig }; - } catch (e) { - setErrorMsg( -
Error loading config.yaml:
{e.toString()}
- ); - return; - } - - // Check for required config keys (we don't check value types, yet) - const missingRequiredKeys = filter( - key => isUndefined(cfg[key]), requiredConfigKeys - ); - if (missingRequiredKeys.length > 0) { - setErrorMsg( - `Error in config.yaml: The following keys must have values, - but do not: ${missingRequiredKeys}` - ); - return; - } - - // TODO: Is there really any value in this? - // Alternatively, just transfer everything in process.env here. - // None of that makes much sense ... - // Extend config with some env var values - cfg.appVersion = process.env.REACT_APP_APP_VERSION ?? "unknown"; - - // Update the config state. - setConfig(cfg); - }) - .catch(error => { - setErrorMsg( -
Error fetching configuration:
{error.toString()}
- ); - }); - }, []); - - return [config, errorMsg]; -} - - -// Custom hook to make it slightly simpler to access the config context from a -// client component. -export function useConfigContext() { - return useContext(ConfigContext); -}