Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[projects] remove configuration page from wizard #7102

Merged
merged 1 commit into from
Dec 8, 2021
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
109 changes: 89 additions & 20 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:
AlexTugarev marked this conversation as resolved.
Show resolved Hide resolved
- init: |
echo 'TODO: build project'
command: |
echo 'TODO: start app'`);
Comment on lines +99 to +104
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there can be a race here -- if await guessedConfigStringPromise takes too long, you might call server.setProjectConfiguration(project.id, undefined) on line 154.

Safer the other way around:

Suggested change
setSourceOfConfig("db");
setGuessedConfigString(await guessedConfigStringPromise || `tasks:
- init: |
echo 'TODO: build project'
command: |
echo 'TODO: start app'`);
setGuessedConfigString(await guessedConfigStringPromise || `tasks:
- init: |
echo 'TODO: build project'
command: |
echo 'TODO: start app'`);
setSourceOfConfig("db");

}
} 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,18 +462,53 @@ export default function NewProject() {
</div>);
}

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

if (!project) {
return (<div className="flex flex-col w-96 mt-24 mx-auto items-center">

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

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

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

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

{isBitbucket() && renderBitbucketWarning()}

</div>);
} else {
const projectLink = User.is(selectedTeamOrUser) ? `/projects/${project.slug}` : `/t/${selectedTeamOrUser?.slug}/${project.slug}`;
const location = User.is(selectedTeamOrUser) ? "" : (<> in team <a className="gp-link" href={`/t/${selectedTeamOrUser?.slug}/projects`}>{selectedTeamOrUser?.name}</a></>);

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

</div>);
<>
<h1>Project Created</h1>

<p className="mt-2 text-gray-500 text-center text-base">Created <a className="gp-link" href={projectLink}>{project.name}</a> {location}
</p>

<div className="mt-12">
<button onClick={onNewWorkspace}>New Workspace</button>
</div>

</>

</div>);
}

}

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