diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 80be561b9e41ca..3827e303250e3a 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -22,6 +22,8 @@ import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_ import { SuggestedRepository } from "@gitpod/public-api/lib/gitpod/v1/scm_pb"; import { PREDEFINED_REPOS } from "../data/git-providers/predefined-repos"; import { useConfiguration, useListConfigurations } from "../data/configurations/configuration-queries"; +import { useUserLoader } from "../hooks/use-user-loader"; +import { conjunctScmProviders, getDeduplicatedScmProviders } from "../utils"; const isPredefined = (repo: SuggestedRepository): boolean => { return PREDEFINED_REPOS.some((predefined) => predefined.url === repo.url) && !repo.configurationId; @@ -54,6 +56,8 @@ export default function RepositoryFinder({ onChange, }: RepositoryFinderProps) { const [searchString, setSearchString] = useState(""); + + const { user } = useUserLoader(); const { data: unifiedRepos, isLoading, @@ -120,6 +124,12 @@ export default function RepositoryFinder({ const authProviders = useAuthProviderDescriptions(); + const usedProviders = useMemo(() => { + if (!user || !authProviders.data) return []; + + return getDeduplicatedScmProviders(user, authProviders.data) ?? []; + }, [user, authProviders]); + const handleSelectionChange = useCallback( (selectedID: string) => { const matchingSuggestion = repos?.find( @@ -310,9 +320,9 @@ export default function RepositoryFinder({ } if ( - searchString.length >= 3 && - authProviders.data?.some((p) => p.type === AuthProviderType.BITBUCKET_SERVER) && - !onlyConfigurations + !onlyConfigurations && + searchString.length > 0 && + authProviders.data?.some((p) => p.type === AuthProviderType.BITBUCKET_SERVER) ) { // add an element that tells the user that the Bitbucket Server does only support prefix search result.push({ @@ -327,27 +337,62 @@ export default function RepositoryFinder({ }); } - if (searchString.length >= 3 && authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS)) { - // ENT-780 + if ( + !onlyConfigurations && + searchString.length > 0 && + searchString.length < 3 && + usedProviders.includes("GitLab") + ) { + // add an element that tells the user that GitLab only does exact searches for short queries result.push({ - id: "azure-devops", + id: "gitlab", element: (
- Azure DevOps doesn't support repository searching. + + Search text is < 3 characters. GitLab will only show exact matches for short + searches. +
), isSelectable: false, }); } - if (searchString.length < 3) { - // add an element that tells the user to type more + const setupProvidersWithoutPathSearchSupport = usedProviders.filter((p) => + ["Bitbucket", "GitLab"].includes(p), + ); + if ( + !onlyConfigurations && + searchString.length > 1 && + setupProvidersWithoutPathSearchSupport.length > 0 && + searchString.includes("/") + ) { result.push({ - id: "not-searched", + id: "whole-path-matching-unsupported", element: ( -
- Please type at least 3 characters to search. +
+ + + {usedProviders + ? conjunctScmProviders(setupProvidersWithoutPathSearchSupport) + : "Some providers"}{" "} + only support searching by repository name, not full paths. + +
+ ), + isSelectable: false, + }); + } + + if (!onlyConfigurations && searchString.length > 0 && usedProviders.includes("Azure DevOps")) { + // CLC-780 + result.push({ + id: "azure-devops", + element: ( +
+ + Azure DevOps doesn't support repository searching.
), isSelectable: false, @@ -356,7 +401,15 @@ export default function RepositoryFinder({ return result; }, - [isShowingExamples, onlyConfigurations, repos, hasMore, authProviders.data, filteredPredefinedRepos], + [ + isShowingExamples, + onlyConfigurations, + repos, + hasMore, + authProviders.data, + filteredPredefinedRepos, + usedProviders, + ], ); 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 410288da2d4c3d..c94e1cb8a0bbb8 100644 --- a/components/dashboard/src/data/git-providers/search-repositories-query.ts +++ b/components/dashboard/src/data/git-providers/search-repositories-query.ts @@ -24,7 +24,7 @@ export const useSearchRepositories = ({ searchString, limit }: { searchString: s return repositories; }, { - enabled: !!org && debouncedSearchString.length >= 3, + enabled: !!org && debouncedSearchString.trim().length > 0, // Need this to keep previous results while we wait for a new search to complete since debouncedSearchString changes and updates the key keepPreviousData: true, // We intentionally don't want to trigger refetches here to avoid a loading state side effect of focusing 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 4c9d250c4c35d1..e8e45b6e7acc8a 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 @@ -62,7 +62,7 @@ export const useUnifiedRepositorySearch = ({ }, [configurationSearch.data, excludeConfigurations]); const filteredRepos = useMemo(() => { - const repos = [suggestedQuery.data || [], searchQuery.data || [], flattenedConfigurations ?? []].flat(); + const repos = [suggestedQuery.data || [], flattenedConfigurations ?? [], searchQuery.data || []].flat(); return deduplicateAndFilterRepositories(searchString, excludeConfigurations, onlyConfigurations, repos); }, [ searchString, @@ -113,7 +113,7 @@ export function deduplicateAndFilterRepositories( } // filter out entries that don't match the search string - if (!`${repo.url}${repo.configurationName || ""}`.toLowerCase().includes(searchString.trim().toLowerCase())) { + if (!`${repo.url}${repo.configurationName ?? ""}`.toLowerCase().includes(searchString.trim().toLowerCase())) { continue; } // filter out duplicates diff --git a/components/dashboard/src/utils.ts b/components/dashboard/src/utils.ts index a47759e0aa2f3e..36048fe7104276 100644 --- a/components/dashboard/src/utils.ts +++ b/components/dashboard/src/utils.ts @@ -4,7 +4,10 @@ * See License.AGPL.txt in the project root for license information. */ +import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb"; +import { AuthProviderDescription, AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import EventEmitter from "events"; +import { uniq } from "lodash"; export interface PollOptions { backoffFactor: number; @@ -230,3 +233,50 @@ export function isTrustedUrlOrPath(urlOrPath: string) { } return isTrusted; } + +type UnifiedAuthProvider = "Bitbucket" | "GitLab" | "GitHub" | "Azure DevOps"; +const unifyProviderType = (type: AuthProviderType): UnifiedAuthProvider | undefined => { + switch (type) { + case AuthProviderType.BITBUCKET: + case AuthProviderType.BITBUCKET_SERVER: + return "Bitbucket"; + case AuthProviderType.GITHUB: + return "GitHub"; + case AuthProviderType.GITLAB: + return "GitLab"; + case AuthProviderType.AZURE_DEVOPS: + return "Azure DevOps"; + default: + return undefined; + } +}; + +export const getDeduplicatedScmProviders = ( + user: User, + descriptions: AuthProviderDescription[], +): UnifiedAuthProvider[] => { + const userIdentities = user.identities.map((identity) => identity.authProviderId); + const userProviders = userIdentities + .map((id) => descriptions?.find((provider) => provider.id === id)) + .filter((p) => !!p) + .map((provider) => provider.type); + + const unifiedProviders = userProviders + .map((type) => unifyProviderType(type)) + .filter((t) => !!t) + .sort(); + + return uniq(unifiedProviders); +}; + +export const disjunctScmProviders = (providers: UnifiedAuthProvider[]): string => { + const formatter = new Intl.ListFormat("en", { style: "long", type: "disjunction" }); + + return formatter.format(providers); +}; + +export const conjunctScmProviders = (providers: UnifiedAuthProvider[]): string => { + const formatter = new Intl.ListFormat("en", { style: "long", type: "conjunction" }); + + return formatter.format(providers); +}; diff --git a/components/dashboard/src/workspaces/BrowserExtensionBanner.tsx b/components/dashboard/src/workspaces/BrowserExtensionBanner.tsx index 136792c52f8aeb..6cc4e1f088b2fa 100644 --- a/components/dashboard/src/workspaces/BrowserExtensionBanner.tsx +++ b/components/dashboard/src/workspaces/BrowserExtensionBanner.tsx @@ -7,8 +7,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import UAParser from "ua-parser-js"; import { useUserLoader } from "../hooks/use-user-loader"; -import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb"; -import { AuthProviderDescription, AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query"; import { useFeatureFlag } from "../data/featureflag-query"; import { trackEvent } from "../Analytics"; @@ -17,7 +15,7 @@ import bitbucketButton from "../images/browser-extension/bitbucket.webp"; import githubButton from "../images/browser-extension/github.webp"; import gitlabButton from "../images/browser-extension/gitlab.webp"; import azuredevopsButton from "../images/browser-extension/azure-devops.webp"; -import uniq from "lodash/uniq"; +import { disjunctScmProviders, getDeduplicatedScmProviders } from "../utils"; const browserExtensionImages = { Bitbucket: bitbucketButton, @@ -31,7 +29,6 @@ type BrowserOption = { aliases?: string[]; url: string; }; -type UnifiedAuthProvider = "Bitbucket" | "GitLab" | "GitHub" | "Azure DevOps"; const installationOptions: BrowserOption[] = [ { @@ -46,45 +43,6 @@ const installationOptions: BrowserOption[] = [ }, ]; -const isIdentity = (identity?: AuthProviderDescription): identity is AuthProviderDescription => !!identity; -const unifyProviderType = (type: AuthProviderType): UnifiedAuthProvider | undefined => { - switch (type) { - case AuthProviderType.BITBUCKET: - case AuthProviderType.BITBUCKET_SERVER: - return "Bitbucket"; - case AuthProviderType.GITHUB: - return "GitHub"; - case AuthProviderType.GITLAB: - return "GitLab"; - case AuthProviderType.AZURE_DEVOPS: - return "Azure DevOps"; - default: - return undefined; - } -}; - -const isAuthProviderType = (type?: UnifiedAuthProvider): type is UnifiedAuthProvider => !!type; -const getDeduplicatedScmProviders = (user: User, descriptions: AuthProviderDescription[]): UnifiedAuthProvider[] => { - const userIdentities = user.identities.map((identity) => identity.authProviderId); - const userProviders = userIdentities - .map((id) => descriptions?.find((provider) => provider.id === id)) - .filter(isIdentity) - .map((provider) => provider.type); - - const unifiedProviders = userProviders - .map((type) => unifyProviderType(type)) - .filter(isAuthProviderType) - .sort(); - - return uniq(unifiedProviders); -}; - -const displayScmProviders = (providers: UnifiedAuthProvider[]): string => { - const formatter = new Intl.ListFormat("en", { style: "long", type: "disjunction" }); - - return formatter.format(providers); -}; - /** * Determines whether the extension has been able to access the current site in the past month. If it hasn't, it's most likely not installed or misconfigured */ @@ -108,7 +66,7 @@ export function BrowserExtensionBanner() { return getDeduplicatedScmProviders(user, authProviderDescriptions); }, [user, authProviderDescriptions]); - const scmProviderString = useMemo(() => usedProviders && displayScmProviders(usedProviders), [usedProviders]); + const scmProviderString = useMemo(() => usedProviders && disjunctScmProviders(usedProviders), [usedProviders]); const parser = useMemo(() => new UAParser(), []); const browserName = useMemo(() => parser.getBrowser().name?.toLowerCase(), [parser]);