Skip to content

Commit a124119

Browse files
committed
Adding getSuggestedRepositories method
1 parent b2bd7fc commit a124119

File tree

7 files changed

+216
-6
lines changed

7 files changed

+216
-6
lines changed

components/dashboard/src/components/RepositoryFinder.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { useCallback, useEffect, useState } from "react";
7+
import { useCallback, useEffect, useMemo, useState } from "react";
88
import { getGitpodService } from "../service/service";
99
import { DropDown2, DropDown2Element, DropDown2SelectedElement } from "./DropDown2";
1010
import Repository from "../icons/Repository.svg";
11+
import { useSuggestedRepositories } from "../data/git-providers/suggested-repositories-query";
12+
import { useFeatureFlag } from "../data/featureflag-query";
1113

1214
const LOCAL_STORAGE_KEY = "open-in-gitpod-search-data";
1315

@@ -28,7 +30,20 @@ function stripOffProtocol(url: string): string {
2830
}
2931

3032
export default function RepositoryFinder(props: RepositoryFinderProps) {
33+
const includeProjectsOnCreateWorkspace = useFeatureFlag("includeProjectsOnCreateWorkspace");
34+
3135
const [suggestedContextURLs, setSuggestedContextURLs] = useState<string[]>(loadSearchData());
36+
const { data: suggestedRepos } = useSuggestedRepositories();
37+
38+
const suggestedRepoURLs = useMemo(() => {
39+
// If the flag is disabled continue to use suggestedContextURLs
40+
if (!includeProjectsOnCreateWorkspace) {
41+
return suggestedContextURLs;
42+
}
43+
44+
return suggestedRepos?.map((repo) => repo.url) || [];
45+
}, [suggestedContextURLs, suggestedRepos, includeProjectsOnCreateWorkspace]);
46+
3247
useEffect(() => {
3348
getGitpodService()
3449
.server.getSuggestedContextURLs()
@@ -43,21 +58,21 @@ export default function RepositoryFinder(props: RepositoryFinderProps) {
4358
let result: string[];
4459
searchString = searchString.trim();
4560
if (searchString.length > 1) {
46-
result = suggestedContextURLs.filter((e) => e.toLowerCase().indexOf(searchString.toLowerCase()) !== -1);
61+
result = suggestedRepoURLs.filter((e) => e.toLowerCase().indexOf(searchString.toLowerCase()) !== -1);
4762
if (result.length > 200) {
4863
result = result.slice(0, 200);
4964
}
5065
if (result.length === 0) {
5166
try {
5267
// If the searchString is a URL, and it's not present in the proposed results, "artificially" add it here.
5368
new URL(searchString);
54-
if (!suggestedContextURLs.includes(searchString)) {
69+
if (!suggestedRepoURLs.includes(searchString)) {
5570
result.push(searchString);
5671
}
5772
} catch {}
5873
}
5974
} else {
60-
result = suggestedContextURLs.slice(0, 200);
75+
result = suggestedRepoURLs.slice(0, 200);
6176
}
6277

6378
return result.map(
@@ -79,7 +94,7 @@ export default function RepositoryFinder(props: RepositoryFinderProps) {
7994
} as DropDown2Element),
8095
);
8196
},
82-
[suggestedContextURLs],
97+
[suggestedRepoURLs],
8398
);
8499

85100
const element = (

components/dashboard/src/data/featureflag-query.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const featureFlags = {
2727
supervisor_live_git_status: false,
2828
enabledOrbitalDiscoveries: "",
2929
newProjectIncrementalRepoSearchBBS: false,
30+
includeProjectsOnCreateWorkspace: false,
3031
};
3132

3233
type FeatureFlags = typeof featureFlags;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { useQuery } from "@tanstack/react-query";
8+
import { useCurrentOrg } from "../organizations/orgs-query";
9+
import { getGitpodService } from "../../service/service";
10+
11+
export const useSuggestedRepositories = () => {
12+
const { data: org } = useCurrentOrg();
13+
14+
return useQuery(["suggested-repositories", { orgId: org?.id }], async () => {
15+
if (!org) {
16+
throw new Error("No org selected");
17+
}
18+
19+
return await getGitpodService().server.getSuggestedRepositories(org.id);
20+
});
21+
};

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
WorkspaceTimeoutSetting,
3131
WorkspaceContext,
3232
LinkedInProfile,
33+
SuggestedRepository,
3334
} from "./protocol";
3435
import {
3536
Team,
@@ -98,6 +99,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
9899
getWorkspaceUsers(workspaceId: string): Promise<WorkspaceInstanceUser[]>;
99100
getFeaturedRepositories(): Promise<WhitelistedRepository[]>;
100101
getSuggestedContextURLs(): Promise<string[]>;
102+
getSuggestedRepositories(organizationId: string): Promise<SuggestedRepository[]>;
101103
/**
102104
* **Security:**
103105
* Sensitive information like an owner token is erased, since it allows access for all team members.

components/gitpod-protocol/src/protocol.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,3 +1615,10 @@ export interface LinkedInProfile {
16151615
profilePicture: string;
16161616
emailAddress: string;
16171617
}
1618+
1619+
export type SuggestedRepository = {
1620+
url: string;
1621+
projectId?: string;
1622+
projectName?: string;
1623+
repositoryName?: string;
1624+
};

components/server/src/auth/rate-limiter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const defaultFunctions: FunctionsConfig = {
6565
getWorkspaceUsers: { group: "default", points: 1 },
6666
getFeaturedRepositories: { group: "default", points: 1 },
6767
getSuggestedContextURLs: { group: "default", points: 1 },
68+
getSuggestedRepositories: { group: "default", points: 1 },
6869
getWorkspace: { group: "default", points: 1 },
6970
isWorkspaceOwner: { group: "default", points: 1 },
7071
getOwnerToken: { group: "default", points: 1 },

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
RoleOrPermission,
6868
WorkspaceInstanceRepoStatus,
6969
GetProviderRepositoriesParams,
70+
SuggestedRepository,
7071
} from "@gitpod/gitpod-protocol";
7172
import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol";
7273
import {
@@ -1067,7 +1068,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
10671068

10681069
public async getWorkspaces(
10691070
ctx: TraceContext,
1070-
options: GitpodServer.GetWorkspacesOptions,
1071+
options: GitpodServer.GetWorkspacesOptions = {},
10711072
): Promise<WorkspaceInfo[]> {
10721073
traceAPIParams(ctx, { options });
10731074

@@ -1736,6 +1737,168 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
17361737
.map((s) => s.url);
17371738
}
17381739

1740+
public async getSuggestedRepositories(ctx: TraceContext, organizationId: string): Promise<SuggestedRepository[]> {
1741+
const user = await this.checkAndBlockUser("getSuggestedProjects");
1742+
const logCtx: LogContext = { userId: user.id };
1743+
1744+
type SuggestedRepositoryWithSorting = SuggestedRepository & {
1745+
priority: number;
1746+
lastUse?: string;
1747+
};
1748+
1749+
const fetchExampleRepos = async (): Promise<SuggestedRepositoryWithSorting[]> => {
1750+
const repos = await this.getFeaturedRepositories(ctx);
1751+
1752+
return repos.map((repo) => ({
1753+
repositoryName: repo.name,
1754+
url: repo.url,
1755+
priority: 0,
1756+
}));
1757+
};
1758+
1759+
const fetchProjects = async (): Promise<SuggestedRepositoryWithSorting[]> => {
1760+
const projects = await this.getAccessibleProjects();
1761+
1762+
return projects.map((project) => ({
1763+
// TODO: determine if we can parse this name out of the cloneUrl
1764+
// repositoryName: project.id,
1765+
projectName: project.name,
1766+
projectId: project.id,
1767+
url: project.cloneUrl.replace(/\.git$/, ""),
1768+
priority: 1,
1769+
}));
1770+
};
1771+
1772+
const fetchUserRepos = async (): Promise<SuggestedRepositoryWithSorting[]> => {
1773+
// User repositories (from Git hosts directly)
1774+
const authProviders = await this.getAuthProviders(ctx);
1775+
1776+
const providerRepos = await Promise.all(
1777+
authProviders.map(async (p): Promise<SuggestedRepositoryWithSorting[]> => {
1778+
try {
1779+
const hostContext = this.hostContextProvider.get(p.host);
1780+
const services = hostContext?.services;
1781+
if (!services) {
1782+
log.error(logCtx, "Unsupported repository host: " + p.host);
1783+
return [];
1784+
}
1785+
const userRepos = await services.repositoryProvider.getUserRepos(user);
1786+
1787+
return userRepos.map((r) => ({
1788+
// repositoryName: "",
1789+
url: r.replace(/\.git$/, ""),
1790+
priority: 5,
1791+
}));
1792+
} catch (error) {
1793+
log.debug(logCtx, "Could not get user repositories from host " + p.host, error);
1794+
}
1795+
1796+
return [];
1797+
}),
1798+
);
1799+
1800+
return providerRepos.flat();
1801+
};
1802+
1803+
const fetchRecentRepos = async (): Promise<SuggestedRepositoryWithSorting[]> => {
1804+
const workspaces = await this.getWorkspaces(ctx);
1805+
const recentRepos: SuggestedRepositoryWithSorting[] = [];
1806+
1807+
for (const ws of workspaces) {
1808+
let repoUrl;
1809+
if (CommitContext.is(ws.workspace.context)) {
1810+
repoUrl = ws.workspace.context?.repository?.cloneUrl?.replace(/\.git$/, "");
1811+
}
1812+
if (!repoUrl) {
1813+
repoUrl = ws.workspace.contextURL;
1814+
}
1815+
if (repoUrl) {
1816+
const lastUse = WorkspaceInfo.lastActiveISODate(ws);
1817+
1818+
recentRepos.push({
1819+
url: repoUrl,
1820+
projectId: ws.workspace.projectId,
1821+
// TODO: get project name
1822+
lastUse,
1823+
priority: 10,
1824+
});
1825+
}
1826+
}
1827+
1828+
return recentRepos;
1829+
};
1830+
1831+
const [examples, projects, userRepos, recentRepos] = await Promise.allSettled([
1832+
fetchExampleRepos(),
1833+
fetchProjects(),
1834+
fetchUserRepos(),
1835+
fetchRecentRepos(),
1836+
]);
1837+
1838+
const uniqueRepositories = new Map<string, SuggestedRepositoryWithSorting>();
1839+
1840+
// Add projects first
1841+
if (projects.status === "fulfilled") {
1842+
for (const repo of projects.value) {
1843+
uniqueRepositories.set(repo.url, repo);
1844+
}
1845+
} else {
1846+
log.error(logCtx, "Could not fetch projects", projects.reason);
1847+
}
1848+
1849+
// Add examples
1850+
if (examples.status === "fulfilled") {
1851+
for (const repo of examples.value) {
1852+
if (!uniqueRepositories.has(repo.url)) {
1853+
uniqueRepositories.set(repo.url, repo);
1854+
}
1855+
}
1856+
}
1857+
1858+
// Add user repos
1859+
if (userRepos.status === "fulfilled") {
1860+
for (const repo of userRepos.value) {
1861+
if (!uniqueRepositories.has(repo.url)) {
1862+
uniqueRepositories.set(repo.url, repo);
1863+
}
1864+
}
1865+
}
1866+
1867+
// Add recent repos
1868+
if (recentRepos.status === "fulfilled") {
1869+
for (const repo of recentRepos.value) {
1870+
if (!uniqueRepositories.has(repo.url)) {
1871+
uniqueRepositories.set(repo.url, repo);
1872+
}
1873+
}
1874+
}
1875+
1876+
const suggestedRepositories = Array.from(uniqueRepositories.values()).sort((a, b) => {
1877+
// priority first
1878+
if (a.priority !== b.priority) {
1879+
return a.priority < b.priority ? 1 : -1;
1880+
}
1881+
// Most recently used second
1882+
if (b.lastUse || a.lastUse) {
1883+
const la = a.lastUse || "";
1884+
const lb = b.lastUse || "";
1885+
return la < lb ? 1 : la === lb ? 0 : -1;
1886+
}
1887+
// Otherwise, alphasort
1888+
const ua = a.url.toLowerCase();
1889+
const ub = b.url.toLowerCase();
1890+
return ua > ub ? 1 : ua === ub ? 0 : -1;
1891+
});
1892+
1893+
return suggestedRepositories.map((repo) => ({
1894+
url: repo.url,
1895+
projectId: repo.projectId,
1896+
projectName: repo.projectName,
1897+
// TODO: determine if we want to include this
1898+
repositoryName: repo.repositoryName,
1899+
}));
1900+
}
1901+
17391902
public async setWorkspaceTimeout(
17401903
ctx: TraceContext,
17411904
workspaceId: string,

0 commit comments

Comments
 (0)