Skip to content

Commit 98080ef

Browse files
committed
[dashboard] Allow Projects for Individuals
1 parent b09ccef commit 98080ef

25 files changed

+312
-172
lines changed

Diff for: components/dashboard/src/App.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ function App() {
192192
<p className="mt-4 text-lg text-gitpod-red">{decodeURIComponent(getURLHash())}</p>
193193
</div>
194194
</Route>
195+
<Route path="/projects">
196+
<Route exact path="/projects" component={Projects} />
197+
<Route exact path="/projects/:projectName/:resourceOrPrebuild?" render={(props) => {
198+
const { resourceOrPrebuild } = props.match.params;
199+
if (resourceOrPrebuild === "configure") {
200+
return <ConfigureProject />;
201+
}
202+
if (resourceOrPrebuild === "prebuilds") {
203+
return <Prebuilds />;
204+
}
205+
return resourceOrPrebuild ? <Prebuild /> : <Project />;
206+
}} />
207+
</Route>
195208
<Route path="/teams">
196209
<Route exact path="/teams" component={Teams} />
197210
<Route exact path="/teams/new" component={NewTeam} />

Diff for: components/dashboard/src/Menu.tsx

+39-29
Original file line numberDiff line numberDiff line change
@@ -77,46 +77,56 @@ export default function Menu() {
7777
})();
7878
}, [ teams ]);
7979

80+
const teamOrUserSlug = !!team ? team.slug : 'projects';
8081
const leftMenu: Entry[] = (() => {
81-
if (!team) {
82+
// Project menu
83+
if (projectName) {
8284
return [
8385
{
84-
title: 'Workspaces',
85-
link: '/workspaces',
86-
alternatives: ['/']
86+
title: 'Overview',
87+
link: `/${teamOrUserSlug}/${projectName}`
8788
},
8889
{
89-
title: 'Settings',
90-
link: '/settings',
91-
alternatives: settingsMenu.flatMap(e => e.link)
90+
title: 'Prebuilds',
91+
link: `/${teamOrUserSlug}/${projectName}/prebuilds`
92+
},
93+
{
94+
title: 'Configure',
95+
link: `/${teamOrUserSlug}/${projectName}/configure`
9296
}
9397
];
9498
}
95-
return projectName ? [
96-
{
97-
title: 'Overview',
98-
link: `/${team.slug}/${projectName}`,
99-
alternatives: [`/${team.slug}`]
100-
},
101-
{
102-
title: 'Prebuilds',
103-
link: `/${team.slug}/${projectName}/prebuilds`
104-
},
105-
{
106-
title: 'Configure',
107-
link: `/${team.slug}/${projectName}/configure`
108-
}
109-
] : [
110-
{
99+
// Team menu
100+
if (team) {
101+
return [
102+
{
103+
title: 'Projects',
104+
link: `/${team.slug}/projects`,
105+
alternatives: [`/${team.slug}`]
106+
},
107+
{
108+
title: 'Members',
109+
link: `/${team.slug}/members`
110+
}
111+
];
112+
}
113+
// User menu
114+
return [
115+
...(showTeamsUI ? [{
111116
title: 'Projects',
112-
link: `/${team.slug}/projects`,
113-
alternatives: [`/${team.slug}`]
117+
link: '/projects'
118+
}] : []),
119+
{
120+
title: 'Workspaces',
121+
link: '/workspaces',
122+
alternatives: ['/']
114123
},
115124
{
116-
title: 'Members',
117-
link: `/${team.slug}/members`
125+
title: 'Settings',
126+
link: '/settings',
127+
alternatives: settingsMenu.flatMap(e => e.link)
118128
}
119-
]
129+
];
120130
})();
121131
const rightMenu: Entry[] = [
122132
...(user?.rolesOrPermissions?.includes('admin') ? [{
@@ -181,7 +191,7 @@ export default function Menu() {
181191
</div>
182192
{ projectName && (
183193
<div className="flex h-full rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 px-2 py-1">
184-
<Link to={`/${team?.slug}/${projectName}${prebuildId ? "/prebuilds" : ""}`}>
194+
<Link to={`/${teamOrUserSlug}/${projectName}${prebuildId ? "/prebuilds" : ""}`}>
185195
<span className="text-base text-gray-600 dark:text-gray-400 font-semibold">{projectName}</span>
186196
</Link>
187197
</div>

Diff for: components/dashboard/src/components/Header.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7+
import { useEffect } from "react";
78
import Separator from "./Separator";
89

910
export interface HeaderProps {
@@ -12,6 +13,12 @@ export interface HeaderProps {
1213
}
1314

1415
export default function Header(p: HeaderProps) {
16+
useEffect(() => {
17+
if (typeof p.title !== "string") {
18+
return;
19+
}
20+
document.title = `${p.title} — Gitpod`;
21+
}, []);
1522
return <div className="lg:px-28 px-10 border-gray-200 dark:border-gray-800">
1623
<div className="flex pb-8 pt-6">
1724
<div className="">

Diff for: components/dashboard/src/components/PageWithSubMenu.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import { useEffect } from "react";
87
import { useLocation } from "react-router";
98
import { Link } from "react-router-dom";
109
import Header from '../components/Header';
@@ -21,7 +20,6 @@ export interface PageWithSubMenuProps {
2120

2221
export function PageWithSubMenu(p: PageWithSubMenuProps) {
2322
const location = useLocation();
24-
useEffect(() => { document.title = `${p.title} — Gitpod` }, []);
2523
return <div className="w-full">
2624
<Header title={p.title} subtitle={p.subtitle} />
2725
<div className='lg:px-28 px-10 flex pt-9'>

Diff for: components/dashboard/src/projects/ConfigureProject.tsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,16 @@ export default function () {
6262
const [ workspaceCreationResult, setWorkspaceCreationResult ] = useState<WorkspaceCreationResult | undefined>();
6363

6464
useEffect(() => {
65-
if (!team) {
66-
return;
67-
}
6865
// Disable editing while loading, or when the config comes from Git.
6966
setIsEditorDisabled(true);
7067
setEditorError(null);
68+
if (!teams) {
69+
return;
70+
}
7171
(async () => {
72-
const projects = await getGitpodService().server.getProjects(team.id);
72+
const projects = (!!team
73+
? await getGitpodService().server.getTeamProjects(team.id)
74+
: await getGitpodService().server.getUserProjects());
7375
const project = projects.find(p => p.name === routeMatch?.params.projectSlug);
7476
if (project) {
7577
setProject(project);
@@ -84,7 +86,7 @@ export default function () {
8486
}
8587
}
8688
})();
87-
}, [ team ]);
89+
}, [ teams, team ]);
8890

8991
const buildProject = async (event: React.MouseEvent) => {
9092
if (!project) {
@@ -107,6 +109,8 @@ export default function () {
107109
}
108110
}
109111

112+
useEffect(() => { document.title = 'Configure Project — Gitpod' }, []);
113+
110114
return <div className="flex flex-col mt-24 mx-auto items-center">
111115
<h1>Configure Project</h1>
112116
<p className="text-gray-500 text-center text-base">Fully-automate your project's dev setup. <a className="learn-more" href="https://www.gitpod.io/docs/references/gitpod-yml">Learn more</a></p>

Diff for: components/dashboard/src/projects/NewProject.tsx

+24-15
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
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 } from "@gitpod/gitpod-protocol";
10+
import { AuthProviderInfo, ProviderRepository, Team, User } from "@gitpod/gitpod-protocol";
1111
import { TeamsContext } from "../teams/teams-context";
1212
import { useHistory, useLocation } from "react-router";
1313
import ContextMenu, { ContextMenuEntry } from "../components/ContextMenu";
@@ -16,13 +16,13 @@ import Plus from "../icons/Plus.svg";
1616
import Switch from "../icons/Switch.svg";
1717
import search from "../icons/search.svg";
1818
import moment from "moment";
19+
import { UserContext } from "../user-context";
1920

2021
export default function NewProject() {
21-
2222
const location = useLocation();
2323
const history = useHistory();
24-
2524
const { teams } = useContext(TeamsContext);
25+
const { user } = useContext(UserContext);
2626

2727
const [provider, setProvider] = useState<string>("github.com");
2828
const [reposInAccounts, setReposInAccounts] = useState<ProviderRepository[]>([]);
@@ -31,7 +31,7 @@ export default function NewProject() {
3131
const [noOrgs, setNoOrgs] = useState<boolean>(false);
3232
const [showGitProviders, setShowGitProviders] = useState<boolean>(false);
3333
const [selectedRepo, setSelectedRepo] = useState<string | undefined>(undefined);
34-
const [selectedTeam, setSelectedTeam] = useState<Team | undefined>(undefined);
34+
const [selectedTeamOrUser, setSelectedTeamOrUser] = useState<Team | User | undefined>(undefined);
3535

3636
const [showNewTeam, setShowNewTeam] = useState<boolean>(false);
3737
const [loaded, setLoaded] = useState<boolean>(false);
@@ -42,7 +42,7 @@ export default function NewProject() {
4242
if (teamParam) {
4343
window.history.replaceState({}, '', window.location.pathname);
4444
const team = teams?.find(t => t.slug === teamParam);
45-
setSelectedTeam(team);
45+
setSelectedTeamOrUser(team);
4646
}
4747

4848
(async () => {
@@ -57,10 +57,10 @@ export default function NewProject() {
5757
}, []);
5858

5959
useEffect(() => {
60-
if (selectedTeam && selectedRepo) {
61-
createProject(selectedTeam, selectedRepo);
60+
if (selectedTeamOrUser && selectedRepo) {
61+
createProject(selectedTeamOrUser, selectedRepo);
6262
}
63-
}, [selectedRepo, selectedTeam]);
63+
}, [selectedTeamOrUser, selectedRepo]);
6464

6565
useEffect(() => {
6666
if (reposInAccounts.length === 0) {
@@ -128,7 +128,7 @@ export default function NewProject() {
128128
}
129129
}
130130

131-
const createProject = async (team: Team, selectedRepo: string) => {
131+
const createProject = async (teamOrUser: Team | User, selectedRepo: string) => {
132132
const repo = reposInAccounts.find(r => r.account === selectedAccount && r.name === selectedRepo);
133133
if (!repo) {
134134
console.error("No repo selected!")
@@ -140,11 +140,11 @@ export default function NewProject() {
140140
cloneUrl: repo.cloneUrl,
141141
account: repo.account,
142142
provider,
143+
...(User.is(teamOrUser) ? { userId: teamOrUser.id } : { teamId: teamOrUser.id }),
143144
appInstallationId: String(repo.installationId),
144-
teamId: team.id
145145
});
146146

147-
history.push(`/${team.slug}/${repo.name}/configure`);
147+
history.push(`/${User.is(teamOrUser) ? 'projects' : teamOrUser.slug}/${repo.name}/configure`);
148148
}
149149

150150
const toSimpleName = (fullName: string) => {
@@ -279,18 +279,27 @@ export default function NewProject() {
279279
};
280280

281281
const renderSelectTeam = () => {
282+
const userFullName = user?.fullName || user?.name || '...';
282283
const teamsToRender = teams || [];
283284
return (<>
284285
<h3 className="pb-2 mt-8">Select Team</h3>
285286
<h4 className="pb-2">Adding <strong>{selectedRepo}</strong></h4>
286287

287288
<div className="mt-8 border rounded-xl border-gray-100 flex-col" >
289+
<div key={`user-${userFullName}`} className={`w-96 border-b px-8 py-4 flex space-x-2 justify-between dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light transition ease-in-out group`}>
290+
<div className="w-8/12 m-auto overflow-ellipsis truncate">{userFullName}</div>
291+
<div className="w-4/12 flex justify-end">
292+
<div className="flex self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer opacity-0 group-hover:opacity-100">
293+
<button className="primary py-1" onClick={() => setSelectedTeamOrUser(user)}>Select</button>
294+
</div>
295+
</div>
296+
</div>
288297
{teamsToRender.map((t) => (
289298
<div key={`team-${t.name}`} className={`w-96 border-b px-8 py-4 flex space-x-2 justify-between dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light transition ease-in-out group`}>
290299
<div className="w-8/12 m-auto overflow-ellipsis truncate">{t.name}</div>
291300
<div className="w-4/12 flex justify-end">
292301
<div className="flex self-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md cursor-pointer opacity-0 group-hover:opacity-100">
293-
<button className="primary py-1" onClick={() => setSelectedTeam(t)}>Select</button>
302+
<button className="primary py-1" onClick={() => setSelectedTeamOrUser(t)}>Select</button>
294303
</div>
295304
</div>
296305
</div>
@@ -304,7 +313,7 @@ export default function NewProject() {
304313
</div>
305314
</div>
306315
{(showNewTeam || teamsToRender.length === 0) && (
307-
<NewTeam className="w-96 px-8 pb-8" onSuccess={(t) => setSelectedTeam(t)} />
316+
<NewTeam className="w-96 px-8 pb-8" onSuccess={(t) => setSelectedTeamOrUser(t)} />
308317
)}
309318
</div>
310319
</>)
@@ -316,9 +325,9 @@ export default function NewProject() {
316325

317326
{!selectedRepo && renderSelectRepository()}
318327

319-
{selectedRepo && !selectedTeam && renderSelectTeam()}
328+
{selectedRepo && !selectedTeamOrUser && renderSelectTeam()}
320329

321-
{selectedRepo && selectedTeam && (<div></div>)}
330+
{selectedRepo && selectedTeamOrUser && (<div></div>)}
322331

323332
</div>);
324333

Diff for: components/dashboard/src/projects/Prebuild.tsx

+14-6
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,28 @@ export default function () {
2323
const prebuildId = match?.params?.prebuildId;
2424
const team = getCurrentTeam(location, teams);
2525

26-
const [prebuild, setPrebuild] = useState<PrebuildInfo | undefined>();
26+
const [ prebuild, setPrebuild ] = useState<PrebuildInfo | undefined>();
2727

2828
useEffect(() => {
29-
if (!team || !projectName || !prebuildId) {
29+
if (!teams || !projectName || !prebuildId) {
3030
return;
3131
}
3232
(async () => {
33+
const projects = (!!team
34+
? await getGitpodService().server.getTeamProjects(team.id)
35+
: await getGitpodService().server.getUserProjects());
36+
const project = projects.find(p => p.name === projectName);
37+
if (!project) {
38+
console.error(new Error(`Project not found! (teamId: ${team?.id}, projectName: ${projectName})`));
39+
return;
40+
}
3341
const prebuilds = await getGitpodService().server.findPrebuilds({
34-
projectName,
35-
teamId: team.id,
42+
projectId: project.id,
3643
prebuildId
3744
});
3845
setPrebuild(prebuilds[0]);
3946
})();
40-
}, [team]);
41-
47+
}, [ teams, team ]);
4248

4349
const renderTitle = () => {
4450
if (!prebuild) {
@@ -70,6 +76,8 @@ export default function () {
7076
</div>)
7177
};
7278

79+
useEffect(() => { document.title = 'Prebuild — Gitpod' }, []);
80+
7381
return <>
7482
<Header title={renderTitle()} subtitle={renderSubtitle()} />
7583
<div className="w-full"><PrebuildLogs workspaceId={prebuild?.buildWorkspaceId}/></div>

0 commit comments

Comments
 (0)