From bf7f1c0ed5c874d24691fe212bbfbb6e21fcc975 Mon Sep 17 00:00:00 2001 From: Alex Tugarev Date: Fri, 5 Aug 2022 10:44:52 +0000 Subject: [PATCH] =?UTF-8?q?Spending=20Limit=20Reached=20modal=20?= =?UTF-8?q?=F0=9F=9B=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/src/start/CreateWorkspace.tsx | 46 +++++++++++++++++++ .../dashboard/src/start/StartWorkspace.tsx | 2 +- .../gitpod-protocol/src/messaging/error.ts | 3 ++ .../ee/src/workspace/gitpod-server-impl.ts | 46 +++++++++++++++---- 4 files changed, 86 insertions(+), 11 deletions(-) diff --git a/components/dashboard/src/start/CreateWorkspace.tsx b/components/dashboard/src/start/CreateWorkspace.tsx index 26d775aacd8adf..b645df3e8238d6 100644 --- a/components/dashboard/src/start/CreateWorkspace.tsx +++ b/components/dashboard/src/start/CreateWorkspace.tsx @@ -11,6 +11,7 @@ import { RunningWorkspacePrebuildStarting, ContextURL, DisposableCollection, + Team, } from "@gitpod/gitpod-protocol"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import Modal from "../components/Modal"; @@ -26,6 +27,8 @@ import CodeText from "../components/CodeText"; import FeedbackComponent from "../feedback-form/FeedbackComponent"; import { isGitpodIo } from "../utils"; import { BillingAccountSelector } from "../components/BillingAccountSelector"; +import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; +import { TeamsContext } from "../teams/teams-context"; export interface CreateWorkspaceProps { contextUrl: string; @@ -199,6 +202,11 @@ export default class CreateWorkspace extends React.Component ); break; + case ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED: + error = undefined; // to hide the error (otherwise rendered behind the modal) + phase = StartPhase.Stopped; + statusMessage = ; + break; default: statusMessage = (

@@ -358,6 +366,44 @@ function LimitReachedOutOfHours() { ); } +function SpendingLimitReachedModal(p: { hints: any }) { + const { teams } = useContext(TeamsContext); + // const [attributionId, setAttributionId] = useState(); + const [attributedTeam, setAttributedTeam] = useState(); + + useEffect(() => { + const attributionId: AttributionId | undefined = + p.hints && p.hints.attributionId && AttributionId.parse(p.hints.attributionId); + if (attributionId) { + // setAttributionId(attributionId); + if (attributionId.kind === "team") { + const team = teams?.find((t) => t.id === attributionId.teamId); + setAttributedTeam(team); + } + } + }, []); + + return ( + {}}> +

+ Spending Limit Reached +

+
+

Please increase the spending limit and retry.

+
+
+ + + + {attributedTeam && ( + + + + )} +
+ + ); +} function RepositoryNotFoundView(p: { error: StartWorkspaceError }) { const [statusMessage, setStatusMessage] = useState(); diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx index 7915b7be1001a6..ab3161c661c35b 100644 --- a/components/dashboard/src/start/StartWorkspace.tsx +++ b/components/dashboard/src/start/StartWorkspace.tsx @@ -528,7 +528,7 @@ export default class StartWorkspace extends React.Component { await super.mayStartWorkspace(ctx, user, runningInstances); + // TODO(at) replace the naive implementation based on usage service + // with a proper call check against the upcoming invoice. + // For now this should just enable the work on fronend. + if (await this.isUsageBasedFeatureFlagEnabled(user)) { + // dummy implementation to test frontend bits + const attributionId = await this.userService.getWorkspaceUsageAttributionId(user); + const costCenter = !!attributionId && (await this.costCenterDB.findById(attributionId)); + if (costCenter) { + const allSessions = await this.listBilledUsage(ctx, { + attributionId, + startedTimeOrder: SortOrder.Descending, + }); + const totalUsage = allSessions.map((s) => s.credits).reduce((a, b) => a + b, 0); + + if (totalUsage >= costCenter.spendingLimit) { + throw new ResponseError( + ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED, + "Increase spending limit and try again.", + { + attributionId: user.usageAttributionId, + }, + ); + } + } + } + const result = await this.entitlementService.mayStartWorkspace(user, new Date(), runningInstances); if (!result.enoughCredits) { throw new ResponseError( @@ -1926,16 +1952,16 @@ export class GitpodServerEEImpl extends GitpodServerImpl { return subscription; } - protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise { + protected async isUsageBasedFeatureFlagEnabled(user: User): Promise { const teams = await this.teamDB.findTeamsByUser(user.id); - const isUsageBasedBillingEnabled = await getExperimentsClientForBackend().getValueAsync( - "isUsageBasedBillingEnabled", - false, - { - user, - teams: teams, - }, - ); + return await getExperimentsClientForBackend().getValueAsync("isUsageBasedBillingEnabled", false, { + user, + teams: teams, + }); + } + + protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise { + const isUsageBasedBillingEnabled = await this.isUsageBasedFeatureFlagEnabled(user); if (!isUsageBasedBillingEnabled) { throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed"); } @@ -2084,7 +2110,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { if (costCenter) { if (totalUsage > costCenter.spendingLimit) { result.unshift("The spending limit is reached."); - } else if (totalUsage > 0.8 * costCenter.spendingLimit * 0.8) { + } else if (totalUsage > costCenter.spendingLimit * 0.8) { result.unshift("The spending limit is almost reached."); } } else {