Skip to content

Commit 332f0d4

Browse files
committed
Spending Limit Reached modal 🛹
1 parent 51189bd commit 332f0d4

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";
@@ -25,6 +26,8 @@ import PrebuildLogs from "../components/PrebuildLogs";
2526
import CodeText from "../components/CodeText";
2627
import FeedbackComponent from "../feedback-form/FeedbackComponent";
2728
import { isGitpodIo } from "../utils";
29+
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
30+
import { TeamsContext } from "../teams/teams-context";
2831

2932
export interface CreateWorkspaceProps {
3033
contextUrl: string;
@@ -185,6 +188,11 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
185188
phase = StartPhase.Stopped;
186189
statusMessage = <LimitReachedOutOfHours />;
187190
break;
191+
case ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED:
192+
error = undefined; // to hide the error (otherwise rendered behind the modal)
193+
phase = StartPhase.Stopped;
194+
statusMessage = <SpendingLimitReachedModal hints={this.state?.error?.data} />;
195+
break;
188196
default:
189197
statusMessage = (
190198
<p className="text-base text-gitpod-red w-96">
@@ -335,6 +343,44 @@ function LimitReachedOutOfHours() {
335343
</LimitReachedModal>
336344
);
337345
}
346+
function SpendingLimitReachedModal(p: { hints: any }) {
347+
const { teams } = useContext(TeamsContext);
348+
// const [attributionId, setAttributionId] = useState<AttributionId | undefined>();
349+
const [attributedTeam, setAttributedTeam] = useState<Team | undefined>();
350+
351+
useEffect(() => {
352+
const attributionId: AttributionId | undefined =
353+
p.hints && p.hints.attributionId && AttributionId.parse(p.hints.attributionId);
354+
if (attributionId) {
355+
// setAttributionId(attributionId);
356+
if (attributionId.kind === "team") {
357+
const team = teams?.find((t) => t.id === attributionId.teamId);
358+
setAttributedTeam(team);
359+
}
360+
}
361+
}, []);
362+
363+
return (
364+
<Modal visible={true} closeable={false} onClose={() => {}}>
365+
<h3 className="flex">
366+
<span className="flex-grow">Spending Limit Reached</span>
367+
</h3>
368+
<div className="border-t border-b border-gray-200 dark:border-gray-800 mt-4 -mx-6 px-6 py-2">
369+
<p className="mt-1 mb-2 text-base dark:text-gray-400">Please increase the spending limit and retry.</p>
370+
</div>
371+
<div className="flex justify-end mt-6 space-x-2">
372+
<a href={gitpodHostUrl.with({ pathname: "billing" }).toString()}>
373+
<button>Billing Settings</button>
374+
</a>
375+
{attributedTeam && (
376+
<a href={gitpodHostUrl.with({ pathname: `t/${attributedTeam?.slug}/billing` }).toString()}>
377+
<button>Team Billing</button>
378+
</a>
379+
)}
380+
</div>
381+
</Modal>
382+
);
383+
}
338384

339385
function RepositoryNotFoundView(p: { error: StartWorkspaceError }) {
340386
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
@@ -41,6 +41,9 @@ export namespace ErrorCodes {
4141
// 450 Payment error
4242
export const PAYMENT_ERROR = 450;
4343

44+
// 451 Out of credits
45+
export const PAYMENT_SPENDING_LIMIT_REACHED = 451;
46+
4447
// 470 User Blocked (custom status code)
4548
export const USER_BLOCKED = 470;
4649

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

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

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

1925-
protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
1951+
protected async isUsageBasedFeatureFlagEnabled(user: User): Promise<boolean> {
19261952
const teams = await this.teamDB.findTeamsByUser(user.id);
1927-
const isUsageBasedBillingEnabled = await getExperimentsClientForBackend().getValueAsync(
1928-
"isUsageBasedBillingEnabled",
1929-
false,
1930-
{
1931-
user,
1932-
teams: teams,
1933-
},
1934-
);
1953+
return await getExperimentsClientForBackend().getValueAsync("isUsageBasedBillingEnabled", false, {
1954+
user,
1955+
teams: teams,
1956+
});
1957+
}
1958+
1959+
protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
1960+
const isUsageBasedBillingEnabled = await this.isUsageBasedFeatureFlagEnabled(user);
19351961
if (!isUsageBasedBillingEnabled) {
19361962
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed");
19371963
}
@@ -2094,7 +2120,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
20942120
if (costCenter) {
20952121
if (totalUsage > costCenter.spendingLimit) {
20962122
result.unshift("The spending limit is reached.");
2097-
} else if (totalUsage > 0.8 * costCenter.spendingLimit * 0.8) {
2123+
} else if (totalUsage > costCenter.spendingLimit * 0.8) {
20982124
result.unshift("The spending limit is almost reached.");
20992125
}
21002126
} else {

0 commit comments

Comments
 (0)