@@ -3,93 +3,33 @@ import { type SlDialog } from "@shoelace-style/shoelace";
3
3
import { html , type PropertyValues } from "lit" ;
4
4
import { customElement , property , state } from "lit/decorators.js" ;
5
5
import { createRef , ref , type Ref } from "lit/directives/ref.js" ;
6
+ import { until } from "lit/directives/until.js" ;
6
7
import { when } from "lit/directives/when.js" ;
7
8
import { isEqual } from "lodash" ;
8
9
import { type Entries } from "type-fest" ;
10
+ import z from "zod" ;
9
11
10
12
import { BtrixElement } from "@/classes/BtrixElement" ;
11
13
import { type RowEditEventDetail } from "@/components/ui/data-grid/data-grid-row" ;
12
14
import {
13
15
GridColumnType ,
14
16
type GridColumn ,
15
17
} from "@/components/ui/data-grid/types" ;
16
- import { type OrgData , type OrgQuotas } from "@/utils/orgs" ;
18
+ import { orgQuotasSchema , type OrgData , type OrgQuotas } from "@/utils/orgs" ;
17
19
import { pluralOf } from "@/utils/pluralize" ;
18
20
19
- // These were manually copied over from Cashew on 2025-09-15 — please update if necessary
20
- const PRESETS = {
21
- Starter : {
22
- quotas : {
23
- maxConcurrentCrawls : 1 ,
24
- maxPagesPerCrawl : 2000 ,
25
- storageQuota : 100_000_000_000 ,
26
- maxExecMinutesPerMonth : 180 ,
27
- } ,
28
- subscriptionIds : [ "starter" , "starterTest" ] ,
29
- } ,
30
- Standard : {
31
- quotas : {
32
- maxConcurrentCrawls : 2 ,
33
- maxPagesPerCrawl : 5000 ,
34
- storageQuota : 220_000_000_000 ,
35
- maxExecMinutesPerMonth : 360 ,
36
- } ,
37
- subscriptionIds : [ "standard" , "standardTest" ] ,
38
- } ,
39
- Plus : {
40
- quotas : {
41
- maxConcurrentCrawls : 3 ,
42
- maxPagesPerCrawl : 10000 ,
43
- storageQuota : 500_000_000_000 ,
44
- maxExecMinutesPerMonth : 720 ,
45
- } ,
46
- subscriptionIds : [ "plus" , "plusTest" ] ,
47
- } ,
48
- "Pro Standard" : {
49
- quotas : {
50
- maxConcurrentCrawls : 4 ,
51
- maxPagesPerCrawl : 50_000 ,
52
- storageQuota : 1_000_000_000_000 ,
53
- maxExecMinutesPerMonth : 50 * 60 ,
54
- } ,
55
- subscriptionIds : [ "pro-standard-monthly" , "pro-standard-yearly" ] ,
56
- } ,
57
- "Pro Teams" : {
58
- quotas : {
59
- maxConcurrentCrawls : 5 ,
60
- maxPagesPerCrawl : 100_000 ,
61
- storageQuota : 3_000_000_000_000 ,
62
- maxExecMinutesPerMonth : 80 * 60 ,
63
- } ,
64
- subscriptionIds : [ "pro-teams-monthly" , "pro-teams-yearly" ] ,
65
- } ,
66
- "Pro Plus" : {
67
- quotas : {
68
- maxConcurrentCrawls : 10 ,
69
- maxPagesPerCrawl : 400_000 ,
70
- storageQuota : 5_000_000_000_000 ,
71
- maxExecMinutesPerMonth : 150 * 60 ,
72
- } ,
73
- subscriptionIds : [ "pro-plus-monthly" , "pro-plus-yearly" ] ,
74
- } ,
75
- Unset : {
76
- quotas : {
77
- maxConcurrentCrawls : 0 ,
78
- maxPagesPerCrawl : 0 ,
79
- storageQuota : 0 ,
80
- maxExecMinutesPerMonth : 0 ,
81
- } ,
82
- subscriptionIds : [ ] ,
83
- } ,
84
- } as const satisfies Record <
85
- string ,
86
- {
87
- quotas : {
88
- [ key in keyof OrgQuotas ] ?: number ;
89
- } ;
90
- subscriptionIds ?: string [ ] ;
91
- }
92
- > ;
21
+ const PlanSchema = z . object ( {
22
+ id : z . string ( ) ,
23
+ name : z . string ( ) ,
24
+ org_quotas : orgQuotasSchema ,
25
+ testmode : z . boolean ( ) ,
26
+ } ) ;
27
+
28
+ const PlansResponseSchema = z . object ( {
29
+ plans : z . array ( PlanSchema ) ,
30
+ } ) ;
31
+
32
+ type PlansResponse = z . infer < typeof PlansResponseSchema > ;
93
33
94
34
const LABELS = {
95
35
maxConcurrentCrawls : {
@@ -136,6 +76,9 @@ export class OrgQuotaEditor extends BtrixElement {
136
76
137
77
dialog : Ref < SlDialog > = createRef ( ) ;
138
78
79
+ @state ( )
80
+ plans = this . api . fetch < PlansResponse > ( "/orgs/plans" ) ;
81
+
139
82
show ( ) {
140
83
void this . dialog . value ?. show ( ) ;
141
84
}
@@ -243,65 +186,75 @@ export class OrgQuotaEditor extends BtrixElement {
243
186
>
244
187
< btrix-overflow-scroll class ="-mx-4 part-[content]:px-4 ">
245
188
< sl-button-group id ="org-quota-presets ">
246
- ${ ( Object . entries ( PRESETS ) as Entries < typeof PRESETS > ) . map (
247
- ( [ key , value ] ) => {
248
- const isCurrentSubscription = (
249
- value . subscriptionIds as string [ ]
250
- ) . includes ( this . activeOrg ?. subscription ?. planId ?? "" ) ;
251
- return html `< btrix-popover placement ="top ">
252
- < sl-button
253
- @click =${ ( ) => {
254
- const newQuota : Partial < OrgQuotas > = { } ;
255
- (
256
- Object . entries ( value . quotas ) as Entries <
257
- typeof value . quotas
258
- >
259
- ) . forEach ( ( [ k , v ] ) => {
260
- newQuota [ k ] = v - quotas [ k ] ;
261
- } ) ;
262
- this . orgQuotaAdjustments = { ...newQuota } ;
263
- } }
264
- >
265
- ${ key }
266
- ${ isCurrentSubscription
267
- ? html `< sl-icon
268
- name ="credit-card "
269
- slot ="prefix "
270
- > </ sl-icon > `
271
- : null }
272
- </ sl-button >
273
- < div slot ="content ">
274
- < header class ="mb-2 font-medium ">
275
- ${ key } ${ isCurrentSubscription
276
- ? html ` -
277
- < b class ="text-primary-600 "
278
- > ${ msg ( "This is the current subscription." ) } </ b
279
- > `
189
+ ${ until (
190
+ this . plans . then ( ( { plans } ) =>
191
+ plans . map ( ( { id, name, org_quotas } ) => {
192
+ const isCurrentSubscription =
193
+ id === this . activeOrg ?. subscription ?. planId ;
194
+ const presets : Omit <
195
+ OrgQuotas ,
196
+ `${"extra" | "gifted" } ExecMinutes`
197
+ > = {
198
+ maxConcurrentCrawls : org_quotas . maxConcurrentCrawls ,
199
+ maxExecMinutesPerMonth : org_quotas . maxExecMinutesPerMonth ,
200
+ maxPagesPerCrawl : org_quotas . maxPagesPerCrawl ,
201
+ storageQuota : org_quotas . storageQuota ,
202
+ } ;
203
+ return html `< btrix-popover placement ="top ">
204
+ < sl-button
205
+ @click =${ ( ) => {
206
+ const newQuota : Partial < OrgQuotas > = { } ;
207
+
208
+ (
209
+ Object . entries ( presets ) as Entries < typeof presets >
210
+ ) . forEach ( ( [ k , v ] ) => {
211
+ newQuota [ k ] = v - quotas [ k ] ;
212
+ } ) ;
213
+ this . orgQuotaAdjustments = { ...newQuota } ;
214
+ } }
215
+ >
216
+ ${ name }
217
+ ${ isCurrentSubscription
218
+ ? html `< sl-icon
219
+ name ="credit-card "
220
+ slot ="prefix "
221
+ > </ sl-icon > `
280
222
: null }
281
- </ header >
223
+ </ sl-button >
224
+ < div slot ="content ">
225
+ < header class ="mb-2 font-medium ">
226
+ ${ name } ${ isCurrentSubscription
227
+ ? html ` -
228
+ < b class ="text-primary-600 "
229
+ > ${ msg (
230
+ "This is the current subscription." ,
231
+ ) } </ b
232
+ > `
233
+ : null }
234
+ </ header >
282
235
283
- < hr class ="my-2 " />
284
- < table >
285
- < tbody >
286
- ${ (
287
- Object . entries ( value . quotas ) as Entries <
288
- typeof value . quotas
289
- >
290
- ) . map (
291
- ( [ key , value ] ) => html `
292
- < tr >
293
- < td class =" pr-2 " > ${ LABELS [ key ] . label } </ td >
294
- < td class =" pr-2 " >
295
- ${ this . format ( value , LABELS [ key ] . type ) }
296
- </ td >
297
- </ tr >
298
- ` ,
299
- ) }
300
- </ tbody >
301
- </ table >
302
- </ div >
303
- </ btrix-popover > ` ;
304
- } ,
236
+ < hr class ="my-2 " />
237
+ < table >
238
+ < tbody >
239
+ ${ (
240
+ Object . entries ( presets ) as Entries < typeof presets >
241
+ ) . map (
242
+ ( [ key , value ] ) => html `
243
+ < tr >
244
+ < td class =" pr-2 " > ${ LABELS [ key ] . label } </ td >
245
+ < td class =" pr-2 " >
246
+ ${ this . format ( value , LABELS [ key ] . type ) }
247
+ </ td >
248
+ </ tr >
249
+ ` ,
250
+ ) }
251
+ </ tbody >
252
+ </ table >
253
+ </ div >
254
+ </ btrix-popover > ` ;
255
+ } ) ,
256
+ ) ,
257
+ msg ( "Loading plans..." ) ,
305
258
) }
306
259
</ sl-button-group >
307
260
</ btrix-overflow-scroll >
0 commit comments