@cccsaurora/howler-ui 2.18.0-dev.704 → 2.18.0-dev.705

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 (245) hide show
  1. package/api/index.d.ts +2 -0
  2. package/api/index.js +4 -2
  3. package/api/search/case.d.ts +4 -0
  4. package/api/search/case.js +8 -0
  5. package/api/search/index.d.ts +2 -1
  6. package/api/search/index.js +2 -1
  7. package/api/v2/case/index.d.ts +8 -0
  8. package/api/v2/case/index.js +20 -0
  9. package/api/v2/case/items.d.ts +6 -0
  10. package/api/v2/case/items.js +18 -0
  11. package/api/v2/index.d.ts +4 -0
  12. package/api/v2/index.js +6 -0
  13. package/api/v2/search/facet.d.ts +3 -0
  14. package/api/v2/search/facet.js +12 -0
  15. package/api/v2/search/index.d.ts +5 -0
  16. package/api/v2/search/index.js +24 -0
  17. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  18. package/components/app/App.js +34 -7
  19. package/components/app/hooks/useMatchers.js +2 -2
  20. package/components/app/hooks/useMatchers.test.js +22 -22
  21. package/components/app/hooks/useTitle.js +3 -3
  22. package/components/app/providers/FavouritesProvider.js +2 -2
  23. package/components/app/providers/ModalProvider.d.ts +1 -0
  24. package/components/app/providers/ParameterProvider.d.ts +9 -2
  25. package/components/app/providers/ParameterProvider.js +165 -240
  26. package/components/app/providers/ParameterProvider.test.js +307 -14
  27. package/components/app/providers/RecordProvider.d.ts +23 -0
  28. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  29. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  30. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  31. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +51 -70
  32. package/components/elements/ContextMenu.d.ts +56 -0
  33. package/components/elements/ContextMenu.js +109 -0
  34. package/components/elements/ContextMenu.test.js +215 -0
  35. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  36. package/components/elements/ObjectDetails.d.ts +6 -0
  37. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  38. package/components/elements/PluginTypography.d.ts +2 -1
  39. package/components/elements/PluginTypography.js +3 -2
  40. package/components/elements/UserList.d.ts +5 -2
  41. package/components/elements/UserList.js +14 -5
  42. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  43. package/components/elements/case/CaseCard.d.ts +12 -0
  44. package/components/elements/case/CaseCard.js +42 -0
  45. package/components/elements/case/CasePreview.d.ts +6 -0
  46. package/components/elements/case/CasePreview.js +17 -0
  47. package/components/elements/case/StatusIcon.d.ts +5 -0
  48. package/components/elements/case/StatusIcon.js +13 -0
  49. package/components/elements/display/ChipPopper.d.ts +1 -1
  50. package/components/elements/display/HowlerCard.js +1 -1
  51. package/components/elements/display/Modal.js +2 -0
  52. package/components/elements/hit/HitActions.js +4 -4
  53. package/components/elements/hit/HitBanner.js +28 -48
  54. package/components/elements/hit/HitCard.js +5 -5
  55. package/components/elements/hit/HitLabels.js +2 -2
  56. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  57. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  58. package/components/elements/hit/HitSummary.d.ts +2 -1
  59. package/components/elements/hit/HitSummary.js +6 -5
  60. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  61. package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
  62. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  63. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  64. package/components/elements/hit/related/RelatedRecords.js +63 -0
  65. package/components/elements/observable/ObservableCard.d.ts +6 -0
  66. package/components/elements/observable/ObservableCard.js +23 -0
  67. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  68. package/components/elements/observable/ObservablePreview.js +12 -0
  69. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  70. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  71. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  72. package/components/elements/record/RecordContextMenu.js +247 -0
  73. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  74. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +94 -39
  75. package/components/elements/record/RecordRelated.d.ts +7 -0
  76. package/components/elements/record/RecordRelated.js +34 -0
  77. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  78. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  79. package/components/elements/view/ViewTitle.d.ts +1 -0
  80. package/components/elements/view/ViewTitle.js +9 -2
  81. package/components/hooks/useHitActions.d.ts +1 -1
  82. package/components/hooks/useHitActions.js +4 -4
  83. package/components/hooks/useMyPreferences.js +10 -1
  84. package/components/hooks/useMySearch.js +2 -2
  85. package/components/hooks/useMySitemap.js +4 -1
  86. package/components/hooks/useMyTheme.js +9 -2
  87. package/components/hooks/useParamState.test.js +3 -4
  88. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  89. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  90. package/components/hooks/useRelatedRecords.d.ts +13 -0
  91. package/components/hooks/useRelatedRecords.js +32 -0
  92. package/components/routes/action/edit/ActionEditor.js +2 -2
  93. package/components/routes/action/view/ActionSearch.js +1 -1
  94. package/components/routes/advanced/QueryBuilder.js +1 -1
  95. package/components/routes/advanced/QueryEditor.js +3 -3
  96. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  97. package/components/routes/analytics/AnalyticDetails.js +2 -2
  98. package/components/routes/analytics/AnalyticSearch.js +1 -1
  99. package/components/routes/cases/CaseViewer.d.ts +2 -0
  100. package/components/routes/cases/CaseViewer.js +22 -0
  101. package/components/routes/cases/Cases.d.ts +2 -0
  102. package/components/routes/cases/Cases.js +101 -0
  103. package/components/routes/cases/constants.d.ts +5 -0
  104. package/components/routes/cases/constants.js +5 -0
  105. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  106. package/components/routes/cases/detail/AlertPanel.js +33 -0
  107. package/components/routes/cases/detail/CaseAssets.d.ts +12 -0
  108. package/components/routes/cases/detail/CaseAssets.js +104 -0
  109. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  110. package/components/routes/cases/detail/CaseAssets.test.js +167 -0
  111. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  112. package/components/routes/cases/detail/CaseDashboard.js +54 -0
  113. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  114. package/components/routes/cases/detail/CaseDetails.js +61 -0
  115. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  116. package/components/routes/cases/detail/CaseOverview.js +43 -0
  117. package/components/routes/cases/detail/CaseSidebar.d.ts +8 -0
  118. package/components/routes/cases/detail/CaseSidebar.js +61 -0
  119. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  120. package/components/routes/cases/detail/CaseTask.js +57 -0
  121. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  122. package/components/routes/cases/detail/ItemPage.js +99 -0
  123. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  124. package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
  125. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  126. package/components/routes/cases/detail/TaskPanel.js +52 -0
  127. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
  128. package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
  129. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  130. package/components/routes/cases/detail/aggregates/SourceAggregate.js +30 -0
  131. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  132. package/components/routes/cases/detail/assets/Asset.js +12 -0
  133. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  134. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  135. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +14 -0
  136. package/components/routes/cases/detail/sidebar/CaseFolder.js +133 -0
  137. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +34 -0
  138. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +105 -0
  139. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.d.ts +1 -0
  140. package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +351 -0
  141. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  142. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  143. package/components/routes/cases/detail/sidebar/utils.js +25 -0
  144. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  145. package/components/routes/cases/hooks/useCase.js +51 -0
  146. package/components/routes/cases/modals/AddToCaseModal.d.ts +7 -0
  147. package/components/routes/cases/modals/AddToCaseModal.js +62 -0
  148. package/components/routes/cases/modals/RenameItemModal.d.ts +9 -0
  149. package/components/routes/cases/modals/RenameItemModal.js +48 -0
  150. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  151. package/components/routes/cases/modals/ResolveModal.js +62 -0
  152. package/components/routes/dossiers/DossierEditor.js +2 -2
  153. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  154. package/components/routes/help/ApiDocumentation.js +1 -1
  155. package/components/routes/help/HitBannerDocumentation.js +1 -0
  156. package/components/routes/help/HitDocumentation.js +1 -3
  157. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  158. package/components/routes/hits/search/InformationPane.js +47 -60
  159. package/components/routes/hits/search/LayoutSettings.js +3 -3
  160. package/components/routes/hits/search/QuerySettings.js +2 -1
  161. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  162. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  163. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  164. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  165. package/components/routes/hits/search/SearchPane.js +26 -49
  166. package/components/routes/hits/search/ViewLink.js +3 -3
  167. package/components/routes/hits/search/ViewLink.test.js +8 -8
  168. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  169. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  170. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  171. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  172. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  173. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  174. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  175. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  176. package/components/routes/hits/view/HitViewer.js +12 -13
  177. package/components/routes/home/ViewCard.js +47 -41
  178. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  179. package/components/routes/observables/ObservableViewer.js +27 -0
  180. package/components/routes/overviews/OverviewViewer.js +2 -2
  181. package/components/routes/views/ViewComposer.js +46 -19
  182. package/locales/en/translation.json +80 -3
  183. package/locales/fr/translation.json +78 -3
  184. package/models/WithMetadata.d.ts +2 -1
  185. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  186. package/models/entities/generated/Case.d.ts +28 -0
  187. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  188. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  189. package/models/entities/generated/EmailParent.d.ts +19 -0
  190. package/models/entities/generated/Enrichments.d.ts +7 -0
  191. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  192. package/models/entities/generated/Hit.d.ts +1 -0
  193. package/models/entities/generated/Howler.d.ts +0 -4
  194. package/models/entities/generated/HttpResponse.d.ts +11 -0
  195. package/models/entities/generated/Item.d.ts +9 -0
  196. package/models/entities/generated/Observable.d.ts +85 -0
  197. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  198. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  199. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  200. package/models/entities/generated/ObservableFile.d.ts +36 -0
  201. package/models/entities/generated/ObservableHowler.d.ts +43 -0
  202. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  203. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  204. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  205. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  206. package/models/entities/generated/ObservableSource.d.ts +23 -0
  207. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  208. package/models/entities/generated/ObservableTls.d.ts +12 -0
  209. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  210. package/models/entities/generated/Rule.d.ts +2 -10
  211. package/models/entities/generated/Task.d.ts +10 -0
  212. package/models/entities/generated/Threat.d.ts +2 -2
  213. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  214. package/models/entities/generated/View.d.ts +1 -0
  215. package/package.json +18 -1
  216. package/plugins/clue/components/ClueTypography.js +2 -2
  217. package/plugins/clue/utils.d.ts +2 -1
  218. package/tests/server-handlers.js +6 -1
  219. package/tests/utils.d.ts +4 -0
  220. package/tests/utils.js +20 -0
  221. package/utils/constants.d.ts +3 -3
  222. package/utils/hitFunctions.d.ts +2 -1
  223. package/utils/hitFunctions.js +4 -4
  224. package/utils/typeUtils.d.ts +7 -0
  225. package/utils/typeUtils.js +27 -0
  226. package/utils/viewUtils.js +3 -0
  227. package/components/app/providers/HitProvider.d.ts +0 -22
  228. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  229. package/components/elements/display/icons/BundleButton.js +0 -32
  230. package/components/elements/hit/HitRelated.d.ts +0 -6
  231. package/components/elements/hit/HitRelated.js +0 -7
  232. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  233. package/components/routes/help/BundleDocumentation.js +0 -12
  234. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  235. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  236. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  237. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  238. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  239. package/components/routes/hits/search/BundleScroller.js +0 -6
  240. package/components/routes/hits/search/HitContextMenu.js +0 -227
  241. /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
  242. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
  243. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  244. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  245. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -1,9 +1,16 @@
1
1
  const DEFAULT_THEME = {
2
+ components: {
3
+ MuiChip: {
4
+ defaultProps: {
5
+ size: 'small'
6
+ }
7
+ }
8
+ },
2
9
  palette: {
3
10
  dark: {
4
11
  background: {
5
- default: '#202020',
6
- paper: '#202020'
12
+ default: '#181818',
13
+ paper: '#181818'
7
14
  },
8
15
  primary: {
9
16
  main: '#7DA1DB'
@@ -5,10 +5,9 @@ import { MemoryRouter, useSearchParams } from 'react-router-dom';
5
5
  import { describe, expect, it } from 'vitest';
6
6
  import useParamState from './useParamState';
7
7
  // Creates a MemoryRouter wrapper using createElement to avoid JSX in a .ts file
8
- const makeWrapper = (search = '') => {
9
- // eslint-disable-next-line react/function-component-definition
10
- return ({ children }) => createElement(MemoryRouter, { initialEntries: [search ? `/?${search}` : '/'] }, children);
11
- };
8
+ const makeWrapper = (search = '') =>
9
+ // eslint-disable-next-line react/function-component-definition
10
+ ({ children }) => createElement(MemoryRouter, { initialEntries: [search ? `/?${search}` : '/'] }, children);
12
11
  // Composite hook: exposes the param state AND the live URL params for URL-level assertions
13
12
  const useParamStateWithUrl = (key, defaultValue) => {
14
13
  const [value, setValue] = useParamState(key, defaultValue);
@@ -1,8 +1,8 @@
1
1
  import type { Hit } from '@cccsaurora/howler-ui/models/entities/generated/Hit';
2
2
  import type React from 'react';
3
- declare const useHitSelection: () => {
3
+ declare const useRecordSelection: () => {
4
4
  lastSelected: string;
5
5
  setLastSelected: React.Dispatch<React.SetStateAction<string>>;
6
6
  onClick: (e: React.MouseEvent<HTMLDivElement>, hit: Hit) => void;
7
7
  };
8
- export default useHitSelection;
8
+ export default useRecordSelection;
@@ -1,20 +1,14 @@
1
- import { useAppBreadcrumbs } from '@cccsaurora/howler-ui/commons/components/app/hooks';
2
- import { HitContext } from '@cccsaurora/howler-ui/components/app/providers/HitProvider';
3
- import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
4
1
  import { ParameterContext } from '@cccsaurora/howler-ui/components/app/providers/ParameterProvider';
5
- import useMySitemap from '@cccsaurora/howler-ui/components/hooks/useMySitemap';
2
+ import { RecordContext } from '@cccsaurora/howler-ui/components/app/providers/RecordProvider';
3
+ import { RecordSearchContext } from '@cccsaurora/howler-ui/components/app/providers/RecordSearchProvider';
6
4
  import { useCallback, useState } from 'react';
7
- import { useNavigate } from 'react-router-dom';
8
5
  import { useContextSelector } from 'use-context-selector';
9
- const useHitSelection = () => {
10
- const navigate = useNavigate();
11
- const { setItems } = useAppBreadcrumbs();
12
- const { routes } = useMySitemap();
13
- const response = useContextSelector(HitSearchContext, ctx => ctx.response);
14
- const selectedHits = useContextSelector(HitContext, ctx => ctx.selectedHits);
15
- const addHitToSelection = useContextSelector(HitContext, ctx => ctx.addHitToSelection);
16
- const removeHitFromSelection = useContextSelector(HitContext, ctx => ctx.removeHitFromSelection);
17
- const clearSelectedHits = useContextSelector(HitContext, ctx => ctx.clearSelectedHits);
6
+ const useRecordSelection = () => {
7
+ const response = useContextSelector(RecordSearchContext, ctx => ctx.response);
8
+ const selectedHits = useContextSelector(RecordContext, ctx => ctx.selectedRecords);
9
+ const addHitToSelection = useContextSelector(RecordContext, ctx => ctx.addRecordToSelection);
10
+ const removeHitFromSelection = useContextSelector(RecordContext, ctx => ctx.removeRecordFromSelection);
11
+ const clearSelectedHits = useContextSelector(RecordContext, ctx => ctx.clearSelectedRecords);
18
12
  const setSelected = useContextSelector(ParameterContext, ctx => ctx.setSelected);
19
13
  const [lastSelected, setLastSelected] = useState(null);
20
14
  const onClick = useCallback((e, hit) => {
@@ -47,32 +41,17 @@ const useHitSelection = () => {
47
41
  e.stopPropagation();
48
42
  return;
49
43
  }
50
- if (hit.howler.is_bundle) {
51
- const searchRoute = routes.find(_route => _route.path.startsWith(location.pathname.replace(/^(\/.*)\/.+/, '$1')));
52
- const newBreadcrumb = {
53
- ...searchRoute,
54
- path: location.pathname + location.search
55
- };
56
- setItems([{ route: newBreadcrumb, matcher: null }]);
57
- navigate(`/bundles/${hit.howler.id}?span=date.range.all&query=howler.id%3A*&offset=0`);
58
- clearSelectedHits(hit.howler.id);
59
- }
60
- else {
61
- clearSelectedHits(hit.howler.id);
62
- setSelected(hit.howler.id);
63
- }
44
+ clearSelectedHits(hit.howler.id);
45
+ setSelected(hit.howler.id);
64
46
  }, [
65
47
  addHitToSelection,
66
48
  clearSelectedHits,
67
49
  lastSelected,
68
- navigate,
69
50
  removeHitFromSelection,
70
- response,
71
- routes,
51
+ response?.items,
72
52
  selectedHits,
73
- setItems,
74
53
  setSelected
75
54
  ]);
76
55
  return { lastSelected, setLastSelected, onClick };
77
56
  };
78
- export default useHitSelection;
57
+ export default useRecordSelection;
@@ -0,0 +1,13 @@
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;
@@ -0,0 +1,32 @@
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;
@@ -7,7 +7,7 @@ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCente
7
7
  import { FieldContext } from '@cccsaurora/howler-ui/components/app/providers/FieldProvider';
8
8
  import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
9
9
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
10
- import HitQuery from '@cccsaurora/howler-ui/components/routes/hits/search/HitQuery';
10
+ import RecordQuery from '@cccsaurora/howler-ui/components/routes/hits/search/RecordQuery';
11
11
  import { difference, uniq } from 'lodash-es';
12
12
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
13
13
  import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
@@ -102,7 +102,7 @@ const ActionEditor = () => {
102
102
  : disabled && userOperations.length > 0
103
103
  ? t('route.actions.trigger.disabled.explanation')
104
104
  : null, children: component }, trigger));
105
- }) }) }), _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "end", sx: { mb: -1 }, children: [_jsx(Typography, { sx: theme => ({ color: theme.palette.text.disabled, fontStyle: 'italic', mb: 0.5 }), variant: "body2", children: t('hit.search.prompt') }), _jsx(SocketBadge, { size: "small" })] }), _jsx(HitQuery, { triggerSearch: onSearch }), response ? (_jsx(QueryResultText, { count: response.total, query: responseQuery })) : (_jsx(Typography, { sx: theme => ({
105
+ }) }) }), _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "end", sx: { mb: -1 }, children: [_jsx(Typography, { sx: theme => ({ color: theme.palette.text.disabled, fontStyle: 'italic', mb: 0.5 }), variant: "body2", children: t('hit.search.prompt') }), _jsx(SocketBadge, { size: "small" })] }), _jsx(RecordQuery, { triggerSearch: onSearch }), response ? (_jsx(QueryResultText, { count: response.total, query: responseQuery })) : (_jsx(Typography, { sx: theme => ({
106
106
  color: theme.palette.text.secondary,
107
107
  fontSize: '0.9em',
108
108
  fontStyle: 'italic',
@@ -109,7 +109,7 @@ const ActionSearch = () => {
109
109
  e.stopPropagation();
110
110
  await deleteAction(item.item.action_id);
111
111
  onSearch();
112
- }, children: _jsx(Delete, {}) })), _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important' }, userId: item.item.owner_id })] }), subheader: item.item.query }), _jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsx(Grid, { container: true, spacing: 1, children: item.item.operations.map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { size: "small", label: t(`operations.${d.operation_id}`) }) }, d.operation_id))) }) })] }, item.item.name));
112
+ }, children: _jsx(Delete, {}) })), _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important' }, userId: item.item.owner_id })] }), subheader: item.item.query }), _jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsx(Grid, { container: true, spacing: 1, children: item.item.operations.map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { label: t(`operations.${d.operation_id}`) }) }, d.operation_id))) }) })] }, item.item.name));
113
113
  }, [deleteAction, navigate, onSearch, t, user.roles, user.username]);
114
114
  return (_jsx(ItemManager, { onSearch: onSearch, onCreate: () => navigate('/action/execute'), onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.actions.search.prompt') }), searchFilters: _jsx(Autocomplete, { multiple: true, size: "small", value: searchModifiers, onChange: (__, values) => setSearchModifiers(values), getOptionLabel: trigger => t(`route.actions.trigger.${trigger}`), options: VALID_ACTION_TRIGGERS, renderInput: params => (_jsx(TextField, { ...params, sx: { maxWidth: '500px' }, label: t('route.actions.trigger') })) }), renderer: renderer, response: response, createPrompt: "route.actions.create", searchPrompt: "route.actions.search", createIcon: _jsx(Terminal, { sx: { mr: 1 } }) }));
115
115
  };
@@ -233,7 +233,7 @@ const QueryBuilder = () => {
233
233
  height: '100%',
234
234
  '& .MuiFormControl-root': { height: '100%', '& > div': { height: '100%' } }
235
235
  }
236
- ], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), type === 'lucene' && (_jsx(Autocomplete, { size: "small", getOptionLabel: opt => t(`route.advanced.query.type.${opt}`), options: LUCENE_QUERY_OPTIONS, value: queryType, onChange: (_event, value) => setQueryType(value), renderInput: params => (_jsx(TextField, { ...params, label: t('route.advanced.query.lucene.type'), sx: { minWidth: '230px' } })), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.type.${option}`), secondary: t(`route.advanced.query.type.${option}.description`) })) })), queryType === 'groupby' && (_jsx(Autocomplete, { size: "small", options: fieldOptions, value: groupByField, onChange: (__, value) => setGroupByField(value), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.pivot.field') }), sx: { minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), allFields && queryType !== 'facet' ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { size: "small", label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { size: "small", label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), _jsx(FlexOne, {}), type === 'lucene' &&
236
+ ], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), type === 'lucene' && (_jsx(Autocomplete, { size: "small", getOptionLabel: opt => t(`route.advanced.query.type.${opt}`), options: LUCENE_QUERY_OPTIONS, value: queryType, onChange: (_event, value) => setQueryType(value), renderInput: params => (_jsx(TextField, { ...params, label: t('route.advanced.query.lucene.type'), sx: { minWidth: '230px' } })), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.type.${option}`), secondary: t(`route.advanced.query.type.${option}.description`) })) })), queryType === 'groupby' && (_jsx(Autocomplete, { size: "small", options: fieldOptions, value: groupByField, onChange: (__, value) => setGroupByField(value), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.pivot.field') }), sx: { minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), allFields && queryType !== 'facet' ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), _jsx(FlexOne, {}), type === 'lucene' &&
237
237
  (smallButtons ? (_jsx(Tooltip, { title: t('route.advanced.open'), children: _jsx(IconButton, { color: "primary", sx: { alignSelf: 'center' }, component: Link, disabled: !response, to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}`, children: _jsx(OpenInNew, { fontSize: "small" }) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: _jsx(OpenInNew, {}), component: Link, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, children: t('route.advanced.open') }))), smallButtons ? (_jsx(Tooltip, { title: response ? t('route.advanced.create.rule') : t('route.advanced.create.rule.disabled'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'center' }, color: "info", onClick: onCreateRule, disabled: !response, children: _jsx(SsidChart, {}) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", color: "info", startIcon: _jsx(SsidChart, {}), onClick: onCreateRule, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, tooltip: !response && t('route.advanced.create.rule.disabled'), children: t('route.advanced.create.rule') }))] }), _jsxs(Box, { width: "100%", height: "calc(100vh - 112px)", sx: { position: 'relative', overflow: 'hidden', borderTop: `thin solid ${theme.palette.divider}` }, children: [_jsx(Box, { sx: {
238
238
  position: 'absolute',
239
239
  top: 0,
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMonaco } from '@monaco-editor/react';
3
3
  import { Box, useTheme } from '@mui/material';
4
4
  import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
5
- import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
5
+ import { RecordSearchContext } from '@cccsaurora/howler-ui/components/app/providers/RecordSearchProvider';
6
6
  import ThemedEditor from '@cccsaurora/howler-ui/components/elements/ThemedEditor';
7
7
  import { memo, useCallback, useContext, useEffect, useMemo } from 'react';
8
8
  import { useContextSelector } from 'use-context-selector';
@@ -20,8 +20,8 @@ const QueryEditor = ({ query, setQuery, onMount, language = 'lucene', fontSize =
20
20
  const yamlCompletion = useYamlCompletionProvider();
21
21
  const eqlCompletion = useEQLCompletionProvider();
22
22
  const historyCompletion = useHistoryCompletionProvider();
23
- const fzfSearch = useContextSelector(HitSearchContext, ctx => ctx?.fzfSearch ?? false);
24
- const setFzfSearch = useContextSelector(HitSearchContext, ctx => ctx?.setFzfSearch);
23
+ const fzfSearch = useContextSelector(RecordSearchContext, ctx => ctx?.fzfSearch ?? false);
24
+ const setFzfSearch = useContextSelector(RecordSearchContext, ctx => ctx?.setFzfSearch);
25
25
  const beforeEditorMount = useCallback((_monaco) => {
26
26
  _monaco.languages.register({ id: 'lucene' });
27
27
  _monaco.languages.register({ id: 'eql' });
@@ -1,13 +1,13 @@
1
1
  import { useMonaco } from '@monaco-editor/react';
2
- import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
2
+ import { RecordSearchContext } from '@cccsaurora/howler-ui/components/app/providers/RecordSearchProvider';
3
3
  import Fuse from 'fuse.js';
4
4
  import { useMemo } from 'react';
5
5
  import { useContextSelector } from 'use-context-selector';
6
6
  import { twitterShort } from '@cccsaurora/howler-ui/utils/utils';
7
7
  const useHistoryCompletionProvider = () => {
8
8
  const monaco = useMonaco();
9
- const fzfSearch = useContextSelector(HitSearchContext, ctx => ctx?.fzfSearch);
10
- const queryHistory = useContextSelector(HitSearchContext, ctx => ctx?.queryHistory ?? {});
9
+ const fzfSearch = useContextSelector(RecordSearchContext, ctx => ctx?.fzfSearch);
10
+ const queryHistory = useContextSelector(RecordSearchContext, ctx => ctx?.queryHistory ?? {});
11
11
  // Using fuse for fuzzy searching
12
12
  const fuse = useMemo(() => new Fuse(Object.keys(queryHistory), { keys: ['key'], threshold: 0.4 }), [queryHistory]);
13
13
  return {
@@ -51,7 +51,7 @@ const AnalyticDetails = () => {
51
51
  _setFilter(detection);
52
52
  }
53
53
  }, [filter]);
54
- const onOwnerChange = useCallback(async (ownerId) => {
54
+ const onOwnerChange = useCallback(async ([ownerId]) => {
55
55
  const result = await dispatchApi(api.analytic.owner.post(analytic.analytic_id, { username: ownerId }), {
56
56
  throwError: true,
57
57
  showError: true
@@ -108,7 +108,7 @@ const AnalyticDetails = () => {
108
108
  marginTop: '0 !important',
109
109
  marginLeft: `${theme.spacing(-1)} !important`,
110
110
  marginRight: `${theme.spacing(-1)} !important`
111
- }, userId: analytic?.owner, onChange: onOwnerChange, i18nLabel: "route.analytics.set.owner" })) : (_jsx(HowlerAvatar, { userId: analytic?.owner })), _jsx(Stack, { children: users[analytic?.owner] ? (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body1", children: users[analytic?.owner].name }), _jsx(Typography, { component: "a", href: `mailto:${users[analytic?.owner].email}`, variant: "caption", color: "text.secondary", children: users[analytic?.owner].email })] })) : (_jsxs(_Fragment, { children: [_jsx(Skeleton, { variant: "text", width: "70px" }), _jsx(Skeleton, { variant: "text", width: "60px" })] })) })] })] }), filteredContributors.length > 0 && (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('route.analytics.contributors') }), _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, children: filteredContributors.map(_user => (_jsx(HowlerAvatar, { userId: _user }, _user))) })] })), analytic?.rule_crontab && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { spacing: 1, justifyContent: "space-between", children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('rule.interval') }), editingInterval ? (_jsxs(FormControl, { sx: { minWidth: '200px' }, children: [_jsx(InputLabel, { children: t('rule.interval') }), _jsx(Select, { size: "small", label: t('rule.interval'), onChange: event => setCrontab(event.target.value), value: crontab, children: RULE_INTERVALS.map(interval => (_jsx(MenuItem, { value: interval.crontab, children: t(interval.key) }, interval.key))) })] })) : (_jsx("code", { style: {
111
+ }, userIds: [analytic?.owner], onChange: onOwnerChange, i18nLabel: "route.analytics.set.owner" })) : (_jsx(HowlerAvatar, { userId: analytic?.owner })), _jsx(Stack, { children: users[analytic?.owner] ? (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body1", children: users[analytic?.owner].name }), _jsx(Typography, { component: "a", href: `mailto:${users[analytic?.owner].email}`, variant: "caption", color: "text.secondary", children: users[analytic?.owner].email })] })) : (_jsxs(_Fragment, { children: [_jsx(Skeleton, { variant: "text", width: "70px" }), _jsx(Skeleton, { variant: "text", width: "60px" })] })) })] })] }), filteredContributors.length > 0 && (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('route.analytics.contributors') }), _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, children: filteredContributors.map(_user => (_jsx(HowlerAvatar, { userId: _user }, _user))) })] })), analytic?.rule_crontab && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { spacing: 1, justifyContent: "space-between", children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('rule.interval') }), editingInterval ? (_jsxs(FormControl, { sx: { minWidth: '200px' }, children: [_jsx(InputLabel, { children: t('rule.interval') }), _jsx(Select, { size: "small", label: t('rule.interval'), onChange: event => setCrontab(event.target.value), value: crontab, children: RULE_INTERVALS.map(interval => (_jsx(MenuItem, { value: interval.crontab, children: t(interval.key) }, interval.key))) })] })) : (_jsx("code", { style: {
112
112
  backgroundColor: theme.palette.background.paper,
113
113
  padding: theme.spacing(0.5),
114
114
  alignSelf: 'start',
@@ -128,7 +128,7 @@ const AnalyticSearchBase = () => {
128
128
  padding: theme.spacing(0.5),
129
129
  borderRadius: theme.shape.borderRadius,
130
130
  border: `thin solid ${theme.palette.divider}`
131
- }, children: item.item.rule_type })] })), _jsx(FlexOne, {}), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [item.item.owner && _jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: item.item.owner }), filteredContributors.length > 0 && _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsx(AvatarGroup, { children: filteredContributors.map(contributor => (_jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: contributor }, contributor))) })] }), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { size: "small", onClick: e => onFavourite(e, item.item), children: appUser.user?.favourite_analytics?.includes(item.item.analytic_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }), item.item.detections?.length > 0 && (_jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsxs(Grid, { container: true, spacing: 0.5, sx: { marginTop: `${theme.spacing(-0.5)} !important` }, children: [item.item.detections.slice(0, 5).map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { size: "small", variant: "outlined", label: d }) }, d))), item.item.detections.length > 5 && (_jsx(Grid, { item: true, children: _jsx(Tooltip, { title: _jsx(Stack, { children: item.item.detections.slice(5).map(d => (_jsx("span", { children: d }, d))) }), children: _jsx(Chip, { size: "small", variant: "outlined", label: `+ ${item.item.detections.length - 5}` }) }) }))] }) }))] }, item.item.name));
131
+ }, children: item.item.rule_type })] })), _jsx(FlexOne, {}), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [item.item.owner && _jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: item.item.owner }), filteredContributors.length > 0 && _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsx(AvatarGroup, { children: filteredContributors.map(contributor => (_jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: contributor }, contributor))) })] }), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { size: "small", onClick: e => onFavourite(e, item.item), children: appUser.user?.favourite_analytics?.includes(item.item.analytic_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }), item.item.detections?.length > 0 && (_jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsxs(Grid, { container: true, spacing: 0.5, sx: { marginTop: `${theme.spacing(-0.5)} !important` }, children: [item.item.detections.slice(0, 5).map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { variant: "outlined", label: d }) }, d))), item.item.detections.length > 5 && (_jsx(Grid, { item: true, children: _jsx(Tooltip, { title: _jsx(Stack, { children: item.item.detections.slice(5).map(d => (_jsx("span", { children: d }, d))) }), children: _jsx(Chip, { variant: "outlined", label: `+ ${item.item.detections.length - 5}` }) }) }))] }) }))] }, item.item.name));
132
132
  }, [appUser.user?.favourite_analytics, navigate, onFavourite, t, theme]);
133
133
  return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, searchAdornment: _jsx(InputAdornment, { position: "end", children: _jsx(Tooltip, { title: t(`route.analytics.search.filter.rules.${onlyRules < 0 ? 'hide' : onlyRules > 0 ? 'show' : 'toggle'}`), children: _jsx(IconButton, { onClick: () => setOnlyRules((((onlyRules + 2) % 3) - 1)), children: _jsx(SsidChart, { color: onlyRules < 0 ? 'error' : onlyRules > 0 ? 'info' : 'inherit', sx: { transition: theme.transitions.create(['color']) } }) }) }) }), aboveSearch: _jsx(Typography, { sx: { fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }, variant: "body2", children: t('route.analytics.search.prompt') }), renderer: renderer, response: response, searchPrompt: "route.analytics.manager.search" }));
134
134
  };
@@ -0,0 +1,2 @@
1
+ declare const _default: import("react").NamedExoticComponent<{}>;
2
+ export default _default;
@@ -0,0 +1,22 @@
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 (_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);
@@ -0,0 +1,2 @@
1
+ declare const Cases: () => import("react/jsx-runtime").JSX.Element;
2
+ export default Cases;
@@ -0,0 +1,101 @@
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;
@@ -0,0 +1,5 @@
1
+ export declare const ESCALATION_COLOR_MAP: {
2
+ normal: string;
3
+ focus: string;
4
+ crisis: string;
5
+ };
@@ -0,0 +1,5 @@
1
+ export const ESCALATION_COLOR_MAP = {
2
+ normal: 'default',
3
+ focus: 'warning',
4
+ crisis: 'error'
5
+ };
@@ -0,0 +1,6 @@
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;
@@ -0,0 +1,33 @@
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 }), _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;
@@ -0,0 +1,12 @@
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 FC } from 'react';
5
+ import { type AssetEntry } from './assets/Asset';
6
+ /** Deduplicate and merge seenIn lists into a map keyed by `type:value` */
7
+ export declare const buildAssetEntries: (records: Partial<Hit | Observable>[]) => AssetEntry[];
8
+ declare const CaseAssets: FC<{
9
+ case?: Case;
10
+ caseId?: string;
11
+ }>;
12
+ export default CaseAssets;
@@ -0,0 +1,104 @@
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 { 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 CaseAssets;