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);