Skip to content

Commit 9bfed0e

Browse files
committed
feat: using tanstack query for search
1 parent 1ef52b3 commit 9bfed0e

5 files changed

Lines changed: 105 additions & 121 deletions

File tree

webui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@mui/x-charts": "^8.28.2",
5151
"@mui/x-data-grid": "^8.28.2",
5252
"@mui/x-date-pickers": "^8.27.2",
53+
"@tanstack/react-query": "^5.99.0",
5354
"clipboard-copy": "^4.0.1",
5455
"clsx": "^1.2.1",
5556
"dompurify": "^3.0.4",

webui/src/default/default-app.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { HelmetProvider } from 'react-helmet-async';
1414
import { BrowserRouter } from 'react-router-dom';
1515
import { ThemeProvider } from '@mui/material/styles';
1616
import useMediaQuery from '@mui/material/useMediaQuery';
17+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
1718
import { ExtensionRegistryService } from '../extension-registry-service';
1819
import { Main } from '../main';
1920
import createPageSettings from './page-settings';
@@ -48,6 +49,8 @@ async function getServerVersion(): Promise<string> {
4849
}
4950
}
5051

52+
const queryClient = new QueryClient();
53+
5154
export const App = () => {
5255
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
5356
const theme = useMemo(
@@ -57,14 +60,16 @@ export const App = () => {
5760

5861
const pageSettings = createPageSettings(prefersDarkMode, service.serverUrl, getServerVersion());
5962
return (
60-
<HelmetProvider>
61-
<ThemeProvider theme={theme}>
62-
<Main
63-
service={service}
64-
pageSettings={pageSettings}
65-
/>
66-
</ThemeProvider>
67-
</HelmetProvider>
63+
<QueryClientProvider client={queryClient}>
64+
<HelmetProvider>
65+
<ThemeProvider theme={theme}>
66+
<Main
67+
service={service}
68+
pageSettings={pageSettings}
69+
/>
70+
</ThemeProvider>
71+
</HelmetProvider>
72+
</QueryClientProvider>
6873
);
6974
};
7075

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/********************************************************************************
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* https://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
********************************************************************************/
13+
14+
import { useContext } from 'react';
15+
import { useInfiniteQuery } from '@tanstack/react-query';
16+
import { MainContext } from '../../context';
17+
import { ExtensionFilter } from '../../extension-registry-service';
18+
import { SearchResult } from '../../extension-registry-types';
19+
20+
export const SEARCH_QUERY_KEY = 'extension-search';
21+
22+
const buildSearchUrl = (serverUrl: string, filter: ExtensionFilter & { offset: number }): URL => {
23+
const url = new URL('/api/-/search', serverUrl);
24+
if (filter.query) url.searchParams.set('query', filter.query);
25+
if (filter.category) url.searchParams.set('category', filter.category);
26+
if (filter.offset) url.searchParams.set('offset', String(filter.offset));
27+
if (filter.size) url.searchParams.set('size', String(filter.size));
28+
if (filter.sortBy) url.searchParams.set('sortBy', filter.sortBy);
29+
if (filter.sortOrder) url.searchParams.set('sortOrder', filter.sortOrder);
30+
return url;
31+
};
32+
33+
export const useSearch = (filter: ExtensionFilter) => {
34+
const { service } = useContext(MainContext);
35+
36+
return useInfiniteQuery({
37+
queryKey: [SEARCH_QUERY_KEY, filter.query, filter.category, filter.sortBy, filter.sortOrder, filter.size],
38+
queryFn: async ({ pageParam, signal }) => {
39+
const url = buildSearchUrl(service.serverUrl, { ...filter, offset: pageParam });
40+
return fetch(url, { signal }).then(res => res.json()) as Promise<SearchResult>;
41+
},
42+
initialPageParam: 0,
43+
getNextPageParam: (lastPage, allPages) => {
44+
const loadedCount = allPages.flatMap(p => p.extensions).length;
45+
if (loadedCount < lastPage.totalSize && lastPage.extensions.length > 0) {
46+
return loadedCount;
47+
}
48+
return undefined;
49+
},
50+
retry: 3,
51+
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
52+
});
53+
};
54+
55+
export type UseSearchReturn = ReturnType<typeof useSearch>;

webui/src/pages/extension-list/extension-list.tsx

Lines changed: 17 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -8,134 +8,38 @@
88
* SPDX-License-Identifier: EPL-2.0
99
********************************************************************************/
1010

11-
import { FunctionComponent, useContext, useEffect, useRef, useState } from 'react';
11+
import { FunctionComponent, useContext, useEffect, useMemo } from 'react';
1212
import InfiniteScroll from 'react-infinite-scroller';
1313
import { Box, Grid, CircularProgress, Container } from '@mui/material';
1414
import { ExtensionListItem } from './extension-list-item';
15-
import { isError, SearchEntry, SearchResult } from '../../extension-registry-types';
1615
import { ExtensionFilter } from '../../extension-registry-service';
17-
import { debounce } from '../../utils';
1816
import { DelayedLoadIndicator } from '../../components/delayed-load-indicator';
1917
import { MainContext } from '../../context';
18+
import { useSearch } from '../../hooks/extension-list/use-search';
2019

2120
export const ExtensionList: FunctionComponent<ExtensionListProps> = props => {
22-
const abortController = useRef<AbortController>(new AbortController());
23-
const cancellationToken = useRef<{ timeout?: number }>({});
24-
const enableLoadMore = useRef(false);
25-
const lastRequestedPage = useRef(0);
26-
const pageOffset = useRef(0);
27-
const filterSize = useRef(props.filter.size ?? 10);
28-
const context = useContext(MainContext);
29-
const [extensions, setExtensions] = useState<SearchEntry[]>([]);
30-
const [extensionKeys, setExtensionKeys] = useState<Set<string>>(new Set<string>());
31-
const [appliedFilter, setAppliedFilter] = useState<ExtensionFilter>();
32-
const [hasMore, setHasMore] = useState<boolean>(false);
33-
const [loading, setLoading] = useState<boolean>(true);
21+
const { handleError } = useContext(MainContext);
3422

35-
useEffect(() => {
36-
enableLoadMore.current = true;
37-
return () => {
38-
abortController.current.abort();
39-
clearTimeout(cancellationToken.current.timeout);
40-
enableLoadMore.current = false;
41-
};
42-
}, []);
43-
44-
useEffect(() => {
45-
filterSize.current = props.filter.size ?? filterSize.current;
46-
debounce(
47-
async () => {
48-
try {
49-
const result = await context.service.search(abortController.current, props.filter);
50-
if (isError(result)) {
51-
throw result;
52-
}
53-
54-
const searchResult = result as SearchResult;
55-
props.onUpdate(searchResult.totalSize);
56-
const actualSize = searchResult.extensions.length;
57-
pageOffset.current = lastRequestedPage.current;
58-
const extensionKeys = new Set<string>();
59-
for (const ext of searchResult.extensions) {
60-
extensionKeys.add(`${ext.namespace}.${ext.name}`);
61-
}
23+
const { data, isFetching, fetchNextPage, hasNextPage, error } = useSearch(props.filter);
6224

63-
setExtensions(searchResult.extensions);
64-
setExtensionKeys(extensionKeys);
65-
setAppliedFilter(props.filter);
66-
setHasMore(actualSize < searchResult.totalSize && actualSize > 0);
67-
} catch (err) {
68-
context.handleError(err);
69-
} finally {
70-
setLoading(false);
71-
}
72-
},
73-
cancellationToken.current,
74-
props.debounceTime
75-
);
76-
}, [props.filter.category, props.filter.query, props.filter.sortBy, props.filter.sortOrder, props.debounceTime]);
25+
const extensions = useMemo(() => data?.pages.flatMap(p => p.extensions) ?? [], [data]);
26+
const totalSize = useMemo(() => data?.pages[0]?.totalSize ?? 0, [data]);
7727

78-
const loadMore = async (p: number): Promise<void> => {
79-
setLoading(true);
80-
setHasMore(false);
81-
lastRequestedPage.current = p;
82-
const filter = copyFilter(appliedFilter as ExtensionFilter);
83-
if (!isSameFilter(props.filter, filter)) {
84-
return;
85-
}
86-
try {
87-
filter.offset = (p - pageOffset.current) * filterSize.current;
88-
const result = await context.service.search(abortController.current, filter);
89-
if (isError(result)) {
90-
throw result;
91-
}
92-
93-
const newExtensions: SearchEntry[] = [];
94-
const newExtensionKeys = new Set<string>();
95-
newExtensions.push(...extensions);
96-
extensionKeys.forEach((key) => newExtensionKeys.add(key));
97-
const searchResult = result as SearchResult;
98-
if (enableLoadMore.current && isSameFilter(props.filter, filter)) {
99-
// Check for duplicate keys to avoid problems due to asynchronous user edit / loadMore call
100-
for (const ext of searchResult.extensions) {
101-
const key = `${ext.namespace}.${ext.name}`;
102-
if (!extensionKeys.has(key)) {
103-
newExtensions.push(ext);
104-
newExtensionKeys.add(key);
105-
}
106-
}
107-
108-
setExtensions(newExtensions);
109-
setExtensionKeys(newExtensionKeys);
110-
setHasMore(extensions.length < searchResult.totalSize && searchResult.extensions.length > 0);
111-
}
112-
} catch (err) {
113-
context.handleError(err);
114-
} finally {
115-
setLoading(false);
116-
}
117-
};
28+
useEffect(() => {
29+
props.onUpdate(totalSize);
30+
}, [totalSize]);
11831

119-
const isSameFilter = (f1: ExtensionFilter, f2: ExtensionFilter): boolean => {
120-
return f1.category === f2.category && f1.query === f2.query && f1.sortBy === f2.sortBy && f1.sortOrder === f2.sortOrder;
121-
};
32+
useEffect(() => {
33+
if (error) handleError(error);
34+
}, [error]);
12235

123-
const copyFilter = (f: ExtensionFilter): ExtensionFilter => {
124-
return {
125-
query: f.query,
126-
category: f.category || '',
127-
size: f.size,
128-
offset: f.offset,
129-
sortBy: f.sortBy,
130-
sortOrder: f.sortOrder
131-
};
132-
};
36+
const loadMore = (): Promise<void> => fetchNextPage().then(() => undefined);
13337

13438
const extensionList = extensions.map((ext, idx) => (
13539
<ExtensionListItem
13640
idx={idx}
13741
extension={ext}
138-
filterSize={filterSize.current}
42+
filterSize={props.filter.size ?? 10}
13943
key={`${ext.namespace}.${ext.name}`} />
14044
));
14145

@@ -144,10 +48,10 @@ export const ExtensionList: FunctionComponent<ExtensionListProps> = props => {
14448
</Box>;
14549

14650
return <>
147-
<DelayedLoadIndicator loading={loading}/>
51+
<DelayedLoadIndicator loading={isFetching}/>
14852
<InfiniteScroll
14953
loadMore={loadMore}
150-
hasMore={hasMore}
54+
hasMore={!!hasNextPage}
15155
loader={loader}
15256
threshold={200} >
15357
<Container maxWidth='xl'>
@@ -163,4 +67,4 @@ export interface ExtensionListProps {
16367
filter: ExtensionFilter;
16468
debounceTime: number;
16569
onUpdate: (resultNumber: number) => void;
166-
}
70+
}

webui/yarn.lock

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,24 @@ __metadata:
16931693
languageName: node
16941694
linkType: hard
16951695

1696+
"@tanstack/query-core@npm:5.99.0":
1697+
version: 5.99.0
1698+
resolution: "@tanstack/query-core@npm:5.99.0"
1699+
checksum: 10/752c38122c772a8c511ffd927e003282a23a3ab6979b066d1d8b100a2061a5448096fce4cfc9e2d93a2ef468ffac5440a6181203b1456d12f6e51f69bcdc1a4d
1700+
languageName: node
1701+
linkType: hard
1702+
1703+
"@tanstack/react-query@npm:^5.99.0":
1704+
version: 5.99.0
1705+
resolution: "@tanstack/react-query@npm:5.99.0"
1706+
dependencies:
1707+
"@tanstack/query-core": "npm:5.99.0"
1708+
peerDependencies:
1709+
react: ^18 || ^19
1710+
checksum: 10/aafbc87fc9c6c01c8ac8229d42ab209273f39f8284a72b8917c5fab39722b949b8dff2cf3b28487b2ebcb286bb08a377a42ed8a56dabe7f1ac4681c5af338401
1711+
languageName: node
1712+
linkType: hard
1713+
16961714
"@tsconfig/node10@npm:^1.0.7":
16971715
version: 1.0.12
16981716
resolution: "@tsconfig/node10@npm:1.0.12"
@@ -5920,6 +5938,7 @@ __metadata:
59205938
"@mui/x-date-pickers": "npm:^8.27.2"
59215939
"@playwright/test": "npm:^1.58.0"
59225940
"@stylistic/eslint-plugin": "npm:^5.9.0"
5941+
"@tanstack/react-query": "npm:^5.99.0"
59235942
"@types/chai": "npm:^4.3.5"
59245943
"@types/d3-scale": "npm:^4.0"
59255944
"@types/d3-shape": "npm:^3.1"

0 commit comments

Comments
 (0)