diff --git a/src/api/endpoints/index.ts b/src/api/endpoints/index.ts
index 9c66e7c6..6c07e0b8 100644
--- a/src/api/endpoints/index.ts
+++ b/src/api/endpoints/index.ts
@@ -156,35 +156,64 @@ 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 = 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": 20,
- "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 elasticSearhParser(result?.hits?.hits)
+
+ return {
+ results: elasticSearhParser(fullResponse?.hits?.hits),
+ total,
+ };
} catch (error) {
- console.error("ElasticSearch Query Failed:", error);
+ 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 87dd9ba5..24efec80 100644
--- a/src/components/Header/Search.jsx
+++ b/src/components/Header/Search.jsx
@@ -157,10 +157,10 @@ 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 dataOntologies = data?.results?.filter(result => result.type === SEARCH_TYPES.ONTOLOGY);
+ 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.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 7017a149..660c9a6c 100644
--- a/src/components/SearchResults/SearchResultsBox.jsx
+++ b/src/components/SearchResults/SearchResultsBox.jsx
@@ -1,12 +1,13 @@
-import React from 'react';
+import { useState, useEffect, useMemo } 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,103 @@ const CustomViewButton = ({ view, listView, onClick, icon }) => (
);
-const SearchResultsBox = ({ searchResults, searchTerm, loading }) => {
- const [numberOfVisiblePages, setNumberOfVisiblePages] = React.useState(20);
- const [listView, setListView] = React.useState('list');
+const getPaginationSettings = (totalItems) => {
+ const largeDatasetOptions = [20, 50, 100, 200];
+ 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,
+ hasActiveFilters
+}) => {
+ const { options, defaultSize } = getPaginationSettings(totalItems);
+ const [listView, setListView] = useState('list');
+ const [page, setPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(defaultSize);
+
+ useEffect(() => {
+ if (!hasActiveFilters) {
+ const from = (page - 1) * itemsPerPage;
+ const remainingItems = totalItems - from;
+ const size = Math.min(itemsPerPage, remainingItems);
- const handleNumberOfPagesChange = (v) => {
- setNumberOfVisiblePages(v);
+ if (size > 0) {
+ fetchPage(from, size);
+ }
+ }
+ }, [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);
+ };
+
+ const handleItemsPerPageChange = (value) => {
+ const newItemsPerPage = Number(value);
+ setItemsPerPage(newItemsPerPage);
+ setPage(1)
};
return (
- {searchResults?.length} results for {searchTerm} search
+
+ {allResults.length} results for {searchTerm} search
+
Show on page:
-
+
{
+
{listView === 'list' ? (
-
+
) : (
table
)}
+
+
);
};
@@ -89,9 +176,13 @@ CustomViewButton.propTypes = {
};
SearchResultsBox.propTypes = {
- searchResults: PropTypes.object,
+ allResults: PropTypes.object,
+ pageResults: PropTypes.object,
searchTerm: PropTypes.string,
- loading: PropTypes.bool
+ loading: PropTypes.bool,
+ totalItems: PropTypes.number,
+ fetchPage: PropTypes.func,
+ hasActiveFilters: PropTypes.bool
};
export default SearchResultsBox;
diff --git a/src/components/SearchResults/index.jsx b/src/components/SearchResults/index.jsx
index dcb86057..604b762e 100644
--- a/src/components/SearchResults/index.jsx
+++ b/src/components/SearchResults/index.jsx
@@ -1,58 +1,89 @@
-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');
+ 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);
+ setFilters(data.results.filters || []);
+ setAllResults(data?.results.results || []);
+ setTotalItems(data?.total || 0);
+ setPageResults([]);
+ } 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);
+ 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, checkedLabels, hasActiveFilters]);
+
+ const fetchPage = useMemo(() => debounce(loadPageData, 500), [loadPageData]);
+
const handleCheckboxChange = (category, label) => {
- setCheckedLabels((prev) => ({
+ setCheckedLabels(prev => ({
...prev,
[category]: {
...prev[category],
[label]: !prev[category]?.[label]
}
}));
- };
-
- // 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]);
+ setPageResults([]);
+ };
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 +92,27 @@ const SearchResults = () => {
});
};
- const filteredResults = filterResults(searchResults || [], checkedLabels);
+ const displayedResults = hasActiveFilters
+ ? filterResults(allResults, checkedLabels)
+ : pageResults;
return (
<>
-
-
+
+
>
);
};
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',
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 {