Skip to content

Commit bf7f1c0

Browse files
AlexTugarevroboquat
authored andcommitted
Spending Limit Reached modal 🛹
1 parent 225344b commit bf7f1c0

File tree

4 files changed

+86
-11
lines changed

4 files changed

+86
-11
lines changed

components/dashboard/src/start/CreateWorkspace.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
RunningWorkspacePrebuildStarting,
1212
ContextURL,
1313
DisposableCollection,
14+
Team,
1415
} from "@gitpod/gitpod-protocol";
1516
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1617
import Modal from "../components/Modal";
@@ -26,6 +27,8 @@ import CodeText from "../components/CodeText";
2627
import FeedbackComponent from "../feedback-form/FeedbackComponent";
2728
import { isGitpodIo } from "../utils";
2829
import { BillingAccountSelector } from "../components/BillingAccountSelector";
30+
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
31+
import { TeamsContext } from "../teams/teams-context";
2932

3033
export interface CreateWorkspaceProps {
3134
contextUrl: string;
@@ -199,6 +202,11 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
199202
/>
200203
);
201204
break;
205+
case ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED:
206+
error = undefined; // to hide the error (otherwise rendered behind the modal)
207+
phase = StartPhase.Stopped;
208+
statusMessage = <SpendingLimitReachedModal hints={this.state?.error?.data} />;
209+
break;
202210
default:
203211
statusMessage = (
204212
<p className="text-base text-gitpod-red w-96">
@@ -358,6 +366,44 @@ function LimitReachedOutOfHours() {
358366
</LimitReachedModal>
359367
);
360368
}
369+
function SpendingLimitReachedModal(p: { hints: any }) {
370+
const { teams } = useContext(TeamsContext);
371+
// const [attributionId, setAttributionId] = useState<AttributionId | undefined>();
372+
const [attributedTeam, setAttributedTeam] = useState<Team | undefined>();
373+
374+
useEffect(() => {
375+
const attributionId: AttributionId | undefined =
376+
p.hints && p.hints.attributionId && AttributionId.parse(p.hints.attributionId);
377+
if (attributionId) {
378+
// setAttributionId(attributionId);
379+
if (attributionId.kind === "team") {
380+
const team = teams?.find((t) => t.id === attributionId.teamId);
381+
setAttributedTeam(team);
382+
}
383+
}
384+
}, []);
385+
386+
return (
387+
<Modal visible={true} closeable={false} onClose={() => {}}>
388+
<h3 className="flex">
389+
<span className="flex-grow">Spending Limit Reached</span>
390+
</h3>
391+
<div className="border-t border-b border-gray-200 dark:border-gray-800 mt-4 -mx-6 px-6 py-2">
392+
<p className="mt-1 mb-2 text-base dark:text-gray-400">Please increase the spending limit and retry.</p>
393+
</div>
394+
<div className="flex justify-end mt-6 space-x-2">
395+
<a href={gitpodHostUrl.with({ pathname: "billing" }).toString()}>
396+
<button>Billing Settings</button>
397+
</a>
398+
{attributedTeam && (
399+
<a href={gitpodHostUrl.with({ pathname: `t/${attributedTeam?.slug}/billing` }).toString()}>
400+
<button>Team Billing</button>
401+
</a>
402+
)}
403+
</div>
404+
</Modal>
405+
);
406+
}
361407

362408
function RepositoryNotFoundView(p: { error: StartWorkspaceError }) {
363409
const [statusMessage, setStatusMessage] = useState<React.ReactNode>();

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
528528
try {
529529
const desktopLink = new URL(openLink);
530530
redirect =
531-
desktopLink.protocol != "http:" && desktopLink.protocol != "https:";
531+
desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:";
532532
} catch {}
533533
if (redirect) {
534534
window.location.href = openLink;

components/gitpod-protocol/src/messaging/error.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export namespace ErrorCodes {
3535
// 450 Payment error
3636
export const PAYMENT_ERROR = 450;
3737

38+
// 451 Out of credits
39+
export const PAYMENT_SPENDING_LIMIT_REACHED = 451;
40+
3841
// 455 Invalid cost center (custom status code)
3942
export const INVALID_COST_CENTER = 455;
4043

components/server/ee/src/workspace/gitpod-server-impl.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,32 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
256256
): Promise<void> {
257257
await super.mayStartWorkspace(ctx, user, runningInstances);
258258

259+
// TODO(at) replace the naive implementation based on usage service
260+
// with a proper call check against the upcoming invoice.
261+
// For now this should just enable the work on fronend.
262+
if (await this.isUsageBasedFeatureFlagEnabled(user)) {
263+
// dummy implementation to test frontend bits
264+
const attributionId = await this.userService.getWorkspaceUsageAttributionId(user);
265+
const costCenter = !!attributionId && (await this.costCenterDB.findById(attributionId));
266+
if (costCenter) {
267+
const allSessions = await this.listBilledUsage(ctx, {
268+
attributionId,
269+
startedTimeOrder: SortOrder.Descending,
270+
});
271+
const totalUsage = allSessions.map((s) => s.credits).reduce((a, b) => a + b, 0);
272+
273+
if (totalUsage >= costCenter.spendingLimit) {
274+
throw new ResponseError(
275+
ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED,
276+
"Increase spending limit and try again.",
277+
{
278+
attributionId: user.usageAttributionId,
279+
},
280+
);
281+
}
282+
}
283+
}
284+
259285
const result = await this.entitlementService.mayStartWorkspace(user, new Date(), runningInstances);
260286
if (!result.enoughCredits) {
261287
throw new ResponseError(
@@ -1926,16 +1952,16 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
19261952
return subscription;
19271953
}
19281954

1929-
protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
1955+
protected async isUsageBasedFeatureFlagEnabled(user: User): Promise<boolean> {
19301956
const teams = await this.teamDB.findTeamsByUser(user.id);
1931-
const isUsageBasedBillingEnabled = await getExperimentsClientForBackend().getValueAsync(
1932-
"isUsageBasedBillingEnabled",
1933-
false,
1934-
{
1935-
user,
1936-
teams: teams,
1937-
},
1938-
);
1957+
return await getExperimentsClientForBackend().getValueAsync("isUsageBasedBillingEnabled", false, {
1958+
user,
1959+
teams: teams,
1960+
});
1961+
}
1962+
1963+
protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
1964+
const isUsageBasedBillingEnabled = await this.isUsageBasedFeatureFlagEnabled(user);
19391965
if (!isUsageBasedBillingEnabled) {
19401966
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed");
19411967
}
@@ -2084,7 +2110,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
20842110
if (costCenter) {
20852111
if (totalUsage > costCenter.spendingLimit) {
20862112
result.unshift("The spending limit is reached.");
2087-
} else if (totalUsage > 0.8 * costCenter.spendingLimit * 0.8) {
2113+
} else if (totalUsage > costCenter.spendingLimit * 0.8) {
20882114
result.unshift("The spending limit is almost reached.");
20892115
}
20902116
} else {

0 commit comments

Comments
 (0)