Skip to content

Commit

Permalink
Spending Limit Reached modal 🛹
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexTugarev authored and roboquat committed Aug 5, 2022
1 parent 225344b commit bf7f1c0
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 11 deletions.
46 changes: 46 additions & 0 deletions components/dashboard/src/start/CreateWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -199,6 +202,11 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
/>
);
break;
case ErrorCodes.PAYMENT_SPENDING_LIMIT_REACHED:
error = undefined; // to hide the error (otherwise rendered behind the modal)
phase = StartPhase.Stopped;
statusMessage = <SpendingLimitReachedModal hints={this.state?.error?.data} />;
break;
default:
statusMessage = (
<p className="text-base text-gitpod-red w-96">
Expand Down Expand Up @@ -358,6 +366,44 @@ function LimitReachedOutOfHours() {
</LimitReachedModal>
);
}
function SpendingLimitReachedModal(p: { hints: any }) {
const { teams } = useContext(TeamsContext);
// const [attributionId, setAttributionId] = useState<AttributionId | undefined>();
const [attributedTeam, setAttributedTeam] = useState<Team | undefined>();

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 (
<Modal visible={true} closeable={false} onClose={() => {}}>
<h3 className="flex">
<span className="flex-grow">Spending Limit Reached</span>
</h3>
<div className="border-t border-b border-gray-200 dark:border-gray-800 mt-4 -mx-6 px-6 py-2">
<p className="mt-1 mb-2 text-base dark:text-gray-400">Please increase the spending limit and retry.</p>
</div>
<div className="flex justify-end mt-6 space-x-2">
<a href={gitpodHostUrl.with({ pathname: "billing" }).toString()}>
<button>Billing Settings</button>
</a>
{attributedTeam && (
<a href={gitpodHostUrl.with({ pathname: `t/${attributedTeam?.slug}/billing` }).toString()}>
<button>Team Billing</button>
</a>
)}
</div>
</Modal>
);
}

function RepositoryNotFoundView(p: { error: StartWorkspaceError }) {
const [statusMessage, setStatusMessage] = useState<React.ReactNode>();
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/start/StartWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
try {
const desktopLink = new URL(openLink);
redirect =
desktopLink.protocol != "http:" && desktopLink.protocol != "https:";
desktopLink.protocol !== "http:" && desktopLink.protocol !== "https:";
} catch {}
if (redirect) {
window.location.href = openLink;
Expand Down
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/messaging/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export namespace ErrorCodes {
// 450 Payment error
export const PAYMENT_ERROR = 450;

// 451 Out of credits
export const PAYMENT_SPENDING_LIMIT_REACHED = 451;

// 455 Invalid cost center (custom status code)
export const INVALID_COST_CENTER = 455;

Expand Down
46 changes: 36 additions & 10 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,32 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
): Promise<void> {
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(
Expand Down Expand Up @@ -1926,16 +1952,16 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
return subscription;
}

protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
protected async isUsageBasedFeatureFlagEnabled(user: User): Promise<boolean> {
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<void> {
const isUsageBasedBillingEnabled = await this.isUsageBasedFeatureFlagEnabled(user);
if (!isUsageBasedBillingEnabled) {
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed");
}
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit bf7f1c0

Please sign in to comment.