@nsxbet/admin-sdk 0.8.0 → 0.9.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 (47) hide show
  1. package/README.md +16 -0
  2. package/dist/auth/client/bff.js +19 -14
  3. package/dist/auth/client/in-memory.js +11 -14
  4. package/dist/auth/client/interface.d.ts +5 -0
  5. package/dist/auth/client/permission-match.d.ts +1 -0
  6. package/dist/auth/client/permission-match.js +13 -0
  7. package/dist/auth/client/rbac-resolution.d.ts +29 -0
  8. package/dist/auth/client/rbac-resolution.js +22 -0
  9. package/dist/auth/client/resolved-role-cache.d.ts +11 -0
  10. package/dist/auth/client/resolved-role-cache.js +54 -0
  11. package/dist/components/AuthProvider.d.ts +3 -1
  12. package/dist/components/AuthProvider.js +95 -23
  13. package/dist/i18n/locales/en-US.json +66 -1
  14. package/dist/i18n/locales/es.json +66 -1
  15. package/dist/i18n/locales/pt-BR.json +66 -1
  16. package/dist/i18n/locales/ro.json +66 -1
  17. package/dist/registry/types/manifest.d.ts +3 -1
  18. package/dist/sdk-version.js +1 -1
  19. package/dist/shell/AdminShell.js +13 -8
  20. package/dist/shell/components/HomePage.js +1 -1
  21. package/dist/shell/components/LeftNav.js +46 -4
  22. package/dist/shell/components/MainContent.js +25 -0
  23. package/dist/shell/components/RegistryPage.js +3 -3
  24. package/dist/shell/components/access-control/AccessControlAuditPage.d.ts +1 -0
  25. package/dist/shell/components/access-control/AccessControlAuditPage.js +135 -0
  26. package/dist/shell/components/access-control/AccessControlGroupDetailPage.d.ts +1 -0
  27. package/dist/shell/components/access-control/AccessControlGroupDetailPage.js +224 -0
  28. package/dist/shell/components/access-control/AccessControlGroupsPage.d.ts +1 -0
  29. package/dist/shell/components/access-control/AccessControlGroupsPage.js +183 -0
  30. package/dist/shell/components/access-control/AccessControlLayout.d.ts +8 -0
  31. package/dist/shell/components/access-control/AccessControlLayout.js +23 -0
  32. package/dist/shell/components/access-control/AccessControlMemberPicker.d.ts +10 -0
  33. package/dist/shell/components/access-control/AccessControlMemberPicker.js +44 -0
  34. package/dist/shell/components/access-control/AccessControlPermissionPicker.d.ts +8 -0
  35. package/dist/shell/components/access-control/AccessControlPermissionPicker.js +38 -0
  36. package/dist/shell/components/access-control/AccessControlUserPage.d.ts +1 -0
  37. package/dist/shell/components/access-control/AccessControlUserPage.js +42 -0
  38. package/dist/shell/components/access-control/AccessControlUsersListPage.d.ts +1 -0
  39. package/dist/shell/components/access-control/AccessControlUsersListPage.js +111 -0
  40. package/dist/shell/components/access-control/api.d.ts +111 -0
  41. package/dist/shell/components/access-control/api.js +119 -0
  42. package/dist/shell/components/access-control/index.d.ts +8 -0
  43. package/dist/shell/components/access-control/index.js +8 -0
  44. package/dist/shell/components/index.d.ts +1 -0
  45. package/dist/shell/components/index.js +1 -0
  46. package/dist/vite/plugins.js +1 -1
  47. package/package.json +1 -1
@@ -10,6 +10,7 @@
10
10
  "edit": "Editar",
11
11
  "create": "Crear",
12
12
  "search": "Buscar",
13
+ "retry": "Reintentar",
13
14
  "noResults": "No se encontraron resultados",
14
15
  "close": "Cerrar",
15
16
  "back": "Atrás",
@@ -42,6 +43,7 @@
42
43
  "settings": "Configuración",
43
44
  "profile": "Perfil",
44
45
  "registry": "Registro",
46
+ "accessControl": "Control de Acceso",
45
47
  "logout": "Cerrar Sesión",
46
48
  "pinned": "Fijados",
47
49
  "platform": "Plataforma",
@@ -219,10 +221,73 @@
219
221
  "loginButton": "Iniciar sesión con SSO Corporativo",
220
222
  "secureAuth": "Autenticación Segura"
221
223
  },
224
+ "accessControlPage": {
225
+ "title": "Control de Acceso",
226
+ "description": "Gestiona autorización local, inspección y auditoría.",
227
+ "groups": "Grupos",
228
+ "users": "Usuarios",
229
+ "audit": "Auditoría",
230
+ "statusAll": "Todos",
231
+ "statusActive": "Activos",
232
+ "statusInactive": "Inactivos",
233
+ "groupsTitle": "Grupos",
234
+ "groupsDescription": "Explora y gestiona grupos RBAC.",
235
+ "noGroupsTitle": "No se encontraron grupos",
236
+ "noGroupsDescription": "Ningún grupo coincide con el filtro actual.",
237
+ "protectedGroup": "Protegido",
238
+ "viewGroup": "Ver Grupo",
239
+ "deactivate": "Desactivar",
240
+ "reactivate": "Reactivar",
241
+ "confirmDeactivate": "¿Estás seguro de que quieres desactivar \"{{group}}\"? Los miembros de este grupo perderán todos los permisos otorgados a través de él.",
242
+ "confirmReactivate": "¿Estás seguro de que quieres reactivar \"{{group}}\"? Los miembros recuperarán todos los permisos asignados a este grupo.",
243
+ "promptCreateGroup": "Ingresa un nombre visible para el nuevo grupo",
244
+ "promptEditGroup": "Actualiza el nombre visible del grupo",
245
+ "groupDetailTitle": "Detalle del Grupo",
246
+ "groupDetailDescription": "Revisa miembros y permisos del grupo {{groupId}}.",
247
+ "membersTab": "Miembros",
248
+ "permissionsTab": "Permisos",
249
+ "noMembers": "Este grupo todavía no tiene miembros.",
250
+ "noPermissions": "Este grupo todavía no tiene permisos asignados.",
251
+ "selfRemoveTitle": "¿Eliminarte de este grupo?",
252
+ "selfRemoveDescription": "Estás a punto de eliminarte del grupo \"{{group}}\". Esto puede revocar tu acceso para gestionar este grupo y otras áreas de la plataforma.",
253
+ "selfRemovePrompt": "Escribe CONFIRM para continuar.",
254
+ "selfRemoveConfirmButton": "Eliminarme",
255
+ "userTitle": "Acceso del Usuario",
256
+ "userDescription": "Inspecciona el acceso resuelto del usuario {{userId}}.",
257
+ "userRolesTitle": "Roles Efectivos",
258
+ "userGroupsTitle": "Grupos Contribuyentes",
259
+ "noRoles": "Este usuario no tiene roles efectivos.",
260
+ "auditTitle": "Historial de Auditoría",
261
+ "auditDescription": "Explora los eventos de auditoría de Control de Acceso en modo solo lectura.",
262
+ "filterEventType": "Tipo de evento",
263
+ "filterActor": "Id del actor",
264
+ "filterTargetType": "Tipo de objetivo",
265
+ "filterTargetId": "Id del objetivo",
266
+ "noAuditTitle": "No se encontraron eventos de auditoría",
267
+ "noAuditDescription": "Ningún evento de auditoría coincide con los filtros actuales.",
268
+ "noAccess": "No tienes permiso para acceder al área de Control de Acceso.",
269
+ "columnName": "Nombre",
270
+ "columnProtected": "Protegido",
271
+ "columnEmail": "Email",
272
+ "columnSubject": "Sujeto",
273
+ "columnTimestamp": "Fecha/Hora",
274
+ "columnEvent": "Evento",
275
+ "columnActor": "Actor",
276
+ "columnTarget": "Objetivo",
277
+ "columnSummary": "Resumen",
278
+ "inspectUser": "Inspeccionar",
279
+ "usersTitle": "Usuarios",
280
+ "noUsersTitle": "No se encontraron usuarios",
281
+ "noUsersDescription": "Ningún usuario local coincide con la búsqueda."
282
+ },
222
283
  "breadcrumbs": {
223
284
  "home": "Inicio",
224
285
  "registry": "Registro",
225
286
  "settings": "Configuración",
226
- "profile": "Perfil"
287
+ "profile": "Perfil",
288
+ "accessControl": "Control de Acceso",
289
+ "accessControlGroups": "Grupos",
290
+ "accessControlUsers": "Usuarios",
291
+ "accessControlAudit": "Auditoría"
227
292
  }
228
293
  }
@@ -10,6 +10,7 @@
10
10
  "edit": "Editar",
11
11
  "create": "Criar",
12
12
  "search": "Buscar",
13
+ "retry": "Tentar novamente",
13
14
  "noResults": "Nenhum resultado encontrado",
14
15
  "close": "Fechar",
15
16
  "back": "Voltar",
@@ -42,6 +43,7 @@
42
43
  "settings": "Configurações",
43
44
  "profile": "Perfil",
44
45
  "registry": "Registro",
46
+ "accessControl": "Controle de Acesso",
45
47
  "logout": "Sair",
46
48
  "pinned": "Fixados",
47
49
  "platform": "Plataforma",
@@ -219,10 +221,73 @@
219
221
  "loginButton": "Entrar com SSO Corporativo",
220
222
  "secureAuth": "Autenticação Segura"
221
223
  },
224
+ "accessControlPage": {
225
+ "title": "Controle de Acesso",
226
+ "description": "Gerencie autorização local, inspeção e auditoria.",
227
+ "groups": "Grupos",
228
+ "users": "Usuários",
229
+ "audit": "Auditoria",
230
+ "statusAll": "Todos",
231
+ "statusActive": "Ativos",
232
+ "statusInactive": "Inativos",
233
+ "groupsTitle": "Grupos",
234
+ "groupsDescription": "Navegue e gerencie grupos de RBAC.",
235
+ "noGroupsTitle": "Nenhum grupo encontrado",
236
+ "noGroupsDescription": "Nenhum grupo corresponde ao filtro atual.",
237
+ "protectedGroup": "Protegido",
238
+ "viewGroup": "Ver Grupo",
239
+ "deactivate": "Desativar",
240
+ "reactivate": "Reativar",
241
+ "confirmDeactivate": "Tem certeza que deseja desativar \"{{group}}\"? Os membros deste grupo perderão todas as permissões concedidas por ele.",
242
+ "confirmReactivate": "Tem certeza que deseja reativar \"{{group}}\"? Os membros recuperarão todas as permissões atribuídas a este grupo.",
243
+ "promptCreateGroup": "Digite um nome de exibição para o novo grupo",
244
+ "promptEditGroup": "Atualize o nome de exibição do grupo",
245
+ "groupDetailTitle": "Detalhes do Grupo",
246
+ "groupDetailDescription": "Revise membros e permissões do grupo {{groupId}}.",
247
+ "membersTab": "Membros",
248
+ "permissionsTab": "Permissões",
249
+ "noMembers": "Este grupo ainda não tem membros.",
250
+ "noPermissions": "Este grupo ainda não tem permissões atribuídas.",
251
+ "selfRemoveTitle": "Remover você mesmo deste grupo?",
252
+ "selfRemoveDescription": "Você está prestes a se remover do grupo \"{{group}}\". Isso pode revogar seu acesso para gerenciar este grupo e outras áreas da plataforma.",
253
+ "selfRemovePrompt": "Digite CONFIRM para prosseguir.",
254
+ "selfRemoveConfirmButton": "Remover a mim mesmo",
255
+ "userTitle": "Acesso do Usuário",
256
+ "userDescription": "Inspecione o acesso resolvido do usuário {{userId}}.",
257
+ "userRolesTitle": "Papéis Efetivos",
258
+ "userGroupsTitle": "Grupos Contribuintes",
259
+ "noRoles": "Este usuário não possui papéis efetivos.",
260
+ "auditTitle": "Histórico de Auditoria",
261
+ "auditDescription": "Navegue pelos eventos de auditoria do Controle de Acesso em modo somente leitura.",
262
+ "filterEventType": "Tipo de evento",
263
+ "filterActor": "Id do ator",
264
+ "filterTargetType": "Tipo de alvo",
265
+ "filterTargetId": "Id do alvo",
266
+ "noAuditTitle": "Nenhum evento de auditoria encontrado",
267
+ "noAuditDescription": "Nenhum evento de auditoria corresponde aos filtros atuais.",
268
+ "noAccess": "Você não tem permissão para acessar a área de Controle de Acesso.",
269
+ "columnName": "Nome",
270
+ "columnProtected": "Protegido",
271
+ "columnEmail": "Email",
272
+ "columnSubject": "Sujeito",
273
+ "columnTimestamp": "Data/Hora",
274
+ "columnEvent": "Evento",
275
+ "columnActor": "Ator",
276
+ "columnTarget": "Alvo",
277
+ "columnSummary": "Resumo",
278
+ "inspectUser": "Inspecionar",
279
+ "usersTitle": "Usuários",
280
+ "noUsersTitle": "Nenhum usuário encontrado",
281
+ "noUsersDescription": "Nenhum usuário local corresponde à pesquisa."
282
+ },
222
283
  "breadcrumbs": {
223
284
  "home": "Início",
224
285
  "registry": "Registro",
225
286
  "settings": "Configurações",
226
- "profile": "Perfil"
287
+ "profile": "Perfil",
288
+ "accessControl": "Controle de Acesso",
289
+ "accessControlGroups": "Grupos",
290
+ "accessControlUsers": "Usuários",
291
+ "accessControlAudit": "Auditoria"
227
292
  }
228
293
  }
@@ -10,6 +10,7 @@
10
10
  "edit": "Editează",
11
11
  "create": "Creează",
12
12
  "search": "Caută",
13
+ "retry": "Reîncearcă",
13
14
  "noResults": "Nu s-au găsit rezultate",
14
15
  "close": "Închide",
15
16
  "back": "Înapoi",
@@ -42,6 +43,7 @@
42
43
  "settings": "Setări",
43
44
  "profile": "Profil",
44
45
  "registry": "Registru",
46
+ "accessControl": "Control Acces",
45
47
  "logout": "Deconectare",
46
48
  "pinned": "Fixate",
47
49
  "platform": "Platformă",
@@ -219,10 +221,73 @@
219
221
  "loginButton": "Autentificare cu SSO Corporativ",
220
222
  "secureAuth": "Autentificare Securizată"
221
223
  },
224
+ "accessControlPage": {
225
+ "title": "Control Acces",
226
+ "description": "Gestionează autorizarea locală, inspecția și auditul.",
227
+ "groups": "Grupuri",
228
+ "users": "Utilizatori",
229
+ "audit": "Audit",
230
+ "statusAll": "Toate",
231
+ "statusActive": "Active",
232
+ "statusInactive": "Inactive",
233
+ "groupsTitle": "Grupuri",
234
+ "groupsDescription": "Răsfoiește și administrează grupurile RBAC.",
235
+ "noGroupsTitle": "Nu au fost găsite grupuri",
236
+ "noGroupsDescription": "Niciun grup nu corespunde filtrului curent.",
237
+ "protectedGroup": "Protejat",
238
+ "viewGroup": "Vezi Grupul",
239
+ "deactivate": "Dezactivează",
240
+ "reactivate": "Reactivează",
241
+ "confirmDeactivate": "Ești sigur că vrei să dezactivezi \"{{group}}\"? Membrii acestui grup vor pierde toate permisiunile acordate prin acesta.",
242
+ "confirmReactivate": "Ești sigur că vrei să reactivezi \"{{group}}\"? Membrii vor recăpăta toate permisiunile atribuite acestui grup.",
243
+ "promptCreateGroup": "Introdu un nume afișat pentru noul grup",
244
+ "promptEditGroup": "Actualizează numele afișat al grupului",
245
+ "groupDetailTitle": "Detalii Grup",
246
+ "groupDetailDescription": "Revizuiește membrii și permisiunile pentru grupul {{groupId}}.",
247
+ "membersTab": "Membri",
248
+ "permissionsTab": "Permisiuni",
249
+ "noMembers": "Acest grup nu are încă membri.",
250
+ "noPermissions": "Acest grup nu are încă permisiuni atribuite.",
251
+ "selfRemoveTitle": "Te elimini din acest grup?",
252
+ "selfRemoveDescription": "Ești pe cale să te elimini din grupul \"{{group}}\". Acest lucru îți poate revoca accesul pentru gestionarea acestui grup și a altor zone ale platformei.",
253
+ "selfRemovePrompt": "Tastează CONFIRM pentru a continua.",
254
+ "selfRemoveConfirmButton": "Elimină-mă",
255
+ "userTitle": "Acces Utilizator",
256
+ "userDescription": "Inspectează accesul rezolvat pentru utilizatorul {{userId}}.",
257
+ "userRolesTitle": "Roluri Efective",
258
+ "userGroupsTitle": "Grupuri Contribuitoare",
259
+ "noRoles": "Acest utilizator nu are roluri efective.",
260
+ "auditTitle": "Istoric Audit",
261
+ "auditDescription": "Răsfoiește evenimentele de audit Control Acces în mod doar citire.",
262
+ "filterEventType": "Tip eveniment",
263
+ "filterActor": "Id actor",
264
+ "filterTargetType": "Tip țintă",
265
+ "filterTargetId": "Id țintă",
266
+ "noAuditTitle": "Nu au fost găsite evenimente de audit",
267
+ "noAuditDescription": "Niciun eveniment de audit nu corespunde filtrelor curente.",
268
+ "noAccess": "Nu ai permisiunea să accesezi zona Control Acces.",
269
+ "columnName": "Nume",
270
+ "columnProtected": "Protejat",
271
+ "columnEmail": "Email",
272
+ "columnSubject": "Subiect",
273
+ "columnTimestamp": "Data/Ora",
274
+ "columnEvent": "Eveniment",
275
+ "columnActor": "Actor",
276
+ "columnTarget": "Țintă",
277
+ "columnSummary": "Rezumat",
278
+ "inspectUser": "Inspectează",
279
+ "usersTitle": "Utilizatori",
280
+ "noUsersTitle": "Nu au fost găsiți utilizatori",
281
+ "noUsersDescription": "Niciun utilizator local nu corespunde căutării."
282
+ },
222
283
  "breadcrumbs": {
223
284
  "home": "Acasă",
224
285
  "registry": "Registru",
225
286
  "settings": "Setări",
226
- "profile": "Profil"
287
+ "profile": "Profil",
288
+ "accessControl": "Control Acces",
289
+ "accessControlGroups": "Grupuri",
290
+ "accessControlUsers": "Utilizatori",
291
+ "accessControlAudit": "Audit"
227
292
  }
228
293
  }
@@ -84,7 +84,9 @@ export interface AdminModuleManifest {
84
84
  owners?: ModuleOwners;
85
85
  /**
86
86
  * Semver of @nsxbet/admin-sdk the module was built against.
87
- * Injected at build time by `defineModuleConfig` — omit in source `admin.module.json`.
87
+ * Injected at build time by `defineModuleConfig`.
88
+ * Source `admin.module.json` values are overwritten in `module.manifest.json`,
89
+ * and the registry refreshes/restores this value on republish and rollback.
88
90
  */
89
91
  sdkVersion?: string;
90
92
  }
@@ -2,4 +2,4 @@
2
2
  * Semver of @nsxbet/admin-sdk (synced from package.json by scripts/write-sdk-version.mjs).
3
3
  * Do not edit manually — run `node scripts/write-sdk-version.mjs` after version bumps.
4
4
  */
5
- export const SDK_PACKAGE_VERSION = "0.8.0";
5
+ export const SDK_PACKAGE_VERSION = "0.9.0";
@@ -13,6 +13,7 @@ import { ProfilePage } from "./components/ProfilePage";
13
13
  import { SettingsPage } from "./components/SettingsPage";
14
14
  import { RegistryPage } from "./components/RegistryPage";
15
15
  import { HomePage } from "./components/HomePage";
16
+ import { AccessControlLayout, AccessControlGroupsPage, AccessControlGroupDetailPage, AccessControlUsersListPage, AccessControlUserPage, AccessControlAuditPage, } from "./components/access-control";
16
17
  import { AuthProvider, useAuthContext } from "../components/AuthProvider";
17
18
  import { createInMemoryAuthClient, createMockUsersFromRoles, } from "../auth/client/in-memory";
18
19
  import { createBffAuthClient } from "../auth/client/bff";
@@ -216,7 +217,7 @@ function ShellContent({ modules, children, environment, locale, onLocaleChange,
216
217
  // Ignore
217
218
  }
218
219
  };
219
- return (_jsxs(_Fragment, { children: [hasUpdates && (_jsx(UpdateBanner, { onReload: () => window.location.reload(), onDismiss: onDismissUpdate })), _jsxs(SidebarProvider, { open: sidebarOpen, onOpenChange: handleSidebarChange, children: [_jsx(LeftNav, { modules: modules }), _jsxs(SidebarInset, { className: "flex flex-col", children: [_jsx(TopBar, { onSearchClick: onSearchClick, environment: environment, locale: locale, onLocaleChange: onLocaleChange, timezoneMode: timezoneMode, onTimezoneToggle: onTimezoneToggle }), cacheStatus.state !== "fresh" && cacheStatus.state !== "unavailable" && (_jsx("div", { className: "px-4 pt-2", children: _jsx(RegistryStatusBanner, { status: cacheStatus, onRetry: onRetry }) })), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(HomePage, {}) }) }), _jsx(Route, { path: "/_profile", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(ProfilePage, {}) }) }), _jsx(Route, { path: "/_settings", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(SettingsPage, {}) }) }), _jsx(Route, { path: "/_registry", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(RegistryPage, { apiUrl: apiUrl, registryClient: registryClient }) }) }), _jsx(Route, { path: "/_modules/*", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(ModuleOverview, { modules: modules }) }) }), isStandaloneMode
220
+ return (_jsxs(_Fragment, { children: [hasUpdates && (_jsx(UpdateBanner, { onReload: () => window.location.reload(), onDismiss: onDismissUpdate })), _jsxs(SidebarProvider, { open: sidebarOpen, onOpenChange: handleSidebarChange, children: [_jsx(LeftNav, { modules: modules }), _jsxs(SidebarInset, { className: "flex flex-col", children: [_jsx(TopBar, { onSearchClick: onSearchClick, environment: environment, locale: locale, onLocaleChange: onLocaleChange, timezoneMode: timezoneMode, onTimezoneToggle: onTimezoneToggle }), cacheStatus.state !== "fresh" && cacheStatus.state !== "unavailable" && (_jsx("div", { className: "px-4 pt-2", children: _jsx(RegistryStatusBanner, { status: cacheStatus, onRetry: onRetry }) })), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(HomePage, {}) }) }), _jsx(Route, { path: "/_profile", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(ProfilePage, {}) }) }), _jsx(Route, { path: "/_settings", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(SettingsPage, {}) }) }), _jsx(Route, { path: "/_registry", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(RegistryPage, { apiUrl: apiUrl, registryClient: registryClient }) }) }), _jsxs(Route, { path: "/_access-control", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(AccessControlLayout, { apiUrl: apiUrl }) }), children: [_jsx(Route, { path: "groups", element: _jsx(AccessControlGroupsPage, {}) }), _jsx(Route, { path: "groups/:groupId", element: _jsx(AccessControlGroupDetailPage, {}) }), _jsx(Route, { path: "users", element: _jsx(AccessControlUsersListPage, {}) }), _jsx(Route, { path: "users/:userId", element: _jsx(AccessControlUserPage, {}) }), _jsx(Route, { path: "audit", element: _jsx(AccessControlAuditPage, {}) })] }), _jsx(Route, { path: "/_modules/*", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: _jsx(ModuleOverview, { modules: modules }) }) }), isStandaloneMode
220
221
  ? // Standalone mode: render children (module is imported directly)
221
222
  modules.map((module) => (_jsx(Route, { path: `${module.routeBase}/*`, element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: children }) }, module.id)))
222
223
  : // Shell mode: load modules dynamically via React.lazy
@@ -296,19 +297,23 @@ export function AdminShell({ modules: manifests = [], children, bff, authClient:
296
297
  'admin.users.view',
297
298
  'admin.users.edit',
298
299
  'admin.users.delete',
299
- 'admin.platform.view',
300
- 'admin.platform.edit',
301
- 'admin.platform.delete',
300
+ 'admin.platform.registry.view',
301
+ 'admin.platform.registry.edit',
302
+ 'admin.platform.registry.delete',
303
+ 'admin.platform.access-control.groups.view',
304
+ 'admin.platform.access-control.groups.manage',
305
+ 'admin.platform.access-control.users.view',
306
+ 'admin.platform.access-control.audit.view',
302
307
  ],
303
308
  editor: [
304
309
  'admin.tasks.view',
305
310
  'admin.tasks.edit',
306
311
  'admin.users.view',
307
312
  'admin.users.edit',
308
- 'admin.platform.view',
309
- 'admin.platform.edit',
313
+ 'admin.platform.registry.view',
314
+ 'admin.platform.registry.edit',
310
315
  ],
311
- viewer: ['admin.tasks.view', 'admin.users.view', 'admin.platform.view'],
316
+ viewer: ['admin.tasks.view', 'admin.users.view', 'admin.platform.registry.view'],
312
317
  noAccess: [],
313
318
  });
314
319
  return createInMemoryAuthClient({ users: defaultMockUsers });
@@ -422,5 +427,5 @@ export function AdminShell({ modules: manifests = [], children, bff, authClient:
422
427
  else {
423
428
  shellContent = (_jsx(ShellContent, { modules: modules, environment: environment, locale: locale, onLocaleChange: handleLocaleChange, onSearchClick: handleSearchClick, catalog: catalog, commandPaletteOpen: commandPaletteOpen, onCommandPaletteChange: setCommandPaletteOpen, apiUrl: apiUrl, registryClient: registryClient, isStandaloneMode: isStandaloneMode, cacheStatus: cacheStatus, onRetry: handleRetry, hasUpdates: hasUpdates, onDismissUpdate: dismissUpdate, localeCallbacksRef: localeCallbacksRef, timezoneMode: timezoneMode, onTimezoneToggle: handleTimezoneToggle, timezoneCallbacksRef: timezoneCallbacksRef, children: children }));
424
429
  }
425
- return (_jsx(BrowserRouter, { children: _jsx(I18nextProvider, { i18n: i18n, children: _jsx(ThemeProvider, { children: _jsx(AuthProvider, { authClient: authClient, children: shellContent }) }) }) }));
430
+ return (_jsx(BrowserRouter, { children: _jsx(I18nextProvider, { i18n: i18n, children: _jsx(ThemeProvider, { children: _jsx(AuthProvider, { authClient: authClient, apiUrl: apiUrl, children: shellContent }) }) }) }));
426
431
  }
@@ -25,7 +25,7 @@ const quickActions = [
25
25
  descriptionKey: "registryPage.description",
26
26
  route: "/_registry",
27
27
  color: "text-purple-500",
28
- permission: "admin.platform.view",
28
+ permission: "admin.platform.registry.view",
29
29
  },
30
30
  ];
31
31
  export function HomePage() {
@@ -182,16 +182,57 @@ export function LeftNav({ modules }) {
182
182
  newValue: JSON.stringify(updated),
183
183
  }));
184
184
  }, [pinnedCommands]);
185
+ const hasAnyAccessControlPermission = useMemo(() => {
186
+ const acPermissions = [
187
+ "admin.platform.access-control.groups.view",
188
+ "admin.platform.access-control.groups.manage",
189
+ "admin.platform.access-control.users.view",
190
+ "admin.platform.access-control.audit.view",
191
+ ];
192
+ return acPermissions.some((p) => auth.hasPermission(p));
193
+ }, [auth]);
194
+ const accessControlVirtualModule = useMemo(() => {
195
+ if (!hasAnyAccessControlPermission)
196
+ return null;
197
+ return {
198
+ id: "@platform/access-control",
199
+ title: { "en-US": "Access Control", "pt-BR": "Controle de Acesso", es: "Control de Acceso", ro: "Control Acces" },
200
+ description: { "en-US": "Manage authorization", "pt-BR": "Gerencie autorização", es: "Gestiona autorización", ro: "Gestionează autorizarea" },
201
+ category: "Platform",
202
+ routeBase: "/_access-control",
203
+ keywords: [],
204
+ permissions: { view: [] },
205
+ owners: { team: "Platform", supportChannel: "#platform" },
206
+ status: "active",
207
+ sdkVersion: "",
208
+ icon: "shield",
209
+ navigation: { style: "stacked" },
210
+ commands: [
211
+ ...(auth.hasPermission("admin.platform.access-control.groups.view") || auth.hasPermission("admin.platform.access-control.groups.manage")
212
+ ? [{ id: "groups", title: { "en-US": "Groups", "pt-BR": "Grupos", es: "Grupos", ro: "Grupuri" }, route: "/_access-control/groups", icon: "users" }]
213
+ : []),
214
+ ...(auth.hasPermission("admin.platform.access-control.users.view")
215
+ ? [{ id: "users", title: { "en-US": "Users", "pt-BR": "Usuários", es: "Usuarios", ro: "Utilizatori" }, route: "/_access-control/users", icon: "user-search" }]
216
+ : []),
217
+ ...(auth.hasPermission("admin.platform.access-control.audit.view")
218
+ ? [{ id: "audit", title: { "en-US": "Audit", "pt-BR": "Auditoria", es: "Auditoría", ro: "Audit" }, route: "/_access-control/audit", icon: "file-clock" }]
219
+ : []),
220
+ ],
221
+ };
222
+ }, [auth, hasAnyAccessControlPermission]);
223
+ const allModulesWithVirtual = useMemo(() => {
224
+ return accessControlVirtualModule ? [...modules, accessControlVirtualModule] : modules;
225
+ }, [modules, accessControlVirtualModule]);
185
226
  // URL-driven stacked panel detection
186
227
  const activeStackedModule = useMemo(() => {
187
- return modules.find((m) => m.navigation?.style === "stacked" &&
228
+ return allModulesWithVirtual.find((m) => m.navigation?.style === "stacked" &&
188
229
  m.status === "active" &&
189
230
  (location.pathname === m.routeBase ||
190
231
  location.pathname.startsWith(m.routeBase + "/"))) ?? null;
191
- }, [modules, location.pathname]);
232
+ }, [allModulesWithVirtual, location.pathname]);
192
233
  const isStacked = activeStackedModule !== null;
193
234
  const isStackedModule = (module) => module.navigation?.style === "stacked";
194
- const userFooter = (_jsx(SidebarFooter, { className: `border-t border-sidebar-border ${isCollapsed ? "items-center" : ""}`, children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", tooltip: user?.displayName || "User", "data-testid": "user-menu-trigger", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.displayName || "User" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email || "" })] }), _jsx(ChevronUp, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { side: isCollapsed ? "right" : "top", align: isCollapsed ? "end" : "start", className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", children: [_jsxs(DropdownMenuItem, { onClick: () => navigate("/_profile"), className: isRouteActive("/_profile") ? "bg-accent" : "", "data-testid": "nav-profile", children: [_jsx(Icon, { name: "user", className: "h-4 w-4 mr-2" }), t('navigation.profile')] }), _jsxs(DropdownMenuItem, { onClick: () => navigate("/_settings"), className: isRouteActive("/_settings") ? "bg-accent" : "", "data-testid": "nav-settings", children: [_jsx(Icon, { name: "settings", className: "h-4 w-4 mr-2" }), t('navigation.settings')] }), auth.hasPermission('admin.platform.view') && (_jsxs(DropdownMenuItem, { onClick: () => navigate("/_registry"), className: isRouteActive("/_registry") ? "bg-accent" : "", "data-testid": "nav-registry", children: [_jsx(Icon, { name: "package", className: "h-4 w-4 mr-2" }), t('navigation.registry')] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => logout(), "data-testid": "nav-logout", children: [_jsx(Icon, { name: "log-out", className: "h-4 w-4 mr-2" }), t('navigation.logout')] })] })] }) }) }) }));
235
+ const userFooter = (_jsx(SidebarFooter, { className: `border-t border-sidebar-border ${isCollapsed ? "items-center" : ""}`, children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", tooltip: user?.displayName || "User", "data-testid": "user-menu-trigger", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.displayName || "User" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email || "" })] }), _jsx(ChevronUp, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { side: isCollapsed ? "right" : "top", align: isCollapsed ? "end" : "start", className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", children: [_jsxs(DropdownMenuItem, { onClick: () => navigate("/_profile"), className: isRouteActive("/_profile") ? "bg-accent" : "", "data-testid": "nav-profile", children: [_jsx(Icon, { name: "user", className: "h-4 w-4 mr-2" }), t('navigation.profile')] }), _jsxs(DropdownMenuItem, { onClick: () => navigate("/_settings"), className: isRouteActive("/_settings") ? "bg-accent" : "", "data-testid": "nav-settings", children: [_jsx(Icon, { name: "settings", className: "h-4 w-4 mr-2" }), t('navigation.settings')] }), auth.hasPermission('admin.platform.registry.view') && (_jsxs(DropdownMenuItem, { onClick: () => navigate("/_registry"), className: isRouteActive("/_registry") ? "bg-accent" : "", "data-testid": "nav-registry", children: [_jsx(Icon, { name: "package", className: "h-4 w-4 mr-2" }), t('navigation.registry')] })), accessControlVirtualModule && (_jsxs(DropdownMenuItem, { onClick: () => navigate("/_access-control/groups"), className: isRouteActive("/_access-control") ? "bg-accent" : "", "data-testid": "nav-access-control", children: [_jsx(Icon, { name: "shield", className: "h-4 w-4 mr-2" }), t('navigation.accessControl')] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => logout(), "data-testid": "nav-logout", children: [_jsx(Icon, { name: "log-out", className: "h-4 w-4 mr-2" }), t('navigation.logout')] })] })] }) }) }) }));
195
236
  return (_jsx(Sidebar, { collapsible: "icon", "data-testid": "left-nav", children: _jsxs("div", { className: "flex h-full flex-col overflow-hidden", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsx(SidebarTrigger, { "data-testid": "sidebar-toggle" }) }) }) }), _jsxs("div", { className: "flex flex-1 min-h-0 transition-transform duration-200 ease-in-out motion-reduce:transition-none", style: {
196
237
  width: "200%",
197
238
  transform: isStacked && !isCollapsed ? "translateX(-50%)" : "translateX(0)",
@@ -241,5 +282,6 @@ export function LeftNav({ modules }) {
241
282
  ? "text-primary hover:text-destructive hover:bg-sidebar-accent opacity-100"
242
283
  : "text-muted-foreground hover:text-primary hover:bg-sidebar-accent opacity-0 group-hover/command:opacity-100"}`, title: isPinned ? "Unpin" : "Pin", "data-testid": `pin-toggle-${command.id}`, children: _jsx(Pin, { className: `h-3.5 w-3.5 ${isPinned ? "fill-current" : ""}` }) }))] }) }, command.id));
243
284
  })] }) })] }) }, module.id));
244
- }) }) })] }, category))), auth.hasPermission('admin.platform.view') && (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: t('navigation.platform') }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => navigate("/_registry"), isActive: isRouteActive("/_registry"), tooltip: t('navigation.registry'), "data-testid": "nav-registry-sidebar", children: [_jsx(Icon, { name: "package", className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: t('navigation.registry') })] }) }) }) })] }))] }) }), _jsx("div", { className: "w-1/2 flex flex-col min-h-0", children: activeStackedModule ? (_jsx(StackedPanel, { module: activeStackedModule, showPinned: showPinned, isCommandPinned: isCommandPinned, togglePin: togglePin })) : null })] }), userFooter] }) }));
285
+ }) }) })] }, category))), (auth.hasPermission('admin.platform.registry.view') ||
286
+ accessControlVirtualModule) && (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: t('navigation.platform') }), _jsx(SidebarGroupContent, { children: _jsxs(SidebarMenu, { children: [auth.hasPermission('admin.platform.registry.view') && (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => navigate("/_registry"), isActive: isRouteActive("/_registry"), tooltip: t('navigation.registry'), "data-testid": "nav-registry-sidebar", children: [_jsx(Icon, { name: "package", className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: t('navigation.registry') })] }) })), accessControlVirtualModule && (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => navigate("/_access-control/groups"), isActive: isRouteActive("/_access-control"), tooltip: t('navigation.accessControl'), "data-testid": "nav-access-control-sidebar", children: [_jsx(Icon, { name: "shield", className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: t('navigation.accessControl') }), _jsx(ChevronRight, { className: "ml-auto h-4 w-4" })] }) }))] }) })] }))] }) }), _jsx("div", { className: "w-1/2 flex flex-col min-h-0", children: activeStackedModule ? (_jsx(StackedPanel, { module: activeStackedModule, showPinned: showPinned, isCommandPinned: isCommandPinned, togglePin: togglePin })) : null })] }), userFooter] }) }));
245
287
  }
@@ -26,6 +26,31 @@ export function MainContent({ modules, children, moduleBreadcrumbs }) {
26
26
  setBreadcrumbs([{ label: t('breadcrumbs.home'), href: "/" }, { label: t('breadcrumbs.registry') }]);
27
27
  return;
28
28
  }
29
+ if (currentPath.startsWith("/_access-control")) {
30
+ const crumbs = [
31
+ { label: t('breadcrumbs.home'), href: "/" },
32
+ { label: t('breadcrumbs.accessControl'), href: "/_access-control" },
33
+ ];
34
+ if (currentPath === "/_access-control/groups") {
35
+ crumbs.push({ label: t('breadcrumbs.accessControlGroups') });
36
+ }
37
+ else if (currentPath.startsWith("/_access-control/groups/")) {
38
+ crumbs.push({ label: t('breadcrumbs.accessControlGroups'), href: "/_access-control/groups" });
39
+ crumbs.push({ label: t('accessControlPage.groupDetailTitle') });
40
+ }
41
+ else if (currentPath === "/_access-control/users") {
42
+ crumbs.push({ label: t('breadcrumbs.accessControlUsers') });
43
+ }
44
+ else if (currentPath.startsWith("/_access-control/users/")) {
45
+ crumbs.push({ label: t('breadcrumbs.accessControlUsers'), href: "/_access-control/users" });
46
+ crumbs.push({ label: t('accessControlPage.userTitle') });
47
+ }
48
+ else if (currentPath === "/_access-control/audit") {
49
+ crumbs.push({ label: t('breadcrumbs.accessControlAudit') });
50
+ }
51
+ setBreadcrumbs(crumbs);
52
+ return;
53
+ }
29
54
  // Find module that matches current path (for module routes)
30
55
  const module = modules.find((m) => m.status === "active" &&
31
56
  (currentPath === m.routeBase ||
@@ -27,9 +27,9 @@ export function RegistryPage({ apiUrl, registryClient }) {
27
27
  const [modules, setModules] = useState([]);
28
28
  const [isLoading, setIsLoading] = useState(true);
29
29
  const [error, setError] = useState(null);
30
- const canView = auth.hasPermission("admin.platform.view");
31
- const canEdit = auth.hasPermission("admin.platform.edit");
32
- const canDelete = auth.hasPermission("admin.platform.delete");
30
+ const canView = auth.hasPermission("admin.platform.registry.view");
31
+ const canEdit = auth.hasPermission("admin.platform.registry.edit");
32
+ const canDelete = auth.hasPermission("admin.platform.registry.delete");
33
33
  const [disableTarget, setDisableTarget] = useState(null);
34
34
  const [historyTarget, setHistoryTarget] = useState(null);
35
35
  const [versionHistory, setVersionHistory] = useState([]);
@@ -0,0 +1 @@
1
+ export declare function AccessControlAuditPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,135 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { DataTable, EmptyState, Input, LoadingState, } from "@nsxbet/admin-ui";
4
+ import { useFetch } from "../../../hooks/useFetch";
5
+ import { useI18n } from "../../../hooks/useI18n";
6
+ import { useTimestamp } from "../../../hooks/useTimestamp";
7
+ import { useOutletContext } from "react-router-dom";
8
+ import { createAccessControlApi } from "./api";
9
+ const PAGE_SIZE = 20;
10
+ export function AccessControlAuditPage() {
11
+ const { t } = useI18n();
12
+ const fetcher = useFetch();
13
+ const { apiUrl } = useOutletContext();
14
+ const api = useMemo(() => createAccessControlApi(fetcher, apiUrl), [fetcher, apiUrl]);
15
+ const apiRef = useRef(api);
16
+ apiRef.current = api;
17
+ const { formatDate } = useTimestamp();
18
+ const [filters, setFilters] = useState({
19
+ eventType: "",
20
+ actorEmail: "",
21
+ targetType: "",
22
+ targetId: "",
23
+ });
24
+ const [committedFilters, setCommittedFilters] = useState(filters);
25
+ const [events, setEvents] = useState([]);
26
+ const [hasLoaded, setHasLoaded] = useState(false);
27
+ const [isFiltering, setIsFiltering] = useState(false);
28
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
29
+ const [error, setError] = useState(null);
30
+ const [nextCursor, setNextCursor] = useState(null);
31
+ const sentinelRef = useRef(null);
32
+ const tRef = useRef(t);
33
+ tRef.current = t;
34
+ const commitFilters = useCallback(() => {
35
+ setCommittedFilters(filters);
36
+ }, [filters]);
37
+ const filterParams = useMemo(() => ({
38
+ eventType: committedFilters.eventType || undefined,
39
+ actorEmail: committedFilters.actorEmail || undefined,
40
+ targetType: committedFilters.targetType || undefined,
41
+ targetId: committedFilters.targetId || undefined,
42
+ limit: PAGE_SIZE,
43
+ }), [committedFilters]);
44
+ useEffect(() => {
45
+ let cancelled = false;
46
+ const loadAudit = async () => {
47
+ try {
48
+ setIsFiltering(true);
49
+ setError(null);
50
+ const page = await apiRef.current.listAuditEvents(filterParams);
51
+ if (cancelled)
52
+ return;
53
+ setEvents(page.items);
54
+ setNextCursor(page.nextCursor);
55
+ setHasLoaded(true);
56
+ }
57
+ catch (nextError) {
58
+ if (cancelled)
59
+ return;
60
+ setError(nextError instanceof Error ? nextError.message : tRef.current("errors.generic"));
61
+ }
62
+ finally {
63
+ if (!cancelled)
64
+ setIsFiltering(false);
65
+ }
66
+ };
67
+ loadAudit();
68
+ return () => { cancelled = true; };
69
+ }, [filterParams]);
70
+ const loadMoreRef = useRef();
71
+ const loadMore = useCallback(async () => {
72
+ if (!nextCursor || isLoadingMore)
73
+ return;
74
+ try {
75
+ setIsLoadingMore(true);
76
+ const page = await apiRef.current.listAuditEvents({
77
+ ...filterParams,
78
+ cursor: nextCursor,
79
+ });
80
+ setEvents((prev) => [...prev, ...page.items]);
81
+ setNextCursor(page.nextCursor);
82
+ }
83
+ catch {
84
+ // Silent failure on load-more; user can scroll again to retry
85
+ }
86
+ finally {
87
+ setIsLoadingMore(false);
88
+ }
89
+ }, [filterParams, nextCursor, isLoadingMore]);
90
+ loadMoreRef.current = loadMore;
91
+ useEffect(() => {
92
+ const sentinel = sentinelRef.current;
93
+ if (!sentinel || !nextCursor)
94
+ return;
95
+ const observer = new IntersectionObserver((entries) => {
96
+ if (entries[0].isIntersecting) {
97
+ loadMoreRef.current?.();
98
+ }
99
+ }, { rootMargin: "200px" });
100
+ observer.observe(sentinel);
101
+ return () => observer.disconnect();
102
+ }, [nextCursor]);
103
+ const columns = useMemo(() => [
104
+ {
105
+ accessor: "occurredAt",
106
+ header: t("accessControlPage.columnTimestamp"),
107
+ cell: (_value, row) => formatDate(new Date(row.occurredAt), "datetime"),
108
+ },
109
+ {
110
+ accessor: "eventType",
111
+ header: t("accessControlPage.columnEvent"),
112
+ },
113
+ {
114
+ accessor: "actorEmailSnapshot",
115
+ header: t("accessControlPage.columnActor"),
116
+ },
117
+ {
118
+ accessor: "targetType",
119
+ header: t("accessControlPage.columnTarget"),
120
+ cell: (_value, row) => `${row.targetType} / ${row.targetId}`,
121
+ },
122
+ {
123
+ accessor: "summary",
124
+ header: t("accessControlPage.columnSummary"),
125
+ },
126
+ ], [t, formatDate]);
127
+ const showInitialLoading = !hasLoaded && isFiltering;
128
+ const showTable = hasLoaded && !error;
129
+ return (_jsxs("section", { "data-testid": "access-control-audit-page", className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h2", { className: "text-xl font-semibold", children: t("accessControlPage.auditTitle") }), _jsx("span", { className: "rounded bg-muted px-2 py-1 text-xs font-medium", children: t("common.readOnly") })] }), _jsxs("div", { className: "grid gap-3 md:grid-cols-4", onKeyDown: (event) => {
130
+ if (event.key === "Enter")
131
+ commitFilters();
132
+ }, children: [_jsx(Input, { placeholder: t("accessControlPage.filterEventType"), value: filters.eventType, onChange: (event) => setFilters((current) => ({ ...current, eventType: event.target.value })), onBlur: commitFilters }), _jsx(Input, { placeholder: t("accessControlPage.filterActor"), value: filters.actorEmail, onChange: (event) => setFilters((current) => ({ ...current, actorEmail: event.target.value })), onBlur: commitFilters }), _jsx(Input, { placeholder: t("accessControlPage.filterTargetType"), value: filters.targetType, onChange: (event) => setFilters((current) => ({ ...current, targetType: event.target.value })), onBlur: commitFilters }), _jsx(Input, { placeholder: t("accessControlPage.filterTargetId"), value: filters.targetId, onChange: (event) => setFilters((current) => ({ ...current, targetId: event.target.value })), onBlur: commitFilters })] }), showInitialLoading ? (_jsx(LoadingState, { text: t("common.loading") })) : error ? (_jsx(EmptyState, { title: t("common.error"), description: error })) : showTable ? (_jsxs("div", { children: [_jsx("div", { className: isFiltering
133
+ ? "pointer-events-none opacity-50 transition-opacity duration-200"
134
+ : "transition-opacity duration-200", children: _jsx(DataTable, { data: events, columns: columns, emptyState: _jsx(EmptyState, { title: t("accessControlPage.noAuditTitle"), description: t("accessControlPage.noAuditDescription") }) }) }), nextCursor !== null && (_jsx("div", { ref: sentinelRef, className: "flex justify-center py-4", children: isLoadingMore && (_jsx("div", { className: "min-h-0 py-2", children: _jsx(LoadingState, { text: t("common.loading") }) })) }))] })) : null] }));
135
+ }