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,304 @@
1
+ import { useRef, useState } from 'react';
2
+ import { useQueryClient } from '@tanstack/react-query';
3
+ import { useCan } from '../../client';
4
+ import {
5
+ ArmedButton, Badge, CollectionSelect, FlatRow, ModelTable, fmt, modal, toast,
6
+ type Column, type FilterDef, type Tone,
7
+ } from '../../ui';
8
+ import {
9
+ DNS_GROUP_CHOICE_ENDPOINT, invalidateDnsCredentials, linkDnsCredential,
10
+ rotateDnsCredential, useDnsCapabilities,
11
+ } from './api';
12
+ import { linkableProviders, providerLabel } from './data';
13
+ import {
14
+ DNS_MANAGE_PERMISSIONS, DNS_VIEW_PERMISSIONS, DnsCredentialModel,
15
+ type DnsCapabilities, type DnsCredentialRow, type DnsGroupChoice,
16
+ } from './models';
17
+
18
+ function groupId(group: DnsCredentialRow['group']): number | null {
19
+ if (group == null) return null;
20
+ return typeof group === 'number' ? group : group.id;
21
+ }
22
+
23
+ function groupLabel(group: DnsCredentialRow['group']): string {
24
+ if (group == null) return 'Platform';
25
+ return typeof group === 'number' ? `Group #${group}` : group.name;
26
+ }
27
+
28
+ function verificationTone(row: DnsCredentialRow): Tone {
29
+ return row.verified ? 'success' : row.last_error ? 'danger' : 'warning';
30
+ }
31
+
32
+ function CredentialSecretForm({ row, caps, close }: {
33
+ row?: DnsCredentialRow;
34
+ caps: DnsCapabilities;
35
+ close: (saved: DnsCredentialRow | null) => void;
36
+ }) {
37
+ const queryClient = useQueryClient();
38
+ const providers = linkableProviders(caps);
39
+ const keyRef = useRef<HTMLInputElement>(null);
40
+ const secretRef = useRef<HTMLInputElement>(null);
41
+ const [name, setName] = useState(row?.name ?? '');
42
+ const [provider, setProvider] = useState(row?.provider ?? providers[0]?.name ?? '');
43
+ const [group, setGroup] = useState<DnsGroupChoice | null>(null);
44
+ const [busy, setBusy] = useState(false);
45
+ const [error, setError] = useState('');
46
+ const fixedGroup = row ? groupLabel(row.group) : null;
47
+
48
+ const submit = async (event: React.FormEvent) => {
49
+ event.preventDefault();
50
+ const apiKey = keyRef.current?.value ?? '';
51
+ const apiSecret = secretRef.current?.value ?? '';
52
+ if (!apiKey || !apiSecret) { setError('Enter both the API key and API secret'); return; }
53
+ if (!row && !group) { setError('Choose a group'); return; }
54
+ if (!provider) { setError('Choose a provider'); return; }
55
+ setBusy(true);
56
+ setError('');
57
+ try {
58
+ const saved = row
59
+ ? await rotateDnsCredential(row, { name, api_key: apiKey, api_secret: apiSecret })
60
+ : await linkDnsCredential({
61
+ group: group!.id, provider, name,
62
+ api_key: apiKey, api_secret: apiSecret,
63
+ });
64
+ queryClient.setQueryData(DnsCredentialModel.keys.one(saved.id), saved);
65
+ toast.success(row ? 'Credential rotated and verified' : 'Credential linked and verified');
66
+ close(saved);
67
+ } catch (reason) {
68
+ setError(reason instanceof Error ? reason.message : 'The provider rejected those credentials');
69
+ } finally {
70
+ if (keyRef.current) keyRef.current.value = '';
71
+ if (secretRef.current) secretRef.current.value = '';
72
+ await invalidateDnsCredentials(queryClient);
73
+ setBusy(false);
74
+ }
75
+ };
76
+
77
+ return (
78
+ <form className="modal-pad" onSubmit={submit} noValidate>
79
+ <h2 className="modal-title">{row ? `Rotate ${row.name}` : 'Link provider credential'}</h2>
80
+ <p className="dim">
81
+ The provider verifies this pair before it is stored. Values are never returned after submission.
82
+ </p>
83
+ {error && <div className="form-alert">{error}</div>}
84
+ <div className="form-grid">
85
+ <div className="col-12">
86
+ <label className="field">
87
+ <span className="field-label">Label</span>
88
+ <input className="input" value={name} placeholder="Acme GoDaddy" onChange={(event) => setName(event.target.value)} />
89
+ </label>
90
+ </div>
91
+ <div className="col-12">
92
+ {row ? (
93
+ <div className="field">
94
+ <span className="field-label">Group</span>
95
+ <div className="input" aria-readonly="true">{fixedGroup}</div>
96
+ <span className="field-help">Rotation keeps the existing credential scope.</span>
97
+ </div>
98
+ ) : (
99
+ <CollectionSelect<DnsGroupChoice>
100
+ endpoint={DNS_GROUP_CHOICE_ENDPOINT}
101
+ value={group}
102
+ onChange={(_id, selected) => { setGroup(selected ?? null); setError(''); }}
103
+ label="Group"
104
+ placeholder="Search eligible groups…"
105
+ help="Only active groups authorized for DNS credential assignment appear."
106
+ required
107
+ maxItems={25}
108
+ defaultParams={{ start: 0 }}
109
+ />
110
+ )}
111
+ </div>
112
+ <div className="col-12">
113
+ <label className="field">
114
+ <span className="field-label">Provider <em>*</em></span>
115
+ {row ? (
116
+ <div className="input" aria-readonly="true">{providerLabel(provider)}</div>
117
+ ) : (
118
+ <select className="input" value={provider} onChange={(event) => setProvider(event.target.value)}>
119
+ {providers.map((entry) => <option key={entry.name} value={entry.name}>{providerLabel(entry.name)}</option>)}
120
+ </select>
121
+ )}
122
+ </label>
123
+ </div>
124
+ <div className="col-12">
125
+ <label className="field">
126
+ <span className="field-label">API key <em>*</em></span>
127
+ <input ref={keyRef} className="input" type="password" autoComplete="new-password" spellCheck={false} />
128
+ </label>
129
+ </div>
130
+ <div className="col-12">
131
+ <label className="field">
132
+ <span className="field-label">API secret <em>*</em></span>
133
+ <input ref={secretRef} className="input" type="password" autoComplete="new-password" spellCheck={false} />
134
+ </label>
135
+ </div>
136
+ </div>
137
+ <div className="modal-actions">
138
+ <button type="button" className="btn" disabled={busy} onClick={() => close(null)}>Cancel</button>
139
+ <button type="submit" className="btn btn-primary" disabled={busy || !provider || (!row && !group)}>
140
+ {busy ? 'Verifying…' : row ? 'Rotate and verify' : 'Link and verify'}
141
+ </button>
142
+ </div>
143
+ </form>
144
+ );
145
+ }
146
+
147
+ async function showCredentialEditor(caps: DnsCapabilities, row?: DnsCredentialRow): Promise<DnsCredentialRow | null> {
148
+ return modal.open<DnsCredentialRow | null>((close) => (
149
+ <CredentialSecretForm caps={caps} row={row} close={close} />
150
+ ), { size: 'md' });
151
+ }
152
+
153
+ function CredentialDetail({ id, caps, close }: { id: number; caps: DnsCapabilities; close: () => void }) {
154
+ const { data: row, isPending, error } = DnsCredentialModel.useOne(id);
155
+ const { can: canManage } = useCan(DNS_MANAGE_PERMISSIONS);
156
+ const save = DnsCredentialModel.useSave();
157
+ const destroy = DnsCredentialModel.useDelete();
158
+ if (isPending) return <div className="modal-pad dim">Loading credential…</div>;
159
+ if (!row || error) return <div className="modal-pad text-bad">{error?.message ?? 'Credential not found'}</div>;
160
+
161
+ const toggle = async () => {
162
+ try {
163
+ await save.mutateAsync({ id: row.id, changes: { is_active: !row.is_active } });
164
+ toast.success(row.is_active ? 'Credential retired' : 'Credential activated');
165
+ } catch (reason) {
166
+ toast.error(reason instanceof Error ? reason.message : 'Credential could not be updated');
167
+ }
168
+ };
169
+
170
+ const remove = async () => {
171
+ try {
172
+ await destroy.mutateAsync({ id: row.id });
173
+ toast.success('Credential deleted');
174
+ close();
175
+ } catch (reason) {
176
+ toast.error(reason instanceof Error ? reason.message : 'Credential could not be deleted');
177
+ }
178
+ };
179
+
180
+ return (
181
+ <div className="modal-pad">
182
+ <div className="eyebrow">Provider credential</div>
183
+ <h2 className="modal-title">{row.name || `Credential #${row.id}`}</h2>
184
+ <div className="chip-row" style={{ marginBottom: 14 }}>
185
+ <Badge tone={verificationTone(row)}>{row.verified ? 'Verified' : 'Unverified'}</Badge>
186
+ <Badge tone={row.is_active ? 'success' : 'muted'}>{row.is_active ? 'Active' : 'Inactive'}</Badge>
187
+ <Badge tone="warning">{providerLabel(row.provider)}</Badge>
188
+ </div>
189
+ <FlatRow label="Group">{groupLabel(row.group)}</FlatRow>
190
+ <FlatRow label="API key"><code>{row.api_key_masked || '—'}</code></FlatRow>
191
+ <FlatRow label="API secret"><code>{row.api_secret_masked || '—'}</code></FlatRow>
192
+ <FlatRow label="Domains at verification">{row.domain_count}</FlatRow>
193
+ <FlatRow label="Verified at">{row.verified_at ? fmt.datetime(row.verified_at) : 'Never'}</FlatRow>
194
+ <FlatRow label="Created">{fmt.datetime(row.created)}</FlatRow>
195
+ {row.last_error && <div className="form-alert" style={{ marginTop: 14 }}>{row.last_error}</div>}
196
+ <p className="dim">
197
+ The linked-domain count is a health signal only. Provider account domains are never enumerated here.
198
+ </p>
199
+ {canManage && (
200
+ <div className="modal-actions">
201
+ <button type="button" className="btn" onClick={() => void showCredentialEditor(caps, row)}>
202
+ <i className="bi bi-arrow-repeat" /> Rotate
203
+ </button>
204
+ <button type="button" className="btn" disabled={save.isPending} onClick={() => void toggle()}>
205
+ <i className={`bi ${row.is_active ? 'bi-pause-circle' : 'bi-play-circle'}`} /> {row.is_active ? 'Retire' : 'Activate'}
206
+ </button>
207
+ <ArmedButton
208
+ label="Delete"
209
+ armedLabel="Click again — linked domains lose provider-backed DNS"
210
+ disabled={destroy.isPending}
211
+ onConfirm={remove}
212
+ />
213
+ <button type="button" className="btn" onClick={close}>Close</button>
214
+ </div>
215
+ )}
216
+ {!canManage && <div className="modal-actions"><button type="button" className="btn" onClick={close}>Close</button></div>}
217
+ </div>
218
+ );
219
+ }
220
+
221
+ const COLUMNS: Column<DnsCredentialRow>[] = [
222
+ {
223
+ key: 'name', label: 'Label', sortable: true, hideable: false, render: (row) => (
224
+ <div className="cell-user">
225
+ <span className="cell-avatar"><i className="bi bi-key" /></span>
226
+ <span><span className="cell-name">{row.name}</span><span className="cell-sub">#{row.id}</span></span>
227
+ </div>
228
+ ),
229
+ },
230
+ { key: 'group', label: 'Group', render: (row) => groupLabel(row.group) },
231
+ { key: 'provider', label: 'Provider', sortable: true, render: (row) => <Badge tone="warning">{providerLabel(row.provider)}</Badge> },
232
+ { key: 'verified', label: 'Verified', sortable: true, align: 'center', render: (row) => <Badge tone={verificationTone(row)}>{row.verified ? 'Verified' : 'Unverified'}</Badge> },
233
+ { key: 'is_active', label: 'Active', sortable: true, align: 'center', render: (row) => <Badge tone={row.is_active ? 'success' : 'muted'}>{row.is_active ? 'Active' : 'Inactive'}</Badge> },
234
+ { key: 'domain_count', label: 'Domains', sortable: true, align: 'end' },
235
+ { key: 'last_error', label: 'Last error', render: (row) => row.last_error ? <span className="text-bad" title={row.last_error}><i className="bi bi-exclamation-triangle" /> Error</span> : '—' },
236
+ { key: 'created', label: 'Created', sortable: true, render: (row) => fmt.date(row.created) },
237
+ ];
238
+
239
+ function CredentialsTable({ caps }: { caps: DnsCapabilities }) {
240
+ const queryClient = useQueryClient();
241
+ const { can: canManage } = useCan(DNS_MANAGE_PERMISSIONS);
242
+ const providers = caps.providers.map((entry) => ({ value: entry.name, label: providerLabel(entry.name) }));
243
+ const filters: FilterDef[] = [
244
+ { key: 'group', label: 'Group ID', type: 'number', lookup: 'exact' },
245
+ { key: 'provider', label: 'Provider', type: 'select', options: providers },
246
+ { key: 'verified', label: 'Verification', type: 'boolean', trueLabel: 'Verified', falseLabel: 'Unverified' },
247
+ { key: 'is_active', label: 'Status', type: 'boolean', trueLabel: 'Active', falseLabel: 'Inactive' },
248
+ ];
249
+ const openDetail = (row: DnsCredentialRow) => {
250
+ void DnsCredentialModel.fetchOne(queryClient, row.id).catch(() => undefined);
251
+ void modal.open((done) => (
252
+ <CredentialDetail id={row.id} caps={caps} close={() => done(null)} />
253
+ ), { size: 'md' });
254
+ };
255
+ const canLink = canManage && linkableProviders(caps).length > 0;
256
+ return (
257
+ <>
258
+ {canManage && !canLink && (
259
+ <div className="panel panel-pad form-alert">
260
+ This deployment reports no provider that accepts linked credentials.
261
+ </div>
262
+ )}
263
+ <ModelTable<DnsCredentialRow>
264
+ model={DnsCredentialModel}
265
+ eyebrow="Infrastructure"
266
+ title="Provider Credentials"
267
+ searchPlaceholder="Search label or provider…"
268
+ columns={COLUMNS}
269
+ filters={filters}
270
+ presets={[
271
+ { key: 'all', label: 'All', params: {} },
272
+ { key: 'verified', label: 'Verified', params: { verified: 'true' } },
273
+ { key: 'attention', label: 'Needs attention', params: { verified: 'false' } },
274
+ { key: 'active', label: 'Active', params: { is_active: 'true' } },
275
+ ]}
276
+ defaultSort="name"
277
+ columnChooser
278
+ persistState
279
+ persistKey="admin-dns-credentials"
280
+ onRowClick={openDetail}
281
+ {...(canLink ? { addLabel: 'Link Credential', onAdd: () => void showCredentialEditor(caps) } : {})}
282
+ />
283
+ </>
284
+ );
285
+ }
286
+
287
+ export function ProviderCredentialsPage() {
288
+ const { can: canView } = useCan(DNS_VIEW_PERMISSIONS);
289
+ const caps = useDnsCapabilities(null, { enabled: canView });
290
+ if (!canView) return <div className="panel panel-pad"><h2>DNS administration unavailable</h2><p className="dim">A global DNS authority is required.</p></div>;
291
+ if (caps.isPending) return <div className="panel panel-pad dim">Loading DNS capabilities…</div>;
292
+ if (caps.error || !caps.data) {
293
+ return (
294
+ <div className="panel panel-pad">
295
+ <h2>DNS administration unavailable</h2>
296
+ <p className="text-bad">{caps.error?.message ?? 'The server did not return a usable DNS capability contract.'}</p>
297
+ <button type="button" className="btn" onClick={() => void caps.refetch()}><i className="bi bi-arrow-clockwise" /> Retry</button>
298
+ </div>
299
+ );
300
+ }
301
+ return <CredentialsTable caps={caps.data} />;
302
+ }
303
+
304
+ export const dnsCredentialGroupId = groupId;
@@ -0,0 +1,42 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useMe } from '../../client';
3
+ import { Badge, CollectionSelect, modal } from '../../ui';
4
+ import { DNS_GROUP_CHOICE_ENDPOINT, clearRegistrantContact, fetchRegistrantContact, saveRegistrantContact } from './api';
5
+ import type { DnsGroupChoice, RegistrantContact, RegistrantContactResponse } from './models';
6
+ import { CONTACT_STRING_FIELDS, contactDraft, contactPayload, validateContactDraft } from './registrant-data';
7
+ import { redactRegistrarError } from './purchase-data';
8
+
9
+ type Scope = { kind: 'group'; group: DnsGroupChoice } | { kind: 'house' };
10
+ const LABELS: Record<(typeof CONTACT_STRING_FIELDS)[number], string> = { FirstName: 'First name', LastName: 'Last name', ContactType: 'Contact type', OrganizationName: 'Organization', AddressLine1: 'Address line 1', AddressLine2: 'Address line 2', City: 'City', State: 'State / province', CountryCode: 'Country code', ZipCode: 'Postal code', PhoneNumber: 'Phone number', Email: 'Email', Fax: 'Fax' };
11
+
12
+ export function RegistrantContactPage() {
13
+ const { data: me } = useMe(); const [scope, setScope] = useState<Scope | null>(null); const [response, setResponse] = useState<RegistrantContactResponse | null>(null); const [draft, setDraft] = useState<RegistrantContact>({}); const [busy, setBusy] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const generation = useRef(0); const mounted = useRef(true);
14
+ useEffect(() => () => { mounted.current = false; generation.current += 1; }, []);
15
+ const choose = async (next: Scope | null) => {
16
+ const current = ++generation.current; setScope(next); setResponse(null); setDraft({}); setError('');
17
+ if (!next) return; setLoading(true); const group = next.kind === 'group' ? next.group.id : null;
18
+ try { const value = await fetchRegistrantContact(group); if (mounted.current && generation.current === current) { setResponse(value); setDraft(contactDraft(value)); } }
19
+ catch (reason) { if (mounted.current && generation.current === current) setError(redactRegistrarError(reason)); }
20
+ finally { if (mounted.current && generation.current === current) setLoading(false); }
21
+ };
22
+ const save = async () => {
23
+ if (!scope || !response || busy) return; const problems = validateContactDraft(draft); if (problems.length) { setError(problems.join('; ')); return; }
24
+ const current = generation.current; const group = scope.kind === 'group' ? scope.group.id : null; setBusy(true); setError('');
25
+ try { const saved = await saveRegistrantContact(contactPayload(draft, response, { group }), group); if (mounted.current && generation.current === current) { setResponse(saved); setDraft(contactDraft(saved)); } }
26
+ catch (reason) { if (mounted.current && generation.current === current) setError(redactRegistrarError(reason)); }
27
+ finally { if (mounted.current && generation.current === current) setBusy(false); }
28
+ };
29
+ const clear = async () => {
30
+ if (!scope || !response || busy || response.source !== 'database') return;
31
+ if (!await modal.confirm({ title: 'Clear this scope’s contact?', message: 'This reveals any inherited or deployment-file contact but never its values.', confirmText: 'Clear contact', danger: true })) return;
32
+ const current = generation.current; const group = scope.kind === 'group' ? scope.group.id : null; setBusy(true); setError('');
33
+ try { const saved = await clearRegistrantContact(group); if (mounted.current && generation.current === current) { setResponse(saved); setDraft(contactDraft(saved)); } }
34
+ catch (reason) { if (mounted.current && generation.current === current) setError(redactRegistrarError(reason)); }
35
+ finally { if (mounted.current && generation.current === current) setBusy(false); }
36
+ };
37
+ return <div className="dns-registrant-page"><div className="panel panel-pad dns-registrant-head"><div><div className="eyebrow">Infrastructure · DNS</div><h1>Registrant Contact</h1><p className="dim">ICANN contact PII is fetched imperatively into this mounted page only. It is cleared on every scope change and never enters Query cache.</p></div><div className="dns-scope-picker"><CollectionSelect<DnsGroupChoice> endpoint={DNS_GROUP_CHOICE_ENDPOINT} value={scope?.kind === 'group' ? scope.group : null} onChange={(_id, row) => void choose(row ? { kind: 'group', group: row } : null)} label="Group scope" placeholder="Search eligible groups…" maxItems={25} defaultParams={{ start: 0 }} />{me?.is_superuser === true && <button className={`btn${scope?.kind === 'house' ? ' btn-primary' : ''}`} disabled={busy} onClick={() => void choose({ kind: 'house' })}><i className="bi bi-house" /> House contact</button>}</div></div>
38
+ {!scope && <div className="panel panel-pad dim">Choose one authorized group{me?.is_superuser === true ? ' or the House scope' : ''}. No contact has been loaded.</div>}
39
+ {loading && <div className="panel panel-pad dim">Loading selected scope…</div>}{error && <div className="form-alert" role="alert">{error}</div>}
40
+ {scope && response && <div className="panel panel-pad dns-contact-card"><div className="dns-contact-status"><h2>{scope.kind === 'house' ? 'House registrant' : scope.group.name}</h2><div className="chip-row"><Badge tone={response.source === 'database' ? 'success' : response.inherited ? 'warning' : 'muted'}>{response.source === 'database' ? 'Direct database contact' : response.inherited ? 'Inherited (values hidden)' : response.source === 'settings_file' ? 'Deployment file' : 'Not configured'}</Badge><Badge tone={response.effective_configured ? 'success' : 'danger'}>{response.effective_configured ? 'Effective contact ready' : 'Purchase blocked'}</Badge></div></div>{response.inherited && <p className="form-alert">A contact is inherited, but its values are intentionally not disclosed. Saving creates this group’s own contact from the blank form.</p>}{response.source === 'settings_file' && <p className="form-alert">The House contact comes from deployment configuration. Saving creates a database override; the file values are shown for editing but opaque extension fields are not copied.</p>}<div className="form-grid">{CONTACT_STRING_FIELDS.map((key) => <div key={key} className={['AddressLine1', 'AddressLine2', 'OrganizationName'].includes(key) ? 'col-12' : 'col-6'}><label className="field"><span className="field-label">{LABELS[key]}</span>{key === 'ContactType' ? <select className="input" value={String(draft[key] ?? '')} disabled={busy} onChange={(event) => setDraft((value) => ({ ...value, [key]: event.target.value }))}><option value="">Select…</option>{['PERSON', 'COMPANY', 'ASSOCIATION', 'PUBLIC_BODY', 'RESELLER'].map((value) => <option key={value}>{value}</option>)}</select> : <input className="input" type={key === 'Email' ? 'email' : 'text'} value={String(draft[key] ?? '')} disabled={busy} onChange={(event) => setDraft((value) => ({ ...value, [key]: event.target.value }))} />}</label></div>)}</div>{response.problems.length > 0 && <div className="form-alert">{response.problems.join('; ')}</div>}<div className="modal-actions">{response.source === 'database' && <button className="btn btn-danger" disabled={busy} onClick={() => void clear()}>Clear this scope</button>}<button className="btn btn-primary" disabled={busy} onClick={() => void save()}>{busy ? 'Saving…' : 'Save contact'}</button></div></div>}
41
+ </div>;
42
+ }