@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/LICENSE +165 -0
- package/README.md +374 -0
- package/dist/index.d.mts +470 -0
- package/dist/index.d.ts +470 -0
- package/dist/index.js +1187 -0
- package/dist/index.mjs +1172 -0
- package/package.json +90 -0
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
|
+
};
|