diff --git a/.env.production b/.env.production index 3f4acca6..8b466ce3 100644 --- a/.env.production +++ b/.env.production @@ -10,6 +10,10 @@ VITE_REPORT_DOWNLOAD=false VITE_API_URL=https://midas.sec.usace.army.mil/api VITE_URL_BASE_PATH=/midas + +# EXTERNAL APIS +VITE_CWMS_API_URL=https://cwms-data.usace.army.mil/cwms-data + # Keycloak VITE_KC_URL=https://identity.sec.usace.army.mil/auth/ VITE_KC_REALM=cwbi diff --git a/.env.test b/.env.test index 5d5294fe..6ed6f5c6 100644 --- a/.env.test +++ b/.env.test @@ -10,6 +10,9 @@ VITE_REPORT_DOWNLOAD=false VITE_API_URL=https://midas-test.cwbi.us/api VITE_URL_BASE_PATH=/midas +# EXTERNAL APIS +VITE_CWMS_API_URL=https://cwms-data.usace.army.mil/cwms-data + # Keycloak VITE_KC_URL=https://identity-test.cwbi.us/auth/ VITE_KC_REALM=cwbi diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 4456eca7..5ca8b4fb 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -19,6 +19,7 @@ jobs: VITE_DEVELOPMENT_BANNER: true VITE_REPORT_DOWNLOAD: true VITE_API_URL: https://develop-midas-api.rsgis.dev + VITE_CWMS_API_URL: https://cwms-data.usace.army.mil/cwms-data VITE_URL_BASE_PATH: '' VITE_KC_URL: https://identity-test.cwbi.us/auth/ VITE_KC_REALM: cwbi diff --git a/package.json b/package.json index fd33ec96..37b61199 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hhd-ui", - "version": "0.16.1", + "version": "0.17.0", "private": true, "dependencies": { "@ag-grid-community/client-side-row-model": "^30.0.3", diff --git a/src/app-components/domain-select.jsx b/src/app-components/domain-select.jsx index 6af4b462..c04b2da6 100644 --- a/src/app-components/domain-select.jsx +++ b/src/app-components/domain-select.jsx @@ -10,13 +10,15 @@ export default connect( useLabelAsDefault = false, onChange, domain, + label = '', + ...customProps }) => { const options = domainsItemsByGroup[domain]?.map(item => ( { value: item.id, label: item.value } )) || []; return ( - <> +
{!options || !options.length ? ( No Options... ) : ( @@ -28,12 +30,12 @@ export default connect( const item = domainsItemsByGroup[domain]?.find(el => el.value === value?.label); onChange(item); }} - renderInput={(params) => } + renderInput={(params) => } options={options} fullWidth /> )} - +
); } ); diff --git a/src/app-pages/instrument/cwms-timeseries/cwmsTimeseries.jsx b/src/app-pages/instrument/cwms-timeseries/cwmsTimeseries.jsx new file mode 100644 index 00000000..0080cfc8 --- /dev/null +++ b/src/app-pages/instrument/cwms-timeseries/cwmsTimeseries.jsx @@ -0,0 +1,156 @@ +import React, { useRef, useState } from 'react'; +import ReactDatePicker from 'react-datepicker'; +import { AgGridReact } from '@ag-grid-community/react'; +import { Button } from '@mui/material'; +import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model'; +import { connect } from 'redux-bundler-react'; +import { DateTime } from 'luxon'; +import { endOfDay, startOfDay, subDays } from 'date-fns'; +import { Icon } from '@iconify/react'; + +import CwmsTimeseriesListItem from './cwmsTimeseriesListItem.jsx'; +import NewCwmsTimeseriesModal from './newCwmsTimeseries.jsx'; +import { useGetMidasCwmsTimeseries, useGetCwmsTimeseriesMeasurements } from '../../../app-services/collections/cwms-timeseries.ts'; + +const CwmsTimeseries = connect( + 'doModalOpen', + 'selectProjectsIdByRoute', + 'selectInstrumentsByRoute', + ({ + doModalOpen, + projectsIdByRoute: project, + instrumentsByRoute: instrument, + }) => { + const grid = useRef(null); + + const { projectId } = project || {}; + const { id } = instrument || {}; + + const [activeTimeseries, setActiveTimeseries] = useState(null); + const [dateRange, setDateRange] = useState([subDays(startOfDay(new Date()), 1), endOfDay(new Date())]); + + const { data: midasTimeseries } = useGetMidasCwmsTimeseries({ projectId, instrumentId: id }); + const { data: cwmsMeasurements, isLoading } = useGetCwmsTimeseriesMeasurements( + { + name: activeTimeseries?.cwms_timeseries_id, + office: activeTimeseries?.cwms_office_id, + begin: DateTime.fromJSDate(dateRange[0]).toISO(), + end: DateTime.fromJSDate(dateRange[1]).toISO(), + }, + { enabled: !!activeTimeseries?.cwms_timeseries_id }, + ); + + const { values = [] } = cwmsMeasurements || {}; + + const data = values.map(el => { + if (el?.length < 2) return null; + + return { + timestamp: DateTime.fromMillis(el[0]).toFormat('D HH:mm:ss'), + value: el[1], + }; + }).filter(e => e); + + return ( + <> +
+
+ CWMS Timeseries are timeseries from the external source, Corps Water Management System (CWMS), that are manually associated to instruments within the MIDAS + system. Use this panel to manage the connected timeseries and view the data associated with them. While connections can be made, the data it provides is strictly read-only. +
+
+
+
+ +
+
+ {!!activeTimeseries?.id && ( +
+
+ setDateRange(prev => [startOfDay(date), prev[1]])} + /> +
+
+  -  +
+
+ setDateRange(prev => [prev[0], endOfDay(date)])} + /> +
+
+ )} +
+
+
+
+ +
+
+ {isLoading ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ + ); + }, +); + +export default CwmsTimeseries; \ No newline at end of file diff --git a/src/app-pages/instrument/cwms-timeseries/cwmsTimeseriesListItem.jsx b/src/app-pages/instrument/cwms-timeseries/cwmsTimeseriesListItem.jsx new file mode 100644 index 00000000..6a5bde47 --- /dev/null +++ b/src/app-pages/instrument/cwms-timeseries/cwmsTimeseriesListItem.jsx @@ -0,0 +1,48 @@ +import React, { useRef } from 'react'; +import { connect } from 'redux-bundler-react'; +import { Edit } from '@mui/icons-material'; + +import RoleFilter from '../../../app-components/role-filter'; +import { classnames } from '../../../common/helpers/utils'; +import NewCwmsTimeseriesModal from './newCwmsTimeseries'; + +export default connect( + 'selectProjectsByRoute', + 'doModalOpen', + ({ projectsByRoute: project, doModalOpen, item, onClick, active }) => { + const li = useRef(null); + const itemClass = classnames({ + 'pointer': true, + 'list-group-item': true, + active: active, + }); + + return ( + item && ( +
  • { + if (e.currentTarget === li.current) onClick(item); + }} + > + + + +
    {item.name}
    +
    + {`${item.parameter} in ${item.unit}`} +
    +
  • + ) + ); + } +); diff --git a/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx b/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx new file mode 100644 index 00000000..1c969791 --- /dev/null +++ b/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx @@ -0,0 +1,152 @@ +import React, { useMemo, useState } from 'react'; +import { Autocomplete, TextField } from '@mui/material'; +import { connect } from 'redux-bundler-react'; +import { useQueryClient } from '@tanstack/react-query'; + +import * as Modal from '../../../app-components/modal'; +import DomainSelect from '../../../app-components/domain-select.jsx'; +import { useGetCwmsOffices, useGetCwmsTimeseries, usePostMidasCwmsTimeseries, usePutMidasCwmsTimeseries } from '../../../app-services/collections/cwms-timeseries.ts'; + +const generateOfficesOptions = cwmsOffices => { + if (!cwmsOffices?.length) return []; + + return cwmsOffices.map(office => ({ + label: `${office[`long-name`]} (${office?.name})`, + value: office?.name, + })); +}; + +const generateCwmsTimeseriesOptions = cwmsTimeseries => { + const { total, entries } = cwmsTimeseries || {}; + + if (!total || !entries) return []; + + return entries.map(entry => ({ + _extents: entry?.extents?.length ? entry.extents[0] : {}, + label: entry?.name, + value: entry?.name, + })); +}; + +const NewCwmsTimeseriesModal = connect( + 'doModalClose', + 'doInstrumentTimeseriesDelete', + ({ + doModalClose, + doInstrumentTimeseriesDelete, + projectId, + instrumentId, + item, + isEdit, + }) => { + const client = useQueryClient(); + const midasCwmsTimeseriesCreator = usePostMidasCwmsTimeseries(client); + const midasCwmsTimeseriesUpdater = usePutMidasCwmsTimeseries(client); + + const [selectedOffice, setSelectedOffice] = useState(isEdit ? item?.cwms_office_id : ''); + const [selectedTimeseries, setSelectedTimeseries] = useState(isEdit ? item?.cwms_timeseries_id : ''); + const [name, setName] = useState(isEdit ? item?.name : ''); + const [parameterId, setParameterId] = useState(isEdit ? item?.parameter_id : ''); + const [unitId, setUnitId] = useState(isEdit ? item?.unit_id : ''); + + const { data: cwmsOffices } = useGetCwmsOffices({}); + const cwmsOfficesOptions = useMemo(() => generateOfficesOptions(cwmsOffices), [cwmsOffices]); + + const { data: cwmsTimeseries } = useGetCwmsTimeseries({ office: selectedOffice?.value || selectedOffice }, { + enabled: !!selectedOffice?.value || !!selectedOffice, + }); + const cwmsTimeseriesOptions = useMemo(() => generateCwmsTimeseriesOptions(cwmsTimeseries), [cwmsTimeseries]); + + return ( + + + + Use the filters below to query for CWMS Timeseries: +
    + opt.value === val || opt.value === val.value} + onChange={(_e, value) => setSelectedOffice(value)} + renderInput={(params) => } + options={cwmsOfficesOptions} + fullWidth + /> + {selectedOffice && ( + opt.value === val || opt.value === val.value} + onChange={(_e, value) => setSelectedTimeseries(value)} + renderInput={(params) => } + options={cwmsTimeseriesOptions} + fullWidth + /> + )} + {selectedTimeseries && ( + <> +
    +

    The following fields will be used by MIDAS exclusively and will not change any data in regards to the CWMS timeseries:

    + setName(e.target.value)} + /> + setParameterId(val?.id)} + domain='parameter' + label='Parameter' + /> + setUnitId(val?.id)} + domain='unit' + label='Unit' + /> + + )} +
    + doInstrumentTimeseriesDelete(item, () => { + client.invalidateQueries('midasCwmsTimeseries'); + doModalClose(); + }, true) : null} + saveIsDisabled={!selectedTimeseries || !name || !parameterId || !unitId} + saveText={isEdit ? 'Save Changes' : 'Add to Instrument'} + onSave={() => { + if (isEdit) { + midasCwmsTimeseriesUpdater.mutate({ projectId, instrumentId, timeseriesId: item?.id, body: { + instrument_id: instrumentId, + cwms_office_id: typeof selectedOffice === 'string' ? selectedOffice : selectedOffice.value, + cwms_timeseries_id: typeof selectedTimeseries === 'string' ? selectedTimeseries : selectedTimeseries.value, + cwms_extent_earliest_time: typeof selectedTimeseries === 'string' ? item?.cwms_extent_earliest_time : selectedTimeseries._extents['earliest-time'], + parameter_id: parameterId, + unit_id: unitId, + name, + }}) + } else { + midasCwmsTimeseriesCreator.mutate({ projectId, instrumentId, body: [{ + instrument_id: instrumentId, + cwms_office_id: selectedOffice.value, + cwms_timeseries_id: selectedTimeseries.value, + cwms_extent_earliest_time: selectedTimeseries._extents['earliest-time'], + parameter_id: parameterId, + unit_id: unitId, + name, + }]}) + } + }} + /> +
    + ); + }, +); + +export default NewCwmsTimeseriesModal; diff --git a/src/app-pages/instrument/settings.jsx b/src/app-pages/instrument/settings.jsx index 0ff1e256..9e22d711 100644 --- a/src/app-pages/instrument/settings.jsx +++ b/src/app-pages/instrument/settings.jsx @@ -5,6 +5,7 @@ import AlertEditor from './alert/alert-editor'; import Card from '../../app-components/card'; import Chart from './chart/chart'; import Constants from './constants/constants'; +import CwmsTimeseries from './cwms-timeseries/cwmsTimeseries'; import FormulaEditor from './formula/formula'; import TabContainer from '../../app-components/tab'; import Timeseries from './timeseries/timeseries'; @@ -17,12 +18,14 @@ export default connect( timeseriesMeasurementsItemsObject: measurements, instrumentsByRoute: instrument, }) => { + const { type, show_cwms_tab } = instrument || {}; + // const alertsReady = import.meta.env.VITE_ALERT_EDITOR === 'true'; const alertsReady = false; const forumlaReady = import.meta.env.VITE_FORMULA_EDITOR === 'true'; const chartReady = import.meta.env.VITE_INSTRUMENT_CHART === 'true'; - const isShapeArray = instrument?.type === 'SAA'; - const isIPI = instrument?.type === 'IPI'; + const isShapeArray = type === 'SAA'; + const isIPI = type === 'IPI'; const tabs = [ alertsReady && { @@ -34,6 +37,9 @@ export default connect( }, { title: 'Timeseries', content: , + }, show_cwms_tab && { + title: 'CWMS Timeseries', + content: , }, (isShapeArray || isIPI) && { title: 'Sensors', content: , diff --git a/src/app-pages/instrument/timeseries/timeseries-list-item.jsx b/src/app-pages/instrument/timeseries/timeseries-list-item.jsx index 2a98023f..5263ad0d 100644 --- a/src/app-pages/instrument/timeseries/timeseries-list-item.jsx +++ b/src/app-pages/instrument/timeseries/timeseries-list-item.jsx @@ -28,8 +28,9 @@ export default connect(