@cccsaurora/howler-ui 2.19.0-dev.836 → 2.19.0-dev.842

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 (328) hide show
  1. package/api/index.d.ts +4 -0
  2. package/api/index.js +10 -2
  3. package/api/search/case.d.ts +4 -0
  4. package/api/search/case.js +8 -0
  5. package/api/search/facet/hit.d.ts +1 -3
  6. package/api/search/facet/index.d.ts +3 -1
  7. package/api/search/index.d.ts +2 -1
  8. package/api/search/index.js +2 -1
  9. package/api/socket/index.d.ts +3 -0
  10. package/api/socket/index.js +6 -0
  11. package/api/socket/viewers.d.ts +2 -0
  12. package/api/socket/viewers.js +8 -0
  13. package/api/socket/viewers.test.js +44 -0
  14. package/api/v2/case/index.d.ts +9 -0
  15. package/api/v2/case/index.js +21 -0
  16. package/api/v2/case/items.d.ts +6 -0
  17. package/api/v2/case/items.js +18 -0
  18. package/api/v2/case/rules.d.ts +6 -0
  19. package/api/v2/case/rules.js +18 -0
  20. package/api/v2/index.d.ts +4 -0
  21. package/api/v2/index.js +6 -0
  22. package/api/v2/search/facet.d.ts +3 -0
  23. package/api/v2/search/facet.js +12 -0
  24. package/api/v2/search/index.d.ts +5 -0
  25. package/api/v2/search/index.js +24 -0
  26. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  27. package/components/app/App.js +41 -8
  28. package/components/app/hooks/useMatchers.d.ts +1 -1
  29. package/components/app/hooks/useMatchers.js +23 -11
  30. package/components/app/hooks/useMatchers.test.js +22 -22
  31. package/components/app/hooks/useTitle.js +5 -5
  32. package/components/app/providers/FavouritesProvider.js +2 -2
  33. package/components/app/providers/ModalProvider.d.ts +1 -0
  34. package/components/app/providers/ParameterProvider.d.ts +9 -2
  35. package/components/app/providers/ParameterProvider.js +165 -240
  36. package/components/app/providers/ParameterProvider.test.js +346 -94
  37. package/components/app/providers/RecordProvider.d.ts +23 -0
  38. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  39. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  40. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  41. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +52 -71
  42. package/components/app/providers/SocketProvider.d.ts +11 -2
  43. package/components/app/providers/SocketProvider.js +18 -5
  44. package/components/app/providers/UserListProvider.js +28 -8
  45. package/components/elements/ContextMenu.d.ts +56 -0
  46. package/components/elements/ContextMenu.js +109 -0
  47. package/components/elements/ContextMenu.test.d.ts +1 -0
  48. package/components/elements/ContextMenu.test.js +215 -0
  49. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  50. package/components/elements/ObjectDetails.d.ts +6 -0
  51. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  52. package/components/elements/PluginTypography.d.ts +2 -1
  53. package/components/elements/PluginTypography.js +3 -2
  54. package/components/elements/UserList.d.ts +5 -2
  55. package/components/elements/UserList.js +18 -8
  56. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  57. package/components/elements/case/CaseCard.d.ts +12 -0
  58. package/components/elements/case/CaseCard.js +42 -0
  59. package/components/elements/case/CasePreview.d.ts +6 -0
  60. package/components/elements/case/CasePreview.js +17 -0
  61. package/components/elements/case/StatusIcon.d.ts +5 -0
  62. package/components/elements/case/StatusIcon.js +13 -0
  63. package/components/elements/display/ChipPopper.d.ts +1 -1
  64. package/components/elements/display/ChipPopper.js +5 -5
  65. package/components/elements/display/HowlerCard.js +1 -1
  66. package/components/elements/display/Modal.js +2 -0
  67. package/components/elements/hit/HitActions.js +4 -4
  68. package/components/elements/hit/HitBanner.d.ts +1 -0
  69. package/components/elements/hit/HitBanner.js +34 -51
  70. package/components/elements/hit/HitCard.d.ts +2 -0
  71. package/components/elements/hit/HitCard.js +7 -7
  72. package/components/elements/hit/HitLabels.js +2 -2
  73. package/components/elements/hit/HitOutline.d.ts +1 -0
  74. package/components/elements/hit/HitOutline.js +3 -3
  75. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  76. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  77. package/components/elements/hit/HitSummary.d.ts +2 -1
  78. package/components/elements/hit/HitSummary.js +6 -5
  79. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  80. package/components/elements/hit/elements/AnalyticLink.d.ts +9 -0
  81. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  82. package/components/elements/hit/elements/Assigned.js +6 -3
  83. package/components/elements/hit/elements/Assigned.test.d.ts +1 -0
  84. package/components/elements/hit/elements/Assigned.test.js +65 -0
  85. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  86. package/components/elements/hit/related/RelatedRecords.js +63 -0
  87. package/components/elements/observable/ObservableCard.d.ts +6 -0
  88. package/components/elements/observable/ObservableCard.js +22 -0
  89. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  90. package/components/elements/observable/ObservablePreview.js +12 -0
  91. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  92. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  93. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  94. package/components/elements/record/RecordContextMenu.js +268 -0
  95. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  96. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +98 -43
  97. package/components/elements/record/RecordRelated.d.ts +7 -0
  98. package/components/elements/record/RecordRelated.js +34 -0
  99. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  100. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  101. package/components/elements/view/ViewTitle.d.ts +1 -0
  102. package/components/elements/view/ViewTitle.js +9 -2
  103. package/components/hooks/useHitActions.d.ts +1 -1
  104. package/components/hooks/useHitActions.js +4 -4
  105. package/components/hooks/useMyPreferences.js +10 -1
  106. package/components/hooks/useMySearch.js +2 -2
  107. package/components/hooks/useMySitemap.js +4 -1
  108. package/components/hooks/useMyTheme.js +9 -2
  109. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  110. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  111. package/components/hooks/useRelatedRecords.d.ts +13 -0
  112. package/components/hooks/useRelatedRecords.js +32 -0
  113. package/components/routes/action/edit/ActionEditor.js +2 -2
  114. package/components/routes/action/view/ActionSearch.js +1 -1
  115. package/components/routes/advanced/QueryBuilder.js +1 -1
  116. package/components/routes/advanced/QueryEditor.js +3 -3
  117. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  118. package/components/routes/analytics/AnalyticDetails.js +2 -2
  119. package/components/routes/analytics/AnalyticSearch.js +1 -1
  120. package/components/routes/cases/CaseViewer.d.ts +2 -0
  121. package/components/routes/cases/CaseViewer.js +44 -0
  122. package/components/routes/cases/CaseViewer.test.d.ts +1 -0
  123. package/components/routes/cases/CaseViewer.test.js +133 -0
  124. package/components/routes/cases/Cases.d.ts +2 -0
  125. package/components/routes/cases/Cases.js +148 -0
  126. package/components/routes/cases/constants.d.ts +6 -0
  127. package/components/routes/cases/constants.js +6 -0
  128. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  129. package/components/routes/cases/detail/AlertPanel.js +33 -0
  130. package/components/routes/cases/detail/CaseAssets.d.ts +11 -0
  131. package/components/routes/cases/detail/CaseAssets.js +104 -0
  132. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  133. package/components/routes/cases/detail/CaseAssets.test.js +167 -0
  134. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  135. package/components/routes/cases/detail/CaseDashboard.js +66 -0
  136. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  137. package/components/routes/cases/detail/CaseDetails.js +70 -0
  138. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  139. package/components/routes/cases/detail/CaseOverview.js +43 -0
  140. package/components/routes/cases/detail/CaseRules.d.ts +7 -0
  141. package/components/routes/cases/detail/CaseRules.js +57 -0
  142. package/components/routes/cases/detail/CaseRules.test.d.ts +1 -0
  143. package/components/routes/cases/detail/CaseRules.test.js +221 -0
  144. package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
  145. package/components/routes/cases/detail/CaseSidebar.js +107 -0
  146. package/components/routes/cases/detail/CaseSidebar.test.d.ts +1 -0
  147. package/components/routes/cases/detail/CaseSidebar.test.js +266 -0
  148. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  149. package/components/routes/cases/detail/CaseTask.js +66 -0
  150. package/components/routes/cases/detail/CaseTimeline.d.ts +12 -0
  151. package/components/routes/cases/detail/CaseTimeline.js +106 -0
  152. package/components/routes/cases/detail/CaseTimeline.test.d.ts +1 -0
  153. package/components/routes/cases/detail/CaseTimeline.test.js +320 -0
  154. package/components/routes/cases/detail/CreateRuleDialog.d.ts +9 -0
  155. package/components/routes/cases/detail/CreateRuleDialog.js +163 -0
  156. package/components/routes/cases/detail/CreateRuleDialog.test.d.ts +1 -0
  157. package/components/routes/cases/detail/CreateRuleDialog.test.js +259 -0
  158. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  159. package/components/routes/cases/detail/ItemPage.js +95 -0
  160. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  161. package/components/routes/cases/detail/RelatedCasePanel.js +34 -0
  162. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  163. package/components/routes/cases/detail/TaskPanel.js +52 -0
  164. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +11 -0
  165. package/components/routes/cases/detail/aggregates/CaseAggregate.js +24 -0
  166. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  167. package/components/routes/cases/detail/aggregates/SourceAggregate.js +26 -0
  168. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  169. package/components/routes/cases/detail/assets/Asset.js +12 -0
  170. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  171. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  172. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +20 -0
  173. package/components/routes/cases/detail/sidebar/CaseFolder.js +83 -0
  174. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +1 -0
  175. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +295 -0
  176. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
  177. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +103 -0
  178. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
  179. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +363 -0
  180. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +25 -0
  181. package/components/routes/cases/detail/sidebar/FolderEntry.js +88 -0
  182. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +1 -0
  183. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +206 -0
  184. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +5 -0
  185. package/components/routes/cases/detail/sidebar/RootDropZone.js +33 -0
  186. package/components/routes/cases/detail/sidebar/types.d.ts +9 -0
  187. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  188. package/components/routes/cases/detail/sidebar/utils.js +29 -0
  189. package/components/routes/cases/detail/sidebar/utils.test.d.ts +1 -0
  190. package/components/routes/cases/detail/sidebar/utils.test.js +82 -0
  191. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  192. package/components/routes/cases/hooks/useCase.js +69 -0
  193. package/components/routes/cases/hooks/useCase.test.d.ts +1 -0
  194. package/components/routes/cases/hooks/useCase.test.js +141 -0
  195. package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
  196. package/components/routes/cases/modals/AddToCaseModal.js +59 -0
  197. package/components/routes/cases/modals/AddToCaseModal.test.d.ts +1 -0
  198. package/components/routes/cases/modals/AddToCaseModal.test.js +313 -0
  199. package/components/routes/cases/modals/CaseRecordRow.d.ts +9 -0
  200. package/components/routes/cases/modals/CaseRecordRow.js +15 -0
  201. package/components/routes/cases/modals/CreateCaseModal.d.ts +7 -0
  202. package/components/routes/cases/modals/CreateCaseModal.js +55 -0
  203. package/components/routes/cases/modals/CreateCaseModal.test.d.ts +1 -0
  204. package/components/routes/cases/modals/CreateCaseModal.test.js +358 -0
  205. package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
  206. package/components/routes/cases/modals/RenameItemModal.js +48 -0
  207. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  208. package/components/routes/cases/modals/ResolveModal.js +115 -0
  209. package/components/routes/cases/modals/ResolveModal.test.d.ts +1 -0
  210. package/components/routes/cases/modals/ResolveModal.test.js +394 -0
  211. package/components/routes/cases/modals/hooks.d.ts +7 -0
  212. package/components/routes/cases/modals/hooks.js +44 -0
  213. package/components/routes/cases/modals/types.d.ts +5 -0
  214. package/components/routes/cases/search/CaseAssigneeFilter.d.ts +6 -0
  215. package/components/routes/cases/search/CaseAssigneeFilter.js +33 -0
  216. package/components/routes/cases/search/CaseAssigneeFilter.test.d.ts +1 -0
  217. package/components/routes/cases/search/CaseAssigneeFilter.test.js +127 -0
  218. package/components/routes/cases/search/CaseDateFilter.d.ts +13 -0
  219. package/components/routes/cases/search/CaseDateFilter.js +26 -0
  220. package/components/routes/cases/search/CaseDateFilter.test.d.ts +1 -0
  221. package/components/routes/cases/search/CaseDateFilter.test.js +115 -0
  222. package/components/routes/cases/search/CaseStatusFilter.d.ts +6 -0
  223. package/components/routes/cases/search/CaseStatusFilter.js +13 -0
  224. package/components/routes/cases/search/CaseStatusFilter.test.d.ts +1 -0
  225. package/components/routes/cases/search/CaseStatusFilter.test.js +86 -0
  226. package/components/routes/dossiers/DossierEditor.js +2 -2
  227. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  228. package/components/routes/help/ApiDocumentation.js +1 -1
  229. package/components/routes/help/HitBannerDocumentation.js +1 -0
  230. package/components/routes/help/HitDocumentation.js +1 -3
  231. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  232. package/components/routes/hits/search/InformationPane.js +50 -63
  233. package/components/routes/hits/search/LayoutSettings.js +3 -3
  234. package/components/routes/hits/search/QuerySettings.js +2 -1
  235. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  236. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  237. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  238. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  239. package/components/routes/hits/search/SearchPane.js +26 -49
  240. package/components/routes/hits/search/ViewLink.js +3 -3
  241. package/components/routes/hits/search/ViewLink.test.js +8 -8
  242. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  243. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  244. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  245. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  246. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  247. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  248. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  249. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  250. package/components/routes/hits/view/HitViewer.js +12 -13
  251. package/components/routes/home/ViewCard.js +47 -41
  252. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  253. package/components/routes/observables/ObservableViewer.js +27 -0
  254. package/components/routes/overviews/OverviewViewer.js +2 -2
  255. package/components/routes/views/ViewComposer.js +46 -19
  256. package/locales/en/translation.json +123 -3
  257. package/locales/fr/translation.json +121 -3
  258. package/models/WithMetadata.d.ts +2 -1
  259. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  260. package/models/entities/generated/Case.d.ts +28 -0
  261. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  262. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  263. package/models/entities/generated/EmailParent.d.ts +19 -0
  264. package/models/entities/generated/Enrichments.d.ts +7 -0
  265. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  266. package/models/entities/generated/Hit.d.ts +1 -0
  267. package/models/entities/generated/Howler.d.ts +0 -5
  268. package/models/entities/generated/HttpResponse.d.ts +11 -0
  269. package/models/entities/generated/Item.d.ts +9 -0
  270. package/models/entities/generated/Observable.d.ts +85 -0
  271. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  272. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  273. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  274. package/models/entities/generated/ObservableFile.d.ts +36 -0
  275. package/models/entities/generated/ObservableHowler.d.ts +42 -0
  276. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  277. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  278. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  279. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  280. package/models/entities/generated/ObservableSource.d.ts +23 -0
  281. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  282. package/models/entities/generated/ObservableTls.d.ts +12 -0
  283. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  284. package/models/entities/generated/Rule.d.ts +6 -9
  285. package/models/entities/generated/Task.d.ts +10 -0
  286. package/models/entities/generated/Threat.d.ts +2 -2
  287. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  288. package/models/entities/generated/View.d.ts +1 -0
  289. package/models/socket/CaseUpdate.d.ts +5 -0
  290. package/models/socket/ViewersUpdate.d.ts +4 -0
  291. package/package.json +21 -1
  292. package/plugins/clue/components/ClueTypography.js +2 -2
  293. package/plugins/clue/utils.d.ts +2 -1
  294. package/tests/mocks.d.ts +11 -1
  295. package/tests/mocks.js +12 -7
  296. package/tests/server-handlers.js +6 -1
  297. package/tests/utils.d.ts +4 -0
  298. package/tests/utils.js +20 -0
  299. package/utils/constants.d.ts +4 -3
  300. package/utils/constants.js +6 -0
  301. package/utils/hitFunctions.d.ts +2 -1
  302. package/utils/hitFunctions.js +4 -4
  303. package/utils/socketUtils.d.ts +14 -0
  304. package/utils/socketUtils.js +17 -1
  305. package/utils/socketUtils.test.d.ts +1 -0
  306. package/utils/socketUtils.test.js +59 -0
  307. package/utils/typeUtils.d.ts +7 -0
  308. package/utils/typeUtils.js +27 -0
  309. package/utils/viewUtils.js +3 -0
  310. package/components/app/providers/HitProvider.d.ts +0 -22
  311. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  312. package/components/elements/display/icons/BundleButton.js +0 -32
  313. package/components/elements/hit/HitRelated.d.ts +0 -6
  314. package/components/elements/hit/HitRelated.js +0 -7
  315. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  316. package/components/routes/help/BundleDocumentation.js +0 -12
  317. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  318. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  319. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  320. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  321. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  322. package/components/routes/hits/search/BundleScroller.js +0 -6
  323. package/components/routes/hits/search/HitContextMenu.js +0 -239
  324. /package/{components/app/providers/HitSearchProvider.test.d.ts → api/socket/viewers.test.d.ts} +0 -0
  325. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → app/providers/RecordSearchProvider.test.d.ts} +0 -0
  326. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  327. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  328. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -5,9 +5,9 @@ import { cloneDeep } from 'lodash-es';
5
5
  import { setupContextSelectorMock, setupLocalStorageMock } from '@cccsaurora/howler-ui/tests/mocks';
6
6
  import { useContextSelector } from 'use-context-selector';
7
7
  import { DEFAULT_QUERY, MY_LOCAL_STORAGE_PREFIX, StorageKey } from '@cccsaurora/howler-ui/utils/constants';
8
- import { HitContext } from './HitProvider';
9
- import HitSearchProvider, { HitSearchContext } from './HitSearchProvider';
10
8
  import { ParameterContext } from './ParameterProvider';
9
+ import { RecordContext } from './RecordProvider';
10
+ import RecordSearchProvider, { RecordSearchContext } from './RecordSearchProvider';
11
11
  import { ViewContext } from './ViewProvider';
12
12
  vi.mock('api', { spy: true });
13
13
  setupContextSelectorMock();
@@ -30,20 +30,21 @@ let mockParameterContext = {
30
30
  mockParameterContext.offset = parseInt(offset);
31
31
  },
32
32
  views: [],
33
+ indexes: ['hit'],
33
34
  addView: vi.fn()
34
35
  };
35
36
  const originalMockParameterContext = cloneDeep(mockParameterContext);
36
37
  const mockHitContext = {
37
- hits: {},
38
- loadHits: hits => {
39
- mockHitContext.hits = {
40
- ...mockHitContext.hits,
38
+ records: {},
39
+ loadRecords: hits => {
40
+ mockHitContext.records = {
41
+ ...mockHitContext.records,
41
42
  ...Object.fromEntries(hits.map(hit => [hit.howler.id, hit]))
42
43
  };
43
44
  }
44
45
  };
45
46
  const Wrapper = ({ children }) => {
46
- return (_jsx(ViewContext.Provider, { value: mockViewContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: _jsx(HitContext.Provider, { value: mockHitContext, children: _jsx(HitSearchProvider, { children: children }) }) }) }));
47
+ return (_jsx(ViewContext.Provider, { value: mockViewContext, children: _jsx(ParameterContext.Provider, { value: mockParameterContext, children: _jsx(RecordContext.Provider, { value: mockHitContext, children: _jsx(RecordSearchProvider, { children: children }) }) }) }));
47
48
  };
48
49
  beforeEach(() => {
49
50
  mockParameterContext = cloneDeep(originalMockParameterContext);
@@ -57,38 +58,32 @@ beforeEach(() => {
57
58
  let mockSearchParams = new URLSearchParams();
58
59
  vi.mocked(useSearchParams).mockReturnValue([mockSearchParams, mockSetParams]);
59
60
  });
60
- describe('HitSearchContext', () => {
61
+ describe('RecordSearchContext', () => {
61
62
  it('should initialize with default values', async () => {
62
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
63
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
63
64
  displayType: ctx.displayType,
64
65
  searching: ctx.searching,
65
66
  error: ctx.error,
66
67
  response: ctx.response,
67
- bundleId: ctx.bundleId,
68
68
  fzfSearch: ctx.fzfSearch
69
69
  })), { wrapper: Wrapper });
70
70
  expect(hook.result.current.displayType).toBe('list');
71
71
  expect(hook.result.current.searching).toBe(false);
72
72
  expect(hook.result.current.error).toBeNull();
73
73
  expect(hook.result.current.response).toBeNull();
74
- expect(hook.result.current.bundleId).toBeNull();
75
74
  expect(hook.result.current.fzfSearch).toBe(false);
76
75
  });
77
- it('should set bundleId when on bundles route', () => {
78
- mockLocation.pathname = '/bundles/test_bundle_id';
79
- mockParams.mockReturnValue({ id: 'test_bundle_id' });
80
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.bundleId), { wrapper: Wrapper });
81
- expect(hook.result.current).toBe('test_bundle_id');
82
- });
83
76
  it('should initialize queryHistory from localStorage', () => {
84
77
  const mockHistory = { 'test:query': new Date().toISOString() };
85
78
  mockLocalStorage.setItem(`${MY_LOCAL_STORAGE_PREFIX}.${StorageKey.QUERY_HISTORY}`, JSON.stringify(mockHistory));
86
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.queryHistory), { wrapper: Wrapper });
79
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.queryHistory), {
80
+ wrapper: Wrapper
81
+ });
87
82
  expect(hook.result.current).toEqual(mockHistory);
88
83
  });
89
84
  describe('setDisplayType', () => {
90
85
  it('should update display type', () => {
91
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
86
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
92
87
  displayType: ctx.displayType,
93
88
  setDisplayType: ctx.setDisplayType
94
89
  })), { wrapper: Wrapper });
@@ -101,7 +96,7 @@ describe('HitSearchContext', () => {
101
96
  });
102
97
  describe('setFzfSearch', () => {
103
98
  it('should update fzfSearch state', () => {
104
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
99
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
105
100
  fzfSearch: ctx.fzfSearch,
106
101
  setFzfSearch: ctx.setFzfSearch
107
102
  })), { wrapper: Wrapper });
@@ -114,7 +109,7 @@ describe('HitSearchContext', () => {
114
109
  });
115
110
  describe('setQueryHistory', () => {
116
111
  it('should update query history', () => {
117
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
112
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
118
113
  queryHistory: ctx.queryHistory,
119
114
  setQueryHistory: ctx.setQueryHistory
120
115
  })), { wrapper: Wrapper });
@@ -127,7 +122,7 @@ describe('HitSearchContext', () => {
127
122
  });
128
123
  describe('search', () => {
129
124
  it('should perform a search and update response', async () => {
130
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
125
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
131
126
  search: ctx.search,
132
127
  searching: ctx.searching,
133
128
  response: ctx.response,
@@ -137,13 +132,13 @@ describe('HitSearchContext', () => {
137
132
  hook.result.current.search('test query');
138
133
  });
139
134
  await waitFor(() => {
140
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
135
+ expect(hpost).toHaveBeenCalledWith('/api/v2/search/hit', expect.objectContaining({
141
136
  query: expect.stringContaining('test query')
142
137
  }));
143
138
  });
144
139
  });
145
140
  it('should set searching state during search', async () => {
146
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
141
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
147
142
  search: ctx.search,
148
143
  searching: ctx.searching
149
144
  })), { wrapper: Wrapper });
@@ -172,7 +167,7 @@ describe('HitSearchContext', () => {
172
167
  });
173
168
  it('should handle search errors', async () => {
174
169
  vi.mocked(hpost).mockRejectedValueOnce(new Error('Search failed'));
175
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
170
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
176
171
  search: ctx.search,
177
172
  error: ctx.error,
178
173
  searching: ctx.searching
@@ -193,7 +188,7 @@ describe('HitSearchContext', () => {
193
188
  total: 10
194
189
  };
195
190
  vi.mocked(hpost).mockResolvedValueOnce(mockResponse);
196
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
191
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
197
192
  search: ctx.search,
198
193
  response: ctx.response
199
194
  })), { wrapper: Wrapper });
@@ -229,7 +224,7 @@ describe('HitSearchContext', () => {
229
224
  total: 10
230
225
  };
231
226
  vi.mocked(hpost).mockResolvedValueOnce(mockResponse);
232
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
227
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
233
228
  search: ctx.search,
234
229
  response: ctx.response
235
230
  })), { wrapper: Wrapper });
@@ -243,27 +238,13 @@ describe('HitSearchContext', () => {
243
238
  expect(hook.result.current.response?.items[0].howler.id).toBe('hit1');
244
239
  });
245
240
  });
246
- it('should include bundle filter when on bundles route', async () => {
247
- mockLocation.pathname = '/bundles/test_bundle_id';
248
- mockParams.mockReturnValue({ id: 'test_bundle_id' });
249
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
250
- act(() => {
251
- hook.result.current('test query');
252
- });
253
- await waitFor(() => {
254
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
255
- query: 'test query',
256
- filters: ['event.created:[now-1w TO now]', 'howler.bundles:test_bundle_id']
257
- }));
258
- });
259
- });
260
241
  it('should apply date range filter from span', async () => {
261
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
242
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
262
243
  act(() => {
263
244
  hook.result.current('test query');
264
245
  });
265
246
  await waitFor(() => {
266
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
247
+ expect(hpost).toHaveBeenCalledWith('/api/v2/search/hit', expect.objectContaining({
267
248
  filters: expect.arrayContaining([expect.stringContaining('event.created:')])
268
249
  }));
269
250
  });
@@ -272,24 +253,24 @@ describe('HitSearchContext', () => {
272
253
  mockParameterContext.span = 'date.range.custom';
273
254
  mockParameterContext.startDate = '2025-01-01';
274
255
  mockParameterContext.endDate = '2025-12-31';
275
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
256
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
276
257
  act(() => {
277
258
  hook.result.current('test query');
278
259
  });
279
260
  await waitFor(() => {
280
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
261
+ expect(hpost).toHaveBeenCalledWith('/api/v2/search/hit', expect.objectContaining({
281
262
  filters: expect.arrayContaining([expect.stringContaining('event.created:')])
282
263
  }));
283
264
  });
284
265
  });
285
266
  it('should exclude filters ending with * from search', async () => {
286
267
  mockParameterContext.filters = ['status:open', 'howler.escalation:*'];
287
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
268
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
288
269
  act(() => {
289
270
  hook.result.current('test query');
290
271
  });
291
272
  await waitFor(() => {
292
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
273
+ expect(hpost).toHaveBeenCalledWith('/api/v2/search/hit', expect.objectContaining({
293
274
  filters: expect.not.arrayContaining([expect.stringContaining('howler.escalation:*')])
294
275
  }));
295
276
  });
@@ -302,7 +283,7 @@ describe('HitSearchContext', () => {
302
283
  rows: 0,
303
284
  total: 50
304
285
  });
305
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
286
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
306
287
  search: ctx.search
307
288
  })), { wrapper: Wrapper });
308
289
  act(() => {
@@ -316,7 +297,7 @@ describe('HitSearchContext', () => {
316
297
  it('should not search when sort or span is null', async () => {
317
298
  mockParameterContext.sort = null;
318
299
  mockParameterContext.span = null;
319
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
300
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
320
301
  act(() => {
321
302
  hook.result.current('test query');
322
303
  });
@@ -328,7 +309,7 @@ describe('HitSearchContext', () => {
328
309
  });
329
310
  describe('automatic search on parameter changes', () => {
330
311
  it('should trigger search when filters change', async () => {
331
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
312
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
332
313
  response: ctx.response
333
314
  })), { wrapper: Wrapper });
334
315
  await waitFor(() => {
@@ -342,23 +323,23 @@ describe('HitSearchContext', () => {
342
323
  expect(hpost).toHaveBeenCalled();
343
324
  }, { timeout: 2000 });
344
325
  });
345
- it('should not trigger search when query is DEFAULT_QUERY and no bundleId', async () => {
326
+ it('should not trigger search when query is DEFAULT_QUERY', async () => {
346
327
  mockParameterContext.query = DEFAULT_QUERY;
347
- renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
328
+ renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.response), { wrapper: Wrapper });
348
329
  await waitFor(() => {
349
330
  expect(hpost).not.toHaveBeenCalled();
350
331
  });
351
332
  });
352
333
  it('should not trigger search when span is custom but dates are missing', async () => {
353
- renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
334
+ renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.response), { wrapper: Wrapper });
354
335
  await waitFor(() => {
355
336
  expect(hpost).not.toHaveBeenCalled();
356
337
  });
357
338
  });
358
339
  });
359
- describe('useHitSearchContextSelector', () => {
340
+ describe('useRecordSearchContextSelector', () => {
360
341
  it('should allow selecting specific values from context', async () => {
361
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ({
342
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ({
362
343
  searching: ctx.searching,
363
344
  error: ctx.error
364
345
  })), { wrapper: Wrapper });
@@ -368,7 +349,7 @@ describe('HitSearchContext', () => {
368
349
  });
369
350
  describe('edge cases', () => {
370
351
  it('should handle concurrent search calls with throttling', async () => {
371
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
352
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
372
353
  // Make multiple rapid calls
373
354
  act(() => {
374
355
  hook.result.current('query1');
@@ -380,8 +361,8 @@ describe('HitSearchContext', () => {
380
361
  expect(hpost).toHaveBeenCalledTimes(1);
381
362
  }, { timeout: 2000 });
382
363
  });
383
- it('should clear response when query becomes DEFAULT_QUERY without viewId or bundleId', async () => {
384
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
364
+ it('should clear response when query becomes DEFAULT_QUERY without viewId', async () => {
365
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.response), { wrapper: Wrapper });
385
366
  await waitFor(() => {
386
367
  expect(hook.result.current).toBeDefined();
387
368
  }, { timeout: 2000 });
@@ -401,12 +382,12 @@ describe('HitSearchContext', () => {
401
382
  { view_id: 'view_1', query: 'howler.status:open' },
402
383
  { view_id: 'view_2', query: 'howler.priority:high' }
403
384
  ]);
404
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
385
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
405
386
  act(() => {
406
387
  hook.result.current('test query');
407
388
  });
408
389
  await waitFor(() => {
409
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
390
+ expect(hpost).toHaveBeenCalledWith('/api/v2/search/hit', expect.objectContaining({
410
391
  query: 'test query',
411
392
  filters: expect.arrayContaining(['howler.status:open', 'howler.priority:high'])
412
393
  }));
@@ -419,12 +400,12 @@ describe('HitSearchContext', () => {
419
400
  { view_id: 'view_2', query: 'howler.priority:high' },
420
401
  { view_id: 'view_3', query: 'howler.analytic:sigma' }
421
402
  ]);
422
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
403
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
423
404
  act(() => {
424
405
  hook.result.current('test query');
425
406
  });
426
407
  await waitFor(() => {
427
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
408
+ expect(hpost).toHaveBeenCalledWith('/api/v2/search/hit', expect.objectContaining({
428
409
  query: 'test query',
429
410
  filters: [
430
411
  'event.created:[now-1w TO now]',
@@ -443,7 +424,7 @@ describe('HitSearchContext', () => {
443
424
  mockParameterContext.views = [];
444
425
  const mockSearchParams = new URLSearchParams();
445
426
  vi.mocked(useSearchParams).mockReturnValue([mockSearchParams, mockSetParams]);
446
- renderHook(() => useContextSelector(HitSearchContext, () => { }), { wrapper: Wrapper });
427
+ renderHook(() => useContextSelector(RecordSearchContext, () => { }), { wrapper: Wrapper });
447
428
  await waitFor(() => {
448
429
  expect(mockParameterContext.addView).toBeCalledWith('default_view_id');
449
430
  });
@@ -455,7 +436,7 @@ describe('HitSearchContext', () => {
455
436
  const mockSearchParams = new URLSearchParams();
456
437
  mockSearchParams.append('view', 'existing_view');
457
438
  vi.mocked(useSearchParams).mockReturnValue([mockSearchParams, mockSetParams]);
458
- renderHook(() => useContextSelector(HitSearchContext, () => { }), { wrapper: Wrapper });
439
+ renderHook(() => useContextSelector(RecordSearchContext, () => { }), { wrapper: Wrapper });
459
440
  await waitFor(() => {
460
441
  expect(mockParameterContext.addView).not.toBeCalled();
461
442
  });
@@ -466,7 +447,7 @@ describe('HitSearchContext', () => {
466
447
  mockParameterContext.views = [];
467
448
  const mockSearchParams = new URLSearchParams();
468
449
  vi.mocked(useSearchParams).mockReturnValue([mockSearchParams, mockSetParams]);
469
- renderHook(() => useContextSelector(HitSearchContext, () => { }), { wrapper: Wrapper });
450
+ renderHook(() => useContextSelector(RecordSearchContext, () => { }), { wrapper: Wrapper });
470
451
  await waitFor(() => {
471
452
  expect(mockSetParams).not.toHaveBeenCalled();
472
453
  });
@@ -476,12 +457,12 @@ describe('HitSearchContext', () => {
476
457
  it('should not break when view ID does not exist', async () => {
477
458
  mockParameterContext.views = ['non_existent_view'];
478
459
  mockViewContext.getCurrentViews = vi.fn(() => Promise.resolve([null]));
479
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
460
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
480
461
  act(() => {
481
462
  hook.result.current('test query');
482
463
  });
483
464
  await waitFor(() => {
484
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
465
+ expect(hpost).toHaveBeenCalledWith('/api/v2/search/hit', expect.objectContaining({
485
466
  query: expect.stringContaining('test query'),
486
467
  filters: ['event.created:[now-1w TO now]']
487
468
  }));
@@ -493,12 +474,12 @@ describe('HitSearchContext', () => {
493
474
  { view_id: 'view_1', query: 'howler.status:open' },
494
475
  { view_id: 'view_2', query: 'howler.priority:high' }
495
476
  ]);
496
- const hook = renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.search), { wrapper: Wrapper });
477
+ const hook = renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.search), { wrapper: Wrapper });
497
478
  act(() => {
498
479
  hook.result.current('test query');
499
480
  });
500
481
  await waitFor(() => {
501
- expect(hpost).toHaveBeenCalledWith('/api/v1/search/hit', expect.objectContaining({
482
+ expect(hpost).toHaveBeenCalledWith('/api/v2/search/hit', expect.objectContaining({
502
483
  query: 'test query',
503
484
  filters: ['event.created:[now-1w TO now]', 'howler.status:open', 'howler.priority:high']
504
485
  }));
@@ -509,7 +490,7 @@ describe('HitSearchContext', () => {
509
490
  it('should not trigger search when views is empty and query is DEFAULT_QUERY', async () => {
510
491
  mockParameterContext.query = DEFAULT_QUERY;
511
492
  mockParameterContext.views = [];
512
- renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
493
+ renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.response), { wrapper: Wrapper });
513
494
  await waitFor(() => {
514
495
  expect(hpost).not.toHaveBeenCalled();
515
496
  });
@@ -517,7 +498,7 @@ describe('HitSearchContext', () => {
517
498
  it('should trigger search when views.length > 0 even with DEFAULT_QUERY', async () => {
518
499
  mockParameterContext.query = DEFAULT_QUERY;
519
500
  mockParameterContext.views = ['view_1'];
520
- renderHook(() => useContextSelector(HitSearchContext, ctx => ctx.response), { wrapper: Wrapper });
501
+ renderHook(() => useContextSelector(RecordSearchContext, ctx => ctx.response), { wrapper: Wrapper });
521
502
  await waitFor(() => {
522
503
  expect(hpost).toHaveBeenCalled();
523
504
  });
@@ -48,9 +48,18 @@ interface SocketContextType {
48
48
  */
49
49
  reconnect: () => void;
50
50
  /**
51
- * Helper function to tell if the socket is open
51
+ * Helper to tell if the socket is open
52
52
  */
53
- isOpen: () => boolean;
53
+ open: boolean;
54
+ /**
55
+ * A map of entity IDs to their current viewers.
56
+ */
57
+ viewers: Record<string, string[]>;
58
+ /**
59
+ * Fetch the current viewers for an entity via REST, then keep in sync via socket.
60
+ * @param entityId The entity ID to fetch viewers for
61
+ */
62
+ fetchViewers: (entityId: string) => Promise<void>;
54
63
  }
55
64
  export declare const SocketContext: import("react").Context<SocketContextType>;
56
65
  declare const SocketProvider: React.FC<PropsWithChildren>;
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /* eslint-disable no-console */
3
3
  import api from '@cccsaurora/howler-ui/api';
4
+ import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
4
5
  import useMyLocalStorage from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
5
- import { createContext, useCallback, useEffect, useRef, useState } from 'react';
6
+ import { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
7
  import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
7
8
  import { getStored, setAxiosCache, setStored } from '@cccsaurora/howler-ui/utils/sessionStorage';
8
- import { isHitUpdate } from '@cccsaurora/howler-ui/utils/socketUtils';
9
+ import { isHitUpdate, isViewersUpdate } from '@cccsaurora/howler-ui/utils/socketUtils';
9
10
  /**
10
11
  * Enum to help track the status of the Websocket, since the corresponding websocket enums are directly on the object
11
12
  */
@@ -19,6 +20,7 @@ export var Status;
19
20
  export const SocketContext = createContext(null);
20
21
  const SocketProvider = ({ children }) => {
21
22
  const { get } = useMyLocalStorage();
23
+ const { dispatchApi } = useMyApi();
22
24
  // In order to persist the connection through state changes, we use a ref
23
25
  const socket = useRef();
24
26
  // Due to react setState race conditions, listeners are also stored in a ref
@@ -31,6 +33,8 @@ const SocketProvider = ({ children }) => {
31
33
  const [retry, setRetry] = useState(true);
32
34
  // Track the number of failed attempts when connecting to the server
33
35
  const [failedAttempts, setFailedAttempts] = useState(0);
36
+ // Track active viewers per entity ID
37
+ const [viewers, setViewers] = useState({});
34
38
  const onClose = useCallback(e => {
35
39
  // https://www.rfc-editor.org/rfc/rfc6455:
36
40
  // 1006 is a reserved value and MUST NOT be set as a status code in a
@@ -183,7 +187,7 @@ const SocketProvider = ({ children }) => {
183
187
  const addListener = useCallback((key, callback) => {
184
188
  // If a listener with the same key already exists, remove it.
185
189
  if (listeners.current[key]) {
186
- socket.current?.removeEventListener('message', listeners[key]);
190
+ socket.current?.removeEventListener('message', listeners.current[key]);
187
191
  }
188
192
  // We wrap the callback so that all the listeners don't need to JSON.parse the data
189
193
  const wrapped = ev => {
@@ -200,6 +204,9 @@ const SocketProvider = ({ children }) => {
200
204
  api_status_code: parsedData.status
201
205
  });
202
206
  }
207
+ if (isViewersUpdate(parsedData)) {
208
+ setViewers(prev => ({ ...prev, [parsedData.id]: parsedData.viewers }));
209
+ }
203
210
  callback(parsedData);
204
211
  };
205
212
  socket.current?.addEventListener('message', wrapped);
@@ -219,8 +226,14 @@ const SocketProvider = ({ children }) => {
219
226
  }
220
227
  socket.current?.send(data);
221
228
  }, [status]);
222
- const isOpen = useCallback(() => status === Status.OPEN, [status]);
229
+ const open = useMemo(() => status === Status.OPEN, [status]);
223
230
  const reconnect = useCallback(() => setRetry(true), []);
224
- return (_jsx(SocketContext.Provider, { value: { addListener, removeListener, emit, status, reconnect, isOpen }, children: children }));
231
+ const fetchViewers = useCallback(async (entityId) => {
232
+ const result = await dispatchApi(api.socket.viewers.get(entityId), { throwError: false });
233
+ if (result) {
234
+ setViewers(prev => ({ ...prev, [entityId]: result }));
235
+ }
236
+ }, [dispatchApi]);
237
+ return (_jsx(SocketContext.Provider, { value: { addListener, removeListener, emit, status, reconnect, open, viewers, fetchViewers }, children: children }));
225
238
  };
226
239
  export default SocketProvider;
@@ -1,11 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import api from '@cccsaurora/howler-ui/api';
3
3
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
4
- import { createContext, useCallback, useState } from 'react';
4
+ import { createContext, useCallback, useEffect, useRef, useState } from 'react';
5
5
  export const UserListContext = createContext(null);
6
6
  const UserListProvider = ({ children }) => {
7
7
  const { dispatchApi } = useMyApi();
8
8
  const [users, setUsers] = useState({});
9
+ const usersRef = useRef(users);
10
+ usersRef.current = users;
11
+ const pendingIds = useRef(new Set());
12
+ const debounceTimer = useRef(null);
9
13
  const searchUsers = useCallback(async (query) => {
10
14
  const newUsers = (await dispatchApi(api.search.user.post({ query, rows: 1000 }), {
11
15
  throwError: false,
@@ -17,14 +21,30 @@ const UserListProvider = ({ children }) => {
17
21
  ...newUsers
18
22
  }));
19
23
  }, [dispatchApi]);
20
- const fetchUsers = useCallback(async (ids) => {
21
- ids.delete('Unknown');
22
- const idsToGet = Array.from(ids.values()).filter(id => !Object.keys(users).includes(id));
23
- if (idsToGet.length <= 0) {
24
- return;
24
+ const fetchUsers = useCallback((ids) => {
25
+ const nextIds = new Set(ids);
26
+ nextIds.delete('Unknown');
27
+ nextIds.forEach(id => pendingIds.current.add(id));
28
+ if (debounceTimer.current) {
29
+ clearTimeout(debounceTimer.current);
25
30
  }
26
- await searchUsers(`id:${[...idsToGet].join(' OR ')}`);
27
- }, [searchUsers, users]);
31
+ debounceTimer.current = setTimeout(async () => {
32
+ const idsToGet = Array.from(pendingIds.current).filter(id => !Object.keys(usersRef.current).includes(id));
33
+ pendingIds.current = new Set();
34
+ debounceTimer.current = null;
35
+ if (idsToGet.length <= 0) {
36
+ return;
37
+ }
38
+ await searchUsers(`id:${idsToGet.join(' OR ')}`);
39
+ }, 200);
40
+ }, [searchUsers]);
41
+ useEffect(() => {
42
+ return () => {
43
+ if (debounceTimer.current) {
44
+ clearTimeout(debounceTimer.current);
45
+ }
46
+ };
47
+ }, []);
28
48
  return _jsx(UserListContext.Provider, { value: { users, fetchUsers, searchUsers }, children: children });
29
49
  };
30
50
  export default UserListProvider;
@@ -0,0 +1,56 @@
1
+ import { type SxProps } from '@mui/material';
2
+ import type { ElementType, FC, MouseEventHandler, PropsWithChildren, ReactNode } from 'react';
3
+ export type ContextMenuDivider = {
4
+ kind: 'divider';
5
+ id: string;
6
+ sx?: SxProps;
7
+ };
8
+ export type ContextMenuLeafItem = {
9
+ kind: 'item';
10
+ id: string;
11
+ icon?: ReactNode;
12
+ label: ReactNode;
13
+ disabled?: boolean;
14
+ onClick?: () => void;
15
+ /** When provided the item renders as a router Link instead of a button. */
16
+ to?: string;
17
+ };
18
+ export type ContextMenuSubItem = {
19
+ key: string;
20
+ label: ReactNode;
21
+ disabled?: boolean;
22
+ onClick?: () => void;
23
+ };
24
+ export type ContextMenuSubmenuItem = {
25
+ kind: 'submenu';
26
+ /**
27
+ * Identifier for this submenu. Used to derive:
28
+ * - the MenuItem's DOM id (`${id}-menu-item`)
29
+ * - the submenu Paper's DOM id (`${id}-submenu`)
30
+ */
31
+ id: string;
32
+ icon?: ReactNode;
33
+ label: ReactNode;
34
+ disabled?: boolean;
35
+ items: ContextMenuSubItem[];
36
+ };
37
+ export type ContextMenuEntry = ContextMenuDivider | ContextMenuLeafItem | ContextMenuSubmenuItem;
38
+ interface ContextMenuProps {
39
+ items: ContextMenuEntry[];
40
+ /** Called after the menu opens, with the triggering event. */
41
+ onOpen?: MouseEventHandler<HTMLElement>;
42
+ /** Called when the menu closes. */
43
+ onClose?: () => void;
44
+ /** Wraps children + menu in this element. Defaults to Box. */
45
+ Component?: ElementType;
46
+ /** id applied to the wrapper element */
47
+ id?: string;
48
+ }
49
+ /**
50
+ * Generic context menu component that renders a MUI Menu from a declarative
51
+ * items structure supporting leaf items, dividers, and single-level submenus.
52
+ *
53
+ * Submenus appear on hover and are positioned to avoid screen overflow.
54
+ */
55
+ declare const ContextMenu: FC<PropsWithChildren<ContextMenuProps>>;
56
+ export default ContextMenu;