@@ -15,24 +15,45 @@ import Label from "./Label";
15
15
import Property from "./Property" ;
16
16
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution" ;
17
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" ;
18
20
19
21
export default function TeamDetail ( props : { team : Team } ) {
20
22
const { team } = props ;
21
23
const [ teamMembers , setTeamMembers ] = useState < TeamMemberInfo [ ] | undefined > ( undefined ) ;
22
24
const [ billingMode , setBillingMode ] = useState < BillingMode | undefined > ( undefined ) ;
23
25
const [ searchText , setSearchText ] = useState < string > ( "" ) ;
26
+ const [ costCenter , setCostCenter ] = useState < CostCenterJSON > ( ) ;
27
+ const [ usageBalance , setUsageBalance ] = useState < number > ( 0 ) ;
28
+ const [ usageLimit , setUsageLimit ] = useState < number > ( ) ;
29
+ const [ editSpendingLimit , setEditSpendingLimit ] = useState < boolean > ( false ) ;
30
+ const [ creditNote , setCreditNote ] = useState < { credits : number ; note ?: string } > ( { credits : 0 } ) ;
31
+ const [ editAddCreditNote , setEditAddCreditNote ] = useState < boolean > ( false ) ;
24
32
25
- useEffect ( ( ) => {
33
+ const initialize = ( ) => {
26
34
( async ( ) => {
27
35
const members = await getGitpodService ( ) . server . adminGetTeamMembers ( team . id ) ;
28
36
if ( members . length > 0 ) {
29
37
setTeamMembers ( members ) ;
30
38
}
31
39
} ) ( ) ;
32
40
getGitpodService ( )
33
- . server . adminGetBillingMode ( AttributionId . render ( { kind : "team" , teamId : props . team . id } ) )
41
+ . server . adminGetBillingMode ( AttributionId . render ( { kind : "team" , teamId : team . id } ) )
34
42
. then ( ( bm ) => setBillingMode ( bm ) ) ;
35
- } , [ team ] ) ;
43
+ const attributionId = AttributionId . render ( AttributionId . create ( team ) ) ;
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 ] ) ;
50
+
51
+ useEffect ( ( ) => {
52
+ if ( ! costCenter ) {
53
+ return ;
54
+ }
55
+ setUsageLimit ( costCenter . spendingLimit ) ;
56
+ } , [ costCenter ] ) ;
36
57
37
58
const filteredMembers = teamMembers ?. filter ( ( m ) => {
38
59
const memberSearchText = `${ m . fullName || "" } ${ m . primaryEmail || "" } ` . toLocaleLowerCase ( ) ;
@@ -64,8 +85,53 @@ export default function TeamDetail(props: { team: Team }) {
64
85
</ div >
65
86
</ div >
66
87
< div className = "flex mt-6" >
67
- { ! team . markedDeleted && teamMembers && < Property name = "Members" > { teamMembers . length } </ Property > }
68
- { ! team . markedDeleted && < Property name = "BillingMode" > { billingMode ?. mode || "---" } </ Property > }
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
+ ) }
69
135
</ div >
70
136
< div className = "flex mt-4" >
71
137
< div className = "flex" >
@@ -151,6 +217,91 @@ export default function TeamDetail(props: { team: Team }) {
151
217
) )
152
218
) }
153
219
</ ItemsList >
220
+ < Modal
221
+ visible = { editSpendingLimit }
222
+ onClose = { ( ) => setEditSpendingLimit ( false ) }
223
+ title = "Change Usage Limit"
224
+ onEnter = { ( ) => false }
225
+ buttons = { [
226
+ < button
227
+ disabled = { usageLimit === costCenter ?. spendingLimit }
228
+ onClick = { async ( ) => {
229
+ if ( usageLimit !== undefined ) {
230
+ await getGitpodService ( ) . server . adminSetUsageLimit (
231
+ AttributionId . render ( AttributionId . create ( team ) ) ,
232
+ usageLimit || 0 ,
233
+ ) ;
234
+ setUsageLimit ( undefined ) ;
235
+ initialize ( ) ;
236
+ setEditSpendingLimit ( false ) ;
237
+ }
238
+ } }
239
+ >
240
+ Change
241
+ </ button > ,
242
+ ] }
243
+ >
244
+ < p className = "pb-4 text-gray-500 text-base" > Change the usage limit in credits per month.</ p >
245
+ < label > Credits</ label >
246
+ < div className = "flex flex-col" >
247
+ < input
248
+ type = "number"
249
+ className = "w-full"
250
+ min = { Math . max ( usageBalance , 0 ) }
251
+ max = { 500000 }
252
+ title = "Change Usage Limit"
253
+ value = { usageLimit }
254
+ onChange = { ( event ) => setUsageLimit ( Number . parseInt ( event . target . value ) ) }
255
+ />
256
+ </ div >
257
+ </ Modal >
258
+ < Modal
259
+ onEnter = { ( ) => false }
260
+ visible = { editAddCreditNote }
261
+ onClose = { ( ) => setEditAddCreditNote ( false ) }
262
+ title = "Add Credits"
263
+ buttons = { [
264
+ < button
265
+ disabled = { creditNote . credits === 0 || ! creditNote . note }
266
+ onClick = { async ( ) => {
267
+ if ( creditNote . credits !== 0 && ! ! creditNote . note ) {
268
+ await getGitpodService ( ) . server . adminAddUsageCreditNote (
269
+ AttributionId . render ( AttributionId . create ( team ) ) ,
270
+ creditNote . credits ,
271
+ creditNote . note ,
272
+ ) ;
273
+ setEditAddCreditNote ( false ) ;
274
+ setCreditNote ( { credits : 0 } ) ;
275
+ initialize ( ) ;
276
+ }
277
+ } }
278
+ >
279
+ Add Credits
280
+ </ button > ,
281
+ ] }
282
+ >
283
+ < p > Adds or subtracts the amount of credits from this account.</ p >
284
+ < div className = "flex flex-col" >
285
+ < label className = "mt-4" > Credits</ label >
286
+ < input
287
+ className = "w-full"
288
+ type = "number"
289
+ min = { - 50000 }
290
+ max = { 50000 }
291
+ title = "Credits"
292
+ value = { creditNote . credits }
293
+ onChange = { ( event ) =>
294
+ setCreditNote ( { credits : Number . parseInt ( event . target . value ) , note : creditNote . note } )
295
+ }
296
+ />
297
+ < label className = "mt-4" > Note</ label >
298
+ < textarea
299
+ className = "w-full"
300
+ title = "Note"
301
+ onChange = { ( event ) => setCreditNote ( { credits : creditNote . credits , note : event . target . value } ) }
302
+ />
303
+ </ div >
304
+ </ Modal >
154
305
</ >
155
306
) ;
156
307
}
0 commit comments