Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions components/dashboard/src/components/RepositoryFinder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +56,8 @@ export default function RepositoryFinder({
onChange,
}: RepositoryFinderProps) {
const [searchString, setSearchString] = useState("");

const { user } = useUserLoader();
const {
data: unifiedRepos,
isLoading,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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({
Expand All @@ -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: (
<div className="text-sm text-pk-content-tertiary flex items-center">
<Exclamation2 className="w-4 h-4 mr-2" />
<span>Azure DevOps doesn't support repository searching.</span>
<span>
Search text is &lt; 3 characters. GitLab will only show exact matches for short
searches.
</span>
</div>
),
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: (
<div className="text-sm text-pk-content-tertiary">
Please type at least 3 characters to search.
<div className="text-sm text-pk-content-tertiary flex items-center">
<Exclamation2 className="w-4 h-4 mr-2" />
<span>
{usedProviders
? conjunctScmProviders(setupProvidersWithoutPathSearchSupport)
: "Some providers"}{" "}
only support searching by repository name, not full paths.
</span>
</div>
),
isSelectable: false,
});
}

if (!onlyConfigurations && searchString.length > 0 && usedProviders.includes("Azure DevOps")) {
// CLC-780
result.push({
id: "azure-devops",
element: (
<div className="text-sm text-pk-content-tertiary flex items-center">
<Exclamation2 className="w-4 h-4 mr-2" />
<span>Azure DevOps doesn't support repository searching.</span>
</div>
),
isSelectable: false,
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done to artificially boost the priority of imported repos vs SCM-provided results.

Every time I change this, I think about how much we need to get rid of it entirely and just let listSuggestedRepositories receive a search string, which will do all deduplication and ordering by itself.

return deduplicateAndFilterRepositories(searchString, excludeConfigurations, onlyConfigurations, repos);
}, [
searchString,
Expand Down Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions components/dashboard/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
backoffFactor: number;
Expand Down Expand Up @@ -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);
};
46 changes: 2 additions & 44 deletions components/dashboard/src/workspaces/BrowserExtensionBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -31,7 +29,6 @@ type BrowserOption = {
aliases?: string[];
url: string;
};
type UnifiedAuthProvider = "Bitbucket" | "GitLab" | "GitHub" | "Azure DevOps";

const installationOptions: BrowserOption[] = [
{
Expand All @@ -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
*/
Expand All @@ -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]);
Expand Down
Loading