@qwickapps/server 1.1.9 → 1.3.0

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 (240) hide show
  1. package/README.md +318 -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 +99 -60
  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 +683 -315
  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 +271 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +326 -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 +9 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +9 -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/auth-plugin.d.ts +40 -0
  41. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  42. package/dist/plugins/auth/auth-plugin.js +255 -0
  43. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  45. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  47. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  48. package/dist/plugins/auth/index.d.ts +12 -0
  49. package/dist/plugins/auth/index.d.ts.map +1 -0
  50. package/dist/plugins/auth/index.js +13 -0
  51. package/dist/plugins/auth/index.js.map +1 -0
  52. package/dist/plugins/auth/types.d.ts +148 -0
  53. package/dist/plugins/auth/types.d.ts.map +1 -0
  54. package/dist/plugins/auth/types.js +14 -0
  55. package/dist/plugins/auth/types.js.map +1 -0
  56. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  57. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  58. package/dist/plugins/bans/bans-plugin.js +428 -0
  59. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  60. package/dist/plugins/bans/index.d.ts +9 -0
  61. package/dist/plugins/bans/index.d.ts.map +1 -0
  62. package/dist/plugins/bans/index.js +10 -0
  63. package/dist/plugins/bans/index.js.map +1 -0
  64. package/dist/plugins/bans/stores/index.d.ts +7 -0
  65. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  66. package/dist/plugins/bans/stores/index.js +7 -0
  67. package/dist/plugins/bans/stores/index.js.map +1 -0
  68. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  69. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  70. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  71. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  72. package/dist/plugins/bans/types.d.ts +128 -0
  73. package/dist/plugins/bans/types.d.ts.map +1 -0
  74. package/dist/plugins/bans/types.js +11 -0
  75. package/dist/plugins/bans/types.js.map +1 -0
  76. package/dist/plugins/cache-plugin.d.ts +14 -3
  77. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  78. package/dist/plugins/cache-plugin.js +27 -7
  79. package/dist/plugins/cache-plugin.js.map +1 -1
  80. package/dist/plugins/cache-plugin.test.js +96 -32
  81. package/dist/plugins/cache-plugin.test.js.map +1 -1
  82. package/dist/plugins/config-plugin.d.ts +3 -2
  83. package/dist/plugins/config-plugin.d.ts.map +1 -1
  84. package/dist/plugins/config-plugin.js +17 -10
  85. package/dist/plugins/config-plugin.js.map +1 -1
  86. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  87. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  88. package/dist/plugins/diagnostics-plugin.js +17 -10
  89. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  90. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  91. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  92. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  93. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  94. package/dist/plugins/entitlements/index.d.ts +12 -0
  95. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  96. package/dist/plugins/entitlements/index.js +16 -0
  97. package/dist/plugins/entitlements/index.js.map +1 -0
  98. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  99. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  100. package/dist/plugins/entitlements/sources/index.js +9 -0
  101. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  102. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  103. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  105. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  106. package/dist/plugins/entitlements/types.d.ts +232 -0
  107. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/types.js +11 -0
  109. package/dist/plugins/entitlements/types.js.map +1 -0
  110. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  111. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  112. package/dist/plugins/frontend-app-plugin.js +14 -9
  113. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  114. package/dist/plugins/health-plugin.d.ts +5 -2
  115. package/dist/plugins/health-plugin.d.ts.map +1 -1
  116. package/dist/plugins/health-plugin.js +20 -5
  117. package/dist/plugins/health-plugin.js.map +1 -1
  118. package/dist/plugins/index.d.ts +8 -2
  119. package/dist/plugins/index.d.ts.map +1 -1
  120. package/dist/plugins/index.js +8 -2
  121. package/dist/plugins/index.js.map +1 -1
  122. package/dist/plugins/logs-plugin.d.ts +3 -2
  123. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  124. package/dist/plugins/logs-plugin.js +21 -12
  125. package/dist/plugins/logs-plugin.js.map +1 -1
  126. package/dist/plugins/postgres-plugin.d.ts +3 -3
  127. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  128. package/dist/plugins/postgres-plugin.js +9 -7
  129. package/dist/plugins/postgres-plugin.js.map +1 -1
  130. package/dist/plugins/postgres-plugin.test.js +47 -29
  131. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  132. package/dist/plugins/users/index.d.ts +12 -0
  133. package/dist/plugins/users/index.d.ts.map +1 -0
  134. package/dist/plugins/users/index.js +13 -0
  135. package/dist/plugins/users/index.js.map +1 -0
  136. package/dist/plugins/users/stores/index.d.ts +7 -0
  137. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  138. package/dist/plugins/users/stores/index.js +7 -0
  139. package/dist/plugins/users/stores/index.js.map +1 -0
  140. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  141. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  142. package/dist/plugins/users/stores/postgres-store.js +157 -0
  143. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  144. package/dist/plugins/users/types.d.ts +189 -0
  145. package/dist/plugins/users/types.d.ts.map +1 -0
  146. package/dist/plugins/users/types.js +12 -0
  147. package/dist/plugins/users/types.js.map +1 -0
  148. package/dist/plugins/users/users-plugin.d.ts +39 -0
  149. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  150. package/dist/plugins/users/users-plugin.js +242 -0
  151. package/dist/plugins/users/users-plugin.js.map +1 -0
  152. package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
  153. package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
  154. package/dist-ui/index.html +1 -1
  155. package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
  156. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  157. package/dist-ui-lib/components/index.d.ts +18 -0
  158. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  159. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  160. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  161. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  162. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
  163. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
  164. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  165. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  166. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  167. package/dist-ui-lib/index.js +6441 -0
  168. package/dist-ui-lib/index.js.map +1 -0
  169. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  170. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  171. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  172. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  173. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  174. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  175. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  176. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  177. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  178. package/package.json +18 -6
  179. package/src/core/control-panel.ts +122 -68
  180. package/src/core/gateway.ts +870 -399
  181. package/src/core/index.ts +21 -2
  182. package/src/core/plugin-registry.ts +653 -0
  183. package/src/core/types.ts +31 -37
  184. package/src/index.ts +118 -19
  185. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  186. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  187. package/src/plugins/auth/adapters/index.ts +9 -0
  188. package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
  189. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  190. package/src/plugins/auth/auth-plugin.ts +303 -0
  191. package/src/plugins/auth/index.ts +33 -0
  192. package/src/plugins/auth/types.ts +165 -0
  193. package/src/plugins/bans/bans-plugin.ts +485 -0
  194. package/src/plugins/bans/index.ts +31 -0
  195. package/src/plugins/bans/stores/index.ts +7 -0
  196. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  197. package/src/plugins/bans/types.ts +141 -0
  198. package/src/plugins/cache-plugin.test.ts +105 -32
  199. package/src/plugins/cache-plugin.ts +40 -9
  200. package/src/plugins/config-plugin.ts +23 -12
  201. package/src/plugins/diagnostics-plugin.ts +22 -12
  202. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  203. package/src/plugins/entitlements/index.ts +51 -0
  204. package/src/plugins/entitlements/sources/index.ts +9 -0
  205. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  206. package/src/plugins/entitlements/types.ts +256 -0
  207. package/src/plugins/frontend-app-plugin.ts +24 -12
  208. package/src/plugins/health-plugin.ts +27 -7
  209. package/src/plugins/index.ts +106 -4
  210. package/src/plugins/logs-plugin.ts +28 -14
  211. package/src/plugins/postgres-plugin.test.ts +49 -29
  212. package/src/plugins/postgres-plugin.ts +11 -9
  213. package/src/plugins/users/index.ts +35 -0
  214. package/src/plugins/users/stores/index.ts +7 -0
  215. package/src/plugins/users/stores/postgres-store.ts +225 -0
  216. package/src/plugins/users/types.ts +209 -0
  217. package/src/plugins/users/users-plugin.ts +281 -0
  218. package/ui/src/App.tsx +185 -31
  219. package/ui/src/api/controlPanelApi.ts +354 -1
  220. package/ui/src/components/ControlPanelApp.tsx +209 -0
  221. package/ui/src/components/index.ts +62 -0
  222. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  223. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  224. package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
  225. package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
  226. package/ui/src/dashboard/builtInWidgets.tsx +29 -0
  227. package/ui/src/dashboard/index.ts +35 -0
  228. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  229. package/ui/src/dashboard/widgets/index.ts +7 -0
  230. package/ui/src/pages/DashboardPage.tsx +28 -149
  231. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  232. package/ui/src/pages/LogsPage.tsx +174 -8
  233. package/ui/src/pages/PluginPage.tsx +148 -0
  234. package/ui/src/pages/SystemPage.tsx +445 -0
  235. package/ui/src/pages/UsersPage.tsx +837 -0
  236. package/ui/tsconfig.lib.json +11 -0
  237. package/ui/vite.lib.config.ts +51 -0
  238. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  239. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  240. 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
+ }