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