From 5733dd0a0d2b3e91dcc6672c52c8a0b6af449c29 Mon Sep 17 00:00:00 2001 From: Srimon Date: Fri, 15 May 2026 06:27:29 +0530 Subject: [PATCH 1/2] Add configurable collections page size with localStorage persistence (#297) --- src/pages/Collections.jsx | 47 +++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/pages/Collections.jsx b/src/pages/Collections.jsx index 0c86975a6..0d0885bdd 100644 --- a/src/pages/Collections.jsx +++ b/src/pages/Collections.jsx @@ -1,7 +1,18 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useClient } from '../context/client-context'; import SearchBar from '../components/Collections/SearchBar'; -import { Typography, Grid, Pagination, Box, Skeleton, IconButton, Tooltip } from '@mui/material'; +import { + Typography, + Grid, + Pagination, + Box, + Skeleton, + IconButton, + Tooltip, + Select, + MenuItem, + FormControl, +} from '@mui/material'; import { keyframes } from '@mui/material/styles'; import { RefreshCw } from 'lucide-react'; import ErrorNotifier from '../components/ToastNotifications/ErrorNotifier'; @@ -27,7 +38,17 @@ function Collections() { const [isRefreshing, setIsRefreshing] = useState(false); const { client: qdrantClient } = useClient(); const [currentPage, setCurrentPage] = useState(1); - const PAGE_SIZE = 5; + const [pageSize, setPageSize] = useState(() => { + const stored = localStorage.getItem('qdrant-web-ui-collections-page-size'); + return stored ? Number(stored) : 5; + }); + + const handlePageSizeChange = (event) => { + const newSize = event.target.value; + setPageSize(newSize); + localStorage.setItem('qdrant-web-ui-collections-page-size', String(newSize)); + setCurrentPage(1); + }; const { maxCollections } = useMaxCollections(); @@ -47,7 +68,7 @@ function Collections() { const sortedCollections = allCollections.collections.sort((a, b) => a.name.localeCompare(b.name)); setCollections(sortedCollections); - const nextPageCollections = sortedCollections.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + const nextPageCollections = sortedCollections.slice((page - 1) * pageSize, page * pageSize); const nextRawCollections = await Promise.all( nextPageCollections.map(async (collection) => { @@ -71,7 +92,7 @@ function Collections() { setRawCollections(null); } }, - [qdrantClient, getErrorMessageWithApiKey] + [qdrantClient, getErrorMessageWithApiKey, pageSize] ); const getFilteredCollectionsCall = useCallback( @@ -187,12 +208,24 @@ function Collections() { + + + getCollectionsCall(currentPage)} /> getCollectionsCall(currentPage)} key={'snapshots'} /> @@ -226,11 +259,11 @@ function Collections() { refreshCollection={refreshCollection} isRefreshing={isRefreshing} /> - {displayCollections && displayCollections.length > PAGE_SIZE && ( + {displayCollections && displayCollections.length > pageSize && ( From d0c749acac9708ca71e3c63c646a0a6a432da713 Mon Sep 17 00:00:00 2001 From: Srimon Date: Fri, 15 May 2026 06:56:48 +0530 Subject: [PATCH 2/2] Harden collections page size handling --- src/pages/Collections.jsx | 49 +++++++++++------- src/pages/Collections.test.jsx | 93 ++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 src/pages/Collections.test.jsx diff --git a/src/pages/Collections.jsx b/src/pages/Collections.jsx index 0d0885bdd..7b947066c 100644 --- a/src/pages/Collections.jsx +++ b/src/pages/Collections.jsx @@ -24,11 +24,20 @@ import { debounce } from 'lodash'; import { useMaxCollections } from '../context/telemetry-context'; import CreateCollectionButton from '../components/Collections/CreateCollection/CreateCollectionButton'; +const PAGE_SIZE_STORAGE_KEY = 'qdrant-web-ui-collections-page-size'; +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; +const DEFAULT_PAGE_SIZE = PAGE_SIZE_OPTIONS[0]; + const spin = keyframes` from { transform: rotate(0deg); } to { transform: rotate(360deg); } `; +function getInitialPageSize() { + const stored = Number(localStorage.getItem(PAGE_SIZE_STORAGE_KEY)); + return PAGE_SIZE_OPTIONS.includes(stored) ? stored : DEFAULT_PAGE_SIZE; +} + function Collections() { const [rawCollections, setRawCollections] = useState(null); const [collections, setCollections] = useState(null); @@ -38,15 +47,12 @@ function Collections() { const [isRefreshing, setIsRefreshing] = useState(false); const { client: qdrantClient } = useClient(); const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(() => { - const stored = localStorage.getItem('qdrant-web-ui-collections-page-size'); - return stored ? Number(stored) : 5; - }); + const [pageSize, setPageSize] = useState(getInitialPageSize); const handlePageSizeChange = (event) => { const newSize = event.target.value; setPageSize(newSize); - localStorage.setItem('qdrant-web-ui-collections-page-size', String(newSize)); + localStorage.setItem(PAGE_SIZE_STORAGE_KEY, String(newSize)); setCurrentPage(1); }; @@ -122,22 +128,28 @@ function Collections() { [collections, qdrantClient, getErrorMessageWithApiKey] ); - useEffect(() => { - getCollectionsCall(currentPage); - }, [currentPage, getCollectionsCall]); + const debouncedGetFilteredCollectionsCall = useMemo( + () => debounce(getFilteredCollectionsCall, 100), + [getFilteredCollectionsCall] + ); useEffect(() => { if (!searchQuery) { getCollectionsCall(currentPage); - } else { - debouncedGetFilteredCollectionsCall(searchQuery); } }, [searchQuery, currentPage, getCollectionsCall]); - const debouncedGetFilteredCollectionsCall = useMemo( - () => debounce(getFilteredCollectionsCall, 100), - [getFilteredCollectionsCall] - ); + useEffect(() => { + if (!searchQuery) { + return; + } + + debouncedGetFilteredCollectionsCall(searchQuery); + + return () => { + debouncedGetFilteredCollectionsCall.cancel(); + }; + }, [searchQuery, debouncedGetFilteredCollectionsCall]); const handleRefresh = useCallback(async () => { setIsRefreshing(true); @@ -220,10 +232,11 @@ function Collections() { onChange={handlePageSizeChange} inputProps={{ 'aria-label': 'Collections per page' }} > - 5 - 10 - 25 - 50 + {PAGE_SIZE_OPTIONS.map((size) => ( + + {size} + + ))} getCollectionsCall(currentPage)} /> diff --git a/src/pages/Collections.test.jsx b/src/pages/Collections.test.jsx new file mode 100644 index 000000000..5ff325a17 --- /dev/null +++ b/src/pages/Collections.test.jsx @@ -0,0 +1,93 @@ +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import Collections from './Collections'; + +const getCollections = vi.fn(); +const getAliases = vi.fn(); +const getCollection = vi.fn(); +const getApiKey = vi.fn(); +const client = { + getCollections, + getAliases, + getCollection, + getApiKey, +}; + +vi.mock('../context/client-context', () => ({ + useClient: () => ({ + client, + }), +})); + +vi.mock('../context/telemetry-context', () => ({ + useMaxCollections: () => ({ maxCollections: null }), +})); + +vi.mock('../components/Collections/SearchBar', () => ({ + default: ({ value, setValue }) => ( + setValue(event.target.value)} /> + ), +})); + +vi.mock('../components/Collections/CollectionsList', () => ({ + default: ({ collections }) =>
{collections.map((item) => item.name).join(',')}
, +})); + +vi.mock('../components/Collections/CreateCollection/CreateCollectionButton', () => ({ + default: () => , +})); + +vi.mock('../components/Snapshots/SnapshotsUpload', () => ({ + SnapshotsUpload: () => , +})); + +vi.mock('../components/ToastNotifications/ErrorNotifier', () => ({ + default: ({ message }) =>
{message}
, +})); + +describe('Collections page size', () => { + beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + + getCollections.mockResolvedValue({ + collections: [{ name: 'alpha' }, { name: 'beta' }, { name: 'gamma' }], + }); + getAliases.mockResolvedValue({ aliases: [] }); + getCollection.mockImplementation(async (name) => ({ vectors_count: name.length })); + getApiKey.mockReturnValue(null); + }); + + it('fetches collections once on initial render', async () => { + render(); + + await screen.findByTestId('collections-list'); + + await waitFor(() => { + expect(getCollections).toHaveBeenCalledTimes(1); + }); + }); + + it('falls back to the default page size when localStorage has an unsupported value', async () => { + localStorage.setItem('qdrant-web-ui-collections-page-size', '999'); + + render(); + + const pageSizeSelect = await screen.findByRole('combobox', { name: 'Collections per page' }); + expect(pageSizeSelect).toHaveTextContent('5'); + }); + + it('persists a newly selected page size', async () => { + render(); + + const pageSizeSelect = await screen.findByRole('combobox', { name: 'Collections per page' }); + fireEvent.mouseDown(pageSizeSelect); + + const option = await screen.findByRole('option', { name: '10' }); + fireEvent.click(option); + + await waitFor(() => { + expect(localStorage.getItem('qdrant-web-ui-collections-page-size')).toBe('10'); + }); + }); +});