Skip to content

Commit a481e8b

Browse files
AlexTugarevjankeromnes
authored andcommitted
[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 <[email protected]> Co-authored-by: Alex Tugarev <[email protected]>
1 parent 34e4dcb commit a481e8b

File tree

5 files changed

+138
-35
lines changed

5 files changed

+138
-35
lines changed

components/dashboard/src/projects/NewProject.tsx

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import { useContext, useEffect, useState } from "react";
88
import { getGitpodService, gitpodHostUrl } from "../service/service";
99
import { iconForAuthProvider, openAuthorizeWindow, simplifyProviderName } from "../provider-utils";
10-
import { AuthProviderInfo, ProviderRepository, Team, TeamMemberInfo, User } from "@gitpod/gitpod-protocol";
10+
import { AuthProviderInfo, Project, ProviderRepository, Team, TeamMemberInfo, User } from "@gitpod/gitpod-protocol";
1111
import { TeamsContext } from "../teams/teams-context";
12-
import { useHistory, useLocation } from "react-router";
12+
import { useLocation } from "react-router";
1313
import ContextMenu, { ContextMenuEntry } from "../components/ContextMenu";
1414
import CaretDown from "../icons/CaretDown.svg";
1515
import Plus from "../icons/Plus.svg";
@@ -23,7 +23,6 @@ import exclamation from "../images/exclamation.svg";
2323

2424
export default function NewProject() {
2525
const location = useLocation();
26-
const history = useHistory();
2726
const { teams } = useContext(TeamsContext);
2827
const { user, setUser } = useContext(UserContext);
2928

@@ -33,12 +32,16 @@ export default function NewProject() {
3332
const [selectedAccount, setSelectedAccount] = useState<string | undefined>(undefined);
3433
const [noOrgs, setNoOrgs] = useState<boolean>(false);
3534
const [showGitProviders, setShowGitProviders] = useState<boolean>(false);
36-
const [selectedRepo, setSelectedRepo] = useState<string | undefined>(undefined);
35+
const [selectedRepo, setSelectedRepo] = useState<ProviderRepository | undefined>(undefined);
3736
const [selectedTeamOrUser, setSelectedTeamOrUser] = useState<Team | User | undefined>(undefined);
3837

3938
const [showNewTeam, setShowNewTeam] = useState<boolean>(false);
4039
const [loaded, setLoaded] = useState<boolean>(false);
4140

41+
const [project, setProject] = useState<Project | undefined>();
42+
const [guessedConfigString, setGuessedConfigString] = useState<string | undefined>();
43+
const [sourceOfConfig, setSourceOfConfig] = useState<"repo" | "db" | undefined>();
44+
4245
useEffect(() => {
4346
if (user && provider === undefined) {
4447
if (user.identities.find(i => i.authProviderId === "Public-GitLab")) {
@@ -83,6 +86,32 @@ export default function NewProject() {
8386
})();
8487
}, [teams]);
8588

89+
useEffect(() => {
90+
if (selectedRepo) {
91+
(async () => {
92+
93+
try {
94+
const guessedConfigStringPromise = getGitpodService().server.guessRepositoryConfiguration(selectedRepo.cloneUrl);
95+
const repoConfigString = await getGitpodService().server.fetchRepositoryConfiguration(selectedRepo.cloneUrl);
96+
if (repoConfigString) {
97+
setSourceOfConfig("repo");
98+
} else {
99+
setSourceOfConfig("db");
100+
setGuessedConfigString(await guessedConfigStringPromise || `tasks:
101+
- init: |
102+
echo 'TODO: build project'
103+
command: |
104+
echo 'TODO: start app'`);
105+
}
106+
} catch (error) {
107+
console.error('Getting project configuration failed', error);
108+
setSourceOfConfig(undefined);
109+
}
110+
111+
})();
112+
}
113+
}, [selectedRepo]);
114+
86115
useEffect(() => {
87116
if (selectedTeamOrUser && selectedRepo) {
88117
createProject(selectedTeamOrUser, selectedRepo);
@@ -118,6 +147,17 @@ export default function NewProject() {
118147
})();
119148
}, [provider]);
120149

150+
useEffect(() => {
151+
if (project && sourceOfConfig) {
152+
(async () => {
153+
if (guessedConfigString && sourceOfConfig === "db") {
154+
await getGitpodService().server.setProjectConfiguration(project.id, guessedConfigString);
155+
}
156+
await getGitpodService().server.triggerPrebuild(project.id, null);
157+
})();
158+
}
159+
}, [project, sourceOfConfig]);
160+
121161
const isGitHub = () => provider === "github.com";
122162
const isBitbucket = () => provider === "bitbucket.org";
123163

@@ -180,16 +220,10 @@ export default function NewProject() {
180220
}
181221
}
182222

183-
const createProject = async (teamOrUser: Team | User, selectedRepo: string) => {
223+
const createProject = async (teamOrUser: Team | User, repo: ProviderRepository) => {
184224
if (!provider || isBitbucket()) {
185225
return;
186226
}
187-
const repo = reposInAccounts.find(r => r.account === selectedAccount && (r.path ? r.path === selectedRepo : r.name === selectedRepo));
188-
if (!repo) {
189-
console.error("No repo selected!")
190-
return;
191-
}
192-
193227
const repoSlug = repo.path || repo.name;
194228

195229
try {
@@ -203,7 +237,7 @@ export default function NewProject() {
203237
appInstallationId: String(repo.installationId),
204238
});
205239

206-
history.push(`/${User.is(teamOrUser) ? 'projects' : 't/'+teamOrUser.slug}/${project.slug}/configure`);
240+
setProject(project);
207241
} catch (error) {
208242
const message = (error && error?.message) || "Failed to create new project."
209243
window.alert(message);
@@ -294,7 +328,7 @@ export default function NewProject() {
294328
<div className="flex justify-end">
295329
<div className="h-full my-auto flex self-center opacity-0 group-hover:opacity-100">
296330
{!r.inUse ? (
297-
<button className="primary" onClick={() => setSelectedRepo(r.path || r.name)}>Select</button>
331+
<button className="primary" onClick={() => setSelectedRepo(r)}>Select</button>
298332
) : (
299333
<p className="my-auto">already taken</p>
300334
)}
@@ -428,18 +462,53 @@ export default function NewProject() {
428462
</div>);
429463
}
430464

431-
return (<div className="flex flex-col w-96 mt-24 mx-auto items-center">
432-
<h1>New Project</h1>
465+
const onNewWorkspace = async () => {
466+
const redirectToNewWorkspace = () => {
467+
// instead of `history.push` we want forcibly to redirect here in order to avoid a following redirect from `/` -> `/projects` (cf. App.tsx)
468+
const url = new URL(window.location.toString());
469+
url.pathname = "/";
470+
url.hash = project?.cloneUrl!;
471+
window.location.href = url.toString();
472+
}
473+
redirectToNewWorkspace();
474+
}
475+
476+
if (!project) {
477+
return (<div className="flex flex-col w-96 mt-24 mx-auto items-center">
433478

434-
{!selectedRepo && renderSelectRepository()}
479+
<>
480+
<h1>New Project</h1>
435481

436-
{selectedRepo && !selectedTeamOrUser && renderSelectTeam()}
482+
{!selectedRepo && renderSelectRepository()}
437483

438-
{selectedRepo && selectedTeamOrUser && (<div></div>)}
484+
{selectedRepo && !selectedTeamOrUser && renderSelectTeam()}
485+
486+
{selectedRepo && selectedTeamOrUser && (<div></div>)}
487+
</>
488+
489+
{isBitbucket() && renderBitbucketWarning()}
490+
491+
</div>);
492+
} else {
493+
const projectLink = User.is(selectedTeamOrUser) ? `/projects/${project.slug}` : `/t/${selectedTeamOrUser?.slug}/${project.slug}`;
494+
const location = User.is(selectedTeamOrUser) ? "" : (<> in team <a className="gp-link" href={`/t/${selectedTeamOrUser?.slug}/projects`}>{selectedTeamOrUser?.name}</a></>);
439495

440-
{isBitbucket() && renderBitbucketWarning()}
496+
return (<div className="flex flex-col w-96 mt-24 mx-auto items-center">
441497

442-
</div>);
498+
<>
499+
<h1>Project Created</h1>
500+
501+
<p className="mt-2 text-gray-500 text-center text-base">Created <a className="gp-link" href={projectLink}>{project.name}</a> {location}
502+
</p>
503+
504+
<div className="mt-12">
505+
<button onClick={onNewWorkspace}>New Workspace</button>
506+
</div>
507+
508+
</>
509+
510+
</div>);
511+
}
443512

444513
}
445514

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
139139
setProjectConfiguration(projectId: string, configString: string): Promise<void>;
140140
fetchProjectRepositoryConfiguration(projectId: string): Promise<string | undefined>;
141141
guessProjectConfiguration(projectId: string): Promise<string | undefined>;
142+
fetchRepositoryConfiguration(cloneUrl: string): Promise<string | undefined>;
143+
guessRepositoryConfiguration(cloneUrl: string): Promise<string | undefined>;
142144

143145
// content service
144146
getContentBlobUploadUrl(name: string): Promise<string>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
101101
"setProjectConfiguration": { group: "default", points: 1 },
102102
"fetchProjectRepositoryConfiguration": { group: "default", points: 1 },
103103
"guessProjectConfiguration": { group: "default", points: 1 },
104+
"fetchRepositoryConfiguration": { group: "default", points: 1 },
105+
"guessRepositoryConfiguration": { group: "default", points: 1 },
104106
"getContentBlobUploadUrl": { group: "default", points: 1 },
105107
"getContentBlobDownloadUrl": { group: "default", points: 1 },
106108
"getGitpodTokens": { group: "default", points: 1 },

components/server/src/projects/projects-service.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,8 @@ export class ProjectsService {
182182
return this.projectDB.setProjectConfiguration(projectId, config);
183183
}
184184

185-
protected async getRepositoryFileProviderAndCommitContext(ctx: TraceContext, user: User, projectId: string): Promise<{fileProvider: FileProvider, commitContext: CommitContext}> {
186-
const project = await this.getProject(projectId);
187-
if (!project) {
188-
throw new Error("Project not found");
189-
}
190-
const normalizedContextUrl = this.contextParser.normalizeContextURL(project.cloneUrl);
185+
protected async getRepositoryFileProviderAndCommitContext(ctx: TraceContext, user: User, cloneUrl: string): Promise<{fileProvider: FileProvider, commitContext: CommitContext}> {
186+
const normalizedContextUrl = this.contextParser.normalizeContextURL(cloneUrl);
191187
const commitContext = (await this.contextParser.handle(ctx, user, normalizedContextUrl)) as CommitContext;
192188
const { host } = commitContext.repository;
193189
const hostContext = this.hostContextProvider.get(host);
@@ -198,17 +194,17 @@ export class ProjectsService {
198194
return { fileProvider, commitContext };
199195
}
200196

201-
async fetchProjectRepositoryConfiguration(ctx: TraceContext, user: User, projectId: string): Promise<string | undefined> {
202-
const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, projectId);
197+
async fetchRepositoryConfiguration(ctx: TraceContext, user: User, cloneUrl: string): Promise<string | undefined> {
198+
const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, cloneUrl);
203199
const configString = await fileProvider.getGitpodFileContent(commitContext, user);
204200
return configString;
205201
}
206202

207203
// a static cache used to prefetch inferrer related files in parallel in advance
208204
private requestedPaths = new Set<string>();
209205

210-
async guessProjectConfiguration(ctx: TraceContext, user: User, projectId: string): Promise<string | undefined> {
211-
const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, projectId);
206+
async guessRepositoryConfiguration(ctx: TraceContext, user: User, cloneUrl: string): Promise<string | undefined> {
207+
const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, cloneUrl);
212208
const cache: { [path: string]: Promise<string | undefined> } = {};
213209
const readFile = async (path: string) => {
214210
if (path in cache) {

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

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,14 +1647,44 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
16471647
await this.projectsService.setProjectConfiguration(projectId, { '.gitpod.yml': configString });
16481648
}
16491649

1650+
public async fetchRepositoryConfiguration(ctx: TraceContext, cloneUrl: string): Promise<string | undefined> {
1651+
traceAPIParams(ctx, { cloneUrl });
1652+
const user = this.checkUser("fetchRepositoryConfiguration");
1653+
try {
1654+
return await this.projectsService.fetchRepositoryConfiguration(ctx, user, cloneUrl);
1655+
} catch (error) {
1656+
if (UnauthorizedError.is(error)) {
1657+
throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);
1658+
}
1659+
throw error;
1660+
}
1661+
}
1662+
16501663
public async fetchProjectRepositoryConfiguration(ctx: TraceContext, projectId: string): Promise<string | undefined> {
16511664
traceAPIParams(ctx, { projectId });
1652-
16531665
const user = this.checkUser("fetchProjectRepositoryConfiguration");
16541666

16551667
await this.guardProjectOperation(user, projectId, "get");
1668+
1669+
const project = await this.projectsService.getProject(projectId);
1670+
if (!project) {
1671+
throw new Error("Project not found");
1672+
}
1673+
16561674
try {
1657-
return await this.projectsService.fetchProjectRepositoryConfiguration(ctx, user, projectId);
1675+
return await this.projectsService.fetchRepositoryConfiguration(ctx, user, project.cloneUrl);
1676+
} catch (error) {
1677+
if (UnauthorizedError.is(error)) {
1678+
throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);
1679+
}
1680+
throw error;
1681+
}
1682+
}
1683+
1684+
public async guessRepositoryConfiguration(ctx: TraceContext, cloneUrl: string): Promise<string | undefined> {
1685+
const user = this.checkUser("guessRepositoryConfiguration");
1686+
try {
1687+
return await this.projectsService.guessRepositoryConfiguration(ctx, user, cloneUrl);
16581688
} catch (error) {
16591689
if (UnauthorizedError.is(error)) {
16601690
throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);
@@ -1665,12 +1695,16 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
16651695

16661696
public async guessProjectConfiguration(ctx: TraceContext, projectId: string): Promise<string | undefined> {
16671697
traceAPIParams(ctx, { projectId });
1668-
16691698
const user = this.checkUser("guessProjectConfiguration");
1670-
16711699
await this.guardProjectOperation(user, projectId, "get");
1700+
1701+
const project = await this.projectsService.getProject(projectId);
1702+
if (!project) {
1703+
throw new Error("Project not found");
1704+
}
1705+
16721706
try {
1673-
return await this.projectsService.guessProjectConfiguration(ctx, user, projectId);
1707+
return await this.projectsService.guessRepositoryConfiguration(ctx, user, project.cloneUrl);
16741708
} catch (error) {
16751709
if (UnauthorizedError.is(error)) {
16761710
throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);

0 commit comments

Comments
 (0)