@@ -15,24 +15,45 @@ import Label from "./Label";
1515import Property from "./Property" ;
1616import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution" ;
1717import { 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" ;
1820
1921export default function TeamDetail ( props : { team : Team } ) {
2022 const { team } = props ;
2123 const [ teamMembers , setTeamMembers ] = useState < TeamMemberInfo [ ] | undefined > ( undefined ) ;
2224 const [ billingMode , setBillingMode ] = useState < BillingMode | undefined > ( undefined ) ;
2325 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 ) ;
2432
25- useEffect ( ( ) => {
33+ const initialize = ( ) => {
2634 ( async ( ) => {
2735 const members = await getGitpodService ( ) . server . adminGetTeamMembers ( team . id ) ;
2836 if ( members . length > 0 ) {
2937 setTeamMembers ( members ) ;
3038 }
3139 } ) ( ) ;
3240 getGitpodService ( )
33- . server . adminGetBillingMode ( AttributionId . render ( { kind : "team" , teamId : props . team . id } ) )
41+ . server . adminGetBillingMode ( AttributionId . render ( { kind : "team" , teamId : team . id } ) )
3442 . 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 ] ) ;
3657
3758 const filteredMembers = teamMembers ?. filter ( ( m ) => {
3859 const memberSearchText = `${ m . fullName || "" } ${ m . primaryEmail || "" } ` . toLocaleLowerCase ( ) ;
@@ -64,8 +85,53 @@ export default function TeamDetail(props: { team: Team }) {
6485 </ div >
6586 </ div >
6687 < 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+ ) }
69135 </ div >
70136 < div className = "flex mt-4" >
71137 < div className = "flex" >
@@ -151,6 +217,91 @@ export default function TeamDetail(props: { team: Team }) {
151217 ) )
152218 ) }
153219 </ 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 >
154305 </ >
155306 ) ;
156307}
0 commit comments