portal-mojo 0.1.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 (369) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +55 -0
  3. package/docs/DetailView.md +173 -0
  4. package/docs/ModelTable.md +128 -0
  5. package/docs/README.md +98 -0
  6. package/docs/admin-assistant.md +72 -0
  7. package/docs/admin-bouncer.md +120 -0
  8. package/docs/admin-cloudwatch.md +38 -0
  9. package/docs/admin-credentials.md +116 -0
  10. package/docs/admin-dashboard.md +15 -0
  11. package/docs/admin-devices-geoip.md +284 -0
  12. package/docs/admin-dns-certificates.md +122 -0
  13. package/docs/admin-dns-records.md +65 -0
  14. package/docs/admin-dns-registrar.md +33 -0
  15. package/docs/admin-dns.md +80 -0
  16. package/docs/admin-identity-users.md +103 -0
  17. package/docs/admin-incidents.md +71 -0
  18. package/docs/admin-jobs.md +426 -0
  19. package/docs/admin-members.md +112 -0
  20. package/docs/admin-messaging.md +56 -0
  21. package/docs/admin-metrics-explorer.md +63 -0
  22. package/docs/admin-monitoring.md +99 -0
  23. package/docs/admin-network-security.md +393 -0
  24. package/docs/admin-phonehub.md +27 -0
  25. package/docs/admin-push.md +36 -0
  26. package/docs/admin-rules.md +87 -0
  27. package/docs/admin-security-tickets.md +40 -0
  28. package/docs/admin-settings.md +78 -0
  29. package/docs/admin-shortlinks.md +32 -0
  30. package/docs/admin-storage.md +141 -0
  31. package/docs/auth-pages.md +212 -0
  32. package/docs/auth.md +58 -0
  33. package/docs/calendar.md +122 -0
  34. package/docs/charts.md +303 -0
  35. package/docs/client.md +206 -0
  36. package/docs/collection-multiselect.md +117 -0
  37. package/docs/collection-select.md +132 -0
  38. package/docs/combobox.md +105 -0
  39. package/docs/dataview.md +226 -0
  40. package/docs/datepicker.md +151 -0
  41. package/docs/daterange.md +170 -0
  42. package/docs/datetimepicker.md +249 -0
  43. package/docs/defineModel.md +72 -0
  44. package/docs/detail-primitives.md +174 -0
  45. package/docs/feedback.md +85 -0
  46. package/docs/file-upload-ux.md +146 -0
  47. package/docs/forms.md +435 -0
  48. package/docs/grouping-and-fmt.md +98 -0
  49. package/docs/idioms.md +112 -0
  50. package/docs/image-editor.md +101 -0
  51. package/docs/loading.md +242 -0
  52. package/docs/location-address.md +76 -0
  53. package/docs/markdown.md +203 -0
  54. package/docs/menus-and-access.md +89 -0
  55. package/docs/multiselect-dropdown.md +153 -0
  56. package/docs/params.md +73 -0
  57. package/docs/popover.md +130 -0
  58. package/docs/realtime.md +61 -0
  59. package/docs/record-feed.md +238 -0
  60. package/docs/right-panel.md +42 -0
  61. package/docs/tabs-and-form-wizard.md +60 -0
  62. package/docs/taginput.md +120 -0
  63. package/docs/timepicker.md +173 -0
  64. package/docs/timezone-select.md +147 -0
  65. package/docs/worldmap.md +302 -0
  66. package/package.json +55 -0
  67. package/src/admin/assistant/AssistantFeed.tsx +29 -0
  68. package/src/admin/assistant/AssistantPanel.tsx +65 -0
  69. package/src/admin/assistant/api.ts +17 -0
  70. package/src/admin/assistant/data.ts +63 -0
  71. package/src/admin/assistant/index.ts +21 -0
  72. package/src/admin/assistant/launchers.tsx +33 -0
  73. package/src/admin/assistant/pages.tsx +78 -0
  74. package/src/admin/assistant/streaming.ts +238 -0
  75. package/src/admin/assistant/types.ts +30 -0
  76. package/src/admin/bouncer/devices.tsx +230 -0
  77. package/src/admin/bouncer/index.ts +34 -0
  78. package/src/admin/bouncer/models.ts +199 -0
  79. package/src/admin/bouncer/signals.tsx +182 -0
  80. package/src/admin/bouncer/signatures.tsx +220 -0
  81. package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
  82. package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
  83. package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
  84. package/src/admin/cloudwatch/data.ts +163 -0
  85. package/src/admin/cloudwatch/index.ts +16 -0
  86. package/src/admin/credentials/group-api-keys.tsx +433 -0
  87. package/src/admin/credentials/index.ts +4 -0
  88. package/src/admin/credentials/models.ts +244 -0
  89. package/src/admin/credentials/secret-dialog.tsx +77 -0
  90. package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
  91. package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
  92. package/src/admin/dashboard/data.ts +60 -0
  93. package/src/admin/dashboard/index.ts +2 -0
  94. package/src/admin/dns/CertificateDetail.tsx +115 -0
  95. package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
  96. package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
  97. package/src/admin/dns/CertificatesPage.tsx +85 -0
  98. package/src/admin/dns/DnsRecordEditor.tsx +110 -0
  99. package/src/admin/dns/DnsRecordsPage.tsx +36 -0
  100. package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
  101. package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
  102. package/src/admin/dns/DomainDetail.tsx +41 -0
  103. package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
  104. package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
  105. package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
  106. package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
  107. package/src/admin/dns/DomainsPage.tsx +27 -0
  108. package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
  109. package/src/admin/dns/RegistrantContactPage.tsx +42 -0
  110. package/src/admin/dns/api.ts +515 -0
  111. package/src/admin/dns/certificate-data.ts +221 -0
  112. package/src/admin/dns/data.ts +32 -0
  113. package/src/admin/dns/dns-data.ts +266 -0
  114. package/src/admin/dns/dns-integration.ts +24 -0
  115. package/src/admin/dns/domain-links.ts +37 -0
  116. package/src/admin/dns/index.ts +86 -0
  117. package/src/admin/dns/models.ts +418 -0
  118. package/src/admin/dns/purchase-data.ts +67 -0
  119. package/src/admin/dns/registrant-data.ts +52 -0
  120. package/src/admin/identity/index.ts +5 -0
  121. package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
  122. package/src/admin/identity/members/MemberDetail.tsx +226 -0
  123. package/src/admin/identity/members/MembersPage.tsx +144 -0
  124. package/src/admin/identity/members/index.ts +24 -0
  125. package/src/admin/identity/members/member-flows.tsx +132 -0
  126. package/src/admin/identity/members/models.ts +204 -0
  127. package/src/admin/identity/users/UserDetail.tsx +257 -0
  128. package/src/admin/identity/users/UsersPage.tsx +163 -0
  129. package/src/admin/identity/users/index.ts +27 -0
  130. package/src/admin/identity/users/models.ts +232 -0
  131. package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
  132. package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
  133. package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
  134. package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
  135. package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
  136. package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
  137. package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
  138. package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
  139. package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
  140. package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
  141. package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
  142. package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
  143. package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
  144. package/src/admin/identity/users/sections/actions.tsx +752 -0
  145. package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
  146. package/src/admin/identity/users/sections/queries.ts +74 -0
  147. package/src/admin/identity/users/sections/shared.tsx +299 -0
  148. package/src/admin/incidents/EventDetail.tsx +43 -0
  149. package/src/admin/incidents/EventsPage.tsx +52 -0
  150. package/src/admin/incidents/IncidentDetail.tsx +68 -0
  151. package/src/admin/incidents/IncidentsPage.tsx +102 -0
  152. package/src/admin/incidents/forensics.tsx +73 -0
  153. package/src/admin/incidents/index.ts +16 -0
  154. package/src/admin/incidents/models.ts +113 -0
  155. package/src/admin/incidents/sanitize.ts +71 -0
  156. package/src/admin/index.ts +383 -0
  157. package/src/admin/jobs/JobDashboardPage.tsx +111 -0
  158. package/src/admin/jobs/JobDetail.tsx +721 -0
  159. package/src/admin/jobs/JobRunnersPage.tsx +216 -0
  160. package/src/admin/jobs/JobsTablePage.tsx +122 -0
  161. package/src/admin/jobs/RunnerDetail.tsx +576 -0
  162. package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
  163. package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
  164. package/src/admin/jobs/columns.tsx +297 -0
  165. package/src/admin/jobs/control.ts +488 -0
  166. package/src/admin/jobs/index.ts +85 -0
  167. package/src/admin/jobs/models.ts +599 -0
  168. package/src/admin/jobs/queries.ts +272 -0
  169. package/src/admin/jobs/scheduled-task-form.tsx +493 -0
  170. package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
  171. package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
  172. package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
  173. package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
  174. package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
  175. package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
  176. package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
  177. package/src/admin/messaging/MailboxesPage.tsx +15 -0
  178. package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
  179. package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
  180. package/src/admin/messaging/SentMessagesPage.tsx +11 -0
  181. package/src/admin/messaging/api.ts +17 -0
  182. package/src/admin/messaging/data.ts +10 -0
  183. package/src/admin/messaging/index.ts +23 -0
  184. package/src/admin/messaging/models.ts +76 -0
  185. package/src/admin/messaging/push/PushPage.tsx +44 -0
  186. package/src/admin/messaging/push/api.ts +17 -0
  187. package/src/admin/messaging/push/index.ts +9 -0
  188. package/src/admin/messaging/push/models.ts +43 -0
  189. package/src/admin/messaging/sanitize.ts +44 -0
  190. package/src/admin/monitoring/LogInspector.tsx +160 -0
  191. package/src/admin/monitoring/LogsPage.tsx +135 -0
  192. package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
  193. package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
  194. package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
  195. package/src/admin/monitoring/index.ts +55 -0
  196. package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
  197. package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
  198. package/src/admin/monitoring/models.ts +155 -0
  199. package/src/admin/network/BlockedIPsPage.tsx +194 -0
  200. package/src/admin/network/BlockedIpDetail.tsx +262 -0
  201. package/src/admin/network/FirewallLogPage.tsx +115 -0
  202. package/src/admin/network/IPSetDetail.tsx +280 -0
  203. package/src/admin/network/IPSetEditor.tsx +191 -0
  204. package/src/admin/network/IPSetsPage.tsx +177 -0
  205. package/src/admin/network/geofence/BlocksTab.tsx +289 -0
  206. package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
  207. package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
  208. package/src/admin/network/geofence/PostureHeader.tsx +184 -0
  209. package/src/admin/network/geofence/RuleEditor.tsx +176 -0
  210. package/src/admin/network/geofence/RulesTab.tsx +254 -0
  211. package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
  212. package/src/admin/network/geofence/geofence-data.ts +674 -0
  213. package/src/admin/network/index.ts +68 -0
  214. package/src/admin/network/models.ts +607 -0
  215. package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
  216. package/src/admin/phonehub/api.ts +47 -0
  217. package/src/admin/phonehub/data.ts +6 -0
  218. package/src/admin/phonehub/index.ts +10 -0
  219. package/src/admin/phonehub/models.ts +68 -0
  220. package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
  221. package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
  222. package/src/admin/rules/RuleSetsPage.tsx +33 -0
  223. package/src/admin/rules/editors.tsx +54 -0
  224. package/src/admin/rules/handler-dsl.ts +213 -0
  225. package/src/admin/rules/index.ts +13 -0
  226. package/src/admin/rules/models.ts +112 -0
  227. package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
  228. package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
  229. package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
  230. package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
  231. package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
  232. package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
  233. package/src/admin/security/devices/index.ts +40 -0
  234. package/src/admin/security/devices/models.ts +470 -0
  235. package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
  236. package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
  237. package/src/admin/security/geoip/geoip-forms.ts +102 -0
  238. package/src/admin/security/geoip/index.ts +27 -0
  239. package/src/admin/security/geoip/models.ts +397 -0
  240. package/src/admin/security/index.ts +31 -0
  241. package/src/admin/security/models.ts +137 -0
  242. package/src/admin/security/tickets.tsx +562 -0
  243. package/src/admin/security-permissions.ts +5 -0
  244. package/src/admin/settings/SettingDetail.tsx +57 -0
  245. package/src/admin/settings/SettingEditor.tsx +157 -0
  246. package/src/admin/settings/SettingsPage.tsx +72 -0
  247. package/src/admin/settings/index.ts +19 -0
  248. package/src/admin/settings/model.ts +143 -0
  249. package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
  250. package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
  251. package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
  252. package/src/admin/shortlinks/index.ts +18 -0
  253. package/src/admin/shortlinks/models.ts +246 -0
  254. package/src/admin/storage/BackendsPage.tsx +43 -0
  255. package/src/admin/storage/BucketDetail.tsx +77 -0
  256. package/src/admin/storage/BucketsPage.tsx +64 -0
  257. package/src/admin/storage/FileManagerDetail.tsx +36 -0
  258. package/src/admin/storage/FilePreview.tsx +54 -0
  259. package/src/admin/storage/FileUploadSurface.tsx +250 -0
  260. package/src/admin/storage/FileView.tsx +109 -0
  261. package/src/admin/storage/FilesPage.tsx +53 -0
  262. package/src/admin/storage/file-renditions.ts +99 -0
  263. package/src/admin/storage/index.ts +31 -0
  264. package/src/admin/storage/models.ts +585 -0
  265. package/src/admin/storage/storage-dialogs.tsx +80 -0
  266. package/src/charts/CircularProgress.tsx +281 -0
  267. package/src/charts/KPIStrip.tsx +162 -0
  268. package/src/charts/KPITile.tsx +125 -0
  269. package/src/charts/MetricsChart.tsx +459 -0
  270. package/src/charts/MetricsMiniWidget.tsx +588 -0
  271. package/src/charts/MiniChart.tsx +283 -0
  272. package/src/charts/PieChart.tsx +354 -0
  273. package/src/charts/SeriesChart.tsx +378 -0
  274. package/src/charts/chart-dialogs.tsx +149 -0
  275. package/src/charts/exportChart.ts +144 -0
  276. package/src/charts/index.ts +28 -0
  277. package/src/charts/pie-math.ts +127 -0
  278. package/src/charts/stats.ts +265 -0
  279. package/src/charts/worldmap/WorldMap.tsx +649 -0
  280. package/src/charts/worldmap/countryCentroids.ts +304 -0
  281. package/src/charts/worldmap/geo.ts +293 -0
  282. package/src/charts/worldmap/use-world-land.ts +47 -0
  283. package/src/charts/worldmap/world-land.ts +18 -0
  284. package/src/charts/worldmap/worldmap-data.ts +285 -0
  285. package/src/client/auth.ts +737 -0
  286. package/src/client/client.ts +268 -0
  287. package/src/client/duid.ts +31 -0
  288. package/src/client/errors.ts +33 -0
  289. package/src/client/group-context.ts +28 -0
  290. package/src/client/group.tsx +126 -0
  291. package/src/client/hooks.ts +50 -0
  292. package/src/client/index.ts +37 -0
  293. package/src/client/jwt.ts +75 -0
  294. package/src/client/location.ts +285 -0
  295. package/src/client/lookups.ts +41 -0
  296. package/src/client/markdown-fallback.tsx +115 -0
  297. package/src/client/markdown-parse.ts +386 -0
  298. package/src/client/markdown-sanitize.ts +217 -0
  299. package/src/client/markdown.ts +122 -0
  300. package/src/client/me.ts +188 -0
  301. package/src/client/mock.ts +9984 -0
  302. package/src/client/model.ts +227 -0
  303. package/src/client/params.ts +292 -0
  304. package/src/client/realtime-mock.ts +221 -0
  305. package/src/client/realtime.ts +561 -0
  306. package/src/client/record-feed.ts +329 -0
  307. package/src/client/safe-export.ts +71 -0
  308. package/src/client/types.ts +278 -0
  309. package/src/client/upload.ts +421 -0
  310. package/src/ui/AddressField.tsx +273 -0
  311. package/src/ui/AttachmentQueue.tsx +92 -0
  312. package/src/ui/CollectionMultiSelect.tsx +622 -0
  313. package/src/ui/CollectionSelect.tsx +517 -0
  314. package/src/ui/ComboBox.tsx +336 -0
  315. package/src/ui/DetailView.tsx +438 -0
  316. package/src/ui/FileDrop.tsx +194 -0
  317. package/src/ui/FileField.tsx +369 -0
  318. package/src/ui/FilterBar.tsx +269 -0
  319. package/src/ui/FormFields.tsx +77 -0
  320. package/src/ui/FormView.tsx +360 -0
  321. package/src/ui/FormWizard.tsx +336 -0
  322. package/src/ui/GroupSwitcher.tsx +202 -0
  323. package/src/ui/Guarded.tsx +16 -0
  324. package/src/ui/MarkdownView.tsx +89 -0
  325. package/src/ui/ModelTable.tsx +848 -0
  326. package/src/ui/MultiSelectDropdown.tsx +390 -0
  327. package/src/ui/Popover.tsx +204 -0
  328. package/src/ui/RecordFeed.tsx +627 -0
  329. package/src/ui/RequiresGroup.tsx +32 -0
  330. package/src/ui/RightPanel.tsx +107 -0
  331. package/src/ui/SidebarNav.tsx +330 -0
  332. package/src/ui/Tabs.tsx +161 -0
  333. package/src/ui/TagInput.tsx +381 -0
  334. package/src/ui/ThemeProvider.tsx +52 -0
  335. package/src/ui/UploadQueue.tsx +603 -0
  336. package/src/ui/armed-button.tsx +91 -0
  337. package/src/ui/date/Calendar.tsx +498 -0
  338. package/src/ui/date/DatePicker.tsx +289 -0
  339. package/src/ui/date/DateRangePicker.tsx +280 -0
  340. package/src/ui/date/DateTimePicker.tsx +587 -0
  341. package/src/ui/date/PresetRail.tsx +165 -0
  342. package/src/ui/date/TimePicker.tsx +624 -0
  343. package/src/ui/date/TimezoneSelect.tsx +267 -0
  344. package/src/ui/date/fns.ts +679 -0
  345. package/src/ui/detail/DataView.tsx +692 -0
  346. package/src/ui/detail/FlowStrip.tsx +80 -0
  347. package/src/ui/detail/KnownFieldsCard.tsx +121 -0
  348. package/src/ui/detail/MetadataSection.tsx +219 -0
  349. package/src/ui/detail/StackTraceView.tsx +150 -0
  350. package/src/ui/detail/StatusPanel.tsx +73 -0
  351. package/src/ui/detail/Timeline.tsx +52 -0
  352. package/src/ui/detail/index.ts +12 -0
  353. package/src/ui/field-registry.tsx +428 -0
  354. package/src/ui/field-wire.ts +322 -0
  355. package/src/ui/form-autosave.ts +583 -0
  356. package/src/ui/format.ts +444 -0
  357. package/src/ui/grouping.ts +150 -0
  358. package/src/ui/image-editor/ImageEditor.tsx +484 -0
  359. package/src/ui/image-editor/canvas.ts +254 -0
  360. package/src/ui/image-editor/index.ts +3 -0
  361. package/src/ui/image-editor/math.ts +416 -0
  362. package/src/ui/index.ts +58 -0
  363. package/src/ui/loading.tsx +604 -0
  364. package/src/ui/menu-registry.ts +213 -0
  365. package/src/ui/modal.tsx +297 -0
  366. package/src/ui/password.tsx +302 -0
  367. package/src/ui/schema-form-core.tsx +315 -0
  368. package/src/ui/toast.tsx +219 -0
  369. package/src/ui/ui.tsx +45 -0
@@ -0,0 +1,146 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useQueryClient } from '@tanstack/react-query';
3
+ import { useCan, type Params } from '../../../client';
4
+ import { Badge, Eyebrow, SecurityItem, fmt, modal } from '../../../ui';
5
+ import { MemberDetail, type MemberNavigationCallbacks } from './MemberDetail';
6
+ import { openMemberAdmissionDialog, type AdmissionGroup, type AdmissionMode } from './member-flows';
7
+ import {
8
+ MEMBER_GROUP_READ_PERMISSIONS,
9
+ MEMBER_INVITE_PERMISSIONS,
10
+ MEMBER_SAVE_PERMISSIONS,
11
+ MEMBER_USER_DIRECTORY_PERMISSIONS,
12
+ MemberModel,
13
+ rawMemberGrants,
14
+ type MemberRow,
15
+ } from './models';
16
+
17
+ type StatusFilter = 'active' | 'inactive' | 'all';
18
+ const PAGE_SIZE = 15;
19
+
20
+ function paramsFor(groupId: number, search: string, status: StatusFilter, start: number): Params {
21
+ const params: Params = { group: groupId, size: PAGE_SIZE, start, sort: '-id' };
22
+ if (search) params.search = search;
23
+ if (status !== 'all') params.is_active = status === 'active';
24
+ return params;
25
+ }
26
+
27
+ function MemberItem({ member, onOpen }: { member: MemberRow; onOpen: () => void }) {
28
+ const user = member.user;
29
+ const grants = rawMemberGrants(member.permissions);
30
+ const role = typeof member.metadata?.role === 'string' && member.metadata.role.trim()
31
+ ? member.metadata.role : null;
32
+ const shown = grants.slice(0, 5);
33
+ return (
34
+ <div
35
+ className="ga-click-row" role="button" tabIndex={0}
36
+ onClick={onOpen}
37
+ onKeyDown={(event) => {
38
+ if (event.key === 'Enter' || event.key === ' ') {
39
+ event.preventDefault();
40
+ onOpen();
41
+ }
42
+ }}
43
+ >
44
+ <SecurityItem
45
+ icon="bi-person-circle"
46
+ title={user?.display_name || user?.email || user?.username || `User #${user?.id ?? '?'}`}
47
+ desc={`${user?.email ?? 'No email'} · Joined ${fmt.date(member.created)}`}
48
+ >
49
+ <span className="chip-row">
50
+ {role && <Badge tone="primary">{role}</Badge>}
51
+ {shown.map((grant) => <Badge key={grant} tone="info">{grant}</Badge>)}
52
+ {grants.length > shown.length && <Badge tone="muted">+{grants.length - shown.length}</Badge>}
53
+ <Badge tone={member.is_active ? 'success' : 'muted'}>{member.is_active ? 'Active' : 'Inactive'}</Badge>
54
+ </span>
55
+ </SecurityItem>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export interface GroupMembersPanelProps extends MemberNavigationCallbacks {
61
+ group: AdmissionGroup;
62
+ }
63
+
64
+ export function GroupMembersPanel({ group, onNavigateUser, onNavigateGroup }: GroupMembersPanelProps) {
65
+ const qc = useQueryClient();
66
+ const [search, setSearch] = useState('');
67
+ const [status, setStatus] = useState<StatusFilter>('active');
68
+ const [start, setStart] = useState(0);
69
+ useEffect(() => setStart(0), [search, status, group.id]);
70
+ const query = MemberModel.useList(paramsFor(group.id, search, status, start));
71
+ const { can: canInvite } = useCan(MEMBER_INVITE_PERMISSIONS);
72
+ const { can: canCreate } = useCan(MEMBER_SAVE_PERMISSIONS);
73
+ const { can: canReadUsers } = useCan(MEMBER_USER_DIRECTORY_PERMISSIONS);
74
+ const members = query.data?.rows ?? [];
75
+ const count = query.data?.count ?? 0;
76
+
77
+ const admit = (initialMode: AdmissionMode) => openMemberAdmissionDialog({
78
+ group,
79
+ initialMode,
80
+ canInvite,
81
+ canCreate,
82
+ canReadUsers,
83
+ queryClient: qc,
84
+ });
85
+ const openMember = (member: MemberRow) => {
86
+ void MemberModel.fetchOne(qc, member.id).catch(() => {});
87
+ void modal.detail((close) => (
88
+ <MemberDetail
89
+ id={member.id}
90
+ onClose={() => close(null)}
91
+ readPermissions={MEMBER_GROUP_READ_PERMISSIONS}
92
+ onNavigateUser={onNavigateUser}
93
+ onNavigateGroup={onNavigateGroup}
94
+ />
95
+ ));
96
+ };
97
+
98
+ return (
99
+ <>
100
+ <Eyebrow>Members</Eyebrow>
101
+ {(canInvite || (canCreate && canReadUsers)) && (
102
+ <div className="ga-toolbar">
103
+ {canInvite && (
104
+ <button className="btn btn-primary btn-compact" onClick={() => { void admit('invite'); }}>
105
+ <i className="bi bi-envelope" /> Invite by email
106
+ </button>
107
+ )}
108
+ {canCreate && canReadUsers && (
109
+ <button className="btn btn-compact" onClick={() => { void admit('add'); }}>
110
+ <i className="bi bi-person-plus" /> Add existing user
111
+ </button>
112
+ )}
113
+ </div>
114
+ )}
115
+ <div className="ga-filter-row">
116
+ <input
117
+ className="input" type="search" placeholder="Search members…"
118
+ value={search} onChange={(event) => setSearch(event.target.value)}
119
+ />
120
+ <select
121
+ className="input input-compact" value={status} aria-label="Status filter"
122
+ onChange={(event) => setStatus(event.target.value as StatusFilter)}
123
+ >
124
+ <option value="active">Active</option>
125
+ <option value="inactive">Inactive</option>
126
+ <option value="all">All</option>
127
+ </select>
128
+ </div>
129
+ {query.isPending && <div className="skel skel-block" />}
130
+ {query.error && <p className="text-bad">{query.error.message}</p>}
131
+ {!query.isPending && !query.error && members.length === 0 && (
132
+ <p className="dim-italic">{search ? 'No members match this search.' : 'No members in this view.'}</p>
133
+ )}
134
+ {members.map((member) => <MemberItem key={member.id} member={member} onOpen={() => openMember(member)} />)}
135
+ {count > 0 && (
136
+ <div className="ga-toolbar" style={{ justifyContent: 'space-between' }}>
137
+ <span className="dim">Showing {start + 1}–{Math.min(start + members.length, count)} of {count}</span>
138
+ <span className="chip-row">
139
+ <button className="btn btn-compact" disabled={start === 0} onClick={() => setStart(Math.max(0, start - PAGE_SIZE))}>Previous</button>
140
+ <button className="btn btn-compact" disabled={start + members.length >= count} onClick={() => setStart(start + PAGE_SIZE)}>Next</button>
141
+ </span>
142
+ </div>
143
+ )}
144
+ </>
145
+ );
146
+ }
@@ -0,0 +1,226 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { useQueryClient } from '@tanstack/react-query';
3
+ import { useCan, type PermSpec } from '../../../client';
4
+ import {
5
+ Badge, DetailView, Eyebrow, FlatRow, FormView, JsonBlock, MetricCard,
6
+ ModelTable, fmt, formModal, toast, type Column, type DetailMenuEntry,
7
+ } from '../../../ui';
8
+ import { LOGS_ADMIN_PERMISSIONS, LogModel, type LogRow } from '../../monitoring';
9
+ import {
10
+ MEMBER_READ_PERMISSIONS,
11
+ MEMBER_SAVE_PERMISSIONS,
12
+ MemberModel,
13
+ effectiveMemberGrants,
14
+ getMemberPermissions,
15
+ ignoredMemberGrants,
16
+ memberPermissionFields,
17
+ memberPermissionsVersion,
18
+ rawMemberGrants,
19
+ subscribeMemberPermissions,
20
+ type MemberRow,
21
+ } from './models';
22
+
23
+ export interface MemberNavigationCallbacks {
24
+ onNavigateUser?: (id: number) => void;
25
+ onNavigateGroup?: (id: number) => void;
26
+ }
27
+
28
+ export interface MemberDetailProps extends MemberNavigationCallbacks {
29
+ id: number;
30
+ onClose: () => void;
31
+ /** Global Admin is system-pinned; fixed-group compositions pass their read gate. */
32
+ readPermissions?: PermSpec;
33
+ }
34
+
35
+ function AuditTable({ memberId }: { memberId: number }) {
36
+ const columns: Column<LogRow>[] = [
37
+ { key: 'created', label: 'Time', sortable: true, render: (row) => fmt.datetime(row.created) },
38
+ { key: 'level', label: 'Level', sortable: true, render: (row) => <Badge>{row.level}</Badge> },
39
+ { key: 'kind', label: 'Kind', sortable: false, render: (row) => row.kind ?? '—' },
40
+ { key: 'log', label: 'Event', sortable: false, render: (row) => row.log ?? '—' },
41
+ ];
42
+ return (
43
+ <ModelTable<LogRow>
44
+ model={LogModel}
45
+ title="Membership audit"
46
+ columns={columns}
47
+ searchable={false}
48
+ defaultParams={{ model_name: 'account.GroupMember', model_id: memberId, sort: '-created', size: 15 }}
49
+ defaultSort="-created"
50
+ />
51
+ );
52
+ }
53
+
54
+ function GrantSummary({ member }: { member: MemberRow }) {
55
+ const raw = rawMemberGrants(member.permissions);
56
+ const effective = effectiveMemberGrants(member.permissions);
57
+ const ignored = ignoredMemberGrants(member.permissions);
58
+ return (
59
+ <>
60
+ <Eyebrow>Effective membership grants</Eyebrow>
61
+ <div className="chip-row">
62
+ {effective.map((grant) => <Badge key={grant} tone="info">{grant}</Badge>)}
63
+ </div>
64
+ <Eyebrow>Raw stored grants</Eyebrow>
65
+ {raw.length > 0
66
+ ? <div className="chip-row">{raw.map((grant) => <Badge key={grant} tone={ignored.includes(grant) ? 'warning' : 'muted'}>{grant}</Badge>)}</div>
67
+ : <p className="dim">No raw grants are stored.</p>}
68
+ {ignored.length > 0 && (
69
+ <p className="dim">
70
+ Ignored: <code>{ignored.join(', ')}</code>. Literal <code>sys.*</code> and stored <code>full_member</code> never grant member authority.
71
+ <code> admin</code> remains a product-portal client compatibility wildcard, but django-mojo does not honor it server-side.
72
+ </p>
73
+ )}
74
+ </>
75
+ );
76
+ }
77
+
78
+ export function MemberDetail({
79
+ id, onClose, onNavigateUser, onNavigateGroup,
80
+ readPermissions = MEMBER_READ_PERMISSIONS,
81
+ }: MemberDetailProps) {
82
+ const qc = useQueryClient();
83
+ const { can: canRead, isLoading: readLoading } = useCan(readPermissions);
84
+ const query = MemberModel.useOne(canRead ? id : null);
85
+ const save = MemberModel.useSave();
86
+ const resend = MemberModel.useAction('resend_invite');
87
+ const { can: canSave } = useCan(MEMBER_SAVE_PERMISSIONS);
88
+ useSyncExternalStore(subscribeMemberPermissions, memberPermissionsVersion, memberPermissionsVersion);
89
+
90
+ if (readLoading) return <div className="detail-loading"><span className="skel skel-block" /></div>;
91
+ if (!canRead) return <div className="modal-pad text-bad">Access denied</div>;
92
+ if (query.isPending) return <div className="detail-loading"><span className="skel skel-block" /></div>;
93
+ if (!query.data || query.error) return <div className="modal-pad text-bad">{query.error?.message ?? 'Membership not found'}</div>;
94
+ const member = query.data;
95
+ const userLabel = member.user?.display_name || member.user?.email || member.user?.username || 'Unknown user';
96
+ const groupLabel = member.group?.name || 'Unknown group';
97
+ const role = typeof member.metadata?.role === 'string' && member.metadata.role.trim()
98
+ ? member.metadata.role.trim() : 'Member';
99
+ const inviter = typeof member.metadata?.invited_by_name === 'string'
100
+ ? member.metadata.invited_by_name
101
+ : typeof member.metadata?.invited_by === 'string' || typeof member.metadata?.invited_by === 'number'
102
+ ? String(member.metadata.invited_by)
103
+ : null;
104
+ const raw = rawMemberGrants(member.permissions);
105
+ const catalog = getMemberPermissions();
106
+ const catalogNames = new Set(catalog.map((permission) => permission.name));
107
+ const deploymentOnly = raw.filter((name) => !catalogNames.has(name) && !ignoredMemberGrants(member.permissions).includes(name));
108
+
109
+ const editRole = async () => {
110
+ const values = await formModal({
111
+ title: 'Edit role label',
112
+ submitText: 'Update label',
113
+ fields: [{
114
+ name: 'role', type: 'text', label: 'Role label',
115
+ help: 'Presentation only. This label does not grant authorization.',
116
+ }],
117
+ initial: { role },
118
+ });
119
+ if (!values) return;
120
+ try {
121
+ await save.mutateAsync({ id: member.id, changes: { metadata: { role: String(values.role ?? '').trim() } } });
122
+ toast.success('Role label updated');
123
+ } catch (error) {
124
+ toast.error(error instanceof Error ? error.message : 'Role update failed');
125
+ }
126
+ };
127
+
128
+ const setActive = async (next: boolean) => {
129
+ try {
130
+ await save.mutateAsync({ id: member.id, changes: { is_active: next } });
131
+ toast.success(next ? 'Membership activated' : 'Membership deactivated');
132
+ } catch (error) {
133
+ toast.error(error instanceof Error ? error.message : 'Membership update failed');
134
+ }
135
+ };
136
+
137
+ const resendInvite = async () => {
138
+ try {
139
+ await resend.mutateAsync({ id: member.id });
140
+ toast.success('Invitation resent');
141
+ } catch (error) {
142
+ toast.error(error instanceof Error ? error.message : 'Invitation resend failed');
143
+ }
144
+ };
145
+
146
+ const menu: DetailMenuEntry<MemberRow>[] = [
147
+ ...(onNavigateUser && member.user ? [{ label: 'View user', icon: 'bi-person', onSelect: () => onNavigateUser(member.user!.id) }] : []),
148
+ ...(onNavigateGroup && member.group ? [{ label: 'View group', icon: 'bi-people', onSelect: () => onNavigateGroup(member.group!.id) }] : []),
149
+ { divider: true as const },
150
+ { label: 'Edit role label', icon: 'bi-person-badge', permissions: MEMBER_SAVE_PERMISSIONS, onSelect: () => { void editRole(); } },
151
+ { label: 'Resend invitation', icon: 'bi-envelope-arrow-up', permissions: MEMBER_SAVE_PERMISSIONS, onSelect: () => { void resendInvite(); } },
152
+ {
153
+ label: member.is_active ? 'Deactivate membership' : 'Activate membership',
154
+ icon: member.is_active ? 'bi-toggle-off' : 'bi-toggle-on',
155
+ danger: member.is_active,
156
+ permissions: MEMBER_SAVE_PERMISSIONS,
157
+ onSelect: () => { void setActive(!member.is_active); },
158
+ },
159
+ ];
160
+
161
+ return (
162
+ <DetailView<MemberRow>
163
+ icon="bi-person-badge"
164
+ title={`${userLabel} in ${groupLabel}`}
165
+ subtitle={`${role} · joined ${fmt.relative(member.created)}`}
166
+ chips={[
167
+ ...(member.user?.email ? [{ icon: 'bi-envelope', text: member.user.email, tone: 'muted' as const }] : []),
168
+ ...(member.group?.kind ? [{ icon: 'bi-people', text: member.group.kind, tone: 'info' as const }] : []),
169
+ { icon: 'bi-person-badge', text: role, tone: 'primary' as const },
170
+ { text: `${effectiveMemberGrants(member.permissions).length} effective grants`, tone: 'muted' as const },
171
+ ]}
172
+ active={canSave ? { value: member.is_active, onChange: (next) => { void setActive(next); } } : undefined}
173
+ contextMenu={menu}
174
+ menuContext={member}
175
+ onClose={onClose}
176
+ sections={[
177
+ {
178
+ key: 'Overview', label: 'Overview', icon: 'bi-grid-1x2', render: () => (
179
+ <>
180
+ <div className="detail-kpi-grid">
181
+ <MetricCard label="Role label" value={role} />
182
+ <MetricCard label="Status" value={member.is_active ? 'Active' : 'Inactive'} />
183
+ <MetricCard label="Joined" value={fmt.relative(member.created)} />
184
+ <MetricCard label="Effective grants" value={effectiveMemberGrants(member.permissions).length} />
185
+ </div>
186
+ <Eyebrow>This membership</Eyebrow>
187
+ <FlatRow label="User" action={member.user && onNavigateUser ? () => onNavigateUser(member.user!.id) : undefined} actionTitle="View user">{userLabel}</FlatRow>
188
+ <FlatRow label="Email">{member.user?.email ?? '—'}</FlatRow>
189
+ <FlatRow label="Group" action={member.group && onNavigateGroup ? () => onNavigateGroup(member.group!.id) : undefined} actionTitle="View group">{groupLabel}</FlatRow>
190
+ <FlatRow label="Role label" action={canSave ? () => { void editRole(); } : undefined}>{role}</FlatRow>
191
+ <FlatRow label="Joined">{fmt.datetime(member.created)}</FlatRow>
192
+ <FlatRow label="Modified">{fmt.datetime(member.modified)}</FlatRow>
193
+ {inviter && <FlatRow label="Invited by">{inviter}</FlatRow>}
194
+ <GrantSummary member={member} />
195
+ </>
196
+ ),
197
+ },
198
+ {
199
+ key: 'Permissions', label: 'Permissions', icon: 'bi-shield-lock', render: () => (
200
+ <>
201
+ <Eyebrow>Per-group authorization</Eyebrow>
202
+ <p className="dim">Role labels are not authorization. Protected changes may be rejected by <code>MEMBER_PERMS_PROTECTION</code>; failed autosaves revert and show the server error.</p>
203
+ {canSave
204
+ ? <FormView model={MemberModel} row={member} fields={memberPermissionFields()} onSaved={() => { void MemberModel.invalidate(qc); }} />
205
+ : <GrantSummary member={member} />}
206
+ {deploymentOnly.length > 0 && (
207
+ <p className="dim">Deployment-specific stored grants without registered editors remain read-only: <code>{deploymentOnly.join(', ')}</code>.</p>
208
+ )}
209
+ </>
210
+ ),
211
+ },
212
+ { divider: 'Activity' },
213
+ {
214
+ key: 'Audit', label: 'Audit', icon: 'bi-clock-history',
215
+ permissions: LOGS_ADMIN_PERMISSIONS,
216
+ render: () => <AuditTable memberId={member.id} />,
217
+ },
218
+ {
219
+ key: 'Raw', label: 'Raw', icon: 'bi-braces', render: () => (
220
+ <JsonBlock value={{ permissions: member.permissions, metadata: member.metadata }} label="Membership JSON" defaultOpen />
221
+ ),
222
+ },
223
+ ]}
224
+ />
225
+ );
226
+ }
@@ -0,0 +1,144 @@
1
+ import { useQueryClient } from '@tanstack/react-query';
2
+ import { useCan } from '../../../client';
3
+ import {
4
+ Badge, ModelTable, fmt, formModal, modal,
5
+ type BatchAction, type Column, type FilterDef,
6
+ } from '../../../ui';
7
+ import { MemberDetail, type MemberNavigationCallbacks } from './MemberDetail';
8
+ import { openMemberAdmissionDialog } from './member-flows';
9
+ import {
10
+ MEMBER_INVITE_PERMISSIONS,
11
+ MEMBER_SAVE_PERMISSIONS,
12
+ MEMBER_USER_DIRECTORY_PERMISSIONS,
13
+ MemberModel,
14
+ rawMemberGrants,
15
+ type MemberRow,
16
+ } from './models';
17
+
18
+ const COLUMNS: Column<MemberRow>[] = [
19
+ {
20
+ key: 'user__display_name', label: 'Member', sortable: false, hideable: false, render: (member) => (
21
+ <div className="cell-user">
22
+ <span className="cell-avatar"><i className="bi bi-person" /></span>
23
+ <span>
24
+ <span className="cell-name">{member.user?.display_name || member.user?.username || `User #${member.user?.id ?? '?'}`}</span>
25
+ <span className="cell-sub">{member.user?.email ?? 'No linked email'}</span>
26
+ </span>
27
+ </div>
28
+ ),
29
+ },
30
+ {
31
+ key: 'group__name', label: 'Group', sortable: false, render: (member) => (
32
+ <span>{member.group?.name ?? 'Unknown group'}{member.group?.kind && <span className="cell-sub">{member.group.kind}</span>}</span>
33
+ ),
34
+ },
35
+ {
36
+ key: 'metadata__role', label: 'Role label', sortable: false, render: (member) => {
37
+ const role = typeof member.metadata?.role === 'string' ? member.metadata.role.trim() : '';
38
+ return role ? <Badge tone="primary">{role}</Badge> : <span className="dim">Member</span>;
39
+ },
40
+ },
41
+ { key: 'is_active', label: 'Status', sortable: true, render: (member) => <Badge>{member.is_active ? 'Active' : 'Inactive'}</Badge> },
42
+ {
43
+ key: 'permissions', label: 'Granted permissions', sortable: false, render: (member) => {
44
+ const grants = rawMemberGrants(member.permissions);
45
+ return grants.length > 0
46
+ ? <span className="chip-row">{grants.slice(0, 3).map((grant) => <Badge key={grant} tone="info">{grant}</Badge>)}{grants.length > 3 && <Badge tone="muted">+{grants.length - 3}</Badge>}</span>
47
+ : <span className="dim">None</span>;
48
+ },
49
+ },
50
+ { key: 'created', label: 'Joined', sortable: true, render: (member) => fmt.datetime(member.created) },
51
+ { key: 'modified', label: 'Modified', sortable: true, render: (member) => fmt.relative(member.modified) },
52
+ ];
53
+
54
+ const FILTERS: FilterDef[] = [
55
+ { key: 'user__email', label: 'Email', type: 'text', lookup: 'icontains', placeholder: 'Contains…' },
56
+ { key: 'user__display_name', label: 'Display name', type: 'text', lookup: 'icontains', placeholder: 'Contains…' },
57
+ { key: 'group__name', label: 'Group name', type: 'text', lookup: 'icontains', placeholder: 'Contains…' },
58
+ { key: 'metadata__role', label: 'Role label', type: 'text', lookup: 'icontains', placeholder: 'Contains…' },
59
+ { key: 'is_active', label: 'Status', type: 'boolean', trueLabel: 'Active', falseLabel: 'Inactive' },
60
+ { key: 'created', label: 'Joined', type: 'daterange' },
61
+ ];
62
+
63
+ export function MembersPage({ onNavigateUser, onNavigateGroup }: MemberNavigationCallbacks = {}) {
64
+ const qc = useQueryClient();
65
+ const save = MemberModel.useSave();
66
+ const { can: canSave } = useCan(MEMBER_SAVE_PERMISSIONS);
67
+ const { can: canInvite } = useCan(MEMBER_INVITE_PERMISSIONS);
68
+ const { can: canReadUsers } = useCan(MEMBER_USER_DIRECTORY_PERMISSIONS);
69
+
70
+ const batchActions: BatchAction<MemberRow>[] = canSave ? [
71
+ {
72
+ key: 'activate', label: 'Activate', icon: 'bi-check-circle',
73
+ run: (member) => save.mutateAsync({ id: member.id, changes: { is_active: true } }),
74
+ },
75
+ {
76
+ key: 'deactivate', label: 'Deactivate', icon: 'bi-x-circle', danger: true,
77
+ run: (member) => save.mutateAsync({ id: member.id, changes: { is_active: false } }),
78
+ },
79
+ {
80
+ key: 'role', label: 'Set role label', icon: 'bi-person-gear',
81
+ prepare: async () => {
82
+ const data = await formModal({
83
+ title: 'Set role label',
84
+ submitText: 'Apply to selected',
85
+ fields: [{
86
+ name: 'role', type: 'text', label: 'Role label',
87
+ help: 'Display-only metadata; this does not change authorization.',
88
+ }],
89
+ });
90
+ return data ? String(data.role ?? '').trim() : null;
91
+ },
92
+ run: (member, prepared) => save.mutateAsync({
93
+ id: member.id,
94
+ changes: { metadata: { role: String(prepared ?? '') } },
95
+ }),
96
+ },
97
+ ] : [];
98
+
99
+ const openMember = (member: MemberRow) => {
100
+ void MemberModel.fetchOne(qc, member.id).catch(() => {});
101
+ void modal.detail((close) => (
102
+ <MemberDetail
103
+ id={member.id}
104
+ onClose={() => close(null)}
105
+ onNavigateUser={onNavigateUser}
106
+ onNavigateGroup={onNavigateGroup}
107
+ />
108
+ ));
109
+ };
110
+
111
+ const admit = () => openMemberAdmissionDialog({
112
+ canInvite,
113
+ canCreate: canSave,
114
+ canReadUsers,
115
+ queryClient: qc,
116
+ });
117
+
118
+ return (
119
+ <ModelTable<MemberRow>
120
+ model={MemberModel}
121
+ eyebrow="Identity & Access"
122
+ title="Members"
123
+ columns={COLUMNS}
124
+ filters={FILTERS}
125
+ searchable
126
+ searchPlaceholder="Search name, email, or username…"
127
+ presets={[
128
+ { key: 'all', label: 'All', params: {} },
129
+ { key: 'active', label: 'Active', params: { is_active: 'true' } },
130
+ { key: 'inactive', label: 'Inactive', params: { is_active: 'false' } },
131
+ ]}
132
+ defaultSort="-created"
133
+ selectable={canSave}
134
+ batchActions={batchActions}
135
+ columnChooser
136
+ persistState
137
+ persistKey="admin-members"
138
+ exportFormats={['csv', 'json']}
139
+ onRowClick={openMember}
140
+ addLabel="Add or invite member"
141
+ onAdd={canInvite || (canSave && canReadUsers) ? () => { void admit(); } : undefined}
142
+ />
143
+ );
144
+ }
@@ -0,0 +1,24 @@
1
+ import type { AdminSection } from '../../index';
2
+ import { MembersPage } from './MembersPage';
3
+ import { MEMBER_READ_PERMISSIONS } from './models';
4
+
5
+ export * from './models';
6
+ export * from './member-flows';
7
+ export * from './MemberDetail';
8
+ export * from './GroupMembersPanel';
9
+ export * from './MembersPage';
10
+
11
+ export const MEMBERS_ADMIN_SECTION: AdminSection = {
12
+ id: 'members',
13
+ basePath: '',
14
+ title: 'Members',
15
+ icon: 'bi-person-badge',
16
+ navigationGroup: 'identity-access',
17
+ permissions: MEMBER_READ_PERMISSIONS,
18
+ routes: [{
19
+ path: 'members',
20
+ label: 'Members',
21
+ component: MembersPage,
22
+ permissions: MEMBER_READ_PERMISSIONS,
23
+ }],
24
+ };