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,272 @@
1
+ // UserDeviceLocationDetail — the UserDeviceLocationView port (465 lines,
2
+ // read in full). The source shipped three inline <style> blocks with
3
+ // hardcoded light-only colours (#f0f0f0, #6c757d, #212529, #dc3545) that were
4
+ // unreadable in dark mode; this renders semantic classes over tokens.
5
+ //
6
+ // Two structural corrections:
7
+ // · "Delete Record" is gone — `UserDeviceLocation` declares no CAN_DELETE,
8
+ // so `rest.py` refuses every DELETE.
9
+ // · "View Device" resolves BY DUID, not by id: the embedded `user_device`
10
+ // sub-graph is `basic`, whose field list carries no `id`. The source read
11
+ // `this._ud?.id` and therefore never showed the item.
12
+ import { Badge, DetailView, Eyebrow, FlatRow, fmt, modal, type Tone } from '../../../ui';
13
+ import { useCan } from '../../../client';
14
+ import { EventModel } from '../../incidents/models';
15
+ import { SECURITY_VIEW_PERMS } from '../../security-permissions';
16
+ import { LogModel, LOGS_ADMIN_PERMISSIONS } from '../../monitoring/models';
17
+ import { GEOIP_VIEW_PERMS, countryFlag, threatTone } from '../geoip/models';
18
+ import { showGeoIpDossier } from '../geoip/GeoIpDossier';
19
+ import {
20
+ UserDeviceLocationModel,
21
+ browserLabel, browserVersion, deviceIcon, deviceLabel, osVersion,
22
+ } from './models';
23
+
24
+ const DASH = <span className="dim-italic">—</span>;
25
+
26
+ function RiskRow({ label, icon, value }: { label: string; icon: string; value: boolean | null | undefined }) {
27
+ return (
28
+ <FlatRow label={label}>
29
+ <span className={value ? 'text-bad' : 'dim'}>
30
+ <i className={`bi ${value ? 'bi-check-circle-fill' : 'bi-dash-circle'}`} /> {value ? 'Yes' : 'No'}
31
+ </span>
32
+ <span className="dim udl-risk-icon"><i className={`bi ${icon}`} /></span>
33
+ </FlatRow>
34
+ );
35
+ }
36
+
37
+ function ActivityTable({ headers, children, empty, pending, error }: {
38
+ headers: string[];
39
+ children: React.ReactNode;
40
+ empty: boolean;
41
+ pending: boolean;
42
+ error: Error | null;
43
+ }) {
44
+ if (pending) return <p className="dim">Loading…</p>;
45
+ if (error) return <p className="text-bad">{error.message}</p>;
46
+ if (empty) return <p className="dim-italic">Nothing recorded for this address.</p>;
47
+ return (
48
+ <div className="geoip-local-table">
49
+ <table className="tbl">
50
+ <thead><tr>{headers.map((h) => <th key={h}>{h}</th>)}</tr></thead>
51
+ <tbody>{children}</tbody>
52
+ </table>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function UserDeviceLocationDetail({ id, onClose, onOpenUser, onOpenDeviceByDuid }: {
58
+ id: number;
59
+ onClose: () => void;
60
+ onOpenUser?: (userId: number) => void;
61
+ onOpenDeviceByDuid?: (duid: string) => void;
62
+ }) {
63
+ const { data: row, isPending, error } = UserDeviceLocationModel.useOne(id);
64
+ const canGeoIp = useCan(GEOIP_VIEW_PERMS).can;
65
+ const canLogs = useCan(LOGS_ADMIN_PERMISSIONS).can;
66
+ const canEvents = useCan(SECURITY_VIEW_PERMS).can;
67
+
68
+ const ip = row?.ip_address ?? '';
69
+ const events = EventModel.useList({ source_ip: ip, size: 10, sort: '-created' }, { enabled: Boolean(ip) && canEvents });
70
+ const logs = LogModel.useList({ ip, size: 10, sort: '-created' }, { enabled: Boolean(ip) && canLogs });
71
+
72
+ if (isPending) return <div className="modal-pad dim">Loading location record…</div>;
73
+ if (!row || error) return <div className="modal-pad text-bad">{error?.message ?? 'Location record not found'}</div>;
74
+
75
+ const geo = row.geolocation;
76
+ const info = row.user_device?.device_info ?? null;
77
+ const where = [geo?.city, geo?.region, geo?.country_name].filter(Boolean).join(', ') || 'Unknown location';
78
+ const hasCoords = geo?.latitude != null && geo.longitude != null;
79
+ const flag = countryFlag(geo?.country_code);
80
+
81
+ return (
82
+ <DetailView
83
+ icon={deviceIcon(info)}
84
+ title={where}
85
+ subtitle={`${browserLabel(info)} on ${deviceLabel(info)} · ${row.ip_address}`}
86
+ chips={[
87
+ ...(geo?.threat_level ? [{ text: `Threat: ${geo.threat_level}`, tone: threatTone(geo.threat_level) as Tone }] : []),
88
+ ...(geo?.is_vpn ? [{ icon: 'bi-shield-shaded', text: 'VPN', tone: 'warning' as const }] : []),
89
+ ...(geo?.is_tor ? [{ icon: 'bi-shield-lock', text: 'Tor', tone: 'danger' as const }] : []),
90
+ ...(geo?.is_proxy ? [{ icon: 'bi-diagram-3', text: 'Proxy', tone: 'warning' as const }] : []),
91
+ ...(geo?.is_datacenter ? [{ icon: 'bi-hdd-stack', text: 'Datacenter', tone: 'warning' as const }] : []),
92
+ ...(geo?.is_cloud ? [{ icon: 'bi-cloud-fill', text: 'Cloud', tone: 'info' as const }] : []),
93
+ ]}
94
+ sections={[
95
+ {
96
+ key: 'location', label: 'Location', icon: 'bi-geo-alt', render: () => (
97
+ <>
98
+ <Eyebrow>Geography</Eyebrow>
99
+ <FlatRow label="City">{geo?.city || DASH}</FlatRow>
100
+ <FlatRow label="Region">
101
+ {geo?.region || DASH}
102
+ {geo?.region_code && <code className="dim geoip-cc">{geo.region_code}</code>}
103
+ </FlatRow>
104
+ <FlatRow label="Country">
105
+ {geo?.country_name || geo?.country_code
106
+ ? <>{flag && <span className="geoip-flag">{flag}</span>}{geo?.country_name || geo?.country_code}</>
107
+ : DASH}
108
+ </FlatRow>
109
+ <FlatRow label="Postal code">{geo?.postal_code || DASH}</FlatRow>
110
+ <FlatRow label="Timezone">{geo?.timezone || DASH}</FlatRow>
111
+ <FlatRow label="Coordinates">{hasCoords ? <code>{geo!.latitude}, {geo!.longitude}</code> : DASH}</FlatRow>
112
+
113
+ <Eyebrow>Network</Eyebrow>
114
+ <FlatRow label="IP address">
115
+ {canGeoIp && geo
116
+ ? <button className="btn-link" onClick={() => showGeoIpDossier(geo.id)}><code>{row.ip_address}</code></button>
117
+ : <code>{row.ip_address}</code>}
118
+ </FlatRow>
119
+ <FlatRow label="ISP">{geo?.isp || DASH}</FlatRow>
120
+ <FlatRow label="ASN">
121
+ {geo?.asn ? <code>{geo.asn}</code> : DASH}
122
+ {geo?.asn_org && <span className="dim"> · {geo.asn_org}</span>}
123
+ </FlatRow>
124
+ <FlatRow label="Connection">{geo?.connection_type || DASH}</FlatRow>
125
+
126
+ <Eyebrow>Timestamps</Eyebrow>
127
+ <FlatRow label="First seen"><code>{fmt.datetime(row.first_seen)}</code></FlatRow>
128
+ <FlatRow label="Last seen"><code>{fmt.datetime(row.last_seen)}</code> <span className="dim">· {fmt.relative(row.last_seen)}</span></FlatRow>
129
+ </>
130
+ ),
131
+ },
132
+ {
133
+ key: 'device', label: 'Device', icon: 'bi-laptop', render: () => (
134
+ <>
135
+ <Eyebrow>Browser</Eyebrow>
136
+ <FlatRow label="Name">{info?.user_agent.family || DASH}</FlatRow>
137
+ <FlatRow label="Version">{browserVersion(info) || DASH}</FlatRow>
138
+
139
+ <Eyebrow>Operating system</Eyebrow>
140
+ <FlatRow label="Name">{info?.os.family || DASH}</FlatRow>
141
+ <FlatRow label="Version">{osVersion(info) || DASH}</FlatRow>
142
+
143
+ <Eyebrow>Hardware</Eyebrow>
144
+ <FlatRow label="Brand">{info?.device.brand || DASH}</FlatRow>
145
+ <FlatRow label="Family">{info?.device.family || DASH}</FlatRow>
146
+ <FlatRow label="Model">{info?.device.model || DASH}</FlatRow>
147
+
148
+ <Eyebrow>Identification</Eyebrow>
149
+ {/* The embedded `basic` graph carries no `id` —
150
+ cross-links resolve by DUID. */}
151
+ <FlatRow label="Device ID">{row.user_device ? <code>{row.user_device.duid}</code> : DASH}</FlatRow>
152
+ <FlatRow label="MUID">{row.user_device?.muid ? <code>{row.user_device.muid}</code> : DASH}</FlatRow>
153
+ <FlatRow label="Owner">
154
+ {row.user
155
+ ? (onOpenUser
156
+ ? <button className="btn-link" onClick={() => onOpenUser(row.user!.id)}>{row.user.display_name || row.user.username}</button>
157
+ : (row.user.display_name || row.user.username))
158
+ : DASH}
159
+ </FlatRow>
160
+ {info?.string && (
161
+ <>
162
+ <Eyebrow>User agent</Eyebrow>
163
+ <pre className="ud-ua-block">{info.string}</pre>
164
+ </>
165
+ )}
166
+ </>
167
+ ),
168
+ },
169
+ {
170
+ key: 'risk', label: 'Risk', icon: 'bi-shield-exclamation', render: () => (
171
+ <>
172
+ <Eyebrow>Threat assessment</Eyebrow>
173
+ <FlatRow label="Threat level">
174
+ <Badge tone={threatTone(geo?.threat_level) as Tone}>{geo?.threat_level || 'unknown'}</Badge>
175
+ </FlatRow>
176
+ <FlatRow label="Risk score">{geo?.risk_score != null ? <><strong>{geo.risk_score}</strong> / 100</> : DASH}</FlatRow>
177
+
178
+ <Eyebrow>Detection flags</Eyebrow>
179
+ <RiskRow label="VPN" icon="bi-shield-shaded" value={geo?.is_vpn} />
180
+ <RiskRow label="Tor exit node" icon="bi-shield-lock" value={geo?.is_tor} />
181
+ <RiskRow label="Proxy" icon="bi-diagram-3" value={geo?.is_proxy} />
182
+ <RiskRow label="Cloud provider" icon="bi-cloud" value={geo?.is_cloud} />
183
+ <RiskRow label="Datacenter" icon="bi-hdd-stack" value={geo?.is_datacenter} />
184
+ <RiskRow label="Mobile" icon="bi-phone" value={geo?.is_mobile} />
185
+
186
+ <Eyebrow>Reputation</Eyebrow>
187
+ <RiskRow label="Known attacker" icon="bi-exclamation-triangle" value={geo?.is_known_attacker} />
188
+ <RiskRow label="Known abuser" icon="bi-flag" value={geo?.is_known_abuser} />
189
+ <RiskRow label="Threat" icon="bi-shield-exclamation" value={geo?.is_threat} />
190
+ <RiskRow label="Suspicious" icon="bi-question-circle" value={geo?.is_suspicious} />
191
+ </>
192
+ ),
193
+ },
194
+ { divider: 'Activity' },
195
+ {
196
+ key: 'events', label: 'Events', icon: 'bi-calendar-event',
197
+ permissions: SECURITY_VIEW_PERMS,
198
+ render: () => (
199
+ <>
200
+ <Eyebrow>Security events from this IP</Eyebrow>
201
+ <ActivityTable
202
+ headers={['Date', 'Category', 'Details']}
203
+ empty={!(events.data?.rows ?? []).length}
204
+ pending={events.isPending}
205
+ error={events.error}
206
+ >
207
+ {(events.data?.rows ?? []).map((event) => (
208
+ <tr key={event.id}>
209
+ <td>{fmt.datetime(event.created)}</td>
210
+ <td><code>{event.category}</code></td>
211
+ <td>{fmt.truncate(event.title || event.details || '—', 70)}</td>
212
+ </tr>
213
+ ))}
214
+ </ActivityTable>
215
+ </>
216
+ ),
217
+ },
218
+ {
219
+ key: 'logs', label: 'Logs', icon: 'bi-journal-text',
220
+ permissions: LOGS_ADMIN_PERMISSIONS,
221
+ render: () => (
222
+ <>
223
+ <Eyebrow>Request logs from this IP</Eyebrow>
224
+ <ActivityTable
225
+ headers={['Timestamp', 'Level', 'Kind', 'Log']}
226
+ empty={!(logs.data?.rows ?? []).length}
227
+ pending={logs.isPending}
228
+ error={logs.error}
229
+ >
230
+ {(logs.data?.rows ?? []).map((log) => (
231
+ <tr key={log.id}>
232
+ <td>{fmt.datetime(log.created)}</td>
233
+ <td>{log.level}</td>
234
+ <td>{log.kind || '—'}</td>
235
+ <td>{fmt.truncate(log.log || log.path || '—', 70)}</td>
236
+ </tr>
237
+ ))}
238
+ </ActivityTable>
239
+ </>
240
+ ),
241
+ },
242
+ ]}
243
+ initialSection="location"
244
+ contextMenu={[
245
+ ...(row.user && onOpenUser
246
+ ? [{ label: 'Open user', icon: 'bi-person', onSelect: () => onOpenUser(row.user!.id) }]
247
+ : []),
248
+ ...(row.user_device && onOpenDeviceByDuid
249
+ ? [{ label: 'Open device', icon: 'bi-laptop', onSelect: () => onOpenDeviceByDuid(row.user_device!.duid) }]
250
+ : []),
251
+ ...(geo && canGeoIp
252
+ ? [{ label: 'Open GeoIP dossier', icon: 'bi-globe2', onSelect: () => showGeoIpDossier(geo.id) }]
253
+ : []),
254
+ ...(hasCoords
255
+ ? [{
256
+ label: 'Open coordinates in Google Maps', icon: 'bi-box-arrow-up-right',
257
+ onSelect: () => { window.open(`https://www.google.com/maps/search/?api=1&query=${geo!.latitude},${geo!.longitude}`, '_blank', 'noopener,noreferrer'); },
258
+ }]
259
+ : []),
260
+ // NO Delete — UserDeviceLocation declares no CAN_DELETE.
261
+ ]}
262
+ onClose={onClose}
263
+ />
264
+ );
265
+ }
266
+
267
+ export function showUserDeviceLocationDetail(id: number, opts: {
268
+ onOpenUser?: (userId: number) => void;
269
+ onOpenDeviceByDuid?: (duid: string) => void;
270
+ } = {}): void {
271
+ void modal.detail((close) => <UserDeviceLocationDetail id={id} onClose={() => close(null)} {...opts} />);
272
+ }
@@ -0,0 +1,84 @@
1
+ // User Devices — the UserDeviceTablePage port.
2
+ //
3
+ // The source's search placeholder promised "Search user, IP, or device ID".
4
+ // `UserDevice` declares NO `SEARCH_FIELDS` at all, so only the DUID and
5
+ // user-agent text the backend's fallback can reach are searchable — user and
6
+ // IP are not, and the placeholder must not claim otherwise. They are offered
7
+ // as explicit filters instead, which is the honest affordance.
8
+ //
9
+ // The user filter is a NUMBER (the FK id), not a select: the directory is far
10
+ // larger than one page and `FilterDef` has no collection type, so a select
11
+ // would silently offer only the first page of users. Opening a device's
12
+ // dossier gives the named cross-link to its owner.
13
+ import { ModelTable, fmt, type Column, type FilterDef, type Preset } from '../../../ui';
14
+ import { showUserDeviceDetail } from './UserDeviceDetail';
15
+ import {
16
+ UserDeviceModel, browserLabel, deviceIcon, osLabel, presenceOf,
17
+ type UserDeviceRow,
18
+ } from './models';
19
+
20
+ const COLUMNS: Column<UserDeviceRow>[] = [
21
+ { key: 'id', label: 'ID', sortable: true, align: 'end', render: (row) => <span className="dim">{row.id}</span> },
22
+ {
23
+ key: 'duid', label: 'Device ID', sortable: true, hideable: false,
24
+ render: (row) => (
25
+ <span className="ud-id">
26
+ <i className={`bi ${deviceIcon(row.device_info)}`} />
27
+ <code title={row.duid}>{fmt.truncateMiddle(row.duid, 16)}</code>
28
+ </span>
29
+ ),
30
+ },
31
+ {
32
+ key: 'user', label: 'User', sortable: true,
33
+ render: (row) => row.user?.display_name || row.user?.username || '—',
34
+ },
35
+ { key: 'device_info__user_agent__family', label: 'Browser', render: (row) => browserLabel(row.device_info) },
36
+ { key: 'device_info__os__family', label: 'OS', render: (row) => osLabel(row.device_info) },
37
+ { key: 'last_ip', label: 'Last IP', sortable: true, render: (row) => <code>{row.last_ip || '—'}</code> },
38
+ { key: 'first_seen', label: 'First seen', sortable: true, render: (row) => fmt.datetime(row.first_seen) },
39
+ {
40
+ key: 'last_seen', label: 'Last seen', sortable: true,
41
+ render: (row) => (
42
+ <span className={`ud-presence ud-presence-${presenceOf(row.last_seen)}`}>
43
+ <span className="ud-dot" />{fmt.relative(row.last_seen)}
44
+ </span>
45
+ ),
46
+ },
47
+ ];
48
+
49
+ const FILTERS: FilterDef[] = [
50
+ { key: 'user', label: 'User ID', type: 'number', lookup: 'exact', placeholder: 'e.g. 1' },
51
+ { key: 'muid', label: 'MUID', type: 'text' },
52
+ { key: 'duid', label: 'Device ID', type: 'text' },
53
+ { key: 'last_ip', label: 'Last IP', type: 'text' },
54
+ { key: 'last_seen', label: 'Last seen', type: 'daterange' },
55
+ ];
56
+
57
+ /** `dayRangeFilter: {field: 'last_seen', value: '30d'}`, as the dr_* triple. */
58
+ function presets(): Preset[] {
59
+ const day = (offsetDays: number) => new Date(Date.now() - offsetDays * 86400000).toISOString().slice(0, 10);
60
+ return [
61
+ { key: 'all', label: 'All', params: {} },
62
+ { key: 'recent', label: 'Last 30 days', params: { dr_field: 'last_seen', dr_start: day(30), dr_end: day(0) } },
63
+ ];
64
+ }
65
+
66
+ export function UserDevicesPage() {
67
+ return (
68
+ <ModelTable<UserDeviceRow>
69
+ model={UserDeviceModel}
70
+ eyebrow="Security · Devices & Logins"
71
+ title="User Devices"
72
+ searchPlaceholder="Search device ID or user agent"
73
+ columns={COLUMNS}
74
+ filters={FILTERS}
75
+ presets={presets()}
76
+ defaultSort="-last_seen"
77
+ columnChooser
78
+ persistState
79
+ persistKey="admin:devices:user"
80
+ exportFormats={['csv', 'json']}
81
+ onRowClick={(row) => showUserDeviceDetail(row.id)}
82
+ />
83
+ );
84
+ }
@@ -0,0 +1,40 @@
1
+ // Devices & Logins — the fleet-wide, groupless device and login-location
2
+ // triage surfaces. Sibling of Bouncer under Security so the PRE-auth
3
+ // (`BouncerDevice.muid`) and POST-auth (`UserDevice.muid`) views of the same
4
+ // physical browser sit next to each other.
5
+ //
6
+ // This module OWNS `/api/user/device`, `/api/user/device/location` and
7
+ // `/api/account/logins`; `admin/identity/users` re-exports those models
8
+ // rather than defining them a second time.
9
+ import type { AdminSection } from '../../index';
10
+ import { LoginLocationsPage } from './LoginLocationsPage';
11
+ import { UserDevicesPage } from './UserDevicesPage';
12
+ import { LOGIN_EVENT_VIEW_PERMS, USER_DEVICE_VIEW_PERMS } from './models';
13
+
14
+ export * from './models';
15
+ export * from './LoginLocationMap';
16
+ export * from './LoginLocationsPage';
17
+ export * from './UserDevicesPage';
18
+ export * from './UserDeviceDetail';
19
+ export * from './UserDeviceLocationDetail';
20
+ export * from './LoginEventDetail';
21
+
22
+ /** Section gate = the union of its routes' clauses (each route re-applies its own). */
23
+ export const DEVICE_INTEL_ADMIN_SECTION: AdminSection = {
24
+ id: 'device-intel',
25
+ basePath: 'security/devices',
26
+ title: 'Devices & Logins',
27
+ icon: 'bi-laptop',
28
+ navigationGroup: 'security',
29
+ permissions: [...new Set([...USER_DEVICE_VIEW_PERMS, ...LOGIN_EVENT_VIEW_PERMS])],
30
+ routes: [
31
+ {
32
+ path: 'user-devices', label: 'User Devices',
33
+ component: UserDevicesPage, permissions: USER_DEVICE_VIEW_PERMS,
34
+ },
35
+ {
36
+ path: 'login-locations', label: 'Login Locations',
37
+ component: LoginLocationsPage, permissions: LOGIN_EVENT_VIEW_PERMS,
38
+ },
39
+ ],
40
+ };