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,52 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Badge, modal } from '../../ui';
3
+ import { LazyCloudWatchChart } from './CloudWatchChart';
4
+ import {
5
+ CLOUDWATCH_DASHBOARD_CHARTS, useCloudWatchResources,
6
+ type CloudWatchAccount, type CloudWatchResource,
7
+ } from './data';
8
+
9
+ const EAGER_CHART_COUNT = 4;
10
+ const TYPE_META: Record<CloudWatchAccount, { label: string; icon: string }> = {
11
+ ec2: { label: 'EC2 instances', icon: 'bi-pc-display' },
12
+ rds: { label: 'RDS databases', icon: 'bi-database' },
13
+ redis: { label: 'ElastiCache Redis', icon: 'bi-lightning-charge' },
14
+ };
15
+
16
+ export function showCloudWatchResourceDetail(resource: CloudWatchResource): void {
17
+ void import('./CloudWatchResourceDetail').then(({ CloudWatchResourceDetail }) => (
18
+ modal.detail((close) => <CloudWatchResourceDetail resource={resource} onClose={() => close(null)} />)
19
+ ));
20
+ }
21
+
22
+ function CloudWatchResourcesDialog({ onClose }: { onClose: () => void }) {
23
+ const query = useCloudWatchResources();
24
+ return <div className="modal-pad cloudwatch-resources-dialog">
25
+ <div className="cloudwatch-modal-heading"><div><div className="eyebrow">AWS inventory</div><h2 className="modal-title">CloudWatch resources</h2></div><button className="btn-icon" title="Close" aria-label="Close" onClick={onClose}><i className="bi bi-x-lg" /></button></div>
26
+ {query.isPending ? <div className="cloudwatch-resource-loading"><span className="skel skel-block" /></div> : query.error ? <div className="form-alert" role="alert">{query.error.message}</div> : (
27
+ <div className="cloudwatch-resource-groups">
28
+ {(Object.keys(TYPE_META) as CloudWatchAccount[]).map((kind) => <section key={kind}><h3><i className={`bi ${TYPE_META[kind].icon}`} /> {TYPE_META[kind].label}</h3><div className="cloudwatch-resource-list">{query.data![kind].length === 0 ? <p className="dim">No resources discovered.</p> : query.data![kind].map((resource) => <button key={resource.id} className="cloudwatch-resource-row" onClick={() => showCloudWatchResourceDetail(resource)}><span><b>{resource.slug}</b><small>{resource.id}</small></span><Badge>{resource.status}</Badge><i className="bi bi-chevron-right" /></button>)}</div></section>)}
29
+ </div>
30
+ )}
31
+ </div>;
32
+ }
33
+
34
+ export function showCloudWatchResources(): void {
35
+ void modal.open((close) => <CloudWatchResourcesDialog onClose={() => close(null)} />, { size: 'lg' });
36
+ }
37
+
38
+ export function CloudWatchDashboardPage() {
39
+ const [refreshSignal, setRefreshSignal] = useState(0);
40
+ useEffect(() => {
41
+ const timer = window.setInterval(() => setRefreshSignal((value) => value + 1), 300_000);
42
+ return () => window.clearInterval(timer);
43
+ }, []);
44
+ return <div className="admin-dashboard cloudwatch-dashboard-page">
45
+ <header className="admin-dashboard-header cloudwatch-page-heading">
46
+ <div><div className="eyebrow">Infrastructure · AWS</div><h1>CloudWatch monitoring</h1><p className="dim">Global EC2, RDS, and ElastiCache Redis health from the configured AWS account.</p></div>
47
+ <div className="cloudwatch-page-actions"><button className="btn" onClick={showCloudWatchResources}><i className="bi bi-clouds" /> Resources</button><button className="btn btn-primary" onClick={() => setRefreshSignal((value) => value + 1)}><i className="bi bi-arrow-repeat" /> Refresh charts</button></div>
48
+ </header>
49
+ <div className="cloudwatch-gap-note"><i className="bi bi-info-circle" /><span>ELB and Lambda are not shown because django-mojo exposes no CloudWatch account mapping for them.</span></div>
50
+ <div className="cloudwatch-grid">{CLOUDWATCH_DASHBOARD_CHARTS.map((definition, index) => <LazyCloudWatchChart key={`${definition.account}:${definition.category}`} definition={definition} refreshSignal={refreshSignal} eager={index < EAGER_CHART_COUNT} />)}</div>
51
+ </div>;
52
+ }
@@ -0,0 +1,33 @@
1
+ import { useState } from 'react';
2
+ import { DetailView, FlatRow } from '../../ui';
3
+ import { LazyCloudWatchChart } from './CloudWatchChart';
4
+ import { CLOUDWATCH_RESOURCE_CHARTS, type CloudWatchAccount, type CloudWatchResource } from './data';
5
+
6
+ const TYPE_META: Record<CloudWatchAccount, { label: string; icon: string }> = {
7
+ ec2: { label: 'EC2 instances', icon: 'bi-pc-display' },
8
+ rds: { label: 'RDS databases', icon: 'bi-database' },
9
+ redis: { label: 'ElastiCache Redis', icon: 'bi-lightning-charge' },
10
+ };
11
+
12
+ function ResourceMetadata({ resource }: { resource: CloudWatchResource }) {
13
+ if (resource.kind === 'ec2') return <><FlatRow label="AWS id"><code>{resource.id}</code></FlatRow><FlatRow label="State">{resource.state}</FlatRow><FlatRow label="Instance type">{resource.instance_type || '—'}</FlatRow><FlatRow label="Private IP">{resource.private_ip || '—'}</FlatRow><FlatRow label="Public IP">{resource.public_ip || '—'}</FlatRow></>;
14
+ if (resource.kind === 'rds') return <><FlatRow label="AWS id"><code>{resource.id}</code></FlatRow><FlatRow label="Status">{resource.status}</FlatRow><FlatRow label="Engine">{resource.engine || '—'}</FlatRow><FlatRow label="Instance class">{resource.instance_class || '—'}</FlatRow><FlatRow label="Endpoint">{resource.endpoint || '—'}</FlatRow></>;
15
+ return <><FlatRow label="AWS id"><code>{resource.id}</code></FlatRow><FlatRow label="Status">{resource.status}</FlatRow><FlatRow label="Engine">{resource.engine || '—'}</FlatRow><FlatRow label="Node type">{resource.node_type || '—'}</FlatRow><FlatRow label="Nodes">{resource.num_nodes.toLocaleString()}</FlatRow></>;
16
+ }
17
+
18
+ export function CloudWatchResourceDetail({ resource, onClose }: { resource: CloudWatchResource; onClose: () => void }) {
19
+ const [refreshSignal, setRefreshSignal] = useState(0);
20
+ const meta = TYPE_META[resource.kind];
21
+ const definitions = CLOUDWATCH_RESOURCE_CHARTS[resource.kind].map((definition) => ({ ...definition, account: resource.kind }));
22
+ return <DetailView
23
+ title={resource.slug}
24
+ subtitle={meta.label}
25
+ icon={meta.icon}
26
+ chips={[{ text: resource.status, tone: resource.status === 'available' || resource.status === 'running' ? 'success' : 'warning' }]}
27
+ onClose={onClose}
28
+ sections={[
29
+ { key: 'overview', label: 'Overview', icon: 'bi-info-circle', render: () => <div className="detail-section"><p className="dim">Charts target the raw AWS id. The backend deliberately returns the friendly slug as the series label.</p><ResourceMetadata resource={resource} /></div> },
30
+ { key: 'metrics', label: 'Metrics', icon: 'bi-graph-up', render: () => <div className="detail-section"><div className="cloudwatch-detail-actions"><button className="btn" onClick={() => setRefreshSignal((value) => value + 1)}><i className="bi bi-arrow-repeat" /> Refresh metrics</button></div><div className="cloudwatch-grid">{definitions.map((definition, index) => <LazyCloudWatchChart key={definition.category} definition={definition} slugs={[resource.id]} refreshSignal={refreshSignal} eager={index < 2} />)}</div></div> },
31
+ ]}
32
+ />;
33
+ }
@@ -0,0 +1,163 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { mojoCall, useAuthSnapshot, type MetricsResponse, type Params } from '../../client';
3
+ import type { MetricsFetchParams } from '../../charts';
4
+
5
+ export const CLOUDWATCH_PERMISSIONS = ['sys.manage_aws'];
6
+ export const CLOUDWATCH_ACCOUNTS = ['ec2', 'rds', 'redis'] as const;
7
+ export const CLOUDWATCH_GRANULARITIES = ['minutes', 'hours', 'days'] as const;
8
+
9
+ export type CloudWatchAccount = typeof CLOUDWATCH_ACCOUNTS[number];
10
+ export type CloudWatchUnit = 'percent' | 'bytes' | 'seconds' | 'count';
11
+
12
+ export interface CloudWatchChartDefinition {
13
+ account: CloudWatchAccount;
14
+ category: string;
15
+ title: string;
16
+ unit: CloudWatchUnit;
17
+ }
18
+
19
+ export const CLOUDWATCH_DASHBOARD_CHARTS: readonly CloudWatchChartDefinition[] = [
20
+ { account: 'ec2', category: 'cpu', title: 'EC2 CPU', unit: 'percent' },
21
+ { account: 'ec2', category: 'net_out', title: 'EC2 Network Out', unit: 'bytes' },
22
+ { account: 'ec2', category: 'memory', title: 'EC2 Memory', unit: 'percent' },
23
+ { account: 'ec2', category: 'disk', title: 'EC2 Disk', unit: 'percent' },
24
+ { account: 'rds', category: 'cpu', title: 'RDS CPU', unit: 'percent' },
25
+ { account: 'rds', category: 'conns', title: 'RDS Connections', unit: 'count' },
26
+ { account: 'rds', category: 'read_latency', title: 'RDS Read Latency', unit: 'seconds' },
27
+ { account: 'rds', category: 'write_latency', title: 'RDS Write Latency', unit: 'seconds' },
28
+ { account: 'redis', category: 'cpu', title: 'Redis CPU', unit: 'percent' },
29
+ { account: 'redis', category: 'conns', title: 'Redis Connections', unit: 'count' },
30
+ { account: 'redis', category: 'cache_misses', title: 'Redis Cache Misses', unit: 'count' },
31
+ { account: 'redis', category: 'cache_hits', title: 'Redis Cache Hits', unit: 'count' },
32
+ ] as const;
33
+
34
+ export const CLOUDWATCH_RESOURCE_CHARTS: Readonly<Record<CloudWatchAccount, readonly Omit<CloudWatchChartDefinition, 'account'>[]>> = {
35
+ ec2: [
36
+ { category: 'cpu', title: 'CPU Utilization', unit: 'percent' },
37
+ { category: 'memory', title: 'Memory Usage', unit: 'percent' },
38
+ { category: 'disk', title: 'Disk Usage', unit: 'percent' },
39
+ { category: 'net_in', title: 'Network In', unit: 'bytes' },
40
+ { category: 'net_out', title: 'Network Out', unit: 'bytes' },
41
+ { category: 'disk_read', title: 'Disk Read Ops', unit: 'count' },
42
+ { category: 'disk_write', title: 'Disk Write Ops', unit: 'count' },
43
+ { category: 'status_check', title: 'Status Check', unit: 'count' },
44
+ ],
45
+ rds: [
46
+ { category: 'cpu', title: 'CPU Utilization', unit: 'percent' },
47
+ { category: 'conns', title: 'Active Connections', unit: 'count' },
48
+ { category: 'free_storage', title: 'Free Storage', unit: 'bytes' },
49
+ { category: 'free_memory', title: 'Freeable Memory', unit: 'bytes' },
50
+ { category: 'read_iops', title: 'Read IOPS', unit: 'count' },
51
+ { category: 'write_iops', title: 'Write IOPS', unit: 'count' },
52
+ { category: 'read_latency', title: 'Read Latency', unit: 'seconds' },
53
+ { category: 'write_latency', title: 'Write Latency', unit: 'seconds' },
54
+ { category: 'net_in', title: 'Network In', unit: 'bytes' },
55
+ { category: 'net_out', title: 'Network Out', unit: 'bytes' },
56
+ ],
57
+ redis: [
58
+ { category: 'cpu', title: 'CPU Utilization', unit: 'percent' },
59
+ { category: 'conns', title: 'Current Connections', unit: 'count' },
60
+ { category: 'cache_memory', title: 'Cache Memory Used', unit: 'bytes' },
61
+ { category: 'cache_hits', title: 'Cache Hits', unit: 'count' },
62
+ { category: 'cache_misses', title: 'Cache Misses', unit: 'count' },
63
+ { category: 'replication_lag', title: 'Replication Lag', unit: 'seconds' },
64
+ { category: 'net_in', title: 'Network In', unit: 'bytes' },
65
+ { category: 'net_out', title: 'Network Out', unit: 'bytes' },
66
+ ],
67
+ };
68
+
69
+ interface ResourceBase { id: string; slug: string; status: string }
70
+ export interface Ec2Resource extends ResourceBase { kind: 'ec2'; name: string; state: string; instance_type: string; private_ip: string; public_ip: string }
71
+ export interface RdsResource extends ResourceBase { kind: 'rds'; engine: string; instance_class: string; endpoint: string }
72
+ export interface RedisResource extends ResourceBase { kind: 'redis'; engine: string; node_type: string; num_nodes: number }
73
+ export type CloudWatchResource = Ec2Resource | RdsResource | RedisResource;
74
+ export type CloudWatchResources = Record<CloudWatchAccount, CloudWatchResource[]>;
75
+
76
+ function object(value: unknown, label: string): Record<string, unknown> {
77
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`CloudWatch ${label} returned a malformed object.`);
78
+ return value as Record<string, unknown>;
79
+ }
80
+
81
+ function text(value: unknown, label: string, allowEmpty = true): string {
82
+ if (typeof value !== 'string') throw new Error(`CloudWatch ${label} returned a non-string value.`);
83
+ const clean = value.replace(/[\u0000-\u001f\u007f]/g, '').slice(0, 1000);
84
+ if (!allowEmpty && !clean) throw new Error(`CloudWatch ${label} returned an empty value.`);
85
+ return clean;
86
+ }
87
+
88
+ function positiveInteger(value: unknown, label: string): number {
89
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) throw new Error(`CloudWatch ${label} must be a positive integer.`);
90
+ return value;
91
+ }
92
+
93
+ export function sanitizeCloudWatchError(error: unknown): Error {
94
+ const source = error instanceof Error ? error.message : typeof error === 'string' ? error : 'CloudWatch request failed';
95
+ const clean = source
96
+ .replace(/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, '[redacted]')
97
+ .replace(/\b(authorization|x-amz-security-token)\s*[:=]\s*(?:bearer\s+)?[^\s,;]+/gi, '$1: [redacted]')
98
+ .replace(/\b(?:aws_secret_access_key|secret_access_key|session_token)\s*[:=]\s*[^\s,;]+/gi, '[redacted]')
99
+ .slice(0, 1000);
100
+ return new Error(clean || 'CloudWatch request failed');
101
+ }
102
+
103
+ function sanitizeResource(kind: CloudWatchAccount, value: unknown): CloudWatchResource {
104
+ const raw = object(value, `${kind} resource`);
105
+ const base = { id: text(raw.id, `${kind}.id`, false), slug: text(raw.slug, `${kind}.slug`, false), status: text(raw.status ?? raw.state ?? 'unknown', `${kind}.status`, false) };
106
+ if (kind === 'ec2') return { ...base, kind, name: text(raw.name ?? '', 'ec2.name'), state: text(raw.state ?? 'unknown', 'ec2.state', false), instance_type: text(raw.instance_type ?? '', 'ec2.instance_type'), private_ip: text(raw.private_ip ?? '', 'ec2.private_ip'), public_ip: text(raw.public_ip ?? '', 'ec2.public_ip') };
107
+ if (kind === 'rds') return { ...base, kind, engine: text(raw.engine ?? '', 'rds.engine'), instance_class: text(raw.instance_class ?? '', 'rds.instance_class'), endpoint: text(raw.endpoint ?? '', 'rds.endpoint') };
108
+ return { ...base, kind, engine: text(raw.engine ?? '', 'redis.engine'), node_type: text(raw.node_type ?? '', 'redis.node_type'), num_nodes: positiveInteger(raw.num_nodes, 'redis.num_nodes') };
109
+ }
110
+
111
+ export function sanitizeCloudWatchResources(value: unknown): CloudWatchResources {
112
+ const raw = object(value, 'resources');
113
+ return Object.fromEntries(CLOUDWATCH_ACCOUNTS.map((kind) => {
114
+ if (!Array.isArray(raw[kind])) throw new Error(`CloudWatch resources omitted the top-level ${kind} list.`);
115
+ return [kind, raw[kind].map((row) => sanitizeResource(kind, row))];
116
+ })) as CloudWatchResources;
117
+ }
118
+
119
+ export async function fetchCloudWatchResources(): Promise<CloudWatchResources> {
120
+ try {
121
+ const body = await mojoCall('/api/aws/cloudwatch/resources');
122
+ return sanitizeCloudWatchResources(body);
123
+ } catch (error) {
124
+ throw sanitizeCloudWatchError(error);
125
+ }
126
+ }
127
+
128
+ export function useCloudWatchResources() {
129
+ const auth = useAuthSnapshot();
130
+ return useQuery({
131
+ queryKey: ['admin-cloudwatch', 'resources', auth.uid],
132
+ queryFn: fetchCloudWatchResources,
133
+ enabled: auth.authenticated && Boolean(auth.uid),
134
+ staleTime: 5 * 60_000,
135
+ });
136
+ }
137
+
138
+ function metricValues(value: unknown, slug: string): number[] {
139
+ if (!Array.isArray(value)) throw new Error(`CloudWatch series ${slug} is not an array.`);
140
+ return value.map((item) => {
141
+ if (typeof item !== 'number' || !Number.isFinite(item) || item < 0) throw new Error(`CloudWatch series ${slug} contains an invalid value.`);
142
+ return item;
143
+ });
144
+ }
145
+
146
+ export async function loadCloudWatchSeries(params: MetricsFetchParams): Promise<MetricsResponse> {
147
+ try {
148
+ const account = text(params.account, 'fetch.account', false) as CloudWatchAccount;
149
+ if (!CLOUDWATCH_ACCOUNTS.includes(account)) throw new Error(`Unsupported CloudWatch account: ${account}`);
150
+ const category = text(params.category, 'fetch.category', false);
151
+ const granularity = text(params.granularity, 'fetch.granularity', false);
152
+ if (!CLOUDWATCH_GRANULARITIES.includes(granularity as typeof CLOUDWATCH_GRANULARITIES[number])) throw new Error(`Unsupported CloudWatch granularity: ${granularity}`);
153
+ const wire: Params = { account, category, granularity, stat: params.stat ?? 'avg', dt_start: params.dt_start, dt_end: params.dt_end };
154
+ if (typeof params.slugs === 'string' && params.slugs.trim()) wire.slugs = params.slugs;
155
+ const body = await mojoCall('/api/aws/cloudwatch/fetch', { params: wire });
156
+ const raw = object(body.data, 'fetch payload');
157
+ const labels = Array.isArray(raw.labels) ? raw.labels.map((label) => text(label, 'fetch label')) : (() => { throw new Error('CloudWatch fetch omitted labels.'); })();
158
+ const data = object(raw.data, 'fetch data');
159
+ return { labels, datasets: Object.entries(data).map(([slug, values]) => ({ label: text(slug, 'response slug', false), data: metricValues(values, slug) })) };
160
+ } catch (error) {
161
+ throw sanitizeCloudWatchError(error);
162
+ }
163
+ }
@@ -0,0 +1,16 @@
1
+ import type { AdminSection } from '../index';
2
+ import { CloudWatchDashboardPage } from './CloudWatchDashboardPage';
3
+ import { CLOUDWATCH_PERMISSIONS } from './data';
4
+
5
+ export * from './data';
6
+ export * from './CloudWatchChart';
7
+ export * from './CloudWatchDashboardPage';
8
+
9
+ export const CLOUDWATCH_ADMIN_SECTION: AdminSection = {
10
+ id: 'cloudwatch',
11
+ title: 'CloudWatch',
12
+ icon: 'bi-clouds',
13
+ navigationGroup: 'observability',
14
+ permissions: CLOUDWATCH_PERMISSIONS,
15
+ routes: [{ path: '', label: 'CloudWatch', component: CloudWatchDashboardPage, permissions: CLOUDWATCH_PERMISSIONS }],
16
+ };