Skip to content

Commit 4c98804

Browse files
committed
[dashboard] Improve team members page
1 parent caa322c commit 4c98804

File tree

9 files changed

+90
-27
lines changed

9 files changed

+90
-27
lines changed

components/dashboard/src/App.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ function App() {
154154
<Route path="/integrations" exact component={Integrations} />
155155
<Route path="/notifications" exact component={Notifications} />
156156
<Route path="/plans" exact component={Plans} />
157-
<Route path="/teams" exact component={Teams} />
158157
<Route path="/variables" exact component={EnvironmentVariables} />
159158
<Route path="/preferences" exact component={Preferences} />
160159
<Route path="/install-github-app" exact component={InstallGitHubApp} />
@@ -184,8 +183,11 @@ function App() {
184183
<p className="mt-4 text-lg text-gitpod-red">{decodeURIComponent(getURLHash())}</p>
185184
</div>
186185
</Route>
187-
<Route path="/new-team" exact component={NewTeam} />
188-
<Route path="/join-team" exact component={JoinTeam} />
186+
<Route path="/teams">
187+
<Route exact path="/teams" component={Teams} />
188+
<Route exact path="/teams/new" component={NewTeam} />
189+
<Route exact path="/teams/join" component={JoinTeam} />
190+
</Route>
189191
{(teams || []).map(team => <Route path={`/${team.slug}`}>
190192
<Route exact path={`/${team.slug}`}>
191193
<Redirect to={`/${team.slug}/projects`} />

components/dashboard/src/Menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export default function Menu() {
136136
<span className="flex-1 font-semibold">New Team</span>
137137
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" className="w-3.5"><path fill="currentColor" fill-rule="evenodd" d="M7 0a1 1 0 011 1v5h5a1 1 0 110 2H8v5a1 1 0 11-2 0V8H1a1 1 0 010-2h5V1a1 1 0 011-1z" clip-rule="evenodd"/></svg>
138138
</div>,
139-
onClick: () => history.push("/new-team"),
139+
onClick: () => history.push("/teams/new"),
140140
}
141141
]}>
142142
<div className="flex p-1.5 pl-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">

components/dashboard/src/teams/Members.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@
77
import { TeamMemberInfo, TeamMembershipInvite } from "@gitpod/gitpod-protocol";
88
import moment from "moment";
99
import { useContext, useEffect, useState } from "react";
10-
import { useLocation } from "react-router";
10+
import { useHistory, useLocation } from "react-router";
1111
import Header from "../components/Header";
1212
import DropDown from "../components/DropDown";
1313
import { ItemsList, Item, ItemField, ItemFieldContextMenu } from "../components/ItemsList";
1414
import Modal from "../components/Modal";
15-
import { getGitpodService } from "../service/service";
1615
import copy from '../images/copy.svg';
16+
import { getGitpodService } from "../service/service";
17+
import { UserContext } from "../user-context";
1718
import { TeamsContext, getCurrentTeam } from "./teams-context";
1819

1920

2021
export default function() {
21-
const { teams } = useContext(TeamsContext);
22+
const { user } = useContext(UserContext);
23+
const { teams, setTeams } = useContext(TeamsContext);
24+
const history = useHistory();
2225
const location = useLocation();
2326
const team = getCurrentTeam(location, teams);
2427
const [ members, setMembers ] = useState<TeamMemberInfo[]>([]);
@@ -41,7 +44,7 @@ export default function() {
4144

4245
const getInviteURL = (inviteId: string) => {
4346
const link = new URL(window.location.href);
44-
link.pathname = '/join-team';
47+
link.pathname = '/teams/join';
4548
link.search = '?inviteId=' + inviteId;
4649
return link.href;
4750
}
@@ -70,6 +73,20 @@ export default function() {
7073
}
7174
}
7275

76+
const removeTeamMember = async (userId: string) => {
77+
await getGitpodService().server.removeTeamMember(team!.id, userId);
78+
const newTeams = await getGitpodService().server.getTeams();
79+
if (newTeams.some(t => t.id === team!.id)) {
80+
// We're still a member of this team.
81+
const newMembers = await getGitpodService().server.getTeamMembers(team!.id);
82+
setMembers(newMembers);
83+
} else {
84+
// We're no longer a member of this team.
85+
setTeams(newTeams);
86+
history.push('/');
87+
}
88+
}
89+
7390
return <>
7491
<Header title="Members" subtitle="Manage team members." />
7592
<div className="lg:px-28 px-10">
@@ -100,8 +117,9 @@ export default function() {
100117
<ItemField>
101118
<span className="pl-14">Name</span>
102119
</ItemField>
103-
<ItemField>
120+
<ItemField className="flex items-center space-x-1">
104121
<span>Joined</span>
122+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" className="h-4 w-4" viewBox="0 0 16 16"><path fill="#A8A29E" fill-rule="evenodd" d="M13.366 8.234a.8.8 0 010 1.132l-4.8 4.8a.8.8 0 01-1.132 0l-4.8-4.8a.8.8 0 111.132-1.132L7.2 11.67V2.4a.8.8 0 111.6 0v9.269l3.434-3.435a.8.8 0 011.132 0z" clip-rule="evenodd"/></svg>
105123
</ItemField>
106124
<ItemField className="flex items-center">
107125
<span className="flex-grow">Role</span>
@@ -123,10 +141,10 @@ export default function() {
123141
<span className="text-gray-400 flex-grow capitalize">{m.role}</span>
124142
<ItemFieldContextMenu menuEntries={[
125143
{
126-
title: 'Remove',
144+
title: (m.userId === user?.id) ? 'Leave Team' : 'Remove',
127145
customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300',
128-
onClick: () => { /* TODO(janx) */ }
129-
},
146+
onClick: () => removeTeamMember(m.userId)
147+
}
130148
]} />
131149
</ItemField>
132150
</Item>)}

components/gitpod-db/src/team-db.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface TeamDB {
1313
findTeamsByUser(userId: string): Promise<Team[]>;
1414
createTeam(userId: string, name: string): Promise<Team>;
1515
addMemberToTeam(userId: string, teamId: string): Promise<void>;
16+
removeMemberFromTeam(userId: string, teamId: string): Promise<void>;
1617
findTeamMembershipInviteById(inviteId: string): Promise<TeamMembershipInvite>;
1718
findGenericInviteByTeamId(teamId: string): Promise<TeamMembershipInvite | undefined>;
1819
resetGenericInvite(teamId: string): Promise<TeamMembershipInvite>;

components/gitpod-db/src/typeorm/team-db-impl.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,30 +41,34 @@ export class TeamDBImpl implements TeamDB {
4141

4242
public async findTeamById(teamId: string): Promise<Team | undefined> {
4343
const teamRepo = await this.getTeamRepo();
44-
return teamRepo.findOne({ id: teamId });
44+
return teamRepo.findOne({ id: teamId, deleted: false });
4545
}
4646

4747
public async findMembersByTeam(teamId: string): Promise<TeamMemberInfo[]> {
4848
const membershipRepo = await this.getMembershipRepo();
4949
const userRepo = await this.getUserRepo();
50-
const memberships = await membershipRepo.find({ teamId });
50+
const memberships = await membershipRepo.find({ teamId, deleted: false });
5151
const users = await userRepo.findByIds(memberships.map(m => m.userId));
52-
const infos = users.map(u => ({
53-
userId: u.id,
54-
fullName: u.fullName || u.name,
55-
primaryEmail: User.getPrimaryEmail(u),
56-
avatarUrl: u.avatarUrl,
57-
role: memberships.find(m => m.userId === u.id)!.role,
58-
memberSince: u.creationDate,
59-
}));
52+
const infos = users.map(u => {
53+
const m = memberships.find(m => m.userId === u.id)!;
54+
return {
55+
userId: u.id,
56+
fullName: u.fullName || u.name,
57+
primaryEmail: User.getPrimaryEmail(u),
58+
avatarUrl: u.avatarUrl,
59+
role: m.role,
60+
memberSince: m.creationTime,
61+
};
62+
});
6063
return infos.sort((a,b) => a.memberSince < b.memberSince ? 1 : (a.memberSince === b.memberSince ? 0 : -1));
6164
}
6265

6366
public async findTeamsByUser(userId: string): Promise<Team[]> {
6467
const teamRepo = await this.getTeamRepo();
6568
const membershipRepo = await this.getMembershipRepo();
66-
const memberships = await membershipRepo.find({ userId });
67-
return teamRepo.findByIds(memberships.map(m => m.teamId));
69+
const memberships = await membershipRepo.find({ userId, deleted: false });
70+
const teams = await teamRepo.findByIds(memberships.map(m => m.teamId));
71+
return teams.filter(t => !t.deleted);
6872
}
6973

7074
public async createTeam(userId: string, name: string): Promise<Team> {
@@ -104,11 +108,11 @@ export class TeamDBImpl implements TeamDB {
104108
}
105109
const teamRepo = await this.getTeamRepo();
106110
const team = await teamRepo.findOneById(teamId);
107-
if (!team) {
111+
if (!team || !!team.deleted) {
108112
throw new Error('A team with this ID could not be found');
109113
}
110114
const membershipRepo = await this.getMembershipRepo();
111-
const membership = await membershipRepo.findOne({ teamId, userId });
115+
const membership = await membershipRepo.findOne({ teamId, userId, deleted: false });
112116
if (!!membership) {
113117
throw new Error('You are already a member of this team');
114118
}
@@ -121,6 +125,24 @@ export class TeamDBImpl implements TeamDB {
121125
});
122126
}
123127

128+
public async removeMemberFromTeam(userId: string, teamId: string): Promise<void> {
129+
if (teamId.length !== 36) {
130+
throw new Error('This team ID is incorrect');
131+
}
132+
const teamRepo = await this.getTeamRepo();
133+
const team = await teamRepo.findOneById(teamId);
134+
if (!team || !!team.deleted) {
135+
throw new Error('A team with this ID could not be found');
136+
}
137+
const membershipRepo = await this.getMembershipRepo();
138+
const membership = await membershipRepo.findOne({ teamId, userId, deleted: false });
139+
if (!membership) {
140+
throw new Error('You are not currently a member of this team');
141+
}
142+
membership.deleted = true;
143+
await membershipRepo.save(membership);
144+
}
145+
124146
public async findTeamMembershipInviteById(inviteId: string): Promise<TeamMembershipInvite> {
125147
const inviteRepo = await this.getMembershipInviteRepo();
126148
const invite = await inviteRepo.findOneById(inviteId);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
112112
getTeamMembers(teamId: string): Promise<TeamMemberInfo[]>;
113113
createTeam(name: string): Promise<Team>;
114114
joinTeam(inviteId: string): Promise<Team>;
115+
removeTeamMember(teamId: string, userId: string): Promise<void>;
115116
getGenericInvite(teamId: string): Promise<TeamMembershipInvite>;
116117
resetGenericInvite(inviteId: string): Promise<TeamMembershipInvite>;
117118

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ function readConfig(): RateLimiterConfig {
8282
"getTeamMembers": { group: "default", points: 1 },
8383
"createTeam": { group: "default", points: 1 },
8484
"joinTeam": { group: "default", points: 1 },
85+
"removeTeamMember": { group: "default", points: 1 },
8586
"getGenericInvite": { group: "default", points: 1 },
8687
"resetGenericInvite": { group: "default", points: 1 },
8788
"getContentBlobUploadUrl": { group: "default", points: 1 },

components/server/src/auth/resource-access.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,18 @@ export class OwnerResourceGuard implements ResourceAccessGuard {
156156
case "envVar":
157157
return resource.subject.userId === this.userId;
158158
case "team":
159-
return resource.members.some(m => m.userId === this.userId);
159+
switch (operation) {
160+
case "create":
161+
// Anyone can create a new team.
162+
return true;
163+
case "get":
164+
// Only members can get infos about a team.
165+
return resource.members.some(m => m.userId === this.userId);
166+
case "update":
167+
case "delete":
168+
// Only owners can update or delete a team.
169+
return resource.members.some(m => m.userId === this.userId && m.role === "owner");
170+
}
160171
}
161172
}
162173

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,13 @@ export class GitpodServerImpl<Client extends GitpodClient, Server extends Gitpod
14171417
return team!;
14181418
}
14191419

1420+
public async removeTeamMember(teamId: string, userId: string): Promise<void> {
1421+
const user = this.checkUser("removeTeamMember");
1422+
// Users are free to leave any team themselves, but only owners can remove others from their teams.
1423+
await this.guardTeamOperation(teamId, user.id === userId ? "get" : "update");
1424+
await this.teamDB.removeMemberFromTeam(userId, teamId);
1425+
}
1426+
14201427
public async getGenericInvite(teamId: string): Promise<TeamMembershipInvite> {
14211428
this.checkUser("getGenericInvite");
14221429
await this.guardTeamOperation(teamId, "get");

0 commit comments

Comments
 (0)