Skip to content

Commit e044c1d

Browse files
svenefftingeroboquat
authored andcommitted
[admin] allow usage adjustments
1 parent 2fef214 commit e044c1d

File tree

13 files changed

+548
-116
lines changed

13 files changed

+548
-116
lines changed

components/dashboard/src/admin/TeamDetail.tsx

+156-5
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,45 @@ import Label from "./Label";
1515
import Property from "./Property";
1616
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
1717
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";
1820

1921
export 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

Comments
 (0)