Skip to content

Commit

Permalink
enhancement/cwms-timeseries-list (#246)
Browse files Browse the repository at this point in the history
* working inf scroll on CWMS Timeseries
* bugfix: autocomplete default failing if rendered before domain group finished fetching
  • Loading branch information
KevinJJackson authored Dec 2, 2024
1 parent e394975 commit 87d2297
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 35 deletions.
40 changes: 38 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
29 changes: 15 additions & 14 deletions src/app-components/domain-select.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,21 @@ const DomainSelect = ({

return (
<div {...customProps}>
<Autocomplete
loading={isLoading}
noOptionsText='No Options...'
size='small'
defaultValue={options?.find(el => 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) => <TextField {...params} label={label} placeholder='Select one...' />}
options={options}
fullWidth
/>
{!isLoading && (
<Autocomplete
noOptionsText='No Options...'
size='small'
defaultValue={options?.find(el => 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) => <TextField {...params} label={label} placeholder='Select one...' />}
options={options}
fullWidth
/>
)}
</div>
);
};
Expand Down
89 changes: 79 additions & 10 deletions src/app-pages/instrument/cwms-timeseries/newCwmsTimeseries.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<Box
key={key}
component='li'
ref={isLast ? sentryRef : null}
{...optionProps}
>
{ownerState.getOptionLabel(option)}
</Box>
);
};

const _filterOptions = createFilterOptions();

const NewCwmsTimeseriesModal = connect(
'doModalClose',
'doInstrumentTimeseriesDelete',
Expand All @@ -45,18 +70,49 @@ 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 : '');

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 (
<Modal.ModalContent style={{ overflow: 'visible' }}>
<Modal.ModalHeader title={isEdit ? 'Edit Connected CWMS Timeseries' : 'Connect CWMS Timeseries to Instrument'} />
Expand All @@ -66,6 +122,7 @@ const NewCwmsTimeseriesModal = connect(
<Autocomplete
size='small'
value={selectedOffice}
filterOptions={filterOptions}
isOptionEqualToValue={(opt, val) => opt.value === val || opt.value === val.value}
onChange={(_e, value) => setSelectedOffice(value)}
renderInput={(params) => <TextField {...params} label='Office' placeholder='Select an office...' />}
Expand All @@ -74,14 +131,26 @@ const NewCwmsTimeseriesModal = connect(
/>
{selectedOffice && (
<Autocomplete
disablePortal
fullWidth
size='small'
className='mt-3'
value={selectedTimeseries}
inputValue={input}
onInputChange={(_e, newInputValue) => {
setInput(newInputValue);
}}
isOptionEqualToValue={(opt, val) => opt.value === val || opt.value === val.value}
onChange={(_e, value) => setSelectedTimeseries(value)}
renderInput={(params) => <TextField {...params} label='CWMS Timeseries' placeholder='Select a timeseries...' />}
renderOption={(props, option, state, ownerState) => renderListItem(props, option, state, ownerState, sentryRef)}
renderInput={(params) => (
<TextField
{...params}
label='CWMS Timeseries'
placeholder='Select a timeseries...'
/>
)}
options={cwmsTimeseriesOptions}
fullWidth
/>
)}
{selectedTimeseries && (
Expand Down
21 changes: 14 additions & 7 deletions src/app-services/collections/cwms-timeseries.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -19,6 +19,8 @@ interface OfficeParams {

interface CwmsTimeseriesParams {
office: string;
like?: string;
pageParam?: string;
}

interface CwmsTimeseriesMeasurementParams {
Expand Down Expand Up @@ -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,
});
};
Expand Down
2 changes: 1 addition & 1 deletion src/app-services/fetch-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface CommonItems {
export const buildQueryParams = (params: Record<string, any>) => {
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);

Expand Down

0 comments on commit 87d2297

Please sign in to comment.