const DEPENDENCY_PATH = '/api/v2/analysis/dependencies';
-export function getDependencies(params: { projectKey: string; q?: string } & BranchLikeParameters) {
+export function getDependencies({
+ pageParam,
+ projectKey,
+ q,
+ ...branchParameters
+}: { pageParam: number; projectKey: string; q?: string } & BranchLikeParameters) {
+ const params = {
+ pageIndex: pageParam,
+ projectKey,
+ q,
+ ...branchParameters,
+ };
return axios.get<DependenciesResponse>(DEPENDENCY_PATH, { params });
}
export const DEFAULT_DEPENDENCIES_MOCK: DependenciesResponse = {
page: {
pageIndex: 1,
- pageSize: 100,
+ pageSize: 50,
total: 0,
},
dependencies: [],
this.#defaultDependenciesData = response;
};
- handleGetDependencies = (data: { q?: string }) => {
+ handleGetDependencies = (data: { pageParam: number; q?: string }) => {
+ const { pageSize } = this.#defaultDependenciesData.page;
+ const totalDependencies = this.#defaultDependenciesData.dependencies.length;
+
+ if (pageSize < totalDependencies) {
+ const startIndex = (data.pageParam - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+
+ return Promise.resolve({
+ ...this.#defaultDependenciesData,
+ dependencies: this.#defaultDependenciesData.dependencies.slice(startIndex, endIndex),
+ });
+ }
+
return Promise.resolve({
...this.#defaultDependenciesData,
dependencies: this.#defaultDependenciesData.dependencies.filter(
import styled from '@emotion/styled';
import { Spinner, Text } from '@sonarsource/echoes-react';
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import { FormattedMessage } from 'react-intl';
import { InputSearch, LargeCenteredLayout } from '~design-system';
import withComponentContext from '../../app/components/componentContext/withComponentContext';
import DocumentationLink from '../../components/common/DocumentationLink';
+import ListFooter from '../../components/controls/ListFooter';
import { DocLink } from '../../helpers/doc-links';
import { translate } from '../../helpers/l10n';
import { useCurrentBranchQuery } from '../../queries/branch';
const [search, setSearch] = useState('');
- const { data: { dependencies = [] } = {}, isLoading } = useDependenciesQuery({
+ const {
+ data: { pages, pageParams } = {
+ pages: [],
+ pageParams: [],
+ },
+ isLoading,
+ fetchNextPage,
+ isFetchingNextPage,
+ } = useDependenciesQuery({
projectKey: component.key,
q: search,
branchParameters: getBranchLikeQuery(branchLike) as BranchLikeParameters,
const listName = search ? 'dependencies.list.name_search.title' : 'dependencies.list.title';
- const resultsExist = dependencies.length > 0 || search.length >= SEARCH_MIN_LENGTH;
+ const { resultsExist, allDependencies, totalDependencies, itemCount } = useMemo(() => {
+ // general paging information
+ const { total, pageSize } = pages[0]?.page ?? { total: 0, pageSize: 0 };
+
+ const resultsExist = total > 0 || search.length >= SEARCH_MIN_LENGTH;
+
+ const allDependencies = pages.flatMap((page) => page.dependencies);
+ const totalDependencies = pages[0]?.page.total ?? 0;
+
+ const itemCount = pageSize * pages.length > total ? total : pageSize * pages.length;
+
+ return { resultsExist, allDependencies, totalDependencies, itemCount };
+ }, [pages, search]);
return (
<LargeCenteredLayout className="sw-py-8 sw-typo-lg sw-h-full" id="dependencies-page">
)}
<Spinner isLoading={isLoading}>
- {dependencies.length === 0 && <EmptyState />}
+ {!resultsExist && <EmptyState />}
{resultsExist && (
<div className="sw-overflow-auto">
<Text>
id={listName}
defaultMessage={translate(listName)}
values={{
- count: dependencies.length,
+ count: allDependencies.length,
}}
/>
</Text>
<ul className="sw-py-4">
- {dependencies.map((dependency) => (
+ {allDependencies.map((dependency) => (
<li key={dependency.key}>
<DependencyListItem dependency={dependency} />
</li>
))}
</ul>
+ {pageParams.length > 0 && (
+ <ListFooter
+ className="sw-mb-4"
+ count={itemCount}
+ loadMore={() => fetchNextPage()}
+ loading={isFetchingNextPage}
+ total={totalDependencies}
+ />
+ )}
</div>
)}
</Spinner>
const MOCK_RESPONSE: DependenciesResponse = {
page: {
pageIndex: 1,
- pageSize: 100,
+ pageSize: 50,
total: 4,
},
dependencies: [
],
};
+const MOCK_RESPONSE_NO_FINDINGS_PAGING: DependenciesResponse = {
+ page: {
+ pageIndex: 1,
+ pageSize: 2,
+ total: 4,
+ },
+ dependencies: [
+ {
+ key: '1',
+ name: 'jackson-databind',
+ longName: 'com.fasterxml.jackson.core:jackson-databind',
+ version: '2.10.0',
+ transitive: false,
+ project: 'project1',
+ },
+ {
+ key: '2',
+ name: 'snappy-java',
+ longName: 'org.xerial.snappy:snappy-java',
+ version: '3.52',
+ transitive: true,
+ project: 'project1',
+ },
+ {
+ key: '3',
+ name: 'SnakeYAML',
+ longName: 'org.yaml:SnakeYAML',
+ version: '2.10.0',
+ transitive: true,
+ project: 'project1',
+ },
+ {
+ key: '4',
+ name: 'random-lib',
+ longName: 'com.random:random-lib',
+ version: '2.10.0',
+ transitive: true,
+ project: 'project1',
+ },
+ ],
+};
+
const MOCK_RESPONSE_NO_FINDINGS: DependenciesResponse = {
page: {
pageIndex: 1,
expect(await ui.searchTitle.get()).toBeInTheDocument();
});
+it('should correctly fetch additional pages of dependencies', async () => {
+ depsHandler.setDefaultDependencies(MOCK_RESPONSE_NO_FINDINGS_PAGING);
+ const { ui, user } = getPageObject();
+
+ renderDependenciesApp();
+
+ expect(await ui.dependencies.findAll()).toHaveLength(2);
+
+ expect(await ui.twoOfFour.find()).toBeInTheDocument();
+
+ await user.click(ui.showMore.get());
+ expect(await ui.fourOfFour.find()).toBeInTheDocument();
+ expect(await ui.dependencies.findAll()).toHaveLength(4);
+});
+
function getPageObject() {
const user = userEvent.setup();
const ui = {
dependencies: byRole('listitem'),
searchInput: byRole('searchbox'),
searchTitle: byText('dependencies.list.name_search.title0'),
+ showMore: byText('show_more'),
+ twoOfFour: byText('x_of_y_shown.2.4'),
+ fourOfFour: byText('x_of_y_shown.4.4'),
};
return { ui, user };
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { queryOptions } from '@tanstack/react-query';
+import { infiniteQueryOptions } from '@tanstack/react-query';
import { getDependencies } from '../api/dependencies';
import { BranchLikeParameters } from '../sonar-aligned/types/branch-like';
-import { createQueryHook } from './common';
+import { createInfiniteQueryHook } from './common';
-export const useDependenciesQuery = createQueryHook(
- (data: { branchParameters: BranchLikeParameters; projectKey: string; q?: string }) => {
- return queryOptions({
- queryKey: ['dependencies', data.projectKey, data.branchParameters, data.q],
- queryFn: () =>
- getDependencies({
+export const useDependenciesQuery = createInfiniteQueryHook(
+ (data: {
+ branchParameters: BranchLikeParameters;
+ pageIndex?: number;
+ projectKey: string;
+ q?: string;
+ }) => {
+ return infiniteQueryOptions({
+ queryKey: ['dependencies', data.projectKey, data.branchParameters, data.q, data.pageIndex],
+ queryFn: ({ pageParam }) => {
+ return getDependencies({
projectKey: data.projectKey,
q: data.q,
...data.branchParameters,
- }),
+ pageParam,
+ });
+ },
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) => {
+ return lastPage.page.pageIndex + 1;
+ },
});
},
);