diff --git a/.env.development b/.env.development index a8098322..51204f2c 100644 --- a/.env.development +++ b/.env.development @@ -28,6 +28,7 @@ REACT_APP_NEON_ROUTER_BASE_HOME="/core-components" REACT_APP_NEON_AUTH_DISABLE_WS="true" REACT_APP_NEON_USE_GRAPHQL="true" REACT_APP_NEON_SHOW_AOP_VIEWER="true" +REACT_APP_NEON_ENABLE_GLOBAL_SIGNIN_STATE="false" #------------------------------------------------------------------------------- # Third party APIs and options diff --git a/.env.production b/.env.production index c85fef3c..4d1dce07 100644 --- a/.env.production +++ b/.env.production @@ -24,6 +24,7 @@ REACT_APP_NEON_ROUTER_BASE_HOME="/core-components" REACT_APP_NEON_USE_GRAPHQL="true" REACT_APP_NEON_SHOW_AOP_VIEWER="true" +REACT_APP_NEON_ENABLE_GLOBAL_SIGNIN_STATE="false" #------------------------------------------------------------------------------- # Third party APIs and options diff --git a/.gitignore b/.gitignore index cf074c60..646c368f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.vscode diff --git a/lib/components/DataProductAvailability/AvailabilityContext.d.ts b/lib/components/DataProductAvailability/AvailabilityContext.d.ts index 60f50f8b..6f5ad600 100644 --- a/lib/components/DataProductAvailability/AvailabilityContext.d.ts +++ b/lib/components/DataProductAvailability/AvailabilityContext.d.ts @@ -38,6 +38,7 @@ declare namespace AvailabilityContext { declare function Provider(props: any): JSX.Element; declare namespace Provider { namespace propTypes { + const dataAvailabilityUniqueId: PropTypes.Requireable; const sites: PropTypes.Requireable<(PropTypes.InferProps<{ siteCode: PropTypes.Validator; tables: PropTypes.Validator<(PropTypes.InferProps<{ @@ -52,11 +53,13 @@ declare namespace Provider { const children: PropTypes.Validator; } namespace defaultProps { + const dataAvailabilityUniqueId_1: number; + export { dataAvailabilityUniqueId_1 as dataAvailabilityUniqueId }; const sites_1: never[]; export { sites_1 as sites }; } } -declare function useAvailabilityState(): any[] | { +declare function useAvailabilityState(): { sites: never[]; tables: {}; rows: {}; @@ -71,7 +74,22 @@ declare function useAvailabilityState(): any[] | { states: {}; domains: {}; }; -}; +} | ({ + sites: never[]; + tables: {}; + rows: {}; + rowTitles: {}; + rowLabels: never[]; + breakouts: never[]; + validBreakouts: string[]; + sortDirection: string; + neonContextHydrated: boolean; + reference: { + sites: {}; + states: {}; + domains: {}; + }; +} | (() => void))[]; declare namespace SORT_DIRECTIONS { const ASC: string; const DESC: string; diff --git a/lib/components/DataProductAvailability/AvailabilityContext.js b/lib/components/DataProductAvailability/AvailabilityContext.js index 9d7b2aa4..8765b080 100644 --- a/lib/components/DataProductAvailability/AvailabilityContext.js +++ b/lib/components/DataProductAvailability/AvailabilityContext.js @@ -15,8 +15,14 @@ var _cloneDeep = _interopRequireDefault(require("lodash/cloneDeep")); var _NeonContext = _interopRequireDefault(require("../NeonContext/NeonContext")); +var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment/NeonEnvironment")); + var _AvailabilityUtils = require("./AvailabilityUtils"); +var _NeonSignInButtonState = _interopRequireDefault(require("../NeonSignInButton/NeonSignInButtonState")); + +var _StateStorageService = _interopRequireDefault(require("../../service/StateStorageService")); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } @@ -285,13 +291,23 @@ var useAvailabilityState = function useAvailabilityState() { return hookResponse; }; +/** + * Defines a lookup of state key to a boolean + * designating whether or not that instance of the context + * should pull the state from the session storage and restore. + * Keeping this lookup outside of the context provider function + * as to not incur lifecycle interference by storing with useState. + */ + + +var restoreStateLookup = {}; /** Context Provider */ - var Provider = function Provider(props) { var sites = props.sites, + dataAvailabilityUniqueId = props.dataAvailabilityUniqueId, children = props.children; var _NeonContext$useNeonC = _NeonContext.default.useNeonContextState(), @@ -300,30 +316,62 @@ var Provider = function Provider(props) { neonContextData = _NeonContext$useNeonC3.data, neonContextIsFinal = _NeonContext$useNeonC3.isFinal, neonContextHasError = _NeonContext$useNeonC3.hasError; + + var key = "availabilityContextState-".concat(dataAvailabilityUniqueId); + + if (typeof restoreStateLookup[key] === 'undefined') { + restoreStateLookup[key] = true; + } + + var shouldRestoreState = restoreStateLookup[key]; + var stateStorage = (0, _StateStorageService.default)(key); + var savedState = stateStorage.readState(); /** Initial State and Reducer Setup */ - var initialState = _extends({}, (0, _cloneDeep.default)(DEFAULT_STATE), { sites: sites }); initialState.tables = extractTables(initialState); - if (neonContextIsFinal && !neonContextHasError) { + if (neonContextIsFinal && !neonContextHasError && !savedState) { initialState = hydrateNeonContextData(initialState, neonContextData); } + if (savedState && shouldRestoreState) { + restoreStateLookup[key] = false; + stateStorage.removeState(); + initialState = savedState; + } + var _useReducer = (0, _react.useReducer)(reducer, calculateRows(initialState)), _useReducer2 = _slicedToArray(_useReducer, 2), state = _useReducer2[0], - dispatch = _useReducer2[1]; + dispatch = _useReducer2[1]; // The current sign in process uses a separate domain. This function + // persists the current state in storage when the button is clicked + // so the state may be reloaded when the page is reloaded after sign + // in. + + + (0, _react.useEffect)(function () { + var subscription = _NeonSignInButtonState.default.getObservable().subscribe({ + next: function next() { + if (!_NeonEnvironment.default.enableGlobalSignInState) return; + restoreStateLookup[key] = false; + stateStorage.saveState(state); + } + }); + + return function () { + subscription.unsubscribe(); + }; + }, [state, stateStorage, key]); /** Effect - Watch for changes to NeonContext data and push into local state */ - (0, _react.useEffect)(function () { if (!state.neonContextHydrated && neonContextIsFinal && !neonContextHasError) { dispatch({ @@ -342,10 +390,12 @@ var Provider = function Provider(props) { }; Provider.propTypes = { + dataAvailabilityUniqueId: _propTypes.default.number, sites: _AvailabilityUtils.AvailabilityPropTypes.enhancedSites, children: _propTypes.default.oneOfType([_propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.string])), _propTypes.default.node, _propTypes.default.string]).isRequired }; Provider.defaultProps = { + dataAvailabilityUniqueId: 0, sites: [] }; /** diff --git a/lib/components/DataProductAvailability/StateStorageConverter.d.ts b/lib/components/DataProductAvailability/StateStorageConverter.d.ts new file mode 100644 index 00000000..7540241e --- /dev/null +++ b/lib/components/DataProductAvailability/StateStorageConverter.d.ts @@ -0,0 +1,11 @@ +/** + * Alter the current state for valid JSON serialization. + * @param currentState The current state + */ +declare const convertStateForStorage: (state: any) => any; +/** + * Restore the state from JSON serialization. + * @param storedState The state read from storage. + */ +declare const convertStateFromStorage: (state: any) => any; +export { convertStateForStorage, convertStateFromStorage }; diff --git a/lib/components/DataProductAvailability/StateStorageConverter.js b/lib/components/DataProductAvailability/StateStorageConverter.js new file mode 100644 index 00000000..2fedde55 --- /dev/null +++ b/lib/components/DataProductAvailability/StateStorageConverter.js @@ -0,0 +1,125 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.convertStateFromStorage = exports.convertStateForStorage = void 0; + +var _cloneDeep = _interopRequireDefault(require("lodash/cloneDeep")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Alter the current state for valid JSON serialization. + * @param currentState The current state + */ +var convertStateForStorage = function convertStateForStorage(state) { + var newState = (0, _cloneDeep.default)(state); // variables + // const { variables: stateVariables } = state; + // Object.keys(stateVariables).forEach((variableKey, index) => { + // const { sites, tables, timeSteps } = stateVariables[variableKey]; + // if (sites instanceof Set && sites.size > 0) { + // newState.variables[variableKey].sites = Array.from(sites); + // } else { + // newState.variables[variableKey].sites = []; + // } + // if (tables instanceof Set && sites.size > 0) { + // newState.variables[variableKey].tables = Array.from(tables); + // } else { + // newState.variables[variableKey].tables = []; + // } + // if (timeSteps instanceof Set && sites.size > 0) { + // newState.variables[variableKey].timeSteps = Array.from(timeSteps); + // } else { + // newState.variables[variableKey].timeSteps = []; + // } + // }); + // // product site variables + // const { sites: productSites } = state.product; + // Object.keys(productSites).forEach((siteKey, index) => { + // const { variables: siteVariables } = productSites[siteKey]; + // if (siteVariables instanceof Set && siteVariables.size > 0) { + // newState.product.sites[siteKey].variables = Array.from(siteVariables); + // } else { + // newState.product.sites[siteKey].variables = []; + // } + // }); + // // available quality flags + // const { availableQualityFlags } = state; + // if (availableQualityFlags instanceof Set) { + // newState.availableQualityFlags = Array.from(availableQualityFlags); + // } else { + // newState.availableQualityFlags = []; + // } + // // available time steps + // const { availableTimeSteps } = state; + // if (availableTimeSteps instanceof Set) { + // newState.availableTimeSteps = Array.from(availableTimeSteps); + // } else { + // newState.availableTimeSteps = []; + // } + + return newState; +}; +/** + * Restore the state from JSON serialization. + * @param storedState The state read from storage. + */ + + +exports.convertStateForStorage = convertStateForStorage; + +var convertStateFromStorage = function convertStateFromStorage(state) { + var newState = (0, _cloneDeep.default)(state); // // graphData data + // const data = state.graphData.data.map((entry: any) => [new Date(entry[0]), entry[1]]); + // newState.graphData.data = data; + // // state variables + // const { variables } = state; + // Object.keys(variables).forEach((key, index) => { + // const { sites, tables, timeSteps } = variables[key]; + // if (Array.isArray(sites)) { + // newState.variables[key].sites = new Set(sites); + // } else { + // newState.variables[key].sites = new Set(); + // } + // if (Array.isArray(tables)) { + // newState.variables[key].tables = new Set(tables); + // } else { + // newState.variables[key].tables = new Set(); + // } + // if (Array.isArray(timeSteps)) { + // newState.variables[key].timeSteps = new Set(timeSteps); + // } else { + // newState.variables[key].timeSteps = new Set(); + // } + // }); + // // product site variables + // const { sites: productSites } = state.product; + // // get the variables for each site + // Object.keys(productSites).forEach((siteKey, index) => { + // const { variables: siteVariables } = productSites[siteKey]; + // if (Array.isArray(siteVariables) && siteVariables.length > 0) { + // newState.product.sites[siteKey].variables = new Set(siteVariables); + // } else { + // newState.product.sites[siteKey].variables = new Set(); + // } + // }); + // // available quality flags + // const { availableQualityFlags } = state; + // if (Array.isArray(availableQualityFlags)) { + // newState.availableQualityFlags = new Set(availableQualityFlags); + // } else { + // newState.availableQualityFlags = new Set(); + // } + // // available quality flags + // const { availableTimeSteps } = state; + // if (Array.isArray(availableTimeSteps)) { + // newState.availableTimeSteps = new Set(availableTimeSteps); + // } else { + // newState.availableTimeSteps = new Set(); + // } + + return newState; +}; + +exports.convertStateFromStorage = convertStateFromStorage; \ No newline at end of file diff --git a/lib/components/DocumentViewer/DocumentViewer.d.ts b/lib/components/DocumentViewer/DocumentViewer.d.ts new file mode 100644 index 00000000..707c8d96 --- /dev/null +++ b/lib/components/DocumentViewer/DocumentViewer.d.ts @@ -0,0 +1,8 @@ +import React from 'react'; +import { NeonDocument } from '../../types/neon'; +export interface DocumentViewerProps { + document: NeonDocument; + width: number; +} +declare const DocumentViewer: React.FC; +export default DocumentViewer; diff --git a/lib/components/DocumentViewer/DocumentViewer.js b/lib/components/DocumentViewer/DocumentViewer.js new file mode 100644 index 00000000..f7aeaee4 --- /dev/null +++ b/lib/components/DocumentViewer/DocumentViewer.js @@ -0,0 +1,150 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _react = _interopRequireWildcard(require("react")); + +var _styles = require("@material-ui/core/styles"); + +var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment")); + +var _Theme = _interopRequireDefault(require("../Theme/Theme")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +var useStyles = (0, _styles.makeStyles)(function (muiTheme) { + return (// eslint-disable-next-line implicit-arrow-linebreak + (0, _styles.createStyles)({ + container: { + width: '100%', + margin: muiTheme.spacing(3, 3, 3, 3) + } + }) + ); +}); + +var noop = function noop() {}; + +var breakpoints = [0, 675, 900, 1200]; +var ratios = ['8:11', '3:4', '3:4', '3:4']; + +var calcAutoHeight = function calcAutoHeight(width) { + var breakIdx = breakpoints.reduce(function (acc, breakpoint, idx) { + return width >= breakpoint ? idx : acc; + }, 0); + var ratio = /^([\d.]+):([\d.]+)$/.exec(ratios[breakIdx]); + var mult = 4 / 3; + + if (ratio) { + mult = (parseFloat(ratio[2]) || 1) / (parseFloat(ratio[1]) || 1); + } + + return Math.floor(width * mult); +}; + +var DocumentViewer = function DocumentViewer(props) { + var classes = useStyles(_Theme.default); + var document = props.document, + width = props.width; + var containerRef = (0, _react.useRef)(); + var embedRef = (0, _react.useRef)(); + + var _useState = (0, _react.useState)(width), + _useState2 = _slicedToArray(_useState, 2), + viewerWidth = _useState2[0], + setViewerWidth = _useState2[1]; + + var handleResizeCb = (0, _react.useCallback)(function () { + var container = containerRef.current; + var embed = embedRef.current; // Do nothing if either container or viz references fail ot point to a DOM node + + if (!container || !embed) { + return; + } // Do nothing if either refs have no offset parent + // (meaning they're hidden from rendering anyway) + + + if (container.offsetParent === null || embed.offsetParent === null) { + return; + } // Do nothing if container and viz have the same width + // (resize event fired but no actual resize necessary) + + + if (container.clientWidth === viewerWidth) { + return; + } + + var newWidth = container.clientWidth; + setViewerWidth(newWidth); + embed.setAttribute('width', "".concat(newWidth)); + embed.setAttribute('height', "".concat(calcAutoHeight(newWidth))); + }, [containerRef, embedRef, viewerWidth, setViewerWidth]); + (0, _react.useLayoutEffect)(function () { + var element = embedRef.current; + + if (!element) { + return noop; + } + + var parent = element.parentElement; + + if (!parent) { + return noop; + } + + handleResizeCb(); + + if (typeof ResizeObserver !== 'function') { + window.addEventListener('resize', handleResizeCb); + return function () { + window.removeEventListener('resize', handleResizeCb); + }; + } + + var resizeObserver = new ResizeObserver(handleResizeCb); + resizeObserver.observe(parent); + return function () { + if (!resizeObserver) { + return; + } + + resizeObserver.disconnect(); + resizeObserver = null; + }; + }, [embedRef, handleResizeCb]); + return /*#__PURE__*/_react.default.createElement("div", { + ref: containerRef, + className: classes.container + }, /*#__PURE__*/_react.default.createElement("embed", { + ref: embedRef, + type: document.type, + src: "".concat(_NeonEnvironment.default.getFullApiPath('documents'), "/").concat(document.name, "?inline=true"), + title: document.description, + width: viewerWidth, + height: calcAutoHeight(viewerWidth) + })); +}; + +var _default = DocumentViewer; +exports.default = _default; \ No newline at end of file diff --git a/lib/components/DocumentViewer/index.d.ts b/lib/components/DocumentViewer/index.d.ts new file mode 100644 index 00000000..774e25e7 --- /dev/null +++ b/lib/components/DocumentViewer/index.d.ts @@ -0,0 +1 @@ +export { default } from "./DocumentViewer"; diff --git a/lib/components/DocumentViewer/index.js b/lib/components/DocumentViewer/index.js new file mode 100644 index 00000000..5617694a --- /dev/null +++ b/lib/components/DocumentViewer/index.js @@ -0,0 +1,15 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "default", { + enumerable: true, + get: function get() { + return _DocumentViewer.default; + } +}); + +var _DocumentViewer = _interopRequireDefault(require("./DocumentViewer")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } \ No newline at end of file diff --git a/lib/components/DocumentViewer/package.json b/lib/components/DocumentViewer/package.json new file mode 100644 index 00000000..e6d1de5a --- /dev/null +++ b/lib/components/DocumentViewer/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "name": "document-viewer", + "main": "./DocumentViewer.tsx", + "module": "./DocumentViewer.tsx" +} diff --git a/lib/components/DownloadDataContext/DownloadDataContext.d.ts b/lib/components/DownloadDataContext/DownloadDataContext.d.ts index e548e15f..d291607b 100644 --- a/lib/components/DownloadDataContext/DownloadDataContext.d.ts +++ b/lib/components/DownloadDataContext/DownloadDataContext.d.ts @@ -115,12 +115,10 @@ declare namespace DownloadDataContext { export { ALL_STEPS }; export { getStateObservable }; } -/** - -*/ declare function Provider(props: any): JSX.Element; declare namespace Provider { namespace propTypes { + const downloadDataContextUniqueId: PropTypes.Requireable; const stateObservable: PropTypes.Requireable<(...args: any[]) => any>; const productData: PropTypes.Requireable; @@ -139,6 +137,8 @@ declare namespace Provider { const children: PropTypes.Validator; } namespace defaultProps { + const downloadDataContextUniqueId_1: number; + export { downloadDataContextUniqueId_1 as downloadDataContextUniqueId }; const stateObservable_1: null; export { stateObservable_1 as stateObservable }; const productData_1: {}; @@ -157,9 +157,6 @@ declare namespace Provider { export { packageType_1 as packageType }; } } -/** - HOOK -*/ declare function useDownloadDataState(): { downloadContextIsActive: boolean; broadcast: boolean; @@ -311,9 +308,6 @@ declare function useDownloadDataState(): { isValid: boolean; }; })[]; -/** - REDUCER -*/ declare function reducer(state: any, action: any): any; declare namespace DEFAULT_STATE { export const downloadContextIsActive: boolean; diff --git a/lib/components/DownloadDataContext/DownloadDataContext.js b/lib/components/DownloadDataContext/DownloadDataContext.js index a535e7f4..8903de4c 100644 --- a/lib/components/DownloadDataContext/DownloadDataContext.js +++ b/lib/components/DownloadDataContext/DownloadDataContext.js @@ -23,6 +23,12 @@ var _manifestUtil = require("../../util/manifestUtil"); var _rxUtil = require("../../util/rxUtil"); +var _StateStorageService = _interopRequireDefault(require("../../service/StateStorageService")); + +var _NeonSignInButtonState = _interopRequireDefault(require("../NeonSignInButton/NeonSignInButtonState")); + +var _StateStorageConverter = require("./StateStorageConverter"); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } @@ -216,10 +222,7 @@ var S3_PATTERN = { regex: /^.*\/(\w+)\/\w+_(\d+)\/(\w+)\/\w+\/\w+(?:\/\w+)*\/[\w\\(\\)._-]+\.\w+$/, groups: ['domain', 'visit', 'level'] } -}; -/** - VALIDATOR FUNCTIONS -*/ +}; // VALIDATOR FUNCTIONS // Naive check, replace with a more robust JSON schema check var productDataIsValid = function productDataIsValid(productData) { @@ -342,12 +345,9 @@ var mutateNewStateIntoRange = function mutateNewStateIntoRange(key, value) { default: return valueIsAllowable ? value : DEFAULT_STATE[key].value; } -}; -/** - Estimate a POST body size from a sile list and sites list for s3Files-based - downloads. Numbers here are based on the current POST API and what it requires - for form data keys, which is excessively verbose. -*/ +}; // Estimate a POST body size from a sile list and sites list for s3Files-based +// downloads. Numbers here are based on the current POST API and what it requires +// for form data keys, which is excessively verbose. var estimatePostSize = function estimatePostSize(s3FilesState, sitesState) { @@ -357,10 +357,7 @@ var estimatePostSize = function estimatePostSize(s3FilesState, sitesState) { return a + encodeURIComponent(b).length + 58; }, 0); return baseLength + sitesLength + filesLength; -}; -/** - GETTER FUNCTIONS -*/ +}; // GETTER FUNCTIONS var getValidValuesFromProductData = function getValidValuesFromProductData(productData, key) { @@ -823,10 +820,7 @@ var getAndValidateNewState = function getAndValidateNewState(previousState, acti } return newState; -}; -/** - REDUCER -*/ +}; // REDUCER var reducer = function reducer(state, action) { @@ -1045,21 +1039,20 @@ var reducer = function reducer(state, action) { return state; } }; +/** + * Wrapped reducer function + * @param {*} state The state. + * @param {*} action An action. + * @returns the new state. + */ -var wrappedReducer = function wrappedReducer(state, action) { - var newState = reducer(state, action); // console.log('ACTION', action, newState); - return newState; -}; -/** - CONTEXT -*/ +var wrappedReducer = function wrappedReducer(state, action) { + return reducer(state, action); +}; // CONTEXT -var Context = /*#__PURE__*/(0, _react.createContext)(DEFAULT_STATE); -/** - HOOK -*/ +var Context = /*#__PURE__*/(0, _react.createContext)(DEFAULT_STATE); // HOOK var useDownloadDataState = function useDownloadDataState() { var hookResponse = (0, _react.useContext)(Context); @@ -1072,10 +1065,7 @@ var useDownloadDataState = function useDownloadDataState() { } return hookResponse; -}; -/** - OBSERVABLES -*/ +}; // OBSERVABLES // Observable and getter for sharing whole state through a higher order component @@ -1092,23 +1082,66 @@ var getManifestAjaxObservable = function getManifestAjaxObservable(request) { return _NeonApi.default.postJsonObservable(request.url, request.body, null, false); }; /** - -*/ + * Defines a lookup of state key to a boolean + * designating whether or not that instance of the context + * should pull the state from the session storage and restore. + * Keeping this lookup outside of the context provider function + * as to not incur lifecycle interference by storing with useState. + */ + +var restoreStateLookup = {}; // Provider var Provider = function Provider(props) { - var stateObservable = props.stateObservable, - children = props.children; + var downloadDataContextUniqueId = props.downloadDataContextUniqueId, + stateObservable = props.stateObservable, + children = props.children; // get the initial state from storage if present, else get from props. + var initialState = getInitialStateFromProps(props); + var product = initialState.productData.productCode; + var stateKey = "downloadDataContextState-".concat(product, "-").concat(downloadDataContextUniqueId); + + if (typeof restoreStateLookup[stateKey] === 'undefined') { + restoreStateLookup[stateKey] = true; + } + + var shouldRestoreState = restoreStateLookup[stateKey]; + var stateStorage = (0, _StateStorageService.default)(stateKey); + var savedState = stateStorage.readState(); + + if (savedState && shouldRestoreState) { + restoreStateLookup[stateKey] = false; + stateStorage.removeState(); + initialState = (0, _StateStorageConverter.convertAOPInitialState)(savedState, initialState); + } var _useReducer = (0, _react.useReducer)(wrappedReducer, initialState), _useReducer2 = _slicedToArray(_useReducer, 2), state = _useReducer2[0], - dispatch = _useReducer2[1]; // Create an observable for manifests requests and subscribe to it to execute + dispatch = _useReducer2[1]; + + var downloadContextIsActive = state.downloadContextIsActive, + dialogOpen = state.dialogOpen; // The current sign in process uses a separate domain. This function + // persists the current state in storage when the button is clicked + // so the state may be reloaded when the page is reloaded after sign + // in. + + (0, _react.useEffect)(function () { + var subscription = _NeonSignInButtonState.default.getObservable().subscribe({ + next: function next() { + if (!downloadContextIsActive || !dialogOpen) return; + restoreStateLookup[stateKey] = false; + stateStorage.saveState((0, _StateStorageConverter.convertStateForStorage)(state)); + } + }); + + return function () { + subscription.unsubscribe(); + }; + }, [downloadContextIsActive, dialogOpen, state, stateKey, stateStorage]); // Create an observable for manifests requests and subscribe to it to execute // the manifest fetch and dispatch results when updated. // eslint-disable-next-line react-hooks/exhaustive-deps - var manifestRequest$ = new _rxjs.Subject(); manifestRequest$.subscribe(function (request) { return getManifestAjaxObservable(request).pipe((0, _operators.switchMap)(function (resp) { @@ -1251,6 +1284,7 @@ var Provider = function Provider(props) { }; Provider.propTypes = { + downloadDataContextUniqueId: _propTypes.default.number, stateObservable: _propTypes.default.func, productData: _propTypes.default.shape({ productCode: _propTypes.default.string.isRequired, @@ -1273,6 +1307,7 @@ Provider.propTypes = { children: _propTypes.default.oneOfType([_propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.string])), _propTypes.default.node, _propTypes.default.string]).isRequired }; Provider.defaultProps = { + downloadDataContextUniqueId: 0, stateObservable: null, productData: {}, availabilityView: DEFAULT_STATE.availabilityView, diff --git a/lib/components/DownloadDataContext/StatePersistence.d.ts b/lib/components/DownloadDataContext/StatePersistence.d.ts new file mode 100644 index 00000000..bf1736af --- /dev/null +++ b/lib/components/DownloadDataContext/StatePersistence.d.ts @@ -0,0 +1,2 @@ +export declare const persistState: (state: object) => void; +export declare const readState: () => object | null; diff --git a/lib/components/DownloadDataContext/StatePersistence.js b/lib/components/DownloadDataContext/StatePersistence.js new file mode 100644 index 00000000..696c24bf --- /dev/null +++ b/lib/components/DownloadDataContext/StatePersistence.js @@ -0,0 +1,24 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.readState = exports.persistState = void 0; + +var _StateService = _interopRequireDefault(require("../../service/StateService")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var key = 'downloadDataContextState'; + +var persistState = function persistState(state) { + _StateService.default.setObject(key, state); +}; + +exports.persistState = persistState; + +var readState = function readState() { + return _StateService.default.getObject(key); +}; + +exports.readState = readState; \ No newline at end of file diff --git a/lib/components/DownloadDataContext/StateStorageConverter.d.ts b/lib/components/DownloadDataContext/StateStorageConverter.d.ts new file mode 100644 index 00000000..14522a6d --- /dev/null +++ b/lib/components/DownloadDataContext/StateStorageConverter.d.ts @@ -0,0 +1,8 @@ +/** + * Alter the current state for valid JSON serialization. Set objects + * must be converted to Array objects for serialization. + * @param currentState The current state + */ +declare const convertStateForStorage: (state: any) => any; +declare const convertAOPInitialState: (state: any, propsState: any) => any; +export { convertStateForStorage, convertAOPInitialState }; diff --git a/lib/components/DownloadDataContext/StateStorageConverter.js b/lib/components/DownloadDataContext/StateStorageConverter.js new file mode 100644 index 00000000..2edf6ff2 --- /dev/null +++ b/lib/components/DownloadDataContext/StateStorageConverter.js @@ -0,0 +1,65 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.convertAOPInitialState = exports.convertStateForStorage = void 0; + +var _DownloadDataContext = _interopRequireDefault(require("./DownloadDataContext")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } + +function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } + +function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } + +/** + * Alter the current state for valid JSON serialization. Set objects + * must be converted to Array objects for serialization. + * @param currentState The current state + */ +var convertStateForStorage = function convertStateForStorage(state) { + if (state.fromAOPManifest) { + // AOP S3 Files will incur too much data to be saved in session state + // Restore to default state in terms of s3Files and selection state. + return _extends({}, state, { + s3FileFetches: _extends({}, _DownloadDataContext.default.DEFAULT_STATE.s3FileFetches), + s3FileFetchProgress: _DownloadDataContext.default.DEFAULT_STATE.s3FileFetchProgress, + s3Files: _extends({}, _DownloadDataContext.default.DEFAULT_STATE.s3Files), + manifest: _extends({}, _DownloadDataContext.default.DEFAULT_STATE.manifest), + allStepsComplete: _DownloadDataContext.default.DEFAULT_STATE.allStepsComplete, + sites: _extends({}, state.sites, { + value: _toConsumableArray(_DownloadDataContext.default.DEFAULT_STATE.sites.value) + }) + }); + } + + return state; +}; + +exports.convertStateForStorage = convertStateForStorage; + +var convertAOPInitialState = function convertAOPInitialState(state, propsState) { + if (!state.fromAOPManifest) return state; + return _extends({}, state, { + s3FileFetches: _extends({}, propsState.s3FileFetches), + s3FileFetchProgress: propsState.s3FileFetchProgress, + s3Files: _extends({}, propsState.s3Files), + manifest: _extends({}, propsState.manifest), + allStepsComplete: propsState.allStepsComplete, + policies: _extends({}, propsState.policies) + }); +}; // eslint-disable-next-line import/prefer-default-export + + +exports.convertAOPInitialState = convertAOPInitialState; \ No newline at end of file diff --git a/lib/components/DownloadDataDialog/DownloadDataDialog.js b/lib/components/DownloadDataDialog/DownloadDataDialog.js index c0ca4092..88a60170 100644 --- a/lib/components/DownloadDataDialog/DownloadDataDialog.js +++ b/lib/components/DownloadDataDialog/DownloadDataDialog.js @@ -75,10 +75,10 @@ var _ExternalHostInfo = _interopRequireDefault(require("../ExternalHostInfo/Exte var _NeonContext = _interopRequireDefault(require("../NeonContext/NeonContext")); -var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment/NeonEnvironment")); - var _Theme = _interopRequireWildcard(require("../Theme/Theme")); +var _NeonSignInButton = _interopRequireDefault(require("../NeonSignInButton/NeonSignInButton")); + var _RouteService = _interopRequireDefault(require("../../service/RouteService")); var _manifestUtil = require("../../util/manifestUtil"); @@ -516,16 +516,6 @@ function DownloadDataDialog() { if (isAuthenticated) { return null; } - - var signInLink = /*#__PURE__*/_react.default.createElement(_Link.default, { - target: "_new", - href: _NeonEnvironment.default.getFullAuthPath('login') - }, "signing in"); - - var benefitsLink = /*#__PURE__*/_react.default.createElement(_Link.default, { - target: "_new", - href: _RouteService.default.getUserAccountsPath() - }, "here"); /* eslint-disable react/jsx-one-expression-per-line */ @@ -540,13 +530,16 @@ function DownloadDataDialog() { marginTop: _Theme.default.spacing(1), fontWeight: 600 }) - }, "Have an account? Consider ", signInLink, " before proceeding."), /*#__PURE__*/_react.default.createElement(_Typography.default, { + }, "Consider signing in or creating an account before proceeding."), /*#__PURE__*/_react.default.createElement(_Typography.default, { variant: "body2", style: _extends({}, authStyles, { fontStyle: 'italic', fontSize: '0.8rem' }) - }, "Learn more about the benefits of signing in ", benefitsLink, ".")); + }, /*#__PURE__*/_react.default.createElement(_Link.default, { + target: "_new", + href: _RouteService.default.getUserAccountsPath() + }, "Learn"), " the benefits of having an account."), /*#__PURE__*/_react.default.createElement(_NeonSignInButton.default, null)); /* eslint-enable react/jsx-one-expression-per-line */ }; @@ -575,9 +568,9 @@ function DownloadDataDialog() { }, showDownloadButton ? 'Cancel' : 'Done'), showDownloadButton ? renderDownloadButton() : null), showDownloadButton && !allStepsComplete ? /*#__PURE__*/_react.default.createElement(_Typography.default, { variant: "body2", style: { - marginTop: _Theme.default.spacing(1) + marginTop: _Theme.default.spacing(2) } - }, "Complete all steps to enable download") : null, renderAuthSuggestion()); + }, "Complete all steps to enable download.") : null, renderAuthSuggestion()); }; var renderStepNavButtons = function renderStepNavButtons() { diff --git a/lib/components/DownloadDataDialog/NeonSignInButton.d.ts b/lib/components/DownloadDataDialog/NeonSignInButton.d.ts new file mode 100644 index 00000000..bea384f5 --- /dev/null +++ b/lib/components/DownloadDataDialog/NeonSignInButton.d.ts @@ -0,0 +1,2 @@ +/// +export default function NeonSignInButton(): JSX.Element; diff --git a/lib/components/DownloadDataDialog/NeonSignInButton.js b/lib/components/DownloadDataDialog/NeonSignInButton.js new file mode 100644 index 00000000..f62421a6 --- /dev/null +++ b/lib/components/DownloadDataDialog/NeonSignInButton.js @@ -0,0 +1,43 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = NeonSignInButton; + +var _react = _interopRequireDefault(require("react")); + +var _styles = require("@material-ui/core/styles"); + +var _Button = _interopRequireDefault(require("@material-ui/core/Button")); + +var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment/NeonEnvironment")); + +var _signInButtonState = require("./signInButtonState"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var useStyles = (0, _styles.makeStyles)(function (theme) { + return { + signInButton: { + margin: theme.spacing(2) + } + }; +}); +var buttonSubject = (0, _signInButtonState.getSignInButtonSubject)(); + +var handleButtonClick = function handleButtonClick() { + // push to the subject to notify subscribers + buttonSubject.next('clicked'); + document.location.href = _NeonEnvironment.default.getFullAuthPath('login'); +}; + +function NeonSignInButton() { + var classes = useStyles(); + return /*#__PURE__*/_react.default.createElement(_Button.default, { + variant: "contained", + className: classes.signInButton, + color: "primary", + onClick: handleButtonClick + }, "Sign In"); +} \ No newline at end of file diff --git a/lib/components/DownloadDataDialog/signInButtonState.d.ts b/lib/components/DownloadDataDialog/signInButtonState.d.ts new file mode 100644 index 00000000..098b6d43 --- /dev/null +++ b/lib/components/DownloadDataDialog/signInButtonState.d.ts @@ -0,0 +1,3 @@ +import { Subject } from 'rxjs'; +export declare const getSignInButtonSubject: () => Subject; +export declare const getSignInButtonObservable: () => import("rxjs").Observable; diff --git a/lib/components/DownloadDataDialog/signInButtonState.js b/lib/components/DownloadDataDialog/signInButtonState.js new file mode 100644 index 00000000..b4d9547a --- /dev/null +++ b/lib/components/DownloadDataDialog/signInButtonState.js @@ -0,0 +1,23 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.getSignInButtonObservable = exports.getSignInButtonSubject = void 0; + +var _rxjs = require("rxjs"); + +// observable for sharing button state with other components +var buttonSubject = new _rxjs.Subject(); + +var getSignInButtonSubject = function getSignInButtonSubject() { + return buttonSubject; +}; + +exports.getSignInButtonSubject = getSignInButtonSubject; + +var getSignInButtonObservable = function getSignInButtonObservable() { + return buttonSubject.asObservable(); +}; + +exports.getSignInButtonObservable = getSignInButtonObservable; \ No newline at end of file diff --git a/lib/components/ExternalHost/ExternalHost.js b/lib/components/ExternalHost/ExternalHost.js index 1904dad9..d656dc88 100644 --- a/lib/components/ExternalHost/ExternalHost.js +++ b/lib/components/ExternalHost/ExternalHost.js @@ -132,21 +132,6 @@ var externalProducts = { title: 'Small mammal sequences DNA barcode (Prototype Data)' }] }, - 'DP1.10107.001': { - host: 'MGRAST', - projects: [{ - id: 'mgp13948', - title: 'NEON Soil Metagenomes' - }, { - id: 'mgp3546', - title: 'NEON Soils (Prototype Data)' - }] - }, - 'DP1.10108.001': { - host: 'MGRAST', - projects: [] // unable to find associated project(s) - - }, 'DP1.20002.001': { host: 'PHENOCAM' }, @@ -157,42 +142,6 @@ var externalProducts = { title: 'Fish sequences DNA barcode' }] }, - 'DP1.20126.001': { - host: 'MGRAST', - projects: [{ - id: 'mgp84670', - title: 'NEON Macroinvertebrate DNA Barcodes - 2017' - }] - }, - 'DP1.20279.001': { - host: 'MGRAST', - projects: [] // unable to find associated project(s) - - }, - 'DP1.20280.001': { - host: 'MGRAST', - projects: [] // unable to find associated project(s) - - }, - 'DP1.20281.001': { - host: 'MGRAST', - projects: [] // unable to find associated project(s) - - }, - 'DP1.20282.001': { - host: 'MGRAST', - projects: [{ - id: 'mgp84669', - title: 'NEON Surface Water Microbe Marker Gene Sequences - 2014' - }] - }, - 'DP1.20221.001': { - host: 'MGRAST', - projects: [{ - id: 'mgp84672', - title: 'NEON Zooplankton DNA Barcodes - 2017' - }] - }, 'DP4.00002.001': { host: 'AMERIFLUX' }, @@ -348,29 +297,6 @@ var externalHosts = { }); } }, - MGRAST: { - id: 'MGRAST', - name: 'MG-RAST', - projectTitle: 'MG-RAST (Metagenomics Rapid Annotation using Subsystem Technology)', - url: 'https://mg-rast.org', - hostType: HOST_TYPES.ADDITIONAL_DATA, - hostDataVariety: 'Raw sequence data', - linkType: LINK_TYPES.BY_PRODUCT, - getProductLinks: function getProductLinks() { - var productCode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - - if (!externalProducts[productCode]) { - return []; - } - - return externalProducts[productCode].projects.map(function (project) { - return { - key: project.id, - node: renderExternalHostLink("https://www.mg-rast.org/mgmain.html?mgpage=project&project=".concat(project.id), project.title, 'MGRAST', productCode) - }; - }); - } - }, NPN: { id: 'NPN', name: 'USA-NPN', diff --git a/lib/components/NeonAuth/AuthService.d.ts b/lib/components/NeonAuth/AuthService.d.ts index cca180ab..a4a53771 100644 --- a/lib/components/NeonAuth/AuthService.d.ts +++ b/lib/components/NeonAuth/AuthService.d.ts @@ -31,6 +31,11 @@ export interface IAuthService { * @return {boolean} */ isAuthOnlyApp: () => boolean; + /** + * Gets the redirect URI to send to the login endpoint. + * @return {Undef} + */ + getLoginRedirectUri: () => Undef; /** * Initializes a login flow * @param {string} path - Optionally path to set for the root login URL diff --git a/lib/components/NeonAuth/AuthService.js b/lib/components/NeonAuth/AuthService.js index ff1ea2d2..925b5bc9 100644 --- a/lib/components/NeonAuth/AuthService.js +++ b/lib/components/NeonAuth/AuthService.js @@ -167,6 +167,13 @@ var AuthService = { isAuthOnlyApp: function isAuthOnlyApp() { return [_NeonEnvironment.default.route.account()].indexOf(_NeonEnvironment.default.getRouterBaseHomePath() || '') >= 0; }, + getLoginRedirectUri: function getLoginRedirectUri() { + var appHomePath = _NeonEnvironment.default.getRouterBaseHomePath(); + + var currentPath = window.location.pathname; + var hasPath = (0, _typeUtil.isStringNonEmpty)(currentPath) && currentPath.includes(appHomePath); + return hasPath ? currentPath : undefined; + }, login: function login(path, redirectUriPath) { var env = _NeonEnvironment.default; var rootPath = (0, _typeUtil.exists)(path) ? path : env.getFullAuthPath('login'); diff --git a/lib/components/NeonAuth/NeonAuth.js b/lib/components/NeonAuth/NeonAuth.js index ccf77046..371c7895 100644 --- a/lib/components/NeonAuth/NeonAuth.js +++ b/lib/components/NeonAuth/NeonAuth.js @@ -25,6 +25,8 @@ var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment/NeonEn var _Theme = _interopRequireDefault(require("../Theme/Theme")); +var _NeonSignInButtonState = _interopRequireDefault(require("../NeonSignInButton/NeonSignInButtonState")); + var _typeUtil = require("../../util/typeUtil"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } @@ -103,17 +105,18 @@ var renderAuth = function renderAuth(props, classes, isAuthenticated, showAuthWo accountPath = props.accountPath; var handleLogin = function handleLogin() { + if (_NeonEnvironment.default.enableGlobalSignInState) { + // Notify observers the sign in button has been clicked. + _NeonSignInButtonState.default.sendNotification(); + } + var appliedLoginType = loginType; // Default to redirect if WS isn't connected if (!isAuthWsConnected) { appliedLoginType = NeonAuthType.REDIRECT; } - var appHomePath = _NeonEnvironment.default.getRouterBaseHomePath(); - - var currentPath = window.location.pathname; - var hasPath = (0, _typeUtil.isStringNonEmpty)(currentPath) && currentPath.includes(appHomePath); - var redirectUriPath = hasPath ? currentPath : undefined; + var redirectUriPath = _AuthService.default.getLoginRedirectUri(); switch (appliedLoginType) { case NeonAuthType.SILENT: diff --git a/lib/components/NeonEnvironment/NeonEnvironment.d.ts b/lib/components/NeonEnvironment/NeonEnvironment.d.ts index e71d6ecd..9e78b184 100644 --- a/lib/components/NeonEnvironment/NeonEnvironment.d.ts +++ b/lib/components/NeonEnvironment/NeonEnvironment.d.ts @@ -24,6 +24,7 @@ export interface INeonEnvironment { useGraphql: boolean; showAopViewer: boolean; authDisableWs: boolean; + enableGlobalSignInState: boolean; getRootApiPath: () => string; getRootGraphqlPath: () => string; getRootJsonLdPath: () => string; diff --git a/lib/components/NeonEnvironment/NeonEnvironment.js b/lib/components/NeonEnvironment/NeonEnvironment.js index 6be8b58b..50fc9b2a 100644 --- a/lib/components/NeonEnvironment/NeonEnvironment.js +++ b/lib/components/NeonEnvironment/NeonEnvironment.js @@ -51,6 +51,7 @@ var NeonEnvironment = { useGraphql: process.env.REACT_APP_NEON_USE_GRAPHQL === 'true', showAopViewer: process.env.REACT_APP_NEON_SHOW_AOP_VIEWER === 'true', authDisableWs: process.env.REACT_APP_NEON_AUTH_DISABLE_WS === 'true', + enableGlobalSignInState: process.env.REACT_APP_NEON_ENABLE_GLOBAL_SIGNIN_STATE === 'true', getRootApiPath: function getRootApiPath() { return process.env.REACT_APP_NEON_PATH_API || '/api/v0'; }, diff --git a/lib/components/NeonGraphQL/NeonGraphQL.d.ts b/lib/components/NeonGraphQL/NeonGraphQL.d.ts index cd79ab7b..1e0280db 100644 --- a/lib/components/NeonGraphQL/NeonGraphQL.d.ts +++ b/lib/components/NeonGraphQL/NeonGraphQL.d.ts @@ -10,7 +10,7 @@ export namespace DIMENSIONALITIES { export default NeonGraphQL; declare namespace NeonGraphQL { function getDataProductByCode(productCode: any, release: any): import("rxjs").Observable | import("rxjs").Observable; - function getAllDataProducts(release: any): import("rxjs").Observable | import("rxjs").Observable; + function getAllDataProducts(release: any, includeAvailableReleases?: boolean): import("rxjs").Observable | import("rxjs").Observable; function getSiteByCode(siteCode: any): import("rxjs").Observable | import("rxjs").Observable; function getAllSites(): import("rxjs").Observable | import("rxjs").Observable; function getLocationByName(locationName: any): import("rxjs").Observable | import("rxjs").Observable; diff --git a/lib/components/NeonGraphQL/NeonGraphQL.js b/lib/components/NeonGraphQL/NeonGraphQL.js index d250b7c1..dce00fd6 100644 --- a/lib/components/NeonGraphQL/NeonGraphQL.js +++ b/lib/components/NeonGraphQL/NeonGraphQL.js @@ -13,6 +13,8 @@ var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment/NeonEn var _NeonApi = _interopRequireDefault(require("../NeonApi/NeonApi")); +var _typeUtil = require("../../util/typeUtil"); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } @@ -37,6 +39,18 @@ var transformQuery = function transformQuery(query) { }); }; +var getAvailableReleaseClause = function getAvailableReleaseClause(args) { + if (!args) return ''; + var hasRelease = (0, _typeUtil.isStringNonEmpty)(args.release); + var availableReleases = ''; + + if (args.includeAvailableReleases === true && !hasRelease) { + availableReleases = "availableReleases {\n release\n availableMonths\n }"; + } + + return availableReleases; +}; + var getQueryBody = function getQueryBody() { var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var dimensionality = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; @@ -48,11 +62,14 @@ var getQueryBody = function getQueryBody() { if (dimensionality === DIMENSIONALITIES.ONE) { // TODO: Add support for deeper product data when querying for one var releaseArgument = !args.release ? '' : ", release: \"".concat(args.release, "\""); - query = "query Products {\n product (productCode: \"".concat(args.productCode, "\"").concat(releaseArgument, ") {\n productCode\n productName\n productDescription\n productScienceTeam\n productHasExpanded\n productBasicDescription\n productExpandedDescription\n productPublicationFormatType\n keywords\n themes\n siteCodes {\n siteCode\n availableMonths\n }\n releases {\n release\n generationDate\n url\n }\n }\n }"); + var availableReleases = getAvailableReleaseClause(args); + query = "query Products {\n product (productCode: \"".concat(args.productCode, "\"").concat(releaseArgument, ") {\n productCode\n productName\n productDescription\n productScienceTeam\n productHasExpanded\n productBasicDescription\n productExpandedDescription\n productPublicationFormatType\n keywords\n themes\n siteCodes {\n siteCode\n availableMonths\n ").concat(availableReleases, "\n }\n releases {\n release\n generationDate\n url\n }\n }\n }"); } else { var _releaseArgument = !args.release ? '' : "(release: \"".concat(args.release, "\")"); - query = "query Products {\n products ".concat(_releaseArgument, "{\n productCode\n productName\n productDescription\n productScienceTeam\n productHasExpanded\n productBasicDescription\n productExpandedDescription\n productPublicationFormatType\n keywords\n themes\n siteCodes {\n siteCode\n availableMonths\n }\n releases {\n release\n generationDate\n url\n }\n }\n }"); + var _availableReleases = getAvailableReleaseClause(args); + + query = "query Products {\n products ".concat(_releaseArgument, "{\n productCode\n productName\n productDescription\n productScienceTeam\n productHasExpanded\n productBasicDescription\n productExpandedDescription\n productPublicationFormatType\n keywords\n themes\n siteCodes {\n siteCode\n availableMonths\n ").concat(_availableReleases, "\n }\n releases {\n release\n generationDate\n url\n }\n }\n }"); } break; @@ -71,7 +88,7 @@ var getQueryBody = function getQueryBody() { if (dimensionality === DIMENSIONALITIES.ONE) { query = "query Location {\n location(name: \"".concat(args.locationName, "\") {\n locationName\n locationDescription\n locationType\n domainCode\n siteCode\n locationDecimalLatitude\n locationDecimalLongitude\n locationElevation\n locationPolygon {\n coordinates {\n latitude\n longitude\n elevation\n }\n }\n locationProperties {\n locationPropertyName\n locationPropertyValue\n }\n }\n }"); } else { - query = "query findLocations {\n locations: findLocations(\n query: { \n locationNames: ".concat(JSON.stringify(args.locationNames), "\n }\n ) {\n locationName\n locationDescription\n locationParent\n locationType\n domainCode\n siteCode\n locationDecimalLatitude\n locationDecimalLongitude\n locationElevation\n locationPolygon {\n coordinates {\n latitude\n longitude\n elevation\n }\n }\n locationProperties {\n locationPropertyName\n locationPropertyValue\n }\n }\n }"); + query = "query findLocations {\n locations: findLocations(\n query: {\n locationNames: ".concat(JSON.stringify(args.locationNames), "\n }\n ) {\n locationName\n locationDescription\n locationParent\n locationType\n domainCode\n siteCode\n locationDecimalLatitude\n locationDecimalLongitude\n locationElevation\n locationPolygon {\n coordinates {\n latitude\n longitude\n elevation\n }\n }\n locationProperties {\n locationPropertyName\n locationPropertyValue\n }\n }\n }"); } break; @@ -140,8 +157,10 @@ var NeonGraphQL = { }); }, getAllDataProducts: function getAllDataProducts(release) { + var includeAvailableReleases = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; return getObservableWith(TYPES.DATA_PRODUCTS, DIMENSIONALITIES.MANY, { - release: release + release: release, + includeAvailableReleases: includeAvailableReleases }); }, getSiteByCode: function getSiteByCode(siteCode) { diff --git a/lib/components/NeonSignInButton/NeonSignInButton.d.ts b/lib/components/NeonSignInButton/NeonSignInButton.d.ts new file mode 100644 index 00000000..bea384f5 --- /dev/null +++ b/lib/components/NeonSignInButton/NeonSignInButton.d.ts @@ -0,0 +1,2 @@ +/// +export default function NeonSignInButton(): JSX.Element; diff --git a/lib/components/NeonSignInButton/NeonSignInButton.js b/lib/components/NeonSignInButton/NeonSignInButton.js new file mode 100644 index 00000000..6571bf51 --- /dev/null +++ b/lib/components/NeonSignInButton/NeonSignInButton.js @@ -0,0 +1,45 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = NeonSignInButton; + +var _react = _interopRequireDefault(require("react")); + +var _styles = require("@material-ui/core/styles"); + +var _Button = _interopRequireDefault(require("@material-ui/core/Button")); + +var _AuthService = _interopRequireDefault(require("../NeonAuth/AuthService")); + +var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment/NeonEnvironment")); + +var _NeonSignInButtonState = _interopRequireDefault(require("./NeonSignInButtonState")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var useStyles = (0, _styles.makeStyles)(function (theme) { + return { + signInButton: { + margin: theme.spacing(2) + } + }; +}); + +var handleButtonClick = function handleButtonClick() { + // Notify observers the sign in button has been clicked. + _NeonSignInButtonState.default.sendNotification(); + + _AuthService.default.login(_NeonEnvironment.default.getFullAuthPath('login'), _AuthService.default.getLoginRedirectUri()); +}; + +function NeonSignInButton() { + var classes = useStyles(); + return /*#__PURE__*/_react.default.createElement(_Button.default, { + variant: "contained", + className: classes.signInButton, + color: "primary", + onClick: handleButtonClick + }, "Sign In"); +} \ No newline at end of file diff --git a/lib/components/NeonSignInButton/NeonSignInButtonState.d.ts b/lib/components/NeonSignInButton/NeonSignInButtonState.d.ts new file mode 100644 index 00000000..3418d3fe --- /dev/null +++ b/lib/components/NeonSignInButton/NeonSignInButtonState.d.ts @@ -0,0 +1,23 @@ +import { Subject, Observable } from 'rxjs'; +/** + * Interface for sharing sign in button state. + */ +export interface INeonSignInButtonState { + /** + * Get the subject. + * @returns the subject. + */ + getSubject: () => Subject; + /** + * Tell all observers the button has been clicked. + * @returns void + */ + sendNotification: () => void; + /** + * Get the observable. + * @returns the observable. + */ + getObservable: () => Observable; +} +declare const NeonSignInButtonState: INeonSignInButtonState; +export default NeonSignInButtonState; diff --git a/lib/components/NeonSignInButton/NeonSignInButtonState.js b/lib/components/NeonSignInButton/NeonSignInButtonState.js new file mode 100644 index 00000000..9097eeff --- /dev/null +++ b/lib/components/NeonSignInButton/NeonSignInButtonState.js @@ -0,0 +1,30 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _rxjs = require("rxjs"); + +// Instantiate the subject and observable. +var subject = new _rxjs.Subject(); +var observable = subject.asObservable(); +/** + * Interface for sharing sign in button state. + */ + +var NeonSignInButtonState = { + getSubject: function getSubject() { + return subject; + }, + sendNotification: function sendNotification() { + return subject.next(); + }, + getObservable: function getObservable() { + return observable; + } +}; +Object.freeze(NeonSignInButtonState); +var _default = NeonSignInButtonState; +exports.default = _default; \ No newline at end of file diff --git a/lib/components/SiteMap/SiteMap.css b/lib/components/SiteMap/SiteMap.css index aa5ff600..c6475438 100644 --- a/lib/components/SiteMap/SiteMap.css +++ b/lib/components/SiteMap/SiteMap.css @@ -1,38 +1,38 @@ /* This file is for defining global map icon CSS classes for the Neon Site Map */ /* We don't do just these with makeStyles because we want consistent class names */ -div[data-component='SiteMap'] { - .mapIcon: { - box-sizing: content-box; - } - .mapIconPLACEHOLDER: { - border-radius: 20%; - } - .mapIconCORE: { - border-radius: 20%; - } - .mapIconRELOCATABLE: { - border-radius: 50%; - } - /* THIS DOESN'T WORK FOR NON-SQUARE ICONS! */ - .mapIconSQUARE { - box-shadow: 0px 0px 5px -0.5px rgba(0,0,0,0.5); - } - .mapIconUnselected: { - box-shadow: none; - &:hover, &:focus: { - box-shadow: 0px 0px 5px 5px #0073cf; - } - &:active: { - box-shadow: 0px 0px 8px 8px #0073cf; - } - } - .mapIconSelected: { - box-shadow: none; - &:hover, &:focus: { - box-shadow: 0px 0px 3px 3px #ffffff; - } - &:active: { - box-shadow: 0px 0px 6px 6px #ffffff; - } - } +div[data-component='SiteMap'] .mapIcon { + box-sizing: content-box; +} +div[data-component='SiteMap'] .mapIconPLACEHOLDER { + border-radius: 20%; +} +div[data-component='SiteMap'] .mapIconCORE { + border-radius: 20%; +} +div[data-component='SiteMap'] .mapIconGRADIENT { + border-radius: 50%; +} +/* THIS DOESN'T WORK FOR NON-SQUARE ICONS! */ +div[data-component='SiteMap'] .mapIconSQUARE { + box-shadow: 0px 0px 5px -0.5px rgba(0,0,0,0.5); +} +div[data-component='SiteMap'] .mapIconUnselected { + box-shadow: none; +} +div[data-component='SiteMap'] .mapIconUnselected:hover, +div[data-component='SiteMap'] .mapIconUnselected:focus { + box-shadow: 0px 0px 5px 5px #0073cf; +} +div[data-component='SiteMap'] .mapIconUnselected:active { + box-shadow: 0px 0px 8px 8px #0073cf; +} +div[data-component='SiteMap'] .mapIconSelected { + box-shadow: none; +} +div[data-component='SiteMap'] .mapIconSelected:hover, +div[data-component='SiteMap'] .mapIconSelected:focus { + box-shadow: 0px 0px 3px 3px #ffffff; +} +div[data-component='SiteMap'] .mapIconSelected:active { + box-shadow: 0px 0px 6px 6px #ffffff; } diff --git a/lib/components/SiteMap/SiteMap.js b/lib/components/SiteMap/SiteMap.js index 333b938f..b93a402c 100644 --- a/lib/components/SiteMap/SiteMap.js +++ b/lib/components/SiteMap/SiteMap.js @@ -20,11 +20,14 @@ var _SiteMapUtils = require("./SiteMapUtils"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var SiteMap = function SiteMap(props) { + // no need to store this in state, just pass it thru var _props$unusableVertic = props.unusableVerticalSpace, - unusableVerticalSpace = _props$unusableVertic === void 0 ? 0 : _props$unusableVertic; // no need to store this in state, just pass it thru - + unusableVerticalSpace = _props$unusableVertic === void 0 ? 0 : _props$unusableVertic, + _props$mapUniqueId = props.mapUniqueId, + mapUniqueId = _props$mapUniqueId === void 0 ? 0 : _props$mapUniqueId; return /*#__PURE__*/_react.default.createElement(_SiteMapContext.default.Provider, props, /*#__PURE__*/_react.default.createElement(_SiteMapContainer.default, { - unusableVerticalSpace: unusableVerticalSpace + unusableVerticalSpace: unusableVerticalSpace, + mapUniqueId: mapUniqueId })); }; diff --git a/lib/components/SiteMap/SiteMapContainer.d.ts b/lib/components/SiteMap/SiteMapContainer.d.ts index ad77c5c0..8565687e 100644 --- a/lib/components/SiteMap/SiteMapContainer.d.ts +++ b/lib/components/SiteMap/SiteMapContainer.d.ts @@ -3,10 +3,13 @@ declare function SiteMapContainer(props: any): JSX.Element; declare namespace SiteMapContainer { namespace propTypes { const unusableVerticalSpace: PropTypes.Requireable; + const mapUniqueId: PropTypes.Requireable; } namespace defaultProps { const unusableVerticalSpace_1: number; export { unusableVerticalSpace_1 as unusableVerticalSpace }; + const mapUniqueId_1: number; + export { mapUniqueId_1 as mapUniqueId }; } } import PropTypes from "prop-types"; diff --git a/lib/components/SiteMap/SiteMapContainer.js b/lib/components/SiteMap/SiteMapContainer.js index 2980d1a4..4bbd933e 100644 --- a/lib/components/SiteMap/SiteMapContainer.js +++ b/lib/components/SiteMap/SiteMapContainer.js @@ -380,7 +380,8 @@ var useStyles = (0, _styles.makeStyles)(function (theme) { var SiteMapContainer = function SiteMapContainer(props) { var classes = useStyles(_Theme.default); var _props$unusableVertic = props.unusableVerticalSpace, - unusableVerticalSpace = _props$unusableVertic === void 0 ? 0 : _props$unusableVertic; + unusableVerticalSpace = _props$unusableVertic === void 0 ? 0 : _props$unusableVertic, + mapUniqueId = props.mapUniqueId; var _NeonContext$useNeonC = _NeonContext.default.useNeonContextState(), _NeonContext$useNeonC2 = _slicedToArray(_NeonContext$useNeonC, 1), @@ -557,7 +558,8 @@ var SiteMapContainer = function SiteMapContainer(props) { ref: containerDivRef, className: classes.outerContainer, 'aria-busy': isLoading ? 'true' : 'false', - 'data-selenium': 'siteMap-container' + 'data-selenium': 'siteMap-container', + id: mapUniqueId }; /** Render - Loading Sites @@ -1422,10 +1424,12 @@ var SiteMapContainer = function SiteMapContainer(props) { }; SiteMapContainer.propTypes = { - unusableVerticalSpace: _propTypes.default.number + unusableVerticalSpace: _propTypes.default.number, + mapUniqueId: _propTypes.default.number }; SiteMapContainer.defaultProps = { - unusableVerticalSpace: 0 + unusableVerticalSpace: 0, + mapUniqueId: 0 }; var _default = SiteMapContainer; exports.default = _default; \ No newline at end of file diff --git a/lib/components/SiteMap/SiteMapContext.d.ts b/lib/components/SiteMap/SiteMapContext.d.ts index 0ca3e285..b71f492b 100644 --- a/lib/components/SiteMap/SiteMapContext.d.ts +++ b/lib/components/SiteMap/SiteMapContext.d.ts @@ -36,9 +36,7 @@ declare namespace SiteMapContext { export { SORT_DIRECTIONS }; export { VIEWS }; } -/** - Context Provider -*/ +/** Context Provider */ declare function Provider(props: any): JSX.Element; declare namespace Provider { export const propTypes: { @@ -47,6 +45,7 @@ declare namespace Provider { aspectRatio: PropTypes.Requireable; fullscreen: PropTypes.Requireable; unusableVerticalSpace: PropTypes.Requireable; + mapUniqueId: PropTypes.Requireable; mapCenter: PropTypes.Requireable<(number | null | undefined)[]>; mapZoom: PropTypes.Requireable; mapBaseLayer: PropTypes.Requireable; @@ -65,7 +64,7 @@ declare namespace Provider { }; export { SITE_MAP_DEFAULT_PROPS as defaultProps }; } -declare function useSiteMapContext(): any[] | { +declare function useSiteMapContext(): { view: { current: null; initialized: { @@ -155,7 +154,97 @@ declare function useSiteMapContext(): any[] | { }; fullscreen: boolean; manualLocationData: null; -}; +} | ({ + view: { + current: null; + initialized: { + [x: string]: boolean; + }; + }; + neonContextHydrated: boolean; + overallFetch: { + expected: number; + completed: number; + pendingHierarchy: number; + }; + focusLocation: { + current: null; + data: null; + fetch: { + status: null; + error: null; + }; + map: { + zoom: null; + center: never[]; + }; + }; + aspectRatio: { + currentValue: null; + isDynamic: boolean; + resizeEventListenerInitialized: boolean; + widthReference: number; + }; + table: { + focus: any; + availableFeatureTypes: { + [x: number]: boolean; + }; + fullHeight: boolean; + maxBodyHeight: null; + maxBodyHeightUpdateFromAspectRatio: boolean; + }; + map: { + zoom: null; + center: never[]; + bounds: null; + baseLayer: null; + baseLayerAutoChangedAbove17: boolean; + overlays: Set; + mouseMode: string; + zoomedIcons: {}; + repositionOpenPopupFunc: null; + isDraggingAreaSelection: boolean; + }; + selection: { + active: null; + limit: null; + valid: boolean; + set: Set; + validSet: null; + hideUnselectable: boolean; + showSummary: boolean; + changed: boolean; + onChange: () => void; + derived: { + [x: number]: {}; + }; + }; + featureDataFetchesHasAwaiting: boolean; + featureDataFetches: { + [k: string]: {}; + }; + featureData: { + [k: string]: {}; + }; + sites: {}; + filters: { + search: null; + legendOpen: boolean; + features: { + available: {}; + visible: { + [k: string]: boolean; + }; + collapsed: Set; + }; + overlays: { + expanded: Set; + }; + }; + fullscreen: boolean; + manualLocationData: null; +} | (() => void))[]; import { SORT_DIRECTIONS } from "./SiteMapUtils"; import { VIEWS } from "./SiteMapUtils"; import PropTypes from "prop-types"; diff --git a/lib/components/SiteMap/SiteMapContext.js b/lib/components/SiteMap/SiteMapContext.js index 1996f329..725d12a0 100644 --- a/lib/components/SiteMap/SiteMapContext.js +++ b/lib/components/SiteMap/SiteMapContext.js @@ -23,6 +23,14 @@ var _NeonApi = _interopRequireDefault(require("../NeonApi/NeonApi")); var _NeonContext = _interopRequireDefault(require("../NeonContext/NeonContext")); +var _NeonEnvironment = _interopRequireDefault(require("../NeonEnvironment/NeonEnvironment")); + +var _NeonSignInButtonState = _interopRequireDefault(require("../NeonSignInButton/NeonSignInButtonState")); + +var _StateStorageService = _interopRequireDefault(require("../../service/StateStorageService")); + +var _StateStorageConverter = require("./StateStorageConverter"); + var _FetchLocationUtils = require("./FetchLocationUtils"); var _SiteMapUtils = require("./SiteMapUtils"); @@ -1176,9 +1184,7 @@ var reducer = function reducer(state, action) { return state; } }; -/** - Context and Hook -*/ +/** Context and Hook */ var Context = /*#__PURE__*/(0, _react.createContext)(_SiteMapUtils.DEFAULT_STATE); @@ -1193,14 +1199,22 @@ var useSiteMapContext = function useSiteMapContext() { return hookResponse; }; /** - Context Provider -*/ + * Defines a lookup of state key to a boolean + * designating whether or not that instance of the context + * should pull the state from the session storage and restore. + * Keeping this lookup outside of the context provider function + * as to not incur lifecycle interference by storing with useState. + */ +var restoreStateLookup = {}; +/** Context Provider */ + var Provider = function Provider(props) { var view = props.view, aspectRatio = props.aspectRatio, fullscreen = props.fullscreen, + mapUniqueId = props.mapUniqueId, mapZoom = props.mapZoom, mapCenter = props.mapCenter, mapBaseLayer = props.mapBaseLayer, @@ -1283,23 +1297,58 @@ var Provider = function Provider(props) { if (Array.isArray(manualLocationData) && manualLocationData.length > 0) { initialState.manualLocationData = manualLocationData; + } // get the initial state from storage if present + + + var stateKey = "siteMapContextState-".concat(mapUniqueId); + + if (typeof restoreStateLookup[stateKey] === 'undefined') { + restoreStateLookup[stateKey] = true; } - if (neonContextIsFinal && !neonContextHasError) { + var shouldRestoreState = restoreStateLookup[stateKey]; + var stateStorage = (0, _StateStorageService.default)(stateKey); + var savedState = stateStorage.readState(); + + if (neonContextIsFinal && !neonContextHasError && !savedState) { initialState = (0, _SiteMapUtils.hydrateNeonContextData)(initialState, neonContextData); } var hasInitialZoom = typeof mapZoom === 'number' && zoomIsValid(mapZoom); - if (hasInitialZoom) { + if (hasInitialZoom && !savedState) { initialState = calculateZoomState(initialMapZoom, initialState, true); } + if (savedState && shouldRestoreState) { + restoreStateLookup[stateKey] = false; + var restoredState = (0, _StateStorageConverter.convertStateFromStorage)(savedState, initialState); + stateStorage.removeState(); + initialState = calculateZoomState(restoredState.map.zoom, restoredState, true); + } + var _useReducer = (0, _react.useReducer)(reducer, initialState), _useReducer2 = _slicedToArray(_useReducer, 2), state = _useReducer2[0], - dispatch = _useReducer2[1]; + dispatch = _useReducer2[1]; // The current sign in process uses a separate domain. This function + // persists the current state in storage when the button is clicked + // so the state may be reloaded when the page is reloaded after sign + // in. + + + (0, _react.useEffect)(function () { + var subscription = _NeonSignInButtonState.default.getObservable().subscribe({ + next: function next() { + if (!_NeonEnvironment.default.enableGlobalSignInState) return; + restoreStateLookup[stateKey] = false; + stateStorage.saveState((0, _StateStorageConverter.convertStateForStorage)(state)); + } + }); + return function () { + subscription.unsubscribe(); + }; + }, [state, stateStorage, stateKey]); var canFetchFeatureData = state.neonContextHydrated && !(state.focusLocation.current && state.focusLocation.fetch.status !== _SiteMapUtils.FETCH_STATUS.SUCCESS); /** Effect - trigger focusLocation fetch or short circuit if found in NeonContext or manual data diff --git a/lib/components/SiteMap/SiteMapFeature.js b/lib/components/SiteMap/SiteMapFeature.js index 780dbd76..d560298e 100644 --- a/lib/components/SiteMap/SiteMapFeature.js +++ b/lib/components/SiteMap/SiteMapFeature.js @@ -1333,7 +1333,7 @@ var SiteMapFeature = function SiteMapFeature(props) { AQUATIC_METEOROLOGICAL_STATIONS: renderLocationPopup, AQUATIC_PLANT_TRANSECTS: renderLocationPopup, AQUATIC_REACHES: renderBoundaryPopup, - AQUATIC_RELOCATABLE_SITES: renderSitePopup, + AQUATIC_GRADIENT_SITES: renderSitePopup, AQUATIC_RIPARIAN_ASSESSMENTS: renderLocationPopup, AQUATIC_SEDIMENT_POINTS: renderLocationPopup, AQUATIC_SENSOR_STATIONS: renderLocationPopup, @@ -1392,7 +1392,7 @@ var SiteMapFeature = function SiteMapFeature(props) { return renderBoundaryPopup(stateCode, featureData[stateCode] ? featureData[stateCode].name : stateCode, [jumpLink, renderChildSites, selectingCurrentFeatureType ? renderItemSelectionActionSnackbar(stateCode) : renderRegionSelectionAction(_SiteMapUtils.FEATURES.DOMAINS.KEY, stateCode)]); }, TERRESTRIAL_CORE_SITES: renderSitePopup, - TERRESTRIAL_RELOCATABLE_SITES: renderSitePopup, + TERRESTRIAL_GRADIENT_SITES: renderSitePopup, TOWER_AIRSHEDS: renderBoundaryPopup, TOWER_BASE_PLOTS: function TOWER_BASE_PLOTS(siteCode, location) { return renderLocationPopup(siteCode, location, [renderPlotSizeAndSlope, renderPlotSamplingModules]); diff --git a/lib/components/SiteMap/SiteMapUtils.d.ts b/lib/components/SiteMap/SiteMapUtils.d.ts index 25c050a1..9e74ac44 100644 --- a/lib/components/SiteMap/SiteMapUtils.d.ts +++ b/lib/components/SiteMap/SiteMapUtils.d.ts @@ -1487,7 +1487,7 @@ export namespace FEATURES { const maxZoom_1: number; export { maxZoom_1 as maxZoom }; } - export namespace TERRESTRIAL_RELOCATABLE_SITES { + export namespace TERRESTRIAL_GRADIENT_SITES { const name_67: string; export { name_67 as name }; const nameSingular_37: string; @@ -1513,8 +1513,8 @@ export namespace FEATURES { export { featureShape_38 as featureShape }; const iconScale_17: number; export { iconScale_17 as iconScale }; - export { iconSiteRelocatableTerrestrialSVG as iconSvg }; - export { iconSiteRelocatableTerrestrialSelectedSVG as iconSelectedSvg }; + export { iconSiteGradientTerrestrialSVG as iconSvg }; + export { iconSiteGradientTerrestrialSelectedSVG as iconSelectedSvg }; import iconShape_25 = KEY; export { iconShape_25 as iconShape }; const maxZoom_2: number; @@ -1553,7 +1553,7 @@ export namespace FEATURES { const maxZoom_3: number; export { maxZoom_3 as maxZoom }; } - export namespace AQUATIC_RELOCATABLE_SITES { + export namespace AQUATIC_GRADIENT_SITES { const name_69: string; export { name_69 as name }; const nameSingular_39: string; @@ -1579,8 +1579,8 @@ export namespace FEATURES { export { featureShape_40 as featureShape }; const iconScale_19: number; export { iconScale_19 as iconScale }; - export { iconSiteRelocatableAquaticSVG as iconSvg }; - export { iconSiteRelocatableAquaticSelectedSVG as iconSelectedSvg }; + export { iconSiteGradientAquaticSVG as iconSvg }; + export { iconSiteGradientAquaticSelectedSVG as iconSelectedSvg }; import iconShape_27 = KEY; export { iconShape_27 as iconShape }; const maxZoom_4: number; @@ -1791,6 +1791,7 @@ export namespace SITE_MAP_PROP_TYPES { export const aspectRatio: PropTypes.Requireable; export const fullscreen: PropTypes.Requireable; export const unusableVerticalSpace: PropTypes.Requireable; + export const mapUniqueId: PropTypes.Requireable; export const mapCenter: PropTypes.Requireable<(number | null | undefined)[]>; export const mapZoom: PropTypes.Requireable; export const mapBaseLayer: PropTypes.Requireable; @@ -1816,6 +1817,8 @@ export namespace SITE_MAP_DEFAULT_PROPS { export { fullscreen_1 as fullscreen }; const unusableVerticalSpace_1: number; export { unusableVerticalSpace_1 as unusableVerticalSpace }; + const mapUniqueId_1: number; + export { mapUniqueId_1 as mapUniqueId }; export { OBSERVATORY_CENTER as mapCenter }; const mapZoom_1: null; export { mapZoom_1 as mapZoom }; diff --git a/lib/components/SiteMap/SiteMapUtils.js b/lib/components/SiteMap/SiteMapUtils.js index 3c69ebd8..6110e985 100644 --- a/lib/components/SiteMap/SiteMapUtils.js +++ b/lib/components/SiteMap/SiteMapUtils.js @@ -49,13 +49,13 @@ var _iconSiteCoreAquatic = _interopRequireDefault(require("./svg/icon-site-core- var _iconSiteCoreAquaticSelected = _interopRequireDefault(require("./svg/icon-site-core-aquatic-selected.svg")); -var _iconSiteRelocatableTerrestrial = _interopRequireDefault(require("./svg/icon-site-relocatable-terrestrial.svg")); +var _iconSiteGradientTerrestrial = _interopRequireDefault(require("./svg/icon-site-gradient-terrestrial.svg")); -var _iconSiteRelocatableTerrestrialSelected = _interopRequireDefault(require("./svg/icon-site-relocatable-terrestrial-selected.svg")); +var _iconSiteGradientTerrestrialSelected = _interopRequireDefault(require("./svg/icon-site-gradient-terrestrial-selected.svg")); -var _iconSiteRelocatableAquatic = _interopRequireDefault(require("./svg/icon-site-relocatable-aquatic.svg")); +var _iconSiteGradientAquatic = _interopRequireDefault(require("./svg/icon-site-gradient-aquatic.svg")); -var _iconSiteRelocatableAquaticSelected = _interopRequireDefault(require("./svg/icon-site-relocatable-aquatic-selected.svg")); +var _iconSiteGradientAquaticSelected = _interopRequireDefault(require("./svg/icon-site-gradient-aquatic-selected.svg")); var _iconSiteDecommissioned = _interopRequireDefault(require("./svg/icon-site-decommissioned.svg")); @@ -1168,22 +1168,22 @@ var FEATURES = { iconShape: LOCATION_ICON_SVG_SHAPES.SQUARE.KEY, maxZoom: 9 }, - TERRESTRIAL_RELOCATABLE_SITES: { - name: 'Terrestrial Relocatable Sites', - nameSingular: 'Terrestrial Relocatable Site', + TERRESTRIAL_GRADIENT_SITES: { + name: 'Terrestrial Gradient Sites', + nameSingular: 'Terrestrial Gradient Site', type: FEATURE_TYPES.SITES.KEY, - description: 'Land-based; location may change', + description: 'Land-based; gradient location', parent: 'SITE_MARKERS', attributes: { - type: 'RELOCATABLE', + type: 'GRADIENT', terrain: 'TERRESTRIAL' }, dataSource: FEATURE_DATA_SOURCES.NEON_CONTEXT, primaryIdOnly: true, featureShape: 'Marker', iconScale: 1, - iconSvg: _iconSiteRelocatableTerrestrial.default, - iconSelectedSvg: _iconSiteRelocatableTerrestrialSelected.default, + iconSvg: _iconSiteGradientTerrestrial.default, + iconSelectedSvg: _iconSiteGradientTerrestrialSelected.default, iconShape: LOCATION_ICON_SVG_SHAPES.CIRCLE.KEY, maxZoom: 9 }, @@ -1206,22 +1206,22 @@ var FEATURES = { iconShape: LOCATION_ICON_SVG_SHAPES.SQUARE.KEY, maxZoom: 9 }, - AQUATIC_RELOCATABLE_SITES: { - name: 'Aquatic Relocatable Sites', - nameSingular: 'Aquatic Relocatable Site', + AQUATIC_GRADIENT_SITES: { + name: 'Aquatic Gradient Sites', + nameSingular: 'Aquatic Gradient Site', type: FEATURE_TYPES.SITES.KEY, - description: 'Water-based; location may change', + description: 'Water-based; gradient location', parent: 'SITE_MARKERS', attributes: { - type: 'RELOCATABLE', + type: 'GRADIENT', terrain: 'AQUATIC' }, dataSource: FEATURE_DATA_SOURCES.NEON_CONTEXT, primaryIdOnly: true, featureShape: 'Marker', iconScale: 1, - iconSvg: _iconSiteRelocatableAquatic.default, - iconSelectedSvg: _iconSiteRelocatableAquaticSelected.default, + iconSvg: _iconSiteGradientAquatic.default, + iconSelectedSvg: _iconSiteGradientAquaticSelected.default, iconShape: LOCATION_ICON_SVG_SHAPES.CIRCLE.KEY, maxZoom: 9 }, @@ -1822,6 +1822,7 @@ var SITE_MAP_PROP_TYPES = { aspectRatio: _propTypes.default.number, fullscreen: _propTypes.default.bool, unusableVerticalSpace: _propTypes.default.number, + mapUniqueId: _propTypes.default.number, // Map props mapCenter: _propTypes.default.arrayOf(_propTypes.default.number), mapZoom: _propTypes.default.number, @@ -1853,6 +1854,7 @@ var SITE_MAP_DEFAULT_PROPS = { aspectRatio: null, fullscreen: false, unusableVerticalSpace: 0, + mapUniqueId: 0, // Map props mapCenter: OBSERVATORY_CENTER, mapZoom: null, diff --git a/lib/components/SiteMap/StateStorageConverter.d.ts b/lib/components/SiteMap/StateStorageConverter.d.ts new file mode 100644 index 00000000..523977a5 --- /dev/null +++ b/lib/components/SiteMap/StateStorageConverter.d.ts @@ -0,0 +1,13 @@ +/** + * Alter the current state for valid JSON serialization. Set objects + * must be converted to Array objects for serialization. + * @param currentState The current state + */ +declare const convertStateForStorage: (state: any) => any; +/** + * Restore the state from JSON serialization. Array objects must be + * converted back to the expected Set objects. + * @param storedState The state read from storage. + */ +declare const convertStateFromStorage: (state: any, initialState: any) => any; +export { convertStateForStorage, convertStateFromStorage }; diff --git a/lib/components/SiteMap/StateStorageConverter.js b/lib/components/SiteMap/StateStorageConverter.js new file mode 100644 index 00000000..6b03f7d4 --- /dev/null +++ b/lib/components/SiteMap/StateStorageConverter.js @@ -0,0 +1,114 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.convertStateFromStorage = exports.convertStateForStorage = void 0; + +var _cloneDeep = _interopRequireDefault(require("lodash/cloneDeep")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Alter the current state for valid JSON serialization. Set objects + * must be converted to Array objects for serialization. + * @param currentState The current state + */ +var convertStateForStorage = function convertStateForStorage(state) { + var _state$selection, _state$selection2, _state$filters, _state$filters$featur, _state$filters2, _state$filters2$overl, _state$map; + + var selectionSet = (_state$selection = state.selection) === null || _state$selection === void 0 ? void 0 : _state$selection.set; + var selectionValidSet = (_state$selection2 = state.selection) === null || _state$selection2 === void 0 ? void 0 : _state$selection2.validSet; + var filtersFeaturesCollapsed = (_state$filters = state.filters) === null || _state$filters === void 0 ? void 0 : (_state$filters$featur = _state$filters.features) === null || _state$filters$featur === void 0 ? void 0 : _state$filters$featur.collapsed; + var filtersOverlaysExpanded = (_state$filters2 = state.filters) === null || _state$filters2 === void 0 ? void 0 : (_state$filters2$overl = _state$filters2.overlays) === null || _state$filters2$overl === void 0 ? void 0 : _state$filters2$overl.expanded; + var mapOverlays = (_state$map = state.map) === null || _state$map === void 0 ? void 0 : _state$map.overlays; + var newState = (0, _cloneDeep.default)(state); + + if (selectionSet instanceof Set) { + newState.selection.set = Array.from(selectionSet); + } else { + newState.selection.set = []; + } + + if (selectionValidSet instanceof Set) { + newState.selection.validSet = Array.from(selectionValidSet); + } else { + newState.selection.validSet = []; + } + + if (filtersFeaturesCollapsed instanceof Set) { + newState.filters.features.collapsed = Array.from(filtersFeaturesCollapsed); + } else { + newState.filters.features.collapsed = []; + } + + if (filtersOverlaysExpanded instanceof Set) { + newState.filters.overlays.expanded = Array.from(filtersOverlaysExpanded); + } else { + newState.filters.overlays.expanded = []; + } + + if (filtersOverlaysExpanded instanceof Set) { + newState.map.overlays = Array.from(mapOverlays); + } else { + newState.map.overlays = []; + } + + return newState; +}; +/** + * Restore the state from JSON serialization. Array objects must be + * converted back to the expected Set objects. + * @param storedState The state read from storage. + */ + + +exports.convertStateForStorage = convertStateForStorage; + +var convertStateFromStorage = function convertStateFromStorage(state, initialState) { + var _state$selection3, _state$selection4, _state$filters3, _state$filters3$featu, _state$filters4, _state$filters4$overl, _state$map2; + + var newState = (0, _cloneDeep.default)(state); + newState.view = initialState.view; + newState.map.zoomedIcons = initialState.map.zoomedIcons; + newState.selection.onChange = initialState.selection.onChange; + var setValue = (_state$selection3 = state.selection) === null || _state$selection3 === void 0 ? void 0 : _state$selection3.set; + var validSet = (_state$selection4 = state.selection) === null || _state$selection4 === void 0 ? void 0 : _state$selection4.validSet; + var collapsedValue = (_state$filters3 = state.filters) === null || _state$filters3 === void 0 ? void 0 : (_state$filters3$featu = _state$filters3.features) === null || _state$filters3$featu === void 0 ? void 0 : _state$filters3$featu.collapsed; + var expandedValue = (_state$filters4 = state.filters) === null || _state$filters4 === void 0 ? void 0 : (_state$filters4$overl = _state$filters4.overlays) === null || _state$filters4$overl === void 0 ? void 0 : _state$filters4$overl.expanded; + var mapOverlays = (_state$map2 = state.map) === null || _state$map2 === void 0 ? void 0 : _state$map2.overlays; + + if (Array.isArray(setValue)) { + newState.selection.set = new Set(setValue); + } else { + newState.selection.set = new Set(); + } + + if (Array.isArray(validSet)) { + newState.selection.validSet = new Set(validSet); + } else { + newState.selection.validSet = new Set(); + } + + if (Array.isArray(collapsedValue)) { + newState.filters.features.collapsed = new Set(collapsedValue); + } else { + newState.filters.features.collapsed = new Set(); + } + + if (Array.isArray(expandedValue)) { + newState.filters.overlays.expanded = new Set(expandedValue); + } else { + newState.filters.overlays.expanded = new Set(); + } + + if (Array.isArray(mapOverlays)) { + newState.map.overlays = new Set(mapOverlays); + } else { + newState.map.overlays = new Set(); + } + + return newState; +}; + +exports.convertStateFromStorage = convertStateFromStorage; \ No newline at end of file diff --git a/src/lib_components/components/SiteMap/svg/icon-site-relocatable-aquatic-selected.svg b/lib/components/SiteMap/svg/icon-site-gradient-aquatic-selected.svg similarity index 98% rename from src/lib_components/components/SiteMap/svg/icon-site-relocatable-aquatic-selected.svg rename to lib/components/SiteMap/svg/icon-site-gradient-aquatic-selected.svg index ef5d5abd..16f0c056 100644 --- a/src/lib_components/components/SiteMap/svg/icon-site-relocatable-aquatic-selected.svg +++ b/lib/components/SiteMap/svg/icon-site-gradient-aquatic-selected.svg @@ -14,7 +14,7 @@ viewBox="0 0 29.104166 29.104167" version="1.1" id="svg19093" - sodipodi:docname="icon-relocatable-aquatic-selected.svg" + sodipodi:docname="icon-gradient-aquatic-selected.svg" inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> diff --git a/src/lib_components/components/SiteMap/svg/icon-site-relocatable-aquatic.svg b/lib/components/SiteMap/svg/icon-site-gradient-aquatic.svg similarity index 98% rename from src/lib_components/components/SiteMap/svg/icon-site-relocatable-aquatic.svg rename to lib/components/SiteMap/svg/icon-site-gradient-aquatic.svg index 21610b63..ef24e9ef 100644 --- a/src/lib_components/components/SiteMap/svg/icon-site-relocatable-aquatic.svg +++ b/lib/components/SiteMap/svg/icon-site-gradient-aquatic.svg @@ -14,7 +14,7 @@ viewBox="0 0 21.166666 21.166667" version="1.1" id="svg19093" - sodipodi:docname="icon-relocatable-aquatic.svg" + sodipodi:docname="icon-gradient-aquatic.svg" inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> diff --git a/src/lib_components/components/SiteMap/svg/icon-site-relocatable-terrestrial-selected.svg b/lib/components/SiteMap/svg/icon-site-gradient-terrestrial-selected.svg similarity index 98% rename from src/lib_components/components/SiteMap/svg/icon-site-relocatable-terrestrial-selected.svg rename to lib/components/SiteMap/svg/icon-site-gradient-terrestrial-selected.svg index bf58346f..f8d5837c 100644 --- a/src/lib_components/components/SiteMap/svg/icon-site-relocatable-terrestrial-selected.svg +++ b/lib/components/SiteMap/svg/icon-site-gradient-terrestrial-selected.svg @@ -14,7 +14,7 @@ viewBox="0 0 29.104166 29.104167" version="1.1" id="svg19093" - sodipodi:docname="icon-relocatable-terrestrial-selected.svg" + sodipodi:docname="icon-gradient-terrestrial-selected.svg" inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> diff --git a/src/lib_components/components/SiteMap/svg/icon-site-relocatable-terrestrial.svg b/lib/components/SiteMap/svg/icon-site-gradient-terrestrial.svg similarity index 98% rename from src/lib_components/components/SiteMap/svg/icon-site-relocatable-terrestrial.svg rename to lib/components/SiteMap/svg/icon-site-gradient-terrestrial.svg index a03858df..b3a5880a 100644 --- a/src/lib_components/components/SiteMap/svg/icon-site-relocatable-terrestrial.svg +++ b/lib/components/SiteMap/svg/icon-site-gradient-terrestrial.svg @@ -14,7 +14,7 @@ viewBox="0 0 21.166666 21.166667" version="1.1" id="svg19093" - sodipodi:docname="icon-relocatable-terrestrial.svg" + sodipodi:docname="icon-gradient-terrestrial.svg" inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> diff --git a/lib/components/TimeSeriesViewer/StateStorageConverter.d.ts b/lib/components/TimeSeriesViewer/StateStorageConverter.d.ts new file mode 100644 index 00000000..7540241e --- /dev/null +++ b/lib/components/TimeSeriesViewer/StateStorageConverter.d.ts @@ -0,0 +1,11 @@ +/** + * Alter the current state for valid JSON serialization. + * @param currentState The current state + */ +declare const convertStateForStorage: (state: any) => any; +/** + * Restore the state from JSON serialization. + * @param storedState The state read from storage. + */ +declare const convertStateFromStorage: (state: any) => any; +export { convertStateForStorage, convertStateFromStorage }; diff --git a/lib/components/TimeSeriesViewer/StateStorageConverter.js b/lib/components/TimeSeriesViewer/StateStorageConverter.js new file mode 100644 index 00000000..1edff473 --- /dev/null +++ b/lib/components/TimeSeriesViewer/StateStorageConverter.js @@ -0,0 +1,170 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.convertStateFromStorage = exports.convertStateForStorage = void 0; + +var _cloneDeep = _interopRequireDefault(require("lodash/cloneDeep")); + +var _constants = require("./constants"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Alter the current state for valid JSON serialization. + * @param currentState The current state + */ +var convertStateForStorage = function convertStateForStorage(state) { + var newState = (0, _cloneDeep.default)(state); + + switch (newState.status) { + case _constants.TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT: + case _constants.TIME_SERIES_VIEWER_STATUS.LOADING_META: + case _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_DATA: + case _constants.TIME_SERIES_VIEWER_STATUS.LOADING_DATA: + case _constants.TIME_SERIES_VIEWER_STATUS.WARNING: + case _constants.TIME_SERIES_VIEWER_STATUS.ERROR: + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT; + break; + + case _constants.TIME_SERIES_VIEWER_STATUS.READY: + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES; + break; + + default: + break; + } // variables + + + var stateVariables = state.variables; + Object.keys(stateVariables).forEach(function (variableKey, index) { + var _stateVariables$varia = stateVariables[variableKey], + sites = _stateVariables$varia.sites, + tables = _stateVariables$varia.tables, + timeSteps = _stateVariables$varia.timeSteps; + + if (sites instanceof Set && sites.size > 0) { + newState.variables[variableKey].sites = Array.from(sites); + } else { + newState.variables[variableKey].sites = []; + } + + if (tables instanceof Set && sites.size > 0) { + newState.variables[variableKey].tables = Array.from(tables); + } else { + newState.variables[variableKey].tables = []; + } + + if (timeSteps instanceof Set && sites.size > 0) { + newState.variables[variableKey].timeSteps = Array.from(timeSteps); + } else { + newState.variables[variableKey].timeSteps = []; + } + }); // product site variables + + var productSites = state.product.sites; + Object.keys(productSites).forEach(function (siteKey, index) { + var siteVariables = productSites[siteKey].variables; + + if (siteVariables instanceof Set && siteVariables.size > 0) { + newState.product.sites[siteKey].variables = Array.from(siteVariables); + } else { + newState.product.sites[siteKey].variables = []; + } + }); // available quality flags + + var availableQualityFlags = state.availableQualityFlags; + + if (availableQualityFlags instanceof Set) { + newState.availableQualityFlags = Array.from(availableQualityFlags); + } else { + newState.availableQualityFlags = []; + } // available time steps + + + var availableTimeSteps = state.availableTimeSteps; + + if (availableTimeSteps instanceof Set) { + newState.availableTimeSteps = Array.from(availableTimeSteps); + } else { + newState.availableTimeSteps = []; + } + + return newState; +}; +/** + * Restore the state from JSON serialization. + * @param storedState The state read from storage. + */ + + +exports.convertStateForStorage = convertStateForStorage; + +var convertStateFromStorage = function convertStateFromStorage(state) { + var newState = (0, _cloneDeep.default)(state); // graphData data + + var data = state.graphData.data.map(function (entry) { + return [new Date(entry[0]), entry[1]]; + }); + newState.graphData.data = data; // state variables + + var variables = state.variables; + Object.keys(variables).forEach(function (key, index) { + var _variables$key = variables[key], + sites = _variables$key.sites, + tables = _variables$key.tables, + timeSteps = _variables$key.timeSteps; + + if (Array.isArray(sites)) { + newState.variables[key].sites = new Set(sites); + } else { + newState.variables[key].sites = new Set(); + } + + if (Array.isArray(tables)) { + newState.variables[key].tables = new Set(tables); + } else { + newState.variables[key].tables = new Set(); + } + + if (Array.isArray(timeSteps)) { + newState.variables[key].timeSteps = new Set(timeSteps); + } else { + newState.variables[key].timeSteps = new Set(); + } + }); // product site variables + + var productSites = state.product.sites; // get the variables for each site + + Object.keys(productSites).forEach(function (siteKey, index) { + var siteVariables = productSites[siteKey].variables; + + if (Array.isArray(siteVariables) && siteVariables.length > 0) { + newState.product.sites[siteKey].variables = new Set(siteVariables); + } else { + newState.product.sites[siteKey].variables = new Set(); + } + }); // available quality flags + + var availableQualityFlags = state.availableQualityFlags; + + if (Array.isArray(availableQualityFlags)) { + newState.availableQualityFlags = new Set(availableQualityFlags); + } else { + newState.availableQualityFlags = new Set(); + } // available quality flags + + + var availableTimeSteps = state.availableTimeSteps; + + if (Array.isArray(availableTimeSteps)) { + newState.availableTimeSteps = new Set(availableTimeSteps); + } else { + newState.availableTimeSteps = new Set(); + } + + return newState; +}; + +exports.convertStateFromStorage = convertStateFromStorage; \ No newline at end of file diff --git a/lib/components/TimeSeriesViewer/TimeSeriesViewerContainer.js b/lib/components/TimeSeriesViewer/TimeSeriesViewerContainer.js index 61883220..204fee79 100644 --- a/lib/components/TimeSeriesViewer/TimeSeriesViewerContainer.js +++ b/lib/components/TimeSeriesViewer/TimeSeriesViewerContainer.js @@ -56,6 +56,8 @@ var _RouteService = _interopRequireDefault(require("../../service/RouteService") var _TimeSeriesViewerContext = _interopRequireWildcard(require("./TimeSeriesViewerContext")); +var _constants = require("./constants"); + var _TimeSeriesViewerSites = _interopRequireDefault(require("./TimeSeriesViewerSites")); var _TimeSeriesViewerDateRange = _interopRequireDefault(require("./TimeSeriesViewerDateRange")); @@ -570,9 +572,9 @@ function TimeSeriesViewerContainer() { }; var renderGraphOverlay = function renderGraphOverlay() { - var isError = state.status === _TimeSeriesViewerContext.TIME_SERIES_VIEWER_STATUS.ERROR; - var isWarning = state.status === _TimeSeriesViewerContext.TIME_SERIES_VIEWER_STATUS.WARNING; - var isLoading = !isError && state.status !== _TimeSeriesViewerContext.TIME_SERIES_VIEWER_STATUS.READY; + var isError = state.status === _constants.TIME_SERIES_VIEWER_STATUS.ERROR; + var isWarning = state.status === _constants.TIME_SERIES_VIEWER_STATUS.WARNING; + var isLoading = !isError && state.status !== _constants.TIME_SERIES_VIEWER_STATUS.READY; if (isError || isWarning) { return /*#__PURE__*/_react.default.createElement("div", { @@ -588,7 +590,7 @@ function TimeSeriesViewerContainer() { })); } - var isLoadingData = isLoading && state.status === _TimeSeriesViewerContext.TIME_SERIES_VIEWER_STATUS.LOADING_DATA; + var isLoadingData = isLoading && state.status === _constants.TIME_SERIES_VIEWER_STATUS.LOADING_DATA; if (isLoading) { var title = _TimeSeriesViewerContext.TIME_SERIES_VIEWER_STATUS_TITLES[state.status] || 'Loading…'; diff --git a/lib/components/TimeSeriesViewer/TimeSeriesViewerContext.d.ts b/lib/components/TimeSeriesViewer/TimeSeriesViewerContext.d.ts index 97029070..2460429a 100644 --- a/lib/components/TimeSeriesViewer/TimeSeriesViewerContext.d.ts +++ b/lib/components/TimeSeriesViewer/TimeSeriesViewerContext.d.ts @@ -1,28 +1,11 @@ -export namespace TIME_SERIES_VIEWER_STATUS { +export namespace TIME_SERIES_VIEWER_STATUS_TITLES { const INIT_PRODUCT: string; const LOADING_META: string; const READY_FOR_DATA: string; const LOADING_DATA: string; - const ERROR: string; - const WARNING: string; const READY_FOR_SERIES: string; - const READY: string; -} -export namespace TIME_SERIES_VIEWER_STATUS_TITLES { - const INIT_PRODUCT_1: string; - export { INIT_PRODUCT_1 as INIT_PRODUCT }; - const LOADING_META_1: string; - export { LOADING_META_1 as LOADING_META }; - const READY_FOR_DATA_1: string; - export { READY_FOR_DATA_1 as READY_FOR_DATA }; - const LOADING_DATA_1: string; - export { LOADING_DATA_1 as LOADING_DATA }; - const READY_FOR_SERIES_1: string; - export { READY_FOR_SERIES_1 as READY_FOR_SERIES }; - const ERROR_1: null; - export { ERROR_1 as ERROR }; - const READY_1: null; - export { READY_1 as READY }; + const ERROR: null; + const READY: null; } export namespace Y_AXIS_RANGE_MODES { const CENTERED: string; @@ -59,8 +42,7 @@ export namespace TabComponentPropTypes { export namespace DEFAULT_STATE { import mode = VIEWER_MODE.DEFAULT; export { mode }; - import status = TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT; - export { status }; + export const status: string; export const displayError: null; export namespace fetchProduct { import status_1 = FETCH_STATUS.AWAITING_CALL; @@ -106,8 +88,18 @@ export namespace DEFAULT_STATE { export const rollPeriod: number; export const logscale: boolean; export namespace yAxes { - const y1: any; - const y2: any; + namespace y1 { + export const units: null; + const logscale_1: boolean; + export { logscale_1 as logscale }; + export const dataRange: null[]; + export const precision: number; + export const standardDeviation: number; + import rangeMode = Y_AXIS_RANGE_MODES.CENTERED; + export { rangeMode }; + export const axisRange: number[]; + } + namespace y2 { } } export const isDefault: boolean; export const invalidDefaultVariables: Set; @@ -222,8 +214,24 @@ export function getTestableItems(): { rollPeriod: number; logscale: boolean; yAxes: { - y1: any; - y2: any; + y1: { + units: null; + logscale: boolean; + dataRange: null[]; + precision: number; + standardDeviation: number; + rangeMode: string; + axisRange: number[]; + }; + y2: { + units: null; + logscale: boolean; + dataRange: null[]; + precision: number; + standardDeviation: number; + rangeMode: string; + axisRange: number[]; + }; }; isDefault: boolean; invalidDefaultVariables: Set; @@ -280,8 +288,8 @@ declare namespace VIEWER_MODE { declare namespace FETCH_STATUS { export const AWAITING_CALL: string; export const FETCHING: string; - const ERROR_2: string; - export { ERROR_2 as ERROR }; + const ERROR_1: string; + export { ERROR_1 as ERROR }; export const SUCCESS: string; } declare namespace TimeSeriesViewerContext { @@ -295,6 +303,7 @@ declare namespace TimeSeriesViewerContext { declare function Provider(props: any): JSX.Element; declare namespace Provider { namespace propTypes { + export { number as timeSeriesUniqueId }; const mode_1: PropTypes.Requireable; export { mode_1 as mode }; import productCode_1 = TimeSeriesViewerPropTypes.productCode; @@ -306,6 +315,7 @@ declare namespace Provider { export const children: PropTypes.Validator; } namespace defaultProps { + export const timeSeriesUniqueId: number; import mode_2 = VIEWER_MODE.DEFAULT; export { mode_2 as mode }; const productCode_2: null; @@ -316,10 +326,145 @@ declare namespace Provider { export { release_2 as release }; } } -declare function useTimeSeriesViewerState(): any; +declare function useTimeSeriesViewerState(): { + mode: string; + status: string; + displayError: null; + fetchProduct: { + status: string; + error: null; + }; + metaFetches: {}; + dataFetches: {}; + dataFetchProgress: number; + variables: {}; + product: { + productCode: null; + productName: null; + productDescription: null; + productSensor: null; + dateRange: null[]; + continuousDateRange: never[]; + sites: {}; + }; + release: null; + graphData: { + data: never[]; + qualityData: never[]; + monthOffsets: {}; + timestampMap: {}; + series: never[]; + labels: string[]; + qualityLabels: string[]; + }; + selection: { + dateRange: null[]; + continuousDateRange: never[]; + variables: never[]; + dateTimeVariable: null; + sites: never[]; + timeStep: string; + autoTimeStep: null; + qualityFlags: never[]; + rollPeriod: number; + logscale: boolean; + yAxes: { + y1: { + units: null; + logscale: boolean; + dataRange: null[]; + precision: number; + standardDeviation: number; + rangeMode: string; + axisRange: number[]; + }; + y2: { + units: null; + logscale: boolean; + dataRange: null[]; + precision: number; + standardDeviation: number; + rangeMode: string; + axisRange: number[]; + }; + }; + isDefault: boolean; + invalidDefaultVariables: Set; + }; + availableQualityFlags: Set; + availableTimeSteps: Set; +} | ({ + mode: string; + status: string; + displayError: null; + fetchProduct: { + status: string; + error: null; + }; + metaFetches: {}; + dataFetches: {}; + dataFetchProgress: number; + variables: {}; + product: { + productCode: null; + productName: null; + productDescription: null; + productSensor: null; + dateRange: null[]; + continuousDateRange: never[]; + sites: {}; + }; + release: null; + graphData: { + data: never[]; + qualityData: never[]; + monthOffsets: {}; + timestampMap: {}; + series: never[]; + labels: string[]; + qualityLabels: string[]; + }; + selection: { + dateRange: null[]; + continuousDateRange: never[]; + variables: never[]; + dateTimeVariable: null; + sites: never[]; + timeStep: string; + autoTimeStep: null; + qualityFlags: never[]; + rollPeriod: number; + logscale: boolean; + yAxes: { + y1: { + units: null; + logscale: boolean; + dataRange: null[]; + precision: number; + standardDeviation: number; + rangeMode: string; + axisRange: number[]; + }; + y2: { + units: null; + logscale: boolean; + dataRange: null[]; + precision: number; + standardDeviation: number; + rangeMode: string; + axisRange: number[]; + }; + }; + isDefault: boolean; + invalidDefaultVariables: Set; + }; + availableQualityFlags: Set; + availableTimeSteps: Set; +} | (() => void))[]; declare namespace TimeSeriesViewerPropTypes { export function productCode_3(props: any, propName: any, componentName: any): Error | null; export { productCode_3 as productCode }; export function productData_2(props: any, propName: any, componentName: any): Error | null; export { productData_2 as productData }; } +import { number } from "prop-types"; diff --git a/lib/components/TimeSeriesViewer/TimeSeriesViewerContext.js b/lib/components/TimeSeriesViewer/TimeSeriesViewerContext.js index 374df790..df46e7e8 100644 --- a/lib/components/TimeSeriesViewer/TimeSeriesViewerContext.js +++ b/lib/components/TimeSeriesViewer/TimeSeriesViewerContext.js @@ -5,11 +5,11 @@ function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "functi Object.defineProperty(exports, "__esModule", { value: true }); -exports.getTestableItems = exports.default = exports.summarizeTimeSteps = exports.TIME_STEPS = exports.DEFAULT_STATE = exports.TabComponentPropTypes = exports.Y_AXIS_RANGE_MODE_DETAILS = exports.Y_AXIS_RANGE_MODES = exports.TIME_SERIES_VIEWER_STATUS_TITLES = exports.TIME_SERIES_VIEWER_STATUS = void 0; +exports.getTestableItems = exports.default = exports.summarizeTimeSteps = exports.TIME_STEPS = exports.DEFAULT_STATE = exports.TabComponentPropTypes = exports.Y_AXIS_RANGE_MODE_DETAILS = exports.Y_AXIS_RANGE_MODES = exports.TIME_SERIES_VIEWER_STATUS_TITLES = void 0; var _react = _interopRequireWildcard(require("react")); -var _propTypes = _interopRequireDefault(require("prop-types")); +var _propTypes = _interopRequireWildcard(require("prop-types")); var _moment = _interopRequireDefault(require("moment")); @@ -37,6 +37,14 @@ var _rxUtil = require("../../util/rxUtil"); var _parseTimeSeriesData = _interopRequireDefault(require("../../workers/parseTimeSeriesData")); +var _NeonSignInButtonState = _interopRequireDefault(require("../NeonSignInButton/NeonSignInButtonState")); + +var _StateStorageService = _interopRequireDefault(require("../../service/StateStorageService")); + +var _StateStorageConverter = require("./StateStorageConverter"); + +var _constants = require("./constants"); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } @@ -78,27 +86,7 @@ var FETCH_STATUS = { FETCHING: 'FETCHING', ERROR: 'ERROR', SUCCESS: 'SUCCESS' -}; // Every possible top-level status the TimeSeriesViewer component can have - -var TIME_SERIES_VIEWER_STATUS = { - INIT_PRODUCT: 'INIT_PRODUCT', - // Handling props; fetching product data if needed - LOADING_META: 'LOADING_META', - // Actively loading meta data (sites, variables, and positions) - READY_FOR_DATA: 'READY_FOR_DATA', - // Ready to trigger fetches for data - LOADING_DATA: 'LOADING_DATA', - // Actively loading plottable series data - ERROR: 'ERROR', - // Stop everything because problem, do not trigger new fetches no matter what - WARNING: 'WARNING', - // Current selection/data makes a graph not possible; show warning - READY_FOR_SERIES: 'READY_FOR_SERIES', - // Ready to re-calculate series data for the graph - READY: 'READY' // Ready for user input - }; -exports.TIME_SERIES_VIEWER_STATUS = TIME_SERIES_VIEWER_STATUS; var TIME_SERIES_VIEWER_STATUS_TITLES = { INIT_PRODUCT: 'Loading data product…', LOADING_META: 'Loading site positions, variables, and data paths…', @@ -200,7 +188,7 @@ var DEFAULT_AXIS_STATE = { }; var DEFAULT_STATE = { mode: VIEWER_MODE.DEFAULT, - status: TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT, + status: _constants.TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT, displayError: null, fetchProduct: { status: FETCH_STATUS.AWAITING_CALL, @@ -914,7 +902,7 @@ var applyDefaultsToSelection = function applyDefaultsToSelection(state) { // isDefault will be false for the lifetime of the time series viewer instance, so this automated // removal of a selected variable can only happen before any user selection happens. - if (status === TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES && selection.isDefault && selection.variables.length && selection.yAxes.y1.dataRange.every(function (x) { + if (status === _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES && selection.isDefault && selection.variables.length && selection.yAxes.y1.dataRange.every(function (x) { return x === null; })) { selection.invalidDefaultVariables.add(selection.variables[0]); @@ -986,27 +974,27 @@ var reducer = function reducer(state, action) { }; var calcStatus = function calcStatus() { - if (newState.status === TIME_SERIES_VIEWER_STATUS.ERROR) { + if (newState.status === _constants.TIME_SERIES_VIEWER_STATUS.ERROR) { return; } if (Object.keys(newState.metaFetches).length) { - newState.status = TIME_SERIES_VIEWER_STATUS.LOADING_META; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.LOADING_META; } else if (Object.keys(newState.dataFetches).length) { - newState.status = TIME_SERIES_VIEWER_STATUS.LOADING_DATA; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.LOADING_DATA; } else { - newState.status = TIME_SERIES_VIEWER_STATUS.READY_FOR_DATA; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_DATA; } }; var softFail = function softFail(error) { - newState.status = TIME_SERIES_VIEWER_STATUS.WARNING; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.WARNING; newState.displayError = error; return newState; }; var fail = function fail(error) { - newState.status = TIME_SERIES_VIEWER_STATUS.ERROR; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.ERROR; newState.displayError = error; return newState; }; @@ -1029,7 +1017,7 @@ var reducer = function reducer(state, action) { case 'initFetchProductFailed': newState.fetchProduct.status = FETCH_STATUS.ERROR; newState.fetchProduct.error = action.error; - newState.status = TIME_SERIES_VIEWER_STATUS.ERROR; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.ERROR; newState.displayError = "Unable to load product: ".concat(action.error); return newState; @@ -1037,7 +1025,7 @@ var reducer = function reducer(state, action) { newState.fetchProduct.status = FETCH_STATUS.SUCCESS; newState.product = parseProductData(action.productData); calcSelection(); - newState.status = TIME_SERIES_VIEWER_STATUS.LOADING_META; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.LOADING_META; return newState; // Fetch Site Month Actions @@ -1051,7 +1039,7 @@ var reducer = function reducer(state, action) { status: FETCH_STATUS.FETCHING, error: null }; - newState.status = TIME_SERIES_VIEWER_STATUS.LOADING_META; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.LOADING_META; return newState; case 'fetchSiteMonthFailed': @@ -1095,7 +1083,7 @@ var reducer = function reducer(state, action) { if (newState.product.sites[action.siteCode].fetches.variables.status !== FETCH_STATUS.SUCCESS || newState.product.sites[action.siteCode].fetches.positions.status !== FETCH_STATUS.SUCCESS // eslint-disable-line max-len ) { - newState.status = TIME_SERIES_VIEWER_STATUS.LOADING_META; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.LOADING_META; } else { calcStatus(); } @@ -1159,7 +1147,7 @@ var reducer = function reducer(state, action) { } newState.graphData = action.graphData; - newState.status = TIME_SERIES_VIEWER_STATUS.READY; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.READY; return newState; // Fetch Site Positions Actions @@ -1201,7 +1189,7 @@ var reducer = function reducer(state, action) { newState.dataFetches[action.token] = true; newState = setDataFileFetchStatuses(newState, action.fetches); newState.dataFetchProgress = 0; - newState.status = TIME_SERIES_VIEWER_STATUS.LOADING_DATA; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.LOADING_DATA; return newState; case 'fetchDataFilesProgress': @@ -1214,7 +1202,7 @@ var reducer = function reducer(state, action) { } delete newState.dataFetches[action.token]; - newState.status = TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES; calcSelection(); if (!newState.selection.variables.length) { @@ -1224,12 +1212,12 @@ var reducer = function reducer(state, action) { return newState; case 'noDataFilesFetchNecessary': - newState.status = TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES; return newState; // Static data injection action, ignore fetches case 'staticFetchDataFilesCompleted': - newState.status = TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES; + newState.status = _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES; calcSelection(); if (!newState.selection.variables.length) { @@ -1491,13 +1479,23 @@ var reducer = function reducer(state, action) { return state; } }; +/** + * Defines a lookup of state key to a boolean + * designating whether or not that instance of the context + * should pull the state from the session storage and restore. + * Keeping this lookup outside of the context provider function + * as to not incur lifecycle interference by storing with useState. + */ + + +var restoreStateLookup = {}; /** Context Provider */ - var Provider = function Provider(props) { - var modeProp = props.mode, + var timeSeriesUniqueId = props.timeSeriesUniqueId, + modeProp = props.mode, productCodeProp = props.productCode, productDataProp = props.productData, releaseProp = props.release, @@ -1512,7 +1510,7 @@ var Provider = function Provider(props) { initialState.mode = modeProp; } - initialState.status = productDataProp ? TIME_SERIES_VIEWER_STATUS.LOADING_META : TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT; + initialState.status = productDataProp ? _constants.TIME_SERIES_VIEWER_STATUS.LOADING_META : _constants.TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT; if (productDataProp) { initialState.fetchProduct.status = FETCH_STATUS.SUCCESS; @@ -1522,17 +1520,55 @@ var Provider = function Provider(props) { } initialState.release = releaseProp; - initialState.selection = applyDefaultsToSelection(initialState); + initialState.selection = applyDefaultsToSelection(initialState); // get the state from storage if present + + var productCode = initialState.product.productCode; + var stateKey = "timeSeriesContextState-".concat(productCode, "-").concat(timeSeriesUniqueId); + + if (typeof restoreStateLookup[stateKey] === 'undefined') { + restoreStateLookup[stateKey] = true; + } + + var shouldRestoreState = restoreStateLookup[stateKey]; + var stateStorage = (0, _StateStorageService.default)(stateKey); + var savedState = stateStorage.readState(); + + if (savedState && shouldRestoreState) { + restoreStateLookup[stateKey] = false; + var convertedState = (0, _StateStorageConverter.convertStateFromStorage)(savedState); + stateStorage.removeState(); + initialState = convertedState; + } var _useReducer = (0, _react.useReducer)(reducer, initialState), _useReducer2 = _slicedToArray(_useReducer, 2), state = _useReducer2[0], dispatch = _useReducer2[1]; + + var viewerStatus = state.viewerStatus; // The current sign in process uses a separate domain. This function + // persists the current state in storage when the button is clicked + // so the state may be reloaded when the page is reloaded after sign + // in. + + (0, _react.useEffect)(function () { + var subscription = _NeonSignInButtonState.default.getObservable().subscribe({ + next: function next() { + if (!_NeonEnvironment.default.enableGlobalSignInState) return; + if (viewerStatus !== _constants.TIME_SERIES_VIEWER_STATUS.READY) return; + restoreStateLookup[stateKey] = false; + var convertedState = (0, _StateStorageConverter.convertStateForStorage)(state); + stateStorage.saveState(convertedState); + } + }); + + return function () { + subscription.unsubscribe(); + }; + }, [viewerStatus, state, stateStorage, stateKey]); /** Effect - Reinitialize state if the product code prop changed */ - (0, _react.useEffect)(function () { // Ignore initialization when in static mode if (state.mode === VIEWER_MODE.STATIC) { @@ -1556,7 +1592,7 @@ var Provider = function Provider(props) { return; } - if (state.status !== TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT) { + if (state.status !== _constants.TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT) { return; } @@ -1803,7 +1839,7 @@ var Provider = function Provider(props) { fetchNeededSiteMonths(siteCode, fetches); // Add any fetch observables for needed data to dataFetches and dataFetchTokens - if (state.status === TIME_SERIES_VIEWER_STATUS.READY_FOR_DATA && !metaFetchTriggered) { + if (state.status === _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_DATA && !metaFetchTriggered) { prepareDataFetches(site); } @@ -1816,7 +1852,7 @@ var Provider = function Provider(props) { } // Trigger all data fetches - if (state.status === TIME_SERIES_VIEWER_STATUS.READY_FOR_DATA && !metaFetchTriggered) { + if (state.status === _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_DATA && !metaFetchTriggered) { if (!dataFetches.length) { dispatch({ type: 'noDataFilesFetchNecessary' @@ -1906,6 +1942,7 @@ var TimeSeriesViewerPropTypes = { } }; Provider.propTypes = { + timeSeriesUniqueId: _propTypes.number, mode: _propTypes.default.string, productCode: TimeSeriesViewerPropTypes.productCode, productData: TimeSeriesViewerPropTypes.productData, @@ -1913,6 +1950,7 @@ Provider.propTypes = { children: _propTypes.default.oneOfType([_propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.string])), _propTypes.default.node, _propTypes.default.string]).isRequired }; Provider.defaultProps = { + timeSeriesUniqueId: 0, mode: VIEWER_MODE.DEFAULT, productCode: null, productData: null, diff --git a/lib/components/TimeSeriesViewer/TimeSeriesViewerGraph.js b/lib/components/TimeSeriesViewer/TimeSeriesViewerGraph.js index df2d02c6..8eb8c1c2 100644 --- a/lib/components/TimeSeriesViewer/TimeSeriesViewerGraph.js +++ b/lib/components/TimeSeriesViewer/TimeSeriesViewerGraph.js @@ -43,10 +43,12 @@ var _VisibilityOff = _interopRequireDefault(require("@material-ui/icons/Visibili var _generateTimeSeriesGraphData = _interopRequireDefault(require("../../workers/generateTimeSeriesGraphData")); -var _TimeSeriesViewerContext = _interopRequireWildcard(require("./TimeSeriesViewerContext")); +var _TimeSeriesViewerContext = _interopRequireDefault(require("./TimeSeriesViewerContext")); var _Theme = _interopRequireWildcard(require("../Theme/Theme")); +var _constants = require("./constants"); + var _NSFNEONLogo = _interopRequireDefault(require("../../images/NSF-NEON-logo.png")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } @@ -565,7 +567,7 @@ function TimeSeriesViewerGraph() { (0, _react.useEffect)(function () { - if (state.status !== _TimeSeriesViewerContext.TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES) { + if (state.status !== _constants.TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES) { return; } @@ -597,7 +599,7 @@ function TimeSeriesViewerGraph() { }); }); - if (state.status === _TimeSeriesViewerContext.TIME_SERIES_VIEWER_STATUS.READY) { + if (state.status === _constants.TIME_SERIES_VIEWER_STATUS.READY) { // Determine the set of axes and their units var previousAxisCount = axisCountRef.current; var axes = Object.keys(yAxes).map(function (axis) { @@ -635,6 +637,10 @@ function TimeSeriesViewerGraph() { var handleResize = (0, _react.useCallback)(function () { + if (state.status !== _constants.TIME_SERIES_VIEWER_STATUS.READY) { + return; + } + if (!dygraphRef.current || !legendRef.current || !graphInnerContainerRef.current) { return; } // Resize the graph relative to the legend width now that the legend is properly rendered @@ -662,7 +668,7 @@ function TimeSeriesViewerGraph() { }); } } - }, [dygraphRef, legendRef, graphInnerContainerRef, downloadRef, graphState.pngDimensions, graphDispatch]); // Layout effect to keep the graph dimensions in-line with resize events + }, [state.status, dygraphRef, legendRef, graphInnerContainerRef, downloadRef, graphState.pngDimensions, graphDispatch]); // Layout effect to keep the graph dimensions in-line with resize events (0, _react.useLayoutEffect)(function () { if (graphInnerContainerRef.current === null) { @@ -690,7 +696,7 @@ function TimeSeriesViewerGraph() { }, [graphInnerContainerRef, handleResize]); // Effect to apply latest options/data to the graph and force a resize (0, _react.useEffect)(function () { - if (state.status !== _TimeSeriesViewerContext.TIME_SERIES_VIEWER_STATUS.READY) { + if (state.status !== _constants.TIME_SERIES_VIEWER_STATUS.READY) { return; } diff --git a/lib/components/TimeSeriesViewer/TimeSeriesViewerSites.js b/lib/components/TimeSeriesViewer/TimeSeriesViewerSites.js index 71f6455d..078dcb2b 100644 --- a/lib/components/TimeSeriesViewer/TimeSeriesViewerSites.js +++ b/lib/components/TimeSeriesViewer/TimeSeriesViewerSites.js @@ -98,9 +98,9 @@ var _iconSiteCoreTerrestrial = _interopRequireDefault(require("../SiteMap/svg/ic var _iconSiteCoreAquatic = _interopRequireDefault(require("../SiteMap/svg/icon-site-core-aquatic.svg")); -var _iconSiteRelocatableTerrestrial = _interopRequireDefault(require("../SiteMap/svg/icon-site-relocatable-terrestrial.svg")); +var _iconSiteGradientTerrestrial = _interopRequireDefault(require("../SiteMap/svg/icon-site-gradient-terrestrial.svg")); -var _iconSiteRelocatableAquatic = _interopRequireDefault(require("../SiteMap/svg/icon-site-relocatable-aquatic.svg")); +var _iconSiteGradientAquatic = _interopRequireDefault(require("../SiteMap/svg/icon-site-gradient-aquatic.svg")); var _TimeSeriesViewerContext = _interopRequireWildcard(require("./TimeSeriesViewerContext")); @@ -145,9 +145,9 @@ var ICON_SVGS = { AQUATIC: _iconSiteCoreAquatic.default, TERRESTRIAL: _iconSiteCoreTerrestrial.default }, - RELOCATABLE: { - AQUATIC: _iconSiteRelocatableAquatic.default, - TERRESTRIAL: _iconSiteRelocatableTerrestrial.default + GRADIENT: { + AQUATIC: _iconSiteGradientAquatic.default, + TERRESTRIAL: _iconSiteGradientTerrestrial.default } }; /** @@ -1040,9 +1040,9 @@ function SelectedSite(props) { var typeTitle = 'Core'; var typeSubtitle = 'fixed location'; - if (type === 'RELOCATABLE') { - typeTitle = 'Relocatable'; - typeSubtitle = 'location may change'; + if (type === 'GRADIENT') { + typeTitle = 'Gradient'; + typeSubtitle = 'gradient location'; } var terrainTitle = 'Terrestrial'; diff --git a/lib/components/TimeSeriesViewer/constants.d.ts b/lib/components/TimeSeriesViewer/constants.d.ts new file mode 100644 index 00000000..a8bdab41 --- /dev/null +++ b/lib/components/TimeSeriesViewer/constants.d.ts @@ -0,0 +1,23 @@ +export declare const TIME_SERIES_VIEWER_STATUS: { + INIT_PRODUCT: string; + LOADING_META: string; + READY_FOR_DATA: string; + LOADING_DATA: string; + ERROR: string; + WARNING: string; + READY_FOR_SERIES: string; + READY: string; +}; +declare const TimeSeriesViewerConstants: { + TIME_SERIES_VIEWER_STATUS: { + INIT_PRODUCT: string; + LOADING_META: string; + READY_FOR_DATA: string; + LOADING_DATA: string; + ERROR: string; + WARNING: string; + READY_FOR_SERIES: string; + READY: string; + }; +}; +export default TimeSeriesViewerConstants; diff --git a/lib/components/TimeSeriesViewer/constants.js b/lib/components/TimeSeriesViewer/constants.js new file mode 100644 index 00000000..a9b0d7b5 --- /dev/null +++ b/lib/components/TimeSeriesViewer/constants.js @@ -0,0 +1,31 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = exports.TIME_SERIES_VIEWER_STATUS = void 0; +// Every possible top-level status the TimeSeriesViewer component can have +var TIME_SERIES_VIEWER_STATUS = { + INIT_PRODUCT: 'INIT_PRODUCT', + // Handling props; fetching product data if needed + LOADING_META: 'LOADING_META', + // Actively loading meta data (sites, variables, and positions) + READY_FOR_DATA: 'READY_FOR_DATA', + // Ready to trigger fetches for data + LOADING_DATA: 'LOADING_DATA', + // Actively loading plottable series data + ERROR: 'ERROR', + // Stop everything because problem, do not trigger new fetches no matter what + WARNING: 'WARNING', + // Current selection/data makes a graph not possible; show warning + READY_FOR_SERIES: 'READY_FOR_SERIES', + // Ready to re-calculate series data for the graph + READY: 'READY' // Ready for user input + +}; +exports.TIME_SERIES_VIEWER_STATUS = TIME_SERIES_VIEWER_STATUS; +var TimeSeriesViewerConstants = { + TIME_SERIES_VIEWER_STATUS: TIME_SERIES_VIEWER_STATUS +}; +var _default = TimeSeriesViewerConstants; +exports.default = _default; \ No newline at end of file diff --git a/lib/remoteAssets/drupal-header.html.d.ts b/lib/remoteAssets/drupal-header.html.d.ts index 1fe9e522..6a0e0399 100644 --- a/lib/remoteAssets/drupal-header.html.d.ts +++ b/lib/remoteAssets/drupal-header.html.d.ts @@ -1,2 +1,2 @@ -declare var _default: "\n
\n
\n \n \n \n
\n Sign In\n
\n
\n
\n
\n \n \n \n\n\n \n\n
\n
\n \n
\n
\n

Search

\n \n\n
\n \n
\n \n\n \n
\n
\n\n\n
\n\n
\n\n\n\n\n
\n \n\n
\n
\n
\n
\n
\n\n\n"; +declare var _default: "\n
\n
\n \n \n \n
\n Sign In\n
\n
\n
\n
\n \n \n \n\n\n \n\n
\n
\n \n
\n
\n

Search

\n \n\n
\n \n
\n \n\n \n
\n
\n\n\n
\n\n
\n\n\n\n\n
\n \n\n
\n
\n
\n
\n
\n\n\n"; export default _default; diff --git a/lib/remoteAssets/drupal-header.html.js b/lib/remoteAssets/drupal-header.html.js index 1a113cc2..bb85270e 100644 --- a/lib/remoteAssets/drupal-header.html.js +++ b/lib/remoteAssets/drupal-header.html.js @@ -6,6 +6,6 @@ Object.defineProperty(exports, "__esModule", { exports.default = void 0; var html; -var _default = html = "\n
\n
\n \n \n \n
\n Sign In\n
\n
\n
\n
\n \n \n \n\n\n \n\n
\n
\n \n
\n
\n

Search

\n \n\n
\n \n
\n \n\n \n
\n
\n\n\n
\n\n
\n\n\n\n\n
\n \n\n
\n
\n
\n
\n
\n\n\n"; +var _default = html = "\n
\n
\n \n \n \n
\n Sign In\n
\n
\n
\n
\n \n \n \n\n\n \n\n
\n
\n \n
\n
\n

Search

\n \n\n
\n \n
\n \n\n \n
\n
\n\n\n
\n\n
\n\n\n\n\n
\n \n\n
\n
\n
\n
\n
\n\n\n"; exports.default = _default; \ No newline at end of file diff --git a/lib/service/StateService.d.ts b/lib/service/StateService.d.ts new file mode 100644 index 00000000..33b7dd89 --- /dev/null +++ b/lib/service/StateService.d.ts @@ -0,0 +1,53 @@ +/** + * Interface to define a service for persisting state. + */ +export interface IStateService { + /** + * Adds the item to persistent storage. + * @param key The item key. + * @param value The item value. + * @returns void. + */ + setItem: (key: string, value: string) => void; + /** + * Gets the item's value from persistent storage. + * @param key The item key. + * @returns The item's value as a string or null if it does not exist in storage. + */ + getItem: (key: string) => string | null; + /** + * Adds the object to persistent storage. + * @param key The object key. + * @param value The object value. + * @returns void. + */ + setObject: (key: string, value: object) => void; + /** + * Gets the item's value from persistent storage. + * @param key The item key. + * @returns The item's value as a string or null if it does not exist in storage. + */ + getObject: (key: string) => object | null; + /** + * Gets the key at the given index. + * @param index The index of the item in storage. + * @returns The item's key. + */ + key: (index: number) => string | null; + /** + * Removes the item with the given key. + * @param key The item's key. + * @returns void. + */ + removeItem: (key: string) => void; + /** + * Clears storage of all items. + */ + clear: () => void; + /** + * Returns the number of items in storage. + */ + length: (key: string) => number; +} +declare const StateService: IStateService; +export default StateService; diff --git a/lib/service/StateService.js b/lib/service/StateService.js new file mode 100644 index 00000000..e7cfcad5 --- /dev/null +++ b/lib/service/StateService.js @@ -0,0 +1,45 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +/** + * Interface to define a service for persisting state. + */ +var StateService = { + setItem: function setItem(key, value) { + return sessionStorage.setItem(key, value); + }, + getItem: function getItem(key) { + return sessionStorage.getItem(key); + }, + setObject: function setObject(key, object) { + return sessionStorage.setItem(key, JSON.stringify(object)); + }, + getObject: function getObject(key) { + var value = sessionStorage.getItem(key); + + if (!value) { + return null; + } + + return JSON.parse(value); + }, + key: function key(index) { + return sessionStorage.key(index); + }, + removeItem: function removeItem(key) { + return sessionStorage.removeItem(key); + }, + clear: function clear() { + return sessionStorage.clear(); + }, + length: function length() { + return sessionStorage.length; + } +}; +Object.freeze(StateService); +var _default = StateService; +exports.default = _default; \ No newline at end of file diff --git a/lib/service/StateStorageService.d.ts b/lib/service/StateStorageService.d.ts new file mode 100644 index 00000000..db88914c --- /dev/null +++ b/lib/service/StateStorageService.d.ts @@ -0,0 +1,15 @@ +/** + * Interface for a simple application state storage service. + */ +export interface IStateStorageService { + saveState: (state: object) => void; + readState: () => object | null; + removeState: () => void; +} +/** + * Function to create application state storage. + * @param key The key to identify the entry in storage. + * @returns A StateStorage object with functions to store and retrieve application state. + */ +declare const makeStateStorage: (key: string) => IStateStorageService; +export default makeStateStorage; diff --git a/lib/service/StateStorageService.js b/lib/service/StateStorageService.js new file mode 100644 index 00000000..51a6786c --- /dev/null +++ b/lib/service/StateStorageService.js @@ -0,0 +1,32 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _StorageService = _interopRequireDefault(require("./StorageService")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Function to create application state storage. + * @param key The key to identify the entry in storage. + * @returns A StateStorage object with functions to store and retrieve application state. + */ +var makeStateStorage = function makeStateStorage(key) { + return { + saveState: function saveState(state) { + return _StorageService.default.setObject(key, state); + }, + readState: function readState() { + return _StorageService.default.getObject(key); + }, + removeState: function removeState() { + return _StorageService.default.remove(key); + } + }; +}; + +var _default = makeStateStorage; +exports.default = _default; \ No newline at end of file diff --git a/lib/service/StorageService.d.ts b/lib/service/StorageService.d.ts new file mode 100644 index 00000000..9d77c223 --- /dev/null +++ b/lib/service/StorageService.d.ts @@ -0,0 +1,53 @@ +/** + * Interface to define a service for data storage. + */ +export interface IStorageService { + /** + * Adds the item to persistent storage. + * @param key The item key. + * @param value The item value. + * @returns void. + */ + setItem: (key: string, value: string) => void; + /** + * Gets the item's value from persistent storage. + * @param key The item key. + * @returns The item's value as a string or null if it does not exist in storage. + */ + getItem: (key: string) => string | null; + /** + * Adds the object to persistent storage. + * @param key The object key. + * @param value The object value. + * @returns void. + */ + setObject: (key: string, value: object) => void; + /** + * Gets the item's value from persistent storage. + * @param key The item key. + * @returns The item's value as a string or null if it does not exist in storage. + */ + getObject: (key: string) => object | null; + /** + * Gets the key at the given index. + * @param index The index of the item in storage. + * @returns The item's key. + */ + getKey: (index: number) => string | null; + /** + * Removes the item with the given key. + * @param key The item's key. + * @returns void. + */ + remove: (key: string) => void; + /** + * Clears storage of all items. + */ + clear: () => void; + /** + * Returns the number of items in storage. + */ + getLength: (key: string) => number; +} +declare const StorageService: IStorageService; +export default StorageService; diff --git a/lib/service/StorageService.js b/lib/service/StorageService.js new file mode 100644 index 00000000..751a396f --- /dev/null +++ b/lib/service/StorageService.js @@ -0,0 +1,51 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +/** + * Interface to define a service for data storage. + */ +var StorageService = { + setItem: function setItem(key, value) { + return sessionStorage.setItem(key, value); + }, + getItem: function getItem(key) { + return sessionStorage.getItem(key); + }, + setObject: function setObject(key, object) { + if (object) { + sessionStorage.setItem(key, JSON.stringify(object)); + } + }, + getObject: function getObject(key) { + var value = sessionStorage.getItem(key); + + if (!value) { + return null; + } + + try { + return JSON.parse(value); + } catch (e) { + return null; + } + }, + getKey: function getKey(index) { + return sessionStorage.key(index); + }, + remove: function remove(key) { + return sessionStorage.removeItem(key); + }, + clear: function clear() { + return sessionStorage.clear(); + }, + getLength: function getLength() { + return sessionStorage.length; + } +}; +Object.freeze(StorageService); +var _default = StorageService; +exports.default = _default; \ No newline at end of file diff --git a/lib/sharedState/SharedState.js b/lib/sharedState/SharedState.js new file mode 100644 index 00000000..8e3a4d7b --- /dev/null +++ b/lib/sharedState/SharedState.js @@ -0,0 +1,56 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _react = require("react"); + +var _operators = require("rxjs/operators"); + +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr && (typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]); if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +/** + * A function to use a distributed shared state for sharing state between components outside + * of the component heirarchy. The subject would be defined in an + * external module and then this function can be imported to incorporate the shared + * state into a component. + * @param subject A subject. + * @returns The value and function to set the state. + */ +var useSharedState = function useSharedState(subject) { + var _useState = (0, _react.useState)(subject.getValue()), + _useState2 = _slicedToArray(_useState, 2), + value = _useState2[0], + setState = _useState2[1]; + + (0, _react.useEffect)(function () { + var subscription = subject.pipe((0, _operators.skip)(1)).subscribe(function (s) { + return setState(s); + }); + return function () { + return subscription.unsubscribe(); + }; + }, [subject]); + + var setDistributedState = function setDistributedState(state) { + return subject.next(state); + }; // @ts-ignore + + + return [value, setDistributedState]; +}; + +var _default = useSharedState; +exports.default = _default; \ No newline at end of file diff --git a/lib/types/neon.d.ts b/lib/types/neon.d.ts new file mode 100644 index 00000000..ef5c51b7 --- /dev/null +++ b/lib/types/neon.d.ts @@ -0,0 +1,6 @@ +export interface NeonDocument { + name: string; + type: string; + size: number; + description: string; +} diff --git a/lib/types/neon.js b/lib/types/neon.js new file mode 100644 index 00000000..9a390c31 --- /dev/null +++ b/lib/types/neon.js @@ -0,0 +1 @@ +"use strict"; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b9095560..d4d229d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "portal-core-components", - "version": "1.8.0", + "version": "1.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.8.0", + "version": "1.9.0", "dependencies": { "@date-io/moment": "^1.3.9", "@material-ui/core": "^4.11.3", @@ -15,6 +15,7 @@ "@material-ui/styles": "^4.10.0", "@stomp/rx-stomp": "^0.3.5", "@stomp/stompjs": "^5.4.4", + "@types/lodash": "^4.14.172", "@types/react-router": "^5.1.8", "@types/react-router-dom": "^5.1.5", "@types/sockjs-client": "^1.1.1", @@ -3890,6 +3891,11 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.172", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==" + }, "node_modules/@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", @@ -28879,6 +28885,11 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/lodash": { + "version": "4.14.172", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==" + }, "@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", diff --git a/package.json b/package.json index 84dc48b4..5717e220 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "portal-core-components", - "version": "1.8.0", + "version": "1.9.0", "main": "./lib/index.js", "private": true, "homepage": "http://localhost:3010/core-components", @@ -13,6 +13,7 @@ "@material-ui/styles": "^4.10.0", "@stomp/rx-stomp": "^0.3.5", "@stomp/stompjs": "^5.4.4", + "@types/lodash": "^4.14.172", "@types/react-router": "^5.1.8", "@types/react-router-dom": "^5.1.5", "@types/sockjs-client": "^1.1.1", @@ -94,7 +95,8 @@ "lib:clean-build": "npm run lib:clean && npm run lib", "lib:types": "npx tsc --project tsconfig.d.json", "lib:pre-process": "node ./scripts/lib/lib-cache-remote-assets.js", - "lib:post-cleanup": "rm ./lib/components/**/StyleGuide.* && rm -rf ./lib/components/SiteMap/png && rm -rf ./lib/*/__tests__ && rm -rf ./lib/*/*/__tests__ && node ./scripts/lib/lib-fix-worker-babel.js" + "lib:post-cleanup": "rm ./lib/components/**/StyleGuide.* && rm -rf ./lib/components/SiteMap/png && rm -rf ./lib/*/__tests__ && rm -rf ./lib/*/*/__tests__ && node ./scripts/lib/lib-fix-worker-babel.js", + "checks": "npm run lint && npm run test && npm run lib && npm run build" }, "browserslist": [ ">0.2%", diff --git a/src/App.jsx b/src/App.jsx index f010e45c..e5a90b50 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -11,6 +11,7 @@ import BasicComponents from './components/BasicComponents'; import AopDataViewerStyleGuide from './lib_components/components/AopDataViewer/StyleGuide'; import DataProductAvailabilityStyleGuide from './lib_components/components/DataProductAvailability/StyleGuide'; import DataThemeIconStyleGuide from './lib_components/components/DataThemeIcon/StyleGuide'; +import DocumentViewerStyleGuide from './lib_components/components/DocumentViewer/StyleGuide'; import DownloadDataButtonStyleGuide from './lib_components/components/DownloadDataButton/StyleGuide'; import DownloadDataContextStyleGuide from './lib_components/components/DownloadDataContext/StyleGuide'; import ExternalHostInfoStyleGuide from './lib_components/components/ExternalHostInfo/StyleGuide'; @@ -56,6 +57,11 @@ const sidebarLinks = [ hash: '#DataThemeIcon', component: DataThemeIconStyleGuide, }, + { + name: 'Document Viewer', + hash: '#DocumentViewer', + component: DocumentViewerStyleGuide, + }, { name: 'Download Data Button', hash: '#DownloadDataButton', diff --git a/src/lib_components/components/DataProductAvailability/AvailabilityContext.jsx b/src/lib_components/components/DataProductAvailability/AvailabilityContext.jsx index 29ec8050..f7ee30d3 100644 --- a/src/lib_components/components/DataProductAvailability/AvailabilityContext.jsx +++ b/src/lib_components/components/DataProductAvailability/AvailabilityContext.jsx @@ -9,9 +9,13 @@ import PropTypes from 'prop-types'; import cloneDeep from 'lodash/cloneDeep'; import NeonContext from '../NeonContext/NeonContext'; +import NeonEnvironment from '../NeonEnvironment/NeonEnvironment'; import { AvailabilityPropTypes } from './AvailabilityUtils'; +import NeonSignInButtonState from '../NeonSignInButton/NeonSignInButtonState'; +import makeStateStorage from '../../service/StateStorageService'; + const SORT_DIRECTIONS = { ASC: 'ASC', DESC: 'DESC' }; const DEFAULT_STATE = { sites: [], @@ -195,26 +199,65 @@ const useAvailabilityState = () => { return hookResponse; }; +/** + * Defines a lookup of state key to a boolean + * designating whether or not that instance of the context + * should pull the state from the session storage and restore. + * Keeping this lookup outside of the context provider function + * as to not incur lifecycle interference by storing with useState. + */ +const restoreStateLookup = {}; + /** Context Provider */ const Provider = (props) => { - const { sites, children } = props; + const { sites, dataAvailabilityUniqueId, children } = props; const [ { data: neonContextData, isFinal: neonContextIsFinal, hasError: neonContextHasError }, ] = NeonContext.useNeonContextState(); + const key = `availabilityContextState-${dataAvailabilityUniqueId}`; + if (typeof restoreStateLookup[key] === 'undefined') { + restoreStateLookup[key] = true; + } + const shouldRestoreState = restoreStateLookup[key]; + const stateStorage = makeStateStorage(key); + const savedState = stateStorage.readState(); + /** Initial State and Reducer Setup */ let initialState = { ...cloneDeep(DEFAULT_STATE), sites }; initialState.tables = extractTables(initialState); - if (neonContextIsFinal && !neonContextHasError) { + if (neonContextIsFinal && !neonContextHasError && !savedState) { initialState = hydrateNeonContextData(initialState, neonContextData); } + + if (savedState && shouldRestoreState) { + restoreStateLookup[key] = false; + stateStorage.removeState(); + initialState = savedState; + } + const [state, dispatch] = useReducer(reducer, calculateRows(initialState)); + // The current sign in process uses a separate domain. This function + // persists the current state in storage when the button is clicked + // so the state may be reloaded when the page is reloaded after sign + // in. + useEffect(() => { + const subscription = NeonSignInButtonState.getObservable().subscribe({ + next: () => { + if (!NeonEnvironment.enableGlobalSignInState) return; + restoreStateLookup[key] = false; + stateStorage.saveState(state); + }, + }); + return () => { subscription.unsubscribe(); }; + }, [state, stateStorage, key]); + /** Effect - Watch for changes to NeonContext data and push into local state */ @@ -241,6 +284,7 @@ const Provider = (props) => { }; Provider.propTypes = { + dataAvailabilityUniqueId: PropTypes.number, sites: AvailabilityPropTypes.enhancedSites, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.oneOfType([ @@ -253,6 +297,7 @@ Provider.propTypes = { }; Provider.defaultProps = { + dataAvailabilityUniqueId: 0, sites: [], }; diff --git a/src/lib_components/components/DataProductAvailability/StyleGuide.jsx b/src/lib_components/components/DataProductAvailability/StyleGuide.jsx index 0f8214da..60da1855 100644 --- a/src/lib_components/components/DataProductAvailability/StyleGuide.jsx +++ b/src/lib_components/components/DataProductAvailability/StyleGuide.jsx @@ -268,7 +268,7 @@ const dataProducts = [...]; site and date range selection. - + @@ -325,6 +325,7 @@ const productData = { @@ -358,6 +359,7 @@ const productData = {...}; productData={sampleProductData.data} sites={sites} dateRange={dateRange} + downloadDataContextUniqueId={2} >
@@ -393,7 +395,7 @@ const dateRange: ['2018-01', '2018-12']; This can be achieved with the disableSelection boolean prop. - + diff --git a/src/lib_components/components/DocumentViewer/DocumentViewer.tsx b/src/lib_components/components/DocumentViewer/DocumentViewer.tsx new file mode 100644 index 00000000..4b6541d9 --- /dev/null +++ b/src/lib_components/components/DocumentViewer/DocumentViewer.tsx @@ -0,0 +1,119 @@ +import React, { + useCallback, + useRef, + useLayoutEffect, + useState, +} from 'react'; + +import { + makeStyles, + createStyles, + Theme as MuiTheme, +} from '@material-ui/core/styles'; + +import NeonEnvironment from '../NeonEnvironment'; +import Theme from '../Theme/Theme'; +import { StylesHook } from '../../types/muiTypes'; +import { NeonDocument } from '../../types/neon'; + +const useStyles: StylesHook = makeStyles((muiTheme: MuiTheme) => + // eslint-disable-next-line implicit-arrow-linebreak + createStyles({ + container: { + width: '100%', + margin: muiTheme.spacing(3, 3, 3, 3), + }, + })) as StylesHook; + +export interface DocumentViewerProps { + document: NeonDocument; + width: number; +} + +const noop = () => {}; + +const breakpoints: number[] = [0, 675, 900, 1200]; +const ratios: string[] = ['8:11', '3:4', '3:4', '3:4']; + +const calcAutoHeight = (width: number): number => { + const breakIdx: number = breakpoints.reduce( + (acc, breakpoint, idx) => (width >= breakpoint ? idx : acc), 0, + ); + const ratio: RegExpExecArray|null = /^([\d.]+):([\d.]+)$/.exec(ratios[breakIdx]); + let mult: number = 4 / 3; + if (ratio) { + mult = (parseFloat(ratio[2]) || 1) / (parseFloat(ratio[1]) || 1); + } + return Math.floor(width * mult); +}; + +const DocumentViewer: React.FC = (props: DocumentViewerProps): JSX.Element => { + const classes = useStyles(Theme); + const { + document, + width, + }: DocumentViewerProps = props; + + const containerRef: React.MutableRefObject = useRef(); + const embedRef: React.MutableRefObject = useRef(); + const [ + viewerWidth, + setViewerWidth, + ]: [number, React.Dispatch>] = useState(width); + + const handleResizeCb = useCallback((): void => { + const container: HTMLDivElement|undefined = containerRef.current; + const embed: HTMLEmbedElement|undefined = embedRef.current; + // Do nothing if either container or viz references fail ot point to a DOM node + if (!container || !embed) { return; } + // Do nothing if either refs have no offset parent + // (meaning they're hidden from rendering anyway) + if ((container.offsetParent === null) || (embed.offsetParent === null)) { return; } + // Do nothing if container and viz have the same width + // (resize event fired but no actual resize necessary) + if (container.clientWidth === viewerWidth) { return; } + const newWidth: number = container.clientWidth; + setViewerWidth(newWidth); + embed.setAttribute('width', `${newWidth}`); + embed.setAttribute('height', `${calcAutoHeight(newWidth)}`); + }, [containerRef, embedRef, viewerWidth, setViewerWidth]); + + useLayoutEffect(() => { + const element = embedRef.current; + if (!element) { return noop; } + const parent: HTMLElement|null = element.parentElement; + if (!parent) { return noop; } + handleResizeCb(); + if (typeof ResizeObserver !== 'function') { + window.addEventListener('resize', handleResizeCb); + return () => { + window.removeEventListener('resize', handleResizeCb); + }; + } + let resizeObserver: ResizeObserver|null = new ResizeObserver(handleResizeCb); + resizeObserver.observe(parent); + return () => { + if (!resizeObserver) { return; } + resizeObserver.disconnect(); + resizeObserver = null; + }; + }, [embedRef, handleResizeCb]); + + return ( +
} + className={classes.container} + > + } + type={document.type} + src={`${NeonEnvironment.getFullApiPath('documents')}/${document.name}?inline=true`} + title={document.description} + width={viewerWidth} + height={calcAutoHeight(viewerWidth)} + /> +
+ ); +}; + +export default DocumentViewer; diff --git a/src/lib_components/components/DocumentViewer/StyleGuide.tsx b/src/lib_components/components/DocumentViewer/StyleGuide.tsx new file mode 100644 index 00000000..3c64eebb --- /dev/null +++ b/src/lib_components/components/DocumentViewer/StyleGuide.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import Divider from '@material-ui/core/Divider'; +import Typography from '@material-ui/core/Typography'; +import { makeStyles } from '@material-ui/core/styles'; + +import CodeBlock from '../../../components/CodeBlock'; +import DocBlock from '../../../components/DocBlock'; +import ExampleBlock from '../../../components/ExampleBlock'; + +import DocumentViewer from './DocumentViewer'; +import Theme from '../Theme/Theme'; +import { NeonDocument } from '../../types/neon'; + +const useStyles = makeStyles((theme) => ({ + divider: { + margin: theme.spacing(3, 0), + }, +})); + +export default function StyleGuide() { + const classes = useStyles(Theme); + const exampleDoc: NeonDocument = { + name: 'NEON.DOC.000780vB.pdf', + type: 'application/pdf', + size: 993762, + description: 'NEON Algorithm Theoretical Basis Document (ATBD) – 2D Wind Speed and Direction', + }; + return ( + <> + + A module for displaying documents inline. + + + {` +import DocumentViewer from 'portal-core-components/lib/components/DocumentViewer'; + `} + + + + Example Document Viewer + + Displays a single embedded document. + + + {` +import DocumentViewer from 'portal-core-components/lib/components/DocumentViewer'; + +const exampleDoc: NeonDocument = { + name: 'NEON.DOC.000780vB.pdf', + type: 'application/pdf', + size: 993762, + description: 'NEON Algorithm Theoretical Basis Document (ATBD) – 2D Wind Speed and Direction', +}; + + + `} + + + + + + ); +} diff --git a/src/lib_components/components/DocumentViewer/index.d.ts b/src/lib_components/components/DocumentViewer/index.d.ts new file mode 100644 index 00000000..4855051f --- /dev/null +++ b/src/lib_components/components/DocumentViewer/index.d.ts @@ -0,0 +1 @@ +export { default } from './DocumentViewer'; diff --git a/src/lib_components/components/DocumentViewer/index.js b/src/lib_components/components/DocumentViewer/index.js new file mode 100644 index 00000000..4855051f --- /dev/null +++ b/src/lib_components/components/DocumentViewer/index.js @@ -0,0 +1 @@ +export { default } from './DocumentViewer'; diff --git a/src/lib_components/components/DocumentViewer/package.json b/src/lib_components/components/DocumentViewer/package.json new file mode 100644 index 00000000..e6d1de5a --- /dev/null +++ b/src/lib_components/components/DocumentViewer/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "name": "document-viewer", + "main": "./DocumentViewer.tsx", + "module": "./DocumentViewer.tsx" +} diff --git a/src/lib_components/components/DownloadDataButton/StyleGuide.jsx b/src/lib_components/components/DownloadDataButton/StyleGuide.jsx index e8c6bda0..4eb8add7 100644 --- a/src/lib_components/components/DownloadDataButton/StyleGuide.jsx +++ b/src/lib_components/components/DownloadDataButton/StyleGuide.jsx @@ -20,7 +20,6 @@ import sampleProductDataAeronet from '../../../sampleData/DP1.00043.001.json'; import sampleProductDataAop from '../../../sampleData/DP1.30010.001.json'; import sampleProductDataAopOSPipeline from '../../../sampleData/DP1.30012.001.json'; import sampleProductDataBold from '../../../sampleData/DP1.20105.001.json'; -import sampleProductDataMGRast from '../../../sampleData/DP1.10107.001.json'; import sampleProductDataNPN from '../../../sampleData/DP1.10055.001.json'; import sampleProductDataPhenocam from '../../../sampleData/DP1.00033.001.json'; @@ -148,10 +147,6 @@ const productData = {...};


- - - -

diff --git a/src/lib_components/components/DownloadDataContext/DownloadDataContext.jsx b/src/lib_components/components/DownloadDataContext/DownloadDataContext.jsx index 2f0769dc..e62a6b6b 100644 --- a/src/lib_components/components/DownloadDataContext/DownloadDataContext.jsx +++ b/src/lib_components/components/DownloadDataContext/DownloadDataContext.jsx @@ -33,15 +33,14 @@ import { } from '../../util/manifestUtil'; import { forkJoinWithProgress } from '../../util/rxUtil'; +import makeStateStorage from '../../service/StateStorageService'; +import NeonSignInButtonState from '../NeonSignInButton/NeonSignInButtonState'; +// eslint-disable-next-line import/no-cycle +import { convertStateForStorage, convertAOPInitialState } from './StateStorageConverter'; -const ALL_POSSIBLE_VALID_DATE_RANGE = [ - '2010-01', - moment().format('YYYY-MM'), -]; - +const ALL_POSSIBLE_VALID_DATE_RANGE = ['2010-01', moment().format('YYYY-MM')]; const ALL_POSSIBLE_VALID_DOCUMENTATION = ['include', 'exclude']; const ALL_POSSIBLE_VALID_PACKAGE_TYPE = ['basic', 'expanded']; - const AVAILABILITY_VIEW_MODES = ['summary', 'sites', 'states', 'domains']; const ALL_STEPS = { @@ -192,9 +191,7 @@ const S3_PATTERN = { }, }; -/** - VALIDATOR FUNCTIONS -*/ +// VALIDATOR FUNCTIONS // Naive check, replace with a more robust JSON schema check const productDataIsValid = (productData) => ( typeof productData === 'object' && productData !== null @@ -315,11 +312,9 @@ const mutateNewStateIntoRange = (key, value, validValues = []) => { } }; -/** - Estimate a POST body size from a sile list and sites list for s3Files-based - downloads. Numbers here are based on the current POST API and what it requires - for form data keys, which is excessively verbose. -*/ +// Estimate a POST body size from a sile list and sites list for s3Files-based +// downloads. Numbers here are based on the current POST API and what it requires +// for form data keys, which is excessively verbose. const estimatePostSize = (s3FilesState, sitesState) => { const baseLength = 300; const sitesLength = sitesState.value.length * 62; @@ -328,9 +323,7 @@ const estimatePostSize = (s3FilesState, sitesState) => { return baseLength + sitesLength + filesLength; }; -/** - GETTER FUNCTIONS -*/ +// GETTER FUNCTIONS const getValidValuesFromProductData = (productData, key) => { switch (key) { case 'release': @@ -733,9 +726,7 @@ const getAndValidateNewState = (previousState, action, broadcast = false) => { return newState; }; -/** - REDUCER -*/ +// REDUCER const reducer = (state, action) => { let newState = {}; const getStateFromHigherOrderState = (newHigherOrderState) => HIGHER_ORDER_TRANSFERABLE_STATE_KEYS @@ -934,20 +925,19 @@ const reducer = (state, action) => { return state; } }; -const wrappedReducer = (state, action) => { - const newState = reducer(state, action); - // console.log('ACTION', action, newState); - return newState; -}; /** - CONTEXT -*/ + * Wrapped reducer function + * @param {*} state The state. + * @param {*} action An action. + * @returns the new state. + */ +const wrappedReducer = (state, action) => reducer(state, action); + +// CONTEXT const Context = createContext(DEFAULT_STATE); -/** - HOOK -*/ +// HOOK const useDownloadDataState = () => { const hookResponse = useContext(Context); if (hookResponse.length !== 2) { @@ -957,19 +947,16 @@ const useDownloadDataState = () => { requiredSteps: [], downloadContextIsActive: false, }, - () => { }, + () => {}, ]; } return hookResponse; }; -/** - OBSERVABLES -*/ +// OBSERVABLES // Observable and getter for sharing whole state through a higher order component const stateSubject$ = new Subject(); const getStateObservable = () => stateSubject$.asObservable(); - // Observables and getters for making and canceling manifest requests const manifestCancelation$ = new Subject(); const getManifestAjaxObservable = (request) => ( @@ -977,17 +964,58 @@ const getManifestAjaxObservable = (request) => ( ); /** - -*/ + * Defines a lookup of state key to a boolean + * designating whether or not that instance of the context + * should pull the state from the session storage and restore. + * Keeping this lookup outside of the context provider function + * as to not incur lifecycle interference by storing with useState. + */ +const restoreStateLookup = {}; + +// Provider const Provider = (props) => { const { + downloadDataContextUniqueId, stateObservable, children, } = props; - const initialState = getInitialStateFromProps(props); + // get the initial state from storage if present, else get from props. + let initialState = getInitialStateFromProps(props); + const { productCode: product } = initialState.productData; + const stateKey = `downloadDataContextState-${product}-${downloadDataContextUniqueId}`; + if (typeof restoreStateLookup[stateKey] === 'undefined') { + restoreStateLookup[stateKey] = true; + } + const shouldRestoreState = restoreStateLookup[stateKey]; + const stateStorage = makeStateStorage(stateKey); + const savedState = stateStorage.readState(); + if (savedState && shouldRestoreState) { + restoreStateLookup[stateKey] = false; + stateStorage.removeState(); + initialState = convertAOPInitialState(savedState, initialState); + } const [state, dispatch] = useReducer(wrappedReducer, initialState); + const { downloadContextIsActive, dialogOpen } = state; + + // The current sign in process uses a separate domain. This function + // persists the current state in storage when the button is clicked + // so the state may be reloaded when the page is reloaded after sign + // in. + useEffect(() => { + const subscription = NeonSignInButtonState.getObservable().subscribe({ + next: () => { + if (!downloadContextIsActive || !dialogOpen) return; + restoreStateLookup[stateKey] = false; + stateStorage.saveState(convertStateForStorage(state)); + }, + }); + return () => { + subscription.unsubscribe(); + }; + }, [downloadContextIsActive, dialogOpen, state, stateKey, stateStorage]); + // Create an observable for manifests requests and subscribe to it to execute // the manifest fetch and dispatch results when updated. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -1123,6 +1151,7 @@ const Provider = (props) => { }; Provider.propTypes = { + downloadDataContextUniqueId: PropTypes.number, stateObservable: PropTypes.func, productData: PropTypes.shape({ productCode: PropTypes.string.isRequired, @@ -1153,6 +1182,7 @@ Provider.propTypes = { }; Provider.defaultProps = { + downloadDataContextUniqueId: 0, stateObservable: null, productData: {}, availabilityView: DEFAULT_STATE.availabilityView, diff --git a/src/lib_components/components/DownloadDataContext/StateStorageConverter.ts b/src/lib_components/components/DownloadDataContext/StateStorageConverter.ts new file mode 100644 index 00000000..63159907 --- /dev/null +++ b/src/lib_components/components/DownloadDataContext/StateStorageConverter.ts @@ -0,0 +1,43 @@ +// eslint-disable-next-line import/no-cycle +import DownloadDataContext from './DownloadDataContext'; + +/** + * Alter the current state for valid JSON serialization. Set objects + * must be converted to Array objects for serialization. + * @param currentState The current state + */ +const convertStateForStorage = (state: any): any => { + if (state.fromAOPManifest) { + // AOP S3 Files will incur too much data to be saved in session state + // Restore to default state in terms of s3Files and selection state. + return { + ...state, + s3FileFetches: { ...DownloadDataContext.DEFAULT_STATE.s3FileFetches }, + s3FileFetchProgress: DownloadDataContext.DEFAULT_STATE.s3FileFetchProgress, + s3Files: { ...DownloadDataContext.DEFAULT_STATE.s3Files }, + manifest: { ...DownloadDataContext.DEFAULT_STATE.manifest }, + allStepsComplete: DownloadDataContext.DEFAULT_STATE.allStepsComplete, + sites: { + ...state.sites, + value: [...DownloadDataContext.DEFAULT_STATE.sites.value], + }, + }; + } + return state; +}; + +const convertAOPInitialState = (state: any, propsState: any): any => { + if (!state.fromAOPManifest) return state; + return { + ...state, + s3FileFetches: { ...propsState.s3FileFetches }, + s3FileFetchProgress: propsState.s3FileFetchProgress, + s3Files: { ...propsState.s3Files }, + manifest: { ...propsState.manifest }, + allStepsComplete: propsState.allStepsComplete, + policies: { ...propsState.policies }, + }; +}; + +// eslint-disable-next-line import/prefer-default-export +export { convertStateForStorage, convertAOPInitialState }; diff --git a/src/lib_components/components/DownloadDataContext/StyleGuide.jsx b/src/lib_components/components/DownloadDataContext/StyleGuide.jsx index 0e39ff44..29b29fa5 100644 --- a/src/lib_components/components/DownloadDataContext/StyleGuide.jsx +++ b/src/lib_components/components/DownloadDataContext/StyleGuide.jsx @@ -148,6 +148,7 @@ const MyAppComponent = () => {

higherOrderSubject.asObservable()} > @@ -183,20 +184,6 @@ const MyAppComponent = () => { export default function StyleGuide() { const classes = useStyles(Theme); - const downloadDataButtonLink = ( - - Download Data Button - - ); - const dataProductAvailabilityLink = ( - - Data Product Availability - - ); const useReducerLink = ( siteCodes array of site availability objects. -
- {/* - + - */} {` @@ -332,6 +319,7 @@ const productData = { @@ -381,10 +370,9 @@ const productData = {...}; configuration state, out of which individual state values can be destructured. -
- {/*
@@ -393,7 +381,6 @@ const productData = {...};
- */} {` @@ -436,10 +423,9 @@ const ListSitesComponent = () => { appropriate, new data to apply. -
- {/*
@@ -448,7 +434,6 @@ const ListSitesComponent = () => {
- */} {` @@ -568,10 +553,7 @@ const AlphaSitesComponent = () => { to the higher order component. -
- {/* - */} {` diff --git a/src/lib_components/components/DownloadDataDialog/DownloadDataDialog.jsx b/src/lib_components/components/DownloadDataDialog/DownloadDataDialog.jsx index 7d8f0b95..8cde2b34 100644 --- a/src/lib_components/components/DownloadDataDialog/DownloadDataDialog.jsx +++ b/src/lib_components/components/DownloadDataDialog/DownloadDataDialog.jsx @@ -37,8 +37,8 @@ import DataThemeIcon from '../DataThemeIcon/DataThemeIcon'; import ExternalHost from '../ExternalHost/ExternalHost'; import ExternalHostInfo from '../ExternalHostInfo/ExternalHostInfo'; import NeonContext from '../NeonContext/NeonContext'; -import NeonEnvironment from '../NeonEnvironment/NeonEnvironment'; import Theme, { COLORS } from '../Theme/Theme'; +import NeonSignInButton from '../NeonSignInButton/NeonSignInButton'; import RouteService from '../../service/RouteService'; import { @@ -410,12 +410,6 @@ export default function DownloadDataDialog() { const renderAuthSuggestion = () => { if (isAuthenticated) { return null; } - const signInLink = ( - signing in - ); - const benefitsLink = ( - here - ); /* eslint-disable react/jsx-one-expression-per-line */ const authStyles = { color: COLORS.GOLD[800], textAlign: 'right', whiteSpace: 'nowrap' }; return ( @@ -428,7 +422,7 @@ export default function DownloadDataDialog() { fontWeight: 600, }} > - Have an account? Consider {signInLink} before proceeding. + Consider signing in or creating an account before proceeding. - Learn more about the benefits of signing in {benefitsLink}. + Learn the benefits of having an account. + ); /* eslint-enable react/jsx-one-expression-per-line */ @@ -466,8 +461,8 @@ export default function DownloadDataDialog() { {showDownloadButton ? renderDownloadButton() : null}
{showDownloadButton && !allStepsComplete ? ( - - Complete all steps to enable download + + Complete all steps to enable download. ) : null} {renderAuthSuggestion()} diff --git a/src/lib_components/components/ExternalHost/ExternalHost.jsx b/src/lib_components/components/ExternalHost/ExternalHost.jsx index fb4b118e..48c47c4f 100644 --- a/src/lib_components/components/ExternalHost/ExternalHost.jsx +++ b/src/lib_components/components/ExternalHost/ExternalHost.jsx @@ -111,17 +111,6 @@ const externalProducts = { { query: 'MAMPR', title: 'Small mammal sequences DNA barcode (Prototype Data)' }, ], }, - 'DP1.10107.001': { - host: 'MGRAST', - projects: [ - { id: 'mgp13948', title: 'NEON Soil Metagenomes' }, - { id: 'mgp3546', title: 'NEON Soils (Prototype Data)' }, - ], - }, - 'DP1.10108.001': { - host: 'MGRAST', - projects: [], // unable to find associated project(s) - }, 'DP1.20002.001': { host: 'PHENOCAM', }, @@ -131,36 +120,6 @@ const externalProducts = { { query: 'FSHN', title: 'Fish sequences DNA barcode' }, ], }, - 'DP1.20126.001': { - host: 'MGRAST', - projects: [ - { id: 'mgp84670', title: 'NEON Macroinvertebrate DNA Barcodes - 2017' }, - ], - }, - 'DP1.20279.001': { - host: 'MGRAST', - projects: [], // unable to find associated project(s) - }, - 'DP1.20280.001': { - host: 'MGRAST', - projects: [], // unable to find associated project(s) - }, - 'DP1.20281.001': { - host: 'MGRAST', - projects: [], // unable to find associated project(s) - }, - 'DP1.20282.001': { - host: 'MGRAST', - projects: [ - { id: 'mgp84669', title: 'NEON Surface Water Microbe Marker Gene Sequences - 2014' }, - ], - }, - 'DP1.20221.001': { - host: 'MGRAST', - projects: [ - { id: 'mgp84672', title: 'NEON Zooplankton DNA Barcodes - 2017' }, - ], - }, 'DP4.00002.001': { host: 'AMERIFLUX', }, @@ -308,27 +267,6 @@ const externalHosts = { })); }, }, - MGRAST: { - id: 'MGRAST', - name: 'MG-RAST', - projectTitle: 'MG-RAST (Metagenomics Rapid Annotation using Subsystem Technology)', - url: 'https://mg-rast.org', - hostType: HOST_TYPES.ADDITIONAL_DATA, - hostDataVariety: 'Raw sequence data', - linkType: LINK_TYPES.BY_PRODUCT, - getProductLinks: (productCode = '') => { - if (!externalProducts[productCode]) { return []; } - return externalProducts[productCode].projects.map((project) => ({ - key: project.id, - node: renderExternalHostLink( - `https://www.mg-rast.org/mgmain.html?mgpage=project&project=${project.id}`, - project.title, - 'MGRAST', - productCode, - ), - })); - }, - }, NPN: { id: 'NPN', name: 'USA-NPN', diff --git a/src/lib_components/components/ExternalHost/__tests__/ExternalHost.jsx b/src/lib_components/components/ExternalHost/__tests__/ExternalHost.jsx index 7979fa28..3255e9d4 100644 --- a/src/lib_components/components/ExternalHost/__tests__/ExternalHost.jsx +++ b/src/lib_components/components/ExternalHost/__tests__/ExternalHost.jsx @@ -206,54 +206,6 @@ describe('ExternalHost', () => { }); }); - /** - MGRAST - */ - describe('MGRAST', () => { - test('getProductLinks with no arg', () => { - const tree = renderer - .create(getByHostId('MGRAST').getProductLinks()) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - test('getProductLinks with a valid productCode', () => { - const tree = renderer - .create(( -
- {getByHostId('MGRAST').getProductLinks('DP1.10107.001').map((link) => ( -
{link.node}
- ))} -
- )) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - test('renderLink without productCode', () => { - const tree = renderer - .create(getByHostId('MGRAST').renderLink()) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - test('renderLink with productCode', () => { - const tree = renderer - .create(getByHostId('MGRAST').renderLink('DP1.10107.001')) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - test('renderShortLink without productCode', () => { - const tree = renderer - .create(getByHostId('MGRAST').renderShortLink()) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - test('renderShortLink with productCode', () => { - const tree = renderer - .create(getByHostId('MGRAST').renderShortLink('DP1.10107.001')) - .toJSON(); - expect(tree).toMatchSnapshot(); - }); - }); - /** NPN */ diff --git a/src/lib_components/components/ExternalHost/__tests__/__snapshots__/ExternalHost.jsx.snap b/src/lib_components/components/ExternalHost/__tests__/__snapshots__/ExternalHost.jsx.snap index 24e1466f..36bb2ad2 100644 --- a/src/lib_components/components/ExternalHost/__tests__/__snapshots__/ExternalHost.jsx.snap +++ b/src/lib_components/components/ExternalHost/__tests__/__snapshots__/ExternalHost.jsx.snap @@ -292,101 +292,6 @@ exports[`ExternalHost BOLD renderShortLink without productCode 1`] = ` `; -exports[`ExternalHost MGRAST getProductLinks with a valid productCode 1`] = ` - -`; - -exports[`ExternalHost MGRAST getProductLinks with no arg 1`] = `null`; - -exports[`ExternalHost MGRAST renderLink with productCode 1`] = ` - - MG-RAST (Metagenomics Rapid Annotation using Subsystem Technology) - -`; - -exports[`ExternalHost MGRAST renderLink without productCode 1`] = ` - - MG-RAST (Metagenomics Rapid Annotation using Subsystem Technology) - -`; - -exports[`ExternalHost MGRAST renderShortLink with productCode 1`] = ` - - MG-RAST - -`; - -exports[`ExternalHost MGRAST renderShortLink without productCode 1`] = ` - - MG-RAST - -`; - exports[`ExternalHost NPN renderLink with productCode 1`] = ` +
{` + `} diff --git a/src/lib_components/components/ExternalHostProductSpecificLinks/__tests__/__snapshots__/ExternalHostProductSpecificLinks.jsx.snap b/src/lib_components/components/ExternalHostProductSpecificLinks/__tests__/__snapshots__/ExternalHostProductSpecificLinks.jsx.snap index 02e314d7..49bb1240 100644 --- a/src/lib_components/components/ExternalHostProductSpecificLinks/__tests__/__snapshots__/ExternalHostProductSpecificLinks.jsx.snap +++ b/src/lib_components/components/ExternalHostProductSpecificLinks/__tests__/__snapshots__/ExternalHostProductSpecificLinks.jsx.snap @@ -443,7 +443,7 @@ exports[`ExternalHostProductSpecificLinks renders with valid product code and no rel="noopener noreferrer" target="_blank" > - KONA - - Konza Prairie Biological Station - Relocatable + KONA - - Konza Prairie Biological Station - Gradient
  • @@ -1804,7 +1804,7 @@ exports[`ExternalHostProductSpecificLinks renders with valid product code and no rel="noopener noreferrer" target="_blank" > - KONA - Konza Prairie Biological Station - Relocatable + KONA - Konza Prairie Biological Station - Gradient
  • @@ -3157,7 +3157,7 @@ exports[`ExternalHostProductSpecificLinks renders with valid product code and si rel="noopener noreferrer" target="_blank" > - KONA - - Konza Prairie Biological Station - Relocatable + KONA - - Konza Prairie Biological Station - Gradient
  • @@ -4518,7 +4518,7 @@ exports[`ExternalHostProductSpecificLinks renders with valid product code and si rel="noopener noreferrer" target="_blank" > - KONA - Konza Prairie Biological Station - Relocatable + KONA - Konza Prairie Biological Station - Gradient
  • diff --git a/src/lib_components/components/NeonAuth/AuthService.ts b/src/lib_components/components/NeonAuth/AuthService.ts index 9be9a347..88f98f8c 100644 --- a/src/lib_components/components/NeonAuth/AuthService.ts +++ b/src/lib_components/components/NeonAuth/AuthService.ts @@ -17,7 +17,7 @@ import NeonEnvironment, { INeonEnvironment } from '../NeonEnvironment/NeonEnviro import BrowserService from '../../util/browserUtil'; import { getJson } from '../../util/rxUtil'; -import { exists } from '../../util/typeUtil'; +import { exists, isStringNonEmpty } from '../../util/typeUtil'; import { AnyVoidFunc, Undef, AuthSilentType } from '../../types/core'; const REDIRECT_URI: string = 'redirectUri'; @@ -67,6 +67,11 @@ export interface IAuthService { * @return {boolean} */ isAuthOnlyApp: () => boolean; + /** + * Gets the redirect URI to send to the login endpoint. + * @return {Undef} + */ + getLoginRedirectUri: () => Undef; /** * Initializes a login flow * @param {string} path - Optionally path to set for the root login URL @@ -290,6 +295,12 @@ const AuthService: IAuthService = { NeonEnvironment.route.account(), ].indexOf(NeonEnvironment.getRouterBaseHomePath() || '') >= 0 ), + getLoginRedirectUri: (): Undef => { + const appHomePath: string = NeonEnvironment.getRouterBaseHomePath(); + const currentPath: string = window.location.pathname; + const hasPath: boolean = isStringNonEmpty(currentPath) && currentPath.includes(appHomePath); + return hasPath ? currentPath : undefined; + }, login: (path?: string, redirectUriPath?: string): void => { const env: INeonEnvironment = NeonEnvironment; const rootPath: string = exists(path) diff --git a/src/lib_components/components/NeonAuth/NeonAuth.tsx b/src/lib_components/components/NeonAuth/NeonAuth.tsx index ea85b965..68fade40 100644 --- a/src/lib_components/components/NeonAuth/NeonAuth.tsx +++ b/src/lib_components/components/NeonAuth/NeonAuth.tsx @@ -9,6 +9,7 @@ import AuthService, { LOGOUT_REDIRECT_PATHS } from './AuthService'; import NeonContext, { FETCH_STATUS } from '../NeonContext/NeonContext'; import NeonEnvironment from '../NeonEnvironment/NeonEnvironment'; import Theme from '../Theme/Theme'; +import NeonSignInButtonState from '../NeonSignInButton/NeonSignInButtonState'; import { StringPropsObject } from '../../types/objectTypes'; import { StylesHook } from '../../types/muiTypes'; @@ -88,15 +89,16 @@ const renderAuth = ( }: NeonAuthProps = props; const handleLogin = (): void => { + if (NeonEnvironment.enableGlobalSignInState) { + // Notify observers the sign in button has been clicked. + NeonSignInButtonState.sendNotification(); + } let appliedLoginType: NeonAuthType = loginType; // Default to redirect if WS isn't connected if (!isAuthWsConnected) { appliedLoginType = NeonAuthType.REDIRECT; } - const appHomePath: string = NeonEnvironment.getRouterBaseHomePath(); - const currentPath: string = window.location.pathname; - const hasPath: boolean = isStringNonEmpty(currentPath) && currentPath.includes(appHomePath); - const redirectUriPath: Undef = hasPath ? currentPath : undefined; + const redirectUriPath: Undef = AuthService.getLoginRedirectUri(); switch (appliedLoginType) { case NeonAuthType.SILENT: AuthService.loginSilently(dispatch, false, loginPath, redirectUriPath); diff --git a/src/lib_components/components/NeonEnvironment/NeonEnvironment.ts b/src/lib_components/components/NeonEnvironment/NeonEnvironment.ts index 8610e945..ed0e891b 100644 --- a/src/lib_components/components/NeonEnvironment/NeonEnvironment.ts +++ b/src/lib_components/components/NeonEnvironment/NeonEnvironment.ts @@ -74,6 +74,7 @@ export interface INeonEnvironment { useGraphql: boolean; showAopViewer: boolean; authDisableWs: boolean; + enableGlobalSignInState: boolean; getRootApiPath: () => string; getRootGraphqlPath: () => string; @@ -138,6 +139,7 @@ const NeonEnvironment: INeonEnvironment = { useGraphql: process.env.REACT_APP_NEON_USE_GRAPHQL === 'true', showAopViewer: process.env.REACT_APP_NEON_SHOW_AOP_VIEWER === 'true', authDisableWs: process.env.REACT_APP_NEON_AUTH_DISABLE_WS === 'true', + enableGlobalSignInState: process.env.REACT_APP_NEON_ENABLE_GLOBAL_SIGNIN_STATE === 'true', getRootApiPath: () => process.env.REACT_APP_NEON_PATH_API || '/api/v0', getRootGraphqlPath: () => process.env.REACT_APP_NEON_PATH_PUBLIC_GRAPHQL || '/graphql', diff --git a/src/lib_components/components/NeonGraphQL/NeonGraphQL.js b/src/lib_components/components/NeonGraphQL/NeonGraphQL.js index d8fdd0bf..f6031098 100644 --- a/src/lib_components/components/NeonGraphQL/NeonGraphQL.js +++ b/src/lib_components/components/NeonGraphQL/NeonGraphQL.js @@ -3,6 +3,7 @@ import { ajax } from 'rxjs/ajax'; import NeonEnvironment from '../NeonEnvironment/NeonEnvironment'; import NeonApi from '../NeonApi/NeonApi'; +import { isStringNonEmpty } from '../../util/typeUtil'; export const TYPES = { DATA_PRODUCTS: 'DATA_PRODUCTS', @@ -17,6 +18,19 @@ export const DIMENSIONALITIES = { const transformQuery = (query) => JSON.stringify({ query }); +const getAvailableReleaseClause = (args) => { + if (!args) return ''; + const hasRelease = isStringNonEmpty(args.release); + let availableReleases = ''; + if ((args.includeAvailableReleases === true) && !hasRelease) { + availableReleases = `availableReleases { + release + availableMonths + }`; + } + return availableReleases; +}; + const getQueryBody = (type = '', dimensionality = '', args = {}) => { let query = ''; switch (type) { @@ -24,6 +38,7 @@ const getQueryBody = (type = '', dimensionality = '', args = {}) => { if (dimensionality === DIMENSIONALITIES.ONE) { // TODO: Add support for deeper product data when querying for one const releaseArgument = !args.release ? '' : `, release: "${args.release}"`; + const availableReleases = getAvailableReleaseClause(args); query = `query Products { product (productCode: "${args.productCode}"${releaseArgument}) { productCode @@ -39,6 +54,7 @@ const getQueryBody = (type = '', dimensionality = '', args = {}) => { siteCodes { siteCode availableMonths + ${availableReleases} } releases { release @@ -49,6 +65,7 @@ const getQueryBody = (type = '', dimensionality = '', args = {}) => { }`; } else { const releaseArgument = !args.release ? '' : `(release: "${args.release}")`; + const availableReleases = getAvailableReleaseClause(args); query = `query Products { products ${releaseArgument}{ productCode @@ -64,6 +81,7 @@ const getQueryBody = (type = '', dimensionality = '', args = {}) => { siteCodes { siteCode availableMonths + ${availableReleases} } releases { release @@ -132,7 +150,7 @@ const getQueryBody = (type = '', dimensionality = '', args = {}) => { } else { query = `query findLocations { locations: findLocations( - query: { + query: { locationNames: ${JSON.stringify(args.locationNames)} } ) { @@ -209,10 +227,10 @@ const NeonGraphQL = { DIMENSIONALITIES.ONE, { productCode, release }, ), - getAllDataProducts: (release) => getObservableWith( + getAllDataProducts: (release, includeAvailableReleases = false) => getObservableWith( TYPES.DATA_PRODUCTS, DIMENSIONALITIES.MANY, - { release }, + { release, includeAvailableReleases }, ), getSiteByCode: (siteCode) => getObservableWith( TYPES.SITES, diff --git a/src/lib_components/components/NeonSignInButton/NeonSignInButton.tsx b/src/lib_components/components/NeonSignInButton/NeonSignInButton.tsx new file mode 100644 index 00000000..e453ff36 --- /dev/null +++ b/src/lib_components/components/NeonSignInButton/NeonSignInButton.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; + +import AuthService from '../NeonAuth/AuthService'; +import NeonEnvironment from '../NeonEnvironment/NeonEnvironment'; +import NeonSignInButtonState from './NeonSignInButtonState'; + +const useStyles = makeStyles((theme) => ({ + signInButton: { + margin: theme.spacing(2), + }, +})); + +const handleButtonClick = () => { + // Notify observers the sign in button has been clicked. + NeonSignInButtonState.sendNotification(); + AuthService.login( + NeonEnvironment.getFullAuthPath('login'), + AuthService.getLoginRedirectUri(), + ); +}; + +export default function NeonSignInButton() { + const classes = useStyles(); + return ( + + ); +} diff --git a/src/lib_components/components/NeonSignInButton/NeonSignInButtonState.ts b/src/lib_components/components/NeonSignInButton/NeonSignInButtonState.ts new file mode 100644 index 00000000..b63b8e85 --- /dev/null +++ b/src/lib_components/components/NeonSignInButton/NeonSignInButtonState.ts @@ -0,0 +1,35 @@ +import { Subject, Observable } from 'rxjs'; + +// Instantiate the subject and observable. +const subject = new Subject(); +const observable = subject.asObservable(); + +/** + * Interface for sharing sign in button state. + */ +export interface INeonSignInButtonState { + /** + * Get the subject. + * @returns the subject. + */ + getSubject: () => Subject; + /** + * Tell all observers the button has been clicked. + * @returns void + */ + sendNotification: () => void; + /** + * Get the observable. + * @returns the observable. + */ + getObservable: () => Observable; +} + +const NeonSignInButtonState: INeonSignInButtonState = { + getSubject: () => subject, + sendNotification: () => subject.next(), + getObservable: () => observable, +}; + +Object.freeze(NeonSignInButtonState); +export default NeonSignInButtonState; diff --git a/src/lib_components/components/SiteMap/SiteMap.css b/src/lib_components/components/SiteMap/SiteMap.css index aa5ff600..c6475438 100644 --- a/src/lib_components/components/SiteMap/SiteMap.css +++ b/src/lib_components/components/SiteMap/SiteMap.css @@ -1,38 +1,38 @@ /* This file is for defining global map icon CSS classes for the Neon Site Map */ /* We don't do just these with makeStyles because we want consistent class names */ -div[data-component='SiteMap'] { - .mapIcon: { - box-sizing: content-box; - } - .mapIconPLACEHOLDER: { - border-radius: 20%; - } - .mapIconCORE: { - border-radius: 20%; - } - .mapIconRELOCATABLE: { - border-radius: 50%; - } - /* THIS DOESN'T WORK FOR NON-SQUARE ICONS! */ - .mapIconSQUARE { - box-shadow: 0px 0px 5px -0.5px rgba(0,0,0,0.5); - } - .mapIconUnselected: { - box-shadow: none; - &:hover, &:focus: { - box-shadow: 0px 0px 5px 5px #0073cf; - } - &:active: { - box-shadow: 0px 0px 8px 8px #0073cf; - } - } - .mapIconSelected: { - box-shadow: none; - &:hover, &:focus: { - box-shadow: 0px 0px 3px 3px #ffffff; - } - &:active: { - box-shadow: 0px 0px 6px 6px #ffffff; - } - } +div[data-component='SiteMap'] .mapIcon { + box-sizing: content-box; +} +div[data-component='SiteMap'] .mapIconPLACEHOLDER { + border-radius: 20%; +} +div[data-component='SiteMap'] .mapIconCORE { + border-radius: 20%; +} +div[data-component='SiteMap'] .mapIconGRADIENT { + border-radius: 50%; +} +/* THIS DOESN'T WORK FOR NON-SQUARE ICONS! */ +div[data-component='SiteMap'] .mapIconSQUARE { + box-shadow: 0px 0px 5px -0.5px rgba(0,0,0,0.5); +} +div[data-component='SiteMap'] .mapIconUnselected { + box-shadow: none; +} +div[data-component='SiteMap'] .mapIconUnselected:hover, +div[data-component='SiteMap'] .mapIconUnselected:focus { + box-shadow: 0px 0px 5px 5px #0073cf; +} +div[data-component='SiteMap'] .mapIconUnselected:active { + box-shadow: 0px 0px 8px 8px #0073cf; +} +div[data-component='SiteMap'] .mapIconSelected { + box-shadow: none; +} +div[data-component='SiteMap'] .mapIconSelected:hover, +div[data-component='SiteMap'] .mapIconSelected:focus { + box-shadow: 0px 0px 3px 3px #ffffff; +} +div[data-component='SiteMap'] .mapIconSelected:active { + box-shadow: 0px 0px 6px 6px #ffffff; } diff --git a/src/lib_components/components/SiteMap/SiteMap.jsx b/src/lib_components/components/SiteMap/SiteMap.jsx index 7b9cd263..950b6bb3 100644 --- a/src/lib_components/components/SiteMap/SiteMap.jsx +++ b/src/lib_components/components/SiteMap/SiteMap.jsx @@ -8,10 +8,11 @@ import SiteMapContainer from './SiteMapContainer'; import { SITE_MAP_PROP_TYPES, SITE_MAP_DEFAULT_PROPS } from './SiteMapUtils'; const SiteMap = (props) => { - const { unusableVerticalSpace = 0 } = props; // no need to store this in state, just pass it thru + // no need to store this in state, just pass it thru + const { unusableVerticalSpace = 0, mapUniqueId = 0 } = props; return ( - + ); }; diff --git a/src/lib_components/components/SiteMap/SiteMapContainer.jsx b/src/lib_components/components/SiteMap/SiteMapContainer.jsx index 732f0e8d..a7290034 100644 --- a/src/lib_components/components/SiteMap/SiteMapContainer.jsx +++ b/src/lib_components/components/SiteMap/SiteMapContainer.jsx @@ -313,7 +313,7 @@ const useStyles = makeStyles((theme) => ({ const SiteMapContainer = (props) => { const classes = useStyles(Theme); - const { unusableVerticalSpace = 0 } = props; + const { unusableVerticalSpace = 0, mapUniqueId } = props; const [neonContextState] = NeonContext.useNeonContextState(); @@ -500,6 +500,7 @@ const SiteMapContainer = (props) => { className: classes.outerContainer, 'aria-busy': isLoading ? 'true' : 'false', 'data-selenium': 'siteMap-container', + id: mapUniqueId, }; /** @@ -1292,10 +1293,12 @@ const SiteMapContainer = (props) => { SiteMapContainer.propTypes = { unusableVerticalSpace: PropTypes.number, + mapUniqueId: PropTypes.number, }; SiteMapContainer.defaultProps = { unusableVerticalSpace: 0, + mapUniqueId: 0, }; export default SiteMapContainer; diff --git a/src/lib_components/components/SiteMap/SiteMapContext.jsx b/src/lib_components/components/SiteMap/SiteMapContext.jsx index 2172688d..6dcc08c1 100644 --- a/src/lib_components/components/SiteMap/SiteMapContext.jsx +++ b/src/lib_components/components/SiteMap/SiteMapContext.jsx @@ -14,6 +14,10 @@ import { map, catchError } from 'rxjs/operators'; import NeonApi from '../NeonApi/NeonApi'; import NeonContext from '../NeonContext/NeonContext'; +import NeonEnvironment from '../NeonEnvironment/NeonEnvironment'; +import NeonSignInButtonState from '../NeonSignInButton/NeonSignInButtonState'; +import makeStateStorage from '../../service/StateStorageService'; +import { convertStateForStorage, convertStateFromStorage } from './StateStorageConverter'; import { fetchManyLocationsGraphQL, @@ -119,7 +123,7 @@ const validateSelection = (state) => { valid = true; if ( (Number.isFinite(limit) && set.size !== limit) - || (Array.isArray(limit) && (set.size < limit[0] || set.size > limit[1])) + || (Array.isArray(limit) && (set.size < limit[0] || set.size > limit[1])) ) { valid = false; } } return { @@ -146,8 +150,8 @@ const zoomIsValid = (zoom) => ( ); const centerIsValid = (center) => ( Array.isArray(center) - && center.length === 2 - && center.every((v) => (typeof v === 'number' && !Number.isNaN(v))) + && center.length === 2 + && center.every((v) => (typeof v === 'number' && !Number.isNaN(v))) ); // Creates fetch objects with an AWAITING_CALL status based on current state. @@ -161,7 +165,7 @@ const calculateFeatureDataFetches = (state, requiredSites = []) => { // manualLocationData is non-empty and contains prototpye sites const fetchablePrototypeSites = (state.manualLocationData || []).filter((manualLocation) => ( manualLocation.manualLocationType === MANUAL_LOCATION_TYPES.PROTOTYPE_SITE - && state.sites[manualLocation.siteCode] + && state.sites[manualLocation.siteCode] )); let sitesToConsider = state.sites; if (state.manualLocationData && fetchablePrototypeSites.length) { @@ -207,8 +211,8 @@ const calculateFeatureDataFetches = (state, requiredSites = []) => { Object.keys(FEATURES) .filter((featureKey) => ( FEATURES[featureKey].dataSource === FEATURE_DATA_SOURCES.ARCGIS_ASSETS_API - && state.filters.features.available[featureKey] - && state.filters.features.visible[featureKey] + && state.filters.features.available[featureKey] + && state.filters.features.visible[featureKey] )) .forEach((featureKey) => { const { dataSource } = FEATURES[featureKey]; @@ -225,9 +229,9 @@ const calculateFeatureDataFetches = (state, requiredSites = []) => { // Only look at available+visible features that get fetched and have a location type match .filter((featureKey) => ( FEATURES[featureKey].dataSource === FEATURE_DATA_SOURCES.REST_LOCATIONS_API - && FEATURES[featureKey].matchLocationType - && state.filters.features.available[featureKey] - && state.filters.features.visible[featureKey] + && FEATURES[featureKey].matchLocationType + && state.filters.features.available[featureKey] + && state.filters.features.visible[featureKey] )) .forEach((featureKey) => { const { dataSource, matchLocationType } = FEATURES[featureKey]; @@ -325,7 +329,7 @@ const calculateFeatureDataFetches = (state, requiredSites = []) => { newState.featureDataFetches[dataSource][minZoom][siteCode].features[featureKey].fetchId = awaitingFetchKey; if ( companionMinZoom && companionFeatureKey - && !newState.featureDataFetches[dataSource][companionMinZoom][siteCode].features[companionFeatureKey].fetchId + && !newState.featureDataFetches[dataSource][companionMinZoom][siteCode].features[companionFeatureKey].fetchId ) { newState.featureDataFetches[dataSource][companionMinZoom][siteCode].features[companionFeatureKey].fetchId = awaitingFetchKey; } @@ -361,7 +365,7 @@ const updateMapTileWithZoom = (state) => { const newState = { ...state }; if ( newState.map.zoom <= 17 && state.map.baseLayer !== BASE_LAYERS.NATGEO_WORLD_MAP.KEY - && state.map.baseLayerAutoChangedAbove17) { + && state.map.baseLayerAutoChangedAbove17) { newState.map.baseLayer = BASE_LAYERS.NATGEO_WORLD_MAP.KEY; newState.map.baseLayerAutoChangedAbove17 = false; } else if (newState.map.zoom >= 17 && state.map.baseLayer === BASE_LAYERS.NATGEO_WORLD_MAP.KEY) { @@ -446,8 +450,8 @@ const setFetchStatusFromAction = (state, action, status) => { if (!FEATURES[featureKey]) { return newState; } if ( !newState.featureDataFetches[dataSource] - || !newState.featureDataFetches[dataSource][featureKey] - || !newState.featureDataFetches[dataSource][featureKey][siteCode] + || !newState.featureDataFetches[dataSource][featureKey] + || !newState.featureDataFetches[dataSource][featureKey][siteCode] ) { return newState; } newState.featureDataFetches[dataSource][featureKey][siteCode] = status; // If the status is SUCCESS and the action has data, also commit the data @@ -470,9 +474,9 @@ const setFetchStatusFromAction = (state, action, status) => { const { type: featureType } = FEATURES[featureKey]; if ( !newState.featureDataFetches[dataSource] - || !newState.featureDataFetches[dataSource][featureKey] - || !newState.featureDataFetches[dataSource][featureKey][siteCode] - || !newState.featureDataFetches[dataSource][featureKey][siteCode][location] + || !newState.featureDataFetches[dataSource][featureKey] + || !newState.featureDataFetches[dataSource][featureKey][siteCode] + || !newState.featureDataFetches[dataSource][featureKey][siteCode][location] ) { return newState; } @@ -504,9 +508,9 @@ const setFetchStatusFromAction = (state, action, status) => { } = action; if ( !newState.featureDataFetches[dataSource] - || !newState.featureDataFetches[dataSource][minZoom] - || !newState.featureDataFetches[dataSource][minZoom][siteCode] - || !newState.featureDataFetches[dataSource][minZoom][siteCode].fetches[fetchId] + || !newState.featureDataFetches[dataSource][minZoom] + || !newState.featureDataFetches[dataSource][minZoom][siteCode] + || !newState.featureDataFetches[dataSource][minZoom][siteCode].fetches[fetchId] ) { return newState; } newState.featureDataFetches[dataSource][minZoom][siteCode].fetches[fetchId].status = status; // If the status is SUCCESS and the action has data, also commit the data @@ -574,7 +578,7 @@ const setFetchStatusFromAction = (state, action, status) => { // Geometry may be loaded by another sub-location so look for that and don't blow it away! const geometry = ( !newState.featureData[featureType][featureKey][siteCode][locName] - || !newState.featureData[featureType][featureKey][siteCode][locName].geometry + || !newState.featureData[featureType][featureKey][siteCode][locName].geometry ) ? null : { ...newState.featureData[featureType][featureKey][siteCode][locName].geometry }; // eslint-disable-line max-len newState.featureData[featureType][featureKey][siteCode][locName] = { ...data[locName], @@ -854,7 +858,7 @@ const reducer = (state, action) => { case 'setDomainLocationHierarchyFetchSucceeded': if ( !newState.featureDataFetches[hierarchiesSource][hierarchiesType][action.domainCode] - || !action.data + || !action.data ) { return state; } /* eslint-disable max-len */ newState.featureDataFetches[hierarchiesSource][hierarchiesType][action.domainCode] = FETCH_STATUS.SUCCESS; @@ -899,7 +903,7 @@ const reducer = (state, action) => { case 'updateSelectionSet': if ( !action.selection || !action.selection.constructor - || action.selection.constructor.name !== 'Set' + || action.selection.constructor.name !== 'Set' ) { return state; } newState.selection.set = getSelectableSet(action.selection, validSet); newState.selection.changed = true; @@ -974,26 +978,32 @@ const reducer = (state, action) => { } }; -/** - Context and Hook -*/ +/** Context and Hook */ const Context = createContext(DEFAULT_STATE); const useSiteMapContext = () => { const hookResponse = useContext(Context); if (hookResponse.length !== 2) { - return [cloneDeep(DEFAULT_STATE), () => {}]; + return [cloneDeep(DEFAULT_STATE), () => { }]; } return hookResponse; }; /** - Context Provider -*/ + * Defines a lookup of state key to a boolean + * designating whether or not that instance of the context + * should pull the state from the session storage and restore. + * Keeping this lookup outside of the context provider function + * as to not incur lifecycle interference by storing with useState. + */ +const restoreStateLookup = {}; + +/** Context Provider */ const Provider = (props) => { const { view, aspectRatio, fullscreen, + mapUniqueId, mapZoom, mapCenter, mapBaseLayer, @@ -1068,25 +1078,58 @@ const Provider = (props) => { if (Array.isArray(manualLocationData) && manualLocationData.length > 0) { initialState.manualLocationData = manualLocationData; } - if (neonContextIsFinal && !neonContextHasError) { + + // get the initial state from storage if present + const stateKey = `siteMapContextState-${mapUniqueId}`; + if (typeof restoreStateLookup[stateKey] === 'undefined') { + restoreStateLookup[stateKey] = true; + } + const shouldRestoreState = restoreStateLookup[stateKey]; + const stateStorage = makeStateStorage(stateKey); + const savedState = stateStorage.readState(); + + if (neonContextIsFinal && !neonContextHasError && !savedState) { initialState = hydrateNeonContextData(initialState, neonContextData); } const hasInitialZoom = (typeof mapZoom === 'number') && zoomIsValid(mapZoom); - if (hasInitialZoom) { + if (hasInitialZoom && !savedState) { initialState = calculateZoomState(initialMapZoom, initialState, true); } + + if (savedState && shouldRestoreState) { + restoreStateLookup[stateKey] = false; + const restoredState = convertStateFromStorage(savedState, initialState); + stateStorage.removeState(); + initialState = calculateZoomState(restoredState.map.zoom, restoredState, true); + } + const [state, dispatch] = useReducer(reducer, initialState); + // The current sign in process uses a separate domain. This function + // persists the current state in storage when the button is clicked + // so the state may be reloaded when the page is reloaded after sign + // in. + useEffect(() => { + const subscription = NeonSignInButtonState.getObservable().subscribe({ + next: () => { + if (!NeonEnvironment.enableGlobalSignInState) return; + restoreStateLookup[stateKey] = false; + stateStorage.saveState(convertStateForStorage(state)); + }, + }); + return () => { subscription.unsubscribe(); }; + }, [state, stateStorage, stateKey]); + const canFetchFeatureData = ( state.neonContextHydrated - && !(state.focusLocation.current && state.focusLocation.fetch.status !== FETCH_STATUS.SUCCESS) + && !(state.focusLocation.current && state.focusLocation.fetch.status !== FETCH_STATUS.SUCCESS) ); /** Effect - trigger focusLocation fetch or short circuit if found in NeonContext or manual data */ useEffect(() => { - const noop = () => {}; + const noop = () => { }; const { current, fetch: { status: currentStatus } } = state.focusLocation; if (!current || currentStatus !== FETCH_STATUS.AWAITING_CALL || !state.neonContextHydrated) { return noop; diff --git a/src/lib_components/components/SiteMap/SiteMapFeature.jsx b/src/lib_components/components/SiteMap/SiteMapFeature.jsx index 63fbe4e7..f53bd1b4 100644 --- a/src/lib_components/components/SiteMap/SiteMapFeature.jsx +++ b/src/lib_components/components/SiteMap/SiteMapFeature.jsx @@ -1222,7 +1222,7 @@ const SiteMapFeature = (props) => { AQUATIC_METEOROLOGICAL_STATIONS: renderLocationPopup, AQUATIC_PLANT_TRANSECTS: renderLocationPopup, AQUATIC_REACHES: renderBoundaryPopup, - AQUATIC_RELOCATABLE_SITES: renderSitePopup, + AQUATIC_GRADIENT_SITES: renderSitePopup, AQUATIC_RIPARIAN_ASSESSMENTS: renderLocationPopup, AQUATIC_SEDIMENT_POINTS: renderLocationPopup, AQUATIC_SENSOR_STATIONS: renderLocationPopup, @@ -1310,7 +1310,7 @@ const SiteMapFeature = (props) => { ); }, TERRESTRIAL_CORE_SITES: renderSitePopup, - TERRESTRIAL_RELOCATABLE_SITES: renderSitePopup, + TERRESTRIAL_GRADIENT_SITES: renderSitePopup, TOWER_AIRSHEDS: renderBoundaryPopup, TOWER_BASE_PLOTS: (siteCode, location) => renderLocationPopup(siteCode, location, [ renderPlotSizeAndSlope, diff --git a/src/lib_components/components/SiteMap/SiteMapUtils.js b/src/lib_components/components/SiteMap/SiteMapUtils.js index 5e0af590..99381e54 100644 --- a/src/lib_components/components/SiteMap/SiteMapUtils.js +++ b/src/lib_components/components/SiteMap/SiteMapUtils.js @@ -28,10 +28,10 @@ import iconSiteCoreTerrestrialSVG from './svg/icon-site-core-terrestrial.svg'; import iconSiteCoreTerrestrialSelectedSVG from './svg/icon-site-core-terrestrial-selected.svg'; import iconSiteCoreAquaticSVG from './svg/icon-site-core-aquatic.svg'; import iconSiteCoreAquaticSelectedSVG from './svg/icon-site-core-aquatic-selected.svg'; -import iconSiteRelocatableTerrestrialSVG from './svg/icon-site-relocatable-terrestrial.svg'; -import iconSiteRelocatableTerrestrialSelectedSVG from './svg/icon-site-relocatable-terrestrial-selected.svg'; -import iconSiteRelocatableAquaticSVG from './svg/icon-site-relocatable-aquatic.svg'; -import iconSiteRelocatableAquaticSelectedSVG from './svg/icon-site-relocatable-aquatic-selected.svg'; +import iconSiteGradientTerrestrialSVG from './svg/icon-site-gradient-terrestrial.svg'; +import iconSiteGradientTerrestrialSelectedSVG from './svg/icon-site-gradient-terrestrial-selected.svg'; +import iconSiteGradientAquaticSVG from './svg/icon-site-gradient-aquatic.svg'; +import iconSiteGradientAquaticSelectedSVG from './svg/icon-site-gradient-aquatic-selected.svg'; import iconSiteDecommissionedSVG from './svg/icon-site-decommissioned.svg'; import iconBenchmarkSVG from './svg/icon-benchmark.svg'; @@ -1030,19 +1030,19 @@ export const FEATURES = { iconShape: LOCATION_ICON_SVG_SHAPES.SQUARE.KEY, maxZoom: 9, }, - TERRESTRIAL_RELOCATABLE_SITES: { - name: 'Terrestrial Relocatable Sites', - nameSingular: 'Terrestrial Relocatable Site', + TERRESTRIAL_GRADIENT_SITES: { + name: 'Terrestrial Gradient Sites', + nameSingular: 'Terrestrial Gradient Site', type: FEATURE_TYPES.SITES.KEY, - description: 'Land-based; location may change', + description: 'Land-based; gradient location', parent: 'SITE_MARKERS', - attributes: { type: 'RELOCATABLE', terrain: 'TERRESTRIAL' }, + attributes: { type: 'GRADIENT', terrain: 'TERRESTRIAL' }, dataSource: FEATURE_DATA_SOURCES.NEON_CONTEXT, primaryIdOnly: true, featureShape: 'Marker', iconScale: 1, - iconSvg: iconSiteRelocatableTerrestrialSVG, - iconSelectedSvg: iconSiteRelocatableTerrestrialSelectedSVG, + iconSvg: iconSiteGradientTerrestrialSVG, + iconSelectedSvg: iconSiteGradientTerrestrialSelectedSVG, iconShape: LOCATION_ICON_SVG_SHAPES.CIRCLE.KEY, maxZoom: 9, }, @@ -1062,19 +1062,19 @@ export const FEATURES = { iconShape: LOCATION_ICON_SVG_SHAPES.SQUARE.KEY, maxZoom: 9, }, - AQUATIC_RELOCATABLE_SITES: { - name: 'Aquatic Relocatable Sites', - nameSingular: 'Aquatic Relocatable Site', + AQUATIC_GRADIENT_SITES: { + name: 'Aquatic Gradient Sites', + nameSingular: 'Aquatic Gradient Site', type: FEATURE_TYPES.SITES.KEY, - description: 'Water-based; location may change', + description: 'Water-based; gradient location', parent: 'SITE_MARKERS', - attributes: { type: 'RELOCATABLE', terrain: 'AQUATIC' }, + attributes: { type: 'GRADIENT', terrain: 'AQUATIC' }, dataSource: FEATURE_DATA_SOURCES.NEON_CONTEXT, primaryIdOnly: true, featureShape: 'Marker', iconScale: 1, - iconSvg: iconSiteRelocatableAquaticSVG, - iconSelectedSvg: iconSiteRelocatableAquaticSelectedSVG, + iconSvg: iconSiteGradientAquaticSVG, + iconSelectedSvg: iconSiteGradientAquaticSelectedSVG, iconShape: LOCATION_ICON_SVG_SHAPES.CIRCLE.KEY, maxZoom: 9, }, @@ -1608,6 +1608,7 @@ export const SITE_MAP_PROP_TYPES = { aspectRatio: PropTypes.number, fullscreen: PropTypes.bool, unusableVerticalSpace: PropTypes.number, + mapUniqueId: PropTypes.number, // Map props mapCenter: PropTypes.arrayOf(PropTypes.number), mapZoom: PropTypes.number, @@ -1637,6 +1638,7 @@ export const SITE_MAP_DEFAULT_PROPS = { aspectRatio: null, fullscreen: false, unusableVerticalSpace: 0, + mapUniqueId: 0, // Map props mapCenter: OBSERVATORY_CENTER, mapZoom: null, diff --git a/src/lib_components/components/SiteMap/StateStorageConverter.ts b/src/lib_components/components/SiteMap/StateStorageConverter.ts new file mode 100644 index 00000000..775eed7f --- /dev/null +++ b/src/lib_components/components/SiteMap/StateStorageConverter.ts @@ -0,0 +1,86 @@ +import cloneDeep from 'lodash/cloneDeep'; + +/** + * Alter the current state for valid JSON serialization. Set objects + * must be converted to Array objects for serialization. + * @param currentState The current state + */ +const convertStateForStorage = (state: any): any => { + const selectionSet = state.selection?.set; + const selectionValidSet = state.selection?.validSet; + const filtersFeaturesCollapsed = state.filters?.features?.collapsed; + const filtersOverlaysExpanded = state.filters?.overlays?.expanded; + const mapOverlays = state.map?.overlays; + const newState = cloneDeep(state); + if (selectionSet instanceof Set) { + newState.selection.set = Array.from(selectionSet); + } else { + newState.selection.set = []; + } + if (selectionValidSet instanceof Set) { + newState.selection.validSet = Array.from(selectionValidSet); + } else { + newState.selection.validSet = []; + } + if (filtersFeaturesCollapsed instanceof Set) { + newState.filters.features.collapsed = Array.from(filtersFeaturesCollapsed); + } else { + newState.filters.features.collapsed = []; + } + if (filtersOverlaysExpanded instanceof Set) { + newState.filters.overlays.expanded = Array.from(filtersOverlaysExpanded); + } else { + newState.filters.overlays.expanded = []; + } + if (filtersOverlaysExpanded instanceof Set) { + newState.map.overlays = Array.from(mapOverlays); + } else { + newState.map.overlays = []; + } + return newState; +}; + +/** + * Restore the state from JSON serialization. Array objects must be + * converted back to the expected Set objects. + * @param storedState The state read from storage. + */ +const convertStateFromStorage = (state: any, initialState: any): any => { + const newState = cloneDeep(state); + newState.view = initialState.view; + newState.map.zoomedIcons = initialState.map.zoomedIcons; + newState.selection.onChange = initialState.selection.onChange; + const setValue = state.selection?.set; + const validSet = state.selection?.validSet; + const collapsedValue = state.filters?.features?.collapsed; + const expandedValue = state.filters?.overlays?.expanded; + const mapOverlays = state.map?.overlays; + if (Array.isArray(setValue)) { + newState.selection.set = new Set(setValue); + } else { + newState.selection.set = new Set(); + } + if (Array.isArray(validSet)) { + newState.selection.validSet = new Set(validSet); + } else { + newState.selection.validSet = new Set(); + } + if (Array.isArray(collapsedValue)) { + newState.filters.features.collapsed = new Set(collapsedValue); + } else { + newState.filters.features.collapsed = new Set(); + } + if (Array.isArray(expandedValue)) { + newState.filters.overlays.expanded = new Set(expandedValue); + } else { + newState.filters.overlays.expanded = new Set(); + } + if (Array.isArray(mapOverlays)) { + newState.map.overlays = new Set(mapOverlays); + } else { + newState.map.overlays = new Set(); + } + return newState; +}; + +export { convertStateForStorage, convertStateFromStorage }; diff --git a/src/lib_components/components/SiteMap/StyleGuide.jsx b/src/lib_components/components/SiteMap/StyleGuide.jsx index fe116e47..e1074a12 100644 --- a/src/lib_components/components/SiteMap/StyleGuide.jsx +++ b/src/lib_components/components/SiteMap/StyleGuide.jsx @@ -433,7 +433,7 @@ import SiteMap from 'portal-core-components/lib/components/SiteMap'; automatic sizing and aspect ratio based on the current viewport. - + {` @@ -451,7 +451,7 @@ import SiteMap from 'portal-core-components/lib/components/SiteMap'; Preset zoom value via the mapZoom property. - + {` @@ -471,7 +471,7 @@ import SiteMap from 'portal-core-components/lib/components/SiteMap'; for details and more varied examples. - + {` @@ -492,7 +492,7 @@ import SiteMap from 'portal-core-components/lib/components/SiteMap'; codes are also supported. - + {` @@ -513,7 +513,7 @@ import SiteMap from 'portal-core-components/lib/components/SiteMap'; sizes are also afforded. - + {` @@ -536,7 +536,7 @@ import SiteMap from 'portal-core-components/lib/components/SiteMap'; supported. - + {` @@ -589,7 +589,7 @@ return ( map will be visible in the table. - + {` diff --git a/src/lib_components/components/SiteMap/__tests__/SiteMapUtils.js b/src/lib_components/components/SiteMap/__tests__/SiteMapUtils.js index b9a6b49d..202e9ada 100644 --- a/src/lib_components/components/SiteMap/__tests__/SiteMapUtils.js +++ b/src/lib_components/components/SiteMap/__tests__/SiteMapUtils.js @@ -33,7 +33,7 @@ jest.mock('leaflet', () => ({ const neonContextData = { sites: { ABBY: { - type: 'RELOCATABLE', terrain: 'TERRESTRIAL', stateCode: 'WA', domainCode: 'D16', + type: 'GRADIENT', terrain: 'TERRESTRIAL', stateCode: 'WA', domainCode: 'D16', }, CLBJ: { type: 'CORE', terrain: 'TERRESTRIAL', stateCode: 'TX', domainCode: 'D11', @@ -42,7 +42,7 @@ const neonContextData = { type: 'CORE', terrain: 'AQUATIC', stateCode: 'FL', domainCode: 'D03', }, WLOU: { - type: 'RELOCATABLE', terrain: 'AQUATIC', stateCode: 'CO', domainCode: 'D13', + type: 'GRADIENT', terrain: 'AQUATIC', stateCode: 'CO', domainCode: 'D13', }, }, states: { @@ -309,8 +309,8 @@ describe('SiteMap - SiteMapUtils', () => { SITES: { TERRESTRIAL_CORE_SITES: {}, AQUATIC_CORE_SITES: {}, - TERRESTRIAL_RELOCATABLE_SITES: {}, - AQUATIC_RELOCATABLE_SITES: {}, + TERRESTRIAL_GRADIENT_SITES: {}, + AQUATIC_GRADIENT_SITES: {}, }, STATES: { STATES: {} }, DOMAINS: { DOMAINS: {} }, @@ -327,10 +327,10 @@ describe('SiteMap - SiteMapUtils', () => { AQUATIC_CORE_SITES: { SUGG: { ...neonContextData.sites.SUGG }, }, - TERRESTRIAL_RELOCATABLE_SITES: { + TERRESTRIAL_GRADIENT_SITES: { ABBY: { ...neonContextData.sites.ABBY }, }, - AQUATIC_RELOCATABLE_SITES: { + AQUATIC_GRADIENT_SITES: { WLOU: { ...neonContextData.sites.WLOU }, }, }, @@ -816,12 +816,12 @@ describe('SiteMap - SiteMapUtils', () => { }); }); test('Handles explicit highlight status', () => { - getZoomedIcon(FEATURES.TERRESTRIAL_RELOCATABLE_SITES.KEY, 7, HIGHLIGHT_STATUS.HIGHLIGHT); + getZoomedIcon(FEATURES.TERRESTRIAL_GRADIENT_SITES.KEY, 7, HIGHLIGHT_STATUS.HIGHLIGHT); expect(L.Icon.mock.calls.length).toBe(1); expect(L.Icon.mock.calls[0].length).toBe(1); expect(L.Icon.mock.calls[0][0]).toStrictEqual({ - iconUrl: 'icon-site-relocatable-terrestrial.svg', - iconRetinaUrl: 'icon-site-relocatable-terrestrial.svg', + iconUrl: 'icon-site-gradient-terrestrial.svg', + iconRetinaUrl: 'icon-site-gradient-terrestrial.svg', iconSize: [34, 34], iconAnchor: [17, 17], popupAnchor: [0, -17], @@ -832,7 +832,7 @@ describe('SiteMap - SiteMapUtils', () => { }); test('Handles explicit selection status', () => { getZoomedIcon( - FEATURES.AQUATIC_RELOCATABLE_SITES.KEY, + FEATURES.AQUATIC_GRADIENT_SITES.KEY, 15, HIGHLIGHT_STATUS.NONE, SELECTION_STATUS.SELECTED, @@ -840,8 +840,8 @@ describe('SiteMap - SiteMapUtils', () => { expect(L.Icon.mock.calls.length).toBe(1); expect(L.Icon.mock.calls[0].length).toBe(1); expect(L.Icon.mock.calls[0][0]).toStrictEqual({ - iconUrl: 'icon-site-relocatable-aquatic-selected.svg', - iconRetinaUrl: 'icon-site-relocatable-aquatic-selected.svg', + iconUrl: 'icon-site-gradient-aquatic-selected.svg', + iconRetinaUrl: 'icon-site-gradient-aquatic-selected.svg', iconSize: [79.75, 79.75], iconAnchor: [39.875, 39.875], popupAnchor: [0, -39.875], diff --git a/src/lib_components/components/SiteMap/png/icon-site-relocatable-aquatic.png b/src/lib_components/components/SiteMap/png/icon-site-gradient-aquatic.png similarity index 100% rename from src/lib_components/components/SiteMap/png/icon-site-relocatable-aquatic.png rename to src/lib_components/components/SiteMap/png/icon-site-gradient-aquatic.png diff --git a/src/lib_components/components/SiteMap/png/icon-site-relocatable-terrestrial.png b/src/lib_components/components/SiteMap/png/icon-site-gradient-terrestrial.png similarity index 100% rename from src/lib_components/components/SiteMap/png/icon-site-relocatable-terrestrial.png rename to src/lib_components/components/SiteMap/png/icon-site-gradient-terrestrial.png diff --git a/src/lib_components/components/SiteMap/svg/icon-site-gradient-aquatic-selected.svg b/src/lib_components/components/SiteMap/svg/icon-site-gradient-aquatic-selected.svg new file mode 100644 index 00000000..16f0c056 --- /dev/null +++ b/src/lib_components/components/SiteMap/svg/icon-site-gradient-aquatic-selected.svg @@ -0,0 +1,101 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/lib_components/components/SiteMap/svg/icon-site-gradient-aquatic.svg b/src/lib_components/components/SiteMap/svg/icon-site-gradient-aquatic.svg new file mode 100644 index 00000000..ef24e9ef --- /dev/null +++ b/src/lib_components/components/SiteMap/svg/icon-site-gradient-aquatic.svg @@ -0,0 +1,83 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/lib_components/components/SiteMap/svg/icon-site-gradient-terrestrial-selected.svg b/src/lib_components/components/SiteMap/svg/icon-site-gradient-terrestrial-selected.svg new file mode 100644 index 00000000..f8d5837c --- /dev/null +++ b/src/lib_components/components/SiteMap/svg/icon-site-gradient-terrestrial-selected.svg @@ -0,0 +1,101 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/lib_components/components/SiteMap/svg/icon-site-gradient-terrestrial.svg b/src/lib_components/components/SiteMap/svg/icon-site-gradient-terrestrial.svg new file mode 100644 index 00000000..b3a5880a --- /dev/null +++ b/src/lib_components/components/SiteMap/svg/icon-site-gradient-terrestrial.svg @@ -0,0 +1,83 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/lib_components/components/TimeSeriesViewer/StateStorageConverter.ts b/src/lib_components/components/TimeSeriesViewer/StateStorageConverter.ts new file mode 100644 index 00000000..1c962072 --- /dev/null +++ b/src/lib_components/components/TimeSeriesViewer/StateStorageConverter.ts @@ -0,0 +1,129 @@ +import cloneDeep from 'lodash/cloneDeep'; +import { TIME_SERIES_VIEWER_STATUS } from './constants'; + +/** + * Alter the current state for valid JSON serialization. + * @param currentState The current state + */ +const convertStateForStorage = (state: any): any => { + const newState = cloneDeep(state); + switch (newState.status) { + case TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT: + case TIME_SERIES_VIEWER_STATUS.LOADING_META: + case TIME_SERIES_VIEWER_STATUS.READY_FOR_DATA: + case TIME_SERIES_VIEWER_STATUS.LOADING_DATA: + case TIME_SERIES_VIEWER_STATUS.WARNING: + case TIME_SERIES_VIEWER_STATUS.ERROR: + newState.status = TIME_SERIES_VIEWER_STATUS.INIT_PRODUCT; + break; + case TIME_SERIES_VIEWER_STATUS.READY: + newState.status = TIME_SERIES_VIEWER_STATUS.READY_FOR_SERIES; + break; + default: + break; + } + // variables + const { variables: stateVariables } = state; + Object.keys(stateVariables).forEach((variableKey, index) => { + const { sites, tables, timeSteps } = stateVariables[variableKey]; + if (sites instanceof Set && sites.size > 0) { + newState.variables[variableKey].sites = Array.from(sites); + } else { + newState.variables[variableKey].sites = []; + } + if (tables instanceof Set && sites.size > 0) { + newState.variables[variableKey].tables = Array.from(tables); + } else { + newState.variables[variableKey].tables = []; + } + if (timeSteps instanceof Set && sites.size > 0) { + newState.variables[variableKey].timeSteps = Array.from(timeSteps); + } else { + newState.variables[variableKey].timeSteps = []; + } + }); + // product site variables + const { sites: productSites } = state.product; + Object.keys(productSites).forEach((siteKey, index) => { + const { variables: siteVariables } = productSites[siteKey]; + if (siteVariables instanceof Set && siteVariables.size > 0) { + newState.product.sites[siteKey].variables = Array.from(siteVariables); + } else { + newState.product.sites[siteKey].variables = []; + } + }); + // available quality flags + const { availableQualityFlags } = state; + if (availableQualityFlags instanceof Set) { + newState.availableQualityFlags = Array.from(availableQualityFlags); + } else { + newState.availableQualityFlags = []; + } + // available time steps + const { availableTimeSteps } = state; + if (availableTimeSteps instanceof Set) { + newState.availableTimeSteps = Array.from(availableTimeSteps); + } else { + newState.availableTimeSteps = []; + } + return newState; +}; + +/** + * Restore the state from JSON serialization. + * @param storedState The state read from storage. + */ +const convertStateFromStorage = (state: any): any => { + const newState = cloneDeep(state); + // graphData data + const data = state.graphData.data.map((entry: any) => [new Date(entry[0]), entry[1]]); + newState.graphData.data = data; + // state variables + const { variables } = state; + Object.keys(variables).forEach((key, index) => { + const { sites, tables, timeSteps } = variables[key]; + if (Array.isArray(sites)) { + newState.variables[key].sites = new Set(sites); + } else { + newState.variables[key].sites = new Set(); + } + if (Array.isArray(tables)) { + newState.variables[key].tables = new Set(tables); + } else { + newState.variables[key].tables = new Set(); + } + if (Array.isArray(timeSteps)) { + newState.variables[key].timeSteps = new Set(timeSteps); + } else { + newState.variables[key].timeSteps = new Set(); + } + }); + // product site variables + const { sites: productSites } = state.product; + // get the variables for each site + Object.keys(productSites).forEach((siteKey, index) => { + const { variables: siteVariables } = productSites[siteKey]; + if (Array.isArray(siteVariables) && siteVariables.length > 0) { + newState.product.sites[siteKey].variables = new Set(siteVariables); + } else { + newState.product.sites[siteKey].variables = new Set(); + } + }); + // available quality flags + const { availableQualityFlags } = state; + if (Array.isArray(availableQualityFlags)) { + newState.availableQualityFlags = new Set(availableQualityFlags); + } else { + newState.availableQualityFlags = new Set(); + } + // available quality flags + const { availableTimeSteps } = state; + if (Array.isArray(availableTimeSteps)) { + newState.availableTimeSteps = new Set(availableTimeSteps); + } else { + newState.availableTimeSteps = new Set(); + } + return newState; +}; + +export { convertStateForStorage, convertStateFromStorage }; diff --git a/src/lib_components/components/TimeSeriesViewer/StyleGuide.jsx b/src/lib_components/components/TimeSeriesViewer/StyleGuide.jsx index 5ee2238d..a9893e72 100644 --- a/src/lib_components/components/TimeSeriesViewer/StyleGuide.jsx +++ b/src/lib_components/components/TimeSeriesViewer/StyleGuide.jsx @@ -146,7 +146,7 @@ const AllProductsTimeSeries = () => { ); })} - +
  • ); } @@ -265,7 +265,7 @@ return ( title="Time Series Viewer" onClose={() => setDialogOpen(false)} > - + @@ -399,7 +399,7 @@ import TimeSeriesViewerContainer from 'portal-core-components/lib/components/Tim `}
    - + diff --git a/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerContainer.jsx b/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerContainer.jsx index 8107cc66..fe4134c0 100644 --- a/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerContainer.jsx +++ b/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerContainer.jsx @@ -29,10 +29,10 @@ import RouteService from '../../service/RouteService'; import TimeSeriesViewerContext, { summarizeTimeSteps, - TIME_SERIES_VIEWER_STATUS, TIME_SERIES_VIEWER_STATUS_TITLES, Y_AXIS_RANGE_MODE_DETAILS, } from './TimeSeriesViewerContext'; +import { TIME_SERIES_VIEWER_STATUS } from './constants'; import TimeSeriesViewerSites from './TimeSeriesViewerSites'; import TimeSeriesViewerDateRange from './TimeSeriesViewerDateRange'; import TimeSeriesViewerVariables from './TimeSeriesViewerVariables'; diff --git a/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerContext.jsx b/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerContext.jsx index 672dcb94..b3a78060 100644 --- a/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerContext.jsx +++ b/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerContext.jsx @@ -4,7 +4,7 @@ import React, { useEffect, useReducer, } from 'react'; -import PropTypes from 'prop-types'; +import PropTypes, { number } from 'prop-types'; import moment from 'moment'; import get from 'lodash/get'; @@ -31,6 +31,11 @@ import { forkJoinWithProgress } from '../../util/rxUtil'; import parseTimeSeriesData from '../../workers/parseTimeSeriesData'; +import NeonSignInButtonState from '../NeonSignInButton/NeonSignInButtonState'; +import makeStateStorage from '../../service/StateStorageService'; +import { convertStateForStorage, convertStateFromStorage } from './StateStorageConverter'; +import { TIME_SERIES_VIEWER_STATUS } from './constants'; + // 'get' is a reserved word so can't be imported with import const lodashGet = require('lodash/get.js'); @@ -47,18 +52,6 @@ const FETCH_STATUS = { SUCCESS: 'SUCCESS', }; -// Every possible top-level status the TimeSeriesViewer component can have -export const TIME_SERIES_VIEWER_STATUS = { - INIT_PRODUCT: 'INIT_PRODUCT', // Handling props; fetching product data if needed - LOADING_META: 'LOADING_META', // Actively loading meta data (sites, variables, and positions) - READY_FOR_DATA: 'READY_FOR_DATA', // Ready to trigger fetches for data - LOADING_DATA: 'LOADING_DATA', // Actively loading plottable series data - ERROR: 'ERROR', // Stop everything because problem, do not trigger new fetches no matter what - WARNING: 'WARNING', // Current selection/data makes a graph not possible; show warning - READY_FOR_SERIES: 'READY_FOR_SERIES', // Ready to re-calculate series data for the graph - READY: 'READY', // Ready for user input -}; - export const TIME_SERIES_VIEWER_STATUS_TITLES = { INIT_PRODUCT: 'Loading data product…', LOADING_META: 'Loading site positions, variables, and data paths…', @@ -1133,11 +1126,21 @@ const reducer = (state, action) => { } }; +/** + * Defines a lookup of state key to a boolean + * designating whether or not that instance of the context + * should pull the state from the session storage and restore. + * Keeping this lookup outside of the context provider function + * as to not incur lifecycle interference by storing with useState. + */ +const restoreStateLookup = {}; + /** Context Provider */ const Provider = (props) => { const { + timeSeriesUniqueId, mode: modeProp, productCode: productCodeProp, productData: productDataProp, @@ -1148,7 +1151,7 @@ const Provider = (props) => { /** Initial State and Reducer Setup */ - const initialState = cloneDeep(DEFAULT_STATE); + let initialState = cloneDeep(DEFAULT_STATE); if ((typeof modeProp === 'string') && (modeProp !== VIEWER_MODE.DEFAULT)) { initialState.mode = modeProp; } @@ -1163,8 +1166,46 @@ const Provider = (props) => { } initialState.release = releaseProp; initialState.selection = applyDefaultsToSelection(initialState); + + // get the state from storage if present + const { productCode } = initialState.product; + const stateKey = `timeSeriesContextState-${productCode}-${timeSeriesUniqueId}`; + if (typeof restoreStateLookup[stateKey] === 'undefined') { + restoreStateLookup[stateKey] = true; + } + const shouldRestoreState = restoreStateLookup[stateKey]; + const stateStorage = makeStateStorage(stateKey); + const savedState = stateStorage.readState(); + if (savedState && shouldRestoreState) { + restoreStateLookup[stateKey] = false; + const convertedState = convertStateFromStorage(savedState); + stateStorage.removeState(); + initialState = convertedState; + } + const [state, dispatch] = useReducer(reducer, initialState); + const { viewerStatus } = state; + + // The current sign in process uses a separate domain. This function + // persists the current state in storage when the button is clicked + // so the state may be reloaded when the page is reloaded after sign + // in. + useEffect(() => { + const subscription = NeonSignInButtonState.getObservable().subscribe({ + next: () => { + if (!NeonEnvironment.enableGlobalSignInState) return; + if (viewerStatus !== TIME_SERIES_VIEWER_STATUS.READY) return; + restoreStateLookup[stateKey] = false; + const convertedState = convertStateForStorage(state); + stateStorage.saveState(convertedState); + }, + }); + return () => { + subscription.unsubscribe(); + }; + }, [viewerStatus, state, stateStorage, stateKey]); + /** Effect - Reinitialize state if the product code prop changed */ @@ -1483,6 +1524,7 @@ const TimeSeriesViewerPropTypes = { }; Provider.propTypes = { + timeSeriesUniqueId: number, mode: PropTypes.string, productCode: TimeSeriesViewerPropTypes.productCode, productData: TimeSeriesViewerPropTypes.productData, @@ -1498,6 +1540,7 @@ Provider.propTypes = { }; Provider.defaultProps = { + timeSeriesUniqueId: 0, mode: VIEWER_MODE.DEFAULT, productCode: null, productData: null, diff --git a/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerGraph.jsx b/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerGraph.jsx index 1f6995ab..7f86ab54 100644 --- a/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerGraph.jsx +++ b/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerGraph.jsx @@ -30,8 +30,9 @@ import HideIcon from '@material-ui/icons/VisibilityOff'; import generateTimeSeriesGraphData from '../../workers/generateTimeSeriesGraphData'; -import TimeSeriesViewerContext, { TIME_SERIES_VIEWER_STATUS } from './TimeSeriesViewerContext'; +import TimeSeriesViewerContext from './TimeSeriesViewerContext'; import Theme, { COLORS } from '../Theme/Theme'; +import { TIME_SERIES_VIEWER_STATUS } from './constants'; import NeonLogo from '../../images/NSF-NEON-logo.png'; @@ -548,6 +549,7 @@ export default function TimeSeriesViewerGraph() { // Callback to refresh graph dimensions for current DOM const handleResize = useCallback(() => { + if (state.status !== TIME_SERIES_VIEWER_STATUS.READY) { return; } if (!dygraphRef.current || !legendRef.current || !graphInnerContainerRef.current) { return; } // Resize the graph relative to the legend width now that the legend is properly rendered const MIN_GRAPH_HEIGHT = 320; @@ -575,6 +577,7 @@ export default function TimeSeriesViewerGraph() { } } }, [ + state.status, dygraphRef, legendRef, graphInnerContainerRef, diff --git a/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerSites.jsx b/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerSites.jsx index 72017a0e..fd0c30ca 100644 --- a/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerSites.jsx +++ b/src/lib_components/components/TimeSeriesViewer/TimeSeriesViewerSites.jsx @@ -51,8 +51,8 @@ import MapSelectionButton from '../MapSelectionButton/MapSelectionButton'; import iconCoreTerrestrialSVG from '../SiteMap/svg/icon-site-core-terrestrial.svg'; import iconCoreAquaticSVG from '../SiteMap/svg/icon-site-core-aquatic.svg'; -import iconRelocatableTerrestrialSVG from '../SiteMap/svg/icon-site-relocatable-terrestrial.svg'; -import iconRelocatableAquaticSVG from '../SiteMap/svg/icon-site-relocatable-aquatic.svg'; +import iconGradientTerrestrialSVG from '../SiteMap/svg/icon-site-gradient-terrestrial.svg'; +import iconGradientAquaticSVG from '../SiteMap/svg/icon-site-gradient-aquatic.svg'; import TimeSeriesViewerContext, { TabComponentPropTypes } from './TimeSeriesViewerContext'; @@ -63,9 +63,9 @@ const ICON_SVGS = { AQUATIC: iconCoreAquaticSVG, TERRESTRIAL: iconCoreTerrestrialSVG, }, - RELOCATABLE: { - AQUATIC: iconRelocatableAquaticSVG, - TERRESTRIAL: iconRelocatableTerrestrialSVG, + GRADIENT: { + AQUATIC: iconGradientAquaticSVG, + TERRESTRIAL: iconGradientTerrestrialSVG, }, }; @@ -844,9 +844,9 @@ function SelectedSite(props) { } = allSites[siteCode]; let typeTitle = 'Core'; let typeSubtitle = 'fixed location'; - if (type === 'RELOCATABLE') { - typeTitle = 'Relocatable'; - typeSubtitle = 'location may change'; + if (type === 'GRADIENT') { + typeTitle = 'Gradient'; + typeSubtitle = 'gradient location'; } let terrainTitle = 'Terrestrial'; let terrainSubtitle = 'land-based'; diff --git a/src/lib_components/components/TimeSeriesViewer/__tests__/TimeSeriesViewerContext.jsx b/src/lib_components/components/TimeSeriesViewer/__tests__/TimeSeriesViewerContext.jsx index 5ff4116a..eea91ec1 100644 --- a/src/lib_components/components/TimeSeriesViewer/__tests__/TimeSeriesViewerContext.jsx +++ b/src/lib_components/components/TimeSeriesViewer/__tests__/TimeSeriesViewerContext.jsx @@ -9,9 +9,9 @@ import { mockAjaxResponse } from '../../../../__mocks__/ajax'; import TimeSeriesViewerContext, { getTestableItems, summarizeTimeSteps, - TIME_SERIES_VIEWER_STATUS, Y_AXIS_RANGE_MODES, } from '../TimeSeriesViewerContext'; +import { TIME_SERIES_VIEWER_STATUS } from '../constants'; const { Provider, useTimeSeriesViewerState } = TimeSeriesViewerContext; diff --git a/src/lib_components/components/TimeSeriesViewer/__tests__/TimeSeriesViewerSites.jsx b/src/lib_components/components/TimeSeriesViewer/__tests__/TimeSeriesViewerSites.jsx index 5ccc11f8..c92414bc 100644 --- a/src/lib_components/components/TimeSeriesViewer/__tests__/TimeSeriesViewerSites.jsx +++ b/src/lib_components/components/TimeSeriesViewer/__tests__/TimeSeriesViewerSites.jsx @@ -212,7 +212,7 @@ describe('TimeSeriesViewerSites', () => { const data = { siteCode: 'ABBY', description: 'Abby Road', - type: 'RELOCATABLE', + type: 'GRADIENT', stateCode: 'WA', domainCode: 'D16', domainName: 'D16 Name', @@ -238,7 +238,7 @@ describe('TimeSeriesViewerSites', () => { beforeEach(() => { useTimeSeriesViewerState.mockReturnValue([cloneDeep(DEFAULT_STATE), () => {}]); }); - test('Renders as expected (terrestrial / relocatable)', () => { + test('Renders as expected (terrestrial / gradient)', () => { const tree = renderer.create( , ).toJSON(); @@ -250,7 +250,7 @@ describe('TimeSeriesViewerSites', () => { ).toJSON(); expect(tree).toMatchSnapshot(); }); - test('Renders as expected (aquatic / relocatable)', () => { + test('Renders as expected (aquatic / gradient)', () => { const tree = renderer.create( , ).toJSON(); diff --git a/src/lib_components/components/TimeSeriesViewer/__tests__/__snapshots__/TimeSeriesViewerSites.jsx.snap b/src/lib_components/components/TimeSeriesViewer/__tests__/__snapshots__/TimeSeriesViewerSites.jsx.snap index c9d92bac..77463ca6 100644 --- a/src/lib_components/components/TimeSeriesViewer/__tests__/__snapshots__/TimeSeriesViewerSites.jsx.snap +++ b/src/lib_components/components/TimeSeriesViewer/__tests__/__snapshots__/TimeSeriesViewerSites.jsx.snap @@ -830,7 +830,7 @@ exports[`TimeSeriesViewerSites SelectedSite Renders as expected (aquatic / core)
    `; -exports[`TimeSeriesViewerSites SelectedSite Renders as expected (aquatic / relocatable) 1`] = ` +exports[`TimeSeriesViewerSites SelectedSite Renders as expected (aquatic / gradient) 1`] = `
    @@ -839,16 +839,16 @@ exports[`TimeSeriesViewerSites SelectedSite Renders as expected (aquatic / reloc className="makeStyles-siteTitleContainer-11" > Aquatic Relocatable
    - Aquatic Relocatable + Aquatic Gradient

    - water-based; location may change + water-based; gradient location

    @@ -1714,7 +1714,7 @@ exports[`TimeSeriesViewerSites SelectedSite Renders as expected (terrestrial / c
    `; -exports[`TimeSeriesViewerSites SelectedSite Renders as expected (terrestrial / relocatable) 1`] = ` +exports[`TimeSeriesViewerSites SelectedSite Renders as expected (terrestrial / gradient) 1`] = `
    @@ -1723,16 +1723,16 @@ exports[`TimeSeriesViewerSites SelectedSite Renders as expected (terrestrial / r className="makeStyles-siteTitleContainer-11" > Terrestrial Relocatable
    - Terrestrial Relocatable + Terrestrial Gradient

    - land-based; location may change + land-based; gradient location

    @@ -2187,9 +2187,9 @@ exports[`TimeSeriesViewerSites SiteOption Renders as expected without stateCode className="makeStyles-startFlex-20" > Terrestrial Relocatable
    - Terrestrial Relocatable - Domain D16 (D16 Name) - Lat/Lon: 45.762439, -122.330317 + Terrestrial Gradient - Domain D16 (D16 Name) - Lat/Lon: 45.762439, -122.330317

    diff --git a/src/lib_components/components/TimeSeriesViewer/constants.ts b/src/lib_components/components/TimeSeriesViewer/constants.ts new file mode 100644 index 00000000..9691557f --- /dev/null +++ b/src/lib_components/components/TimeSeriesViewer/constants.ts @@ -0,0 +1,17 @@ +// Every possible top-level status the TimeSeriesViewer component can have +export const TIME_SERIES_VIEWER_STATUS = { + INIT_PRODUCT: 'INIT_PRODUCT', // Handling props; fetching product data if needed + LOADING_META: 'LOADING_META', // Actively loading meta data (sites, variables, and positions) + READY_FOR_DATA: 'READY_FOR_DATA', // Ready to trigger fetches for data + LOADING_DATA: 'LOADING_DATA', // Actively loading plottable series data + ERROR: 'ERROR', // Stop everything because problem, do not trigger new fetches no matter what + WARNING: 'WARNING', // Current selection/data makes a graph not possible; show warning + READY_FOR_SERIES: 'READY_FOR_SERIES', // Ready to re-calculate series data for the graph + READY: 'READY', // Ready for user input +}; + +const TimeSeriesViewerConstants = { + TIME_SERIES_VIEWER_STATUS, +}; + +export default TimeSeriesViewerConstants; diff --git a/src/lib_components/remoteAssets/drupal-header.html.js b/src/lib_components/remoteAssets/drupal-header.html.js index 906306a4..20832f80 100644 --- a/src/lib_components/remoteAssets/drupal-header.html.js +++ b/src/lib_components/remoteAssets/drupal-header.html.js @@ -519,11 +519,33 @@ export default html = ` + +