Skip to content

Commit

Permalink
[frontend] Remove duplicate requests for pagination & filters
Browse files Browse the repository at this point in the history
  • Loading branch information
guillaumejparis committed Oct 18, 2024
1 parent 4f02983 commit 081420a
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { emptyFilterGroup } from './FilterUtils';
import { FilterHelpers } from './FilterHelpers';
import {
Expand All @@ -24,6 +24,7 @@ const useFiltersState = (
const [filtersState, setFiltersState] = useState<Props>({
filters: initFilters,
});
const hasBeenInitialized = useRef<boolean>(false);
const helpers: FilterHelpers = {
// Switch filter group operator
handleSwitchMode: () => {
Expand Down Expand Up @@ -74,7 +75,10 @@ const useFiltersState = (
};

useEffect(() => {
onChange?.(filtersState.filters);
if (hasBeenInitialized.current) {
onChange?.(filtersState.filters);
}
hasBeenInitialized.current = true;
}, [filtersState]);

return [filtersState.filters, helpers];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,6 @@ const PaginationComponentV2 = <T extends object>({
const [options, setOptions] = useState<OptionPropertySchema[]>([]);

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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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];

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<boolean>(false);
const [totalElements, setTotalElements] = useState(0);

const helpers: PaginationHelpers = {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<boolean>(false);

const helpers: SortHelpers = {
handleSort: (field: string) => {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>(initTextSearch);
const hasBeenInitialized = useRef<boolean>(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;
Expand Down
54 changes: 34 additions & 20 deletions openbas-front/src/components/common/queryable/uri/useUriState.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchPaginationInput>(initSearchPaginationInput);
const hasBeenInitialized = useRef<boolean>(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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = (
Expand Down Expand Up @@ -70,9 +72,21 @@ export const useQueryable = (initSearchPaginationInput: Partial<SearchPagination
};

export const useQueryableWithLocalStorage = (localStorageKey: string, initSearchPaginationInput: Partial<SearchPaginationInput>) => {
const [searchParams] = useSearchParams();
const finalSearchPaginationInput: SearchPaginationInput = buildSearchPagination(initSearchPaginationInput);
const searchPaginationInputFromUri = retrieveFromUri(localStorageKey, searchParams);

const [searchPaginationInput, setSearchPaginationInput] = useLocalStorage<SearchPaginationInput>(localStorageKey, finalSearchPaginationInput);
const [searchPaginationInputFromLocalStorage, setSearchPaginationInputFromLocalStorage] = useLocalStorage<SearchPaginationInput>(
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);
};
9 changes: 7 additions & 2 deletions openbas-front/src/utils/hooks/useDataLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
};
Expand Down

0 comments on commit 081420a

Please sign in to comment.