Skip to content

Commit

Permalink
[projects] remove configuration page from wizard
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
AlexTugarev and jankeromnes committed Dec 7, 2021
1 parent 537672b commit bce4aa2
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 32 deletions.
97 changes: 80 additions & 17 deletions components/dashboard/src/projects/NewProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);

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

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

const [project, setProject] = useState<Project | undefined>();
const [guessedConfigString, setGuessedConfigString] = useState<string | undefined>();
const [sourceOfConfig, setSourceOfConfig] = useState<"repo" | "db" | undefined>();

useEffect(() => {
if (user && provider === undefined) {
if (user.identities.find(i => i.authProviderId === "Public-GitLab")) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -294,7 +328,7 @@ export default function NewProject() {
<div className="flex justify-end">
<div className="h-full my-auto flex self-center opacity-0 group-hover:opacity-100">
{!r.inUse ? (
<button className="primary" onClick={() => setSelectedRepo(r.path || r.name)}>Select</button>
<button className="primary" onClick={() => setSelectedRepo(r)}>Select</button>
) : (
<p className="my-auto">already taken</p>
)}
Expand Down Expand Up @@ -428,14 +462,43 @@ export default function NewProject() {
</div>);
}

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 (<div className="flex flex-col w-96 mt-24 mx-auto items-center">
<h1>New Project</h1>

{!selectedRepo && renderSelectRepository()}
{!project
? (<>
<h1>New Project</h1>

{!selectedRepo && renderSelectRepository()}

{selectedRepo && !selectedTeamOrUser && renderSelectTeam()}

{selectedRepo && selectedTeamOrUser && (<div></div>)}
</>)
: (<>
<h1>Project created</h1>
<p className="text-gray-500 text-center text-base"><a href={`${User.is(selectedTeamOrUser) ? "" : `/t/${selectedTeamOrUser?.slug}}/${project.slug}`}`} className="gp-link">Navigate</a> to the details of <strong>{project.name}</strong>.</p>

<div className="mt-6">
<button onClick={onNewWorkspace}>New Workspace</button>
</div>
<div className="mt-4">
{sourceOfConfig === "db" && (<p>Start with a configuration example.</p>)}
{sourceOfConfig === "repo" && (<p>Start with the configuration from git.</p>)}
</div>

{selectedRepo && !selectedTeamOrUser && renderSelectTeam()}
</>)}

{selectedRepo && selectedTeamOrUser && (<div></div>)}

{isBitbucket() && renderBitbucketWarning()}

Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
setProjectConfiguration(projectId: string, configString: string): Promise<void>;
fetchProjectRepositoryConfiguration(projectId: string): Promise<string | undefined>;
guessProjectConfiguration(projectId: string): Promise<string | undefined>;
fetchRepositoryConfiguration(cloneUrl: string): Promise<string | undefined>;
guessRepositoryConfiguration(cloneUrl: string): Promise<string | undefined>;

// content service
getContentBlobUploadUrl(name: string): Promise<string>
Expand Down
2 changes: 2 additions & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
16 changes: 6 additions & 10 deletions components/server/src/projects/projects-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -198,17 +194,17 @@ export class ProjectsService {
return { fileProvider, commitContext };
}

async fetchProjectRepositoryConfiguration(ctx: TraceContext, user: User, projectId: string): Promise<string | undefined> {
const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, projectId);
async fetchRepositoryConfiguration(ctx: TraceContext, user: User, cloneUrl: string): Promise<string | undefined> {
const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, cloneUrl);
const configString = await fileProvider.getGitpodFileContent(commitContext, user);
return configString;
}

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

async guessProjectConfiguration(ctx: TraceContext, user: User, projectId: string): Promise<string | undefined> {
const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, projectId);
async guessRepositoryConfiguration(ctx: TraceContext, user: User, cloneUrl: string): Promise<string | undefined> {
const { fileProvider, commitContext } = await this.getRepositoryFileProviderAndCommitContext(ctx, user, cloneUrl);
const cache: { [path: string]: Promise<string | undefined> } = {};
const readFile = async (path: string) => {
if (path in cache) {
Expand Down
44 changes: 39 additions & 5 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined> {
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<string | undefined> {
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<string | undefined> {
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);
Expand All @@ -1665,12 +1695,16 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {

public async guessProjectConfiguration(ctx: TraceContext, projectId: string): Promise<string | undefined> {
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);
Expand Down

0 comments on commit bce4aa2

Please sign in to comment.