diff --git a/src/common/api/collectionsApi.ts b/src/common/api/collectionsApi.ts index d8200c5d..7b5de01b 100644 --- a/src/common/api/collectionsApi.ts +++ b/src/common/api/collectionsApi.ts @@ -8,7 +8,7 @@ const collectionsService = httpService({ url: 'services/collections', }); -type ProcessState = 'processing' | 'failed' | 'complete'; +export type ProcessState = 'processing' | 'failed' | 'complete'; type UPA = string; // Collections-specific item ID strings (not an UPA or Data Object) type KBaseId = string; diff --git a/src/features/collections/MatchModal.tsx b/src/features/collections/MatchModal.tsx index 96f7177a..23272d8b 100644 --- a/src/features/collections/MatchModal.tsx +++ b/src/features/collections/MatchModal.tsx @@ -10,14 +10,14 @@ import { listObjects } from '../../common/api/workspaceApi'; import { getNarratives } from '../../common/api/searchApi'; import { parseError } from '../../common/api/utils/parseError'; import { useUpdateAppParams } from '../params/hooks'; -import { useAppDispatch, useBackoffPolling } from '../../common/hooks'; +import { useAppDispatch } from '../../common/hooks'; import { setLocalSelection, useCurrentSelection, useMatchId, } from './collectionsSlice'; import { store } from '../../app/store'; -import { useParamsForNarrativeDropdown } from './hooks'; +import { useParamsForNarrativeDropdown, useProcessStatePolling } from './hooks'; import { MatcherUserParams } from './MatcherUserParams'; import Ajv from 'ajv'; import { Modal, useModalControls } from '../layout/Modal'; @@ -54,14 +54,8 @@ const ViewMatch = ({ collectionId }: { collectionId: string }) => { const matchQuery = getMatch.useQuery(matchId || '', { skip: !matchId, }); - useBackoffPolling( - matchQuery, - (result) => - !( - Boolean(result.error) || - (Boolean(result.data?.state) && result.data?.state !== 'processing') - ) - ); + useProcessStatePolling(matchQuery, ['state'], { skipPoll: !matchId }); + const match = matchQuery.data; const matchStategy = match?.state === 'complete' diff --git a/src/features/collections/collectionsSlice.ts b/src/features/collections/collectionsSlice.ts index 555bb803..5de28867 100644 --- a/src/features/collections/collectionsSlice.ts +++ b/src/features/collections/collectionsSlice.ts @@ -7,12 +7,9 @@ import { getSelection, } from '../../common/api/collectionsApi'; import { parseError } from '../../common/api/utils/parseError'; -import { - useAppDispatch, - useAppSelector, - useBackoffPolling, -} from '../../common/hooks'; +import { useAppDispatch, useAppSelector } from '../../common/hooks'; import { useAppParam } from '../params/hooks'; +import { useProcessStatePolling } from './hooks'; interface SelectionState { current: string[]; @@ -348,12 +345,15 @@ export const useGenerateSelectionId = ( skip: !_pendingId || !!_verifiedId, }); - useBackoffPolling(validateSelection, (result) => { - if (result.error) toast(parseError(result.error).message); - if (result.data?.state === 'processing') return true; - return false; + useProcessStatePolling(validateSelection, ['state'], { + skipPoll: !_pendingId || !!_verifiedId, }); + useEffect(() => { + if (validateSelection.error) + toast(parseError(validateSelection.error).message); + }, [validateSelection.error]); + useEffect(() => { if (validateSelection.data?.state === 'complete') { if ( diff --git a/src/features/collections/data_products/Biolog.tsx b/src/features/collections/data_products/Biolog.tsx index 7ccb3952..6ce5840d 100644 --- a/src/features/collections/data_products/Biolog.tsx +++ b/src/features/collections/data_products/Biolog.tsx @@ -15,11 +15,12 @@ import { import { parseError } from '../../../common/api/utils/parseError'; import { DataViewLink } from '../../../common/components'; import { Pagination, usePageBounds } from '../../../common/components/Table'; -import { useAppDispatch, useBackoffPolling } from '../../../common/hooks'; +import { useAppDispatch } from '../../../common/hooks'; import { formatNumber } from '../../../common/utils/stringUtils'; import { useAppParam } from '../../params/hooks'; import classes from '../Collections.module.scss'; import { useGenerateSelectionId } from '../collectionsSlice'; +import { useProcessStatePolling } from '../hooks'; import { HeatMap, HeatMapCallback, MAX_HEATMAP_PAGE } from './HeatMap'; export const Biolog: FC<{ @@ -148,15 +149,9 @@ const useBiolog = (collection_id: string | undefined) => { skip: !collection_id, }); const biolog = biologQuery.data; - useBackoffPolling( - biologQuery, - (result) => { - if (matchId && result?.data?.match_state === 'processing') return true; - if (selId && result?.data?.selection_state === 'processing') return true; - return false; - }, - { skipPoll: !collection_id || !(matchId || selId) } - ); + useProcessStatePolling(biologQuery, ['match_state', 'selection_state'], { + skipPoll: !collection_id || !(matchId || selId), + }); //cache last row of each page, we should implement better backend pagination this is silly useEffect(() => { diff --git a/src/features/collections/data_products/Microtrait.tsx b/src/features/collections/data_products/Microtrait.tsx index f34d222e..85fab350 100644 --- a/src/features/collections/data_products/Microtrait.tsx +++ b/src/features/collections/data_products/Microtrait.tsx @@ -15,10 +15,11 @@ import { import { parseError } from '../../../common/api/utils/parseError'; import { DataViewLink } from '../../../common/components'; import { Pagination, usePageBounds } from '../../../common/components/Table'; -import { useAppDispatch, useBackoffPolling } from '../../../common/hooks'; +import { useAppDispatch } from '../../../common/hooks'; import { formatNumber } from '../../../common/utils/stringUtils'; import classes from '../Collections.module.scss'; import { useMatchId, useGenerateSelectionId } from '../collectionsSlice'; +import { useProcessStatePolling } from '../hooks'; import { HeatMap, HeatMapCallback, MAX_HEATMAP_PAGE } from './HeatMap'; const getUPAFromEncoded = (encoded: string) => encoded.replaceAll('_', '/'); @@ -151,15 +152,9 @@ const useMicrotrait = (collection_id: string | undefined) => { skip: !collection_id, }); const microtrait = microtraitQuery.data; - useBackoffPolling( - microtraitQuery, - (result) => { - if (matchId && result?.data?.match_state === 'processing') return true; - if (selId && result?.data?.selection_state === 'processing') return true; - return false; - }, - { skipPoll: !collection_id || !(matchId || selId) } - ); + useProcessStatePolling(microtraitQuery, ['match_state', 'selection_state'], { + skipPoll: !collection_id || !(matchId || selId), + }); //cache last row of each page, we should implement better backend pagination this is silly useEffect(() => { diff --git a/src/features/collections/data_products/SampleAttribs.tsx b/src/features/collections/data_products/SampleAttribs.tsx index b47569a2..59b32ec2 100644 --- a/src/features/collections/data_products/SampleAttribs.tsx +++ b/src/features/collections/data_products/SampleAttribs.tsx @@ -19,7 +19,7 @@ import { usePageBounds, useTableColumns, } from '../../../common/components/Table'; -import { useAppDispatch, useBackoffPolling } from '../../../common/hooks'; +import { useAppDispatch } from '../../../common/hooks'; import { clearAllFilters, setFilter, @@ -31,7 +31,7 @@ import classes from './../Collections.module.scss'; import { Alert, Grid, Paper, PaperProps, Stack, Link } from '@mui/material'; import { formatNumber } from '../../../common/utils/stringUtils'; import { filterContextMode, useFilterContexts } from '../Filters'; -import { useTableViewParams } from '../hooks'; +import { useProcessStatePolling, useTableViewParams } from '../hooks'; export const SampleAttribs: FC<{ collection_id: string; @@ -92,7 +92,8 @@ export const SampleAttribs: FC<{ [attribParams] ); // Current Data - const { data, isFetching } = getSampleAttribs.useQuery(attribParams); + const result = getSampleAttribs.useQuery(attribParams); + const { data, isFetching } = result; const { data: countData } = getSampleAttribs.useQuery(countParams); const allCountParams = useMemo( () => ({ @@ -127,11 +128,19 @@ export const SampleAttribs: FC<{ selectData.data?.selection_state === 'processing' ); - useBackoffPolling( - selectData, - (result) => selectionPending || !result.data?.selection_state, - { skipPoll: !selectData.data?.selection_state } - ); + // Polling needed for main result, as it is not a primary dataproduct like genomes + const pollFor: ('match_state' | 'selection_state')[] = [ + 'match_state', + 'selection_state', + ]; + if (!attribParams.match_id) pollFor.splice(pollFor.indexOf('match_state'), 1); + if (!attribParams.selection_id) + pollFor.splice(pollFor.indexOf('selection_state'), 1); + useProcessStatePolling(result, pollFor, { + skipPoll: !(attribParams.match_id || attribParams.selection_id), + }); + // Also poll for the selection state + useProcessStatePolling(selectData, ['selection_state']); useFilterContexts(collection_id, [ { diff --git a/src/features/collections/data_products/TaxaCount.tsx b/src/features/collections/data_products/TaxaCount.tsx index 34395ab0..b1d5ab8a 100644 --- a/src/features/collections/data_products/TaxaCount.tsx +++ b/src/features/collections/data_products/TaxaCount.tsx @@ -13,12 +13,12 @@ import { } from '../../../common/api/collectionsApi'; import { Loader } from '../../../common/components/Loader'; import { Select, SelectOption } from '../../../common/components/Select'; -import { useBackoffPolling } from '../../../common/hooks'; import { snakeCaseToHumanReadable } from '../../../common/utils/stringUtils'; import { useMatchId, useSelectionId } from '../collectionsSlice'; import classes from './TaxaCount.module.scss'; import { Paper, PaperProps, Stack } from '@mui/material'; import { useFilterContexts } from '../Filters'; +import { useProcessStatePolling } from '../hooks'; export const TaxaCount: FC<{ collection_id: string; @@ -105,12 +105,7 @@ export const TaxaCount: FC<{ skip: !rank, }); - useBackoffPolling(countsQuery, (result) => { - if (matchId && result?.data?.match_state === 'processing') return true; - if (selectionId && result?.data?.selection_state === 'processing') - return true; - return false; - }); + useProcessStatePolling(countsQuery, ['match_state', 'selection_state']); const taxa = countsQuery.data?.data || []; diff --git a/src/features/collections/hooks.ts b/src/features/collections/hooks.ts index 2713e31c..186db295 100644 --- a/src/features/collections/hooks.ts +++ b/src/features/collections/hooks.ts @@ -1,6 +1,10 @@ +import { QueryDefinition } from '@reduxjs/toolkit/dist/query'; +import { UseQueryHookResult } from '@reduxjs/toolkit/dist/query/react/buildHooks'; import { useMemo } from 'react'; +import { toast } from 'react-hot-toast'; +import { ProcessState } from '../../common/api/collectionsApi'; import { getNarratives } from '../../common/api/searchApi'; -import { useAppSelector } from '../../common/hooks'; +import { useAppSelector, useBackoffPolling } from '../../common/hooks'; import { FilterContext, useFilters, @@ -90,3 +94,38 @@ export const useTableViewParams = ( ] ); }; + +export const useProcessStatePolling = < + StateKey extends string, + Result extends { [processStateKey in StateKey]: ProcessState }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + R extends UseQueryHookResult> +>( + result: R, + processStateKeys: StateKey[], + options?: { baseInterval?: number; rate?: number; skipPoll?: boolean } +) => { + useBackoffPolling( + result, + (result) => { + for (let i = 0; i < processStateKeys.length; i++) { + const processStateKey = processStateKeys[i]; + if (result.error || !result.data?.[processStateKey]) return false; + if (result.data[processStateKey] === 'processing') { + continue; + } else if (result.data[processStateKey] === 'complete') { + return false; + } else if (result.data[processStateKey] === 'failed') { + toast('ProcessState Polling Failed, see console for more info'); + // eslint-disable-next-line no-console + console.error('ProcessState Polling Failed', { result }); + return false; + } else { + return false; + } + } + return true; + }, + options + ); +};