From 4c35db75875a884d81e2d69ae7f74d4df6038e3a Mon Sep 17 00:00:00 2001 From: Sven Efftinge Date: Mon, 6 Nov 2023 22:41:45 +0100 Subject: [PATCH] Add "more elements" hint to repo search. (#19016) --- .../src/components/RepositoryFinder.tsx | 55 ++++++++++++++++++- .../search-repositories-query.ts | 5 +- .../unified-repositories-search-query.ts | 5 +- .../src/workspaces/CreateWorkspacePage.tsx | 4 +- .../gitpod-protocol/src/gitpod-service.ts | 1 + .../bitbucket-server-repository-provider.ts | 6 +- .../bitbucket-repository-provider.ts | 4 +- .../src/github/github-repository-provider.ts | 4 +- .../src/gitlab/gitlab-repository-provider.ts | 3 +- .../src/repohost/repository-provider.ts | 2 +- .../src/workspace/gitpod-server-impl.ts | 6 +- 11 files changed, 76 insertions(+), 19 deletions(-) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 0695fd6edf8b5a..3f185b8e527f2c 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -11,6 +11,8 @@ import { ReactComponent as RepositoryIcon } from "../icons/RepositoryWithColor.s import { SuggestedRepository } from "@gitpod/gitpod-protocol"; import { MiddleDot } from "./typography/MiddleDot"; import { useUnifiedRepositorySearch } from "../data/git-providers/unified-repositories-search-query"; +import { useAuthProviders } from "../data/auth-providers/auth-provider-query"; +import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg"; interface RepositoryFinderProps { selectedContextURL?: string; @@ -30,7 +32,14 @@ export default function RepositoryFinder({ onChange, }: RepositoryFinderProps) { const [searchString, setSearchString] = useState(""); - const { data: repos, isLoading, isSearching } = useUnifiedRepositorySearch({ searchString, excludeProjects }); + const { + data: repos, + isLoading, + isSearching, + hasMore, + } = useUnifiedRepositorySearch({ searchString, excludeProjects }); + + const authProviders = useAuthProviders(); const handleSelectionChange = useCallback( (selectedID: string) => { @@ -87,15 +96,55 @@ export default function RepositoryFinder({ // searchString ignore here as list is already pre-filtered against it // w/ mirrored state via useUnifiedRepositorySearch (searchString: string) => { - return repos.map((repo) => { + const result = repos.map((repo) => { return { id: repo.projectId || repo.url, element: , isSelectable: true, } as ComboboxElement; }); + if (hasMore) { + // add an element that tells the user to refince the search + result.push({ + id: "more", + element: ( +
+ Repo missing? Try refining your search. +
+ ), + isSelectable: false, + } as ComboboxElement); + } + if (searchString.length >= 3 && authProviders.data?.some((p) => p.authProviderType === "BitbucketServer")) { + // add an element that tells the user that the Bitbucket Server does only support prefix search + result.push({ + id: "bitbucket-server", + element: ( +
+
+ + Bitbucket Server only supports searching by prefix. +
+
+ ), + isSelectable: false, + } as ComboboxElement); + } + if (searchString.length < 3) { + // add an element that tells the user to type more + result.push({ + id: "not-searched", + element: ( +
+ Please type at least 3 characters to search. +
+ ), + isSelectable: false, + } as ComboboxElement); + } + return result; }, - [repos], + [repos, hasMore, authProviders.data], ); return ( diff --git a/components/dashboard/src/data/git-providers/search-repositories-query.ts b/components/dashboard/src/data/git-providers/search-repositories-query.ts index 49c27cfa0e7511..9ec13ca8bad105 100644 --- a/components/dashboard/src/data/git-providers/search-repositories-query.ts +++ b/components/dashboard/src/data/git-providers/search-repositories-query.ts @@ -10,18 +10,19 @@ import { useCurrentOrg } from "../organizations/orgs-query"; import { useDebounce } from "../../hooks/use-debounce"; import { useFeatureFlag } from "../featureflag-query"; -export const useSearchRepositories = ({ searchString }: { searchString: string }) => { +export const useSearchRepositories = ({ searchString, limit }: { searchString: string; limit: number }) => { // This disables the search behavior when flag is disabled const repositoryFinderSearchEnabled = useFeatureFlag("repositoryFinderSearch"); const { data: org } = useCurrentOrg(); const debouncedSearchString = useDebounce(searchString); return useQuery( - ["search-repositories", { organizationId: org?.id || "", searchString: debouncedSearchString }], + ["search-repositories", { organizationId: org?.id || "", searchString: debouncedSearchString, limit }], async () => { return await getGitpodService().server.searchRepositories({ searchString, organizationId: org?.id ?? "", + limit, }); }, { diff --git a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts index 0642bb47c5246b..1df558f9c112c7 100644 --- a/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts +++ b/components/dashboard/src/data/git-providers/unified-repositories-search-query.ts @@ -17,15 +17,18 @@ type UnifiedRepositorySearchArgs = { // Combines the suggested repositories and the search repositories query into one hook export const useUnifiedRepositorySearch = ({ searchString, excludeProjects = false }: UnifiedRepositorySearchArgs) => { const suggestedQuery = useSuggestedRepositories(); - const searchQuery = useSearchRepositories({ searchString }); + const searchLimit = 30; + const searchQuery = useSearchRepositories({ searchString, limit: searchLimit }); const filteredRepos = useMemo(() => { const flattenedRepos = [suggestedQuery.data || [], searchQuery.data || []].flat(); + return deduplicateAndFilterRepositories(searchString, excludeProjects, flattenedRepos); }, [excludeProjects, searchQuery.data, searchString, suggestedQuery.data]); return { data: filteredRepos, + hasMore: searchQuery.data?.length === searchLimit, isLoading: suggestedQuery.isLoading, isSearching: searchQuery.isFetching, isError: suggestedQuery.isError || searchQuery.isError, diff --git a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx index c1d542bd88ca33..efe6d490422349 100644 --- a/components/dashboard/src/workspaces/CreateWorkspacePage.tsx +++ b/components/dashboard/src/workspaces/CreateWorkspacePage.tsx @@ -354,7 +354,7 @@ export function CreateWorkspacePage() { if (needsGitAuthorization) { return (
-
+
New Workspace
Start a new workspace with the following options. @@ -367,7 +367,7 @@ export function CreateWorkspacePage() { return (
-
+
New Workspace
Create a new workspace in the{" "} diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index ebd6b624314e34..f6a18474884ea9 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -318,6 +318,7 @@ export interface GetProviderRepositoriesParams { export interface SearchRepositoriesParams { organizationId: string; searchString: string; + limit?: number; // defaults to 30 } export interface ProviderRepository { name: string; diff --git a/components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts b/components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts index b9574dc835c0be..62087fdabd4f20 100644 --- a/components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts +++ b/components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts @@ -208,9 +208,9 @@ export class BitbucketServerRepositoryProvider implements RepositoryProvider { return commits.map((c) => c.id); } - public async searchRepos(user: User, searchString: string): Promise { - // Only load 1 page of 10 results for our searchString - const results = await this.api.getRepos(user, { maxPages: 1, limit: 30, searchString }); + public async searchRepos(user: User, searchString: string, limit: number): Promise { + // Only load 1 page of limit results for our searchString + const results = await this.api.getRepos(user, { maxPages: 1, limit, searchString }); const repos: RepositoryInfo[] = []; results.forEach((r) => { diff --git a/components/server/src/bitbucket/bitbucket-repository-provider.ts b/components/server/src/bitbucket/bitbucket-repository-provider.ts index d7e76a33d5d3e6..e0c3f2a2be6c3d 100644 --- a/components/server/src/bitbucket/bitbucket-repository-provider.ts +++ b/components/server/src/bitbucket/bitbucket-repository-provider.ts @@ -164,10 +164,10 @@ export class BitbucketRepositoryProvider implements RepositoryProvider { // 1. Get all workspaces for the user // 2. Fan out and search each workspace for the repos // - public async searchRepos(user: User, searchString: string): Promise { + public async searchRepos(user: User, searchString: string, limit: number): Promise { const api = await this.apiFactory.create(user); - const workspaces = await api.workspaces.getWorkspaces({ pagelen: 25 }); + const workspaces = await api.workspaces.getWorkspaces({ pagelen: limit }); const workspaceSlugs: string[] = ( workspaces.data.values?.map((w) => { diff --git a/components/server/src/github/github-repository-provider.ts b/components/server/src/github/github-repository-provider.ts index 6bb856e84205b8..239ebd278e62a4 100644 --- a/components/server/src/github/github-repository-provider.ts +++ b/components/server/src/github/github-repository-provider.ts @@ -230,7 +230,7 @@ export class GithubRepositoryProvider implements RepositoryProvider { return repos; } - public async searchRepos(user: User, searchString: string): Promise { + public async searchRepos(user: User, searchString: string, limit: number): Promise { // graphql api only returns public orgs, so we need to use the rest api to get both public & private orgs const orgs = await this.github.run(user, async (api) => { return api.orgs.listMembershipsForAuthenticatedUser({ @@ -245,7 +245,7 @@ export class GithubRepositoryProvider implements RepositoryProvider { const query = JSON.stringify(`${searchString} in:name user:@me ${orgFilters}`); const repoSearchQuery = ` query SearchRepos { - search (type: REPOSITORY, first: 10, query: ${query}){ + search (type: REPOSITORY, first: ${limit}, query: ${query}){ edges { node { ... on Repository { diff --git a/components/server/src/gitlab/gitlab-repository-provider.ts b/components/server/src/gitlab/gitlab-repository-provider.ts index 68f3af5591c3a1..2a5adf53bc88b8 100644 --- a/components/server/src/gitlab/gitlab-repository-provider.ts +++ b/components/server/src/gitlab/gitlab-repository-provider.ts @@ -140,11 +140,12 @@ export class GitlabRepositoryProvider implements RepositoryProvider { return result.slice(1).map((c: GitLab.Commit) => c.id); } - public async searchRepos(user: User, searchString: string): Promise { + public async searchRepos(user: User, searchString: string, limit: number): Promise { const result = await this.gitlab.run(user, async (gitlab) => { return gitlab.Projects.all({ membership: true, search: searchString, + perPage: limit, simple: true, }); }); diff --git a/components/server/src/repohost/repository-provider.ts b/components/server/src/repohost/repository-provider.ts index 8d332051c6d9ab..5e059a1bdae6b4 100644 --- a/components/server/src/repohost/repository-provider.ts +++ b/components/server/src/repohost/repository-provider.ts @@ -15,5 +15,5 @@ export interface RepositoryProvider { getUserRepos(user: User): Promise; hasReadAccess(user: User, owner: string, repo: string): Promise; getCommitHistory(user: User, owner: string, repo: string, ref: string, maxDepth: number): Promise; - searchRepos(user: User, searchString: string): Promise; + searchRepos(user: User, searchString: string, limit: number): Promise; } diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index d161c81b4f76c0..2bffb0cac6361d 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -1694,6 +1694,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const user = await this.checkAndBlockUser("searchRepositories"); const logCtx: LogContext = { userId: user.id }; + const limit: number = params.limit || 30; // Search repos across scm providers for this user // Will search personal, and org repos @@ -1708,7 +1709,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { log.error(logCtx, "Unsupported repository host: " + p.host); return []; } - const repos = await services.repositoryProvider.searchRepos(user, params.searchString); + const repos = await services.repositoryProvider.searchRepos(user, params.searchString, limit); return repos.map((r) => suggestionFromUserRepo({ @@ -1726,7 +1727,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const sortedRepos = sortSuggestedRepositories(providerRepos.flat()); - return sortedRepos.map( + //return only the first 'limit' results + return sortedRepos.slice(0, limit).map( (repo): SuggestedRepository => ({ url: repo.url, repositoryName: repo.repositoryName,