@qwickapps/server 1.2.0 → 1.3.1

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.
Files changed (299) hide show
  1. package/README.md +392 -0
  2. package/dist/core/control-panel.d.ts +7 -2
  3. package/dist/core/control-panel.d.ts.map +1 -1
  4. package/dist/core/control-panel.js +120 -54
  5. package/dist/core/control-panel.js.map +1 -1
  6. package/dist/core/gateway.d.ts +159 -79
  7. package/dist/core/gateway.d.ts.map +1 -1
  8. package/dist/core/gateway.js +679 -319
  9. package/dist/core/gateway.js.map +1 -1
  10. package/dist/core/index.d.ts +3 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/plugin-registry.d.ts +307 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +352 -0
  17. package/dist/core/plugin-registry.js.map +1 -0
  18. package/dist/core/types.d.ts +16 -33
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
  25. package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
  26. package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
  27. package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
  28. package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
  29. package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
  30. package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
  31. package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
  32. package/dist/plugins/auth/adapters/index.d.ts +10 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +10 -0
  35. package/dist/plugins/auth/adapters/index.js.map +1 -0
  36. package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
  37. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
  38. package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
  39. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
  40. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  41. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  42. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  43. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  45. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.js +255 -0
  47. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  48. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  49. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  51. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  52. package/dist/plugins/auth/env-config.d.ts +88 -0
  53. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  54. package/dist/plugins/auth/env-config.js +489 -0
  55. package/dist/plugins/auth/env-config.js.map +1 -0
  56. package/dist/plugins/auth/index.d.ts +14 -0
  57. package/dist/plugins/auth/index.d.ts.map +1 -0
  58. package/dist/plugins/auth/index.js +16 -0
  59. package/dist/plugins/auth/index.js.map +1 -0
  60. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  61. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  62. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  63. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  64. package/dist/plugins/auth/types.d.ts +218 -0
  65. package/dist/plugins/auth/types.d.ts.map +1 -0
  66. package/dist/plugins/auth/types.js +14 -0
  67. package/dist/plugins/auth/types.js.map +1 -0
  68. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  69. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  70. package/dist/plugins/bans/bans-plugin.js +428 -0
  71. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  72. package/dist/plugins/bans/index.d.ts +9 -0
  73. package/dist/plugins/bans/index.d.ts.map +1 -0
  74. package/dist/plugins/bans/index.js +10 -0
  75. package/dist/plugins/bans/index.js.map +1 -0
  76. package/dist/plugins/bans/stores/index.d.ts +7 -0
  77. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  78. package/dist/plugins/bans/stores/index.js +7 -0
  79. package/dist/plugins/bans/stores/index.js.map +1 -0
  80. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  81. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  82. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  83. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  84. package/dist/plugins/bans/types.d.ts +128 -0
  85. package/dist/plugins/bans/types.d.ts.map +1 -0
  86. package/dist/plugins/bans/types.js +11 -0
  87. package/dist/plugins/bans/types.js.map +1 -0
  88. package/dist/plugins/cache-plugin.d.ts +14 -3
  89. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  90. package/dist/plugins/cache-plugin.js +27 -7
  91. package/dist/plugins/cache-plugin.js.map +1 -1
  92. package/dist/plugins/cache-plugin.test.js +99 -32
  93. package/dist/plugins/cache-plugin.test.js.map +1 -1
  94. package/dist/plugins/config-plugin.d.ts +3 -2
  95. package/dist/plugins/config-plugin.d.ts.map +1 -1
  96. package/dist/plugins/config-plugin.js +17 -10
  97. package/dist/plugins/config-plugin.js.map +1 -1
  98. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  99. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  100. package/dist/plugins/diagnostics-plugin.js +17 -10
  101. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  102. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  103. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  105. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  106. package/dist/plugins/entitlements/index.d.ts +12 -0
  107. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/index.js +16 -0
  109. package/dist/plugins/entitlements/index.js.map +1 -0
  110. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  111. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  112. package/dist/plugins/entitlements/sources/index.js +9 -0
  113. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  114. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  115. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  116. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  117. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  118. package/dist/plugins/entitlements/types.d.ts +232 -0
  119. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  120. package/dist/plugins/entitlements/types.js +11 -0
  121. package/dist/plugins/entitlements/types.js.map +1 -0
  122. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  123. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  124. package/dist/plugins/frontend-app-plugin.js +14 -9
  125. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  126. package/dist/plugins/health-plugin.d.ts +5 -2
  127. package/dist/plugins/health-plugin.d.ts.map +1 -1
  128. package/dist/plugins/health-plugin.js +20 -5
  129. package/dist/plugins/health-plugin.js.map +1 -1
  130. package/dist/plugins/index.d.ts +10 -2
  131. package/dist/plugins/index.d.ts.map +1 -1
  132. package/dist/plugins/index.js +10 -2
  133. package/dist/plugins/index.js.map +1 -1
  134. package/dist/plugins/logs-plugin.d.ts +3 -2
  135. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  136. package/dist/plugins/logs-plugin.js +21 -12
  137. package/dist/plugins/logs-plugin.js.map +1 -1
  138. package/dist/plugins/postgres-plugin.d.ts +3 -3
  139. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  140. package/dist/plugins/postgres-plugin.js +9 -7
  141. package/dist/plugins/postgres-plugin.js.map +1 -1
  142. package/dist/plugins/postgres-plugin.test.js +50 -29
  143. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  144. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  145. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  146. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  147. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  148. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  149. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  150. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  151. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  152. package/dist/plugins/preferences/index.d.ts +12 -0
  153. package/dist/plugins/preferences/index.d.ts.map +1 -0
  154. package/dist/plugins/preferences/index.js +13 -0
  155. package/dist/plugins/preferences/index.js.map +1 -0
  156. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  157. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  158. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  159. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  160. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  161. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  162. package/dist/plugins/preferences/stores/index.js +9 -0
  163. package/dist/plugins/preferences/stores/index.js.map +1 -0
  164. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  165. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  166. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  167. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  168. package/dist/plugins/preferences/types.d.ts +91 -0
  169. package/dist/plugins/preferences/types.d.ts.map +1 -0
  170. package/dist/plugins/preferences/types.js +10 -0
  171. package/dist/plugins/preferences/types.js.map +1 -0
  172. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  173. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  174. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  175. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  176. package/dist/plugins/users/index.d.ts +12 -0
  177. package/dist/plugins/users/index.d.ts.map +1 -0
  178. package/dist/plugins/users/index.js +13 -0
  179. package/dist/plugins/users/index.js.map +1 -0
  180. package/dist/plugins/users/stores/index.d.ts +7 -0
  181. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  182. package/dist/plugins/users/stores/index.js +7 -0
  183. package/dist/plugins/users/stores/index.js.map +1 -0
  184. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  185. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  186. package/dist/plugins/users/stores/postgres-store.js +157 -0
  187. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  188. package/dist/plugins/users/types.d.ts +225 -0
  189. package/dist/plugins/users/types.d.ts.map +1 -0
  190. package/dist/plugins/users/types.js +12 -0
  191. package/dist/plugins/users/types.js.map +1 -0
  192. package/dist/plugins/users/users-plugin.d.ts +45 -0
  193. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  194. package/dist/plugins/users/users-plugin.js +359 -0
  195. package/dist/plugins/users/users-plugin.js.map +1 -0
  196. package/dist-ui/assets/index-BY8OxNgO.js +465 -0
  197. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  198. package/dist-ui/index.html +1 -1
  199. package/dist-ui-lib/api/controlPanelApi.d.ts +278 -0
  200. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  201. package/dist-ui-lib/components/index.d.ts +18 -0
  202. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  203. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  204. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  205. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  206. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +48 -0
  207. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +25 -0
  208. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  209. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  210. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  211. package/dist-ui-lib/index.js +5172 -0
  212. package/dist-ui-lib/index.js.map +1 -0
  213. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  214. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  215. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  216. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  217. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  218. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  219. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  220. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  221. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  222. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  223. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  224. package/package.json +24 -7
  225. package/src/core/control-panel.ts +145 -61
  226. package/src/core/gateway.ts +863 -403
  227. package/src/core/index.ts +21 -2
  228. package/src/core/plugin-registry.ts +716 -0
  229. package/src/core/types.ts +31 -37
  230. package/src/index.ts +125 -19
  231. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  232. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  233. package/src/plugins/auth/adapters/index.ts +10 -0
  234. package/src/plugins/auth/adapters/supabase-adapter.ts +149 -0
  235. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  236. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  237. package/src/plugins/auth/auth-plugin.ts +303 -0
  238. package/src/plugins/auth/env-config.ts +572 -0
  239. package/src/plugins/auth/index.ts +42 -0
  240. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  241. package/src/plugins/auth/types.ts +245 -0
  242. package/src/plugins/bans/bans-plugin.ts +485 -0
  243. package/src/plugins/bans/index.ts +31 -0
  244. package/src/plugins/bans/stores/index.ts +7 -0
  245. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  246. package/src/plugins/bans/types.ts +141 -0
  247. package/src/plugins/cache-plugin.test.ts +108 -32
  248. package/src/plugins/cache-plugin.ts +40 -9
  249. package/src/plugins/config-plugin.ts +23 -12
  250. package/src/plugins/diagnostics-plugin.ts +22 -12
  251. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  252. package/src/plugins/entitlements/index.ts +51 -0
  253. package/src/plugins/entitlements/sources/index.ts +9 -0
  254. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  255. package/src/plugins/entitlements/types.ts +256 -0
  256. package/src/plugins/frontend-app-plugin.ts +24 -12
  257. package/src/plugins/health-plugin.ts +27 -7
  258. package/src/plugins/index.ts +132 -4
  259. package/src/plugins/logs-plugin.ts +28 -14
  260. package/src/plugins/postgres-plugin.test.ts +52 -29
  261. package/src/plugins/postgres-plugin.ts +11 -9
  262. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  263. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  264. package/src/plugins/preferences/index.ts +30 -0
  265. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  266. package/src/plugins/preferences/stores/index.ts +9 -0
  267. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  268. package/src/plugins/preferences/types.ts +100 -0
  269. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  270. package/src/plugins/users/index.ts +38 -0
  271. package/src/plugins/users/stores/index.ts +7 -0
  272. package/src/plugins/users/stores/postgres-store.ts +225 -0
  273. package/src/plugins/users/types.ts +247 -0
  274. package/src/plugins/users/users-plugin.ts +418 -0
  275. package/ui/src/App.tsx +188 -31
  276. package/ui/src/api/controlPanelApi.ts +453 -1
  277. package/ui/src/components/ControlPanelApp.tsx +212 -0
  278. package/ui/src/components/index.ts +62 -0
  279. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  280. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  281. package/ui/src/dashboard/PluginWidgetRenderer.tsx +118 -0
  282. package/ui/src/dashboard/WidgetComponentRegistry.tsx +120 -0
  283. package/ui/src/dashboard/builtInWidgets.tsx +35 -0
  284. package/ui/src/dashboard/index.ts +35 -0
  285. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  286. package/ui/src/dashboard/widgets/index.ts +7 -0
  287. package/ui/src/pages/AuthPage.tsx +259 -0
  288. package/ui/src/pages/DashboardPage.tsx +28 -149
  289. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  290. package/ui/src/pages/LogsPage.tsx +174 -8
  291. package/ui/src/pages/PluginPage.tsx +148 -0
  292. package/ui/src/pages/PluginsPage.tsx +394 -0
  293. package/ui/src/pages/SystemPage.tsx +445 -0
  294. package/ui/src/pages/UsersPage.tsx +837 -0
  295. package/ui/tsconfig.lib.json +11 -0
  296. package/ui/vite.lib.config.ts +56 -0
  297. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  298. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  299. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -0,0 +1,557 @@
1
+ /**
2
+ * EntitlementsPage Component
3
+ *
4
+ * Entitlement catalog management page. Allows viewing and managing available entitlements.
5
+ * Write operations (create, edit, delete) are only available when source is not readonly.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import { useState, useEffect, useCallback } from 'react';
11
+ import {
12
+ Box,
13
+ Card,
14
+ CardContent,
15
+ TextField,
16
+ Table,
17
+ TableBody,
18
+ TableCell,
19
+ TableContainer,
20
+ TableHead,
21
+ TableRow,
22
+ Chip,
23
+ Alert,
24
+ LinearProgress,
25
+ InputAdornment,
26
+ CircularProgress,
27
+ Tooltip,
28
+ IconButton,
29
+ } from '@mui/material';
30
+ import { Text, Button, Dialog, DialogTitle, DialogContent, DialogActions, GridLayout } from '@qwickapps/react-framework';
31
+ import SearchIcon from '@mui/icons-material/Search';
32
+ import LocalOfferIcon from '@mui/icons-material/LocalOffer';
33
+ import EditIcon from '@mui/icons-material/Edit';
34
+ import DeleteIcon from '@mui/icons-material/Delete';
35
+ import LockIcon from '@mui/icons-material/Lock';
36
+ import {
37
+ api,
38
+ type EntitlementDefinition,
39
+ type EntitlementsStatus,
40
+ } from '../api/controlPanelApi';
41
+
42
+ export interface EntitlementsPageProps {
43
+ /** Page title */
44
+ title?: string;
45
+ /** Page subtitle */
46
+ subtitle?: string;
47
+ /** Custom actions to render in the header */
48
+ headerActions?: React.ReactNode;
49
+ }
50
+
51
+ export function EntitlementsPage({
52
+ title = 'Entitlements',
53
+ subtitle = 'Manage available entitlements',
54
+ headerActions,
55
+ }: EntitlementsPageProps) {
56
+ // Status state
57
+ const [status, setStatus] = useState<EntitlementsStatus | null>(null);
58
+ const [statusLoading, setStatusLoading] = useState(true);
59
+
60
+ // Entitlements state
61
+ const [entitlements, setEntitlements] = useState<EntitlementDefinition[]>([]);
62
+ const [filteredEntitlements, setFilteredEntitlements] = useState<EntitlementDefinition[]>([]);
63
+ const [loading, setLoading] = useState(true);
64
+ const [error, setError] = useState<string | null>(null);
65
+ const [success, setSuccess] = useState<string | null>(null);
66
+ const [search, setSearch] = useState('');
67
+
68
+ // Dialog state
69
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
70
+ const [editDialogOpen, setEditDialogOpen] = useState(false);
71
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
72
+ const [selectedEntitlement, setSelectedEntitlement] = useState<EntitlementDefinition | null>(null);
73
+ const [newEntitlement, setNewEntitlement] = useState({
74
+ name: '',
75
+ category: '',
76
+ description: '',
77
+ });
78
+ const [saving, setSaving] = useState(false);
79
+
80
+ // Fetch status
81
+ useEffect(() => {
82
+ api.getEntitlementsStatus()
83
+ .then(setStatus)
84
+ .catch((err) => setError(err instanceof Error ? err.message : 'Failed to get status'))
85
+ .finally(() => setStatusLoading(false));
86
+ }, []);
87
+
88
+ // Fetch entitlements
89
+ const fetchEntitlements = useCallback(async () => {
90
+ setLoading(true);
91
+ try {
92
+ const data = await api.getAvailableEntitlements();
93
+ setEntitlements(data);
94
+ setError(null);
95
+ } catch (err) {
96
+ setError(err instanceof Error ? err.message : 'Failed to fetch entitlements');
97
+ } finally {
98
+ setLoading(false);
99
+ }
100
+ }, []);
101
+
102
+ useEffect(() => {
103
+ fetchEntitlements();
104
+ }, [fetchEntitlements]);
105
+
106
+ // Filter entitlements based on search
107
+ useEffect(() => {
108
+ if (!search.trim()) {
109
+ setFilteredEntitlements(entitlements);
110
+ } else {
111
+ const lowerSearch = search.toLowerCase();
112
+ setFilteredEntitlements(
113
+ entitlements.filter(
114
+ (e) =>
115
+ e.name.toLowerCase().includes(lowerSearch) ||
116
+ e.category?.toLowerCase().includes(lowerSearch) ||
117
+ e.description?.toLowerCase().includes(lowerSearch)
118
+ )
119
+ );
120
+ }
121
+ }, [entitlements, search]);
122
+
123
+ // Group entitlements by category
124
+ const categories = [...new Set(entitlements.map((e) => e.category || 'Uncategorized'))];
125
+
126
+ // Handlers
127
+ const handleCreate = async () => {
128
+ if (!newEntitlement.name.trim()) {
129
+ setError('Name is required');
130
+ return;
131
+ }
132
+
133
+ setSaving(true);
134
+ try {
135
+ // TODO: Add create entitlement API endpoint
136
+ // For now, just show success
137
+ setSuccess(`Entitlement "${newEntitlement.name}" created`);
138
+ setCreateDialogOpen(false);
139
+ setNewEntitlement({ name: '', category: '', description: '' });
140
+ fetchEntitlements();
141
+ } catch (err) {
142
+ setError(err instanceof Error ? err.message : 'Failed to create entitlement');
143
+ } finally {
144
+ setSaving(false);
145
+ }
146
+ };
147
+
148
+ const handleEdit = async () => {
149
+ if (!selectedEntitlement) return;
150
+
151
+ setSaving(true);
152
+ try {
153
+ // TODO: Add update entitlement API endpoint
154
+ setSuccess(`Entitlement "${selectedEntitlement.name}" updated`);
155
+ setEditDialogOpen(false);
156
+ setSelectedEntitlement(null);
157
+ fetchEntitlements();
158
+ } catch (err) {
159
+ setError(err instanceof Error ? err.message : 'Failed to update entitlement');
160
+ } finally {
161
+ setSaving(false);
162
+ }
163
+ };
164
+
165
+ const handleDelete = async () => {
166
+ if (!selectedEntitlement) return;
167
+
168
+ setSaving(true);
169
+ try {
170
+ // TODO: Add delete entitlement API endpoint
171
+ setSuccess(`Entitlement "${selectedEntitlement.name}" deleted`);
172
+ setDeleteDialogOpen(false);
173
+ setSelectedEntitlement(null);
174
+ fetchEntitlements();
175
+ } catch (err) {
176
+ setError(err instanceof Error ? err.message : 'Failed to delete entitlement');
177
+ } finally {
178
+ setSaving(false);
179
+ }
180
+ };
181
+
182
+ const openEditDialog = (entitlement: EntitlementDefinition) => {
183
+ setSelectedEntitlement(entitlement);
184
+ setEditDialogOpen(true);
185
+ };
186
+
187
+ const openDeleteDialog = (entitlement: EntitlementDefinition) => {
188
+ setSelectedEntitlement(entitlement);
189
+ setDeleteDialogOpen(true);
190
+ };
191
+
192
+ const isReadonly = status?.readonly ?? true;
193
+
194
+ if (statusLoading) {
195
+ return (
196
+ <Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
197
+ <CircularProgress />
198
+ </Box>
199
+ );
200
+ }
201
+
202
+ return (
203
+ <Box>
204
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
205
+ <Box>
206
+ <Text variant="h4" content={title} customColor="var(--theme-text-primary)" />
207
+ <Text variant="body2" content={subtitle} customColor="var(--theme-text-secondary)" />
208
+ </Box>
209
+ <Box sx={{ display: 'flex', gap: 1 }}>
210
+ {headerActions}
211
+ {!isReadonly && (
212
+ <Button
213
+ variant="primary"
214
+ icon="add"
215
+ label="Add Entitlement"
216
+ onClick={() => setCreateDialogOpen(true)}
217
+ />
218
+ )}
219
+ </Box>
220
+ </Box>
221
+
222
+ {loading && <LinearProgress sx={{ mb: 2 }} />}
223
+
224
+ {error && (
225
+ <Alert severity="error" onClose={() => setError(null)} sx={{ mb: 2 }}>
226
+ {error}
227
+ </Alert>
228
+ )}
229
+
230
+ {success && (
231
+ <Alert severity="success" onClose={() => setSuccess(null)} sx={{ mb: 2 }}>
232
+ {success}
233
+ </Alert>
234
+ )}
235
+
236
+ {/* Status Cards */}
237
+ <GridLayout columns={3} spacing="medium" sx={{ mb: 3 }} equalHeight>
238
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
239
+ <CardContent>
240
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
241
+ <LocalOfferIcon sx={{ fontSize: 40, color: 'var(--theme-primary)' }} />
242
+ <Box>
243
+ <Text variant="h4" content={entitlements.length.toString()} customColor="var(--theme-text-primary)" />
244
+ <Text variant="body2" content="Total Entitlements" customColor="var(--theme-text-secondary)" />
245
+ </Box>
246
+ </Box>
247
+ </CardContent>
248
+ </Card>
249
+
250
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
251
+ <CardContent>
252
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
253
+ <Box
254
+ sx={{
255
+ width: 40,
256
+ height: 40,
257
+ borderRadius: 1,
258
+ bgcolor: 'var(--theme-primary)20',
259
+ display: 'flex',
260
+ alignItems: 'center',
261
+ justifyContent: 'center',
262
+ }}
263
+ >
264
+ <Text variant="h6" content={categories.length.toString()} customColor="var(--theme-primary)" />
265
+ </Box>
266
+ <Box>
267
+ <Text variant="body1" fontWeight="500" content="Categories" customColor="var(--theme-text-primary)" />
268
+ <Text variant="body2" content={categories.slice(0, 3).join(', ')} customColor="var(--theme-text-secondary)" />
269
+ </Box>
270
+ </Box>
271
+ </CardContent>
272
+ </Card>
273
+
274
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
275
+ <CardContent>
276
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
277
+ {isReadonly ? (
278
+ <LockIcon sx={{ fontSize: 40, color: 'var(--theme-warning)' }} />
279
+ ) : (
280
+ <EditIcon sx={{ fontSize: 40, color: 'var(--theme-success)' }} />
281
+ )}
282
+ <Box>
283
+ <Text
284
+ variant="body1"
285
+ fontWeight="500"
286
+ content={isReadonly ? 'Read-only' : 'Editable'}
287
+ customColor={isReadonly ? 'var(--theme-warning)' : 'var(--theme-success)'}
288
+ />
289
+ <Text variant="body2" content={`Source: ${status?.sources[0]?.name || 'Unknown'}`} customColor="var(--theme-text-secondary)" />
290
+ </Box>
291
+ </Box>
292
+ </CardContent>
293
+ </Card>
294
+ </GridLayout>
295
+
296
+ {/* Main Content */}
297
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
298
+ <CardContent sx={{ p: 0 }}>
299
+ {/* Search Bar */}
300
+ <Box sx={{ p: 2, borderBottom: 1, borderColor: 'var(--theme-border)' }}>
301
+ <TextField
302
+ size="small"
303
+ placeholder="Search entitlements..."
304
+ value={search}
305
+ onChange={(e) => setSearch(e.target.value)}
306
+ InputProps={{
307
+ startAdornment: (
308
+ <InputAdornment position="start">
309
+ <SearchIcon sx={{ color: 'var(--theme-text-secondary)' }} />
310
+ </InputAdornment>
311
+ ),
312
+ }}
313
+ sx={{ minWidth: 300 }}
314
+ />
315
+ </Box>
316
+
317
+ {/* Entitlements Table */}
318
+ <TableContainer>
319
+ <Table>
320
+ <TableHead>
321
+ <TableRow>
322
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Name</TableCell>
323
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Category</TableCell>
324
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>Description</TableCell>
325
+ {!isReadonly && (
326
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }} align="right">Actions</TableCell>
327
+ )}
328
+ </TableRow>
329
+ </TableHead>
330
+ <TableBody>
331
+ {filteredEntitlements.map((entitlement) => (
332
+ <TableRow key={entitlement.id} hover>
333
+ <TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
334
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
335
+ <LocalOfferIcon sx={{ fontSize: 18, color: 'var(--theme-primary)' }} />
336
+ <Text variant="body1" content={entitlement.name} fontWeight="500" />
337
+ </Box>
338
+ </TableCell>
339
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
340
+ {entitlement.category ? (
341
+ <Chip
342
+ size="small"
343
+ label={entitlement.category}
344
+ sx={{
345
+ bgcolor: 'var(--theme-primary)20',
346
+ color: 'var(--theme-primary)',
347
+ }}
348
+ />
349
+ ) : (
350
+ <Text variant="body2" content="--" customColor="var(--theme-text-secondary)" />
351
+ )}
352
+ </TableCell>
353
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', maxWidth: 300 }}>
354
+ {entitlement.description || '--'}
355
+ </TableCell>
356
+ {!isReadonly && (
357
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }} align="right">
358
+ <Tooltip title="Edit">
359
+ <IconButton size="small" onClick={() => openEditDialog(entitlement)}>
360
+ <EditIcon fontSize="small" />
361
+ </IconButton>
362
+ </Tooltip>
363
+ <Tooltip title="Delete">
364
+ <IconButton size="small" onClick={() => openDeleteDialog(entitlement)} sx={{ color: 'var(--theme-error)' }}>
365
+ <DeleteIcon fontSize="small" />
366
+ </IconButton>
367
+ </Tooltip>
368
+ </TableCell>
369
+ )}
370
+ </TableRow>
371
+ ))}
372
+ {filteredEntitlements.length === 0 && !loading && (
373
+ <TableRow>
374
+ <TableCell colSpan={isReadonly ? 3 : 4} align="center" sx={{ py: 4, color: 'var(--theme-text-secondary)' }}>
375
+ {search ? 'No entitlements match your search' : 'No entitlements defined'}
376
+ </TableCell>
377
+ </TableRow>
378
+ )}
379
+ </TableBody>
380
+ </Table>
381
+ </TableContainer>
382
+ </CardContent>
383
+ </Card>
384
+
385
+ {/* Source Info */}
386
+ {status && status.sources.length > 0 && (
387
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mt: 3 }}>
388
+ <CardContent>
389
+ <Text variant="subtitle2" content="Entitlement Sources" customColor="var(--theme-text-secondary)" style={{ marginBottom: '12px' }} />
390
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
391
+ {status.sources.map((source, idx) => (
392
+ <Box key={idx} sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
393
+ <Chip
394
+ size="small"
395
+ label={source.primary ? 'Primary' : 'Additional'}
396
+ sx={{
397
+ bgcolor: source.primary ? 'var(--theme-primary)20' : 'var(--theme-text-secondary)20',
398
+ color: source.primary ? 'var(--theme-primary)' : 'var(--theme-text-secondary)',
399
+ }}
400
+ />
401
+ <Text variant="body1" content={source.name} fontWeight="500" customColor="var(--theme-text-primary)" />
402
+ {source.description && (
403
+ <Text variant="body2" content={`- ${source.description}`} customColor="var(--theme-text-secondary)" />
404
+ )}
405
+ {source.readonly && (
406
+ <Chip
407
+ size="small"
408
+ icon={<LockIcon sx={{ fontSize: 14 }} />}
409
+ label="Read-only"
410
+ sx={{
411
+ bgcolor: 'var(--theme-warning)20',
412
+ color: 'var(--theme-warning)',
413
+ }}
414
+ />
415
+ )}
416
+ </Box>
417
+ ))}
418
+ </Box>
419
+
420
+ {status.cacheEnabled && (
421
+ <Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'var(--theme-border)' }}>
422
+ <Text variant="caption" content={`Caching: Enabled (TTL: ${status.cacheTtl}s)`} customColor="var(--theme-text-secondary)" />
423
+ </Box>
424
+ )}
425
+ </CardContent>
426
+ </Card>
427
+ )}
428
+
429
+ {/* Create Dialog */}
430
+ {!isReadonly && (
431
+ <Dialog
432
+ open={createDialogOpen}
433
+ onClose={() => setCreateDialogOpen(false)}
434
+ maxWidth="sm"
435
+ fullWidth
436
+ >
437
+ <DialogTitle>Add Entitlement</DialogTitle>
438
+ <DialogContent>
439
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
440
+ <TextField
441
+ label="Name"
442
+ fullWidth
443
+ value={newEntitlement.name}
444
+ onChange={(e) => setNewEntitlement({ ...newEntitlement, name: e.target.value })}
445
+ placeholder="e.g., premium, pro, feature:analytics"
446
+ required
447
+ />
448
+ <TextField
449
+ label="Category (Optional)"
450
+ fullWidth
451
+ value={newEntitlement.category}
452
+ onChange={(e) => setNewEntitlement({ ...newEntitlement, category: e.target.value })}
453
+ placeholder="e.g., subscription, feature, access"
454
+ />
455
+ <TextField
456
+ label="Description (Optional)"
457
+ fullWidth
458
+ multiline
459
+ rows={2}
460
+ value={newEntitlement.description}
461
+ onChange={(e) => setNewEntitlement({ ...newEntitlement, description: e.target.value })}
462
+ placeholder="Describe what this entitlement grants access to"
463
+ />
464
+ </Box>
465
+ </DialogContent>
466
+ <DialogActions>
467
+ <Button variant="text" label="Cancel" onClick={() => setCreateDialogOpen(false)} />
468
+ <Button
469
+ variant="primary"
470
+ label="Create"
471
+ onClick={handleCreate}
472
+ disabled={!newEntitlement.name.trim() || saving}
473
+ />
474
+ </DialogActions>
475
+ </Dialog>
476
+ )}
477
+
478
+ {/* Edit Dialog */}
479
+ {!isReadonly && selectedEntitlement && (
480
+ <Dialog
481
+ open={editDialogOpen}
482
+ onClose={() => setEditDialogOpen(false)}
483
+ maxWidth="sm"
484
+ fullWidth
485
+ >
486
+ <DialogTitle>Edit Entitlement</DialogTitle>
487
+ <DialogContent>
488
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
489
+ <TextField
490
+ label="Name"
491
+ fullWidth
492
+ value={selectedEntitlement.name}
493
+ disabled
494
+ helperText="Name cannot be changed"
495
+ />
496
+ <TextField
497
+ label="Category"
498
+ fullWidth
499
+ value={selectedEntitlement.category || ''}
500
+ onChange={(e) => setSelectedEntitlement({ ...selectedEntitlement, category: e.target.value })}
501
+ />
502
+ <TextField
503
+ label="Description"
504
+ fullWidth
505
+ multiline
506
+ rows={2}
507
+ value={selectedEntitlement.description || ''}
508
+ onChange={(e) => setSelectedEntitlement({ ...selectedEntitlement, description: e.target.value })}
509
+ />
510
+ </Box>
511
+ </DialogContent>
512
+ <DialogActions>
513
+ <Button variant="text" label="Cancel" onClick={() => setEditDialogOpen(false)} />
514
+ <Button
515
+ variant="primary"
516
+ label="Save"
517
+ onClick={handleEdit}
518
+ disabled={saving}
519
+ />
520
+ </DialogActions>
521
+ </Dialog>
522
+ )}
523
+
524
+ {/* Delete Confirmation Dialog */}
525
+ {!isReadonly && selectedEntitlement && (
526
+ <Dialog
527
+ open={deleteDialogOpen}
528
+ onClose={() => setDeleteDialogOpen(false)}
529
+ maxWidth="sm"
530
+ fullWidth
531
+ >
532
+ <DialogTitle>Delete Entitlement</DialogTitle>
533
+ <DialogContent>
534
+ <Text
535
+ variant="body1"
536
+ content={`Are you sure you want to delete the entitlement "${selectedEntitlement.name}"?`}
537
+ customColor="var(--theme-text-primary)"
538
+ />
539
+ <Alert severity="warning" sx={{ mt: 2 }}>
540
+ This will remove the entitlement from all users who currently have it.
541
+ </Alert>
542
+ </DialogContent>
543
+ <DialogActions>
544
+ <Button variant="text" label="Cancel" onClick={() => setDeleteDialogOpen(false)} />
545
+ <Button
546
+ variant="primary"
547
+ color="error"
548
+ label="Delete"
549
+ onClick={handleDelete}
550
+ disabled={saving}
551
+ />
552
+ </DialogActions>
553
+ </Dialog>
554
+ )}
555
+ </Box>
556
+ );
557
+ }