From 39f8360a13f5d49fe2f59677af35bdf074fba9ea Mon Sep 17 00:00:00 2001 From: Kevin Jackson <30411845+KevinJJackson@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:27:01 -0500 Subject: [PATCH] feature/ipi-dataloggers (#195) --- package.json | 2 +- src/app-bundles/create-auth-bundle.js | 2 +- src/app-bundles/instrument-sensors-bundle.js | 13 +- src/app-components/domain-select.jsx | 2 +- src/app-pages/help/apiHelp.jsx | 28 ++ src/app-pages/help/help.jsx | 4 +- src/app-pages/help/onboarding.jsx | 23 +- src/app-pages/instrument/details.jsx | 15 +- .../instrument/ipi-depth-based-plots.jsx | 252 ++++++++++++++++++ ...ed-plots.jsx => saa-depth-based-plots.jsx} | 12 +- .../instrument/sensors/automapSensorModal.jsx | 3 +- .../instrument/sensors/sensorDetails.jsx | 171 ++++++++---- src/app-pages/instrument/sensors/sensors.jsx | 40 ++- .../instrument/setInitialTimeModal.jsx | 5 +- src/app-pages/instrument/settings.jsx | 5 +- .../instrument/timeseries/timeseries-form.jsx | 3 +- src/common/forms/instrument-form.jsx | 7 +- 17 files changed, 482 insertions(+), 105 deletions(-) create mode 100644 src/app-pages/help/apiHelp.jsx create mode 100644 src/app-pages/instrument/ipi-depth-based-plots.jsx rename src/app-pages/instrument/{depth-based-plots.jsx => saa-depth-based-plots.jsx} (96%) diff --git a/package.json b/package.json index 6b337ea5..cefdeb45 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hhd-ui", - "version": "0.14.1", + "version": "0.15.0", "private": true, "dependencies": { "@ag-grid-community/client-side-row-model": "^30.0.3", diff --git a/src/app-bundles/create-auth-bundle.js b/src/app-bundles/create-auth-bundle.js index 870bc73a..f1bc2bf7 100644 --- a/src/app-bundles/create-auth-bundle.js +++ b/src/app-bundles/create-auth-bundle.js @@ -61,7 +61,7 @@ const createAuthBundle = (opts) => { try { xhr(url, (err, response, body) => { if (err) { - throw new Error('Login Response not ok'); + throw new Error(`Login Response not ok. ${err}`); } else { const token = typeof body === 'string' ? body : JSON.parse(body); dispatch({ diff --git a/src/app-bundles/instrument-sensors-bundle.js b/src/app-bundles/instrument-sensors-bundle.js index a321bf33..3efed25a 100644 --- a/src/app-bundles/instrument-sensors-bundle.js +++ b/src/app-bundles/instrument-sensors-bundle.js @@ -31,11 +31,11 @@ export default { selectInstrumentSensorsMeasurements: (state) => state.instrumentSensors.measurements, selectInstrumentSensorsLastFetched: (state) => state.instrumentSensors._lastFetched, - doFetchInstrumentSensorsById: () => ({ dispatch, store, apiGet }) => { + doFetchInstrumentSensorsById: (type) => ({ dispatch, store, apiGet }) => { dispatch({ type: 'INSTRUMENT_SENSORS_BY_ID_FETCH_START' }); const { instrumentId } = store.selectInstrumentsIdByRoute(); - const url = `/instruments/saa/${instrumentId}/segments`; + const url = `/instruments/${type}/${instrumentId}/segments`; apiGet(url, (err, body) => { if (err) { @@ -52,11 +52,11 @@ export default { }); }, - doUpdateInstrumentSensor: (formData) => ({ dispatch, store, apiPut }) => { + doUpdateInstrumentSensor: (type, formData) => ({ dispatch, store, apiPut }) => { dispatch({ type: 'INSTRUMENT_SENSOR_UPDATE_START' }); const { instrumentId } = store.selectInstrumentsIdByRoute(); - const url = `/instruments/saa/${instrumentId}/segments`; + const url = `/instruments/${type}/${instrumentId}/segments`; apiPut(url, formData, (err, _body) => { if (err) { @@ -68,10 +68,10 @@ export default { }); }, - doFetchInstrumentSensorMeasurements: (before, after) => ({ dispatch, store, apiGet }) => { + doFetchInstrumentSensorMeasurements: (type, before, after) => ({ dispatch, store, apiGet }) => { dispatch({ type: 'SENSOR_MEASUREMENTS_FETCH_START' }); const { instrumentId } = store.selectInstrumentsIdByRoute(); - const url = `/instruments/saa/${instrumentId}/measurements?before=${before}&after=${after}`; + const url = `/instruments/${type}/${instrumentId}/measurements?before=${before}&after=${after}`; apiGet(url, (err, body) => { if (err) { @@ -86,6 +86,5 @@ export default { }); dispatch({ type: 'SENSOR_MEASUREMENTS_FETCH_FINISHED' }); - }, }; diff --git a/src/app-components/domain-select.jsx b/src/app-components/domain-select.jsx index 0f6b9b42..45ea13eb 100644 --- a/src/app-components/domain-select.jsx +++ b/src/app-components/domain-select.jsx @@ -29,7 +29,7 @@ export default connect( size='small' defaultValue={options.find(el => el.value === defaultValue)} isOptionEqualToValue={(opt, val) => opt.value === val.value} - onChange={e => setSelectValue(e.target.innerText)} + onChange={(_e, value) => setSelectValue(value?.label)} renderInput={(params) => } options={options} fullWidth diff --git a/src/app-pages/help/apiHelp.jsx b/src/app-pages/help/apiHelp.jsx new file mode 100644 index 00000000..5b83cc77 --- /dev/null +++ b/src/app-pages/help/apiHelp.jsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import Card from '../../app-components/card/card'; + +const apiURL = import.meta.env.DEV + ? 'http://localhost:8080' + : import.meta.env.VITE_API_URL; + +const ApiHelp = () => ( + + +

+ Q:I want to view and interact with the API. Where can I find documetation for the API? +

+

+ The API is hosted at the link below. Simply click the link to be directed towards the swagger documentation. The documentation + is also interactable, requiring a Bearer token to authenticate and make requests with. +

+
+
+

Click the link below to view the Swagger Documentation for the API:

+ {apiURL}/swagger/index.html +
+
+
+); + +export default ApiHelp; diff --git a/src/app-pages/help/help.jsx b/src/app-pages/help/help.jsx index ad0de5f9..44618dd5 100644 --- a/src/app-pages/help/help.jsx +++ b/src/app-pages/help/help.jsx @@ -1,16 +1,18 @@ import React from 'react'; +import ApiHelp from './apiHelp'; import Hero from '../../app-components/hero'; import Onboarding from './onboarding'; const HelpPage = () => ( <> -
+

Frequently Asked Questions


+
diff --git a/src/app-pages/help/onboarding.jsx b/src/app-pages/help/onboarding.jsx index 085f1e1a..f0c1a28e 100644 --- a/src/app-pages/help/onboarding.jsx +++ b/src/app-pages/help/onboarding.jsx @@ -117,7 +117,6 @@ const DownloadCSVButton = ({ csvContent, filename }) => { export default connect( 'selectDomainsItemsByGroup', ({ domainsItemsByGroup }) => { - // NotesXYZ are additional information to be included with each tab const NotesInstruments = () => (

Optional Fields:

@@ -171,14 +170,18 @@ export default connect( const buildContent = (title, csvData, notes) => (
-
- -
- -
+ {csvData && ( + <> +
+ +
+ + + )} +
{notes}
@@ -214,7 +217,7 @@ export default connect( Q:I want to try the site with my own projects. How should I organize my dataset for upload?

-

+

Glad you asked! The site supports .csv uploads of Instruments, Timeseries, Timeseries measurements and Inclinometer measurements at this time. Uploaders for Projects are coming soon. To get organized, start with projects{' '} diff --git a/src/app-pages/instrument/details.jsx b/src/app-pages/instrument/details.jsx index 7c0786a8..819e5d11 100644 --- a/src/app-pages/instrument/details.jsx +++ b/src/app-pages/instrument/details.jsx @@ -5,10 +5,11 @@ import { Edit, Refresh, SettingsOutlined } from '@mui/icons-material'; import AlertEntry from './alert/alert-entry'; import Button from '../../app-components/button'; import Card from '../../app-components/card'; -import DepthBasedPlots from './depth-based-plots'; +import SaaDepthBasedPlots from './saa-depth-based-plots'; import Dropdown from '../../app-components/dropdown'; import InstrumentDisplay from './instrument-display'; import InstrumentForm from '../../common/forms/instrument-form'; +import IpiDepthBasedPlots from './ipi-depth-based-plots'; import LoginMessage from '../../app-components/login-message'; import Map from '../../app-components/classMap'; import NoAlerts from './alert/no-alerts'; @@ -54,12 +55,13 @@ export default connect( const timeseries = timeseriesByInstrumentId[instrument.id] || []; const isShapeArray = instrument?.type === 'SAA'; + const isIPI = instrument?.type === 'IPI'; const len = timeseries.length; let firstTimeseries = null; if (len && len > 0) firstTimeseries = timeseries[0]; - if (isShapeArray && !notifcationFired && !instrument?.opts?.initial_time) { + if ((isShapeArray || isIPI) && !notifcationFired && !instrument?.opts?.initial_time) { setNotificationFired(true); doNotificationFire({ title: 'Missing Initial Time', @@ -71,7 +73,7 @@ export default connect( size='small' variant='info' text='Set Initial Time' - handleClick={() => doModalOpen(SetInitialTimeModal, {}, 'lg')} + handleClick={() => doModalOpen(SetInitialTimeModal, { type: isShapeArray ? 'saa' : 'ipi'}, 'lg')} /> ), @@ -178,7 +180,12 @@ export default connect(

{isShapeArray && (
- + +
+ )} + {isIPI && ( +
+
)}
diff --git a/src/app-pages/instrument/ipi-depth-based-plots.jsx b/src/app-pages/instrument/ipi-depth-based-plots.jsx new file mode 100644 index 00000000..195717d8 --- /dev/null +++ b/src/app-pages/instrument/ipi-depth-based-plots.jsx @@ -0,0 +1,252 @@ +import React, { useEffect, useState } from 'react'; +import ReactDatePicker from 'react-datepicker'; +import { Add, Remove } from '@mui/icons-material'; +import { Checkbox, FormControlLabel, Slider, Stack, Switch } from '@mui/material'; +import { addDays, subDays } from 'date-fns'; +import { connect } from 'redux-bundler-react'; +import { DateTime } from 'luxon'; +import { useDeepCompareEffect } from 'react-use'; + +import Button from '../../app-components/button'; +import Card from '../../app-components/card'; +import Chart from '../../app-components/chart/chart'; +import SetInitialTimeModal from './setInitialTimeModal'; + +const colors = { + init: '#000000', +}; + +const config = { + repsonsive: true, + displaylogo: false, + displayModeBar: true, + scrollZoom: true, +}; + +const layout = (showTemperature, showIncremental) => ({ + showlegend: true, + autosize: true, + height: 800, + rows: 1, + columns: showTemperature ? 2 : 1, + yaxis: { + domain: [0, 1], + anchor: 'x1', + autorange: 'reversed', + title: `Depth in Feet`, + }, + xaxis: { + domain: [0, showTemperature ? 0.4 : 1], + anchor: 'y1', + title: `${showIncremental ? 'Incremental' : 'Cumulative'} Displacement`, + }, + ...showTemperature && { + xaxis2: { + title: 'Temperature', + domain: [0.6, 1], + anchor: 'y2', + }, + yaxis2: { + domain: [0, 1], + anchor: 'x2', + autorange: 'reversed', + } + }, +}); + +const formatData = (measurements, indexes, showInitial, showTemperature, showIncremental) => { + if (!measurements.length) return {}; + + const timeIncrements = measurements.sort((a, b) => DateTime.fromISO(a.time).toMillis() - DateTime.fromISO(b.time).toMillis()) + const relevantData = timeIncrements.slice(indexes[0], indexes[1] + 1); + + const dataArray = [ + ...(showInitial ? build2dTrace(timeIncrements[0], true, showTemperature, showIncremental).flat() : []), + ...relevantData.map(m => build2dTrace(m, false, showTemperature, showIncremental)).flat(), + ].filter(e => e); + + return { dataArray, timeIncrements, relevantData }; +}; + +const build2dTrace = (data, isInit, showTemperature, showIncremental) => { + if (!Object.keys(data).length) return {}; + + const { time, measurements } = data; + + const x = [], xTemp = [], y = []; + + measurements?.forEach(element => { + x.push(showIncremental ? (element?.inc_dev || 0) : (element?.cum_dev || 0)); + xTemp.push(element?.temp); + y.push(element?.elevation || 0); + }); + + const localDateString = DateTime.fromISO(time).toLocaleString(DateTime.DATETIME_SHORT); + const common = { + y, + mode: 'markers+lines', + marker: { size: 5, color: isInit ? colors['init'] : undefined }, + line: { width: 1 }, + type: 'scatter', + }; + + return [{ + ...common, + x, + name: isInit ? `Initial Displacement (${localDateString})` : `Displacement at ${localDateString}`, + hovertemplate: ` + ${localDateString}
+ Elevation: %{y}
+ ${showIncremental ? 'Incremental' : 'Cumulative'} Displacement: %{x}
+ + `, + }, showTemperature ? { + ...common, + xTemp, + xaxis: 'x2', + yaxis: 'y2', + name: isInit ? `Initial Temperature (${localDateString})` : `Temperature at ${localDateString}`, + hovertemplate: ` + ${localDateString}
+ Elevation: %{y}
+ Temperature: %{x}
+ + `, + } : {}]; +}; + +const IpiDepthBasedPlots = connect( + 'doModalOpen', + 'doFetchInstrumentSensorsById', + 'doFetchInstrumentSensorMeasurements', + 'selectInstrumentSensors', + 'selectInstrumentSensorsMeasurements', + ({ + doModalOpen, + doFetchInstrumentSensorsById, + doFetchInstrumentSensorMeasurements, + instrumentSensors, + instrumentSensorsMeasurements, + }) => { + const [isOpen, setIsOpen] = useState(true); + const [showTemperature, setShowTemperature] = useState(true); + const [showInitial, setShowInitial] = useState(false); + const [showIncremental, setShowIncremental] = useState(false); + const [sliderVal, setSliderVal] = useState([0, 0]); + const [dateRange, setDateRange] = useState([subDays(new Date(), 7), new Date()]); + + const { dataArray = [], timeIncrements = [] } = formatData(instrumentSensorsMeasurements, sliderVal, showInitial, showTemperature, showIncremental); + + useEffect(() => { + doFetchInstrumentSensorsById('ipi'); + }, [doFetchInstrumentSensorsById]); + + useDeepCompareEffect(() => { + if (isOpen) { + doFetchInstrumentSensorMeasurements('ipi', dateRange[1].toISOString(), dateRange[0].toISOString()); + } + }, [isOpen, dateRange]); + + return ( + + +