From 081420ab8306d5b97c27a4ae2f3b91d003d851d8 Mon Sep 17 00:00:00 2001 From: Guillaume Paris Date: Fri, 18 Oct 2024 10:45:54 +0200 Subject: [PATCH] [frontend] Remove duplicate requests for pagination & filters --- .../queryable/filter/useFiltersState.tsx | 8 ++- .../pagination/PaginationComponentV2.tsx | 5 -- .../pagination/usPaginationState.tsx | 8 ++- .../common/queryable/sort/useSortState.tsx | 8 ++- .../textSearch/useTextSearchState.tsx | 8 ++- .../common/queryable/uri/useUriState.tsx | 54 ++++++++++++------- .../useQueryableWithLocalStorage.tsx | 22 ++++++-- .../src/utils/hooks/useDataLoader.js | 9 +++- 8 files changed, 83 insertions(+), 39 deletions(-) diff --git a/openbas-front/src/components/common/queryable/filter/useFiltersState.tsx b/openbas-front/src/components/common/queryable/filter/useFiltersState.tsx index 7aa7dfd6d6..263898fd71 100644 --- a/openbas-front/src/components/common/queryable/filter/useFiltersState.tsx +++ b/openbas-front/src/components/common/queryable/filter/useFiltersState.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { emptyFilterGroup } from './FilterUtils'; import { FilterHelpers } from './FilterHelpers'; import { @@ -24,6 +24,7 @@ const useFiltersState = ( const [filtersState, setFiltersState] = useState({ filters: initFilters, }); + const hasBeenInitialized = useRef(false); const helpers: FilterHelpers = { // Switch filter group operator handleSwitchMode: () => { @@ -74,7 +75,10 @@ const useFiltersState = ( }; useEffect(() => { - onChange?.(filtersState.filters); + if (hasBeenInitialized.current) { + onChange?.(filtersState.filters); + } + hasBeenInitialized.current = true; }, [filtersState]); return [filtersState.filters, helpers]; diff --git a/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx b/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx index 7b0ab930a1..f73ce4edfa 100644 --- a/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx +++ b/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx @@ -69,11 +69,6 @@ const PaginationComponentV2 = ({ const [options, setOptions] = useState([]); useEffect(() => { - // Retrieve input from uri - if (queryableHelpers.uriHelpers) { - queryableHelpers.uriHelpers.retrieveFromUri(); - } - if (entityPrefix) { useFilterableProperties(entityPrefix, availableFilterNames).then((propertySchemas: PropertySchemaDTO[]) => { const newOptions = propertySchemas.filter((property) => property.schema_property_name !== MITRE_FILTER_KEY) diff --git a/openbas-front/src/components/common/queryable/pagination/usPaginationState.tsx b/openbas-front/src/components/common/queryable/pagination/usPaginationState.tsx index 2ba1af0313..a5e8272fae 100644 --- a/openbas-front/src/components/common/queryable/pagination/usPaginationState.tsx +++ b/openbas-front/src/components/common/queryable/pagination/usPaginationState.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { PaginationHelpers } from './PaginationHelpers'; export const ROWS_PER_PAGE_OPTIONS = [20, 50, 100]; @@ -6,6 +6,7 @@ export const ROWS_PER_PAGE_OPTIONS = [20, 50, 100]; const usPaginationState = (initSize?: number, onChange?: (page: number, size: number) => void): PaginationHelpers => { const [page, setPage] = React.useState(0); const [size, setSize] = React.useState(initSize ?? ROWS_PER_PAGE_OPTIONS[0]); + const hasBeenInitialized = useRef(false); const [totalElements, setTotalElements] = useState(0); const helpers: PaginationHelpers = { @@ -19,7 +20,10 @@ const usPaginationState = (initSize?: number, onChange?: (page: number, size: nu }; useEffect(() => { - onChange?.(page, size); + if (hasBeenInitialized.current) { + onChange?.(page, size); + } + hasBeenInitialized.current = true; }, [page, size]); return helpers; diff --git a/openbas-front/src/components/common/queryable/sort/useSortState.tsx b/openbas-front/src/components/common/queryable/sort/useSortState.tsx index 10aae48de6..94af7c7e17 100644 --- a/openbas-front/src/components/common/queryable/sort/useSortState.tsx +++ b/openbas-front/src/components/common/queryable/sort/useSortState.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import type { SortField } from '../../../../utils/api-types'; import { SortHelpers } from './SortHelpers'; @@ -12,6 +12,7 @@ const computeDirection = (direction?: string) => { const useSortState = (initSorts: SortField[] = [], onChange?: (sorts: SortField[]) => void) => { const [sortBy, setSortBy] = useState(initSorts?.[0]?.property ?? ''); const [sortAsc, setSortAsc] = useState(computeDirection(initSorts?.[0]?.direction)); + const hasBeenInitialized = useRef(false); const helpers: SortHelpers = { handleSort: (field: string) => { @@ -23,7 +24,10 @@ const useSortState = (initSorts: SortField[] = [], onChange?: (sorts: SortField[ }; useEffect(() => { - onChange?.([{ property: sortBy, direction: sortAsc ? 'ASC' : 'DESC' }]); + if (hasBeenInitialized.current) { + onChange?.([{ direction: sortAsc ? 'ASC' : 'DESC', property: sortBy }]); + } + hasBeenInitialized.current = true; }, [sortBy, sortAsc]); return helpers; diff --git a/openbas-front/src/components/common/queryable/textSearch/useTextSearchState.tsx b/openbas-front/src/components/common/queryable/textSearch/useTextSearchState.tsx index 18e9791e32..cf42184fae 100644 --- a/openbas-front/src/components/common/queryable/textSearch/useTextSearchState.tsx +++ b/openbas-front/src/components/common/queryable/textSearch/useTextSearchState.tsx @@ -1,14 +1,18 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { TextSearchHelpers } from './TextSearchHelpers'; const useTextSearchState = (initTextSearch: string = '', onChange?: (textSearch: string, page: number) => void): TextSearchHelpers => { const [textSearch, setTextSearch] = useState(initTextSearch); + const hasBeenInitialized = useRef(false); const helpers: TextSearchHelpers = { handleTextSearch: (value?: string) => setTextSearch(value ?? ''), }; useEffect(() => { - onChange?.(textSearch, 0); + if (hasBeenInitialized.current) { + onChange?.(textSearch, 0); + } + hasBeenInitialized.current = true; }, [textSearch]); return helpers; diff --git a/openbas-front/src/components/common/queryable/uri/useUriState.tsx b/openbas-front/src/components/common/queryable/uri/useUriState.tsx index 073851aa5a..8885c483e8 100644 --- a/openbas-front/src/components/common/queryable/uri/useUriState.tsx +++ b/openbas-front/src/components/common/queryable/uri/useUriState.tsx @@ -1,44 +1,58 @@ import * as qs from 'qs'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useEffect, useRef, useState } from 'react'; import * as R from 'ramda'; import { z } from 'zod'; import { UriHelpers } from './UriHelpers'; import type { SearchPaginationInput } from '../../../../utils/api-types'; import { buildSearchPagination, SearchPaginationInputSchema } from '../QueryableUtils'; +export const retrieveFromUri = (localStorageKey: string, searchParams: URLSearchParams): SearchPaginationInput | null => { + const encodedParams = searchParams.get('query') || ''; + const params = atob(encodedParams); + const paramsJson = qs.parse(params, { allowEmptyArrays: true }) as unknown as SearchPaginationInput & { key: string }; + if (!R.isEmpty(paramsJson) && paramsJson.key === localStorageKey) { + try { + const parse = SearchPaginationInputSchema.parse(paramsJson); + return buildSearchPagination(parse); + } catch (err) { + if (err instanceof z.ZodError) { + // eslint-disable-next-line no-console + console.log(`Validation error: the uri has not a valid format ${err.issues}`); + return null; + } + } + } + return null; +}; + const useUriState = (localStorageKey: string, initSearchPaginationInput: SearchPaginationInput, onChange: (input: SearchPaginationInput) => void) => { - const location = useLocation(); - const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const [input, setInput] = useState(initSearchPaginationInput); + const hasBeenInitialized = useRef(false); const helpers: UriHelpers = { retrieveFromUri: () => { - const encodedParams = location.search?.startsWith('?') ? location.search.substring(1) : ''; - const params = atob(encodedParams); - const paramsJson = qs.parse(params) as unknown as SearchPaginationInput & { key: string }; - if (!R.isEmpty(paramsJson) && paramsJson.key === localStorageKey) { - try { - const parse = SearchPaginationInputSchema.parse(paramsJson); - setInput(buildSearchPagination(parse)); - } catch (err) { - if (err instanceof z.ZodError) { - // eslint-disable-next-line no-console - console.log(`Validation error: the uri has not a valid format ${err.issues}`); - } - } + const built = retrieveFromUri(localStorageKey, searchParams); + if (built) { + setInput(built); } }, updateUri: () => { - const params = qs.stringify({ key: localStorageKey, ...initSearchPaginationInput }); + const params = qs.stringify({ ...initSearchPaginationInput, key: localStorageKey }, { allowEmptyArrays: true }); const encodedParams = btoa(params); - navigate(`?${encodedParams}`); + setSearchParams({ + query: encodedParams, + }); }, }; useEffect(() => { - onChange(input); + if (hasBeenInitialized.current) { + onChange(input); + } + hasBeenInitialized.current = true; }, [input]); return helpers; diff --git a/openbas-front/src/components/common/queryable/useQueryableWithLocalStorage.tsx b/openbas-front/src/components/common/queryable/useQueryableWithLocalStorage.tsx index 8f99628ea7..d1424f94c1 100644 --- a/openbas-front/src/components/common/queryable/useQueryableWithLocalStorage.tsx +++ b/openbas-front/src/components/common/queryable/useQueryableWithLocalStorage.tsx @@ -1,12 +1,14 @@ import { useLocalStorage } from 'usehooks-ts'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import * as R from 'ramda'; import useFiltersState from './filter/useFiltersState'; import type { FilterGroup, SearchPaginationInput, SortField } from '../../../utils/api-types'; import useTextSearchState from './textSearch/useTextSearchState'; import usPaginationState from './pagination/usPaginationState'; import { QueryableHelpers } from './QueryableHelpers'; import useSortState from './sort/useSortState'; -import useUriState from './uri/useUriState'; +import useUriState, { retrieveFromUri } from './uri/useUriState'; import { buildSearchPagination } from './QueryableUtils'; const buildUseQueryable = ( @@ -70,9 +72,21 @@ export const useQueryable = (initSearchPaginationInput: Partial) => { + const [searchParams] = useSearchParams(); const finalSearchPaginationInput: SearchPaginationInput = buildSearchPagination(initSearchPaginationInput); + const searchPaginationInputFromUri = retrieveFromUri(localStorageKey, searchParams); - const [searchPaginationInput, setSearchPaginationInput] = useLocalStorage(localStorageKey, finalSearchPaginationInput); + const [searchPaginationInputFromLocalStorage, setSearchPaginationInputFromLocalStorage] = useLocalStorage( + localStorageKey, + searchPaginationInputFromUri ?? finalSearchPaginationInput, + ); + const [searchPaginationInput, setSearchPaginationInput] = useState(searchPaginationInputFromUri ?? searchPaginationInputFromLocalStorage); - return buildUseQueryable(localStorageKey, initSearchPaginationInput, searchPaginationInput, setSearchPaginationInput); + useEffect(() => { + if (!R.equals(searchPaginationInputFromLocalStorage, searchPaginationInput)) { + setSearchPaginationInput(searchPaginationInputFromLocalStorage); + } + }, [searchPaginationInputFromLocalStorage]); + + return buildUseQueryable(localStorageKey, initSearchPaginationInput, searchPaginationInput, setSearchPaginationInputFromLocalStorage); }; diff --git a/openbas-front/src/utils/hooks/useDataLoader.js b/openbas-front/src/utils/hooks/useDataLoader.js index a22c6f7357..a5c7da0251 100644 --- a/openbas-front/src/utils/hooks/useDataLoader.js +++ b/openbas-front/src/utils/hooks/useDataLoader.js @@ -15,6 +15,8 @@ const ERROR_2S_DELAY = 2000; const ERROR_8S_DELAY = 8000; const ERROR_30S_DELAY = 30000; +// pristine is used to avoid duplicate requests at the launch of the app +let pristine = true; let sseClient; let lastPingDate = new Date().getTime(); const listeners = new Map(); @@ -31,7 +33,10 @@ const useDataLoader = (loader = () => {}, refetchArg = []) => { sseConnect(); } }, EVENT_TRY_DELAY); - sseClient.addEventListener('open', () => [...listeners.keys()].forEach((load) => load())); + sseClient.addEventListener('open', () => { + pristine = false; + [...listeners.keys()].forEach((load) => load()); + }); sseClient.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.listened) { @@ -80,7 +85,7 @@ const useDataLoader = (loader = () => {}, refetchArg = []) => { listeners.set(loader, ''); if (EventSource !== undefined && sseClient === undefined) { sseClient = sseConnect(); - } else { + } else if (!pristine) { const load = async () => { await loader(); };