Skip to content

Commit a33ac0c

Browse files
committed
allow creating from git clone url
1 parent 3d40f96 commit a33ac0c

File tree

4 files changed

+113
-21
lines changed

4 files changed

+113
-21
lines changed

components/dashboard/src/data/projects/create-project-mutation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useCurrentOrg } from "../organizations/orgs-query";
1010
import { useRefreshProjects } from "./list-projects-query";
1111
import { CreateProjectParams, Project } from "@gitpod/gitpod-protocol";
1212

13-
type CreateProjectArgs = Omit<CreateProjectParams, "teamId">;
13+
export type CreateProjectArgs = Omit<CreateProjectParams, "teamId">;
1414

1515
export const useCreateProject = () => {
1616
const refreshProjects = useRefreshProjects();
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 { FC, useCallback, useMemo } from "react";
8+
import isURL from "validator/lib/isURL";
9+
import { CreateProjectArgs } from "../../data/projects/create-project-mutation";
10+
import { Subheading } from "../../components/typography/headings";
11+
import { Button } from "../../components/Button";
12+
import { useToast } from "../../components/toasts/Toasts";
13+
14+
type Props = {
15+
repoSearchFilter: string;
16+
isCreating: boolean;
17+
onCreateProject: (args: CreateProjectArgs) => void;
18+
};
19+
export const NewProjectCreateFromURL: FC<Props> = ({ repoSearchFilter, isCreating, onCreateProject }) => {
20+
const { toast } = useToast();
21+
const showCreateFromURL = useMemo(() => {
22+
// TODO: Only accounts for https urls, need to account for ssh clone urls too?
23+
const looksLikeURL = isURL(repoSearchFilter, {
24+
require_protocol: true,
25+
protocols: ["https"],
26+
});
27+
28+
const hasTrailingGit = /\.git$/.test(repoSearchFilter);
29+
30+
return looksLikeURL && hasTrailingGit;
31+
}, [repoSearchFilter]);
32+
33+
const normalizedURL = useMemo(() => {
34+
let url = repoSearchFilter.toLowerCase().trim();
35+
36+
return url;
37+
}, [repoSearchFilter]);
38+
39+
const handleCreate = useCallback(() => {
40+
let name = "";
41+
let slug = "";
42+
43+
try {
44+
// try and parse the url for owner/repo path parts
45+
console.log("url: ", normalizedURL.substring(0, normalizedURL.length - 4));
46+
const [owner, repo] = new URL(normalizedURL.substring(0, normalizedURL.length - 4)).pathname.split("/");
47+
if (!owner && !repo) {
48+
throw new Error();
49+
}
50+
name = repo || owner;
51+
slug = repo || owner;
52+
} catch (e) {
53+
toast("Sorry, it looks like we can't handle that URL. Is it a valid git clone url?");
54+
return;
55+
}
56+
57+
onCreateProject({
58+
name,
59+
slug,
60+
cloneUrl: normalizedURL,
61+
appInstallationId: "",
62+
});
63+
}, [normalizedURL, onCreateProject, toast]);
64+
65+
if (!showCreateFromURL) {
66+
return null;
67+
}
68+
69+
return (
70+
<div className="flex flex-col items-center">
71+
<Subheading>Create project from Git clone url?</Subheading>
72+
73+
<pre className="my-2 font-mono text-sm">{normalizedURL}</pre>
74+
75+
<Button onClick={handleCreate} loading={isCreating}>
76+
Create Project
77+
</Button>
78+
</div>
79+
);
80+
};

components/dashboard/src/projects/new-project/NewProjectRepoList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type Props = {
1515
};
1616
export const NewProjectRepoList: FC<Props> = ({ filteredRepos, noReposAvailable, onRepoSelected }) => {
1717
return (
18-
<div className="p-6 flex-col">
18+
<>
1919
{filteredRepos.length > 0 && (
2020
<div className="overscroll-contain max-h-80 overflow-y-auto pr-2">
2121
{filteredRepos.map((r, index) => (
@@ -58,7 +58,7 @@ export const NewProjectRepoList: FC<Props> = ({ filteredRepos, noReposAvailable,
5858
</div>
5959
)}
6060
{!noReposAvailable && filteredRepos.length === 0 && <p className="text-center">No Results</p>}
61-
</div>
61+
</>
6262
);
6363
};
6464

components/dashboard/src/projects/new-project/NewProjectRepoSelection.tsx

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import { trackEvent } from "../../Analytics";
1313
import { NewProjectSearchInput } from "./NewProjectSearchInput";
1414
import { NewProjectAccountSelector } from "./NewProjectAccountSelector";
1515
import { NewProjectRepoList } from "./NewProjectRepoList";
16-
import { useCreateProject } from "../../data/projects/create-project-mutation";
16+
import { CreateProjectArgs, useCreateProject } from "../../data/projects/create-project-mutation";
1717
import { NewProjectAuthRequired } from "./NewProjectAuthRequired";
1818
import { useToast } from "../../components/toasts/Toasts";
1919
import { useProviderRepositoriesForUser } from "../../data/git-providers/provider-repositories-query";
2020
import { NewProjectSubheading } from "./NewProjectSubheading";
2121
import { openReconfigureWindow } from "./reconfigure-github";
22+
import { NewProjectCreateFromURL } from "./NewProjectCreateFromURL";
2223

2324
type Props = {
2425
selectedProviderHost?: string;
@@ -90,29 +91,33 @@ export const NewProjectRepoSelection: FC<Props> = ({ selectedProviderHost, onPro
9091
});
9192
}, [selectedAccount]);
9293

93-
// Creates the project
94-
const handleRepoSelected = useCallback(
95-
(repo) => {
96-
createProject.mutate(
97-
{
98-
name: repo.name,
99-
cloneUrl: repo.cloneUrl,
100-
slug: repo.path || repo.name,
101-
appInstallationId: String(repo.installationId),
94+
const onCreateProject = useCallback(
95+
(args: CreateProjectArgs) => {
96+
createProject.mutate(args, {
97+
onSuccess: (project) => {
98+
onProjectCreated(project);
10299
},
103-
{
104-
onSuccess: (project) => {
105-
onProjectCreated(project);
106-
},
107-
onError: (error) => {
108-
toast(error?.message ?? "Failed to create new project.");
109-
},
100+
onError: (error) => {
101+
toast(error?.message ?? "Failed to create new project.");
110102
},
111-
);
103+
});
112104
},
113105
[createProject, onProjectCreated, toast],
114106
);
115107

108+
// Creates the project
109+
const handleRepoSelected = useCallback(
110+
(repo) => {
111+
onCreateProject({
112+
name: repo.name,
113+
cloneUrl: repo.cloneUrl,
114+
slug: repo.path || repo.name,
115+
appInstallationId: String(repo.installationId),
116+
});
117+
},
118+
[onCreateProject],
119+
);
120+
116121
// Adjusts selectedAccount when repos change
117122
useEffect(() => {
118123
if (reposInAccounts?.length === 0) {
@@ -180,6 +185,13 @@ export const NewProjectRepoSelection: FC<Props> = ({ selectedProviderHost, onPro
180185
</div>
181186
</div>
182187
)}
188+
{(filteredRepos?.length ?? 0) === 0 && repoSearchFilter.length > 0 && (
189+
<NewProjectCreateFromURL
190+
repoSearchFilter={repoSearchFilter}
191+
isCreating={createProject.isLoading}
192+
onCreateProject={onCreateProject}
193+
/>
194+
)}
183195
</>
184196
);
185197
};

0 commit comments

Comments
 (0)