]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23469 Support Pagination of Dependencies (#12149)
authorLucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com>
Fri, 25 Oct 2024 14:35:50 +0000 (17:35 +0300)
committersonartech <sonartech@sonarsource.com>
Fri, 25 Oct 2024 20:02:44 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/dependencies.ts
server/sonar-web/src/main/js/api/mocks/DependenciesServiceMock.ts
server/sonar-web/src/main/js/apps/dependencies/DependenciesApp.tsx
server/sonar-web/src/main/js/apps/dependencies/__tests__/DependenciesApp-it.tsx
server/sonar-web/src/main/js/queries/dependencies.ts

index a4cd1695e5e04e30e0d8886605ef34d5bf5547f5..58c3129179acfaceadc56cc8809d1e534d4a4bf4 100644 (file)
@@ -24,6 +24,17 @@ import { DependenciesResponse } from '../types/dependencies';
 
 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 });
 }
index 15578b9609ef6a3ee7fcda0f0e914b9f11359880..9fed3a7692e01892710e3218a9817698a3302f2b 100644 (file)
@@ -27,7 +27,7 @@ jest.mock('../dependencies');
 export const DEFAULT_DEPENDENCIES_MOCK: DependenciesResponse = {
   page: {
     pageIndex: 1,
-    pageSize: 100,
+    pageSize: 50,
     total: 0,
   },
   dependencies: [],
@@ -49,7 +49,20 @@ export default class DependenciesServiceMock {
     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(
index bd7dc34513b0a30b098048bc22349b00e3030d88..4f8bedf6265a96cdda3b20f265c9200163e63b0a 100644 (file)
 
 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';
@@ -48,7 +49,15 @@ function App(props: Readonly<Props>) {
 
   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,
@@ -56,7 +65,19 @@ function App(props: Readonly<Props>) {
 
   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">
@@ -77,7 +98,7 @@ function App(props: Readonly<Props>) {
         )}
 
         <Spinner isLoading={isLoading}>
-          {dependencies.length === 0 && <EmptyState />}
+          {!resultsExist && <EmptyState />}
           {resultsExist && (
             <div className="sw-overflow-auto">
               <Text>
@@ -85,17 +106,26 @@ function App(props: Readonly<Props>) {
                   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>
index 3eca30fb38b95e4b4330b645beaeff6dd66bcb2b..9ea4b597c885ec96fdc4143ccbb011d15d0bd4b3 100644 (file)
@@ -37,7 +37,7 @@ const settingsHandler = new SettingsServiceMock();
 const MOCK_RESPONSE: DependenciesResponse = {
   page: {
     pageIndex: 1,
-    pageSize: 100,
+    pageSize: 50,
     total: 4,
   },
   dependencies: [
@@ -90,6 +90,48 @@ const MOCK_RESPONSE: DependenciesResponse = {
   ],
 };
 
+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,
@@ -201,6 +243,21 @@ it('should correctly show empty results state when no dependencies are found', a
   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 = {
@@ -211,6 +268,9 @@ function getPageObject() {
     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 };
 }
index 7186d299d11187c3d6269f2b044dd8c9d58061f4..37d2f1ca606e855fb7261769b7325fc3234a1ec7 100644 (file)
  * 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;
+      },
     });
   },
 );