@cccsaurora/howler-ui 2.18.0-dev.648 → 2.18.0-dev.674

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 (235) 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 +6 -0
  8. package/api/v2/case/index.js +18 -0
  9. package/api/v2/index.d.ts +4 -0
  10. package/api/v2/index.js +6 -0
  11. package/api/v2/search/facet.d.ts +3 -0
  12. package/api/v2/search/facet.js +12 -0
  13. package/api/v2/search/index.d.ts +5 -0
  14. package/api/v2/search/index.js +24 -0
  15. package/commons/components/leftnav/LeftNavDrawer.js +1 -1
  16. package/components/app/App.js +34 -7
  17. package/components/app/hooks/useMatchers.js +2 -2
  18. package/components/app/hooks/useMatchers.test.js +22 -22
  19. package/components/app/hooks/useTitle.js +3 -3
  20. package/components/app/providers/FavouritesProvider.js +2 -2
  21. package/components/app/providers/ParameterProvider.d.ts +9 -2
  22. package/components/app/providers/ParameterProvider.js +165 -240
  23. package/components/app/providers/ParameterProvider.test.js +307 -14
  24. package/components/app/providers/RecordProvider.d.ts +23 -0
  25. package/components/app/providers/{HitProvider.js → RecordProvider.js} +41 -41
  26. package/components/app/providers/{HitSearchProvider.d.ts → RecordSearchProvider.d.ts} +6 -6
  27. package/components/app/providers/{HitSearchProvider.js → RecordSearchProvider.js} +12 -17
  28. package/components/app/providers/{HitSearchProvider.test.js → RecordSearchProvider.test.js} +51 -70
  29. package/components/elements/ContextMenu.d.ts +56 -0
  30. package/components/elements/ContextMenu.js +109 -0
  31. package/components/elements/ContextMenu.test.js +215 -0
  32. package/components/{routes/overviews/OverviewEditor.js → elements/MarkdownEditor.js} +3 -3
  33. package/components/elements/ObjectDetails.d.ts +6 -0
  34. package/components/elements/{hit/HitDetails.js → ObjectDetails.js} +17 -17
  35. package/components/elements/PluginTypography.d.ts +2 -1
  36. package/components/elements/PluginTypography.js +3 -2
  37. package/components/elements/UserList.d.ts +5 -2
  38. package/components/elements/UserList.js +14 -5
  39. package/components/elements/addons/search/phrase/Phrase.js +1 -1
  40. package/components/elements/case/CaseCard.d.ts +8 -0
  41. package/components/elements/case/CaseCard.js +39 -0
  42. package/components/elements/case/CasePreview.d.ts +6 -0
  43. package/components/elements/case/CasePreview.js +17 -0
  44. package/components/elements/case/StatusIcon.d.ts +5 -0
  45. package/components/elements/case/StatusIcon.js +13 -0
  46. package/components/elements/display/ChipPopper.d.ts +1 -1
  47. package/components/elements/display/HowlerCard.js +1 -1
  48. package/components/elements/display/Modal.js +1 -0
  49. package/components/elements/hit/HitActions.js +4 -4
  50. package/components/elements/hit/HitBanner.js +28 -48
  51. package/components/elements/hit/HitCard.js +5 -5
  52. package/components/elements/hit/HitLabels.js +2 -2
  53. package/components/elements/hit/{HitQuickSearch.d.ts → HitPreview.d.ts} +3 -3
  54. package/components/elements/hit/{HitQuickSearch.js → HitPreview.js} +10 -4
  55. package/components/elements/hit/HitSummary.d.ts +2 -1
  56. package/components/elements/hit/HitSummary.js +6 -5
  57. package/components/elements/hit/aggregate/HitGraph.js +8 -8
  58. package/components/elements/hit/elements/AnalyticLink.d.ts +8 -0
  59. package/components/elements/hit/elements/AnalyticLink.js +22 -0
  60. package/components/elements/hit/outlines/DefaultOutline.js +1 -1
  61. package/components/elements/hit/related/RelatedRecords.js +63 -0
  62. package/components/elements/observable/ObservableCard.d.ts +6 -0
  63. package/components/elements/observable/ObservableCard.js +23 -0
  64. package/components/elements/observable/ObservablePreview.d.ts +6 -0
  65. package/components/elements/observable/ObservablePreview.js +12 -0
  66. package/components/elements/{hit/HitComments.d.ts → record/RecordComments.d.ts} +5 -4
  67. package/components/elements/{hit/HitComments.js → record/RecordComments.js} +29 -28
  68. package/components/{routes/hits/search/HitContextMenu.d.ts → elements/record/RecordContextMenu.d.ts} +3 -3
  69. package/components/elements/record/RecordContextMenu.js +235 -0
  70. package/components/elements/record/RecordContextMenu.test.d.ts +1 -0
  71. package/components/{routes/hits/search/HitContextMenu.test.js → elements/record/RecordContextMenu.test.js} +39 -39
  72. package/components/elements/record/RecordRelated.d.ts +7 -0
  73. package/components/elements/record/RecordRelated.js +34 -0
  74. package/components/elements/{hit/HitWorklog.d.ts → record/RecordWorklog.d.ts} +4 -3
  75. package/components/elements/{hit/HitWorklog.js → record/RecordWorklog.js} +15 -13
  76. package/components/elements/view/ViewTitle.js +1 -1
  77. package/components/hooks/useHitActions.d.ts +1 -1
  78. package/components/hooks/useHitActions.js +4 -4
  79. package/components/hooks/useLocalStorage.test.d.ts +1 -0
  80. package/components/hooks/useLocalStorage.test.js +137 -0
  81. package/components/hooks/useMyPreferences.js +10 -1
  82. package/components/hooks/useMySearch.js +2 -2
  83. package/components/hooks/useMySitemap.js +4 -1
  84. package/components/hooks/useMyTheme.js +9 -2
  85. package/components/hooks/useParamState.d.ts +6 -0
  86. package/components/hooks/useParamState.js +48 -0
  87. package/components/hooks/useParamState.test.d.ts +1 -0
  88. package/components/hooks/useParamState.test.js +166 -0
  89. package/components/hooks/{useHitSelection.d.ts → useRecordSelection.d.ts} +2 -2
  90. package/components/hooks/{useHitSelection.js → useRecordSelection.js} +12 -33
  91. package/components/hooks/useRelatedRecords.d.ts +13 -0
  92. package/components/hooks/useRelatedRecords.js +32 -0
  93. package/components/routes/action/edit/ActionEditor.js +2 -2
  94. package/components/routes/action/view/ActionSearch.js +1 -1
  95. package/components/routes/advanced/QueryBuilder.js +1 -1
  96. package/components/routes/advanced/QueryEditor.js +3 -3
  97. package/components/routes/advanced/historyCompletionProvider.js +3 -3
  98. package/components/routes/analytics/AnalyticDetails.js +2 -2
  99. package/components/routes/analytics/AnalyticSearch.js +1 -1
  100. package/components/routes/cases/CaseViewer.d.ts +2 -0
  101. package/components/routes/cases/CaseViewer.js +22 -0
  102. package/components/routes/cases/Cases.d.ts +2 -0
  103. package/components/routes/cases/Cases.js +101 -0
  104. package/components/routes/cases/constants.d.ts +5 -0
  105. package/components/routes/cases/constants.js +5 -0
  106. package/components/routes/cases/detail/AlertPanel.d.ts +6 -0
  107. package/components/routes/cases/detail/AlertPanel.js +33 -0
  108. package/components/routes/cases/detail/CaseAssets.d.ts +12 -0
  109. package/components/routes/cases/detail/CaseAssets.js +101 -0
  110. package/components/routes/cases/detail/CaseAssets.test.d.ts +1 -0
  111. package/components/routes/cases/detail/CaseAssets.test.js +163 -0
  112. package/components/routes/cases/detail/CaseDashboard.d.ts +7 -0
  113. package/components/routes/cases/detail/CaseDashboard.js +51 -0
  114. package/components/routes/cases/detail/CaseDetails.d.ts +6 -0
  115. package/components/routes/cases/detail/CaseDetails.js +61 -0
  116. package/components/routes/cases/detail/CaseOverview.d.ts +7 -0
  117. package/components/routes/cases/detail/CaseOverview.js +43 -0
  118. package/components/routes/cases/detail/CaseSidebar.d.ts +6 -0
  119. package/components/routes/cases/detail/CaseSidebar.js +61 -0
  120. package/components/routes/cases/detail/CaseTask.d.ts +11 -0
  121. package/components/routes/cases/detail/CaseTask.js +57 -0
  122. package/components/routes/cases/detail/ItemPage.d.ts +6 -0
  123. package/components/routes/cases/detail/ItemPage.js +99 -0
  124. package/components/routes/cases/detail/RelatedCasePanel.d.ts +6 -0
  125. package/components/routes/cases/detail/RelatedCasePanel.js +31 -0
  126. package/components/routes/cases/detail/TaskPanel.d.ts +7 -0
  127. package/components/routes/cases/detail/TaskPanel.js +52 -0
  128. package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +12 -0
  129. package/components/routes/cases/detail/aggregates/CaseAggregate.js +19 -0
  130. package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +6 -0
  131. package/components/routes/cases/detail/aggregates/SourceAggregate.js +27 -0
  132. package/components/routes/cases/detail/assets/Asset.d.ts +14 -0
  133. package/components/routes/cases/detail/assets/Asset.js +12 -0
  134. package/components/routes/cases/detail/assets/Asset.test.d.ts +1 -0
  135. package/components/routes/cases/detail/assets/Asset.test.js +72 -0
  136. package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +13 -0
  137. package/components/routes/cases/detail/sidebar/CaseFolder.js +131 -0
  138. package/components/routes/cases/detail/sidebar/types.d.ts +3 -0
  139. package/components/routes/cases/detail/sidebar/utils.d.ts +3 -0
  140. package/components/routes/cases/detail/sidebar/utils.js +25 -0
  141. package/components/routes/cases/hooks/useCase.d.ts +13 -0
  142. package/components/routes/cases/hooks/useCase.js +38 -0
  143. package/components/routes/cases/modals/ResolveModal.d.ts +7 -0
  144. package/components/routes/cases/modals/ResolveModal.js +59 -0
  145. package/components/routes/dossiers/DossierEditor.js +2 -2
  146. package/components/routes/dossiers/DossierEditor.test.js +1 -1
  147. package/components/routes/help/ApiDocumentation.js +1 -1
  148. package/components/routes/help/HitBannerDocumentation.js +1 -0
  149. package/components/routes/help/HitDocumentation.js +1 -3
  150. package/components/routes/hits/search/InformationPane.d.ts +1 -0
  151. package/components/routes/hits/search/InformationPane.js +47 -60
  152. package/components/routes/hits/search/LayoutSettings.js +3 -3
  153. package/components/routes/hits/search/QuerySettings.js +2 -1
  154. package/components/routes/hits/search/QuerySettings.test.js +14 -9
  155. package/components/routes/hits/search/{HitBrowser.js → RecordBrowser.js} +9 -9
  156. package/components/routes/hits/search/{HitQuery.d.ts → RecordQuery.d.ts} +2 -2
  157. package/components/routes/hits/search/{HitQuery.js → RecordQuery.js} +6 -6
  158. package/components/routes/hits/search/SearchPane.js +26 -49
  159. package/components/routes/hits/search/ViewLink.js +3 -3
  160. package/components/routes/hits/search/ViewLink.test.js +8 -8
  161. package/components/routes/hits/search/grid/AddColumnModal.js +5 -4
  162. package/components/routes/hits/search/grid/EnhancedCell.d.ts +2 -1
  163. package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
  164. package/components/routes/hits/search/grid/HitGrid.js +20 -18
  165. package/components/routes/hits/search/grid/{HitRow.d.ts → RecordRow.d.ts} +3 -2
  166. package/components/routes/hits/search/grid/{HitRow.js → RecordRow.js} +10 -8
  167. package/components/routes/hits/search/shared/IndexPicker.d.ts +2 -0
  168. package/components/routes/hits/search/shared/IndexPicker.js +20 -0
  169. package/components/routes/hits/view/HitViewer.js +12 -13
  170. package/components/routes/home/ViewCard.js +4 -4
  171. package/components/routes/observables/ObservableViewer.d.ts +7 -0
  172. package/components/routes/observables/ObservableViewer.js +27 -0
  173. package/components/routes/overviews/OverviewViewer.js +2 -2
  174. package/components/routes/views/ViewComposer.js +4 -4
  175. package/locales/en/translation.json +65 -3
  176. package/locales/fr/translation.json +63 -3
  177. package/models/WithMetadata.d.ts +2 -1
  178. package/models/entities/generated/AttachmentsFile.d.ts +12 -0
  179. package/models/entities/generated/Case.d.ts +28 -0
  180. package/models/entities/generated/DestinationOriginal.d.ts +19 -0
  181. package/models/entities/generated/EmailAttachment.d.ts +8 -0
  182. package/models/entities/generated/EmailParent.d.ts +19 -0
  183. package/models/entities/generated/Enrichments.d.ts +7 -0
  184. package/models/entities/generated/EnrichmentsIndicator.d.ts +21 -0
  185. package/models/entities/generated/Hit.d.ts +1 -0
  186. package/models/entities/generated/Howler.d.ts +0 -4
  187. package/models/entities/generated/HttpResponse.d.ts +11 -0
  188. package/models/entities/generated/Item.d.ts +9 -0
  189. package/models/entities/generated/Observable.d.ts +85 -0
  190. package/models/entities/generated/ObservableCloud.d.ts +20 -0
  191. package/models/entities/generated/ObservableDestination.d.ts +23 -0
  192. package/models/entities/generated/ObservableEmail.d.ts +30 -0
  193. package/models/entities/generated/ObservableFile.d.ts +36 -0
  194. package/models/entities/generated/ObservableHowler.d.ts +43 -0
  195. package/models/entities/generated/ObservableHttp.d.ts +11 -0
  196. package/models/entities/generated/ObservableObserver.d.ts +21 -0
  197. package/models/entities/generated/ObservableOrganization.d.ts +7 -0
  198. package/models/entities/generated/ObservableProcess.d.ts +34 -0
  199. package/models/entities/generated/ObservableSource.d.ts +23 -0
  200. package/models/entities/generated/ObservableThreat.d.ts +21 -0
  201. package/models/entities/generated/ObservableTls.d.ts +12 -0
  202. package/models/entities/generated/ObserverIngress.d.ts +9 -0
  203. package/models/entities/generated/Rule.d.ts +2 -10
  204. package/models/entities/generated/Task.d.ts +10 -0
  205. package/models/entities/generated/Threat.d.ts +2 -2
  206. package/models/entities/generated/{Enrichment.d.ts → ThreatEnrichment.d.ts} +1 -1
  207. package/package.json +18 -1
  208. package/plugins/clue/components/ClueTypography.js +2 -2
  209. package/plugins/clue/utils.d.ts +2 -1
  210. package/tests/utils.d.ts +2 -0
  211. package/tests/utils.js +8 -0
  212. package/utils/constants.d.ts +3 -3
  213. package/utils/hitFunctions.d.ts +2 -1
  214. package/utils/hitFunctions.js +4 -4
  215. package/utils/typeUtils.d.ts +7 -0
  216. package/utils/typeUtils.js +27 -0
  217. package/components/app/providers/HitProvider.d.ts +0 -22
  218. package/components/elements/display/icons/BundleButton.d.ts +0 -6
  219. package/components/elements/display/icons/BundleButton.js +0 -32
  220. package/components/elements/hit/HitRelated.d.ts +0 -6
  221. package/components/elements/hit/HitRelated.js +0 -7
  222. package/components/routes/help/BundleDocumentation.d.ts +0 -3
  223. package/components/routes/help/BundleDocumentation.js +0 -12
  224. package/components/routes/help/markdown/en/bundles.md.js +0 -1
  225. package/components/routes/help/markdown/fr/bundles.md.js +0 -1
  226. package/components/routes/hits/search/BundleParentMenu.d.ts +0 -6
  227. package/components/routes/hits/search/BundleParentMenu.js +0 -32
  228. package/components/routes/hits/search/BundleScroller.d.ts +0 -2
  229. package/components/routes/hits/search/BundleScroller.js +0 -6
  230. package/components/routes/hits/search/HitContextMenu.js +0 -227
  231. /package/components/app/providers/{HitSearchProvider.test.d.ts → RecordSearchProvider.test.d.ts} +0 -0
  232. /package/components/{routes/hits/search/HitContextMenu.test.d.ts → elements/ContextMenu.test.d.ts} +0 -0
  233. /package/components/{routes/overviews/OverviewEditor.d.ts → elements/MarkdownEditor.d.ts} +0 -0
  234. /package/components/elements/hit/{HitDetails.d.ts → related/RelatedRecords.d.ts} +0 -0
  235. /package/components/routes/hits/search/{HitBrowser.d.ts → RecordBrowser.d.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Alert, Box, Typography } from '@mui/material';
3
3
  import api from '@cccsaurora/howler-ui/api';
4
- import HitQuickSearch from '@cccsaurora/howler-ui/components/elements/hit/HitQuickSearch';
4
+ import HitPreview from '@cccsaurora/howler-ui/components/elements/hit/HitPreview';
5
5
  import { useMemo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Link, useNavigate } from 'react-router-dom';
@@ -40,7 +40,7 @@ const useMySearch = () => {
40
40
  },
41
41
  headerRenderer: (state) => (state.result?.error || !state.items) && (_jsx(Box, { sx: { p: 1, pb: 0, textAlign: 'center' }, children: state.result?.error ? (_jsx(Alert, { severity: "error", color: "error", children: t('hit.search.invalid') })) : ((!state.items || state.items.length === 0) && (_jsx(Typography, { sx: { mb: -1, color: 'text.secondary' }, children: t('hit.quicksearch') }))) })),
42
42
  itemRenderer: (item, options) => {
43
- return (_jsx(Link, { to: `/hits/${item.id}`, style: { flex: 1, textDecoration: 'none', color: 'inherit', overflow: 'hidden' }, children: _jsx(HitQuickSearch, { hit: item.item, options: options }) }));
43
+ return (_jsx(Link, { to: `/hits/${item.id}`, style: { flex: 1, textDecoration: 'none', color: 'inherit', overflow: 'hidden' }, children: _jsx(HitPreview, { hit: item.item, options: options }) }));
44
44
  }
45
45
  }), [navigate, pageCount, t]);
46
46
  };
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Article, Book, Code, CreateNewFolder, Dashboard, Description, Edit, EditNote, FormatListBulleted, Help, Info, Key, Person, PersonSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, Terminal, Topic, Work } from '@mui/icons-material';
2
+ import { Article, Book, BookRounded, Code, CreateNewFolder, Dashboard, Description, Edit, EditNote, FormatListBulleted, Help, Info, Key, Person, PersonSearch, QueryStats, SavedSearch, Search, Settings, SettingsSuggest, Shield, Storage, Terminal, Topic, Work } from '@mui/icons-material';
3
3
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
4
4
  import { useMemo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
@@ -24,6 +24,9 @@ const useMySitemap = () => {
24
24
  return useMemo(() => ({
25
25
  routes: [
26
26
  { path: '/', title: t('route.home'), isRoot: true, icon: _jsx(Dashboard, {}) },
27
+ { path: '/cases', title: t('route.cases'), isRoot: true, icon: _jsx(BookRounded, {}) },
28
+ { path: '/cases/:id', title: t('route.cases.view'), breadcrumbs: ['/cases'] },
29
+ { path: '/cases/:id/*', title: t('route.cases.view'), breadcrumbs: ['/cases'] },
27
30
  { path: '/admin/users', title: t('route.admin.user.search'), isRoot: true, icon: _jsx(PersonSearch, {}) },
28
31
  {
29
32
  path: '/admin/users/:id',
@@ -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'
@@ -0,0 +1,6 @@
1
+ type Primitive = string | number | boolean | null;
2
+ declare const useParamState: {
3
+ <T extends Primitive>(key: string, defaultValue?: T, list?: false): [T, (value: T) => void];
4
+ <T extends Exclude<Primitive, null>>(key: string, defaultValue: T, list: true): [T[], (value: T[]) => void];
5
+ };
6
+ export default useParamState;
@@ -0,0 +1,48 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+ const parseValue = (raw, defaultValue) => {
4
+ if (raw === null)
5
+ return defaultValue;
6
+ if (typeof defaultValue === 'boolean')
7
+ return (raw === 'true');
8
+ if (typeof defaultValue === 'number') {
9
+ const n = Number(raw);
10
+ return (isNaN(n) ? defaultValue : n);
11
+ }
12
+ return raw;
13
+ };
14
+ const serializeValue = (value) => {
15
+ if (value === null || value === undefined)
16
+ return '';
17
+ return String(value);
18
+ };
19
+ // Scalar mode
20
+ const useParamState = (key, defaultValue = null, list = false) => {
21
+ const [searchParams, setSearchParams] = useSearchParams();
22
+ const [value, setValue] = useState(() => {
23
+ if (list) {
24
+ const raws = searchParams.getAll(key);
25
+ return raws.map(r => parseValue(r, defaultValue));
26
+ }
27
+ return parseValue(searchParams.get(key), defaultValue);
28
+ });
29
+ const setter = useCallback((newValue) => {
30
+ setValue(newValue);
31
+ setSearchParams(prev => {
32
+ const next = new URLSearchParams(prev);
33
+ next.delete(key);
34
+ if (list) {
35
+ newValue.forEach(item => next.append(key, serializeValue(item)));
36
+ }
37
+ else {
38
+ const scalar = newValue;
39
+ if (scalar !== defaultValue && scalar !== null && scalar !== undefined) {
40
+ next.set(key, serializeValue(scalar));
41
+ }
42
+ }
43
+ return next;
44
+ }, { replace: true });
45
+ }, [key, defaultValue, list, setSearchParams]);
46
+ return [value, setter];
47
+ };
48
+ export default useParamState;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,166 @@
1
+ /// <reference types="vitest" />
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { createElement } from 'react';
4
+ import { MemoryRouter, useSearchParams } from 'react-router-dom';
5
+ import { describe, expect, it } from 'vitest';
6
+ import useParamState from './useParamState';
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
+ ({ children }) => createElement(MemoryRouter, { initialEntries: [search ? `/?${search}` : '/'] }, children);
11
+ // Composite hook: exposes the param state AND the live URL params for URL-level assertions
12
+ const useParamStateWithUrl = (key, defaultValue) => {
13
+ const [value, setValue] = useParamState(key, defaultValue);
14
+ const [params] = useSearchParams();
15
+ return { value, setValue, params };
16
+ };
17
+ describe('useParamState', () => {
18
+ describe('scalar mode – initialization', () => {
19
+ it('returns the default value when the param is absent from the URL', () => {
20
+ const { result } = renderHook(() => useParamState('tab', 'dashboard'), { wrapper: makeWrapper() });
21
+ expect(result.current[0]).toBe('dashboard');
22
+ });
23
+ it('returns null when no default is provided and the param is absent', () => {
24
+ const { result } = renderHook(() => useParamState('tab'), { wrapper: makeWrapper() });
25
+ expect(result.current[0]).toBeNull();
26
+ });
27
+ it('reads a string value present in the URL', () => {
28
+ const { result } = renderHook(() => useParamState('tab', 'dashboard'), {
29
+ wrapper: makeWrapper('tab=settings')
30
+ });
31
+ expect(result.current[0]).toBe('settings');
32
+ });
33
+ it('coerces a URL string to a number when defaultValue is a number', () => {
34
+ const { result } = renderHook(() => useParamState('page', 0), { wrapper: makeWrapper('page=3') });
35
+ expect(result.current[0]).toBe(3);
36
+ });
37
+ it('returns the default number when the URL value is non-numeric', () => {
38
+ const { result } = renderHook(() => useParamState('page', 0), {
39
+ wrapper: makeWrapper('page=notanumber')
40
+ });
41
+ expect(result.current[0]).toBe(0);
42
+ });
43
+ it("coerces 'true' string to boolean true when defaultValue is boolean", () => {
44
+ const { result } = renderHook(() => useParamState('active', false), {
45
+ wrapper: makeWrapper('active=true')
46
+ });
47
+ expect(result.current[0]).toBe(true);
48
+ });
49
+ it("coerces 'false' string to boolean false when defaultValue is boolean", () => {
50
+ const { result } = renderHook(() => useParamState('active', true), {
51
+ wrapper: makeWrapper('active=false')
52
+ });
53
+ expect(result.current[0]).toBe(false);
54
+ });
55
+ });
56
+ describe('scalar mode – setter', () => {
57
+ it('updates the in-state value', () => {
58
+ const { result } = renderHook(() => useParamState('tab', 'dashboard'), { wrapper: makeWrapper() });
59
+ act(() => result.current[1]('settings'));
60
+ expect(result.current[0]).toBe('settings');
61
+ });
62
+ it('writes the new value to the URL', () => {
63
+ const { result } = renderHook(() => useParamStateWithUrl('tab', 'dashboard'), { wrapper: makeWrapper() });
64
+ act(() => result.current.setValue('settings'));
65
+ expect(result.current.params.get('tab')).toBe('settings');
66
+ });
67
+ it('removes the param from the URL when set to the default value', () => {
68
+ const { result } = renderHook(() => useParamStateWithUrl('tab', 'dashboard'), {
69
+ wrapper: makeWrapper('tab=settings')
70
+ });
71
+ act(() => result.current.setValue('dashboard'));
72
+ expect(result.current.params.has('tab')).toBe(false);
73
+ });
74
+ it('removes the param from the URL when set to null', () => {
75
+ const { result } = renderHook(() => useParamStateWithUrl('tab', null), {
76
+ wrapper: makeWrapper('tab=settings')
77
+ });
78
+ act(() => result.current.setValue(null));
79
+ expect(result.current.params.has('tab')).toBe(false);
80
+ });
81
+ it('serializes a number to the URL', () => {
82
+ const { result } = renderHook(() => useParamStateWithUrl('page', 0), { wrapper: makeWrapper() });
83
+ act(() => result.current.setValue(5));
84
+ expect(result.current.params.get('page')).toBe('5');
85
+ });
86
+ it('removes the param when a number is set back to its default', () => {
87
+ const { result } = renderHook(() => useParamStateWithUrl('page', 0), { wrapper: makeWrapper('page=3') });
88
+ act(() => result.current.setValue(0));
89
+ expect(result.current.params.has('page')).toBe(false);
90
+ });
91
+ it('serializes boolean true to the URL', () => {
92
+ const { result } = renderHook(() => useParamStateWithUrl('active', false), { wrapper: makeWrapper() });
93
+ act(() => result.current.setValue(true));
94
+ expect(result.current.params.get('active')).toBe('true');
95
+ });
96
+ it('removes boolean param when set back to its default', () => {
97
+ const { result } = renderHook(() => useParamStateWithUrl('active', false), {
98
+ wrapper: makeWrapper('active=true')
99
+ });
100
+ act(() => result.current.setValue(false));
101
+ expect(result.current.params.has('active')).toBe(false);
102
+ });
103
+ it('does not clobber unrelated URL params when writing', () => {
104
+ const { result } = renderHook(() => useParamStateWithUrl('tab', 'dashboard'), {
105
+ wrapper: makeWrapper('sort=asc')
106
+ });
107
+ act(() => result.current.setValue('settings'));
108
+ expect(result.current.params.get('sort')).toBe('asc');
109
+ });
110
+ });
111
+ describe('list mode – initialization', () => {
112
+ it('returns an empty array when no list params are present', () => {
113
+ const { result } = renderHook(() => useParamState('tag', 'x', true), { wrapper: makeWrapper() });
114
+ expect(result.current[0]).toEqual([]);
115
+ });
116
+ it('reads multiple values from the URL', () => {
117
+ const { result } = renderHook(() => useParamState('tag', 'x', true), {
118
+ wrapper: makeWrapper('tag=a&tag=b&tag=c')
119
+ });
120
+ expect(result.current[0]).toEqual(['a', 'b', 'c']);
121
+ });
122
+ });
123
+ describe('list mode – setter', () => {
124
+ it('updates the in-state array value', () => {
125
+ const { result } = renderHook(() => useParamState('tag', '', true), { wrapper: makeWrapper() });
126
+ act(() => result.current[1](['alpha', 'beta']));
127
+ expect(result.current[0]).toEqual(['alpha', 'beta']);
128
+ });
129
+ it('writes multiple values as repeated URL params', () => {
130
+ const { result } = renderHook(() => {
131
+ const [value, setValue] = useParamState('tag', '', true);
132
+ const [params] = useSearchParams();
133
+ return { value, setValue, params };
134
+ }, { wrapper: makeWrapper() });
135
+ act(() => result.current.setValue(['x', 'y', 'z']));
136
+ expect(result.current.params.getAll('tag')).toEqual(['x', 'y', 'z']);
137
+ });
138
+ it('clears all repeated params when set to an empty array', () => {
139
+ const { result } = renderHook(() => {
140
+ const [value, setValue] = useParamState('tag', '', true);
141
+ const [params] = useSearchParams();
142
+ return { value, setValue, params };
143
+ }, { wrapper: makeWrapper('tag=a&tag=b') });
144
+ act(() => result.current.setValue([]));
145
+ expect(result.current.params.getAll('tag')).toEqual([]);
146
+ });
147
+ it('replaces existing params entirely on update', () => {
148
+ const { result } = renderHook(() => {
149
+ const [value, setValue] = useParamState('tag', '', true);
150
+ const [params] = useSearchParams();
151
+ return { value, setValue, params };
152
+ }, { wrapper: makeWrapper('tag=old1&tag=old2') });
153
+ act(() => result.current.setValue(['new1']));
154
+ expect(result.current.params.getAll('tag')).toEqual(['new1']);
155
+ });
156
+ it('does not clobber unrelated URL params when writing list values', () => {
157
+ const { result } = renderHook(() => {
158
+ const [value, setValue] = useParamState('tag', '', true);
159
+ const [params] = useSearchParams();
160
+ return { value, setValue, params };
161
+ }, { wrapper: makeWrapper('sort=asc') });
162
+ act(() => result.current.setValue(['x']));
163
+ expect(result.current.params.get('sort')).toBe('asc');
164
+ });
165
+ });
166
+ });
@@ -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 } = 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 }), _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;