diff --git a/components/dashboard/src/admin/TeamDetail.tsx b/components/dashboard/src/admin/TeamDetail.tsx index 2104bc5faddbbc..98bfdd32dd085a 100644 --- a/components/dashboard/src/admin/TeamDetail.tsx +++ b/components/dashboard/src/admin/TeamDetail.tsx @@ -13,10 +13,13 @@ import DropDown from "../components/DropDown"; import { Link } from "react-router-dom"; import Label from "./Label"; import Property from "./Property"; +import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; +import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; export default function TeamDetail(props: { team: Team }) { const { team } = props; const [teamMembers, setTeamMembers] = useState(undefined); + const [billingMode, setBillingMode] = useState(undefined); const [searchText, setSearchText] = useState(""); useEffect(() => { @@ -26,6 +29,9 @@ export default function TeamDetail(props: { team: Team }) { setTeamMembers(members); } })(); + getGitpodService() + .server.adminGetBillingMode(AttributionId.render({ kind: "team", teamId: props.team.id })) + .then((bm) => setBillingMode(bm)); }, [team]); const filteredMembers = teamMembers?.filter((m) => { @@ -59,6 +65,7 @@ export default function TeamDetail(props: { team: Team }) {
{!team.markedDeleted && teamMembers && {teamMembers.length}} + {!team.markedDeleted && {billingMode?.mode || "---"}}
diff --git a/components/dashboard/src/admin/UserDetail.tsx b/components/dashboard/src/admin/UserDetail.tsx index fca319f038f78b..2379e1136f0242 100644 --- a/components/dashboard/src/admin/UserDetail.tsx +++ b/components/dashboard/src/admin/UserDetail.tsx @@ -22,10 +22,13 @@ import { getGitpodService } from "../service/service"; import { WorkspaceSearch } from "./WorkspacesSearch"; import Property from "./Property"; import { PageWithAdminSubMenu } from "./PageWithAdminSubMenu"; +import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; +import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; export default function UserDetail(p: { user: User }) { const [activity, setActivity] = useState(false); const [user, setUser] = useState(p.user); + const [billingMode, setBillingMode] = useState(undefined); const [accountStatement, setAccountStatement] = useState(); const [isStudent, setIsStudent] = useState(); const [editFeatureFlags, setEditFeatureFlags] = useState(false); @@ -46,6 +49,9 @@ export default function UserDetail(p: { user: User }) { getGitpodService() .server.adminIsStudent(p.user.id) .then((isStud) => setIsStudent(isStud)); + getGitpodService() + .server.adminGetBillingMode(AttributionId.render({ kind: "user", userId: p.user.id })) + .then((bm) => setBillingMode(bm)); }, [p.user]); const email = User.getPrimaryEmail(p.user); @@ -120,6 +126,101 @@ export default function UserDetail(p: { user: User }) { } }; + function renderUserBillingProperties(): JSX.Element { + if (billingMode?.mode === "none") { + return <>; // nothing to show here atm + } + + const properties: JSX.Element[] = [ + + {isStudent === undefined ? "---" : isStudent ? "Enabled" : "Disabled"} + , + {billingMode?.mode || "---"}, + ]; + + switch (billingMode?.mode) { + case "chargebee": + properties.push( + downloadAccountStatement(), + }, + { + label: "Grant 20 Extra Hours", + onClick: async () => { + await getGitpodService().server.adminGrantExtraHours(user.id, 20); + setAccountStatement( + await getGitpodService().server.adminGetAccountStatement(user.id), + ); + }, + }, + ] + } + > + {accountStatement?.remainingHours ? accountStatement?.remainingHours.toString() : "---"} + , + ); + properties.push( + { + await getGitpodService().server.adminSetProfessionalOpenSource( + user.id, + !isProfessionalOpenSource, + ); + setAccountStatement( + await getGitpodService().server.adminGetAccountStatement(user.id), + ); + }, + }, + ] + } + > + {accountStatement?.subscriptions + ? accountStatement.subscriptions + .filter((s) => !s.deleted && Subscription.isActive(s, new Date().toISOString())) + .map((s) => Plans.getById(s.planId)?.name) + .join(", ") + : "---"} + , + ); + break; + case "usage-based": + // TODO(gpl) Add info about Stripe plan, etc. + break; + default: + break; + } + + // Split properties into rows of 3 + const rows: JSX.Element[] = []; + while (properties.length > 0) { + const row = properties.splice(0, 3); + rows.push(
{row}
); + } + return <>{rows}; + } + return ( <> @@ -157,59 +258,6 @@ export default function UserDetail(p: { user: User }) {
{moment(user.creationDate).format("MMM D, YYYY")} - downloadAccountStatement(), - }, - { - label: "Grant 20 Extra Hours", - onClick: async () => { - await getGitpodService().server.adminGrantExtraHours(user.id, 20); - setAccountStatement( - await getGitpodService().server.adminGetAccountStatement(user.id), - ); - }, - }, - ] - } - > - {accountStatement?.remainingHours ? accountStatement?.remainingHours.toString() : "---"} - - { - await getGitpodService().server.adminSetProfessionalOpenSource( - user.id, - !isProfessionalOpenSource, - ); - setAccountStatement( - await getGitpodService().server.adminGetAccountStatement(user.id), - ); - }, - }, - ] - } - > - {accountStatement?.subscriptions - ? accountStatement.subscriptions - .filter( - (s) => !s.deleted && Subscription.isActive(s, new Date().toISOString()), - ) - .map((s) => Plans.getById(s.planId)?.name) - .join(", ") - : "---"} - -
-
{user.rolesOrPermissions?.join(", ") || "---"} - - {isStudent === undefined ? "---" : isStudent ? "Enabled" : "Disabled"} -
+ {renderUserBillingProperties()}
diff --git a/components/gitpod-protocol/src/admin-protocol.ts b/components/gitpod-protocol/src/admin-protocol.ts index ded57da5e37a9e..f0bee69db3cba0 100644 --- a/components/gitpod-protocol/src/admin-protocol.ts +++ b/components/gitpod-protocol/src/admin-protocol.ts @@ -12,6 +12,7 @@ import { WorkspaceInstance, WorkspaceInstancePhase } from "./workspace-instance" import { RoleOrPermission } from "./permission"; import { AccountStatement } from "./accounting-protocol"; import { InstallationAdminSettings } from "./installation-admin-protocol"; +import { BillingMode } from "./billing-mode"; export interface AdminServer { adminGetUsers(req: AdminGetListRequest): Promise>; @@ -49,6 +50,7 @@ export interface AdminServer { adminIsStudent(userId: string): Promise; adminAddStudentEmailDomain(userId: string, domain: string): Promise; adminGrantExtraHours(userId: string, extraHours: number): Promise; + adminGetBillingMode(attributionId: string): Promise; adminGetSettings(): Promise; adminUpdateSettings(settings: InstallationAdminSettings): Promise; diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 4eee094bf9906b..c1a89c9e6d15c7 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -2374,6 +2374,21 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.subscriptionService.addCredit(userId, extraHours, new Date().toISOString()); } + async adminGetBillingMode(ctx: TraceContextWithSpan, attributionId: string): Promise { + traceAPIParams(ctx, { attributionId }); + + const user = this.checkAndBlockUser("adminGetBillingMode"); + if (!this.authorizationService.hasPermission(user, Permission.ADMIN_USERS)) { + throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed"); + } + + const parsedAttributionId = AttributionId.parse(attributionId); + if (!parsedAttributionId) { + throw new ResponseError(ErrorCodes.BAD_REQUEST, "Unable to parse attributionId"); + } + return this.billingModes.getBillingMode(parsedAttributionId, new Date()); + } + // various async sendFeedback(ctx: TraceContext, feedback: string): Promise { traceAPIParams(ctx, {}); // feedback is not interesting here, any may contain names diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 4c093d99863ac9..9fc877dea509cd 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -163,6 +163,7 @@ const defaultFunctions: FunctionsConfig = { adminGetBlockedRepositories: { group: "default", points: 1 }, adminCreateBlockedRepository: { group: "default", points: 1 }, adminDeleteBlockedRepository: { group: "default", points: 1 }, + adminGetBillingMode: { group: "default", points: 1 }, validateLicense: { group: "default", points: 1 }, getLicenseInfo: { group: "default", points: 1 }, diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 18cf8fe6ea54ac..385b185431251c 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -3098,6 +3098,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { async adminGrantExtraHours(ctx: TraceContext, userId: string, extraHours: number): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } + async adminGetBillingMode(ctx: TraceContextWithSpan, attributionId: string): Promise { + throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); + } async isStudent(ctx: TraceContext): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); }