Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/admin/TeamDetail.tsx
2500 views
1
/**
2
* Copyright (c) 2022 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 dayjs from "dayjs";
8
import { useEffect, useState } from "react";
9
import { Team, TeamMemberInfo, TeamMemberRole, VALID_ORG_MEMBER_ROLES } from "@gitpod/gitpod-protocol";
10
import { getGitpodService } from "../service/service";
11
import { Item, ItemField, ItemsList } from "../components/ItemsList";
12
import DropDown from "../components/DropDown";
13
import { Link } from "react-router-dom";
14
import Label from "./Label";
15
import Property from "./Property";
16
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
17
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
18
import { CostCenterJSON, CostCenter_BillingStrategy } from "@gitpod/gitpod-protocol/lib/usage";
19
import Modal from "../components/Modal";
20
import { Heading2 } from "../components/typography/headings";
21
import search from "../icons/search.svg";
22
import { Button } from "@podkit/buttons/Button";
23
24
export default function TeamDetail(props: { team: Team }) {
25
const { team } = props;
26
const [teamMembers, setTeamMembers] = useState<TeamMemberInfo[] | undefined>(undefined);
27
const [billingMode, setBillingMode] = useState<BillingMode | undefined>(undefined);
28
const [searchText, setSearchText] = useState<string>("");
29
const [costCenter, setCostCenter] = useState<CostCenterJSON>();
30
const [usageBalance, setUsageBalance] = useState<number>(0);
31
const [usageLimit, setUsageLimit] = useState<number>();
32
const [editSpendingLimit, setEditSpendingLimit] = useState<boolean>(false);
33
const [creditNote, setCreditNote] = useState<{ credits: number; note?: string }>({ credits: 0 });
34
const [editAddCreditNote, setEditAddCreditNote] = useState<boolean>(false);
35
36
const attributionId = AttributionId.render(AttributionId.create(team));
37
const initialize = () => {
38
(async () => {
39
const members = await getGitpodService().server.adminGetTeamMembers(team.id);
40
if (members.length > 0) {
41
setTeamMembers(members);
42
}
43
})();
44
getGitpodService().server.adminGetBillingMode(attributionId).then(setBillingMode);
45
getGitpodService().server.adminGetCostCenter(attributionId).then(setCostCenter);
46
getGitpodService().server.adminGetUsageBalance(attributionId).then(setUsageBalance);
47
};
48
49
useEffect(initialize, [team, attributionId]);
50
51
useEffect(() => {
52
if (!costCenter) {
53
return;
54
}
55
setUsageLimit(costCenter.spendingLimit);
56
}, [costCenter]);
57
58
const filteredMembers = teamMembers?.filter((m) => {
59
const memberSearchText = `${m.fullName || ""}${m.primaryEmail || ""}`.toLocaleLowerCase();
60
if (!memberSearchText.includes(searchText.toLocaleLowerCase())) {
61
return false;
62
}
63
return true;
64
});
65
66
const setTeamMemberRole = async (userId: string, role: TeamMemberRole) => {
67
await getGitpodService().server.adminSetTeamMemberRole(team!.id, userId, role);
68
setTeamMembers(await getGitpodService().server.adminGetTeamMembers(team!.id));
69
};
70
return (
71
<>
72
<div className="flex mt-8">
73
<div className="flex-1">
74
<div className="flex">
75
<Heading2>{team.name}</Heading2>
76
{team.markedDeleted && (
77
<span className="mt-2">
78
<Label text="Deleted" color="red" />
79
</span>
80
)}
81
</div>
82
<span className="mb-6 text-gray-400">{team.id}</span>
83
<span className="text-gray-400"> ยท </span>
84
<span className="text-gray-400">Created on {dayjs(team.creationTime).format("MMM D, YYYY")}</span>
85
</div>
86
</div>
87
<div className="flex mt-6">
88
{!team.markedDeleted && <Property name="Members">{teamMembers?.length || "?"}</Property>}
89
{!team.markedDeleted && <Property name="Billing Mode">{billingMode?.mode || "---"}</Property>}
90
{costCenter && (
91
<Property name="Stripe Subscription" actions={[]}>
92
<span>
93
{costCenter?.billingStrategy === CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE
94
? "Active"
95
: "Inactive"}
96
</span>
97
</Property>
98
)}
99
</div>
100
<div className="flex mt-6">
101
{costCenter && (
102
<Property name="Current Cycle" actions={[]}>
103
<span>
104
{dayjs(costCenter?.billingCycleStart).format("MMM D")} -{" "}
105
{dayjs(costCenter?.nextBillingTime).format("MMM D")}
106
</span>
107
</Property>
108
)}
109
{costCenter && (
110
<Property
111
name="Available Credits"
112
actions={[
113
{
114
label: "Add Credits",
115
onClick: () => setEditAddCreditNote(true),
116
},
117
]}
118
>
119
<span>{usageBalance * -1 + (costCenter?.spendingLimit || 0)} Credits</span>
120
</Property>
121
)}
122
{costCenter && (
123
<Property
124
name="Usage Limit"
125
actions={[
126
{
127
label: "Change Usage Limit",
128
onClick: () => setEditSpendingLimit(true),
129
},
130
]}
131
>
132
<span>{costCenter?.spendingLimit} Credits</span>
133
</Property>
134
)}
135
</div>
136
<div className="flex">
137
<div className="flex mt-3 pb-3">
138
<div className="flex relative h-10 my-auto">
139
<img
140
src={search}
141
title="Search"
142
className="filter-grayscale absolute top-3 left-3"
143
alt="search icon"
144
/>
145
<input
146
className="w-64 pl-9 border-0"
147
type="search"
148
placeholder="Search Members"
149
onChange={(e) => setSearchText(e.target.value)}
150
/>
151
</div>
152
</div>
153
</div>
154
155
<ItemsList className="mt-2">
156
<Item header={true} className="grid grid-cols-3">
157
<ItemField className="my-auto">
158
<span className="pl-14">Name</span>
159
</ItemField>
160
<ItemField className="flex items-center space-x-1 my-auto">
161
<span>Joined</span>
162
<svg xmlns="http://www.w3.org/2000/svg" fill="none" className="h-4 w-4" viewBox="0 0 16 16">
163
<path
164
fill="#A8A29E"
165
fillRule="evenodd"
166
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"
167
clipRule="evenodd"
168
/>
169
</svg>
170
</ItemField>
171
<ItemField className="flex items-center my-auto">
172
<span className="flex-grow">Role</span>
173
</ItemField>
174
</Item>
175
{team.markedDeleted || !filteredMembers || filteredMembers.length === 0 ? (
176
<p className="pt-16 text-center">No members found</p>
177
) : (
178
filteredMembers &&
179
filteredMembers.map((m) => (
180
<Item className="grid grid-cols-3" key={m.userId}>
181
<ItemField className="flex items-center my-auto">
182
<div className="w-14">
183
{m.avatarUrl && (
184
<img
185
className="rounded-full w-8 h-8"
186
src={m.avatarUrl || ""}
187
alt={m.fullName}
188
/>
189
)}
190
</div>
191
<Link to={"/admin/users/" + m.userId}>
192
<div>
193
<div className="text-base text-gray-900 dark:text-gray-50 font-medium">
194
{m.fullName}
195
</div>
196
<p>{m.primaryEmail}</p>
197
</div>
198
</Link>
199
</ItemField>
200
<ItemField className="my-auto">
201
<span className="text-gray-400">{dayjs(m.memberSince).fromNow()}</span>
202
</ItemField>
203
<ItemField className="flex items-center my-auto">
204
<span className="text-gray-400 capitalize">
205
<DropDown
206
customClasses="w-32"
207
activeEntry={m.role}
208
entries={VALID_ORG_MEMBER_ROLES.map((role) => ({
209
title: role,
210
onClick: () => setTeamMemberRole(m.userId, role),
211
}))}
212
/>
213
</span>
214
</ItemField>
215
</Item>
216
))
217
)}
218
</ItemsList>
219
<Modal
220
visible={editSpendingLimit}
221
onClose={() => setEditSpendingLimit(false)}
222
title="Change Usage Limit"
223
buttons={[
224
<Button
225
disabled={usageLimit === costCenter?.spendingLimit}
226
onClick={async () => {
227
if (usageLimit !== undefined) {
228
await getGitpodService().server.adminSetUsageLimit(attributionId, usageLimit || 0);
229
setUsageLimit(undefined);
230
initialize();
231
setEditSpendingLimit(false);
232
}
233
}}
234
>
235
Change
236
</Button>,
237
]}
238
>
239
<p className="pb-4 text-gray-500 text-base">Change the usage limit in credits per month.</p>
240
<label>Credits</label>
241
<div className="flex flex-col">
242
<input
243
type="number"
244
className="w-full"
245
min={Math.max(usageBalance, 0)}
246
max={500000}
247
title="Change Usage Limit"
248
value={usageLimit}
249
onChange={(event) => setUsageLimit(Number.parseInt(event.target.value))}
250
/>
251
</div>
252
</Modal>
253
<Modal
254
visible={editAddCreditNote}
255
onClose={() => setEditAddCreditNote(false)}
256
title="Add Credits"
257
buttons={[
258
<Button
259
disabled={creditNote.credits === 0 || !creditNote.note}
260
onClick={async () => {
261
if (creditNote.credits !== 0 && !!creditNote.note) {
262
await getGitpodService().server.adminAddUsageCreditNote(
263
attributionId,
264
creditNote.credits,
265
creditNote.note,
266
);
267
setEditAddCreditNote(false);
268
setCreditNote({ credits: 0 });
269
initialize();
270
}
271
}}
272
>
273
Add Credits
274
</Button>,
275
]}
276
>
277
<p>Adds or subtracts the amount of credits from this account.</p>
278
<div className="flex flex-col">
279
<label className="mt-4">Credits</label>
280
<input
281
className="w-full"
282
type="number"
283
min={-50000}
284
max={50000}
285
title="Credits"
286
value={creditNote.credits}
287
onChange={(event) =>
288
setCreditNote({ credits: Number.parseInt(event.target.value), note: creditNote.note })
289
}
290
/>
291
<label className="mt-4">Note</label>
292
<textarea
293
className="w-full"
294
title="Note"
295
onChange={(event) => setCreditNote({ credits: creditNote.credits, note: event.target.value })}
296
/>
297
</div>
298
</Modal>
299
</>
300
);
301
}
302
303