From 4896f98bea6342fd461745c1163d4f7016f29bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Tue, 3 Dec 2024 13:49:38 +0000 Subject: [PATCH 1/6] Allow short search strings on new workspace page --- .../src/components/RepositoryFinder.tsx | 71 ++++++++++++++++--- .../search-repositories-query.ts | 2 +- .../unified-repositories-search-query.ts | 4 +- components/dashboard/src/utils.ts | 53 ++++++++++++++ .../src/workspaces/BrowserExtensionBanner.tsx | 46 +----------- 5 files changed, 118 insertions(+), 58 deletions(-) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 80be561b9e41ca..a3e920d5193b25 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,7 +320,7 @@ export default function RepositoryFinder({ } if ( - searchString.length >= 3 && + searchString.length > 0 && authProviders.data?.some((p) => p.type === AuthProviderType.BITBUCKET_SERVER) && !onlyConfigurations ) { @@ -327,27 +337,58 @@ export default function RepositoryFinder({ }); } - if (searchString.length >= 3 && authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS)) { - // ENT-780 + if ( + searchString.length > 0 && + searchString.length < 3 && + authProviders.data?.some((p) => p.type === AuthProviderType.GITLAB) && + !onlyConfigurations + ) { + // 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 is too short for searching on GitLab. +
+ ), + isSelectable: false, + }); + } + + const setupProvidersWithoutPathSearchSupport = usedProviders.filter((p) => + ["Bitbucket", "Bitbucket Server", "GitLab"].includes(p), + ); + if ( + searchString.length > 1 && + setupProvidersWithoutPathSearchSupport.length > 0 && + searchString.includes("/") + ) { + result.push({ + id: "whole-path-matching-unsupported", + element: ( +
+ + + {usedProviders + ? conjunctScmProviders(setupProvidersWithoutPathSearchSupport) + : "Some providers"}{" "} + only support searching by repository name, not full paths. +
), isSelectable: false, }); } - if (searchString.length < 3) { - // add an element that tells the user to type more + if (searchString.length > 0 && authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS)) { + // CLC-780 result.push({ - id: "not-searched", + id: "azure-devops", element: ( -
- Please type at least 3 characters to search. +
+ + Azure DevOps doesn't support repository searching.
), isSelectable: false, @@ -356,7 +397,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..e2431be9f97c0b 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.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..9f53b486369150 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,53 @@ export function isTrustedUrlOrPath(urlOrPath: string) { } return isTrusted; } + +type UnifiedAuthProvider = "Bitbucket" | "GitLab" | "GitHub" | "Azure DevOps"; + +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; +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(isIdentity) + .map((provider) => provider.type); + + const unifiedProviders = userProviders + .map((type) => unifyProviderType(type)) + .filter(isAuthProviderType) + .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]); From 0147dd7c4d083f13713abdfa8513ae08d70bff66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 4 Dec 2024 08:57:11 +0000 Subject: [PATCH 2/6] Get rid of `isIdentity` --- components/dashboard/src/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/dashboard/src/utils.ts b/components/dashboard/src/utils.ts index 9f53b486369150..8102e385bd5805 100644 --- a/components/dashboard/src/utils.ts +++ b/components/dashboard/src/utils.ts @@ -236,7 +236,6 @@ export function isTrustedUrlOrPath(urlOrPath: string) { type UnifiedAuthProvider = "Bitbucket" | "GitLab" | "GitHub" | "Azure DevOps"; -const isIdentity = (identity?: AuthProviderDescription): identity is AuthProviderDescription => !!identity; const unifyProviderType = (type: AuthProviderType): UnifiedAuthProvider | undefined => { switch (type) { case AuthProviderType.BITBUCKET: @@ -261,7 +260,7 @@ export const getDeduplicatedScmProviders = ( const userIdentities = user.identities.map((identity) => identity.authProviderId); const userProviders = userIdentities .map((id) => descriptions?.find((provider) => provider.id === id)) - .filter(isIdentity) + .filter((p) => !!p) .map((provider) => provider.type); const unifiedProviders = userProviders From c50559bf614ff590c221e3f986fe75efbef897ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 4 Dec 2024 09:07:41 +0000 Subject: [PATCH 3/6] Only show nudges when `!onlyConfigurations` Co-authored-by: Gero Posmyk-Leinemann --- .../dashboard/src/components/RepositoryFinder.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index a3e920d5193b25..875cf274ec7f8d 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -320,9 +320,9 @@ export default function RepositoryFinder({ } if ( + !onlyConfigurations && searchString.length > 0 && - authProviders.data?.some((p) => p.type === AuthProviderType.BITBUCKET_SERVER) && - !onlyConfigurations + 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({ @@ -338,10 +338,10 @@ export default function RepositoryFinder({ } if ( + !onlyConfigurations && searchString.length > 0 && searchString.length < 3 && - authProviders.data?.some((p) => p.type === AuthProviderType.GITLAB) && - !onlyConfigurations + authProviders.data?.some((p) => p.type === AuthProviderType.GITLAB) ) { // add an element that tells the user that GitLab only does exact searches for short queries result.push({ @@ -360,6 +360,7 @@ export default function RepositoryFinder({ ["Bitbucket", "Bitbucket Server", "GitLab"].includes(p), ); if ( + !onlyConfigurations && searchString.length > 1 && setupProvidersWithoutPathSearchSupport.length > 0 && searchString.includes("/") @@ -381,7 +382,11 @@ export default function RepositoryFinder({ }); } - if (searchString.length > 0 && authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS)) { + if ( + !onlyConfigurations && + searchString.length > 0 && + authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS) + ) { // CLC-780 result.push({ id: "azure-devops", From 89221af6dda9ae5df0786a3b63f4acb538c5143b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 4 Dec 2024 12:12:45 +0000 Subject: [PATCH 4/6] Improve GitLab search length help text Co-authored-by: Gero Posmyk-Leinemann --- components/dashboard/src/components/RepositoryFinder.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 875cf274ec7f8d..970e7ea5310fdd 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -341,7 +341,7 @@ export default function RepositoryFinder({ !onlyConfigurations && searchString.length > 0 && searchString.length < 3 && - authProviders.data?.some((p) => p.type === AuthProviderType.GITLAB) + usedProviders.includes("GitLab") ) { // add an element that tells the user that GitLab only does exact searches for short queries result.push({ @@ -349,7 +349,10 @@ export default function RepositoryFinder({ element: (
- Search is too short for searching on GitLab. + + Search text is < 3 characters. GitLab will only show exact matches for short + searches. +
), isSelectable: false, From 3dcba47a27e6052ac5e5c6ee48c4a6807c68f12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 4 Dec 2024 13:41:00 +0000 Subject: [PATCH 5/6] Remove duplicate Bitbucket value check --- components/dashboard/src/components/RepositoryFinder.tsx | 8 ++------ components/dashboard/src/utils.ts | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/components/dashboard/src/components/RepositoryFinder.tsx b/components/dashboard/src/components/RepositoryFinder.tsx index 970e7ea5310fdd..3827e303250e3a 100644 --- a/components/dashboard/src/components/RepositoryFinder.tsx +++ b/components/dashboard/src/components/RepositoryFinder.tsx @@ -360,7 +360,7 @@ export default function RepositoryFinder({ } const setupProvidersWithoutPathSearchSupport = usedProviders.filter((p) => - ["Bitbucket", "Bitbucket Server", "GitLab"].includes(p), + ["Bitbucket", "GitLab"].includes(p), ); if ( !onlyConfigurations && @@ -385,11 +385,7 @@ export default function RepositoryFinder({ }); } - if ( - !onlyConfigurations && - searchString.length > 0 && - authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS) - ) { + if (!onlyConfigurations && searchString.length > 0 && usedProviders.includes("Azure DevOps")) { // CLC-780 result.push({ id: "azure-devops", diff --git a/components/dashboard/src/utils.ts b/components/dashboard/src/utils.ts index 8102e385bd5805..5a264b8da152e6 100644 --- a/components/dashboard/src/utils.ts +++ b/components/dashboard/src/utils.ts @@ -252,7 +252,6 @@ const unifyProviderType = (type: AuthProviderType): UnifiedAuthProvider | undefi } }; -const isAuthProviderType = (type?: UnifiedAuthProvider): type is UnifiedAuthProvider => !!type; export const getDeduplicatedScmProviders = ( user: User, descriptions: AuthProviderDescription[], @@ -265,7 +264,7 @@ export const getDeduplicatedScmProviders = ( const unifiedProviders = userProviders .map((type) => unifyProviderType(type)) - .filter(isAuthProviderType) + .filter((t) => !!t) .sort(); return uniq(unifiedProviders); From b029dd51f07ab655a2e8d2b8ac2ab557b65e0f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Tron=C3=AD=C4=8Dek?= Date: Wed, 4 Dec 2024 14:33:51 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/data/git-providers/search-repositories-query.ts | 2 +- components/dashboard/src/utils.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 e2431be9f97c0b..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 > 0, + 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/utils.ts b/components/dashboard/src/utils.ts index 5a264b8da152e6..36048fe7104276 100644 --- a/components/dashboard/src/utils.ts +++ b/components/dashboard/src/utils.ts @@ -235,7 +235,6 @@ export function isTrustedUrlOrPath(urlOrPath: string) { } type UnifiedAuthProvider = "Bitbucket" | "GitLab" | "GitHub" | "Azure DevOps"; - const unifyProviderType = (type: AuthProviderType): UnifiedAuthProvider | undefined => { switch (type) { case AuthProviderType.BITBUCKET: