From bce4aa2492485ae37ef3f471be8dfda2b09d8199 Mon Sep 17 00:00:00 2001 From: Alex Tugarev Date: Tue, 7 Dec 2021 12:29:41 +0000 Subject: [PATCH] [projects] remove configuration page from wizard instead of showing the configuration page, let's show a simple `New Workspace` button to start a workspace. we rescue the auto-inferred configuration or use the existing one to trigger a prebuild right away. Co-authored-by: Jan Keromnes Co-authored-by: Alex Tugarev --- .../dashboard/src/projects/NewProject.tsx | 97 +++++++++++++++---- .../gitpod-protocol/src/gitpod-service.ts | 2 + components/server/src/auth/rate-limiter.ts | 2 + .../server/src/projects/projects-service.ts | 16 ++- .../src/workspace/gitpod-server-impl.ts | 44 ++++++++- 5 files changed, 129 insertions(+), 32 deletions(-) diff --git a/components/dashboard/src/projects/NewProject.tsx b/components/dashboard/src/projects/NewProject.tsx index 8385b96016c13b..a839e0de3d0aeb 100644 --- a/components/dashboard/src/projects/NewProject.tsx +++ b/components/dashboard/src/projects/NewProject.tsx @@ -7,9 +7,9 @@ import { useContext, useEffect, useState } from "react"; import { getGitpodService, gitpodHostUrl } from "../service/service"; import { iconForAuthProvider, openAuthorizeWindow, simplifyProviderName } from "../provider-utils"; -import { AuthProviderInfo, ProviderRepository, Team, TeamMemberInfo, User } from "@gitpod/gitpod-protocol"; +import { AuthProviderInfo, Project, ProviderRepository, Team, TeamMemberInfo, User } from "@gitpod/gitpod-protocol"; import { TeamsContext } from "../teams/teams-context"; -import { useHistory, useLocation } from "react-router"; +import { useLocation } from "react-router"; import ContextMenu, { ContextMenuEntry } from "../components/ContextMenu"; import CaretDown from "../icons/CaretDown.svg"; import Plus from "../icons/Plus.svg"; @@ -23,7 +23,6 @@ import exclamation from "../images/exclamation.svg"; export default function NewProject() { const location = useLocation(); - const history = useHistory(); const { teams } = useContext(TeamsContext); const { user, setUser } = useContext(UserContext); @@ -33,12 +32,16 @@ export default function NewProject() { const [selectedAccount, setSelectedAccount] = useState(undefined); const [noOrgs, setNoOrgs] = useState(false); const [showGitProviders, setShowGitProviders] = useState(false); - const [selectedRepo, setSelectedRepo] = useState(undefined); + const [selectedRepo, setSelectedRepo] = useState(undefined); const [selectedTeamOrUser, setSelectedTeamOrUser] = useState(undefined); const [showNewTeam, setShowNewTeam] = useState(false); const [loaded, setLoaded] = useState(false); + const [project, setProject] = useState(); + const [guessedConfigString, setGuessedConfigString] = useState(); + const [sourceOfConfig, setSourceOfConfig] = useState<"repo" | "db" | undefined>(); + useEffect(() => { if (user && provider === undefined) { if (user.identities.find(i => i.authProviderId === "Public-GitLab")) { @@ -83,6 +86,32 @@ export default function NewProject() { })(); }, [teams]); + useEffect(() => { + if (selectedRepo) { + (async () => { + + try { + const guessedConfigStringPromise = getGitpodService().server.guessRepositoryConfiguration(selectedRepo.cloneUrl); + const repoConfigString = await getGitpodService().server.fetchRepositoryConfiguration(selectedRepo.cloneUrl); + if (repoConfigString) { + setSourceOfConfig("repo"); + } else { + setSourceOfConfig("db"); + setGuessedConfigString(await guessedConfigStringPromise || `tasks: + - init: | + echo 'TODO: build project' + command: | + echo 'TODO: start app'`); + } + } catch (error) { + console.error('Getting project configuration failed', error); + setSourceOfConfig(undefined); + } + + })(); + } + }, [selectedRepo]); + useEffect(() => { if (selectedTeamOrUser && selectedRepo) { createProject(selectedTeamOrUser, selectedRepo); @@ -118,6 +147,17 @@ export default function NewProject() { })(); }, [provider]); + useEffect(() => { + if (project && sourceOfConfig) { + (async () => { + if (guessedConfigString && sourceOfConfig === "db") { + await getGitpodService().server.setProjectConfiguration(project.id, guessedConfigString); + } + await getGitpodService().server.triggerPrebuild(project.id, null); + })(); + } + }, [project, sourceOfConfig]); + const isGitHub = () => provider === "github.com"; const isBitbucket = () => provider === "bitbucket.org"; @@ -180,16 +220,10 @@ export default function NewProject() { } } - const createProject = async (teamOrUser: Team | User, selectedRepo: string) => { + const createProject = async (teamOrUser: Team | User, repo: ProviderRepository) => { if (!provider || isBitbucket()) { return; } - const repo = reposInAccounts.find(r => r.account === selectedAccount && (r.path ? r.path === selectedRepo : r.name === selectedRepo)); - if (!repo) { - console.error("No repo selected!") - return; - } - const repoSlug = repo.path || repo.name; try { @@ -203,7 +237,7 @@ export default function NewProject() { appInstallationId: String(repo.installationId), }); - history.push(`/${User.is(teamOrUser) ? 'projects' : 't/'+teamOrUser.slug}/${project.slug}/configure`); + setProject(project); } catch (error) { const message = (error && error?.message) || "Failed to create new project." window.alert(message); @@ -294,7 +328,7 @@ export default function NewProject() {
{!r.inUse ? ( - + ) : (

already taken

)} @@ -428,14 +462,43 @@ export default function NewProject() {
); } + const onNewWorkspace = async () => { + const redirectToNewWorkspace = () => { + // instead of `history.push` we want forcibly to redirect here in order to avoid a following redirect from `/` -> `/projects` (cf. App.tsx) + const url = new URL(window.location.toString()); + url.pathname = "/"; + url.hash = project?.cloneUrl!; + window.location.href = url.toString(); + } + redirectToNewWorkspace(); + } + return (
-

New Project

- {!selectedRepo && renderSelectRepository()} + {!project + ? (<> +

New Project

+ + {!selectedRepo && renderSelectRepository()} + + {selectedRepo && !selectedTeamOrUser && renderSelectTeam()} + + {selectedRepo && selectedTeamOrUser && (
)} + ) + : (<> +

Project created

+

Navigate to the details of {project.name}.

+ +
+ +
+
+ {sourceOfConfig === "db" && (

Start with a configuration example.

)} + {sourceOfConfig === "repo" && (

Start with the configuration from git.

)} +
- {selectedRepo && !selectedTeamOrUser && renderSelectTeam()} + )} - {selectedRepo && selectedTeamOrUser && (
)} {isBitbucket() && renderBitbucketWarning()} diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 6760c278263ea4..d30dbd75233ae9 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -139,6 +139,8 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, setProjectConfiguration(projectId: string, configString: string): Promise; fetchProjectRepositoryConfiguration(projectId: string): Promise; guessProjectConfiguration(projectId: string): Promise; + fetchRepositoryConfiguration(cloneUrl: string): Promise; + guessRepositoryConfiguration(cloneUrl: string): Promise; // content service getContentBlobUploadUrl(name: string): Promise diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 98fa359c29c05e..ff1ffa11130919 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -101,6 +101,8 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig { "setProjectConfiguration": { group: "default", points: 1 }, "fetchProjectRepositoryConfiguration": { group: "default", points: 1 }, "guessProjectConfiguration": { group: "default", points: 1 }, + "fetchRepositoryConfiguration": { group: "default", points: 1 }, + "guessRepositoryConfiguration": { group: "default", points: 1 }, "getContentBlobUploadUrl": { group: "default", points: 1 }, "getContentBlobDownloadUrl": { group: "default", points: 1 }, "getGitpodTokens": { group: "default", points: 1 }, diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index 2c6e4ce71154fc..6688e110a24042 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -182,12 +182,8 @@ export class ProjectsService { return this.projectDB.setProjectConfiguration(projectId, config); } - protected async getRepositoryFileProviderAndCommitContext(ctx: TraceContext, user: User, projectId: string): Promise<{fileProvider: FileProvider, commitContext: CommitContext}> { - const project = await this.getProject(projectId); - if (!project) { - throw new Error("Project not found"); - } - const normalizedContextUrl = this.contextParser.normalizeContextURL(project.cloneUrl); + protected async getRepositoryFileProviderAndCommitContext(ctx: TraceContext, user: User, cloneUrl: string): Promise<{fileProvider: FileProvider, commitContext: CommitContext}> { + const normalizedContextUrl = this.contextParser.normalizeContextURL(cloneUrl); const commitContext = (await this.contextParser.handle(ctx, user, normalizedContextUrl)) as CommitContext; const { host } = commitContext.repository; const hostContext = this.hostContextProvider.get(host); @@ -198,8 +194,8 @@ export class ProjectsService { return { fileProvider, commitContext }; } - async fetchProjectRepositoryConfiguration(ctx: TraceContext, user: User, projectId: string): Promise { - const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, projectId); + async fetchRepositoryConfiguration(ctx: TraceContext, user: User, cloneUrl: string): Promise { + const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, cloneUrl); const configString = await fileProvider.getGitpodFileContent(commitContext, user); return configString; } @@ -207,8 +203,8 @@ export class ProjectsService { // a static cache used to prefetch inferrer related files in parallel in advance private requestedPaths = new Set(); - async guessProjectConfiguration(ctx: TraceContext, user: User, projectId: string): Promise { - const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, projectId); + async guessRepositoryConfiguration(ctx: TraceContext, user: User, cloneUrl: string): Promise { + const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, cloneUrl); const cache: { [path: string]: Promise } = {}; const readFile = async (path: string) => { if (path in cache) { diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 837c62eb666ce0..408a0446ef5d5a 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -1647,14 +1647,44 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { await this.projectsService.setProjectConfiguration(projectId, { '.gitpod.yml': configString }); } + public async fetchRepositoryConfiguration(ctx: TraceContext, cloneUrl: string): Promise { + traceAPIParams(ctx, { cloneUrl }); + const user = this.checkUser("fetchRepositoryConfiguration"); + try { + return await this.projectsService.fetchRepositoryConfiguration(ctx, user, cloneUrl); + } catch (error) { + if (UnauthorizedError.is(error)) { + throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data); + } + throw error; + } + } + public async fetchProjectRepositoryConfiguration(ctx: TraceContext, projectId: string): Promise { traceAPIParams(ctx, { projectId }); - const user = this.checkUser("fetchProjectRepositoryConfiguration"); await this.guardProjectOperation(user, projectId, "get"); + + const project = await this.projectsService.getProject(projectId); + if (!project) { + throw new Error("Project not found"); + } + try { - return await this.projectsService.fetchProjectRepositoryConfiguration(ctx, user, projectId); + return await this.projectsService.fetchRepositoryConfiguration(ctx, user, project.cloneUrl); + } catch (error) { + if (UnauthorizedError.is(error)) { + throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data); + } + throw error; + } + } + + public async guessRepositoryConfiguration(ctx: TraceContext, cloneUrl: string): Promise { + const user = this.checkUser("guessRepositoryConfiguration"); + try { + return await this.projectsService.guessRepositoryConfiguration(ctx, user, cloneUrl); } catch (error) { if (UnauthorizedError.is(error)) { throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data); @@ -1665,12 +1695,16 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { public async guessProjectConfiguration(ctx: TraceContext, projectId: string): Promise { traceAPIParams(ctx, { projectId }); - const user = this.checkUser("guessProjectConfiguration"); - await this.guardProjectOperation(user, projectId, "get"); + + const project = await this.projectsService.getProject(projectId); + if (!project) { + throw new Error("Project not found"); + } + try { - return await this.projectsService.guessProjectConfiguration(ctx, user, projectId); + return await this.projectsService.guessRepositoryConfiguration(ctx, user, project.cloneUrl); } catch (error) { if (UnauthorizedError.is(error)) { throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);