From c98a18943026da4f8086cfae467b42255e801037 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 27 Sep 2021 15:23:12 -0400 Subject: [PATCH] Organization autoscaling (#1193) * Add seat autoscale component * Move small description under title * tweak autoscale terminology * Linter fixes * Use single component for org subscription updates * Delete unused localization string * Clarify max bill copy * Remove cancel from org subscription adjustment * Update jslib * PR review * update jslib * Simplify success toast --- jslib | 2 +- .../settings/adjust-seats.component.html | 29 --- .../settings/adjust-seats.component.ts | 87 -------- .../adjust-subscription.component.html | 42 ++++ .../settings/adjust-subscription.component.ts | 68 ++++++ .../organization-subscription.component.html | 208 +++++++++--------- .../organization-subscription.component.ts | 31 ++- src/app/oss.module.ts | 4 +- src/locales/en/messages.json | 49 +++++ 9 files changed, 286 insertions(+), 234 deletions(-) delete mode 100644 src/app/organizations/settings/adjust-seats.component.html delete mode 100644 src/app/organizations/settings/adjust-seats.component.ts create mode 100644 src/app/organizations/settings/adjust-subscription.component.html create mode 100644 src/app/organizations/settings/adjust-subscription.component.ts diff --git a/jslib b/jslib index 2c892eb3a2..cb00604617 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 2c892eb3a2a9aff1e238146b037e6f3eb5dacf9a +Subproject commit cb00604617a3d38fb450d900dbdf63b636ae01f6 diff --git a/src/app/organizations/settings/adjust-seats.component.html b/src/app/organizations/settings/adjust-seats.component.html deleted file mode 100644 index dbb15fa4fb..0000000000 --- a/src/app/organizations/settings/adjust-seats.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
- -

{{(add ? 'addSeats' : 'removeSeats') | i18n}}

-
-
- - -
-
-
- {{'total' | i18n}}: {{seatAdjustment || 0}} × {{seatPrice | currency:'$'}} = {{adjustedSeatTotal - | currency:'$'}} /{{interval | i18n}} -
- - - - {{(add ? 'seatsAddNote' : 'seatsRemoveNote') | i18n}} - -
-
- diff --git a/src/app/organizations/settings/adjust-seats.component.ts b/src/app/organizations/settings/adjust-seats.component.ts deleted file mode 100644 index 33689dcaf5..0000000000 --- a/src/app/organizations/settings/adjust-seats.component.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - Component, - EventEmitter, - Input, - Output, - ViewChild, -} from '@angular/core'; - -import { - ActivatedRoute, - Router, -} from '@angular/router'; - -import { ToasterService } from 'angular2-toaster'; - -import { ApiService } from 'jslib-common/abstractions/api.service'; -import { I18nService } from 'jslib-common/abstractions/i18n.service'; - -import { SeatRequest } from 'jslib-common/models/request/seatRequest'; - -import { PaymentComponent } from '../../settings/payment.component'; - -@Component({ - selector: 'app-adjust-seats', - templateUrl: 'adjust-seats.component.html', -}) -export class AdjustSeatsComponent { - @Input() seatPrice = 0; - @Input() add = true; - @Input() organizationId: string; - @Input() interval = 'year'; - @Output() onAdjusted = new EventEmitter(); - @Output() onCanceled = new EventEmitter(); - - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - - seatAdjustment = 0; - formPromise: Promise; - - constructor(private apiService: ApiService, private i18nService: I18nService, - private toasterService: ToasterService, private router: Router, - private activatedRoute: ActivatedRoute) { } - - async submit() { - try { - const request = new SeatRequest(); - request.seatAdjustment = this.seatAdjustment; - if (!this.add) { - request.seatAdjustment *= -1; - } - - let paymentFailed = false; - const action = async () => { - const result = await this.apiService.postOrganizationSeat(this.organizationId, request); - if (result != null && result.paymentIntentClientSecret != null) { - try { - await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); - } catch { - paymentFailed = true; - } - } - }; - this.formPromise = action(); - await this.formPromise; - this.onAdjusted.emit(this.seatAdjustment); - if (paymentFailed) { - this.toasterService.popAsync({ - body: this.i18nService.t('couldNotChargeCardPayInvoice'), - type: 'warning', - timeout: 10000, - }); - this.router.navigate(['../billing'], { relativeTo: this.activatedRoute }); - } else { - this.toasterService.popAsync('success', null, - this.i18nService.t('adjustedSeats', request.seatAdjustment.toString())); - } - } catch { } - } - - cancel() { - this.onCanceled.emit(); - } - - get adjustedSeatTotal(): number { - return this.seatAdjustment * this.seatPrice; - } -} diff --git a/src/app/organizations/settings/adjust-subscription.component.html b/src/app/organizations/settings/adjust-subscription.component.html new file mode 100644 index 0000000000..f3bfe0f221 --- /dev/null +++ b/src/app/organizations/settings/adjust-subscription.component.html @@ -0,0 +1,42 @@ +
+
+
+
+ + + + {{'total' | i18n}}: {{newSeatCount || 0}} × {{seatPrice | currency:'$'}} = + {{adjustedSeatTotal | currency:'$'}} / {{interval | i18n}} + +
+
+
+
+
+ + +
+ {{'limitSubscriptionDesc' | i18n}} +
+
+
+
+ + + + {{'maxSeatCost' | i18n}}: {{newMaxSeats || 0}} × + {{seatPrice | currency:'$'}} = {{maxSeatTotal | currency:'$'}} / {{interval | i18n}} + +
+
+ +
+
+ diff --git a/src/app/organizations/settings/adjust-subscription.component.ts b/src/app/organizations/settings/adjust-subscription.component.ts new file mode 100644 index 0000000000..31990bbf10 --- /dev/null +++ b/src/app/organizations/settings/adjust-subscription.component.ts @@ -0,0 +1,68 @@ +import { + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; + +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { OrganizationSubscriptionUpdateRequest } from 'jslib-common/models/request/organizationSubscriptionUpdateRequest'; + +@Component({ + selector: 'app-adjust-subscription', + templateUrl: 'adjust-subscription.component.html', +}) +export class AdjustSubscription { + @Input() organizationId: string; + @Input() maxAutoscaleSeats: number; + @Input() currentSeatCount: number; + @Input() seatPrice = 0; + @Input() interval = 'year'; + @Output() onAdjusted = new EventEmitter(); + + formPromise: Promise; + limitSubscription: boolean; + newSeatCount: number; + newMaxSeats: number; + + constructor(private apiService: ApiService, private i18nService: I18nService, + private toasterService: ToasterService) { } + + ngOnInit() { + this.limitSubscription = this.maxAutoscaleSeats != null; + this.newSeatCount = this.currentSeatCount; + this.newMaxSeats = this.maxAutoscaleSeats; + } + + async submit() { + try { + const seatAdjustment = this.newSeatCount - this.currentSeatCount; + const request = new OrganizationSubscriptionUpdateRequest(seatAdjustment, this.newMaxSeats); + this.formPromise = this.apiService.postOrganizationUpdateSubscription(this.organizationId, request); + + await this.formPromise; + + this.toasterService.popAsync('success', null, this.i18nService.t('subscriptionUpdated')); + } catch { } + this.onAdjusted.emit(); + } + + limitSubscriptionChanged() { + if (!this.limitSubscription) { + this.newMaxSeats = null; + } + } + + get adjustedSeatTotal(): number { + return this.newSeatCount * this.seatPrice; + } + + get maxSeatTotal(): number { + return this.newMaxSeats * this.seatPrice; + } +} diff --git a/src/app/organizations/settings/organization-subscription.component.html b/src/app/organizations/settings/organization-subscription.component.html index 0db263f8b6..7b22a49bdc 100644 --- a/src/app/organizations/settings/organization-subscription.component.html +++ b/src/app/organizations/settings/organization-subscription.component.html @@ -23,110 +23,60 @@ {{'reinstateSubscription' | i18n}} -
-
{{'billingPlan' | i18n}}
-
{{sub.plan.name}}
-
{{'expiration' | i18n}}
-
- {{sub.expiration | date:'mediumDate'}} - - - {{'licenseIsExpired' | i18n}} - -
-
{{'neverExpires' | i18n}}
-
-
-
-
-
{{'billingPlan' | i18n}}
-
{{sub.plan.name}}
- -
{{'status' | i18n}}
-
- {{subscription.status || '-'}} - {{'pendingCancellation' | i18n}} -
-
{{'nextCharge' | i18n}}
-
{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$')) - : '-'}} -
-
-
-
-
- {{'details' | i18n}} - - - - - - - -
- {{i.name}} {{i.quantity > 1 ? '×' + i.quantity : ''}} @ {{i.amount | currency:'$'}} - - {{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}} -
-
-
- -
- - - {{'manageSubscription' | i18n}} - -
-
-
- -

{{'updateLicense' | i18n}}

- -
-
-
-
- - - +
+
+
+
{{'billingPlan' | i18n}}
+
{{sub.plan.name}}
+ +
{{'status' | i18n}}
+
+ {{subscription.status || '-'}} + {{'pendingCancellation' | + i18n}} +
+
{{'nextCharge' | i18n}}
+
{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | + currency:'$')) + : '-'}} +
+
+
+
+
+ {{'details' | i18n}} + + + + + + + +
+ {{i.name}} {{i.quantity > 1 ? '×' + i.quantity : ''}} @ {{i.amount | + currency:'$'}} + + {{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}} +
+
+ +
+
+
{{'provider' | i18n}}
+
{{'yourProviderIs' | i18n : userOrg.providerName}}
+
+
+
- -
- -
-

{{'userSeats' | i18n}}

-

{{'subscriptionUserSeats' | i18n : sub.seats}}

+

{{'manageSubscription' | i18n}}

+

{{subscriptionDesc}}

-
- - -
- + +

{{'storage' | i18n}}

@@ -152,11 +102,59 @@
- -
-

{{'provider' | i18n}}

+

{{'additionalOptions' | i18n}}

+

+ {{'additionalOptionsDesc' | i18n }} +

+
+ + +
+ +
+ +
+ + +
+
{{'billingPlan' | i18n}}
+
{{sub.plan.name}}
+
{{'expiration' | i18n}}
+
+ {{sub.expiration | date:'mediumDate'}} + + + {{'licenseIsExpired' | i18n}} + +
+
{{'neverExpires' | i18n}}
+
+
+ + + {{'manageSubscription' | i18n}} + +
+
+
+ +

{{'updateLicense' | i18n}}

+
- {{'yourProviderIs' | i18n : userOrg.providerName}} - +
diff --git a/src/app/organizations/settings/organization-subscription.component.ts b/src/app/organizations/settings/organization-subscription.component.ts index 3a8f47e347..600041e4bb 100644 --- a/src/app/organizations/settings/organization-subscription.component.ts +++ b/src/app/organizations/settings/organization-subscription.component.ts @@ -26,6 +26,7 @@ export class OrganizationSubscriptionComponent implements OnInit { organizationId: string; adjustSeatsAdd = true; showAdjustSeats = false; + showAdjustSeatAutoscale = false; adjustStorageAdd = true; showAdjustStorage = false; showUpdateLicense = false; @@ -142,16 +143,8 @@ export class OrganizationSubscriptionComponent implements OnInit { } } - adjustSeats(add: boolean) { - this.adjustSeatsAdd = add; - this.showAdjustSeats = true; - } - - closeSeats(load: boolean) { - this.showAdjustSeats = false; - if (load) { - this.load(); - } + subscriptionAdjusted() { + this.load(); } adjustStorage(add: boolean) { @@ -205,6 +198,14 @@ export class OrganizationSubscriptionComponent implements OnInit { return this.sub.plan.seatPrice; } + get seats() { + return this.sub.seats; + } + + get maxAutoscaleSeats() { + return this.sub.maxAutoscaleSeats; + } + get canAdjustSeats() { return this.sub.plan.hasAdditionalSeatsOption; } @@ -213,4 +214,14 @@ export class OrganizationSubscriptionComponent implements OnInit { return (this.sub.planType !== PlanType.Free && this.subscription == null) || (this.subscription != null && !this.subscription.cancelled); } + + get subscriptionDesc() { + if (this.sub.maxAutoscaleSeats == this.sub.seats && this.sub.seats != null) { + return this.i18nService.t('subscriptionMaxReached', this.sub.seats.toString()); + } else if (this.sub.maxAutoscaleSeats == null) { + return this.i18nService.t('subscriptionUserSeatsUnlimitedAutoscale'); + } else { + return this.i18nService.t('subscriptionUserSeatsLimitedAutoscale', this.sub.maxAutoscaleSeats.toString()); + } + } } diff --git a/src/app/oss.module.ts b/src/app/oss.module.ts index 01315acff2..a4cf7f3337 100644 --- a/src/app/oss.module.ts +++ b/src/app/oss.module.ts @@ -59,7 +59,7 @@ import { UserConfirmComponent as OrgUserConfirmComponent } from './organizations import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component'; import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component'; -import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component'; +import { AdjustSubscription } from './organizations/settings/adjust-subscription.component'; import { ChangePlanComponent } from './organizations/settings/change-plan.component'; import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component'; import { DownloadLicenseComponent } from './organizations/settings/download-license.component'; @@ -303,7 +303,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); AddCreditComponent, AddEditComponent, AdjustPaymentComponent, - AdjustSeatsComponent, + AdjustSubscription, AdjustStorageComponent, ApiActionDirective, ApiKeyComponent, diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 1c4c1f903f..e87893e143 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -2878,6 +2878,16 @@ "enterInstallationId": { "message": "Enter your installation id" }, + "limitSubscriptionDesc": { + "message": "Set a seat limit for your subscription. Once this limit is reached, you will not be able to invite new users." + }, + "maxSeatLimit": { + "message": "Maximum Seat Limit (optional)", + "description": "Upper limit of seats to allow through autoscaling" + }, + "maxSeatCost": { + "message": "Max potential seat cost" + }, "addSeats": { "message": "Add Seats", "description": "Seat = User Seat" @@ -2886,6 +2896,9 @@ "message": "Remove Seats", "description": "Seat = User Seat" }, + "subscriptionDesc": { + "message": "Adjustments to your subscription will result in prorated changes to your billing totals. If newly invited users exceed your subscription seats, you will immediately receive a prorated charge for the additional users." + }, "subscriptionUserSeats": { "message": "Your subscription allows for a total of $COUNT$ users.", "placeholders": { @@ -2895,6 +2908,42 @@ } } }, + "limitSubscription": { + "message": "Limit Subscription (Optional)" + }, + "subscriptionSeats": { + "message": "Subscription Seats" + }, + "subscriptionUpdated": { + "message": "Subscription updated" + }, + "additionalOptions": { + "message": "Additional Options" + }, + "additionalOptionsDesc": { + "message": "For additional help in managing your subscription, please contact Customer Support." + }, + "subscriptionUserSeatsUnlimitedAutoscale": { + "message": "Adjustments to your subscription will result in prorated changes to your billing totals. If newly invited users exceed your subscription seats, you will immediately receive a prorated charge for the additional users." + }, + "subscriptionUserSeatsLimitedAutoscale": { + "message": "Adjustments to your subscription will result in prorated changes to your billing totals. If newly invited users exceed your subscription seats, you will immediately receive a prorated charge for the additional users until your $MAX$ seat limit is reached.", + "placeholders": { + "max": { + "content": "$1", + "example": "50" + } + } + }, + "subscriptionMaxReached": { + "message": "Adjustments to your subscription will result in prorated changes to your billing totals. You cannot invite more than $COUNT$ users without increasing your subscription seats.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, "seatsToAdd": { "message": "Seats To Add" },