diff --git a/package-lock.json b/package-lock.json
index dc039670..3988352a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "hhd-ui",
- "version": "0.18.0",
+ "version": "0.18.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "hhd-ui",
- "version": "0.18.0",
+ "version": "0.18.1",
"dependencies": {
"@ag-grid-community/client-side-row-model": "^30.0.3",
"@ag-grid-community/core": "^30.0.3",
@@ -46,6 +46,7 @@
"react-csv": "^2.2.2",
"react-datepicker": "^4.16.0",
"react-dom": "^18.2.0",
+ "react-infinite-scroll-hook": "^5.0.1",
"react-papaparse": "^4.1.0",
"react-plotly.js": "^2.6.0",
"react-resizable-panels": "^2.0.7",
@@ -6800,6 +6801,27 @@
"version": "3.2.0",
"license": "MIT"
},
+ "node_modules/react-infinite-scroll-hook": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/react-infinite-scroll-hook/-/react-infinite-scroll-hook-5.0.1.tgz",
+ "integrity": "sha512-fn6+8BAZLQ9C1fvO5kPicGjDR2WHxK7rP4aaSWuaJkvtoJjYuudGJ9wjgPox7dghKm5Xj9cpKFycM86/wAJ3ig==",
+ "dependencies": {
+ "react-intersection-observer-hook": "^3.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/react-intersection-observer-hook": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/react-intersection-observer-hook/-/react-intersection-observer-hook-3.0.1.tgz",
+ "integrity": "sha512-nehzFUV+jVY49tc9FOm4yJHrmFFtCmsBZKiN1rUUB2675CMBf6kKu1ekuTqvCnmh4XG7RFP3bMyAevfqfV94xw==",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"license": "MIT"
@@ -13678,6 +13700,20 @@
"react-fast-compare": {
"version": "3.2.0"
},
+ "react-infinite-scroll-hook": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/react-infinite-scroll-hook/-/react-infinite-scroll-hook-5.0.1.tgz",
+ "integrity": "sha512-fn6+8BAZLQ9C1fvO5kPicGjDR2WHxK7rP4aaSWuaJkvtoJjYuudGJ9wjgPox7dghKm5Xj9cpKFycM86/wAJ3ig==",
+ "requires": {
+ "react-intersection-observer-hook": "^3.0.0"
+ }
+ },
+ "react-intersection-observer-hook": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/react-intersection-observer-hook/-/react-intersection-observer-hook-3.0.1.tgz",
+ "integrity": "sha512-nehzFUV+jVY49tc9FOm4yJHrmFFtCmsBZKiN1rUUB2675CMBf6kKu1ekuTqvCnmh4XG7RFP3bMyAevfqfV94xw==",
+ "requires": {}
+ },
"react-is": {
"version": "17.0.2"
},
diff --git a/package.json b/package.json
index 83fce011..54af6bbb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hhd-ui",
- "version": "0.18.1",
+ "version": "0.18.2",
"private": true,
"dependencies": {
"@ag-grid-community/client-side-row-model": "^30.0.3",
@@ -41,6 +41,7 @@
"react-csv": "^2.2.2",
"react-datepicker": "^4.16.0",
"react-dom": "^18.2.0",
+ "react-infinite-scroll-hook": "^5.0.1",
"react-papaparse": "^4.1.0",
"react-plotly.js": "^2.6.0",
"react-resizable-panels": "^2.0.7",
diff --git a/src/app-components/domain-select.jsx b/src/app-components/domain-select.jsx
index cec8690b..e5e4d0e5 100644
--- a/src/app-components/domain-select.jsx
+++ b/src/app-components/domain-select.jsx
@@ -29,20 +29,21 @@ const DomainSelect = ({
return (
-
el[useLabelAsDefault ? 'label' : 'value'] === defaultValue)}
- isOptionEqualToValue={(opt, val) => opt.value === val.value}
- onChange={(_e, value) => {
- const item = domainsByGroup[domain]?.find(el => el.value === value?.label);
- onChange(item);
- }}
- renderInput={(params) => }
- options={options}
- fullWidth
- />
+ {!isLoading && (
+ el[useLabelAsDefault ? 'label' : 'value'] === defaultValue)}
+ isOptionEqualToValue={(opt, val) => opt.value === val.value}
+ onChange={(_e, value) => {
+ const item = domainsByGroup[domain]?.find(el => el.value === value?.label);
+ onChange(item);
+ }}
+ renderInput={(params) => }
+ options={options}
+ fullWidth
+ />
+ )}
);
};
diff --git a/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx b/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx
index 1c969791..a6ed5a70 100644
--- a/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx
+++ b/src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx
@@ -1,7 +1,9 @@
-import React, { useMemo, useState } from 'react';
-import { Autocomplete, TextField } from '@mui/material';
+import React, { useCallback, useMemo, useState } from 'react';
+import { Autocomplete, Box, createFilterOptions, TextField } from '@mui/material';
import { connect } from 'redux-bundler-react';
import { useQueryClient } from '@tanstack/react-query';
+import { useDebounce } from 'react-use';
+import useInfiniteScroll from 'react-infinite-scroll-hook';
import * as Modal from '../../../app-components/modal';
import DomainSelect from '../../../app-components/domain-select.jsx';
@@ -17,17 +19,40 @@ const generateOfficesOptions = cwmsOffices => {
};
const generateCwmsTimeseriesOptions = cwmsTimeseries => {
- const { total, entries } = cwmsTimeseries || {};
+ const { pages } = cwmsTimeseries || {};
- if (!total || !entries) return [];
+ if (!pages?.length) return [];
- return entries.map(entry => ({
+ const entries = pages.map(p => p.entries).flat();
+
+ const elements = entries.map((entry, i) => ({
_extents: entry?.extents?.length ? entry.extents[0] : {},
label: entry?.name,
value: entry?.name,
+ isLast: pages.at(-1)['next-page'] && i === entries.length - 1,
}));
+
+ return elements;
+};
+
+const renderListItem = (props, option, _state, ownerState, sentryRef) => {
+ const { key, ...optionProps } = props;
+ const { isLast } = option;
+
+ return (
+
+ {ownerState.getOptionLabel(option)}
+
+ );
};
+const _filterOptions = createFilterOptions();
+
const NewCwmsTimeseriesModal = connect(
'doModalClose',
'doInstrumentTimeseriesDelete',
@@ -45,6 +70,8 @@ const NewCwmsTimeseriesModal = connect(
const [selectedOffice, setSelectedOffice] = useState(isEdit ? item?.cwms_office_id : '');
const [selectedTimeseries, setSelectedTimeseries] = useState(isEdit ? item?.cwms_timeseries_id : '');
+ const [input, setInput] = useState('');
+ const [likeQuery, setLikeQuery] = useState('');
const [name, setName] = useState(isEdit ? item?.name : '');
const [parameterId, setParameterId] = useState(isEdit ? item?.parameter_id : '');
const [unitId, setUnitId] = useState(isEdit ? item?.unit_id : '');
@@ -52,11 +79,40 @@ const NewCwmsTimeseriesModal = connect(
const { data: cwmsOffices } = useGetCwmsOffices({});
const cwmsOfficesOptions = useMemo(() => generateOfficesOptions(cwmsOffices), [cwmsOffices]);
- const { data: cwmsTimeseries } = useGetCwmsTimeseries({ office: selectedOffice?.value || selectedOffice }, {
- enabled: !!selectedOffice?.value || !!selectedOffice,
- });
+ const { data: cwmsTimeseries, error, isFetching, fetchNextPage } = useGetCwmsTimeseries(
+ {
+ office: selectedOffice?.value || selectedOffice,
+ like: likeQuery,
+ }, { enabled: !!selectedOffice?.value || !!selectedOffice }
+ );
+
const cwmsTimeseriesOptions = useMemo(() => generateCwmsTimeseriesOptions(cwmsTimeseries), [cwmsTimeseries]);
+ const [optionCount, setOptionCount] = useState(0);
+ const filterOptions = useCallback((options, state) => {
+ const results = _filterOptions(options, state);
+
+ if (optionCount !== results.length) {
+ setOptionCount(results.length);
+ }
+
+ return results;
+ });
+
+ const [,] = useDebounce(() => {
+ setLikeQuery(input);
+ }, 300, [input]);
+
+ const [sentryRef] = useInfiniteScroll({
+ loading: isFetching,
+ hasNextPage: !!(cwmsTimeseries?.pages?.at(-1) || {})['next-page'],
+ disabled: !!error,
+ onLoadMore: () => {
+ fetchNextPage();
+ },
+ rootMargin: '0px 0px 100px 0px',
+ });
+
return (
@@ -66,6 +122,7 @@ const NewCwmsTimeseriesModal = connect(
opt.value === val || opt.value === val.value}
onChange={(_e, value) => setSelectedOffice(value)}
renderInput={(params) => }
@@ -74,14 +131,26 @@ const NewCwmsTimeseriesModal = connect(
/>
{selectedOffice && (
{
+ setInput(newInputValue);
+ }}
isOptionEqualToValue={(opt, val) => opt.value === val || opt.value === val.value}
onChange={(_e, value) => setSelectedTimeseries(value)}
- renderInput={(params) => }
+ renderOption={(props, option, state, ownerState) => renderListItem(props, option, state, ownerState, sentryRef)}
+ renderInput={(params) => (
+
+ )}
options={cwmsTimeseriesOptions}
- fullWidth
/>
)}
{selectedTimeseries && (
diff --git a/src/app-services/collections/cwms-timeseries.ts b/src/app-services/collections/cwms-timeseries.ts
index 4f6f2d57..25d57f8b 100644
--- a/src/app-services/collections/cwms-timeseries.ts
+++ b/src/app-services/collections/cwms-timeseries.ts
@@ -1,4 +1,4 @@
-import { QueryClient, useMutation, useQuery } from '@tanstack/react-query';
+import { QueryClient, useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
import { apiGet, apiPost, apiPut, buildQueryParams } from '../fetch-helpers';
@@ -19,6 +19,8 @@ interface OfficeParams {
interface CwmsTimeseriesParams {
office: string;
+ like?: string;
+ pageParam?: string;
}
interface CwmsTimeseriesMeasurementParams {
@@ -52,12 +54,17 @@ export const useGetCwmsOffices = ({ hasData = true }: OfficeParams, opts: Client
});
};
-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'),
+export const useGetCwmsTimeseries = ({ office, like }: CwmsTimeseriesParams, opts: ClientQueryOptions) => {
+ const buildUri = (pageParam?: string) => {
+ const query = buildQueryParams({ office, like, page: pageParam, 'page-size': 500 });
+ return `/catalog/TIMESERIES${query}`;
+ };
+
+ return useInfiniteQuery({
+ queryKey: [`cwmsTimeseries`, office, like],
+ queryFn: ({ pageParam }) => apiGet(buildUri(pageParam), 'CWMS'),
+ initialPageParam: undefined,
+ getNextPageParam: (lastPage) => lastPage['next-page'],
...opts,
});
};
diff --git a/src/app-services/fetch-helpers.ts b/src/app-services/fetch-helpers.ts
index 1ac7c726..90a8af3a 100644
--- a/src/app-services/fetch-helpers.ts
+++ b/src/app-services/fetch-helpers.ts
@@ -21,7 +21,7 @@ interface CommonItems {
export const buildQueryParams = (params: Record) => {
const keys = Object.keys(params);
const mapped = keys.map(key => {
- if (!key || params[key] === undefined) return null;
+ if (!key || params[key] === undefined || params[key] === '') return null;
return `${key}=${params[key]}`;
}).filter(e => e);