@cccsaurora/howler-ui 2.18.0-dev.736 → 2.18.0-dev.737

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 (273) hide show
  1. package/api/index.d.ts +0 -2
  2. package/api/index.js +2 -4
  3. package/api/search/facet/hit.d.ts +3 -1
  4. package/api/search/facet/index.d.ts +1 -3
  5. package/api/search/index.d.ts +1 -2
  6. package/api/search/index.js +1 -2
  7. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  8. package/components/app/App.js +7 -39
  9. package/components/app/hooks/useMatchers.d.ts +1 -1
  10. package/components/app/hooks/useMatchers.js +11 -23
  11. package/components/app/hooks/useMatchers.test.js +22 -22
  12. package/components/app/hooks/useTitle.js +3 -3
  13. package/components/app/providers/FavouritesProvider.js +2 -2
  14. package/components/app/providers/HitProvider.d.ts +22 -0
  15. package/components/app/providers/{RecordProvider.js → HitProvider.js} +41 -41
  16. package/components/app/providers/{RecordSearchProvider.d.ts → HitSearchProvider.d.ts} +6 -6
  17. package/components/app/providers/{RecordSearchProvider.js → HitSearchProvider.js} +17 -12
  18. package/components/app/providers/{RecordSearchProvider.test.js → HitSearchProvider.test.js} +70 -51
  19. package/components/app/providers/ModalProvider.d.ts +0 -1
  20. package/components/app/providers/ParameterProvider.d.ts +2 -9
  21. package/components/app/providers/ParameterProvider.js +240 -165
  22. package/components/app/providers/ParameterProvider.test.js +94 -346
  23. package/components/app/providers/UserListProvider.js +8 -28
  24. package/components/elements/PluginTypography.d.ts +1 -2
  25. package/components/elements/PluginTypography.js +2 -3
  26. package/components/elements/UserList.d.ts +2 -5
  27. package/components/elements/UserList.js +8 -18
  28. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  29. package/components/elements/display/ChipPopper.d.ts +1 -1
  30. package/components/elements/display/HowlerCard.js +1 -1
  31. package/components/elements/display/Modal.js +0 -2
  32. package/components/elements/display/icons/BundleButton.d.ts +6 -0
  33. package/components/elements/display/icons/BundleButton.js +32 -0
  34. package/components/elements/hit/HitActions.js +4 -4
  35. package/components/elements/hit/HitBanner.d.ts +0 -1
  36. package/components/elements/hit/HitBanner.js +49 -29
  37. package/components/elements/hit/HitCard.d.ts +0 -2
  38. package/components/elements/hit/HitCard.js +7 -7
  39. package/components/elements/{record/RecordComments.d.ts → hit/HitComments.d.ts} +4 -5
  40. package/components/elements/{record/RecordComments.js → hit/HitComments.js} +28 -29
  41. package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
  42. package/components/elements/hit/HitLabels.js +2 -2
  43. package/components/elements/hit/HitOutline.d.ts +0 -1
  44. package/components/elements/hit/HitOutline.js +3 -3
  45. package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
  46. package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
  47. package/components/elements/hit/HitRelated.d.ts +6 -0
  48. package/components/elements/hit/HitRelated.js +7 -0
  49. package/components/elements/hit/HitSummary.d.ts +1 -2
  50. package/components/elements/hit/HitSummary.js +5 -6
  51. package/components/elements/{record/RecordWorklog.d.ts → hit/HitWorklog.d.ts} +3 -4
  52. package/components/elements/{record/RecordWorklog.js → hit/HitWorklog.js} +13 -15
  53. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  54. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  55. package/components/elements/view/ViewTitle.d.ts +0 -1
  56. package/components/elements/view/ViewTitle.js +2 -9
  57. package/components/hooks/useHitActions.d.ts +1 -1
  58. package/components/hooks/useHitActions.js +4 -4
  59. package/components/hooks/{useRecordSelection.d.ts → useHitSelection.d.ts} +2 -2
  60. package/components/hooks/{useRecordSelection.js → useHitSelection.js} +33 -12
  61. package/components/hooks/useMyPreferences.js +1 -10
  62. package/components/hooks/useMySearch.js +2 -2
  63. package/components/hooks/useMySitemap.js +1 -4
  64. package/components/hooks/useMyTheme.js +2 -9
  65. package/components/hooks/useParamState.test.js +4 -3
  66. package/components/routes/action/edit/ActionEditor.js +2 -2
  67. package/components/routes/action/view/ActionSearch.js +1 -1
  68. package/components/routes/advanced/QueryBuilder.js +1 -1
  69. package/components/routes/advanced/QueryEditor.js +3 -3
  70. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  71. package/components/routes/analytics/AnalyticDetails.js +2 -2
  72. package/components/routes/analytics/AnalyticSearch.js +1 -1
  73. package/components/routes/dossiers/DossierEditor.js +2 -2
  74. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  75. package/components/routes/help/ApiDocumentation.js +1 -1
  76. package/components/routes/help/BundleDocumentation.d.ts +3 -0
  77. package/components/routes/help/BundleDocumentation.js +12 -0
  78. package/components/routes/help/HitBannerDocumentation.js +0 -1
  79. package/components/routes/help/HitDocumentation.js +3 -1
  80. package/components/routes/help/markdown/en/bundles.md.js +1 -0
  81. package/components/routes/help/markdown/fr/bundles.md.js +1 -0
  82. package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
  83. package/components/routes/hits/search/BundleParentMenu.js +32 -0
  84. package/components/routes/hits/search/BundleScroller.d.ts +2 -0
  85. package/components/routes/hits/search/BundleScroller.js +6 -0
  86. package/components/routes/hits/search/{RecordBrowser.js → HitBrowser.js} +9 -9
  87. package/components/{elements/record/RecordContextMenu.d.ts → routes/hits/search/HitContextMenu.d.ts} +3 -3
  88. package/components/routes/hits/search/HitContextMenu.js +227 -0
  89. package/components/{elements/record/RecordContextMenu.test.js → routes/hits/search/HitContextMenu.test.js} +39 -94
  90. package/components/routes/hits/search/{RecordQuery.d.ts → HitQuery.d.ts} +2 -2
  91. package/components/routes/hits/search/{RecordQuery.js → HitQuery.js} +6 -6
  92. package/components/routes/hits/search/InformationPane.d.ts +0 -1
  93. package/components/routes/hits/search/InformationPane.js +60 -47
  94. package/components/routes/hits/search/LayoutSettings.js +3 -3
  95. package/components/routes/hits/search/QuerySettings.js +1 -2
  96. package/components/routes/hits/search/QuerySettings.test.js +9 -14
  97. package/components/routes/hits/search/SearchPane.js +49 -26
  98. package/components/routes/hits/search/ViewLink.js +3 -3
  99. package/components/routes/hits/search/ViewLink.test.js +8 -8
  100. package/components/routes/hits/search/grid/AddColumnModal.js +4 -5
  101. package/components/routes/hits/search/grid/EnhancedCell.d.ts +1 -2
  102. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  103. package/components/routes/hits/search/grid/HitGrid.js +18 -20
  104. package/components/routes/hits/search/grid/{RecordRow.d.ts → HitRow.d.ts} +2 -3
  105. package/components/routes/hits/search/grid/{RecordRow.js → HitRow.js} +8 -10
  106. package/components/routes/hits/view/HitViewer.js +13 -12
  107. package/components/routes/home/ViewCard.js +41 -47
  108. package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
  109. package/components/routes/overviews/OverviewViewer.js +2 -2
  110. package/components/routes/views/ViewComposer.js +19 -46
  111. package/locales/en/translation.json +3 -89
  112. package/locales/fr/translation.json +3 -87
  113. package/models/WithMetadata.d.ts +1 -2
  114. package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
  115. package/models/entities/generated/Hit.d.ts +0 -1
  116. package/models/entities/generated/Howler.d.ts +4 -0
  117. package/models/entities/generated/Rule.d.ts +10 -2
  118. package/models/entities/generated/Threat.d.ts +2 -2
  119. package/models/entities/generated/View.d.ts +0 -1
  120. package/package.json +2 -19
  121. package/plugins/clue/components/ClueTypography.js +2 -2
  122. package/plugins/clue/utils.d.ts +1 -2
  123. package/tests/mocks.d.ts +1 -11
  124. package/tests/mocks.js +7 -12
  125. package/tests/server-handlers.js +1 -6
  126. package/tests/utils.d.ts +0 -4
  127. package/tests/utils.js +0 -20
  128. package/utils/constants.d.ts +3 -3
  129. package/utils/hitFunctions.d.ts +1 -2
  130. package/utils/hitFunctions.js +4 -4
  131. package/utils/viewUtils.js +0 -3
  132. package/api/search/case.d.ts +0 -4
  133. package/api/search/case.js +0 -8
  134. package/api/v2/case/index.d.ts +0 -8
  135. package/api/v2/case/index.js +0 -20
  136. package/api/v2/case/items.d.ts +0 -6
  137. package/api/v2/case/items.js +0 -18
  138. package/api/v2/index.d.ts +0 -4
  139. package/api/v2/index.js +0 -6
  140. package/api/v2/search/facet.d.ts +0 -3
  141. package/api/v2/search/facet.js +0 -12
  142. package/api/v2/search/index.d.ts +0 -5
  143. package/api/v2/search/index.js +0 -24
  144. package/components/app/providers/RecordProvider.d.ts +0 -23
  145. package/components/elements/ContextMenu.d.ts +0 -56
  146. package/components/elements/ContextMenu.js +0 -109
  147. package/components/elements/ContextMenu.test.js +0 -215
  148. package/components/elements/ObjectDetails.d.ts +0 -6
  149. package/components/elements/case/CaseCard.d.ts +0 -12
  150. package/components/elements/case/CaseCard.js +0 -42
  151. package/components/elements/case/CasePreview.d.ts +0 -6
  152. package/components/elements/case/CasePreview.js +0 -17
  153. package/components/elements/case/StatusIcon.d.ts +0 -5
  154. package/components/elements/case/StatusIcon.js +0 -13
  155. package/components/elements/hit/elements/AnalyticLink.d.ts +0 -9
  156. package/components/elements/hit/elements/AnalyticLink.js +0 -22
  157. package/components/elements/hit/related/RelatedRecords.js +0 -63
  158. package/components/elements/observable/ObservableCard.d.ts +0 -6
  159. package/components/elements/observable/ObservableCard.js +0 -22
  160. package/components/elements/observable/ObservablePreview.d.ts +0 -6
  161. package/components/elements/observable/ObservablePreview.js +0 -12
  162. package/components/elements/record/RecordContextMenu.js +0 -247
  163. package/components/elements/record/RecordContextMenu.test.d.ts +0 -1
  164. package/components/elements/record/RecordRelated.d.ts +0 -7
  165. package/components/elements/record/RecordRelated.js +0 -34
  166. package/components/hooks/useRelatedRecords.d.ts +0 -13
  167. package/components/hooks/useRelatedRecords.js +0 -32
  168. package/components/routes/cases/CaseViewer.d.ts +0 -2
  169. package/components/routes/cases/CaseViewer.js +0 -22
  170. package/components/routes/cases/Cases.d.ts +0 -2
  171. package/components/routes/cases/Cases.js +0 -101
  172. package/components/routes/cases/constants.d.ts +0 -5
  173. package/components/routes/cases/constants.js +0 -5
  174. package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
  175. package/components/routes/cases/detail/AlertPanel.js +0 -33
  176. package/components/routes/cases/detail/CaseAssets.d.ts +0 -11
  177. package/components/routes/cases/detail/CaseAssets.js +0 -104
  178. package/components/routes/cases/detail/CaseAssets.test.d.ts +0 -1
  179. package/components/routes/cases/detail/CaseAssets.test.js +0 -167
  180. package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
  181. package/components/routes/cases/detail/CaseDashboard.js +0 -66
  182. package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
  183. package/components/routes/cases/detail/CaseDetails.js +0 -61
  184. package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
  185. package/components/routes/cases/detail/CaseOverview.js +0 -43
  186. package/components/routes/cases/detail/CaseSidebar.d.ts +0 -8
  187. package/components/routes/cases/detail/CaseSidebar.js +0 -107
  188. package/components/routes/cases/detail/CaseSidebar.test.d.ts +0 -1
  189. package/components/routes/cases/detail/CaseSidebar.test.js +0 -246
  190. package/components/routes/cases/detail/CaseTask.d.ts +0 -11
  191. package/components/routes/cases/detail/CaseTask.js +0 -57
  192. package/components/routes/cases/detail/CaseTimeline.d.ts +0 -12
  193. package/components/routes/cases/detail/CaseTimeline.js +0 -106
  194. package/components/routes/cases/detail/CaseTimeline.test.d.ts +0 -1
  195. package/components/routes/cases/detail/CaseTimeline.test.js +0 -227
  196. package/components/routes/cases/detail/ItemPage.d.ts +0 -6
  197. package/components/routes/cases/detail/ItemPage.js +0 -99
  198. package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
  199. package/components/routes/cases/detail/RelatedCasePanel.js +0 -34
  200. package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
  201. package/components/routes/cases/detail/TaskPanel.js +0 -52
  202. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -11
  203. package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -24
  204. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
  205. package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -26
  206. package/components/routes/cases/detail/assets/Asset.d.ts +0 -14
  207. package/components/routes/cases/detail/assets/Asset.js +0 -12
  208. package/components/routes/cases/detail/assets/Asset.test.d.ts +0 -1
  209. package/components/routes/cases/detail/assets/Asset.test.js +0 -72
  210. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -20
  211. package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -83
  212. package/components/routes/cases/detail/sidebar/CaseFolder.test.d.ts +0 -1
  213. package/components/routes/cases/detail/sidebar/CaseFolder.test.js +0 -295
  214. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +0 -34
  215. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +0 -103
  216. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +0 -1
  217. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +0 -363
  218. package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +0 -25
  219. package/components/routes/cases/detail/sidebar/FolderEntry.js +0 -88
  220. package/components/routes/cases/detail/sidebar/FolderEntry.test.d.ts +0 -1
  221. package/components/routes/cases/detail/sidebar/FolderEntry.test.js +0 -206
  222. package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +0 -5
  223. package/components/routes/cases/detail/sidebar/RootDropZone.js +0 -33
  224. package/components/routes/cases/detail/sidebar/types.d.ts +0 -9
  225. package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
  226. package/components/routes/cases/detail/sidebar/utils.js +0 -29
  227. package/components/routes/cases/detail/sidebar/utils.test.d.ts +0 -1
  228. package/components/routes/cases/detail/sidebar/utils.test.js +0 -82
  229. package/components/routes/cases/hooks/useCase.d.ts +0 -13
  230. package/components/routes/cases/hooks/useCase.js +0 -51
  231. package/components/routes/cases/modals/AddToCaseModal.d.ts +0 -7
  232. package/components/routes/cases/modals/AddToCaseModal.js +0 -62
  233. package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
  234. package/components/routes/cases/modals/RenameItemModal.js +0 -48
  235. package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
  236. package/components/routes/cases/modals/ResolveModal.js +0 -115
  237. package/components/routes/cases/modals/ResolveModal.test.d.ts +0 -1
  238. package/components/routes/cases/modals/ResolveModal.test.js +0 -384
  239. package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
  240. package/components/routes/hits/search/shared/IndexPicker.js +0 -20
  241. package/components/routes/observables/ObservableViewer.d.ts +0 -7
  242. package/components/routes/observables/ObservableViewer.js +0 -27
  243. package/models/entities/generated/AttachmentsFile.d.ts +0 -12
  244. package/models/entities/generated/Case.d.ts +0 -28
  245. package/models/entities/generated/DestinationOriginal.d.ts +0 -19
  246. package/models/entities/generated/EmailAttachment.d.ts +0 -8
  247. package/models/entities/generated/EmailParent.d.ts +0 -19
  248. package/models/entities/generated/Enrichments.d.ts +0 -7
  249. package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
  250. package/models/entities/generated/HttpResponse.d.ts +0 -11
  251. package/models/entities/generated/Item.d.ts +0 -9
  252. package/models/entities/generated/Observable.d.ts +0 -85
  253. package/models/entities/generated/ObservableCloud.d.ts +0 -20
  254. package/models/entities/generated/ObservableDestination.d.ts +0 -23
  255. package/models/entities/generated/ObservableEmail.d.ts +0 -30
  256. package/models/entities/generated/ObservableFile.d.ts +0 -36
  257. package/models/entities/generated/ObservableHowler.d.ts +0 -43
  258. package/models/entities/generated/ObservableHttp.d.ts +0 -11
  259. package/models/entities/generated/ObservableObserver.d.ts +0 -21
  260. package/models/entities/generated/ObservableOrganization.d.ts +0 -7
  261. package/models/entities/generated/ObservableProcess.d.ts +0 -34
  262. package/models/entities/generated/ObservableSource.d.ts +0 -23
  263. package/models/entities/generated/ObservableThreat.d.ts +0 -21
  264. package/models/entities/generated/ObservableTls.d.ts +0 -12
  265. package/models/entities/generated/ObserverIngress.d.ts +0 -9
  266. package/models/entities/generated/Task.d.ts +0 -10
  267. package/utils/typeUtils.d.ts +0 -7
  268. package/utils/typeUtils.js +0 -27
  269. /package/components/app/providers/{RecordSearchProvider.test.d.ts → HitSearchProvider.test.d.ts} +0 -0
  270. /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
  271. /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
  272. /package/components/{elements/ContextMenu.test.d.ts → routes/hits/search/HitContextMenu.test.d.ts} +0 -0
  273. /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
@@ -1,247 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { AddCircleOutline, Assignment, CreateNewFolder, Edit, HowToVote, OpenInNew, QueryStats, RemoveCircleOutline, SettingsSuggest, Terminal } from '@mui/icons-material';
3
- import api from '@cccsaurora/howler-ui/api';
4
- import useMatchers from '@cccsaurora/howler-ui/components/app/hooks/useMatchers';
5
- import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
6
- import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
7
- import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
8
- import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
9
- import ContextMenu, {} from '@cccsaurora/howler-ui/components/elements/ContextMenu';
10
- import { TOP_ROW, VOTE_OPTIONS } from '@cccsaurora/howler-ui/components/elements/hit/actions/SharedComponents';
11
- import useHitActions from '@cccsaurora/howler-ui/components/hooks/useHitActions';
12
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
13
- import useMyActionFunctions from '@cccsaurora/howler-ui/components/routes/action/useMyActionFunctions';
14
- import AddToCaseModal from '@cccsaurora/howler-ui/components/routes/cases/modals/AddToCaseModal';
15
- import { capitalize, get, groupBy, isEmpty, toString } from 'lodash-es';
16
- import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
17
- import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
18
- import { useTranslation } from 'react-i18next';
19
- import { usePluginStore } from 'react-pluggable';
20
- import { useContextSelector } from 'use-context-selector';
21
- import { DEFAULT_QUERY } from '@cccsaurora/howler-ui/utils/constants';
22
- import { sanitizeLuceneQuery } from '@cccsaurora/howler-ui/utils/stringUtils';
23
- import { isHit } from '@cccsaurora/howler-ui/utils/typeUtils';
24
- /**
25
- * Order in which action types should be displayed in the context menu
26
- */
27
- const ORDER = ['assessment', 'vote', 'action'];
28
- /**
29
- * Icon mapping for different action types
30
- */
31
- const ICON_MAP = {
32
- assessment: _jsx(Assignment, {}),
33
- vote: _jsx(HowToVote, {}),
34
- action: _jsx(Edit, {})
35
- };
36
- /**
37
- * Context menu component for hit operations.
38
- * Provides quick access to common hit actions including assessment, voting,
39
- * transitions, and exclusion filters based on template fields.
40
- */
41
- const RecordContextMenu = ({ children, getSelectedId, Component }) => {
42
- const { t } = useTranslation();
43
- const { dispatchApi } = useMyApi();
44
- const { executeAction } = useMyActionFunctions();
45
- const { config } = useContext(ApiConfigContext);
46
- const { showModal } = useContext(ModalContext);
47
- const pluginStore = usePluginStore();
48
- const { getMatchingAnalytic, getMatchingTemplate } = useMatchers();
49
- const query = useContextSelector(ParameterContext, ctx => ctx?.query);
50
- const setQuery = useContextSelector(ParameterContext, ctx => ctx?.setQuery);
51
- const [id, setId] = useState(null);
52
- const record = useContextSelector(RecordContext, ctx => ctx.records[id]);
53
- const selectedRecords = useContextSelector(RecordContext, ctx => ctx.selectedRecords);
54
- const [analytic, setAnalytic] = useState(null);
55
- const [template, setTemplate] = useState(null);
56
- const [actions, setActions] = useState([]);
57
- const records = useMemo(() => selectedRecords.some(_record => _record.howler.id === record?.howler.id)
58
- ? selectedRecords
59
- : record
60
- ? [record]
61
- : [], [record, selectedRecords]);
62
- const hits = useMemo(() => records.filter(isHit), [records]);
63
- const { availableTransitions, canVote, canAssess, assess, vote } = useHitActions(hits);
64
- /**
65
- * Called by ContextMenu after the menu is positioned and opened.
66
- * Identifies the clicked record and fetches available actions.
67
- */
68
- const onOpen = useCallback(async (event) => {
69
- const _id = getSelectedId(event);
70
- setId(_id);
71
- const _actions = (await dispatchApi(api.search.action.post({ query: 'action_id:*' }), { throwError: false }))
72
- ?.items;
73
- if (_actions) {
74
- setActions(_actions);
75
- }
76
- }, [dispatchApi, getSelectedId]);
77
- const rowStatus = useMemo(() => ({
78
- assessment: canAssess,
79
- vote: canVote
80
- }), [canAssess, canVote]);
81
- const pluginActions = howlerPluginStore.plugins.flatMap(plugin => pluginStore.executeFunction(`${plugin}.actions`, records));
82
- /**
83
- * Generates grouped action entries for the context menu.
84
- * Combines transitions, plugin actions, votes, and assessments based on permissions.
85
- */
86
- const entries = useMemo(() => {
87
- let _actions = [...availableTransitions, ...pluginActions];
88
- if (canVote) {
89
- _actions = [
90
- ..._actions,
91
- ...VOTE_OPTIONS.map(option => ({ ...option, actionFunction: () => vote(option.name.toLowerCase()) }))
92
- ];
93
- }
94
- if (config.lookups?.['howler.assessment'] && canAssess) {
95
- _actions = [
96
- ..._actions,
97
- ...config.lookups['howler.assessment']
98
- .filter(_assessment => analytic?.triage_settings?.valid_assessments
99
- ? analytic.triage_settings?.valid_assessments.includes(_assessment)
100
- : true)
101
- .sort((a, b) => +TOP_ROW.includes(b) - +TOP_ROW.includes(a))
102
- .map(assessment => ({
103
- type: 'assessment',
104
- name: assessment,
105
- actionFunction: async () => {
106
- await assess(assessment, analytic?.triage_settings?.skip_rationale);
107
- }
108
- }))
109
- ];
110
- }
111
- return Object.entries(groupBy(_actions, 'type')).sort(([a], [b]) => ORDER.indexOf(a) - ORDER.indexOf(b));
112
- }, [analytic, assess, availableTransitions, canAssess, canVote, config.lookups, vote, pluginActions]);
113
- // Load analytic and template data when a hit is selected
114
- useEffect(() => {
115
- if (!record?.howler.analytic) {
116
- return;
117
- }
118
- getMatchingAnalytic(record).then(setAnalytic);
119
- getMatchingTemplate(record).then(setTemplate);
120
- // eslint-disable-next-line react-hooks/exhaustive-deps
121
- }, [record]);
122
- /**
123
- * Builds the declarative items structure for the ContextMenu component.
124
- */
125
- const items = useMemo(() => {
126
- const result = [
127
- {
128
- kind: 'item',
129
- id: 'open-record',
130
- icon: _jsx(OpenInNew, {}),
131
- label: t(`${record?.__index ?? 'hit'}.open`),
132
- disabled: !record,
133
- to: `/${record?.__index}s/${record?.howler.id}`
134
- }
135
- ];
136
- if (isHit(record)) {
137
- result.push({
138
- kind: 'item',
139
- id: 'open-analytic',
140
- icon: _jsx(QueryStats, {}),
141
- label: t('analytic.open'),
142
- disabled: !analytic,
143
- to: `/analytics/${analytic?.analytic_id}`
144
- });
145
- result.push({ kind: 'divider', id: 'actions-divider' });
146
- for (const [type, typeItems] of entries) {
147
- result.push({
148
- kind: 'submenu',
149
- id: type,
150
- icon: ICON_MAP[type] ?? _jsx(Terminal, {}),
151
- label: t(`hit.details.actions.${type}`),
152
- disabled: rowStatus[type] === false,
153
- items: typeItems.map(a => ({
154
- key: a.name,
155
- label: a.i18nKey ? t(a.i18nKey) : capitalize(a.name),
156
- onClick: a.actionFunction
157
- }))
158
- });
159
- }
160
- result.push({
161
- kind: 'submenu',
162
- id: 'actions',
163
- icon: _jsx(SettingsSuggest, {}),
164
- label: t('route.actions.change'),
165
- disabled: actions.length < 1,
166
- items: actions.map(action => ({
167
- key: action.action_id,
168
- label: action.name,
169
- onClick: () => executeAction(action.action_id, `howler.id:${record?.howler.id}`)
170
- }))
171
- });
172
- if (!isEmpty(template?.keys ?? []) && setQuery) {
173
- result.push({ kind: 'divider', id: 'filter-divider' });
174
- result.push({
175
- kind: 'submenu',
176
- id: 'excludes',
177
- icon: _jsx(RemoveCircleOutline, {}),
178
- label: t('hit.panel.exclude'),
179
- items: (template?.keys ?? []).flatMap(key => {
180
- let newQuery = '';
181
- if (query !== DEFAULT_QUERY) {
182
- newQuery = `(${query}) AND `;
183
- }
184
- const value = get(record, key);
185
- if (!value) {
186
- return [];
187
- }
188
- else if (Array.isArray(value)) {
189
- const sanitizedValues = value
190
- .map(toString)
191
- .filter(val => !!val)
192
- .map(val => `"${sanitizeLuceneQuery(val)}"`);
193
- if (sanitizedValues.length < 1) {
194
- return [];
195
- }
196
- newQuery += `-${key}:(${sanitizedValues.join(' OR ')})`;
197
- }
198
- else {
199
- newQuery += `-${key}:"${sanitizeLuceneQuery(value.toString())}"`;
200
- }
201
- return [{ key, label: key, onClick: () => setQuery(newQuery) }];
202
- })
203
- });
204
- result.push({
205
- kind: 'submenu',
206
- id: 'includes',
207
- icon: _jsx(AddCircleOutline, {}),
208
- label: t('hit.panel.include'),
209
- items: (template?.keys ?? []).flatMap(key => {
210
- let newQuery = `(${query}) AND `;
211
- const value = get(record, key);
212
- if (!value) {
213
- return [];
214
- }
215
- else if (Array.isArray(value)) {
216
- const sanitizedValues = value
217
- .map(toString)
218
- .filter(val => !!val)
219
- .map(val => `"${sanitizeLuceneQuery(val)}"`);
220
- if (sanitizedValues.length < 1) {
221
- return [];
222
- }
223
- newQuery += `${key}:(${sanitizedValues.join(' OR ')})`;
224
- }
225
- else {
226
- newQuery += `${key}:"${sanitizeLuceneQuery(value.toString())}"`;
227
- }
228
- return [{ key, label: key, onClick: () => setQuery(newQuery) }];
229
- })
230
- });
231
- }
232
- }
233
- result.push({ kind: 'divider', id: 'add-to-case-divider' });
234
- result.push({
235
- kind: 'item',
236
- id: 'add-to-case',
237
- icon: _jsx(CreateNewFolder, {}),
238
- label: t('modal.cases.add_to_case'),
239
- disabled: !record,
240
- onClick: () => showModal(_jsx(AddToCaseModal, { records: records }))
241
- });
242
- return result;
243
- // eslint-disable-next-line react-hooks/exhaustive-deps
244
- }, [record, analytic, template, entries, rowStatus, actions, query, t, setQuery, executeAction, showModal, records]);
245
- return (_jsx(ContextMenu, { id: "contextMenu", Component: Component, onOpen: onOpen, onClose: () => setAnalytic(null), items: items, children: children }));
246
- };
247
- export default RecordContextMenu;
@@ -1,7 +0,0 @@
1
- import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
- import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
3
- import { type FC } from 'react';
4
- declare const RecordRelated: FC<{
5
- record: Hit | Observable;
6
- }>;
7
- export default RecordRelated;
@@ -1,34 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Stack, Tab, Tabs, useTheme } from '@mui/material';
3
- import ObservableCard from '@cccsaurora/howler-ui/components/elements/observable/ObservableCard';
4
- import useRelatedRecords from '@cccsaurora/howler-ui/components/hooks/useRelatedRecords';
5
- import { groupBy } from 'lodash-es';
6
- import { useMemo, useState } from 'react';
7
- import { useTranslation } from 'react-i18next';
8
- import { Link } from 'react-router-dom';
9
- import { isCase, isHit, isObservable } from '@cccsaurora/howler-ui/utils/typeUtils';
10
- import CaseCard from '../case/CaseCard';
11
- import HitCard from '../hit/HitCard';
12
- import { HitLayout } from '../hit/HitLayout';
13
- import RelatedLink from '../hit/related/RelatedLink';
14
- const RecordRelated = ({ record }) => {
15
- const theme = useTheme();
16
- const { t } = useTranslation();
17
- const related = useMemo(() => record?.howler.related ?? [], [record?.howler.related]);
18
- const records = useRelatedRecords(related, related.length > 0);
19
- const groups = groupBy(records, '__index');
20
- const hasLinks = (record?.howler.links?.length ?? 0) > 0;
21
- const tabs = [
22
- hasLinks && 'links',
23
- groups.hit?.length > 0 && 'hit',
24
- groups.case?.length > 0 && 'case',
25
- groups.observable?.length > 0 && 'observable'
26
- ].filter(Boolean);
27
- const [activeTab, setActiveTab] = useState(false);
28
- const currentTab = activeTab !== false && tabs.includes(activeTab) ? activeTab : (tabs[0] ?? false);
29
- if (!record) {
30
- return null;
31
- }
32
- return (_jsxs(Box, { sx: { borderTop: `thin solid ${theme.palette.divider}`, height: '100%', flex: 1, mr: 2, pb: 2 }, children: [_jsxs(Tabs, { value: currentTab, onChange: (_, v) => setActiveTab(v), variant: "scrollable", scrollButtons: "auto", children: [hasLinks && _jsx(Tab, { value: "links", label: t('hit.related.tab.links') }), groups.hit?.length > 0 && _jsx(Tab, { value: "hit", label: t('hit.related.tab.hit') }), groups.case?.length > 0 && _jsx(Tab, { value: "case", label: t('hit.related.tab.case') }), groups.observable?.length > 0 && _jsx(Tab, { value: "observable", label: t('hit.related.tab.observable') })] }), currentTab === 'links' && (_jsx(Box, { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 1, pt: 1, children: record.howler.links.map(l => (_jsx(RelatedLink, { ...l }, l.title + l.href))) })), currentTab === 'hit' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isHit).map(h => (_jsx(Link, { to: `/hits/${h.howler.id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(HitCard, { id: h.howler.id, layout: HitLayout.NORMAL }) }, h.howler.id))) })), currentTab === 'case' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isCase).map(c => (_jsx(Link, { to: `/cases/${c.case_id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(CaseCard, { case: c }) }, c.case_id))) })), currentTab === 'observable' && (_jsx(Stack, { spacing: 1, pt: 1, children: records.filter(isObservable).map(o => (_jsx(Link, { to: `/observables/${o.howler.id}`, target: "_blank", rel: "noopener noreferrer", style: { textDecoration: 'none' }, children: _jsx(ObservableCard, { observable: o }) }, o.howler.id))) }))] }));
33
- };
34
- export default RecordRelated;
@@ -1,13 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
- import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
4
- import type { WithMetadata } from '@cccsaurora/howler-ui/models/WithMetadata';
5
- type MixedRecords = Hit | Observable | Case;
6
- /**
7
- * Fetches records matching the provided IDs from the hit, observable, and case indexes.
8
- *
9
- * @param ids - List of howler.id / case_id values to look up.
10
- * @param enabled - When false the fetch is skipped (e.g. while a panel is closed).
11
- */
12
- declare const useRelatedRecords: <T = MixedRecords>(ids: string[], enabled?: boolean) => WithMetadata<T>[];
13
- export default useRelatedRecords;
@@ -1,32 +0,0 @@
1
- import api from '@cccsaurora/howler-ui/api';
2
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
3
- import { useEffect, useState } from 'react';
4
- /**
5
- * Fetches records matching the provided IDs from the hit, observable, and case indexes.
6
- *
7
- * @param ids - List of howler.id / case_id values to look up.
8
- * @param enabled - When false the fetch is skipped (e.g. while a panel is closed).
9
- */
10
- const useRelatedRecords = (ids, enabled = true) => {
11
- const { dispatchApi } = useMyApi();
12
- const [records, setRecords] = useState([]);
13
- useEffect(() => {
14
- if (!enabled || ids.length === 0) {
15
- if (records.length > 0) {
16
- setRecords([]);
17
- }
18
- return;
19
- }
20
- (async () => {
21
- const joined = ids.join(' OR ');
22
- const result = await dispatchApi(api.v2.search.post('hit,observable,case', {
23
- query: `howler.id:(${joined}) OR case_id:(${joined})`
24
- }), { throwError: false, showError: true });
25
- if (result) {
26
- setRecords(result.items);
27
- }
28
- })();
29
- }, [dispatchApi, enabled, ids, records.length]);
30
- return records;
31
- };
32
- export default useRelatedRecords;
@@ -1,2 +0,0 @@
1
- declare const _default: import("react").NamedExoticComponent<{}>;
2
- export default _default;
@@ -1,22 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Stack } from '@mui/material';
3
- import { memo } from 'react';
4
- import { Outlet, useParams } from 'react-router-dom';
5
- import NotFoundPage from '../404';
6
- import ErrorBoundary from '../ErrorBoundary';
7
- import CaseDetails from './detail/CaseDetails';
8
- import CaseSidebar from './detail/CaseSidebar';
9
- import useCase from './hooks/useCase';
10
- const CaseViewer = () => {
11
- const params = useParams();
12
- const { case: _case, missing, update } = useCase({ caseId: params.id });
13
- if (missing) {
14
- return _jsx(NotFoundPage, {});
15
- }
16
- return (_jsx(ErrorBoundary, { children: _jsxs(Stack, { direction: "row", height: "100%", children: [_jsx(CaseSidebar, { case: _case, update: updatedCase => update(updatedCase, false) }), _jsx(Box, { sx: {
17
- maxHeight: 'calc(100vh - 64px)',
18
- flex: 1,
19
- overflow: 'auto'
20
- }, children: _jsx(ErrorBoundary, { children: _jsx(Outlet, { context: _case }) }) }), _jsx(CaseDetails, { case: _case })] }) }));
21
- };
22
- export default memo(CaseViewer);
@@ -1,2 +0,0 @@
1
- declare const Cases: () => import("react/jsx-runtime").JSX.Element;
2
- export default Cases;
@@ -1,101 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Topic } from '@mui/icons-material';
3
- import { Typography } from '@mui/material';
4
- import api from '@cccsaurora/howler-ui/api';
5
- import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
6
- import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
7
- import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
8
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
9
- import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
10
- import { useCallback, useContext, useEffect, useState } from 'react';
11
- import { useTranslation } from 'react-i18next';
12
- import { useNavigate, useSearchParams } from 'react-router-dom';
13
- import { StorageKey } from '@cccsaurora/howler-ui/utils/constants';
14
- import CaseCard from '../../elements/case/CaseCard';
15
- const CasesBase = () => {
16
- const { t } = useTranslation();
17
- const navigate = useNavigate();
18
- const { dispatchApi } = useMyApi();
19
- const [searchParams, setSearchParams] = useSearchParams();
20
- const { load } = useContext(TuiListMethodContext);
21
- const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
22
- const [phrase, setPhrase] = useState('');
23
- const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
24
- const [response, setResponse] = useState(null);
25
- const [hasError, setHasError] = useState(false);
26
- const [loading, setLoading] = useState(false);
27
- const onSearch = useCallback(async () => {
28
- try {
29
- setLoading(true);
30
- setHasError(false);
31
- if (phrase) {
32
- searchParams.set('phrase', phrase);
33
- }
34
- else {
35
- searchParams.delete('phrase');
36
- }
37
- setSearchParams(searchParams, { replace: true });
38
- // Check for the actual search query
39
- const query = phrase ? `*:*${phrase}*` : '*:*';
40
- // Ensure the overview should be visible and/or matches the type we are filtering for
41
- setResponse(await dispatchApi(api.search.case.post({
42
- query,
43
- rows: pageCount,
44
- offset
45
- })));
46
- }
47
- catch (e) {
48
- setHasError(true);
49
- }
50
- finally {
51
- setLoading(false);
52
- }
53
- }, [phrase, setSearchParams, searchParams, dispatchApi, pageCount, offset]);
54
- // Load the items into list when response changes.
55
- // This hook should only trigger when the 'response' changes.
56
- useEffect(() => {
57
- if (response) {
58
- load(response.items.map((item) => ({
59
- id: item.case_id,
60
- item,
61
- selected: false,
62
- cursor: false
63
- })));
64
- }
65
- // eslint-disable-next-line react-hooks/exhaustive-deps
66
- }, [response, load]);
67
- const onPageChange = useCallback((_offset) => {
68
- if (_offset !== offset) {
69
- searchParams.set('offset', _offset.toString());
70
- setSearchParams(searchParams, { replace: true });
71
- setOffset(_offset);
72
- }
73
- }, [offset, searchParams, setSearchParams]);
74
- useEffect(() => {
75
- onSearch();
76
- if (!searchParams.has('offset')) {
77
- searchParams.set('offset', '0');
78
- setSearchParams(searchParams, { replace: true });
79
- }
80
- // eslint-disable-next-line react-hooks/exhaustive-deps
81
- }, []);
82
- useEffect(() => {
83
- if (response?.total <= offset) {
84
- setOffset(0);
85
- searchParams.set('offset', '0');
86
- setSearchParams(searchParams, { replace: true });
87
- }
88
- }, [offset, response?.total, searchParams, setSearchParams]);
89
- useEffect(() => {
90
- if (!loading) {
91
- onSearch();
92
- }
93
- // eslint-disable-next-line react-hooks/exhaustive-deps
94
- }, [offset]);
95
- const renderer = useCallback((item, className) => _jsx(CaseCard, { case: item, className: className }), []);
96
- return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: loading, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.cases.search.prompt') }), renderer: ({ item }, classRenderer) => renderer(item.item, classRenderer()), response: response, onSelect: (item) => navigate(`/cases/${item.id}`), onCreate: () => navigate('/cases/create'), createPrompt: "route.cases.create", searchPrompt: "route.cases.manager.search", createIcon: _jsx(Topic, { sx: { mr: 1 } }) }));
97
- };
98
- const Cases = () => {
99
- return (_jsx(TuiListProvider, { children: _jsx(CasesBase, {}) }));
100
- };
101
- export default Cases;
@@ -1,5 +0,0 @@
1
- export declare const ESCALATION_COLOR_MAP: {
2
- normal: string;
3
- focus: string;
4
- crisis: string;
5
- };
@@ -1,5 +0,0 @@
1
- export const ESCALATION_COLOR_MAP = {
2
- normal: 'default',
3
- focus: 'warning',
4
- crisis: 'error'
5
- };
@@ -1,6 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import { type FC } from 'react';
3
- declare const AlertPanel: FC<{
4
- case: Case;
5
- }>;
6
- export default AlertPanel;
@@ -1,33 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Divider, Pagination, Skeleton, Stack, Typography, useTheme } from '@mui/material';
3
- import HitCard from '@cccsaurora/howler-ui/components/elements/hit/HitCard';
4
- import { HitLayout } from '@cccsaurora/howler-ui/components/elements/hit/HitLayout';
5
- import { chunk, uniq } from 'lodash-es';
6
- import { useMemo, useState } from 'react';
7
- import { useTranslation } from 'react-i18next';
8
- import { Link } from 'react-router-dom';
9
- const AlertPanel = ({ case: _case }) => {
10
- const theme = useTheme();
11
- const { t } = useTranslation();
12
- const [alertPage, setAlertPage] = useState(1);
13
- const alertPages = useMemo(() => chunk(uniq((_case?.items ?? []).filter(item => item.type === 'hit')), 5), [_case?.items]);
14
- if (!_case) {
15
- return _jsx(Skeleton, { height: 240 });
16
- }
17
- return (_jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", children: [_jsx(Typography, { flex: 1, variant: "h4", children: t('page.cases.dashboard.alerts') }), _jsx(Pagination, { count: alertPages.length, page: alertPage, onChange: (_, page) => setAlertPage(page) })] }), _jsx(Divider, {}), alertPages?.length > 0 &&
18
- alertPages[alertPage - 1].map(item => (_jsxs(Box, { position: "relative", children: [_jsx(HitCard, { layout: HitLayout.DENSE, id: item.value, lazy: true }), _jsx(Box, { component: Link, to: item.path, sx: {
19
- position: 'absolute',
20
- top: 0,
21
- left: 0,
22
- width: '100%',
23
- height: '100%',
24
- cursor: 'pointer',
25
- zIndex: 100,
26
- borderRadius: '4px',
27
- '&:hover': {
28
- background: theme.palette.divider,
29
- border: `thin solid ${theme.palette.primary.light}`
30
- }
31
- } })] }, item.path)))] }));
32
- };
33
- export default AlertPanel;
@@ -1,11 +0,0 @@
1
- import type { Case } from '@cccsaurora/howler-ui/models/entities/generated/Case';
2
- import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
3
- import type { Observable } from '@cccsaurora/howler-ui/models/entities/generated/Observable';
4
- import { type AssetEntry } from './assets/Asset';
5
- /** Deduplicate and merge seenIn lists into a map keyed by `type:value` */
6
- export declare const buildAssetEntries: (records: Partial<Hit | Observable>[]) => AssetEntry[];
7
- declare const _default: import("react").NamedExoticComponent<{
8
- case?: Case;
9
- caseId?: string;
10
- }>;
11
- export default _default;
@@ -1,104 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Chip, Grid, Skeleton, Stack, Typography } from '@mui/material';
3
- import api from '@cccsaurora/howler-ui/api';
4
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
5
- import { memo, useEffect, useMemo, useState } from 'react';
6
- import { useTranslation } from 'react-i18next';
7
- import { useOutletContext } from 'react-router-dom';
8
- import useCase from '../hooks/useCase';
9
- import Asset, {} from './assets/Asset';
10
- /** All Related fields that carry asset values */
11
- const ASSET_FIELDS = ['hash', 'hosts', 'ip', 'user', 'ids', 'id', 'uri', 'signature'];
12
- /** Extract (type, value, seenInId) triples from a record's related field */
13
- const extractAssets = (related, recordId) => {
14
- if (!related) {
15
- return [];
16
- }
17
- const results = [];
18
- for (const field of ASSET_FIELDS) {
19
- const raw = related[field];
20
- if (!raw) {
21
- continue;
22
- }
23
- const values = Array.isArray(raw) ? raw : [raw];
24
- for (const value of values) {
25
- if (value) {
26
- results.push({ type: field, value: String(value), id: recordId });
27
- }
28
- }
29
- }
30
- return results;
31
- };
32
- /** Deduplicate and merge seenIn lists into a map keyed by `type:value` */
33
- export const buildAssetEntries = (records) => {
34
- const map = new Map();
35
- for (const record of records) {
36
- const related = record.related ?? record.related;
37
- const recordId = record.howler?.id ?? record.howler?.id;
38
- if (!recordId) {
39
- continue;
40
- }
41
- for (const { type, value, id } of extractAssets(related, recordId)) {
42
- const key = `${type}:${value}`;
43
- if (!map.has(key)) {
44
- map.set(key, { type, value, seenIn: [] });
45
- }
46
- const entry = map.get(key);
47
- if (!entry.seenIn.includes(id)) {
48
- entry.seenIn.push(id);
49
- }
50
- }
51
- }
52
- return Array.from(map.values());
53
- };
54
- const RELATED_FIELDS = ASSET_FIELDS.map(f => `related.${f}`).join(',');
55
- const CaseAssets = ({ case: providedCase, caseId }) => {
56
- const { t } = useTranslation();
57
- const { dispatchApi } = useMyApi();
58
- const routeCase = useOutletContext();
59
- const { case: _case } = useCase({ case: providedCase ?? routeCase, caseId });
60
- const [records, setRecords] = useState(null);
61
- const [activeFilters, setActiveFilters] = useState(new Set());
62
- const ids = useMemo(() => (_case?.items ?? [])
63
- .filter(item => ['hit', 'observable'].includes(item.type))
64
- .map(item => item.value)
65
- .filter(val => !!val), [_case?.items]);
66
- useEffect(() => {
67
- if (ids.length < 1) {
68
- setRecords([]);
69
- return;
70
- }
71
- dispatchApi(api.v2.search.post(['hit', 'observable'], {
72
- query: `howler.id:(${ids.join(' OR ')})`,
73
- fl: `howler.id,${RELATED_FIELDS}`
74
- })).then(response => setRecords(response.items));
75
- }, [dispatchApi, ids]);
76
- const allAssets = useMemo(() => (records ? buildAssetEntries(records) : []), [records]);
77
- const assetTypes = useMemo(() => (allAssets ? [...new Set(allAssets.map(a => a.type))].sort() : []), [allAssets]);
78
- const filteredAssets = useMemo(() => {
79
- if (allAssets.length < 1) {
80
- return [];
81
- }
82
- if (activeFilters.size === 0) {
83
- return allAssets;
84
- }
85
- return allAssets.filter(a => activeFilters.has(a.type));
86
- }, [allAssets, activeFilters]);
87
- const toggleFilter = (type) => {
88
- setActiveFilters(prev => {
89
- const next = new Set(prev);
90
- if (next.has(type)) {
91
- next.delete(type);
92
- }
93
- else {
94
- next.add(type);
95
- }
96
- return next;
97
- });
98
- };
99
- if (!_case) {
100
- return null;
101
- }
102
- return (_jsxs(Grid, { container: true, spacing: 2, px: 2, children: [_jsx(Grid, { item: true, xs: 12, children: _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, flexWrap: "wrap", children: [_jsx(Typography, { variant: "subtitle2", color: "text.secondary", children: t('page.cases.assets.filter_by_type') }), records === null ? (_jsx(Skeleton, { width: 240, height: 32 })) : (assetTypes.map(type => (_jsx(Chip, { label: t(`page.cases.assets.type.${type}`), size: "small", onClick: () => toggleFilter(type), color: activeFilters.has(type) ? 'primary' : 'default', variant: activeFilters.has(type) ? 'filled' : 'outlined' }, type))))] }) }), records === null ? (Array.from({ length: 6 }, (_, i) => (_jsx(Grid, { item: true, xs: 12, sm: 6, md: 4, xl: 3, children: _jsx(Skeleton, { height: 100 }) }, `skeleton-${i}`)))) : filteredAssets.length === 0 ? (_jsx(Grid, { item: true, xs: 12, children: _jsx(Typography, { color: "text.secondary", children: t('page.cases.assets.empty') }) })) : (filteredAssets.map(asset => (_jsx(Grid, { item: true, xs: 12, md: 6, xl: 4, children: _jsx(Asset, { asset: asset, case: _case }) }, `${asset.type}:${asset.value}`))))] }));
103
- };
104
- export default memo(CaseAssets);
@@ -1 +0,0 @@
1
- export {};