Skip to content

Commit 6e4fe1c

Browse files
[dashboard] Allow granting a user 20 extra hours from the admin dashboard (#3929)
Co-authored-by: George Tsiolis <[email protected]>
1 parent 3760f7e commit 6e4fe1c

File tree

4 files changed

+32
-18
lines changed

4 files changed

+32
-18
lines changed

components/dashboard/src/admin/UserDetail.tsx

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -104,45 +104,52 @@ export default function UserDetail(p: { user: User }) {
104104
<div className="flex w-full mt-6">
105105
<Property name="Sign Up Date">{moment(user.creationDate).format('MMM D, YYYY')}</Property>
106106
<Property name="Remaining Hours"
107-
action={
108-
accountStatement && {
107+
actions={
108+
accountStatement && [{
109109
label: 'View Account Statement',
110110
onClick: () => setViewAccountStatement(true)
111-
}
111+
}, {
112+
label: 'Grant 20 Extra Hours',
113+
onClick: async () => {
114+
await getGitpodService().server.adminGrantExtraHours(user.id, 20);
115+
setAccountStatement(await getGitpodService().server.adminGetAccountStatement(user.id));
116+
}
117+
}]
112118
}
113119
>{accountStatement?.remainingHours ? accountStatement?.remainingHours.toString() : '---'}</Property>
114120
<Property
115121
name="Plan"
116-
action={accountStatement && {
122+
actions={accountStatement && [{
117123
label: (isProfessionalOpenSource ? 'Disable' : 'Enable') + ' Professional OSS',
118-
onClick: () => {
119-
getGitpodService().server.adminSetProfessionalOpenSource(user.id, !isProfessionalOpenSource);
124+
onClick: async () => {
125+
await getGitpodService().server.adminSetProfessionalOpenSource(user.id, !isProfessionalOpenSource);
126+
setAccountStatement(await getGitpodService().server.adminGetAccountStatement(user.id));
120127
}
121-
}}
128+
}]}
122129
>{accountStatement?.subscriptions ? accountStatement.subscriptions.filter(s => Subscription.isActive(s, new Date().toISOString())).map(s => Plans.getById(s.planId)?.name).join(', ') : '---'}</Property>
123130
</div>
124131
<div className="flex w-full mt-6">
125132
<Property name="Feature Flags"
126-
action={{
133+
actions={[{
127134
label: 'Edit Feature Flags',
128135
onClick: () => {
129136
setEditFeatureFlags(true);
130137
}
131-
}}
138+
}]}
132139
>{user.featureFlags?.permanentWSFeatureFlags?.join(', ') || '---'}</Property>
133140
<Property name="Roles"
134-
action={{
141+
actions={[{
135142
label: 'Edit Roles',
136143
onClick: () => {
137144
setEditRoles(true);
138145
}
139-
}}
146+
}]}
140147
>{user.rolesOrPermissions?.join(', ') || '---'}</Property>
141148
<Property name="Student"
142-
action={ !isStudent ? {
149+
actions={ !isStudent ? [{
143150
label: `Make '${emailDomain}' a student domain`,
144151
onClick: addStudentDomain
145-
} : undefined}
152+
}] : undefined}
146153
>{isStudent === undefined ? '---' : (isStudent ? 'Enabled' : 'Disabled')}</Property>
147154
</div>
148155
</div>
@@ -185,17 +192,19 @@ function Label(p: { text: string, color: string }) {
185192
return <div className={`ml-3 text-sm text-${p.color}-600 truncate bg-${p.color}-100 px-1.5 py-0.5 rounded-md my-auto`}>{p.text}</div>;
186193
}
187194

188-
export function Property(p: { name: string, children: string | ReactChild, action?: { label: string, onClick: () => void } }) {
195+
export function Property(p: { name: string, children: string | ReactChild, actions?: { label: string, onClick: () => void }[] }) {
189196
return <div className="ml-3 flex flex-col w-4/12 truncate">
190197
<div className="text-base text-gray-500 truncate">
191198
{p.name}
192199
</div>
193200
<div className="text-lg text-gray-600 font-semibold truncate">
194201
{p.children}
195202
</div>
196-
<div className="cursor-pointer text-sm text-blue-400 hover:text-blue-500 truncate" onClick={p.action?.onClick}>
197-
{p.action?.label || ''}
198-
</div>
203+
{(p.actions || []).map(a =>
204+
<div className="cursor-pointer text-sm text-blue-400 hover:text-blue-500 truncate" onClick={a.onClick}>
205+
{a.label || ''}
206+
</div>
207+
)}
199208
</div>;
200209
}
201210

@@ -253,4 +262,4 @@ function getRopEntries(user: User, updateUser: UpdateUserFunction): Entry[] {
253262
...Object.entries(Permissions).map(e => createRopEntry(e[0] as RoleOrPermission)),
254263
...Object.entries(Roles).map(e => createRopEntry(e[0] as RoleOrPermission, true))
255264
];
256-
};
265+
};

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface AdminServer {
2828
adminSetProfessionalOpenSource(userId: string, shouldGetProfOSS: boolean): Promise<void>;
2929
adminIsStudent(userId: string): Promise<boolean>;
3030
adminAddStudentEmailDomain(userId: string, domain: string): Promise<void>;
31+
adminGrantExtraHours(userId: string, extraHours: number): Promise<void>;
3132
}
3233

3334
export interface AdminGetListRequest<T> {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ function readConfig(): RateLimiterConfig {
120120
"adminGetAccountStatement": { group: "default", points: 1 },
121121
"adminIsStudent": { group: "default", points: 1 },
122122
"adminSetProfessionalOpenSource": { group: "default", points: 1 },
123+
"adminGrantExtraHours": { group: "default", points: 1 },
123124
"checkout": { group: "default", points: 1 },
124125
"createPortalSession": { group: "default", points: 1 },
125126
"getAccountStatement": { group: "default", points: 1 },

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,6 +1643,9 @@ export class GitpodServerImpl<Client extends GitpodClient, Server extends Gitpod
16431643
async adminAddStudentEmailDomain(userId: string, domain: string): Promise<void> {
16441644
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
16451645
}
1646+
async adminGrantExtraHours(userId: string, extraHours: number): Promise<void> {
1647+
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
1648+
}
16461649
async isStudent(): Promise<boolean> {
16471650
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
16481651
}

0 commit comments

Comments
 (0)