From e70cef81d159907f5d282c570812f8e04e67f671 Mon Sep 17 00:00:00 2001 From: sourcegraph-release-guild-bot <107104610+sourcegraph-release-guild-bot@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:59:54 -0500 Subject: [PATCH] [Backport 5.2] alter function using apollo client to achieve pagination (#59837) Search contexts: paginate repo names (#58685) A customer reported that search contexts with over a thousand repos were failing when trying to create them. The root cause is that the webapp was fetching repositories to validate them, but was not using pagination, and instead tried to get them all in one go. The backend limits the number of results in a page to 1000, even if the client requests more. Now, the web app pages through results. (cherry picked from commit 439728733eb1fb080164dafeb35dc6ba9edc5948) Co-authored-by: Jason Hawk Harris --- .../searchContexts/SearchContextForm.tsx | 4 +- .../src/enterprise/searchContexts/backend.ts | 60 ++++++++++++------- .../src/integration/search-contexts.test.ts | 30 ++++++++-- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/client/web/src/enterprise/searchContexts/SearchContextForm.tsx b/client/web/src/enterprise/searchContexts/SearchContextForm.tsx index 7c02870e3c416..5184a04c97970 100644 --- a/client/web/src/enterprise/searchContexts/SearchContextForm.tsx +++ b/client/web/src/enterprise/searchContexts/SearchContextForm.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react' import classNames from 'classnames' import { useNavigate } from 'react-router-dom' -import { type Observable, of, throwError } from 'rxjs' +import { type Observable, of, throwError, from } from 'rxjs' import { catchError, map, startWith, switchMap, tap } from 'rxjs/operators' import { SyntaxHighlightedSearchQuery, LazyQueryInput } from '@sourcegraph/branded' @@ -221,7 +221,7 @@ export const SearchContextForm: React.FunctionComponent { const repositoryNameToID = new Map(repositories.map(({ id, name }) => [name, id])) const errors: Error[] = [] diff --git a/client/web/src/enterprise/searchContexts/backend.ts b/client/web/src/enterprise/searchContexts/backend.ts index 689b929fb78cc..a700f202c84d1 100644 --- a/client/web/src/enterprise/searchContexts/backend.ts +++ b/client/web/src/enterprise/searchContexts/backend.ts @@ -1,32 +1,48 @@ -import type { Observable } from 'rxjs' -import { map } from 'rxjs/operators' - import { dataOrThrowErrors, gql } from '@sourcegraph/http-client' +import type { GraphQLResult } from '@sourcegraph/http-client' import { requestGraphQL } from '../../backend/graphql' -import type { RepositoriesByNamesResult, RepositoriesByNamesVariables } from '../../graphql-operations' +import type { InputMaybe, RepositoriesByNamesResult, RepositoriesByNamesVariables } from '../../graphql-operations' + +const query = gql` + query RepositoriesByNames($names: [String!]!, $first: Int!, $after: String) { + repositories(names: $names, first: $first, after: $after) { + nodes { + id + name + } + pageInfo { + endCursor + hasNextPage + } + } + } +` -export function fetchRepositoriesByNames( +export async function fetchRepositoriesByNames( names: string[] -): Observable { +): Promise { + let repos: RepositoriesByNamesResult['repositories']['nodes'] = [] const first = names.length - return requestGraphQL( - gql` - query RepositoriesByNames($names: [String!]!, $first: Int!) { - repositories(names: $names, first: $first) { - nodes { - id - name - } - } - } - `, - { + let after: InputMaybe = null + + while (true) { + const result: GraphQLResult = await requestGraphQL< + RepositoriesByNamesResult, + RepositoriesByNamesVariables + >(query, { names, first, + after, + }).toPromise() + + const data: RepositoriesByNamesResult = dataOrThrowErrors(result) + + repos = repos.concat(data.repositories.nodes) + if (!data.repositories.pageInfo.hasNextPage) { + break } - ).pipe( - map(dataOrThrowErrors), - map(data => data.repositories.nodes) - ) + after = data.repositories.pageInfo.endCursor + } + return repos } diff --git a/client/web/src/integration/search-contexts.test.ts b/client/web/src/integration/search-contexts.test.ts index 43c03c3d46b9f..817bf5a7766b8 100644 --- a/client/web/src/integration/search-contexts.test.ts +++ b/client/web/src/integration/search-contexts.test.ts @@ -142,8 +142,19 @@ describe('Search contexts', () => { test('Create static search context', async () => { testContext.overrideGraphQL({ ...testContextForSearchContexts, - RepositoriesByNames: ({ names }) => ({ - repositories: { nodes: names.map((name, index) => ({ id: `index-${index}`, name })) }, + RepositoriesByNames: ({ names, first, after }) => ({ + repositories: { + nodes: names.map((name, index) => ({ id: `index-${index}`, name })), + pageInfo: { + endCursor: null, + hasNextPage: false, + }, + }, + variables: { + names, + first, + after, + }, }), CreateSearchContext: ({ searchContext, repositories }) => ({ createSearchContext: { @@ -291,8 +302,19 @@ describe('Search contexts', () => { test('Edit search context', async () => { testContext.overrideGraphQL({ ...testContextForSearchContexts, - RepositoriesByNames: ({ names }) => ({ - repositories: { nodes: names.map((name, index) => ({ id: `index-${index}`, name })) }, + RepositoriesByNames: ({ names, first, after }) => ({ + repositories: { + nodes: names.map((name, index) => ({ id: `index-${index}`, name })), + pageInfo: { + endCursor: null, + hasNextPage: false, + }, + }, + variables: { + names, + first, + after, + }, }), UpdateSearchContext: ({ id, searchContext, repositories }) => ({ updateSearchContext: {