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.
+
+
+
+
+ doModalOpen(NewCwmsTimeseriesModal, { projectId, instrumentId: id })}
+ >
+ + New CWMS Timeseries
+
+
+
+ {!!activeTimeseries?.id && (
+
+
+ setDateRange(prev => [startOfDay(date), prev[1]])}
+ />
+
+
+ -
+
+
+ setDateRange(prev => [prev[0], endOfDay(date)])}
+ />
+
+
+ )}
+
+
+
+
+
+ {midasTimeseries?.length ? (
+ <>
+ {midasTimeseries.map(t => (
+ setActiveTimeseries(activeTimeseries?.id === t.id ? null : item)}
+ />
+ ))}
+ >
+ ) : (
+ No Timeseries Configured
+ )}
+
+
+
+ {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);
+ }}
+ >
+
+ {
+ doModalOpen(NewCwmsTimeseriesModal, { projectId: project.id, instrumentId: item.instrument_id, item, isEdit: true });
+ e.stopPropagation();
+ }}
+ >
+
+
+
+ {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(
{
+ onClick={e => {
doModalOpen(TimeseriesForm, { item: item, isEdit: true });
+ e.stopPropagation();
}}
>
diff --git a/src/app-pages/instrument/timeseries/timeseries.jsx b/src/app-pages/instrument/timeseries/timeseries.jsx
index ceb10392..78bee170 100644
--- a/src/app-pages/instrument/timeseries/timeseries.jsx
+++ b/src/app-pages/instrument/timeseries/timeseries.jsx
@@ -108,10 +108,10 @@ export default connect(
const [activeTimeseries, setActiveTimeseries] = useState(null);
const [isInclinometer, setIsInclinometer] = useState(false);
- // filter out any timeseries used for constants
+ // filter out any timeseries used for constants AND cwms timeseries
const actualSeries = timeseries.filter((ts) => (
ts.instrument_id === instrument.id &&
- instrument.constants.indexOf(ts.id) === -1
+ !['constant', 'cwms'].includes(ts.type)
));
const deleteSelectedRows = () => {
diff --git a/src/app-pages/project/batch-plotting/batch-plotting.jsx b/src/app-pages/project/batch-plotting/batch-plotting.jsx
index ee197f88..bce16db5 100644
--- a/src/app-pages/project/batch-plotting/batch-plotting.jsx
+++ b/src/app-pages/project/batch-plotting/batch-plotting.jsx
@@ -3,6 +3,7 @@ import { connect } from 'redux-bundler-react';
import { Engineering } from '@mui/icons-material';
import { Link } from '@mui/material';
import { toast } from 'react-toastify';
+import { useQueries } from '@tanstack/react-query';
import BullseyePlot from './chart-content/bullseye-plot.jsx';
import Card from '../../../app-components/card';
@@ -11,11 +12,12 @@ import DataConfiguration from './components/data-configuration';
import Map from '../../../app-components/classMap';
import ProfilePlot from './chart-content/profile-plot.jsx';
import ScatterLinePlot from './chart-content/scatter-line-plot.jsx';
+import { apiGet, buildQueryParams } from '../../../app-services/fetch-helpers.ts';
import { downloadFinalReport, useGetReportStatus, useInitializeReportDownload } from '../../../app-services/collections/report-configuration-download.ts';
+import { extractTimeseriesFrom, PlotTypeText } from './helper.js';
import { tUpdateSuccess } from '../../../common/helpers/toast-helpers';
import './batch-plotting.scss';
-import { PlotTypeText } from './helper.js';
const BatchPlotting = connect(
'doMapsInitialize',
@@ -26,7 +28,6 @@ const BatchPlotting = connect(
'selectBatchPlotConfigurationsActiveId',
'selectBatchPlotConfigurationsItemsObject',
'selectInstrumentTimeseriesItems',
- 'selectInstrumentsItems',
'selectDomainsItemsByGroup',
'selectProjectReportConfigurations',
({
@@ -37,14 +38,55 @@ const BatchPlotting = connect(
projectsByRoute: project,
batchPlotConfigurationsActiveId: batchPlotId,
batchPlotConfigurationsItemsObject: batchPlotItems,
+ instrumentTimeseriesItems: timeseries,
projectReportConfigurations: reportConfigs,
}) => {
+ const [toastId, setToastId] = useState(undefined);
+
const crossSectionReady = import.meta.env.VITE_CROSS_SECTION === 'true';
const userConfigId = hashQuery ? hashQuery['c'] : '';
const activeConfig = batchPlotItems[batchPlotId];
const { plot_type } = activeConfig || {};
+ const plotTimeseries = extractTimeseriesFrom(activeConfig, timeseries);
+ const cwmsTimeseries = plotTimeseries.filter(el => el.type === 'cwms');
+ const instrumentIds = cwmsTimeseries.map(t => t.instrument_id);
- const [toastId, setToastId] = useState(undefined);
+ const { data: midasCwmsTimeseries } = useQueries({
+ queries: instrumentIds.map((id) => ({
+ queryKey: ['midasCwmsTimeseriesPlotting', id],
+ queryFn: () => {
+ const uri = `/projects/${projectId}/instruments/${id}/timeseries/cwms`;
+ return apiGet(uri);
+ },
+ staleTime: Infinity,
+ enabled: !!cwmsTimeseries.length,
+ })),
+ combine: (ret) => {
+ return {
+ data: ret.map((result) => result.data).flat(),
+ }
+ },
+ });
+
+ const cwmsTimeseriesIds = midasCwmsTimeseries.map(ts => ts?.cwms_timeseries_id);
+
+ const { data: cwmsTimeseriesMeasurements } = useQueries({
+ queries: cwmsTimeseriesIds.map(ts => ({
+ queryKey: ['cwmsTimeseriesMeasurementsPlotting', ts],
+ queryFn: () => {
+ const { cwms_timeseries_id, cwms_office_id, cwms_extent_earliest_time } = midasCwmsTimeseries.find(el => el.cwms_timeseries_id === ts);
+ const uri = `/timeseries${buildQueryParams({ name: cwms_timeseries_id, office: cwms_office_id, begin: cwms_extent_earliest_time })}`;
+ return apiGet(uri, 'CWMS');
+ },
+ staleTime: Infinity,
+ enabled: !!midasCwmsTimeseries.length,
+ })),
+ combine: (ret) => {
+ return {
+ data: ret.map((result) => result.data),
+ }
+ },
+ });
const { id: projectId } = project;
const { data: jobDetails, mutate: initReportJobMutator } = useInitializeReportDownload(projectId);
@@ -77,6 +119,12 @@ const BatchPlotting = connect(
const containedInReports = reportConfigs?.filter(cfg => cfg.plot_configs.some(el => el.id === batchPlotId)) || [];
+ const cwmsProps = {
+ hasCwmsData: !!cwmsTimeseries.length,
+ cwmsData: cwmsTimeseriesMeasurements,
+ midasCwmsTimeseries,
+ };
+
return (
<>
@@ -88,8 +136,8 @@ const BatchPlotting = connect(
<>
- This Batch Plot Configuration is a part of the following reports. Click on the report name to download the report or click on the
- Remove Button to remove the plot configuration from the correlated Report Configuration.
+ This Batch Plot Configuration is a part of the following reports. Click on the report name to download the report.
+ {/* or click on the Remove Button to remove the plot configuration from the correlated Report Configuration. */}
{containedInReports.length ? containedInReports.map(cfg => (
- {plot_type === 'scatter-line' && }
+ {plot_type === 'scatter-line' && }
{plot_type === 'profile' && }
{plot_type === 'contour' && }
{plot_type === 'bullseye' && }
diff --git a/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx b/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx
index c8c6497d..05914de1 100644
--- a/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx
+++ b/src/app-pages/project/batch-plotting/chart-content/scatter-line-plot.jsx
@@ -25,6 +25,9 @@ const ScatterLinePlot = connect(
timeseriesMeasurementsItems: timeseriesMeasurements,
instrumentTimeseriesItems: timeseries,
plotConfig,
+ hasCwmsData,
+ cwmsData,
+ midasCwmsTimeseries,
}) => {
const { auto_range, date_range, display, show_masked, show_comments, show_nonvalidated, plot_type, id } = plotConfig || {};
@@ -36,7 +39,7 @@ const ScatterLinePlot = connect(
const [threshold, setThreshold] = useState(3000);
const [chartSettings, setChartSettings] = useState({ auto_range, display, show_masked, show_comments, show_nonvalidated, date_range, plot_type });
- const chartData = useMemo(() => generateNewChartData(plotMeasurements, plotTimeseries, chartSettings, plotConfig), [plotMeasurements, activeId]);
+ const chartData = useMemo(() => generateNewChartData(plotMeasurements, plotTimeseries, chartSettings, plotConfig, hasCwmsData, cwmsData, midasCwmsTimeseries), [plotMeasurements, activeId]);
const withPrecipitation = plotTimeseries.some(ts => ts.parameter === 'precipitation');
const layout = {
xaxis: {
diff --git a/src/app-pages/project/batch-plotting/helper.js b/src/app-pages/project/batch-plotting/helper.js
index 7eebdcf5..3dea2a01 100644
--- a/src/app-pages/project/batch-plotting/helper.js
+++ b/src/app-pages/project/batch-plotting/helper.js
@@ -55,7 +55,64 @@ export const determineDateRange = date_range => {
};
};
-export const generateNewChartData = (measurements, timeseries, chartSettings, plotConfig) => {
+export const extractTimeseriesFrom = (plotConfig = {}, timeseries = []) => {
+ if (!Object.keys(plotConfig).length || !timeseries.length) return [];
+
+ const { plot_type, display } = plotConfig;
+
+ switch (plot_type) {
+ case 'scatter-line': {
+ const { traces = [] } = display || {};
+ const tsIds = traces.map(t => t.timeseries_id);
+
+ return timeseries.filter(ts => tsIds.includes(ts.id));
+ }
+ case 'profile':
+ return [];
+ case 'contour':
+ return [];
+ case 'bullseye':
+ return [];
+ default:
+ return [];
+ }
+};
+
+const buildCwmsTraces = (cwmsData = [], cwmsTimeseries = [], plotConfig) => {
+ if (!cwmsData.length || !cwmsTimeseries.length) return [];
+
+ const data = cwmsData.filter(e => e);
+
+ if (!data.length) return [];
+ const { traces } = plotConfig?.display || {};
+
+ return traces.map(trace => {
+ const { timeseries_id } = trace;
+ const { cwms_timeseries_id, instrument, name, unit } = cwmsTimeseries.find(el => el.id === timeseries_id) || {};
+ const { values = [] } = cwmsData.find(el => el?.name === cwms_timeseries_id) || {};
+
+ const plotData = values.map(el => {
+ if (el?.length < 2) return null;
+
+ return {
+ x: DateTime.fromMillis(el[0]).toISO(),
+ y: el[1],
+ };
+ }).filter(e => e);
+
+ return {
+ ...getStyle(trace),
+ x: plotData.map(d => d.x),
+ y: plotData.map(d => d.y),
+ name: `${instrument} - ${name} (${unit})` || '',
+ showlegend: true,
+ hoverinfo: 'x+y+text',
+ timeseriesId: timeseries_id,
+ }
+ });
+};
+
+export const generateNewChartData = (measurements, timeseries, chartSettings, plotConfig, hasCwmsData = false, cwmsData = [], midasCwmsTimeseries = []) => {
const { show_comments, show_masked, show_nonvalidated } = chartSettings || {};
const { traces } = plotConfig?.display || {};
@@ -129,6 +186,10 @@ export const generateNewChartData = (measurements, timeseries, chartSettings, pl
}
}).filter(e => e);
+ if (hasCwmsData) {
+ data.push(...buildCwmsTraces(cwmsData, midasCwmsTimeseries, plotConfig));
+ }
+
return data || [];
}
diff --git a/src/app-services/collections/cwms-timeseries.ts b/src/app-services/collections/cwms-timeseries.ts
new file mode 100644
index 00000000..4f6f2d57
--- /dev/null
+++ b/src/app-services/collections/cwms-timeseries.ts
@@ -0,0 +1,113 @@
+import { QueryClient, useMutation, useQuery } from '@tanstack/react-query';
+
+import { apiGet, apiPost, apiPut, buildQueryParams } from '../fetch-helpers';
+
+type CwmsTimeseries = {
+ name: string,
+ instrument_id: string,
+ unit_id: string,
+ parameter_id: string,
+ cwms_office_id: string,
+ cwms_timeseries_id: string,
+ cwms_extent_earliest_time: string,
+};
+
+interface OfficeParams {
+ like?: string,
+ hasData?: boolean,
+}
+
+interface CwmsTimeseriesParams {
+ office: string;
+}
+
+interface CwmsTimeseriesMeasurementParams {
+ name: string,
+ office?: string,
+ page?: string,
+ begin?: string,
+ end?: string,
+}
+
+interface MidasCwmsPostTimeseriesParams {
+ projectId: string,
+ instrumentId: string,
+ body: CwmsTimeseries[],
+}
+
+interface MidasCwmsPutTimeseriesParams {
+ projectId: string,
+ instrumentId: string,
+ timeseriesId: string,
+ body: CwmsTimeseries,
+}
+
+export const useGetCwmsOffices = ({ hasData = true }: OfficeParams, opts: ClientQueryOptions) => {
+ const uri = `/offices${buildQueryParams({ hasData })}`;
+
+ return useQuery({
+ queryKey: [`cwmsOffices`, hasData],
+ queryFn: () => apiGet(uri, 'CWMS'),
+ ...opts,
+ });
+};
+
+export const useGetCwmsTimeseries = ({ office }: CwmsTimeseriesParams, opts: ClientQueryOptions) => {
+ const uri = `/catalog/TIMESERIES?office=${office}&page-size=5000`;
+
+ return useQuery({
+ queryKey: [`cwmsTimeseries`, office],
+ queryFn: () => apiGet(uri, 'CWMS'),
+ ...opts,
+ });
+};
+
+export const useGetCwmsTimeseriesMeasurements = ({ name, office, page, begin, end }: CwmsTimeseriesMeasurementParams, opts: ClientQueryOptions) => {
+ const uri = `/timeseries${buildQueryParams({ name, office, page, begin, end })}`;
+
+ return useQuery({
+ queryKey: [`cwmsTimeseriesMeasurements`, name, office, page, begin, end],
+ queryFn: () => apiGet(uri, 'CWMS'),
+ ...opts,
+ });
+};
+
+export const useGetMidasCwmsTimeseries = ({ projectId, instrumentId }: MidasCwmsPostTimeseriesParams, opts: ClientQueryOptions) => {
+ const uri = `/projects/${projectId}/instruments/${instrumentId}/timeseries/cwms`;
+
+ return useQuery({
+ queryKey: [`midasCwmsTimeseries`, projectId, instrumentId],
+ queryFn: () => apiGet(uri),
+ ...opts,
+ });
+};
+
+export const usePostMidasCwmsTimeseries = (client: QueryClient) => {
+ return useMutation({
+ mutationFn: ({ projectId, instrumentId, body }: MidasCwmsPostTimeseriesParams) => {
+ const uri = `/projects/${projectId}/instruments/${instrumentId}/timeseries/cwms`;
+
+ return apiPost(uri, body);
+ },
+ onSuccess: (_body, _, _ctx) => {
+ client.invalidateQueries({
+ queryKey: ['midasCwmsTimeseries'],
+ })
+ },
+ });
+};
+
+export const usePutMidasCwmsTimeseries = (client: QueryClient) => {
+ return useMutation({
+ mutationFn: ({ projectId, instrumentId, timeseriesId, body }: MidasCwmsPutTimeseriesParams) => {
+ const uri = `/projects/${projectId}/instruments/${instrumentId}/timeseries/cwms/${timeseriesId}`;
+
+ return apiPut(uri, body);
+ },
+ onSuccess: (_body, _, _ctx) => {
+ client.invalidateQueries({
+ queryKey: ['midasCwmsTimeseries'],
+ })
+ },
+ });
+};
diff --git a/src/app-services/collections/report-configuration-download.ts b/src/app-services/collections/report-configuration-download.ts
index d0d0414a..4e9f5354 100644
--- a/src/app-services/collections/report-configuration-download.ts
+++ b/src/app-services/collections/report-configuration-download.ts
@@ -10,48 +10,48 @@ interface QueryParams {
}
export const downloadFinalReport = ({ projectId, reportConfigId, jobId }: QueryParams) => {
- const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs/${jobId}/downloads`;
-
- apiGetBlob(uri)
- .then((file) => {
- const fileUrl = file ? URL.createObjectURL(file) : '';
- window.open(fileUrl, '_blank');
- window.URL.revokeObjectURL(fileUrl);
- return fileUrl;
- })
- .catch(err => console.error(err));
+ const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs/${jobId}/downloads`;
+
+ apiGetBlob(uri)
+ .then((file) => {
+ const fileUrl = file ? URL.createObjectURL(file) : '';
+ window.open(fileUrl, '_blank');
+ window.URL.revokeObjectURL(fileUrl);
+ return fileUrl;
+ })
+ .catch(err => console.error(err));
};
export const useGetReportStatus = ({ projectId, reportConfigId, jobId }: QueryParams, opts: ClientQueryOptions) => {
- const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs/${jobId}`;
+ const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs/${jobId}`;
- return useQuery({
- queryKey: [`reportStatus`, reportConfigId, jobId],
- queryFn: () => apiGet(uri),
- ...opts,
- });
+ return useQuery({
+ queryKey: [`reportStatus`, reportConfigId, jobId],
+ queryFn: () => apiGet(uri),
+ ...opts,
+ });
};
export const useInitializeReportDownload = (projectId: string, toastId: string) => {
- return useMutation({
- mutationFn: (reportConfigId: string) => {
- const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs`;
-
- return apiPost(uri);
- },
- onError: (err, __, _ctx) => {
- toast.update(toastId, {
- render: 'Failed to initialize report job. Please try again later.',
- type: 'error',
- isLoading: false,
- autoClose: 5000,
- closeOnClick: true,
- draggable: true,
- })
- return { error: 'Failed to Initialize Job', _raw: err };
- },
- onSuccess: (body, _, _ctx) => {
- return body;
- },
- });
+ return useMutation({
+ mutationFn: (reportConfigId: string) => {
+ const uri = `/projects/${projectId}/report_configs/${reportConfigId}/jobs`;
+
+ return apiPost(uri);
+ },
+ onError: (err, __, _ctx) => {
+ toast.update(toastId, {
+ render: 'Failed to initialize report job. Please try again later.',
+ type: 'error',
+ isLoading: false,
+ autoClose: 5000,
+ closeOnClick: true,
+ draggable: true,
+ })
+ return { error: 'Failed to Initialize Job', _raw: err };
+ },
+ onSuccess: (body, _, _ctx) => {
+ return body;
+ },
+ });
};
diff --git a/src/app-services/fetch-helpers.ts b/src/app-services/fetch-helpers.ts
index a319fa3f..a634393c 100644
--- a/src/app-services/fetch-helpers.ts
+++ b/src/app-services/fetch-helpers.ts
@@ -1,6 +1,14 @@
import { getToken } from '../userService';
+type ApiSource = 'MIDAS' | 'CWMS';
+
const API_ROOT = import.meta.env.VITE_API_URL as string;
+const CWMS_ROOT = import.meta.env.VITE_CWMS_API_URL as string;
+
+const roots = {
+ 'MIDAS': API_ROOT,
+ 'CWMS': CWMS_ROOT,
+};
interface OptsType extends RequestInit {
isFormData?: boolean;
@@ -10,6 +18,16 @@ interface CommonItems {
root: string;
}
+export const buildQueryParams = (params: Record) => {
+ const keys = Object.keys(params);
+ const mapped = keys.map(key => {
+ if (!key || params[key] === undefined) return null;
+ return `${key}=${params[key]}`;
+ }).filter(e => e);
+
+ return `?${mapped.join('&')}`;
+};
+
export const commonFetch = async (root: string, path: string, options: OptsType): Promise => {
const res = await fetch(`${root}${path}`, options);
@@ -19,12 +37,17 @@ export const commonFetch = async (root: string, path: string, options: OptsType)
return (await res.json()) as JSON;
};
-const getCommonItems = (): CommonItems => ({
- root: API_ROOT,
-});
+const getCommonItems = (source: ApiSource = 'MIDAS'): CommonItems => {
+ return ({
+ root: roots[source],
+ });
+};
-const defaultHeaders = () => ({
- Authorization: `Bearer ${getToken()}`,
+const defaultHeaders = (source: ApiSource = 'MIDAS') => ({
+ Authorization: source !== 'MIDAS' ? '' : `Bearer ${getToken()}`,
+ ...source === 'CWMS' && {
+ accept: 'application/json;version=2',
+ },
});
export const apiFetch = (path: string, options: OptsType) => {
@@ -34,11 +57,11 @@ export const apiFetch = (path: string, options: OptsType) => {
return fetch(`${root}${path}`, options);
};
-export const apiGet = (path: string) => {
- const { root } = getCommonItems();
+export const apiGet = (path: string, source?: ApiSource) => {
+ const { root } = getCommonItems(source);
const options = { method: 'GET' } as OptsType;
- options.headers = { ...defaultHeaders() };
+ options.headers = { ...defaultHeaders(source) };
return commonFetch(root, path, options);
};
@@ -52,7 +75,7 @@ export const apiGetBlob = async (path: string) => {
return response.blob();
};
-export const apiPut = (path: string, payload: JSON) => {
+export const apiPut = (path: string, payload: JSON | Record) => {
const { root } = getCommonItems();
const options = {
method: 'PUT',
@@ -76,11 +99,11 @@ export const apiPut = (path: string, payload: JSON) => {
/**
* Function to send a POST request to the api.
* @param {String} path - The URI of the request, will be appended to base path.
- * @param {JSON | FormData} payload - The payload of the request. If FormData, request will be sent with Content-Type: multipart/form-data
+ * @param {JSON | FormData | any[]} payload - The payload of the request. If FormData, request will be sent with Content-Type: multipart/form-data
* @param {OptsType} opts - Options provided to the function to alter the request.
* @returns
*/
-export const apiPost = (path: string, payload?: JSON | FormData, opts?: OptsType) => {
+export const apiPost = (path: string, payload?: JSON | FormData | any[], opts?: OptsType) => {
const { root } = getCommonItems();
const { isFormData } = opts || {};
@@ -103,7 +126,7 @@ export const apiPost = (path: string, payload?: JSON | FormData, opts?: OptsType
return commonFetch(root, path, options);
};
-export const apiPatch = (path: string, payload: JSON | FormData) => {
+export const apiPatch = (path: string, payload: JSON | FormData | Record) => {
const { root } = getCommonItems();
const options = {
method: 'PATCH',
diff --git a/src/common/forms/instrument-form.jsx b/src/common/forms/instrument-form.jsx
index bcf29c2e..81d93b6f 100644
--- a/src/common/forms/instrument-form.jsx
+++ b/src/common/forms/instrument-form.jsx
@@ -51,6 +51,7 @@ export default connect(
isEdit = true,
}) => {
const [name, setName] = useState(item?.name || '');
+ const [showCwms, setShowCwms] = useState(item?.show_cwms_tab || '');
const [type_id, setTypeId] = useState(item?.type_id || '');
const [station, setStation] = useState(item?.station || '');
const [offset, setOffset] = useState(item?.offset || '');
@@ -125,6 +126,7 @@ export default connect(
type_id: type_id ?? item.type_id,
status_id: status_id ?? item.status_id,
status_time,
+ show_cwms_tab: showCwms,
opts,
station:
station === null || station === ''
@@ -209,6 +211,15 @@ export default connect(
placeholder='Name'
/>
+
+ setShowCwms(e.target.checked)}
+ />
+ Contains CWMS Data
+
Type
setTypeId(val?.id)} domain='instrument_type' />