@abpjs/saas 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1172 @@
1
+ // src/constants/routes.ts
2
+ var SAAS_ROUTES = {
3
+ routes: [
4
+ {
5
+ name: "Saas",
6
+ path: "saas",
7
+ layout: "application",
8
+ order: 50,
9
+ children: [
10
+ {
11
+ name: "Tenants",
12
+ path: "tenants",
13
+ order: 1,
14
+ requiredPolicy: "Saas.Tenants"
15
+ },
16
+ {
17
+ name: "Editions",
18
+ path: "editions",
19
+ order: 2,
20
+ requiredPolicy: "Saas.Editions"
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ };
26
+
27
+ // src/services/saas.service.ts
28
+ var SaasService = class {
29
+ constructor(restService) {
30
+ this.restService = restService;
31
+ }
32
+ // ==================== Tenant Operations ====================
33
+ /**
34
+ * Get paginated list of tenants
35
+ * @param params Query parameters for filtering and pagination
36
+ * @returns Promise with paginated tenant response
37
+ */
38
+ async getTenants(params = {}) {
39
+ return this.restService.request({
40
+ method: "GET",
41
+ url: "/api/saas/tenants",
42
+ params
43
+ });
44
+ }
45
+ /**
46
+ * Get a tenant by ID
47
+ * @param id Tenant ID
48
+ * @returns Promise with tenant data
49
+ */
50
+ async getTenantById(id) {
51
+ return this.restService.request({
52
+ method: "GET",
53
+ url: `/api/saas/tenants/${id}`
54
+ });
55
+ }
56
+ /**
57
+ * Create a new tenant
58
+ * @param body Tenant creation request
59
+ * @returns Promise with created tenant data
60
+ */
61
+ async createTenant(body) {
62
+ return this.restService.request({
63
+ method: "POST",
64
+ url: "/api/saas/tenants",
65
+ body
66
+ });
67
+ }
68
+ /**
69
+ * Update an existing tenant
70
+ * @param body Tenant update request (must include id)
71
+ * @returns Promise with updated tenant data
72
+ */
73
+ async updateTenant(body) {
74
+ const { id, ...rest } = body;
75
+ return this.restService.request({
76
+ method: "PUT",
77
+ url: `/api/saas/tenants/${id}`,
78
+ body: rest
79
+ });
80
+ }
81
+ /**
82
+ * Delete a tenant by ID
83
+ * @param id Tenant ID to delete
84
+ * @returns Promise that resolves when deletion is complete
85
+ */
86
+ async deleteTenant(id) {
87
+ return this.restService.request({
88
+ method: "DELETE",
89
+ url: `/api/saas/tenants/${id}`
90
+ });
91
+ }
92
+ // ==================== Connection String Operations ====================
93
+ /**
94
+ * Get the default connection string for a tenant
95
+ * @param id Tenant ID
96
+ * @returns Promise with connection string (empty string if using shared database)
97
+ */
98
+ async getDefaultConnectionString(id) {
99
+ return this.restService.request({
100
+ method: "GET",
101
+ url: `/api/saas/tenants/${id}/default-connection-string`,
102
+ responseType: "text"
103
+ });
104
+ }
105
+ /**
106
+ * Update the default connection string for a tenant
107
+ * @param payload Object containing tenant ID and connection string
108
+ * @returns Promise that resolves when update is complete
109
+ */
110
+ async updateDefaultConnectionString(payload) {
111
+ return this.restService.request({
112
+ method: "PUT",
113
+ url: `/api/saas/tenants/${payload.id}/default-connection-string`,
114
+ params: { defaultConnectionString: payload.defaultConnectionString }
115
+ });
116
+ }
117
+ /**
118
+ * Delete the default connection string for a tenant (revert to shared database)
119
+ * @param id Tenant ID
120
+ * @returns Promise that resolves when deletion is complete
121
+ */
122
+ async deleteDefaultConnectionString(id) {
123
+ return this.restService.request({
124
+ method: "DELETE",
125
+ url: `/api/saas/tenants/${id}/default-connection-string`
126
+ });
127
+ }
128
+ // ==================== Edition Operations ====================
129
+ /**
130
+ * Get paginated list of editions
131
+ * @param params Query parameters for filtering and pagination
132
+ * @returns Promise with paginated editions response
133
+ */
134
+ async getEditions(params = {}) {
135
+ return this.restService.request({
136
+ method: "GET",
137
+ url: "/api/saas/editions",
138
+ params
139
+ });
140
+ }
141
+ /**
142
+ * Get an edition by ID
143
+ * @param id Edition ID
144
+ * @returns Promise with edition data
145
+ */
146
+ async getEditionById(id) {
147
+ return this.restService.request({
148
+ method: "GET",
149
+ url: `/api/saas/editions/${id}`
150
+ });
151
+ }
152
+ /**
153
+ * Create a new edition
154
+ * @param body Edition creation request
155
+ * @returns Promise with created edition data
156
+ */
157
+ async createEdition(body) {
158
+ return this.restService.request({
159
+ method: "POST",
160
+ url: "/api/saas/editions",
161
+ body
162
+ });
163
+ }
164
+ /**
165
+ * Update an existing edition
166
+ * @param body Edition update request (must include id)
167
+ * @returns Promise with updated edition data
168
+ */
169
+ async updateEdition(body) {
170
+ const { id, ...rest } = body;
171
+ return this.restService.request({
172
+ method: "PUT",
173
+ url: `/api/saas/editions/${id}`,
174
+ body: rest
175
+ });
176
+ }
177
+ /**
178
+ * Delete an edition by ID
179
+ * @param id Edition ID to delete
180
+ * @returns Promise that resolves when deletion is complete
181
+ */
182
+ async deleteEdition(id) {
183
+ return this.restService.request({
184
+ method: "DELETE",
185
+ url: `/api/saas/editions/${id}`
186
+ });
187
+ }
188
+ // ==================== Statistics Operations ====================
189
+ /**
190
+ * Get usage statistics for editions
191
+ * @returns Promise with usage statistics data
192
+ */
193
+ async getUsageStatistics() {
194
+ return this.restService.request({
195
+ method: "GET",
196
+ url: "/api/saas/editions/statistics/usage-statistic"
197
+ });
198
+ }
199
+ };
200
+
201
+ // src/hooks/useTenants.ts
202
+ import { useState, useCallback, useMemo } from "react";
203
+ import { useRestService } from "@abpjs/core";
204
+ function useTenants() {
205
+ const restService = useRestService();
206
+ const service = useMemo(() => new SaasService(restService), [restService]);
207
+ const [tenants, setTenants] = useState([]);
208
+ const [totalCount, setTotalCount] = useState(0);
209
+ const [selectedTenant, setSelectedTenant] = useState(null);
210
+ const [isLoading, setIsLoading] = useState(false);
211
+ const [error, setError] = useState(null);
212
+ const [sortKey, setSortKey] = useState("name");
213
+ const [sortOrder, setSortOrder] = useState("");
214
+ const [defaultConnectionString, setDefaultConnectionString] = useState("");
215
+ const [useSharedDatabase, setUseSharedDatabase] = useState(true);
216
+ const fetchTenants = useCallback(
217
+ async (params) => {
218
+ setIsLoading(true);
219
+ setError(null);
220
+ try {
221
+ const response = await service.getTenants(params);
222
+ setTenants(response.items || []);
223
+ setTotalCount(response.totalCount || 0);
224
+ setIsLoading(false);
225
+ return { success: true, data: response };
226
+ } catch (err) {
227
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch tenants";
228
+ setError(errorMessage);
229
+ setIsLoading(false);
230
+ return { success: false, error: errorMessage };
231
+ }
232
+ },
233
+ [service]
234
+ );
235
+ const getTenantById = useCallback(
236
+ async (id) => {
237
+ setIsLoading(true);
238
+ setError(null);
239
+ try {
240
+ const tenant = await service.getTenantById(id);
241
+ setSelectedTenant(tenant);
242
+ setIsLoading(false);
243
+ return { success: true, data: tenant };
244
+ } catch (err) {
245
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch tenant";
246
+ setError(errorMessage);
247
+ setIsLoading(false);
248
+ return { success: false, error: errorMessage };
249
+ }
250
+ },
251
+ [service]
252
+ );
253
+ const createTenant = useCallback(
254
+ async (tenant) => {
255
+ setIsLoading(true);
256
+ setError(null);
257
+ try {
258
+ const created = await service.createTenant(tenant);
259
+ setIsLoading(false);
260
+ return { success: true, data: created };
261
+ } catch (err) {
262
+ const errorMessage = err instanceof Error ? err.message : "Failed to create tenant";
263
+ setError(errorMessage);
264
+ setIsLoading(false);
265
+ return { success: false, error: errorMessage };
266
+ }
267
+ },
268
+ [service]
269
+ );
270
+ const updateTenant = useCallback(
271
+ async (tenant) => {
272
+ setIsLoading(true);
273
+ setError(null);
274
+ try {
275
+ const updated = await service.updateTenant(tenant);
276
+ setIsLoading(false);
277
+ return { success: true, data: updated };
278
+ } catch (err) {
279
+ const errorMessage = err instanceof Error ? err.message : "Failed to update tenant";
280
+ setError(errorMessage);
281
+ setIsLoading(false);
282
+ return { success: false, error: errorMessage };
283
+ }
284
+ },
285
+ [service]
286
+ );
287
+ const deleteTenant = useCallback(
288
+ async (id) => {
289
+ setIsLoading(true);
290
+ setError(null);
291
+ try {
292
+ await service.deleteTenant(id);
293
+ setIsLoading(false);
294
+ return { success: true };
295
+ } catch (err) {
296
+ const errorMessage = err instanceof Error ? err.message : "Failed to delete tenant";
297
+ setError(errorMessage);
298
+ setIsLoading(false);
299
+ return { success: false, error: errorMessage };
300
+ }
301
+ },
302
+ [service]
303
+ );
304
+ const getDefaultConnectionString = useCallback(
305
+ async (id) => {
306
+ setError(null);
307
+ try {
308
+ const connString = await service.getDefaultConnectionString(id);
309
+ setDefaultConnectionString(connString || "");
310
+ setUseSharedDatabase(!connString);
311
+ return { success: true, data: connString };
312
+ } catch (err) {
313
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch connection string";
314
+ setError(errorMessage);
315
+ return { success: false, error: errorMessage };
316
+ }
317
+ },
318
+ [service]
319
+ );
320
+ const updateDefaultConnectionString = useCallback(
321
+ async (payload) => {
322
+ setIsLoading(true);
323
+ setError(null);
324
+ try {
325
+ await service.updateDefaultConnectionString(payload);
326
+ setDefaultConnectionString(payload.defaultConnectionString);
327
+ setUseSharedDatabase(false);
328
+ setIsLoading(false);
329
+ return { success: true };
330
+ } catch (err) {
331
+ const errorMessage = err instanceof Error ? err.message : "Failed to update connection string";
332
+ setError(errorMessage);
333
+ setIsLoading(false);
334
+ return { success: false, error: errorMessage };
335
+ }
336
+ },
337
+ [service]
338
+ );
339
+ const deleteDefaultConnectionString = useCallback(
340
+ async (id) => {
341
+ setIsLoading(true);
342
+ setError(null);
343
+ try {
344
+ await service.deleteDefaultConnectionString(id);
345
+ setDefaultConnectionString("");
346
+ setUseSharedDatabase(true);
347
+ setIsLoading(false);
348
+ return { success: true };
349
+ } catch (err) {
350
+ const errorMessage = err instanceof Error ? err.message : "Failed to delete connection string";
351
+ setError(errorMessage);
352
+ setIsLoading(false);
353
+ return { success: false, error: errorMessage };
354
+ }
355
+ },
356
+ [service]
357
+ );
358
+ const reset = useCallback(() => {
359
+ setTenants([]);
360
+ setTotalCount(0);
361
+ setSelectedTenant(null);
362
+ setIsLoading(false);
363
+ setError(null);
364
+ setSortKey("name");
365
+ setSortOrder("");
366
+ setDefaultConnectionString("");
367
+ setUseSharedDatabase(true);
368
+ }, []);
369
+ return {
370
+ tenants,
371
+ totalCount,
372
+ selectedTenant,
373
+ isLoading,
374
+ error,
375
+ sortKey,
376
+ sortOrder,
377
+ defaultConnectionString,
378
+ useSharedDatabase,
379
+ fetchTenants,
380
+ getTenantById,
381
+ createTenant,
382
+ updateTenant,
383
+ deleteTenant,
384
+ getDefaultConnectionString,
385
+ updateDefaultConnectionString,
386
+ deleteDefaultConnectionString,
387
+ setSelectedTenant,
388
+ setSortKey,
389
+ setSortOrder,
390
+ reset
391
+ };
392
+ }
393
+
394
+ // src/hooks/useEditions.ts
395
+ import { useState as useState2, useCallback as useCallback2, useMemo as useMemo2 } from "react";
396
+ import { useRestService as useRestService2 } from "@abpjs/core";
397
+ function useEditions() {
398
+ const restService = useRestService2();
399
+ const service = useMemo2(() => new SaasService(restService), [restService]);
400
+ const [editions, setEditions] = useState2([]);
401
+ const [totalCount, setTotalCount] = useState2(0);
402
+ const [selectedEdition, setSelectedEdition] = useState2(null);
403
+ const [isLoading, setIsLoading] = useState2(false);
404
+ const [error, setError] = useState2(null);
405
+ const [sortKey, setSortKey] = useState2("displayName");
406
+ const [sortOrder, setSortOrder] = useState2("");
407
+ const [usageStatistics, setUsageStatistics] = useState2({});
408
+ const fetchEditions = useCallback2(
409
+ async (params) => {
410
+ setIsLoading(true);
411
+ setError(null);
412
+ try {
413
+ const response = await service.getEditions(params);
414
+ setEditions(response.items || []);
415
+ setTotalCount(response.totalCount || 0);
416
+ setIsLoading(false);
417
+ return { success: true, data: response };
418
+ } catch (err) {
419
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch editions";
420
+ setError(errorMessage);
421
+ setIsLoading(false);
422
+ return { success: false, error: errorMessage };
423
+ }
424
+ },
425
+ [service]
426
+ );
427
+ const getEditionById = useCallback2(
428
+ async (id) => {
429
+ setIsLoading(true);
430
+ setError(null);
431
+ try {
432
+ const edition = await service.getEditionById(id);
433
+ setSelectedEdition(edition);
434
+ setIsLoading(false);
435
+ return { success: true, data: edition };
436
+ } catch (err) {
437
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch edition";
438
+ setError(errorMessage);
439
+ setIsLoading(false);
440
+ return { success: false, error: errorMessage };
441
+ }
442
+ },
443
+ [service]
444
+ );
445
+ const createEdition = useCallback2(
446
+ async (edition) => {
447
+ setIsLoading(true);
448
+ setError(null);
449
+ try {
450
+ const created = await service.createEdition(edition);
451
+ setIsLoading(false);
452
+ return { success: true, data: created };
453
+ } catch (err) {
454
+ const errorMessage = err instanceof Error ? err.message : "Failed to create edition";
455
+ setError(errorMessage);
456
+ setIsLoading(false);
457
+ return { success: false, error: errorMessage };
458
+ }
459
+ },
460
+ [service]
461
+ );
462
+ const updateEdition = useCallback2(
463
+ async (edition) => {
464
+ setIsLoading(true);
465
+ setError(null);
466
+ try {
467
+ const updated = await service.updateEdition(edition);
468
+ setIsLoading(false);
469
+ return { success: true, data: updated };
470
+ } catch (err) {
471
+ const errorMessage = err instanceof Error ? err.message : "Failed to update edition";
472
+ setError(errorMessage);
473
+ setIsLoading(false);
474
+ return { success: false, error: errorMessage };
475
+ }
476
+ },
477
+ [service]
478
+ );
479
+ const deleteEdition = useCallback2(
480
+ async (id) => {
481
+ setIsLoading(true);
482
+ setError(null);
483
+ try {
484
+ await service.deleteEdition(id);
485
+ setIsLoading(false);
486
+ return { success: true };
487
+ } catch (err) {
488
+ const errorMessage = err instanceof Error ? err.message : "Failed to delete edition";
489
+ setError(errorMessage);
490
+ setIsLoading(false);
491
+ return { success: false, error: errorMessage };
492
+ }
493
+ },
494
+ [service]
495
+ );
496
+ const fetchUsageStatistics = useCallback2(
497
+ async () => {
498
+ setError(null);
499
+ try {
500
+ const response = await service.getUsageStatistics();
501
+ setUsageStatistics(response.data || {});
502
+ return { success: true, data: response.data };
503
+ } catch (err) {
504
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch usage statistics";
505
+ setError(errorMessage);
506
+ return { success: false, error: errorMessage };
507
+ }
508
+ },
509
+ [service]
510
+ );
511
+ const reset = useCallback2(() => {
512
+ setEditions([]);
513
+ setTotalCount(0);
514
+ setSelectedEdition(null);
515
+ setIsLoading(false);
516
+ setError(null);
517
+ setSortKey("displayName");
518
+ setSortOrder("");
519
+ setUsageStatistics({});
520
+ }, []);
521
+ return {
522
+ editions,
523
+ totalCount,
524
+ selectedEdition,
525
+ isLoading,
526
+ error,
527
+ sortKey,
528
+ sortOrder,
529
+ usageStatistics,
530
+ fetchEditions,
531
+ getEditionById,
532
+ createEdition,
533
+ updateEdition,
534
+ deleteEdition,
535
+ fetchUsageStatistics,
536
+ setSelectedEdition,
537
+ setSortKey,
538
+ setSortOrder,
539
+ reset
540
+ };
541
+ }
542
+
543
+ // src/components/Tenants/TenantsComponent.tsx
544
+ import { useEffect, useState as useState3, useCallback as useCallback3 } from "react";
545
+ import { useLocalization } from "@abpjs/core";
546
+ import { Modal, Button, FormField, useConfirmation, Toaster } from "@abpjs/theme-shared";
547
+ import {
548
+ Box,
549
+ Flex,
550
+ Input,
551
+ Table,
552
+ Spinner,
553
+ VStack,
554
+ Text,
555
+ Badge
556
+ } from "@chakra-ui/react";
557
+ import { NativeSelectRoot, NativeSelectField } from "@chakra-ui/react";
558
+ import { jsx, jsxs } from "react/jsx-runtime";
559
+ function TenantsComponent({
560
+ onTenantCreated,
561
+ onTenantUpdated,
562
+ onTenantDeleted
563
+ }) {
564
+ const { t } = useLocalization();
565
+ const { warn } = useConfirmation();
566
+ const {
567
+ tenants,
568
+ totalCount,
569
+ selectedTenant,
570
+ isLoading,
571
+ error,
572
+ fetchTenants,
573
+ getTenantById,
574
+ createTenant,
575
+ updateTenant,
576
+ deleteTenant,
577
+ getDefaultConnectionString,
578
+ updateDefaultConnectionString,
579
+ deleteDefaultConnectionString,
580
+ setSelectedTenant
581
+ } = useTenants();
582
+ const { editions, fetchEditions } = useEditions();
583
+ const [modalType, setModalType] = useState3(null);
584
+ const [modalVisible, setModalVisible] = useState3(false);
585
+ const [modalBusy, setModalBusy] = useState3(false);
586
+ const [page, setPage] = useState3(0);
587
+ const pageSize = 10;
588
+ const [filter, setFilter] = useState3("");
589
+ const [tenantName, setTenantName] = useState3("");
590
+ const [tenantEditionId, setTenantEditionId] = useState3("");
591
+ const [connStringUseShared, setConnStringUseShared] = useState3(true);
592
+ const [connString, setConnString] = useState3("");
593
+ useEffect(() => {
594
+ fetchTenants({
595
+ skipCount: page * pageSize,
596
+ maxResultCount: pageSize,
597
+ filter: filter || void 0,
598
+ getEditionNames: true
599
+ });
600
+ }, [page, pageSize, fetchTenants]);
601
+ useEffect(() => {
602
+ fetchEditions();
603
+ }, [fetchEditions]);
604
+ const handleSearch = useCallback3(() => {
605
+ setPage(0);
606
+ fetchTenants({
607
+ skipCount: 0,
608
+ maxResultCount: pageSize,
609
+ filter: filter || void 0,
610
+ getEditionNames: true
611
+ });
612
+ }, [filter, pageSize, fetchTenants]);
613
+ const handleAddTenant = useCallback3(() => {
614
+ setSelectedTenant(null);
615
+ setTenantName("");
616
+ setTenantEditionId("");
617
+ setModalType("tenant");
618
+ setModalVisible(true);
619
+ }, [setSelectedTenant]);
620
+ const handleEditTenant = useCallback3(
621
+ async (id) => {
622
+ const result = await getTenantById(id);
623
+ if (result.success && result.data) {
624
+ setTenantName(result.data.name);
625
+ setTenantEditionId(result.data.editionId || "");
626
+ setModalType("tenant");
627
+ setModalVisible(true);
628
+ }
629
+ },
630
+ [getTenantById]
631
+ );
632
+ const handleEditConnectionString = useCallback3(
633
+ async (id) => {
634
+ const tenantResult = await getTenantById(id);
635
+ if (tenantResult.success) {
636
+ const connResult = await getDefaultConnectionString(id);
637
+ setConnStringUseShared(!connResult.data);
638
+ setConnString(connResult.data || "");
639
+ setModalType("connectionString");
640
+ setModalVisible(true);
641
+ }
642
+ },
643
+ [getTenantById, getDefaultConnectionString]
644
+ );
645
+ const handleDeleteTenant = useCallback3(
646
+ async (tenant) => {
647
+ const status = await warn(
648
+ t("Saas::TenantDeletionConfirmationMessage").replace("{0}", tenant.name),
649
+ t("Saas::AreYouSure")
650
+ );
651
+ if (status === Toaster.Status.confirm) {
652
+ const result = await deleteTenant(tenant.id);
653
+ if (result.success) {
654
+ onTenantDeleted?.(tenant.id);
655
+ fetchTenants({
656
+ skipCount: page * pageSize,
657
+ maxResultCount: pageSize,
658
+ filter: filter || void 0,
659
+ getEditionNames: true
660
+ });
661
+ }
662
+ }
663
+ },
664
+ [warn, t, deleteTenant, onTenantDeleted, fetchTenants, page, pageSize, filter]
665
+ );
666
+ const handleSaveTenant = useCallback3(async () => {
667
+ if (!tenantName.trim()) return;
668
+ setModalBusy(true);
669
+ try {
670
+ if (selectedTenant?.id) {
671
+ const result = await updateTenant({
672
+ id: selectedTenant.id,
673
+ name: tenantName,
674
+ editionId: tenantEditionId || void 0,
675
+ concurrencyStamp: selectedTenant.concurrencyStamp
676
+ });
677
+ if (result.success && result.data) {
678
+ onTenantUpdated?.(result.data);
679
+ setModalVisible(false);
680
+ fetchTenants({
681
+ skipCount: page * pageSize,
682
+ maxResultCount: pageSize,
683
+ filter: filter || void 0,
684
+ getEditionNames: true
685
+ });
686
+ }
687
+ } else {
688
+ const result = await createTenant({
689
+ name: tenantName,
690
+ editionId: tenantEditionId || void 0
691
+ });
692
+ if (result.success && result.data) {
693
+ onTenantCreated?.(result.data);
694
+ setModalVisible(false);
695
+ fetchTenants({
696
+ skipCount: page * pageSize,
697
+ maxResultCount: pageSize,
698
+ filter: filter || void 0,
699
+ getEditionNames: true
700
+ });
701
+ }
702
+ }
703
+ } finally {
704
+ setModalBusy(false);
705
+ }
706
+ }, [
707
+ tenantName,
708
+ tenantEditionId,
709
+ selectedTenant,
710
+ updateTenant,
711
+ createTenant,
712
+ onTenantUpdated,
713
+ onTenantCreated,
714
+ fetchTenants,
715
+ page,
716
+ pageSize,
717
+ filter
718
+ ]);
719
+ const handleSaveConnectionString = useCallback3(async () => {
720
+ if (!selectedTenant?.id) return;
721
+ setModalBusy(true);
722
+ try {
723
+ if (connStringUseShared || !connString.trim()) {
724
+ await deleteDefaultConnectionString(selectedTenant.id);
725
+ } else {
726
+ await updateDefaultConnectionString({
727
+ id: selectedTenant.id,
728
+ defaultConnectionString: connString
729
+ });
730
+ }
731
+ setModalVisible(false);
732
+ } finally {
733
+ setModalBusy(false);
734
+ }
735
+ }, [
736
+ selectedTenant,
737
+ connStringUseShared,
738
+ connString,
739
+ deleteDefaultConnectionString,
740
+ updateDefaultConnectionString
741
+ ]);
742
+ const handleSave = useCallback3(() => {
743
+ if (modalType === "tenant") {
744
+ handleSaveTenant();
745
+ } else if (modalType === "connectionString") {
746
+ handleSaveConnectionString();
747
+ }
748
+ }, [modalType, handleSaveTenant, handleSaveConnectionString]);
749
+ const handleCloseModal = useCallback3(() => {
750
+ setModalVisible(false);
751
+ setModalType(null);
752
+ setSelectedTenant(null);
753
+ }, [setSelectedTenant]);
754
+ const totalPages = Math.ceil(totalCount / pageSize);
755
+ const getModalTitle = () => {
756
+ if (modalType === "connectionString") {
757
+ return t("Saas::ConnectionStrings");
758
+ }
759
+ return selectedTenant?.id ? t("Saas::Edit") : t("Saas::NewTenant");
760
+ };
761
+ return /* @__PURE__ */ jsxs(Box, { id: "saas-tenants-wrapper", className: "card", children: [
762
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", mb: 4, children: [
763
+ /* @__PURE__ */ jsx(Text, { fontSize: "xl", fontWeight: "bold", children: t("Saas::Tenants") }),
764
+ /* @__PURE__ */ jsx(Button, { colorPalette: "blue", onClick: handleAddTenant, children: t("Saas::NewTenant") })
765
+ ] }),
766
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, mb: 4, children: [
767
+ /* @__PURE__ */ jsx(
768
+ Input,
769
+ {
770
+ value: filter,
771
+ onChange: (e) => setFilter(e.target.value),
772
+ placeholder: t("AbpUi::PagerSearch"),
773
+ onKeyDown: (e) => e.key === "Enter" && handleSearch()
774
+ }
775
+ ),
776
+ /* @__PURE__ */ jsx(Button, { onClick: handleSearch, children: t("AbpUi::Search") })
777
+ ] }),
778
+ error && /* @__PURE__ */ jsx(Box, { mb: 4, p: 3, bg: "red.100", borderRadius: "md", children: /* @__PURE__ */ jsx(Text, { color: "red.800", children: error }) }),
779
+ isLoading && tenants.length === 0 && /* @__PURE__ */ jsx(Flex, { justifyContent: "center", p: 8, children: /* @__PURE__ */ jsx(Spinner, { size: "lg" }) }),
780
+ !isLoading && tenants.length === 0 ? /* @__PURE__ */ jsx(Text, { textAlign: "center", p: 8, color: "gray.500", children: t("Saas::NoTenantsFound") }) : /* @__PURE__ */ jsxs(Box, { borderWidth: "1px", borderRadius: "md", overflow: "hidden", children: [
781
+ /* @__PURE__ */ jsxs(Table.Root, { children: [
782
+ /* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
783
+ /* @__PURE__ */ jsx(Table.ColumnHeader, { children: t("Saas::Actions") }),
784
+ /* @__PURE__ */ jsx(Table.ColumnHeader, { children: t("Saas::TenantName") }),
785
+ /* @__PURE__ */ jsx(Table.ColumnHeader, { children: t("Saas::EditionName") })
786
+ ] }) }),
787
+ /* @__PURE__ */ jsx(Table.Body, { children: tenants.map((tenant) => /* @__PURE__ */ jsxs(Table.Row, { children: [
788
+ /* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
789
+ /* @__PURE__ */ jsx(
790
+ Button,
791
+ {
792
+ size: "sm",
793
+ colorPalette: "blue",
794
+ variant: "outline",
795
+ onClick: () => handleEditTenant(tenant.id),
796
+ children: t("Saas::Edit")
797
+ }
798
+ ),
799
+ /* @__PURE__ */ jsx(
800
+ Button,
801
+ {
802
+ size: "sm",
803
+ variant: "outline",
804
+ onClick: () => handleEditConnectionString(tenant.id),
805
+ children: t("Saas::ConnectionStrings")
806
+ }
807
+ ),
808
+ /* @__PURE__ */ jsx(
809
+ Button,
810
+ {
811
+ size: "sm",
812
+ colorPalette: "red",
813
+ variant: "outline",
814
+ onClick: () => handleDeleteTenant(tenant),
815
+ children: t("Saas::Delete")
816
+ }
817
+ )
818
+ ] }) }),
819
+ /* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Text, { fontWeight: "medium", children: tenant.name }) }),
820
+ /* @__PURE__ */ jsx(Table.Cell, { children: tenant.editionName ? /* @__PURE__ */ jsx(Badge, { colorPalette: "blue", children: tenant.editionName }) : /* @__PURE__ */ jsx(Text, { color: "gray.500", children: "-" }) })
821
+ ] }, tenant.id)) })
822
+ ] }),
823
+ totalCount > pageSize && /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", p: 4, borderTopWidth: "1px", children: [
824
+ /* @__PURE__ */ jsx(Text, { fontSize: "sm", children: `${page * pageSize + 1} - ${Math.min((page + 1) * pageSize, totalCount)} / ${totalCount}` }),
825
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
826
+ /* @__PURE__ */ jsx(
827
+ Button,
828
+ {
829
+ size: "sm",
830
+ disabled: page === 0,
831
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
832
+ children: t("AbpUi::Previous")
833
+ }
834
+ ),
835
+ /* @__PURE__ */ jsx(
836
+ Button,
837
+ {
838
+ size: "sm",
839
+ disabled: page >= totalPages - 1,
840
+ onClick: () => setPage((p) => p + 1),
841
+ children: t("AbpUi::Next")
842
+ }
843
+ )
844
+ ] })
845
+ ] })
846
+ ] }),
847
+ /* @__PURE__ */ jsxs(
848
+ Modal,
849
+ {
850
+ visible: modalVisible,
851
+ onVisibleChange: setModalVisible,
852
+ header: getModalTitle(),
853
+ footer: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
854
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: handleCloseModal, children: t("Saas::Cancel") }),
855
+ /* @__PURE__ */ jsx(Button, { colorPalette: "blue", onClick: handleSave, loading: modalBusy, children: t("AbpIdentity::Save") })
856
+ ] }),
857
+ children: [
858
+ modalType === "tenant" && /* @__PURE__ */ jsxs(VStack, { gap: 4, align: "stretch", children: [
859
+ /* @__PURE__ */ jsx(FormField, { label: t("Saas::TenantName"), required: true, children: /* @__PURE__ */ jsx(
860
+ Input,
861
+ {
862
+ value: tenantName,
863
+ onChange: (e) => setTenantName(e.target.value),
864
+ placeholder: t("Saas::TenantName"),
865
+ autoFocus: true
866
+ }
867
+ ) }),
868
+ editions.length > 0 && /* @__PURE__ */ jsx(FormField, { label: t("Saas::Edition"), children: /* @__PURE__ */ jsx(NativeSelectRoot, { children: /* @__PURE__ */ jsxs(
869
+ NativeSelectField,
870
+ {
871
+ value: tenantEditionId,
872
+ onChange: (e) => setTenantEditionId(e.target.value),
873
+ children: [
874
+ /* @__PURE__ */ jsx("option", { value: "", children: t("Saas::SelectEdition") }),
875
+ editions.map((edition) => /* @__PURE__ */ jsx("option", { value: edition.id, children: edition.displayName }, edition.id))
876
+ ]
877
+ }
878
+ ) }) })
879
+ ] }),
880
+ modalType === "connectionString" && /* @__PURE__ */ jsxs(VStack, { gap: 4, align: "stretch", children: [
881
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
882
+ /* @__PURE__ */ jsx(
883
+ "input",
884
+ {
885
+ type: "checkbox",
886
+ id: "useSharedDatabase",
887
+ checked: connStringUseShared,
888
+ onChange: (e) => setConnStringUseShared(e.target.checked)
889
+ }
890
+ ),
891
+ /* @__PURE__ */ jsx("label", { htmlFor: "useSharedDatabase", children: t("Saas::DisplayName:UseSharedDatabase") })
892
+ ] }),
893
+ !connStringUseShared && /* @__PURE__ */ jsx(FormField, { label: t("Saas::DisplayName:DefaultConnectionString"), children: /* @__PURE__ */ jsx(
894
+ Input,
895
+ {
896
+ value: connString,
897
+ onChange: (e) => setConnString(e.target.value),
898
+ placeholder: t("Saas::DisplayName:DefaultConnectionString")
899
+ }
900
+ ) })
901
+ ] })
902
+ ]
903
+ }
904
+ )
905
+ ] });
906
+ }
907
+
908
+ // src/components/Editions/EditionsComponent.tsx
909
+ import { useEffect as useEffect2, useState as useState4, useCallback as useCallback4 } from "react";
910
+ import { useLocalization as useLocalization2 } from "@abpjs/core";
911
+ import { Modal as Modal2, Button as Button2, FormField as FormField2, useConfirmation as useConfirmation2, Toaster as Toaster2 } from "@abpjs/theme-shared";
912
+ import {
913
+ Box as Box2,
914
+ Flex as Flex2,
915
+ Input as Input2,
916
+ Table as Table2,
917
+ Spinner as Spinner2,
918
+ VStack as VStack2,
919
+ Text as Text2
920
+ } from "@chakra-ui/react";
921
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
922
+ function EditionsComponent({
923
+ onEditionCreated,
924
+ onEditionUpdated,
925
+ onEditionDeleted,
926
+ onManageFeatures
927
+ }) {
928
+ const { t } = useLocalization2();
929
+ const { warn } = useConfirmation2();
930
+ const {
931
+ editions,
932
+ totalCount,
933
+ selectedEdition,
934
+ isLoading,
935
+ error,
936
+ fetchEditions,
937
+ getEditionById,
938
+ createEdition,
939
+ updateEdition,
940
+ deleteEdition,
941
+ setSelectedEdition
942
+ } = useEditions();
943
+ const [modalVisible, setModalVisible] = useState4(false);
944
+ const [modalBusy, setModalBusy] = useState4(false);
945
+ const [page, setPage] = useState4(0);
946
+ const pageSize = 10;
947
+ const [filter, setFilter] = useState4("");
948
+ const [displayName, setDisplayName] = useState4("");
949
+ useEffect2(() => {
950
+ fetchEditions({
951
+ skipCount: page * pageSize,
952
+ maxResultCount: pageSize,
953
+ filter: filter || void 0
954
+ });
955
+ }, [page, pageSize, fetchEditions]);
956
+ const handleSearch = useCallback4(() => {
957
+ setPage(0);
958
+ fetchEditions({
959
+ skipCount: 0,
960
+ maxResultCount: pageSize,
961
+ filter: filter || void 0
962
+ });
963
+ }, [filter, pageSize, fetchEditions]);
964
+ const handleAddEdition = useCallback4(() => {
965
+ setSelectedEdition(null);
966
+ setDisplayName("");
967
+ setModalVisible(true);
968
+ }, [setSelectedEdition]);
969
+ const handleEditEdition = useCallback4(
970
+ async (id) => {
971
+ const result = await getEditionById(id);
972
+ if (result.success && result.data) {
973
+ setDisplayName(result.data.displayName);
974
+ setModalVisible(true);
975
+ }
976
+ },
977
+ [getEditionById]
978
+ );
979
+ const handleDeleteEdition = useCallback4(
980
+ async (edition) => {
981
+ const status = await warn(
982
+ t("Saas::EditionDeletionConfirmationMessage").replace("{0}", edition.displayName),
983
+ t("Saas::AreYouSure")
984
+ );
985
+ if (status === Toaster2.Status.confirm) {
986
+ const result = await deleteEdition(edition.id);
987
+ if (result.success) {
988
+ onEditionDeleted?.(edition.id);
989
+ fetchEditions({
990
+ skipCount: page * pageSize,
991
+ maxResultCount: pageSize,
992
+ filter: filter || void 0
993
+ });
994
+ }
995
+ }
996
+ },
997
+ [warn, t, deleteEdition, onEditionDeleted, fetchEditions, page, pageSize, filter]
998
+ );
999
+ const handleManageFeatures = useCallback4(
1000
+ (editionId) => {
1001
+ onManageFeatures?.(editionId);
1002
+ },
1003
+ [onManageFeatures]
1004
+ );
1005
+ const handleSave = useCallback4(async () => {
1006
+ if (!displayName.trim()) return;
1007
+ setModalBusy(true);
1008
+ try {
1009
+ if (selectedEdition?.id) {
1010
+ const result = await updateEdition({
1011
+ id: selectedEdition.id,
1012
+ displayName,
1013
+ concurrencyStamp: selectedEdition.concurrencyStamp
1014
+ });
1015
+ if (result.success && result.data) {
1016
+ onEditionUpdated?.(result.data);
1017
+ setModalVisible(false);
1018
+ fetchEditions({
1019
+ skipCount: page * pageSize,
1020
+ maxResultCount: pageSize,
1021
+ filter: filter || void 0
1022
+ });
1023
+ }
1024
+ } else {
1025
+ const result = await createEdition({
1026
+ displayName
1027
+ });
1028
+ if (result.success && result.data) {
1029
+ onEditionCreated?.(result.data);
1030
+ setModalVisible(false);
1031
+ fetchEditions({
1032
+ skipCount: page * pageSize,
1033
+ maxResultCount: pageSize,
1034
+ filter: filter || void 0
1035
+ });
1036
+ }
1037
+ }
1038
+ } finally {
1039
+ setModalBusy(false);
1040
+ }
1041
+ }, [
1042
+ displayName,
1043
+ selectedEdition,
1044
+ updateEdition,
1045
+ createEdition,
1046
+ onEditionUpdated,
1047
+ onEditionCreated,
1048
+ fetchEditions,
1049
+ page,
1050
+ pageSize,
1051
+ filter
1052
+ ]);
1053
+ const handleCloseModal = useCallback4(() => {
1054
+ setModalVisible(false);
1055
+ setSelectedEdition(null);
1056
+ }, [setSelectedEdition]);
1057
+ const totalPages = Math.ceil(totalCount / pageSize);
1058
+ return /* @__PURE__ */ jsxs2(Box2, { id: "saas-editions-wrapper", className: "card", children: [
1059
+ /* @__PURE__ */ jsxs2(Flex2, { justifyContent: "space-between", alignItems: "center", mb: 4, children: [
1060
+ /* @__PURE__ */ jsx2(Text2, { fontSize: "xl", fontWeight: "bold", children: t("Saas::Editions") }),
1061
+ /* @__PURE__ */ jsx2(Button2, { colorPalette: "blue", onClick: handleAddEdition, children: t("Saas::NewEdition") })
1062
+ ] }),
1063
+ /* @__PURE__ */ jsxs2(Flex2, { gap: 2, mb: 4, children: [
1064
+ /* @__PURE__ */ jsx2(
1065
+ Input2,
1066
+ {
1067
+ value: filter,
1068
+ onChange: (e) => setFilter(e.target.value),
1069
+ placeholder: t("AbpUi::PagerSearch"),
1070
+ onKeyDown: (e) => e.key === "Enter" && handleSearch()
1071
+ }
1072
+ ),
1073
+ /* @__PURE__ */ jsx2(Button2, { onClick: handleSearch, children: t("AbpUi::Search") })
1074
+ ] }),
1075
+ error && /* @__PURE__ */ jsx2(Box2, { mb: 4, p: 3, bg: "red.100", borderRadius: "md", children: /* @__PURE__ */ jsx2(Text2, { color: "red.800", children: error }) }),
1076
+ isLoading && editions.length === 0 && /* @__PURE__ */ jsx2(Flex2, { justifyContent: "center", p: 8, children: /* @__PURE__ */ jsx2(Spinner2, { size: "lg" }) }),
1077
+ !isLoading && editions.length === 0 ? /* @__PURE__ */ jsx2(Text2, { textAlign: "center", p: 8, color: "gray.500", children: t("Saas::NoEditionsFound") }) : /* @__PURE__ */ jsxs2(Box2, { borderWidth: "1px", borderRadius: "md", overflow: "hidden", children: [
1078
+ /* @__PURE__ */ jsxs2(Table2.Root, { children: [
1079
+ /* @__PURE__ */ jsx2(Table2.Header, { children: /* @__PURE__ */ jsxs2(Table2.Row, { children: [
1080
+ /* @__PURE__ */ jsx2(Table2.ColumnHeader, { children: t("Saas::Actions") }),
1081
+ /* @__PURE__ */ jsx2(Table2.ColumnHeader, { children: t("Saas::EditionName") })
1082
+ ] }) }),
1083
+ /* @__PURE__ */ jsx2(Table2.Body, { children: editions.map((edition) => /* @__PURE__ */ jsxs2(Table2.Row, { children: [
1084
+ /* @__PURE__ */ jsx2(Table2.Cell, { children: /* @__PURE__ */ jsxs2(Flex2, { gap: 1, children: [
1085
+ /* @__PURE__ */ jsx2(
1086
+ Button2,
1087
+ {
1088
+ size: "sm",
1089
+ colorPalette: "blue",
1090
+ variant: "outline",
1091
+ onClick: () => handleEditEdition(edition.id),
1092
+ children: t("Saas::Edit")
1093
+ }
1094
+ ),
1095
+ onManageFeatures && /* @__PURE__ */ jsx2(
1096
+ Button2,
1097
+ {
1098
+ size: "sm",
1099
+ variant: "outline",
1100
+ onClick: () => handleManageFeatures(edition.id),
1101
+ children: t("Saas::Features")
1102
+ }
1103
+ ),
1104
+ /* @__PURE__ */ jsx2(
1105
+ Button2,
1106
+ {
1107
+ size: "sm",
1108
+ colorPalette: "red",
1109
+ variant: "outline",
1110
+ onClick: () => handleDeleteEdition(edition),
1111
+ children: t("Saas::Delete")
1112
+ }
1113
+ )
1114
+ ] }) }),
1115
+ /* @__PURE__ */ jsx2(Table2.Cell, { children: /* @__PURE__ */ jsx2(Text2, { fontWeight: "medium", children: edition.displayName }) })
1116
+ ] }, edition.id)) })
1117
+ ] }),
1118
+ totalCount > pageSize && /* @__PURE__ */ jsxs2(Flex2, { justifyContent: "space-between", alignItems: "center", p: 4, borderTopWidth: "1px", children: [
1119
+ /* @__PURE__ */ jsx2(Text2, { fontSize: "sm", children: `${page * pageSize + 1} - ${Math.min((page + 1) * pageSize, totalCount)} / ${totalCount}` }),
1120
+ /* @__PURE__ */ jsxs2(Flex2, { gap: 2, children: [
1121
+ /* @__PURE__ */ jsx2(
1122
+ Button2,
1123
+ {
1124
+ size: "sm",
1125
+ disabled: page === 0,
1126
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1127
+ children: t("AbpUi::Previous")
1128
+ }
1129
+ ),
1130
+ /* @__PURE__ */ jsx2(
1131
+ Button2,
1132
+ {
1133
+ size: "sm",
1134
+ disabled: page >= totalPages - 1,
1135
+ onClick: () => setPage((p) => p + 1),
1136
+ children: t("AbpUi::Next")
1137
+ }
1138
+ )
1139
+ ] })
1140
+ ] })
1141
+ ] }),
1142
+ /* @__PURE__ */ jsx2(
1143
+ Modal2,
1144
+ {
1145
+ visible: modalVisible,
1146
+ onVisibleChange: setModalVisible,
1147
+ header: selectedEdition?.id ? t("Saas::Edit") : t("Saas::NewEdition"),
1148
+ footer: /* @__PURE__ */ jsxs2(Flex2, { gap: 2, children: [
1149
+ /* @__PURE__ */ jsx2(Button2, { variant: "outline", onClick: handleCloseModal, children: t("Saas::Cancel") }),
1150
+ /* @__PURE__ */ jsx2(Button2, { colorPalette: "blue", onClick: handleSave, loading: modalBusy, children: t("AbpIdentity::Save") })
1151
+ ] }),
1152
+ children: /* @__PURE__ */ jsx2(VStack2, { gap: 4, align: "stretch", children: /* @__PURE__ */ jsx2(FormField2, { label: t("Saas::EditionName"), required: true, children: /* @__PURE__ */ jsx2(
1153
+ Input2,
1154
+ {
1155
+ value: displayName,
1156
+ onChange: (e) => setDisplayName(e.target.value),
1157
+ placeholder: t("Saas::EditionName"),
1158
+ autoFocus: true
1159
+ }
1160
+ ) }) })
1161
+ }
1162
+ )
1163
+ ] });
1164
+ }
1165
+ export {
1166
+ EditionsComponent,
1167
+ SAAS_ROUTES,
1168
+ SaasService,
1169
+ TenantsComponent,
1170
+ useEditions,
1171
+ useTenants
1172
+ };