@oneuptime/common 12.0.13 → 12.0.14

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 (376) hide show
  1. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
  2. package/Models/DatabaseModels/UserCall.ts +94 -3
  3. package/Models/DatabaseModels/UserEmail.ts +94 -3
  4. package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
  5. package/Models/DatabaseModels/UserPush.ts +37 -0
  6. package/Models/DatabaseModels/UserSMS.ts +94 -3
  7. package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
  8. package/Server/API/BaseAPI.ts +79 -14
  9. package/Server/API/UserCallAPI.ts +53 -45
  10. package/Server/API/UserEmailAPI.ts +53 -44
  11. package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
  12. package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
  13. package/Server/API/UserPushAPI.ts +126 -2
  14. package/Server/API/UserSmsAPI.ts +53 -44
  15. package/Server/API/UserWhatsAppAPI.ts +53 -50
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
  21. package/Server/Services/AnalyticsDatabaseService.ts +169 -0
  22. package/Server/Services/DatabaseService.ts +101 -0
  23. package/Server/Services/LogAggregationService.ts +5 -0
  24. package/Server/Services/PushNotificationService.ts +127 -12
  25. package/Server/Services/TeamMemberService.ts +53 -13
  26. package/Server/Services/UserCallService.ts +55 -24
  27. package/Server/Services/UserEmailService.ts +83 -17
  28. package/Server/Services/UserIncomingCallNumberService.ts +49 -85
  29. package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
  30. package/Server/Services/UserNotificationRuleService.ts +26 -0
  31. package/Server/Services/UserPushService.ts +89 -0
  32. package/Server/Services/UserSmsService.ts +50 -17
  33. package/Server/Services/UserWhatsAppService.ts +48 -15
  34. package/Server/Types/Database/QueryHelper.ts +30 -0
  35. package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
  36. package/Server/Utils/ChannelVerification.ts +510 -0
  37. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
  38. package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
  39. package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
  40. package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
  41. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
  42. package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
  43. package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
  44. package/Server/Utils/TotpAuth.ts +137 -9
  45. package/Server/Utils/UserRegistrationToken.ts +182 -0
  46. package/Server/Utils/VerificationCode.ts +134 -0
  47. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
  48. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
  49. package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
  50. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
  51. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
  52. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
  53. package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
  54. package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
  55. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
  56. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
  57. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
  58. package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
  59. package/Tests/Server/API/UserSmsApi.test.ts +107 -14
  60. package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
  61. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
  62. package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
  63. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
  64. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
  65. package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
  66. package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
  67. package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
  68. package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
  69. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
  70. package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
  71. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  72. package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
  73. package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
  74. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
  75. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
  76. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
  77. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
  78. package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
  79. package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
  80. package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
  81. package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
  82. package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
  83. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
  84. package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
  85. package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
  86. package/Tests/Server/Utils/Browser.test.ts +162 -0
  87. package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
  88. package/Tests/Server/Utils/Express.test.ts +371 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
  90. package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
  91. package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
  92. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
  93. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
  94. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
  95. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
  96. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
  97. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
  98. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
  99. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
  100. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
  101. package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
  102. package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
  103. package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
  104. package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
  105. package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
  106. package/Tests/Types/Call/CallRequest.test.ts +189 -0
  107. package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
  108. package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
  109. package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
  110. package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
  111. package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
  112. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
  113. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
  114. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
  115. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
  116. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
  117. package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
  118. package/Tests/Types/TextRandomGeneration.test.ts +211 -0
  119. package/Tests/UI/Components/Button.test.tsx +175 -1
  120. package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
  121. package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
  122. package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
  123. package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
  124. package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
  125. package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
  126. package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
  127. package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
  128. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
  129. package/Tests/UI/Components/List.test.tsx +9 -2
  130. package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
  131. package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
  132. package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
  133. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
  134. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
  135. package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
  136. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
  137. package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
  138. package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
  139. package/Tests/UI/Components/Skeleton.test.tsx +71 -0
  140. package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
  141. package/Tests/UI/Components/Tabs.test.tsx +24 -0
  142. package/Tests/UI/Components/Toast.test.tsx +130 -7
  143. package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
  144. package/Tests/UI/Utils/Dropdown.test.ts +423 -0
  145. package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
  146. package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
  147. package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
  148. package/Tests/Utils/API.test.ts +16 -2
  149. package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
  150. package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
  151. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
  152. package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
  153. package/Types/Email/EmailTemplateType.ts +1 -0
  154. package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
  155. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
  156. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
  157. package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
  158. package/Types/Permission.ts +35 -0
  159. package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
  160. package/Types/PushNotification/PushNotificationMessage.ts +15 -0
  161. package/Types/PushNotification/PushNotificationRequest.ts +8 -16
  162. package/Types/Service/ServiceLanguage.ts +144 -0
  163. package/Types/Text.ts +111 -19
  164. package/UI/Components/Button/Button.tsx +27 -14
  165. package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
  166. package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
  167. package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
  168. package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
  169. package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
  170. package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
  171. package/UI/Components/CommandPalette/Types.ts +49 -0
  172. package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
  173. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  174. package/UI/Components/Feed/FeedItem.tsx +1 -1
  175. package/UI/Components/List/List.tsx +37 -5
  176. package/UI/Components/List/ListSkeleton.tsx +54 -0
  177. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
  178. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
  179. package/UI/Components/Modal/Modal.tsx +2 -2
  180. package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
  181. package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
  182. package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
  183. package/UI/Components/Navbar/NavBar.tsx +14 -1
  184. package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
  185. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
  186. package/UI/Components/SideOver/SideOver.tsx +37 -2
  187. package/UI/Components/Skeleton/Skeleton.tsx +53 -0
  188. package/UI/Components/Table/Table.tsx +74 -44
  189. package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
  190. package/UI/Components/Tabs/Tab.tsx +1 -1
  191. package/UI/Components/Toast/Toast.tsx +118 -53
  192. package/UI/Components/Toast/ToastInit.tsx +82 -32
  193. package/UI/Styles/Theme.css +40 -0
  194. package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
  195. package/UI/Utils/ModelListCache.ts +188 -0
  196. package/UI/Utils/Project.ts +29 -0
  197. package/Utils/API.ts +6 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  200. package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
  201. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  202. package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
  203. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
  205. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
  206. package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
  207. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  208. package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
  209. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  210. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
  211. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  212. package/build/dist/Server/API/BaseAPI.js +69 -13
  213. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  214. package/build/dist/Server/API/UserCallAPI.js +42 -38
  215. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  216. package/build/dist/Server/API/UserEmailAPI.js +42 -38
  217. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  218. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
  219. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  220. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
  221. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
  222. package/build/dist/Server/API/UserPushAPI.js +95 -2
  223. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  224. package/build/dist/Server/API/UserSmsAPI.js +42 -38
  225. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  226. package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
  227. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
  233. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
  234. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  236. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
  237. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
  239. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  240. package/build/dist/Server/Services/DatabaseService.js +74 -0
  241. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  242. package/build/dist/Server/Services/LogAggregationService.js +5 -0
  243. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  244. package/build/dist/Server/Services/PushNotificationService.js +69 -30
  245. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  246. package/build/dist/Server/Services/TeamMemberService.js +43 -7
  247. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  248. package/build/dist/Server/Services/UserCallService.js +54 -22
  249. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  250. package/build/dist/Server/Services/UserEmailService.js +78 -17
  251. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  252. package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
  253. package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
  254. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
  255. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
  256. package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
  257. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  258. package/build/dist/Server/Services/UserPushService.js +77 -0
  259. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  260. package/build/dist/Server/Services/UserSmsService.js +50 -17
  261. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  262. package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
  263. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  264. package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
  265. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  266. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
  267. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
  268. package/build/dist/Server/Utils/ChannelVerification.js +348 -0
  269. package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
  270. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
  271. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  272. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
  273. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  274. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
  275. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  276. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
  277. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
  278. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
  279. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
  280. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
  281. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
  282. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  283. package/build/dist/Server/Utils/TotpAuth.js +127 -8
  284. package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
  285. package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
  286. package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
  287. package/build/dist/Server/Utils/VerificationCode.js +120 -0
  288. package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
  289. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  290. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  291. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
  292. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  293. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
  294. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  295. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
  296. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  297. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
  298. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
  299. package/build/dist/Types/Permission.js +33 -0
  300. package/build/dist/Types/Permission.js.map +1 -1
  301. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
  302. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
  303. package/build/dist/Types/Service/ServiceLanguage.js +96 -0
  304. package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
  305. package/build/dist/Types/Text.js +82 -17
  306. package/build/dist/Types/Text.js.map +1 -1
  307. package/build/dist/UI/Components/Button/Button.js +22 -14
  308. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  309. package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
  310. package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
  311. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
  312. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
  313. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
  314. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
  315. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
  316. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
  317. package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
  318. package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
  319. package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
  320. package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
  321. package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
  322. package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
  323. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
  324. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
  325. package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
  326. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  327. package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
  328. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  329. package/build/dist/UI/Components/List/List.js +19 -5
  330. package/build/dist/UI/Components/List/List.js.map +1 -1
  331. package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
  332. package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
  333. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  334. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
  335. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  336. package/build/dist/UI/Components/Modal/Modal.js +2 -2
  337. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
  338. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  339. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
  340. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  341. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
  342. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  343. package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
  344. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  345. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
  346. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
  347. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
  348. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  349. package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
  350. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  351. package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
  352. package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
  353. package/build/dist/UI/Components/Table/Table.js +35 -24
  354. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  355. package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
  356. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
  357. package/build/dist/UI/Components/Tabs/Tab.js +1 -1
  358. package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
  359. package/build/dist/UI/Components/Toast/Toast.js +77 -23
  360. package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
  361. package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
  362. package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
  363. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
  364. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  365. package/build/dist/UI/Utils/ModelListCache.js +126 -0
  366. package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
  367. package/build/dist/UI/Utils/Project.js +24 -1
  368. package/build/dist/UI/Utils/Project.js.map +1 -1
  369. package/build/dist/Utils/API.js +6 -1
  370. package/build/dist/Utils/API.js.map +1 -1
  371. package/jest.config.json +1 -1
  372. package/package.json +2 -3
  373. package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
  374. package/Tests/__mocks__/otpauth.js +0 -30
  375. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
  376. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +0 -1
@@ -0,0 +1,790 @@
1
+ import Icon from "../Icon/Icon";
2
+ import KeyboardShortcut, {
3
+ KeyboardShortcutSize,
4
+ KeyboardShortcutVariant,
5
+ } from "../KeyboardShortcut/KeyboardShortcut";
6
+ import KeyboardKey, {
7
+ KeyboardShortcutKey,
8
+ } from "../KeyboardShortcut/KeyboardKey";
9
+ import IconProp from "../../../Types/Icon/IconProp";
10
+ import useTranslateValue from "../../Utils/Translation";
11
+ import { lockPageScroll, unlockPageScroll } from "../../Utils/PageScrollLock";
12
+ import {
13
+ PaletteCommand,
14
+ PaletteSearchProvider,
15
+ PaletteSearchResult,
16
+ } from "./Types";
17
+ import {
18
+ filterPaletteCommands,
19
+ getPaletteSectionId,
20
+ normalizePaletteQuery,
21
+ PaletteCommandMatch,
22
+ } from "./PaletteFilter";
23
+ import {
24
+ readRecentCommandIds,
25
+ RECENT_COMMANDS_LIMIT,
26
+ writeRecentCommandId,
27
+ } from "./RecentCommands";
28
+ import useProviderSearch, { ProviderSearchSection } from "./UseProviderSearch";
29
+ import PaletteRow from "./PaletteRow";
30
+ import React, {
31
+ FunctionComponent,
32
+ ReactElement,
33
+ useEffect,
34
+ useMemo,
35
+ useRef,
36
+ useState,
37
+ } from "react";
38
+ import { createPortal } from "react-dom";
39
+
40
+ export interface ComponentProps {
41
+ commands: Array<PaletteCommand>;
42
+ searchProviders?: Array<PaletteSearchProvider> | undefined;
43
+ isOpen: boolean;
44
+ onClose: () => void;
45
+ /** When set, selected command ids are persisted here and surface as "Recent". */
46
+ recentStorageKey?: string | undefined;
47
+ }
48
+
49
+ /** One selectable row in the flattened keyboard order. */
50
+ interface FlatEntry {
51
+ /** Unique across the whole open palette (recents/provider results are prefixed). */
52
+ id: string;
53
+ title: string;
54
+ description?: string | undefined;
55
+ icon?: IconProp | undefined;
56
+ iconColor?: string | undefined;
57
+ shortcut?: Array<KeyboardShortcutKey> | undefined;
58
+ /** The id recorded into recents — only static commands carry one. */
59
+ recentCommandId: string | null;
60
+ onSelect: () => void;
61
+ flatIndex: number;
62
+ }
63
+
64
+ interface PaletteSectionView {
65
+ /** Slug used in `command-palette-section-<id>` test ids. */
66
+ id: string;
67
+ title: string;
68
+ /** True while this (provider) section's search is still in flight. */
69
+ isPending: boolean;
70
+ entries: Array<FlatEntry>;
71
+ }
72
+
73
+ const EMPTY_PROVIDERS: Array<PaletteSearchProvider> = [];
74
+
75
+ type HasPendingSectionFunction = (
76
+ sections: Array<PaletteSectionView>,
77
+ ) => boolean;
78
+
79
+ const hasPendingSection: HasPendingSectionFunction = (
80
+ sections: Array<PaletteSectionView>,
81
+ ): boolean => {
82
+ return sections.some((section: PaletteSectionView) => {
83
+ return section.isPending;
84
+ });
85
+ };
86
+
87
+ type CommandToEntryFunction = (
88
+ command: PaletteCommand,
89
+ idPrefix: string,
90
+ ) => Omit<FlatEntry, "flatIndex">;
91
+
92
+ const commandToEntry: CommandToEntryFunction = (
93
+ command: PaletteCommand,
94
+ idPrefix: string,
95
+ ): Omit<FlatEntry, "flatIndex"> => {
96
+ return {
97
+ id: `${idPrefix}${command.id}`,
98
+ title: command.title,
99
+ description: command.description,
100
+ icon: command.icon,
101
+ iconColor: command.iconColor,
102
+ shortcut: command.shortcut,
103
+ recentCommandId: command.id,
104
+ onSelect: command.onSelect,
105
+ };
106
+ };
107
+
108
+ /*
109
+ * The open palette lives in its own component so that every open is a fresh
110
+ * mount: the enter animation, the autofocus, the scroll lock, and the
111
+ * read-once recents snapshot all run per open instead of once per page.
112
+ */
113
+ interface PanelProps {
114
+ commands: Array<PaletteCommand>;
115
+ searchProviders: Array<PaletteSearchProvider>;
116
+ onClose: () => void;
117
+ recentStorageKey?: string | undefined;
118
+ }
119
+
120
+ const CommandPalettePanel: FunctionComponent<PanelProps> = (
121
+ props: PanelProps,
122
+ ): ReactElement => {
123
+ const [query, setQuery] = useState<string>("");
124
+ const [activeIndex, setActiveIndex] = useState<number>(0);
125
+
126
+ /*
127
+ * The panel mounts in its closed state and is flipped open one frame later
128
+ * so the browser has something to transition from (Modal's pattern).
129
+ */
130
+ const [hasEntered, setHasEntered] = useState<boolean>(false);
131
+
132
+ const inputRef: React.RefObject<HTMLInputElement> =
133
+ useRef<HTMLInputElement>(null);
134
+ const rowRefs: React.MutableRefObject<Array<HTMLDivElement | null>> = useRef<
135
+ Array<HTMLDivElement | null>
136
+ >([]);
137
+
138
+ /*
139
+ * Whatever had focus when the palette opened gets it back on close —
140
+ * captured at first render, before the autofocus effect moves focus into
141
+ * the input (Modal's pattern).
142
+ */
143
+ const previouslyFocusedElementRef: React.MutableRefObject<HTMLElement | null> =
144
+ useRef<HTMLElement | null>(
145
+ typeof document !== "undefined" &&
146
+ document.activeElement instanceof HTMLElement
147
+ ? document.activeElement
148
+ : null,
149
+ );
150
+
151
+ // Kept fresh so the mount-once document listener never calls a stale close.
152
+ const onCloseRef: React.MutableRefObject<() => void> = useRef<() => void>(
153
+ props.onClose,
154
+ );
155
+
156
+ useEffect(() => {
157
+ onCloseRef.current = props.onClose;
158
+ }, [props.onClose]);
159
+
160
+ // Recently run command ids, read once when the palette opens.
161
+ const [recentCommandIds] = useState<Array<string>>(() => {
162
+ return props.recentStorageKey
163
+ ? readRecentCommandIds(props.recentStorageKey)
164
+ : [];
165
+ });
166
+
167
+ /*
168
+ * Callers hand us plain English literals. Run every user-facing string
169
+ * through the same flat-key lookup the navbar uses so localised deployments
170
+ * render localised chrome.
171
+ */
172
+ const { translateString } = useTranslateValue();
173
+ const tx: (value: string) => string = (value: string): string => {
174
+ return translateString(value) ?? value;
175
+ };
176
+
177
+ useEffect(() => {
178
+ const open: () => void = (): void => {
179
+ setHasEntered(true);
180
+ };
181
+
182
+ const frame: number = requestAnimationFrame(open);
183
+
184
+ /*
185
+ * A hidden or throttled tab never runs an animation frame; the timer is
186
+ * the safety net that skips the transition rather than the palette.
187
+ */
188
+ const fallbackTimer: ReturnType<typeof setTimeout> = setTimeout(open, 80);
189
+
190
+ return () => {
191
+ cancelAnimationFrame(frame);
192
+ clearTimeout(fallbackTimer);
193
+ };
194
+ }, []);
195
+
196
+ // The page behind the palette must not scroll while it is open.
197
+ useEffect(() => {
198
+ lockPageScroll();
199
+
200
+ return () => {
201
+ unlockPageScroll();
202
+ };
203
+ }, []);
204
+
205
+ useEffect(() => {
206
+ inputRef.current?.focus();
207
+ }, []);
208
+
209
+ /*
210
+ * Escape must dismiss the palette even after focus has escaped the dialog
211
+ * subtree (a click on the backdrop scrollbar, a programmatic blur): the
212
+ * React onKeyDown on the dialog root only hears keys fired inside it, so a
213
+ * document-level listener backs it up while the panel is mounted (Modal's
214
+ * pattern). No double-fire: the React handler preventDefaults AND stops
215
+ * propagation, so an Escape it consumed either never reaches document or
216
+ * arrives flagged defaultPrevented — which is also the flag any other
217
+ * dialog above us would set.
218
+ */
219
+ useEffect(() => {
220
+ const handleDocumentKeyDown: (event: KeyboardEvent) => void = (
221
+ event: KeyboardEvent,
222
+ ): void => {
223
+ if (event.defaultPrevented || event.key !== "Escape") {
224
+ return;
225
+ }
226
+
227
+ // Flag the key consumed for listeners after us (window-level ones too).
228
+ event.preventDefault();
229
+ onCloseRef.current();
230
+ };
231
+
232
+ document.addEventListener("keydown", handleDocumentKeyDown);
233
+
234
+ return () => {
235
+ document.removeEventListener("keydown", handleDocumentKeyDown);
236
+ };
237
+ }, []);
238
+
239
+ // Return focus to where it was before the palette opened (Modal's pattern).
240
+ useEffect(() => {
241
+ return () => {
242
+ if (previouslyFocusedElementRef.current?.isConnected) {
243
+ previouslyFocusedElementRef.current.focus();
244
+ }
245
+ };
246
+ }, []);
247
+
248
+ const providerSections: Array<ProviderSearchSection> = useProviderSearch(
249
+ props.searchProviders,
250
+ query,
251
+ );
252
+
253
+ const normalizedQuery: string = normalizePaletteQuery(query);
254
+
255
+ const sections: Array<PaletteSectionView> = useMemo(() => {
256
+ const built: Array<PaletteSectionView> = [];
257
+
258
+ type PushSectionFunction = (
259
+ id: string,
260
+ title: string,
261
+ isPending: boolean,
262
+ entries: Array<Omit<FlatEntry, "flatIndex">>,
263
+ ) => void;
264
+
265
+ const pushSection: PushSectionFunction = (
266
+ id: string,
267
+ title: string,
268
+ isPending: boolean,
269
+ entries: Array<Omit<FlatEntry, "flatIndex">>,
270
+ ): void => {
271
+ built.push({
272
+ id,
273
+ title,
274
+ isPending,
275
+ // flatIndex is stamped in a single pass after all sections exist.
276
+ entries: entries as Array<FlatEntry>,
277
+ });
278
+ };
279
+
280
+ type GroupByCategoryFunction = (
281
+ commands: Array<PaletteCommand>,
282
+ ) => Array<{ category: string; commands: Array<PaletteCommand> }>;
283
+
284
+ // Group commands by category, preserving first-seen order.
285
+ const groupByCategory: GroupByCategoryFunction = (
286
+ commands: Array<PaletteCommand>,
287
+ ): Array<{ category: string; commands: Array<PaletteCommand> }> => {
288
+ const order: Array<string> = [];
289
+ const byCategory: Map<string, Array<PaletteCommand>> = new Map();
290
+ commands.forEach((command: PaletteCommand) => {
291
+ const category: string = command.category || "Other";
292
+ if (!byCategory.has(category)) {
293
+ byCategory.set(category, []);
294
+ order.push(category);
295
+ }
296
+ byCategory.get(category)!.push(command);
297
+ });
298
+ return order.map((category: string) => {
299
+ return { category, commands: byCategory.get(category)! };
300
+ });
301
+ };
302
+
303
+ if (!normalizedQuery) {
304
+ // Browsing: "Recent" first (when we have any), then the full catalog.
305
+ if (recentCommandIds.length > 0) {
306
+ const commandById: Map<string, PaletteCommand> = new Map();
307
+ props.commands.forEach((command: PaletteCommand) => {
308
+ commandById.set(command.id, command);
309
+ });
310
+ const recentCommands: Array<PaletteCommand> = [];
311
+ recentCommandIds.forEach((commandId: string) => {
312
+ const command: PaletteCommand | undefined =
313
+ commandById.get(commandId);
314
+ if (command) {
315
+ recentCommands.push(command);
316
+ }
317
+ });
318
+ if (recentCommands.length > 0) {
319
+ pushSection(
320
+ "recent",
321
+ "Recent",
322
+ false,
323
+ recentCommands
324
+ .slice(0, RECENT_COMMANDS_LIMIT)
325
+ /*
326
+ * Recents repeat commands that also sit in their category
327
+ * section below — prefix the entry id so option test ids and
328
+ * DOM ids stay unique.
329
+ */
330
+ .map((command: PaletteCommand) => {
331
+ return commandToEntry(command, "recent-");
332
+ }),
333
+ );
334
+ }
335
+ }
336
+
337
+ groupByCategory(props.commands).forEach(
338
+ (group: { category: string; commands: Array<PaletteCommand> }) => {
339
+ pushSection(
340
+ getPaletteSectionId(group.category),
341
+ group.category,
342
+ false,
343
+ group.commands.map((command: PaletteCommand) => {
344
+ return commandToEntry(command, "");
345
+ }),
346
+ );
347
+ },
348
+ );
349
+ } else {
350
+ /*
351
+ * Searching: ranked command matches grouped by category — the ranked
352
+ * order decides both row order within a section and which category
353
+ * surfaces first — then one section per async provider.
354
+ */
355
+ const matches: Array<PaletteCommandMatch> = filterPaletteCommands(
356
+ props.commands,
357
+ query,
358
+ );
359
+ groupByCategory(
360
+ matches.map((match: PaletteCommandMatch) => {
361
+ return match.command;
362
+ }),
363
+ ).forEach(
364
+ (group: { category: string; commands: Array<PaletteCommand> }) => {
365
+ pushSection(
366
+ getPaletteSectionId(group.category),
367
+ group.category,
368
+ false,
369
+ group.commands.map((command: PaletteCommand) => {
370
+ return commandToEntry(command, "");
371
+ }),
372
+ );
373
+ },
374
+ );
375
+
376
+ providerSections.forEach((providerSection: ProviderSearchSection) => {
377
+ // A provider with nothing to show (or that failed) has no section.
378
+ if (
379
+ !providerSection.isPending &&
380
+ providerSection.results.length === 0
381
+ ) {
382
+ return;
383
+ }
384
+ pushSection(
385
+ providerSection.provider.id,
386
+ providerSection.provider.title,
387
+ providerSection.isPending,
388
+ providerSection.results.map((result: PaletteSearchResult) => {
389
+ return {
390
+ id: `${providerSection.provider.id}-${result.id}`,
391
+ title: result.title,
392
+ description: result.description,
393
+ icon: result.icon,
394
+ iconColor: result.iconColor,
395
+ shortcut: undefined,
396
+ recentCommandId: null,
397
+ onSelect: result.onSelect,
398
+ };
399
+ }),
400
+ );
401
+ });
402
+ }
403
+
404
+ // Stamp each entry with its index in the flattened keyboard order.
405
+ let flatIndex: number = 0;
406
+ built.forEach((section: PaletteSectionView) => {
407
+ section.entries.forEach((entry: FlatEntry) => {
408
+ entry.flatIndex = flatIndex;
409
+ flatIndex++;
410
+ });
411
+ });
412
+
413
+ return built;
414
+ }, [
415
+ normalizedQuery,
416
+ query,
417
+ props.commands,
418
+ recentCommandIds,
419
+ providerSections,
420
+ ]);
421
+
422
+ // Flattened list in render order — the keyboard walks this.
423
+ const flatEntries: Array<FlatEntry> = useMemo(() => {
424
+ return sections.reduce(
425
+ (accumulator: Array<FlatEntry>, section: PaletteSectionView) => {
426
+ section.entries.forEach((entry: FlatEntry) => {
427
+ accumulator.push(entry);
428
+ });
429
+ return accumulator;
430
+ },
431
+ [],
432
+ );
433
+ }, [sections]);
434
+
435
+ // A new query snaps the selection back to the best match.
436
+ useEffect(() => {
437
+ setActiveIndex(0);
438
+ }, [query]);
439
+
440
+ /*
441
+ * Clamp the selection when async results shrink the list under it. The
442
+ * functional updater matters: on a query change this effect and the reset
443
+ * above run in the same pass, and reading activeIndex from the closure here
444
+ * would clobber the queued reset with a stale clamp.
445
+ */
446
+ useEffect(() => {
447
+ if (flatEntries.length === 0) {
448
+ return;
449
+ }
450
+ setActiveIndex((index: number) => {
451
+ return index > flatEntries.length - 1 ? flatEntries.length - 1 : index;
452
+ });
453
+ }, [flatEntries.length]);
454
+
455
+ // Keep the active row scrolled into view as the selection moves.
456
+ useEffect(() => {
457
+ const row: HTMLDivElement | null = rowRefs.current[activeIndex] || null;
458
+ if (row && typeof row.scrollIntoView === "function") {
459
+ row.scrollIntoView({ block: "nearest" });
460
+ }
461
+ }, [activeIndex]);
462
+
463
+ const runEntry: (entry: FlatEntry) => void = (entry: FlatEntry): void => {
464
+ if (entry.recentCommandId && props.recentStorageKey) {
465
+ writeRecentCommandId(props.recentStorageKey, entry.recentCommandId);
466
+ }
467
+ entry.onSelect();
468
+ props.onClose();
469
+ };
470
+
471
+ /*
472
+ * One handler on the dialog root: keys fired in the input bubble here, and
473
+ * so do keys fired with focus anywhere else inside the palette. Every
474
+ * handled key calls preventDefault so listeners beneath the palette
475
+ * (page shortcuts, scrolling) can see the key was consumed. Escape goes
476
+ * one step further and also stops propagating: the topmost dialog consumes
477
+ * its dismiss key outright, so document/window Escape listeners underneath
478
+ * (the Ask AI slide-over, page-level shortcuts) never see it at all.
479
+ */
480
+ const handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void = (
481
+ event: React.KeyboardEvent<HTMLDivElement>,
482
+ ): void => {
483
+ if (event.key === "Escape") {
484
+ event.preventDefault();
485
+ event.stopPropagation();
486
+ props.onClose();
487
+ return;
488
+ }
489
+ if (flatEntries.length === 0) {
490
+ return;
491
+ }
492
+ if (event.key === "ArrowDown") {
493
+ event.preventDefault();
494
+ setActiveIndex((index: number) => {
495
+ return (index + 1) % flatEntries.length;
496
+ });
497
+ } else if (event.key === "ArrowUp") {
498
+ event.preventDefault();
499
+ setActiveIndex((index: number) => {
500
+ return (index - 1 + flatEntries.length) % flatEntries.length;
501
+ });
502
+ } else if (event.key === "Home") {
503
+ event.preventDefault();
504
+ setActiveIndex(0);
505
+ } else if (event.key === "End") {
506
+ event.preventDefault();
507
+ setActiveIndex(flatEntries.length - 1);
508
+ } else if (event.key === "Enter") {
509
+ event.preventDefault();
510
+ const entry: FlatEntry | undefined = flatEntries[activeIndex];
511
+ if (entry) {
512
+ runEntry(entry);
513
+ }
514
+ }
515
+ };
516
+
517
+ /*
518
+ * A mousedown on non-interactive panel chrome (section headers, the hints
519
+ * footer, the empty state) would move focus to the body and kill arrow-key
520
+ * navigation. preventDefault keeps focus in the input. Interactive targets
521
+ * keep their default: the input (caret/selection), option rows, and the
522
+ * scroll container itself — a scrollbar drag starts as a mousedown on it,
523
+ * and preventDefault would cancel the drag.
524
+ */
525
+ const handlePanelMouseDown: (
526
+ event: React.MouseEvent<HTMLDivElement>,
527
+ ) => void = (event: React.MouseEvent<HTMLDivElement>): void => {
528
+ const target: HTMLElement | null =
529
+ event.target instanceof HTMLElement ? event.target : null;
530
+
531
+ if (!target) {
532
+ return;
533
+ }
534
+
535
+ if (target.id === "command-palette-listbox") {
536
+ return;
537
+ }
538
+
539
+ if (target.closest('input, textarea, select, button, a, [role="option"]')) {
540
+ return;
541
+ }
542
+
543
+ event.preventDefault();
544
+ };
545
+
546
+ const activeEntry: FlatEntry | undefined = flatEntries[activeIndex];
547
+
548
+ /*
549
+ * Once the panel has settled it carries no transform at all — a lingering
550
+ * scale would make it the containing block for position:fixed descendants
551
+ * (the Modal invariant).
552
+ */
553
+ const panelTransitionClassName: string = hasEntered
554
+ ? "opacity-100"
555
+ : "scale-[0.98] opacity-0";
556
+
557
+ const resultCount: number = flatEntries.length;
558
+
559
+ return (
560
+ <div
561
+ className="relative z-50"
562
+ role="dialog"
563
+ aria-modal="true"
564
+ aria-label={tx("Command palette")}
565
+ data-testid="command-palette"
566
+ onKeyDown={handleKeyDown}
567
+ >
568
+ {/* Backdrop */}
569
+ <div
570
+ className={`fixed inset-0 bg-gray-500/25 backdrop-blur-sm transition-opacity duration-150 ease-out motion-reduce:transition-none ${
571
+ hasEntered ? "opacity-100" : "opacity-0"
572
+ }`}
573
+ data-testid="command-palette-backdrop"
574
+ aria-hidden="true"
575
+ />
576
+
577
+ {/* Scroll + click-away container */}
578
+ <div
579
+ className="fixed inset-0 z-50 overflow-y-auto"
580
+ onClick={props.onClose}
581
+ >
582
+ <div className="flex min-h-full items-start justify-center p-4 sm:p-6">
583
+ <div
584
+ className={`relative mt-[15vh] flex w-full max-w-xl flex-col overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black/5 transition duration-150 ease-out motion-reduce:transition-none ${panelTransitionClassName}`}
585
+ data-testid="command-palette-panel"
586
+ onClick={(event: React.MouseEvent<HTMLDivElement>) => {
587
+ event.stopPropagation();
588
+ }}
589
+ onMouseDown={handlePanelMouseDown}
590
+ >
591
+ {/* Search input */}
592
+ <div className="flex items-center gap-3 border-b border-gray-100 px-4 py-3.5">
593
+ <Icon
594
+ icon={IconProp.Search}
595
+ className="h-5 w-5 flex-shrink-0 text-gray-400"
596
+ />
597
+ <input
598
+ ref={inputRef}
599
+ data-testid="command-palette-input"
600
+ type="text"
601
+ role="combobox"
602
+ aria-expanded={true}
603
+ aria-autocomplete="list"
604
+ aria-controls="command-palette-listbox"
605
+ aria-activedescendant={
606
+ activeEntry
607
+ ? `command-palette-option-${activeEntry.id}`
608
+ : undefined
609
+ }
610
+ aria-label={tx("Search pages, actions…")}
611
+ autoComplete="off"
612
+ autoCorrect="off"
613
+ autoCapitalize="off"
614
+ spellCheck={false}
615
+ value={query}
616
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
617
+ setQuery(event.target.value);
618
+ }}
619
+ placeholder={tx("Search pages, actions…")}
620
+ className="flex-1 border-0 bg-transparent p-0 text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-0"
621
+ />
622
+ <KeyboardShortcut
623
+ keys={[KeyboardKey.Mod, "K"]}
624
+ size={KeyboardShortcutSize.Small}
625
+ className="hidden flex-shrink-0 sm:inline-flex"
626
+ />
627
+ </div>
628
+
629
+ {/* Screen readers hear how many rows the query produced. */}
630
+ <p
631
+ aria-live="polite"
632
+ data-testid="command-palette-result-count"
633
+ className="sr-only"
634
+ >
635
+ {resultCount} {resultCount === 1 ? tx("result") : tx("results")}
636
+ </p>
637
+
638
+ {/* Results */}
639
+ <div
640
+ id="command-palette-listbox"
641
+ role="listbox"
642
+ aria-label={tx("Commands")}
643
+ className="max-h-[55vh] flex-1 overflow-y-auto overscroll-contain p-2"
644
+ >
645
+ {flatEntries.length === 0 && !hasPendingSection(sections) ? (
646
+ <div
647
+ data-testid="command-palette-empty"
648
+ className="flex flex-col items-center justify-center py-14 text-center"
649
+ >
650
+ <div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 ring-1 ring-gray-100">
651
+ <Icon
652
+ icon={IconProp.Search}
653
+ className="h-4 w-4 text-gray-400"
654
+ />
655
+ </div>
656
+ <p className="text-sm font-medium text-gray-900">
657
+ {tx("No results found.")}
658
+ </p>
659
+ {query.trim() && (
660
+ <p className="mt-1 text-xs text-gray-500">
661
+ &ldquo;{query.trim()}&rdquo;
662
+ </p>
663
+ )}
664
+ </div>
665
+ ) : (
666
+ sections.map((section: PaletteSectionView) => {
667
+ return (
668
+ <div
669
+ key={section.id}
670
+ role="group"
671
+ aria-label={section.title}
672
+ data-testid={`command-palette-section-${section.id}`}
673
+ className="mb-1 last:mb-0"
674
+ >
675
+ <div
676
+ aria-hidden="true"
677
+ className="flex items-center gap-2 px-3 pb-1 pt-2.5"
678
+ >
679
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
680
+ {section.title}
681
+ </h3>
682
+ {section.isPending && (
683
+ <span
684
+ data-testid={`command-palette-section-pending-${section.id}`}
685
+ className="animate-pulse text-[10px] font-normal normal-case tracking-normal text-gray-400"
686
+ >
687
+ {tx("Searching…")}
688
+ </span>
689
+ )}
690
+ </div>
691
+ {section.entries.map((entry: FlatEntry) => {
692
+ return (
693
+ <PaletteRow
694
+ key={entry.id}
695
+ item={{
696
+ id: entry.id,
697
+ title: entry.title,
698
+ description: entry.description,
699
+ icon: entry.icon,
700
+ iconColor: entry.iconColor,
701
+ shortcut: entry.shortcut,
702
+ }}
703
+ query={query}
704
+ isActive={entry.flatIndex === activeIndex}
705
+ onActivate={() => {
706
+ setActiveIndex(entry.flatIndex);
707
+ }}
708
+ onSelect={() => {
709
+ runEntry(entry);
710
+ }}
711
+ setRowElement={(element: HTMLDivElement | null) => {
712
+ rowRefs.current[entry.flatIndex] = element;
713
+ }}
714
+ />
715
+ );
716
+ })}
717
+ </div>
718
+ );
719
+ })
720
+ )}
721
+ </div>
722
+
723
+ {/* Keyboard hints */}
724
+ <div
725
+ data-testid="command-palette-footer"
726
+ className="flex items-center gap-4 border-t border-gray-100 px-4 py-2"
727
+ >
728
+ <span className="flex items-center gap-1.5 text-[11px] text-gray-400">
729
+ <KeyboardShortcut
730
+ keys={[KeyboardKey.ArrowUp, KeyboardKey.ArrowDown]}
731
+ size={KeyboardShortcutSize.ExtraSmall}
732
+ variant={KeyboardShortcutVariant.Ghost}
733
+ />
734
+ {tx("to navigate")}
735
+ </span>
736
+ <span className="flex items-center gap-1.5 text-[11px] text-gray-400">
737
+ <KeyboardShortcut
738
+ keys={[KeyboardKey.Enter]}
739
+ size={KeyboardShortcutSize.ExtraSmall}
740
+ variant={KeyboardShortcutVariant.Ghost}
741
+ />
742
+ {tx("to select")}
743
+ </span>
744
+ <span className="flex items-center gap-1.5 text-[11px] text-gray-400">
745
+ <KeyboardShortcut
746
+ keys={[KeyboardKey.Escape]}
747
+ size={KeyboardShortcutSize.ExtraSmall}
748
+ variant={KeyboardShortcutVariant.Ghost}
749
+ />
750
+ {tx("to close")}
751
+ </span>
752
+ </div>
753
+ </div>
754
+ </div>
755
+ </div>
756
+ </div>
757
+ );
758
+ };
759
+
760
+ /**
761
+ * Generic Linear-style command palette. Knows nothing about routes or models:
762
+ * callers pass a static command catalog (grouped by `category`), optional
763
+ * async search providers for entity results, and control open state.
764
+ *
765
+ * Portals to document.body at the modal z-tier (z-50).
766
+ */
767
+ const CommandPalette: FunctionComponent<ComponentProps> = (
768
+ props: ComponentProps,
769
+ ): ReactElement => {
770
+ /*
771
+ * No hooks in this component on purpose: the early return below would
772
+ * otherwise change hook order between renders. The panel handles all state
773
+ * and mounts fresh on every open.
774
+ */
775
+ if (!props.isOpen) {
776
+ return <></>;
777
+ }
778
+
779
+ return createPortal(
780
+ <CommandPalettePanel
781
+ commands={props.commands}
782
+ searchProviders={props.searchProviders || EMPTY_PROVIDERS}
783
+ onClose={props.onClose}
784
+ recentStorageKey={props.recentStorageKey}
785
+ />,
786
+ document.body,
787
+ );
788
+ };
789
+
790
+ export default CommandPalette;