@@ -53,6 +53,7 @@ import { concurrencyPath, EnvironmentParamSchema, v3BillingPath } from "~/utils/
5353import { SetConcurrencyAddOnService } from "~/v3/services/setConcurrencyAddOn.server" ;
5454import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route" ;
5555import { SpinnerWhite } from "~/components/primitives/Spinner" ;
56+ import { cn } from "~/utils/cn" ;
5657
5758export const meta : MetaFunction = ( ) => {
5859 return [
@@ -235,6 +236,7 @@ function Upgradable({
235236 < PurchaseConcurrencyModal
236237 concurrencyPricing = { concurrencyPricing }
237238 extraConcurrency = { extraConcurrency }
239+ extraUnallocatedConcurrency = { extraUnallocatedConcurrency }
238240 maxQuota = { maxQuota }
239241 />
240242 </ div >
@@ -365,13 +367,15 @@ function NotUpgradable({ environments }: { environments: EnvironmentWithConcurre
365367function PurchaseConcurrencyModal ( {
366368 concurrencyPricing,
367369 extraConcurrency,
370+ extraUnallocatedConcurrency,
368371 maxQuota,
369372} : {
370373 concurrencyPricing : {
371374 stepSize : number ;
372375 centsPerStep : number ;
373376 } ;
374377 extraConcurrency : number ;
378+ extraUnallocatedConcurrency : number ;
375379 maxQuota : number ;
376380} ) {
377381 const lastSubmission = useActionData ( ) ;
@@ -385,33 +389,40 @@ function PurchaseConcurrencyModal({
385389 shouldRevalidate : "onSubmit" ,
386390 } ) ;
387391
388- const [ amountValue , setAmountValue ] = useState ( 0 ) ;
392+ const [ amountValue , setAmountValue ] = useState ( extraConcurrency ) ;
389393 const navigation = useNavigation ( ) ;
390394 const isLoading = navigation . state !== "idle" && navigation . formMethod === "POST" ;
391395
392- const maximum = maxQuota - extraConcurrency ;
393- const isAboveMaxQuota = amountValue > maximum ;
396+ const state = updateState ( {
397+ value : amountValue ,
398+ existingValue : extraConcurrency ,
399+ quota : maxQuota ,
400+ extraUnallocatedConcurrency,
401+ } ) ;
402+ const changeClassName =
403+ state === "decrease" ? "text-error" : state === "increase" ? "text-success" : undefined ;
404+
405+ const title = extraConcurrency === 0 ? "Purchase extra concurrency" : "Add/remove concurrency" ;
394406
395407 return (
396408 < Dialog >
397409 < DialogTrigger asChild >
398- < Button variant = "primary/small" LeadingIcon = { PlusIcon } >
399- Purchase extra concurrency
400- </ Button >
410+ < Button variant = "primary/small" > { title } </ Button >
401411 </ DialogTrigger >
402412 < DialogContent >
403- < DialogHeader > Purchase extra concurrency </ DialogHeader >
413+ < DialogHeader > { title } </ DialogHeader >
404414 < Form method = "post" { ...form . props } >
405415 < div className = "flex flex-col gap-4 pt-2" >
406416 < Paragraph variant = "base/bright" spacing >
407417 You can purchase bundles of { concurrencyPricing . stepSize } concurrency for{ " " }
408- { formatCurrency ( concurrencyPricing . centsPerStep / 100 , false ) } /month. You’ll be billed
409- monthly, with changes available after a full billing cycle.
418+ { formatCurrency ( concurrencyPricing . centsPerStep / 100 , false ) } /month. Or you can
419+ remove any extra concurrency after you have unallocated it from your environments
420+ first.
410421 </ Paragraph >
411422 < Fieldset >
412423 < InputGroup fullWidth >
413424 < Label htmlFor = "amount" className = "text-text-dimmed" >
414- Extra concurrency to purchase
425+ Total extra concurrency
415426 </ Label >
416427 < InputNumberStepper
417428 { ...conform . input ( amount , { type : "number" } ) }
@@ -425,12 +436,20 @@ function PurchaseConcurrencyModal({
425436 < FormError > { form . error } </ FormError >
426437 </ InputGroup >
427438 </ Fieldset >
428- { isAboveMaxQuota ? (
439+ { state === "need_to_increase_unallocated" ? (
440+ < div className = "flex flex-col pb-3" >
441+ < Paragraph variant = "small" className = "text-warning" spacing >
442+ You need to unallocate{ " " }
443+ { formatNumber ( extraConcurrency - amountValue - extraUnallocatedConcurrency ) } more
444+ concurrency from your environments in order to remove{ " " }
445+ { formatNumber ( extraConcurrency - amountValue ) } concurrency from your account.
446+ </ Paragraph >
447+ </ div >
448+ ) : state === "above_quota" ? (
429449 < div className = "flex flex-col pb-3" >
430450 < Paragraph variant = "small" className = "text-warning" spacing >
431- Currently you can only have up to { maxQuota } extra concurrency. This request for{ " " }
432- { formatNumber ( amountValue ) } takes you to{ " " }
433- { formatNumber ( extraConcurrency + amountValue ) } extra concurrency.
451+ Currently you can only have up to { maxQuota } extra concurrency. This is a request
452+ for { formatNumber ( amountValue ) } .
434453 </ Paragraph >
435454 < Paragraph variant = "small" className = "text-warning" >
436455 Send a request below to lift your current limit. We'll get back to you soon.
@@ -439,14 +458,16 @@ function PurchaseConcurrencyModal({
439458 ) : (
440459 < div className = "flex flex-col pb-3" >
441460 < div className = "grid grid-cols-2 border-b border-grid-dimmed pb-1" >
442- < Header3 className = "font-normal text-text-dimmed" > Summary </ Header3 >
443- < Header3 className = "justify-self-end font-normal text-text-dimmed" > Total </ Header3 >
461+ < Header3 className = "font-normal text-text-dimmed" > Purchase </ Header3 >
462+ < Header3 className = "justify-self-end font-normal text-text-dimmed" > Cost </ Header3 >
444463 </ div >
445464 < div className = "grid grid-cols-2 pt-2" >
446- < Header3 className = "pb-0 font-normal" > { amountValue } </ Header3 >
447- < Header3 className = "justify-self-end font-normal" >
465+ < Header3 className = { cn ( "pb-0 font-normal" , changeClassName ) } >
466+ { formatNumber ( amountValue - extraConcurrency ) }
467+ </ Header3 >
468+ < Header3 className = { cn ( "justify-self-end font-normal" , changeClassName ) } >
448469 { formatCurrency (
449- ( amountValue * concurrencyPricing . centsPerStep ) /
470+ ( ( amountValue - extraConcurrency ) * concurrencyPricing . centsPerStep ) /
450471 concurrencyPricing . stepSize /
451472 100 ,
452473 false
@@ -455,7 +476,7 @@ function PurchaseConcurrencyModal({
455476 </ div >
456477 < div className = "grid grid-cols-2 text-xs" >
457478 < span className = "text-text-dimmed" >
458- ({ amountValue / concurrencyPricing . stepSize } bundles @{ " " }
479+ ({ ( amountValue - extraConcurrency ) / concurrencyPricing . stepSize } bundles @{ " " }
459480 { formatCurrency ( concurrencyPricing . centsPerStep / 100 , false ) } /mth)
460481 </ span >
461482 < span className = "justify-self-end text-text-dimmed" > /mth</ span >
@@ -465,7 +486,7 @@ function PurchaseConcurrencyModal({
465486 </ div >
466487 < FormButtons
467488 confirmButton = {
468- isAboveMaxQuota ? (
489+ state === "above_quota" ? (
469490 < >
470491 < input type = "hidden" name = "action" value = "quota-increase" />
471492 < Button
@@ -474,7 +495,19 @@ function PurchaseConcurrencyModal({
474495 type = "submit"
475496 disabled = { isLoading }
476497 >
477- { `Send request for ${ formatNumber ( extraConcurrency + amountValue ) } ` }
498+ { `Send request for ${ formatNumber ( amountValue ) } ` }
499+ </ Button >
500+ </ >
501+ ) : state === "decrease" || state === "need_to_increase_unallocated" ? (
502+ < >
503+ < input type = "hidden" name = "action" value = "purchase" />
504+ < Button
505+ variant = "danger/medium"
506+ type = "submit"
507+ disabled = { isLoading || state === "need_to_increase_unallocated" }
508+ LeadingIcon = { isLoading ? SpinnerWhite : undefined }
509+ >
510+ { `Remove ${ formatNumber ( extraConcurrency - amountValue ) } concurrency` }
478511 </ Button >
479512 </ >
480513 ) : (
@@ -483,10 +516,10 @@ function PurchaseConcurrencyModal({
483516 < Button
484517 variant = "primary/medium"
485518 type = "submit"
486- disabled = { isLoading }
519+ disabled = { isLoading || state === "no_change" }
487520 LeadingIcon = { isLoading ? SpinnerWhite : undefined }
488521 >
489- Purchase
522+ { ` Purchase ${ formatNumber ( amountValue - extraConcurrency ) } ` }
490523 </ Button >
491524 </ >
492525 )
@@ -504,3 +537,26 @@ function PurchaseConcurrencyModal({
504537 </ Dialog >
505538 ) ;
506539}
540+
541+ function updateState ( {
542+ value,
543+ existingValue,
544+ quota,
545+ extraUnallocatedConcurrency,
546+ } : {
547+ value : number ;
548+ existingValue : number ;
549+ quota : number ;
550+ extraUnallocatedConcurrency : number ;
551+ } ) : "no_change" | "increase" | "decrease" | "above_quota" | "need_to_increase_unallocated" {
552+ if ( value === existingValue ) return "no_change" ;
553+ if ( value < existingValue ) {
554+ const difference = existingValue - value ;
555+ if ( difference > extraUnallocatedConcurrency ) {
556+ return "need_to_increase_unallocated" ;
557+ }
558+ return "decrease" ;
559+ }
560+ if ( value > quota ) return "above_quota" ;
561+ return "increase" ;
562+ }
0 commit comments