From 4db802bb9cba0d5f878f716fb95a54b624f11a55 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Tue, 6 May 2025 15:36:44 +0200 Subject: [PATCH 1/3] ILEX-107 make changes to pagination logic and elasticSearch function --- src/api/endpoints/index.ts | 15 +++- src/components/Header/Search.jsx | 6 +- .../SearchResults/SearchResultsBox.jsx | 74 +++++++++++++--- src/components/SearchResults/index.jsx | 86 +++++++++++++------ 4 files changed, 134 insertions(+), 47 deletions(-) diff --git a/src/api/endpoints/index.ts b/src/api/endpoints/index.ts index cba7a7d1..07de21d4 100644 --- a/src/api/endpoints/index.ts +++ b/src/api/endpoints/index.ts @@ -156,12 +156,12 @@ const fetchData = async (url, method = "GET", data: object | null = null) => { } }; -export const elasticSearch = async (query) => { +export const elasticSearch = async (query: string, size: number, from: number) => { const url = API_CONFIG.BASE_SCICRUNCH_URL + API_CONFIG.SCICRUNCH_KEY; try { const result = await fetchData(url, "POST", { - "size": 20, - "from": 0, + "size": size, + "from": from || 0, "query": { "bool": { "must": [ @@ -180,9 +180,16 @@ export const elasticSearch = async (query) => { } } }); - return elasticSearhParser(result?.hits?.hits) + return { + results: elasticSearhParser(result?.hits?.hits), + total: 20, + }; } catch (error) { console.error("ElasticSearch Query Failed:", error); + return { + results: [], + total: 0 + }; } } diff --git a/src/components/Header/Search.jsx b/src/components/Header/Search.jsx index 87dd9ba5..087d0882 100644 --- a/src/components/Header/Search.jsx +++ b/src/components/Header/Search.jsx @@ -157,9 +157,9 @@ const Search = () => { // eslint-disable-next-line react-hooks/exhaustive-deps const fetchTerms = useCallback(debounce(async (searchTerm) => { - const data = await elasticSearch(searchTerm); - const dataTerms = data?.results?.filter(result => result.type === SEARCH_TYPES.TERM); - const dataOrganizations = data?.results?.filter(result => result.type === SEARCH_TYPES.ORGANIZATION); + const data = await elasticSearch(searchTerm, 20, 0); + const dataTerms = data?.results.results?.filter(result => result.type === SEARCH_TYPES.TERM); + const dataOrganizations = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ORGANIZATION); const dataOntologies = data?.results?.filter(result => result.type === SEARCH_TYPES.ONTOLOGY); setTerms(dataTerms); setOrganizations(dataOrganizations); diff --git a/src/components/SearchResults/SearchResultsBox.jsx b/src/components/SearchResults/SearchResultsBox.jsx index 7017a149..3dcac3d4 100644 --- a/src/components/SearchResults/SearchResultsBox.jsx +++ b/src/components/SearchResults/SearchResultsBox.jsx @@ -1,12 +1,13 @@ -import React from 'react'; +import { useState, useEffect } from 'react'; import ListView from './ListView'; import PropTypes from 'prop-types'; import { TableChartIcon, ListIcon } from '../../Icons'; import OntologySearch from '../SingleTermView/OntologySearch'; -import CustomSingleSelect from "../common/CustomSingleSelect"; +import CustomSingleSelect from '../common/CustomSingleSelect'; import { Box, Typography, Grid, ButtonGroup, Button, Stack, Divider } from '@mui/material'; - +import CustomPagination from '../common/CustomPagination'; import { vars } from '../../theme/variables'; + const { gray50, gray200, gray300, gray600 } = vars; const CustomViewButton = ({ view, listView, onClick, icon }) => ( @@ -29,25 +30,62 @@ const CustomViewButton = ({ view, listView, onClick, icon }) => ( ); -const SearchResultsBox = ({ searchResults, searchTerm, loading }) => { - const [numberOfVisiblePages, setNumberOfVisiblePages] = React.useState(20); - const [listView, setListView] = React.useState('list'); +const SearchResultsBox = ({ + pageResults, + searchTerm, + loading, + totalItems, + fetchPage +}) => { + const [listView, setListView] = useState('list'); + const [page, setPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(20); + + useEffect(() => { + const from = (page - 1) * itemsPerPage; + const remainingItems = totalItems - from; + const size = Math.min(itemsPerPage, remainingItems); + + if (size > 0) { + fetchPage(from, size); + } + }, [page, itemsPerPage, totalItems, fetchPage]); + + const handlePageChange = (_, newPage) => { + setPage(newPage); + }; - const handleNumberOfPagesChange = (v) => { - setNumberOfVisiblePages(v); + const handleItemsPerPageChange = (value) => { + const newItemsPerPage = Number(value); + setItemsPerPage(newItemsPerPage); + setPage(1) + }; + + const getPaginationOptions = () => { + const options = [5, 10, 15, 20]; + if (!options.includes(itemsPerPage)) { + options.push(itemsPerPage); + } + return options.sort((a, b) => a - b); }; return ( - {searchResults?.length} results for {searchTerm} search + + {totalItems} results for {searchTerm} search + Show on page: - + { + {listView === 'list' ? ( - + ) : (

table

)} + +
); }; @@ -89,9 +135,11 @@ CustomViewButton.propTypes = { }; SearchResultsBox.propTypes = { - searchResults: PropTypes.object, + pageResults: PropTypes.object, searchTerm: PropTypes.string, - loading: PropTypes.bool + loading: PropTypes.bool, + totalItems: PropTypes.number, + fetchPage: PropTypes.func }; export default SearchResultsBox; diff --git a/src/components/SearchResults/index.jsx b/src/components/SearchResults/index.jsx index dcb86057..96e253f0 100644 --- a/src/components/SearchResults/index.jsx +++ b/src/components/SearchResults/index.jsx @@ -1,23 +1,54 @@ -import { debounce } from 'lodash'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; +import { debounce } from 'lodash'; import { useQuery } from '../../helpers'; import SearchResultsBox from './SearchResultsBox'; -import { useEffect, useState, useCallback } from 'react'; import FiltersSidebar from '../Sidebar/FiltersSidebar'; -import { searchAll, elasticSearch } from '../../api/endpoints'; - +import { elasticSearch } from '../../api/endpoints'; const SearchResults = () => { const [loading, setLoading] = useState(true); const [filters, setFilters] = useState([]); const [checkedLabels, setCheckedLabels] = useState({}); - const [searchResults, setSearchResults] = useState([]); + const [allResults, setAllResults] = useState([]); + const [pageResults, setPageResults] = useState([]); + const [totalItems, setTotalItems] = useState(0); const query = useQuery(); - const searchTerm = query.get('searchTerm'); + useEffect(() => { + const loadAllResults = async () => { + setLoading(true); + try { + const data = await elasticSearch(searchTerm, 20, 0); + setFilters(data.results.filters || []); + setAllResults(data?.results.results || []); + setTotalItems(data?.total || 0); + } catch (error) { + console.error('Search error:', error); + } finally { + setLoading(false); + } + }; + loadAllResults(); + }, [searchTerm]); + + const loadPageData = useCallback(async (from, size) => { + setLoading(true); + try { + const data = await elasticSearch(searchTerm, size, from); + setPageResults(data?.results.results || []); + } catch (error) { + console.error('Pagination error:', error); + } finally { + setLoading(false); + } + }, [searchTerm]); + + const fetchPage = useMemo(() => debounce(loadPageData, 500), [loadPageData]); + const handleCheckboxChange = (category, label) => { - setCheckedLabels((prev) => ({ + setCheckedLabels(prev => ({ ...prev, [category]: { ...prev[category], @@ -26,33 +57,20 @@ const SearchResults = () => { })); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - const fetchTerms = useCallback(debounce(async (searchTerm) => { - const data = await elasticSearch(searchTerm); - setFilters(data.filters) - setSearchResults(data?.results || []); - setLoading(false) - }, 500), [searchAll]); - - useEffect(() => { - fetchTerms(searchTerm); - }, [searchTerm, fetchTerms]); - - const filterResults = (results, checkedLabels) => { return results.filter(item => { for (let category in checkedLabels) { if (!checkedLabels[category]) continue; // Skip empty categories - + const selectedLabels = Object.entries(checkedLabels[category]) .filter(([, isChecked]) => isChecked) .map(([label]) => label); - + if (selectedLabels.length === 0) continue; - + const categoryLower = category.toLowerCase(); const itemValue = item[categoryLower] || item[categoryLower === 'type' ? 'Type' : categoryLower]; - + if (!selectedLabels.includes(itemValue)) { return false; } @@ -61,12 +79,26 @@ const SearchResults = () => { }); }; - const filteredResults = filterResults(searchResults || [], checkedLabels); + const filteredResults = filterResults(pageResults || [], checkedLabels); + + console.log("totalItems: ", totalItems) return ( <> - - + + ); }; From 5496667986a4ff2b81d8736b617ecfed7397af2d Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Tue, 6 May 2025 23:50:26 +0200 Subject: [PATCH 2/3] ILEX-107 fix filtering issue --- src/api/endpoints/index.ts | 80 ++++++++++------- src/components/Header/Search.jsx | 2 +- .../SearchResults/SearchResultsBox.jsx | 85 ++++++++++++++----- src/components/SearchResults/index.jsx | 30 +++++-- src/components/term_activity/TermActivity.jsx | 2 +- 5 files changed, 139 insertions(+), 60 deletions(-) diff --git a/src/api/endpoints/index.ts b/src/api/endpoints/index.ts index 07de21d4..12274223 100644 --- a/src/api/endpoints/index.ts +++ b/src/api/endpoints/index.ts @@ -156,42 +156,64 @@ const fetchData = async (url, method = "GET", data: object | null = null) => { } }; -export const elasticSearch = async (query: string, size: number, from: number) => { +export const elasticSearch = async ( + query: string, + size?: number, + from: number = 0 +) => { const url = API_CONFIG.BASE_SCICRUNCH_URL + API_CONFIG.SCICRUNCH_KEY; + + let total = size; + + if (!size) { + try { + const initialResponse = await fetchData(url, "POST", { + size: 1, + from: 0, + query: buildQuery(query), + }); + + total = initialResponse?.hits?.total ?? 0; + } catch (error) { + console.error("Failed to fetch total count from Elasticsearch:", error); + return { results: [], total: 0 }; + } + } + try { - const result = await fetchData(url, "POST", { - "size": size, - "from": from || 0, - "query": { - "bool": { - "must": [ - { - "query_string": { - "fields": [ - "*" - ], - "query": query, - "type": "cross_fields", - "default_operator": "and", - "lenient": "true" - } - } - ] - } - } + const fullResponse = await fetchData(url, "POST", { + size: total, + from, + query: buildQuery(query), }); + return { - results: elasticSearhParser(result?.hits?.hits), - total: 20, + results: elasticSearhParser(fullResponse?.hits?.hits), + total, }; } catch (error) { - console.error("ElasticSearch Query Failed:", error); - return { - results: [], - total: 0 - }; + console.error("Error when performing elastic search", error); + return { results: [], total: 0 }; } -} +}; + +const buildQuery = (query: string) => ({ + "bool": { + "must": [ + { + "query_string": { + "fields": [ + "*" + ], + "query": query, + "type": "cross_fields", + "default_operator": "and", + "lenient": "true" + } + } + ] + } +}); export const searchAll = async (term, filters = {}) => { const { searchAll } = useMockApi(); diff --git a/src/components/Header/Search.jsx b/src/components/Header/Search.jsx index 087d0882..24efec80 100644 --- a/src/components/Header/Search.jsx +++ b/src/components/Header/Search.jsx @@ -160,7 +160,7 @@ const Search = () => { const data = await elasticSearch(searchTerm, 20, 0); const dataTerms = data?.results.results?.filter(result => result.type === SEARCH_TYPES.TERM); const dataOrganizations = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ORGANIZATION); - const dataOntologies = data?.results?.filter(result => result.type === SEARCH_TYPES.ONTOLOGY); + const dataOntologies = data?.results.results?.filter(result => result.type === SEARCH_TYPES.ONTOLOGY); setTerms(dataTerms); setOrganizations(dataOrganizations); setOntologies(dataOntologies); diff --git a/src/components/SearchResults/SearchResultsBox.jsx b/src/components/SearchResults/SearchResultsBox.jsx index 3dcac3d4..7102ee6b 100644 --- a/src/components/SearchResults/SearchResultsBox.jsx +++ b/src/components/SearchResults/SearchResultsBox.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import ListView from './ListView'; import PropTypes from 'prop-types'; import { TableChartIcon, ListIcon } from '../../Icons'; @@ -30,26 +30,75 @@ const CustomViewButton = ({ view, listView, onClick, icon }) => ( ); +const getPaginationSettings = (totalItems) => { + const largeDatasetOptions = [20, 50, 100, 200, 500]; + const smallDatasetOptions = [10, 20, 50, 100]; + + const options = totalItems >= 200 + ? largeDatasetOptions.filter(opt => opt <= totalItems) + : smallDatasetOptions.filter(opt => opt <= totalItems); + + if (options.length === 0) { + return { + options: [totalItems], + defaultSize: totalItems + }; + } + + let defaultSize; + if (totalItems >= 1000) defaultSize = 100; + else if (totalItems >= 500) defaultSize = 100; + else if (totalItems >= 200) defaultSize = 50; + else if (totalItems >= 100) defaultSize = 50; + else if (totalItems >= 50) defaultSize = 20; + else defaultSize = 10; + + if (!options.includes(defaultSize)) { + defaultSize = options[Math.floor(options.length / 2)]; + } + + return { options, defaultSize }; +}; + const SearchResultsBox = ({ + allResults, pageResults, searchTerm, loading, totalItems, - fetchPage + fetchPage, + hasActiveFilters }) => { + const { options, defaultSize } = getPaginationSettings(totalItems); const [listView, setListView] = useState('list'); const [page, setPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(20); + const [itemsPerPage, setItemsPerPage] = useState(defaultSize); useEffect(() => { - const from = (page - 1) * itemsPerPage; - const remainingItems = totalItems - from; - const size = Math.min(itemsPerPage, remainingItems); + if (!hasActiveFilters) { + const from = (page - 1) * itemsPerPage; + const remainingItems = totalItems - from; + const size = Math.min(itemsPerPage, remainingItems); - if (size > 0) { - fetchPage(from, size); + if (size > 0) { + fetchPage(from, size); + } } - }, [page, itemsPerPage, totalItems, fetchPage]); + }, [page, itemsPerPage, totalItems, fetchPage, hasActiveFilters]); + + useEffect(() => { + const { defaultSize: newDefault } = getPaginationSettings(totalItems); + setItemsPerPage(newDefault); + setPage(1); + }, [totalItems]); + + const paginatedResults = useMemo(() => { + if (!hasActiveFilters) return pageResults; + + const startIndex = (page - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return pageResults.slice(startIndex, endIndex); + }, [pageResults, page, itemsPerPage, hasActiveFilters]); const handlePageChange = (_, newPage) => { setPage(newPage); @@ -61,20 +110,12 @@ const SearchResultsBox = ({ setPage(1) }; - const getPaginationOptions = () => { - const options = [5, 10, 15, 20]; - if (!options.includes(itemsPerPage)) { - options.push(itemsPerPage); - } - return options.sort((a, b) => a - b); - }; - return ( - {totalItems} results for {searchTerm} search + {allResults.length} results for {searchTerm} search @@ -84,7 +125,7 @@ const SearchResultsBox = ({ @@ -112,7 +153,7 @@ const SearchResultsBox = ({ {listView === 'list' ? ( - + ) : (

table

)} @@ -135,11 +176,13 @@ CustomViewButton.propTypes = { }; SearchResultsBox.propTypes = { + allResults: PropTypes.object, pageResults: PropTypes.object, searchTerm: PropTypes.string, loading: PropTypes.bool, totalItems: PropTypes.number, - fetchPage: PropTypes.func + fetchPage: PropTypes.func, + hasActiveFilters: PropTypes.bool }; export default SearchResultsBox; diff --git a/src/components/SearchResults/index.jsx b/src/components/SearchResults/index.jsx index 96e253f0..604b762e 100644 --- a/src/components/SearchResults/index.jsx +++ b/src/components/SearchResults/index.jsx @@ -16,14 +16,21 @@ const SearchResults = () => { const query = useQuery(); const searchTerm = query.get('searchTerm'); + const hasActiveFilters = useMemo(() => { + return Object.values(checkedLabels).some(category => + category && Object.values(category).some(Boolean) + ); + }, [checkedLabels]); + useEffect(() => { const loadAllResults = async () => { setLoading(true); try { - const data = await elasticSearch(searchTerm, 20, 0); + const data = await elasticSearch(searchTerm); setFilters(data.results.filters || []); setAllResults(data?.results.results || []); setTotalItems(data?.total || 0); + setPageResults([]); } catch (error) { console.error('Search error:', error); } finally { @@ -37,13 +44,17 @@ const SearchResults = () => { setLoading(true); try { const data = await elasticSearch(searchTerm, size, from); - setPageResults(data?.results.results || []); + const results = data?.results.results || []; + + const filtered = hasActiveFilters ? filterResults(results, checkedLabels) : results; + + setPageResults(filtered); } catch (error) { console.error('Pagination error:', error); } finally { setLoading(false); } - }, [searchTerm]); + }, [searchTerm, checkedLabels, hasActiveFilters]); const fetchPage = useMemo(() => debounce(loadPageData, 500), [loadPageData]); @@ -55,6 +66,8 @@ const SearchResults = () => { [label]: !prev[category]?.[label] } })); + + setPageResults([]); }; const filterResults = (results, checkedLabels) => { @@ -79,9 +92,9 @@ const SearchResults = () => { }); }; - const filteredResults = filterResults(pageResults || [], checkedLabels); - - console.log("totalItems: ", totalItems) + const displayedResults = hasActiveFilters + ? filterResults(allResults, checkedLabels) + : pageResults; return ( <> @@ -92,12 +105,13 @@ const SearchResults = () => { /> ); diff --git a/src/components/term_activity/TermActivity.jsx b/src/components/term_activity/TermActivity.jsx index 1158c057..0deb3e75 100644 --- a/src/components/term_activity/TermActivity.jsx +++ b/src/components/term_activity/TermActivity.jsx @@ -108,7 +108,7 @@ const TermActivity = () => { try { setLoading(true); const data = await elasticSearch(""); - setRows(data?.results); + setRows(data?.results.results); } catch (err) { setError(err); } finally { From 84faf1c4c4a725abacd16ddae91f26182ed7ed91 Mon Sep 17 00:00:00 2001 From: Aiga115 Date: Fri, 9 May 2025 12:48:23 +0200 Subject: [PATCH 3/3] ILEX-107 fix ui issue with ontology search --- src/components/SearchResults/SearchResultsBox.jsx | 2 +- src/components/SingleTermView/OntologySearch.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SearchResults/SearchResultsBox.jsx b/src/components/SearchResults/SearchResultsBox.jsx index 7102ee6b..660c9a6c 100644 --- a/src/components/SearchResults/SearchResultsBox.jsx +++ b/src/components/SearchResults/SearchResultsBox.jsx @@ -31,7 +31,7 @@ const CustomViewButton = ({ view, listView, onClick, icon }) => ( ); const getPaginationSettings = (totalItems) => { - const largeDatasetOptions = [20, 50, 100, 200, 500]; + const largeDatasetOptions = [20, 50, 100, 200]; const smallDatasetOptions = [10, 20, 50, 100]; const options = totalItems >= 200 diff --git a/src/components/SingleTermView/OntologySearch.jsx b/src/components/SingleTermView/OntologySearch.jsx index 93b55b1d..8a576566 100644 --- a/src/components/SingleTermView/OntologySearch.jsx +++ b/src/components/SingleTermView/OntologySearch.jsx @@ -26,7 +26,7 @@ const OPTIONS = [ const styles = { autocomplete: (fullWidth, selectedValue, openList) => ({ - width: fullWidth ? '100%' : '21.75rem', + width: fullWidth ? '100%' : 'auto', '& .MuiOutlinedInput-root': { minWidth: fullWidth ? '100%' : (selectedValue ? '21.75rem' : '11.75rem'), width: fullWidth ? '100%' : 'fit-content',