Skip to content

Commit 9608282

Browse files
committed
The purchase form styling and functionality (minus actually purchasing)
1 parent 3b7314c commit 9608282

File tree

6 files changed

+156
-21
lines changed

6 files changed

+156
-21
lines changed

apps/webapp/app/components/primitives/InputNumberStepper.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function InputNumberStepper({
2424
readOnly = false,
2525
className,
2626
placeholder = "Type a number",
27+
...props
2728
}: InputNumberStepperProps) {
2829
const inputRef = useRef<HTMLInputElement>(null);
2930

@@ -176,6 +177,7 @@ export function InputNumberStepper({
176177
// Hide number input arrows
177178
"[type=number]:border-0 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
178179
)}
180+
{...props}
179181
/>
180182

181183
<div className={cn("flex items-center", size.gap)}>

apps/webapp/app/presenters/v3/ManageConcurrencyPresenter.server.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { type RuntimeEnvironmentType } from "@trigger.dev/database";
2-
import { getCurrentPlan, getDefaultEnvironmentLimitFromPlan } from "~/services/platform.v3.server";
2+
import {
3+
getCurrentPlan,
4+
getDefaultEnvironmentLimitFromPlan,
5+
getPlans,
6+
} from "~/services/platform.v3.server";
37
import { BasePresenter } from "./basePresenter.server";
48
import { sortEnvironments } from "~/utils/environmentSort";
59

@@ -9,6 +13,10 @@ export type ConcurrencyResult = {
913
extraConcurrency: number;
1014
extraAllocatedConcurrency: number;
1115
extraUnallocatedConcurrency: number;
16+
concurrencyPricing: {
17+
stepSize: number;
18+
centsPerStep: number;
19+
};
1220
};
1321

1422
export type EnvironmentWithConcurrency = {
@@ -97,12 +105,18 @@ export class ManageConcurrencyPresenter extends BasePresenter {
97105

98106
const extraAllocated = Math.min(extraConcurrency, extraAllocatedConcurrency);
99107

108+
const plans = await getPlans();
109+
if (!plans) {
110+
throw new Error("Couldn't retrieve add on pricing");
111+
}
112+
100113
return {
101114
canAddConcurrency,
102115
extraConcurrency,
103116
extraAllocatedConcurrency: extraAllocated,
104117
extraUnallocatedConcurrency: extraConcurrency - extraAllocated,
105118
environments: sortEnvironments(projectEnvironments).reverse(),
119+
concurrencyPricing: plans.addOnPricing.concurrency,
106120
};
107121
}
108122
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { PlusIcon } from "@heroicons/react/20/solid";
2-
import { type MetaFunction } from "@remix-run/react";
2+
import { Form, type MetaFunction } from "@remix-run/react";
33
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
44
import { tryCatch } from "@trigger.dev/core";
5+
import { useState } from "react";
56
import { typedjson, useTypedLoaderData } from "remix-typedjson";
67
import { z } from "zod";
78
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
@@ -12,7 +13,19 @@ import {
1213
PageContainer,
1314
} from "~/components/layout/AppLayout";
1415
import { Button, LinkButton } from "~/components/primitives/Buttons";
16+
import {
17+
Dialog,
18+
DialogContent,
19+
DialogFooter,
20+
DialogHeader,
21+
DialogTrigger,
22+
} from "~/components/primitives/Dialog";
23+
import { Fieldset } from "~/components/primitives/Fieldset";
1524
import { Header2, Header3 } from "~/components/primitives/Headers";
25+
import { Input } from "~/components/primitives/Input";
26+
import { InputGroup } from "~/components/primitives/InputGroup";
27+
import { InputNumberStepper } from "~/components/primitives/InputNumberStepper";
28+
import { Label } from "~/components/primitives/Label";
1629
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
1730
import { Paragraph } from "~/components/primitives/Paragraph";
1831
import * as Property from "~/components/primitives/PropertyTable";
@@ -34,7 +47,9 @@ import {
3447
type EnvironmentWithConcurrency,
3548
ManageConcurrencyPresenter,
3649
} from "~/presenters/v3/ManageConcurrencyPresenter.server";
50+
import { getPlans } from "~/services/platform.v3.server";
3751
import { requireUser, requireUserId } from "~/services/session.server";
52+
import { formatCurrency } from "~/utils/numberFormatter";
3853
import { EnvironmentParamSchema, regionsPath, v3BillingPath } from "~/utils/pathBuilder";
3954
import { SetDefaultRegionService } from "~/v3/services/setDefaultRegion.server";
4055
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
@@ -75,6 +90,11 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7590
});
7691
}
7792

93+
const plans = await tryCatch(getPlans());
94+
if (!plans) {
95+
throw new Response(null, { status: 404, statusText: "Plans not found" });
96+
}
97+
7898
return typedjson(result);
7999
};
80100

@@ -128,6 +148,7 @@ export default function Page() {
128148
extraAllocatedConcurrency,
129149
extraUnallocatedConcurrency,
130150
environments,
151+
concurrencyPricing,
131152
} = useTypedLoaderData<typeof loader>();
132153

133154
return (
@@ -159,6 +180,7 @@ export default function Page() {
159180
extraAllocatedConcurrency={extraAllocatedConcurrency}
160181
extraUnallocatedConcurrency={extraUnallocatedConcurrency}
161182
environments={environments}
183+
concurrencyPricing={concurrencyPricing}
162184
/>
163185
) : (
164186
<NotUpgradable environments={environments} />
@@ -175,6 +197,7 @@ function Upgradable({
175197
extraAllocatedConcurrency,
176198
extraUnallocatedConcurrency,
177199
environments,
200+
concurrencyPricing,
178201
}: ConcurrencyResult) {
179202
const organization = useOrganization();
180203

@@ -192,20 +215,16 @@ function Upgradable({
192215
<div className="flex flex-col gap-2">
193216
<div className="flex items-center first-letter:pb-1">
194217
<Header3 className="grow">Extra concurrency</Header3>
195-
<Button variant="primary/small" LeadingIcon={PlusIcon}>
196-
Purchase extra concurrency...
197-
</Button>
218+
<PurchaseConcurrencyModal concurrencyPricing={concurrencyPricing} />
198219
</div>
199220
<Table>
200-
<TableHeader>
221+
<TableBody>
201222
<TableRow>
202-
<TableHeaderCell className="pl-0">Extra concurrency purchased</TableHeaderCell>
203-
<TableHeaderCell alignment="right" className="text-text-bright">
223+
<TableCell className="pl-0 text-text-bright">Extra concurrency purchased</TableCell>
224+
<TableCell alignment="right" className="text-text-bright">
204225
{extraConcurrency}
205-
</TableHeaderCell>
226+
</TableCell>
206227
</TableRow>
207-
</TableHeader>
208-
<TableBody>
209228
<TableRow>
210229
<TableCell>Allocated concurrency</TableCell>
211230
<TableCell alignment="right" className="text-text-bright">
@@ -233,7 +252,7 @@ function Upgradable({
233252
<TableRow>
234253
<TableHeaderCell className="pl-0">Environment</TableHeaderCell>
235254
<TableHeaderCell alignment="right">
236-
<span className="flex items-center gap-x-1">
255+
<span className="flex items-center justify-end gap-x-1">
237256
Included{" "}
238257
<InfoIconTooltip content="This is the included concurrency based on your plan." />
239258
</span>
@@ -250,10 +269,20 @@ function Upgradable({
250269
</TableCell>
251270
<TableCell alignment="right">{environment.planConcurrencyLimit}</TableCell>
252271
<TableCell alignment="right" className="text-text-bright">
253-
{Math.max(
254-
0,
255-
environment.maximumConcurrencyLimit - environment.planConcurrencyLimit
256-
)}
272+
<div className="flex items-center justify-end">
273+
<Input
274+
type="number"
275+
variant="secondary-small"
276+
className="text-right"
277+
containerClassName="w-16 bg-transparent"
278+
fullWidth={false}
279+
defaultValue={Math.max(
280+
0,
281+
environment.maximumConcurrencyLimit - environment.planConcurrencyLimit
282+
)}
283+
min="0"
284+
/>
285+
</div>
257286
</TableCell>
258287
<TableCell alignment="right">{environment.maximumConcurrencyLimit}</TableCell>
259288
</TableRow>
@@ -311,3 +340,77 @@ function NotUpgradable({ environments }: { environments: EnvironmentWithConcurre
311340
</div>
312341
);
313342
}
343+
344+
function PurchaseConcurrencyModal({
345+
concurrencyPricing,
346+
}: {
347+
concurrencyPricing: {
348+
stepSize: number;
349+
centsPerStep: number;
350+
};
351+
}) {
352+
const [amount, setAmount] = useState(0);
353+
354+
return (
355+
<Dialog>
356+
<DialogTrigger asChild>
357+
<Button variant="primary/small" LeadingIcon={PlusIcon}>
358+
Purchase extra concurrency
359+
</Button>
360+
</DialogTrigger>
361+
<DialogContent>
362+
<DialogHeader>Purchase extra concurrency</DialogHeader>
363+
<div className="flex flex-col gap-4 pt-2">
364+
<Paragraph variant="base/bright" spacing>
365+
You can purchase bundles of {concurrencyPricing.stepSize} concurrency for
366+
{formatCurrency(concurrencyPricing.centsPerStep / 100, false)}/month. You’ll be billed
367+
monthly, with changes available after a full billing cycle.
368+
</Paragraph>
369+
<Form method="post">
370+
<Fieldset>
371+
<InputGroup fullWidth>
372+
<Label htmlFor="amount" className="text-text-dimmed">
373+
Extra concurrency to purchase
374+
</Label>
375+
<InputNumberStepper
376+
type="number"
377+
id="amount"
378+
name="amount"
379+
step={concurrencyPricing.stepSize}
380+
min={0}
381+
value={amount}
382+
onChange={(e) => setAmount(Number(e.target.value))}
383+
/>
384+
</InputGroup>
385+
</Fieldset>
386+
</Form>
387+
<div className="flex flex-col">
388+
<div className="grid grid-cols-2 border-b border-grid-dimmed pb-1">
389+
<Header3 className="font-normal text-text-dimmed">Summary</Header3>
390+
<Header3 className="justify-self-end font-normal text-text-dimmed">Total</Header3>
391+
</div>
392+
<div className="grid grid-cols-2 pt-2">
393+
<Header3 className="pb-0 font-normal">{amount}</Header3>
394+
<Header3 className="justify-self-end font-normal">
395+
{formatCurrency(
396+
(amount * concurrencyPricing.centsPerStep) / concurrencyPricing.stepSize / 100,
397+
false
398+
)}
399+
</Header3>
400+
</div>
401+
<div className="grid grid-cols-2 text-xs">
402+
<span className="text-text-dimmed">
403+
({amount / concurrencyPricing.stepSize} bundles @{" "}
404+
{formatCurrency(concurrencyPricing.centsPerStep / 100, false)}/mth)
405+
</span>
406+
<span className="justify-self-end text-text-dimmed">/mth</span>
407+
</div>
408+
</div>
409+
</div>
410+
<DialogFooter>
411+
<Button variant="primary/small">Purchase</Button>
412+
</DialogFooter>
413+
</DialogContent>
414+
</Dialog>
415+
);
416+
}

apps/webapp/app/services/platform.v3.server.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,22 @@ export async function setPlan(
403403
}
404404
}
405405

406+
export async function setConcurrencyAddOn(organizationId: string, amount: number) {
407+
if (!client) return undefined;
408+
409+
try {
410+
const result = await client.setAddOn(organizationId, { type: "concurrency", amount });
411+
if (!result.success) {
412+
logger.error("Error setting concurrency add on - no success", { error: result.error });
413+
return undefined;
414+
}
415+
return result;
416+
} catch (e) {
417+
logger.error("Error setting concurrency add on - caught error", { error: e });
418+
return undefined;
419+
}
420+
}
421+
406422
export async function getUsage(organizationId: string, { from, to }: { from: Date; to: Date }) {
407423
if (!client) return undefined;
408424

apps/webapp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"@trigger.dev/core": "workspace:*",
115115
"@trigger.dev/database": "workspace:*",
116116
"@trigger.dev/otlp-importer": "workspace:*",
117-
"@trigger.dev/platform": "1.0.20-beta.0",
117+
"@trigger.dev/platform": "1.0.20-beta.2",
118118
"@trigger.dev/redis-worker": "workspace:*",
119119
"@trigger.dev/sdk": "workspace:*",
120120
"@types/pg": "8.6.6",

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)